Krolyxon
Tag v0.3.0

ferreniumsrcgitrefs.rs

167 lines · 5.0 KB · Rust

1//! Resolving revisions from URLs and listing refs.
2
3use gix::bstr::{BStr, ByteSlice};
4
5use super::{CommitInfo, ObjectId, commit_info};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum RefKind {
9 Branch,
10 Tag,
11 /// A (possibly abbreviated) commit id written in the URL.
12 Commit,
13}
14
15/// A revision named in a URL, resolved to a commit.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Rev {
18 /// The name as written in the URL (`main`, `feature/x`, `v1.0`, an oid).
19 pub name: String,
20 pub kind: RefKind,
21 pub commit: ObjectId,
22}
23
24impl Rev {
25 /// URLs naming a full commit id always show the same content.
26 pub fn is_immutable(&self) -> bool {
27 self.kind == RefKind::Commit && self.name.len() == self.commit.kind().len_in_hex()
28 }
29}
30
31/// Resolve `name` to a commit: a branch, then a tag, then a hex commit id
32/// (at least 4 digits).
33pub fn resolve(repo: &gix::Repository, name: &str) -> Option<Rev> {
34 if name.is_empty() || name.starts_with('-') {
35 return None;
36 }
37 for (prefix, kind) in [
38 ("refs/heads/", RefKind::Branch),
39 ("refs/tags/", RefKind::Tag),
40 ] {
41 if let Ok(mut reference) = repo.find_reference(&format!("{prefix}{name}"))
42 && let Ok(commit) = reference.peel_to_commit()
43 {
44 return Some(Rev {
45 name: name.to_string(),
46 kind,
47 commit: commit.id,
48 });
49 }
50 }
51 if (4..=40).contains(&name.len()) && name.bytes().all(|b| b.is_ascii_hexdigit()) {
52 let id = repo.rev_parse_single(BStr::new(name)).ok()?;
53 let commit = id.object().ok()?.peel_to_commit().ok()?;
54 return Some(Rev {
55 name: name.to_string(),
56 kind: RefKind::Commit,
57 commit: commit.id,
58 });
59 }
60 None
61}
62
63/// Resolve the longest prefix of `segments` that names a revision; the
64/// remaining segments are the path. Ref names may contain `/`.
65pub fn resolve_prefix<'a>(
66 repo: &gix::Repository,
67 segments: &'a [Vec<u8>],
68) -> Option<(Rev, &'a [Vec<u8>])> {
69 (1..=segments.len()).rev().find_map(|n| {
70 let parts: Option<Vec<&str>> = segments[..n]
71 .iter()
72 .map(|s| std::str::from_utf8(s).ok())
73 .collect();
74 let rev = resolve(repo, &parts?.join("/"))?;
75 Some((rev, &segments[n..]))
76 })
77}
78
79#[derive(Debug, Clone)]
80pub struct RefEntry {
81 /// Short name (`main`, `v1.0`).
82 pub name: String,
83 pub commit: ObjectId,
84 pub info: CommitInfo,
85 /// Tag message subject, for annotated tags.
86 pub tag_subject: Option<String>,
87}
88
89#[derive(Debug, Clone, Default)]
90pub struct RefList {
91 /// Newest commit first.
92 pub branches: Vec<RefEntry>,
93 /// Newest first.
94 pub tags: Vec<RefEntry>,
95}
96
97impl RefList {
98 pub fn weight(&self) -> usize {
99 let entry = |e: &RefEntry| {
100 64 + e.name.len() + e.info.weight() + e.tag_subject.as_ref().map_or(0, String::len)
101 };
102 self.branches.iter().chain(&self.tags).map(entry).sum()
103 }
104}
105
106/// Every branch and tag that points (possibly through tags) at a commit.
107pub fn list(repo: &gix::Repository) -> anyhow::Result<RefList> {
108 let mut list = RefList::default();
109 let platform = repo.references()?;
110 for reference in platform.all()? {
111 let Ok(mut reference) = reference else {
112 continue;
113 };
114 let full = reference.name().as_bstr().to_owned();
115 let (name, is_tag) = if let Some(n) = full.strip_prefix(b"refs/heads/") {
116 (n, false)
117 } else if let Some(n) = full.strip_prefix(b"refs/tags/") {
118 (n, true)
119 } else {
120 continue;
121 };
122 let Ok(name) = name.to_str() else {
123 continue;
124 };
125 let tag_subject = is_tag
126 .then(|| {
127 let id = reference.target().try_id()?.to_owned();
128 let tag = repo.find_object(id).ok()?.try_into_tag().ok()?;
129 let tag = tag.decode().ok()?;
130 let subject = tag
131 .message
132 .lines()
133 .next()?
134 .to_str_lossy()
135 .trim()
136 .to_string();
137 (!subject.is_empty()).then_some(subject)
138 })
139 .flatten();
140 let Ok(commit) = reference.peel_to_commit() else {
141 continue;
142 };
143 let Ok(info) = commit_info(repo, commit.id) else {
144 continue;
145 };
146 let entry = RefEntry {
147 name: name.to_string(),
148 commit: commit.id,
149 info,
150 tag_subject,
151 };
152 if is_tag {
153 list.tags.push(entry);
154 } else {
155 list.branches.push(entry);
156 }
157 }
158 let newest_first = |a: &RefEntry, b: &RefEntry| {
159 b.info
160 .committer_time
161 .cmp(&a.info.committer_time)
162 .then_with(|| a.name.cmp(&b.name))
163 };
164 list.branches.sort_by(newest_first);
165 list.tags.sort_by(newest_first);
166 Ok(list)
167}