1use std::{
2 borrow::Cow,
3 fmt::{Display, Formatter},
4};
5
6use bstr::BStr;
7use gix_hashtable::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct Outcome<'name> {
12 pub name: Option<Cow<'name, BStr>>,
16 pub id: gix_hash::ObjectId,
18 pub depth: u32,
21 pub name_by_oid: HashMap<gix_hash::ObjectId, Cow<'name, BStr>>,
23 pub commits_seen: u32,
25}
26
27impl<'a> Outcome<'a> {
28 pub fn into_format(self, hex_len: usize) -> Format<'a> {
30 Format {
31 name: self.name,
32 id: self.id,
33 hex_len,
34 depth: self.depth,
35 long: false,
36 dirty_suffix: None,
37 }
38 }
39}
40
41#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
43pub struct Format<'a> {
44 pub name: Option<Cow<'a, BStr>>,
48 pub id: gix_hash::ObjectId,
50 pub hex_len: usize,
52 pub depth: u32,
54 pub long: bool,
57 pub dirty_suffix: Option<String>,
60}
61
62impl Format<'_> {
63 pub fn is_exact_match(&self) -> bool {
65 self.depth == 0
66 }
67
68 pub fn long(&mut self, long: bool) -> &mut Self {
73 self.long = long;
74 self
75 }
76}
77
78impl Display for Format<'_> {
79 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80 if let Some(name) = self.name.as_deref() {
81 if !self.long && self.is_exact_match() {
82 name.fmt(f)?;
83 } else {
84 write!(f, "{}-{}-g{}", name, self.depth, self.id.to_hex_with_len(self.hex_len))?;
85 }
86 } else {
87 self.id.to_hex_with_len(self.hex_len).fmt(f)?;
88 }
89
90 if let Some(suffix) = &self.dirty_suffix {
91 write!(f, "-{suffix}")?;
92 }
93 Ok(())
94 }
95}
96
97pub type Flags = u32;
99const MAX_CANDIDATES: usize = std::mem::size_of::<Flags>() * 8;
100
101#[derive(Clone, Debug)]
103pub struct Options<'name> {
104 pub name_by_oid: HashMap<gix_hash::ObjectId, Cow<'name, BStr>>,
107 pub max_candidates: usize,
111 pub fallback_to_oid: bool,
113 pub first_parent: bool,
117}
118
119impl Default for Options<'_> {
120 fn default() -> Self {
121 Options {
122 max_candidates: 10, name_by_oid: Default::default(),
124 fallback_to_oid: false,
125 first_parent: false,
126 }
127 }
128}
129
130#[derive(Debug, thiserror::Error)]
132#[allow(missing_docs)]
133pub enum Error {
134 #[error("The parents of commit {} could not be added to graph during traversal", oid.to_hex())]
135 InsertParentsToGraph {
136 #[source]
137 err: crate::graph::insert_parents::Error,
138 oid: gix_hash::ObjectId,
139 },
140 #[error("A commit could not be decoded during traversal")]
141 Decode(#[from] gix_object::decode::Error),
142}
143
144pub(crate) mod function {
145 use std::{borrow::Cow, cmp::Ordering};
146
147 use bstr::BStr;
148 use gix_hash::oid;
149
150 use super::{Error, Outcome};
151 use crate::{
152 describe::{CommitTime, Flags, Options, MAX_CANDIDATES},
153 Graph, PriorityQueue,
154 };
155
156 pub fn describe<'name>(
162 commit: &oid,
163 graph: &mut Graph<'_, '_, Flags>,
164 Options {
165 name_by_oid,
166 mut max_candidates,
167 fallback_to_oid,
168 first_parent,
169 }: Options<'name>,
170 ) -> Result<Option<Outcome<'name>>, Error> {
171 let _span = gix_trace::coarse!(
172 "gix_revision::describe()",
173 commit = %commit,
174 name_count = name_by_oid.len(),
175 max_candidates,
176 first_parent
177 );
178 max_candidates = max_candidates.min(MAX_CANDIDATES);
179 if let Some(name) = name_by_oid.get(commit) {
180 return Ok(Some(Outcome {
181 name: name.clone().into(),
182 id: commit.to_owned(),
183 depth: 0,
184 name_by_oid,
185 commits_seen: 0,
186 }));
187 }
188
189 if max_candidates == 0 || name_by_oid.is_empty() {
190 return if fallback_to_oid {
191 Ok(Some(Outcome {
192 id: commit.to_owned(),
193 name: None,
194 name_by_oid,
195 depth: 0,
196 commits_seen: 0,
197 }))
198 } else {
199 Ok(None)
200 };
201 }
202
203 let mut queue = PriorityQueue::from_iter(Some((u32::MAX, commit.to_owned())));
204 let mut candidates = Vec::new();
205 let mut commits_seen = 0;
206 let mut gave_up_on_commit = None;
207 graph.clear();
208 graph.insert(commit.to_owned(), 0u32);
209
210 while let Some(commit) = queue.pop_value() {
211 commits_seen += 1;
212 let flags = if let Some(name) = name_by_oid.get(&commit) {
213 if candidates.len() < max_candidates {
214 let identity_bit = 1 << candidates.len();
215 candidates.push(Candidate {
216 name: name.clone(),
217 commits_in_its_future: commits_seen - 1,
218 identity_bit,
219 order: candidates.len(),
220 });
221 let flags = graph.get_mut(&commit).expect("inserted");
222 *flags |= identity_bit;
223 *flags
224 } else {
225 gave_up_on_commit = Some(commit);
226 break;
227 }
228 } else {
229 graph[&commit]
230 };
231
232 for candidate in candidates
233 .iter_mut()
234 .filter(|c| (flags & c.identity_bit) != c.identity_bit)
235 {
236 candidate.commits_in_its_future += 1;
237 }
238
239 if queue.is_empty() && !candidates.is_empty() {
240 let mut shortest_depth = Flags::MAX;
243 let mut best_candidates_at_same_depth = 0_u32;
244 for candidate in &candidates {
245 match candidate.commits_in_its_future.cmp(&shortest_depth) {
246 Ordering::Less => {
247 shortest_depth = candidate.commits_in_its_future;
248 best_candidates_at_same_depth = candidate.identity_bit;
249 }
250 Ordering::Equal => {
251 best_candidates_at_same_depth |= candidate.identity_bit;
252 }
253 Ordering::Greater => {}
254 }
255 }
256
257 if (flags & best_candidates_at_same_depth) == best_candidates_at_same_depth {
258 break;
259 }
260 }
261
262 parents_by_date_onto_queue_and_track_names(graph, &mut queue, commit, flags, first_parent)?;
263 }
264
265 if candidates.is_empty() {
266 return if fallback_to_oid {
267 Ok(Some(Outcome {
268 id: commit.to_owned(),
269 name: None,
270 name_by_oid,
271 depth: 0,
272 commits_seen,
273 }))
274 } else {
275 Ok(None)
276 };
277 }
278
279 candidates.sort_by(|a, b| {
280 a.commits_in_its_future
281 .cmp(&b.commits_in_its_future)
282 .then_with(|| a.order.cmp(&b.order))
283 });
284
285 if let Some(commit_id) = gave_up_on_commit {
286 queue.insert(u32::MAX, commit_id);
287 commits_seen -= 1;
288 }
289
290 commits_seen += finish_depth_computation(
291 queue,
292 graph,
293 candidates.first_mut().expect("at least one candidate"),
294 first_parent,
295 )?;
296
297 Ok(candidates.into_iter().next().map(|c| Outcome {
298 name: c.name.into(),
299 id: commit.to_owned(),
300 depth: c.commits_in_its_future,
301 name_by_oid,
302 commits_seen,
303 }))
304 }
305
306 fn parents_by_date_onto_queue_and_track_names(
307 graph: &mut Graph<'_, '_, Flags>,
308 queue: &mut PriorityQueue<CommitTime, gix_hash::ObjectId>,
309 commit: gix_hash::ObjectId,
310 commit_flags: Flags,
311 first_parent: bool,
312 ) -> Result<(), Error> {
313 graph
314 .insert_parents(
315 &commit,
316 &mut |parent_id, parent_commit_date| {
317 queue.insert(parent_commit_date as u32, parent_id);
318 commit_flags
319 },
320 &mut |_parent_id, flags| *flags |= commit_flags,
321 first_parent,
322 )
323 .map_err(|err| Error::InsertParentsToGraph { err, oid: commit })?;
324 Ok(())
325 }
326
327 fn finish_depth_computation(
328 mut queue: PriorityQueue<CommitTime, gix_hash::ObjectId>,
329 graph: &mut Graph<'_, '_, Flags>,
330 best_candidate: &mut Candidate<'_>,
331 first_parent: bool,
332 ) -> Result<u32, Error> {
333 let mut commits_seen = 0;
334 while let Some(commit) = queue.pop_value() {
335 commits_seen += 1;
336 let flags = graph[&commit];
337 if (flags & best_candidate.identity_bit) == best_candidate.identity_bit {
338 if queue
339 .iter_unordered()
340 .all(|id| (graph[id] & best_candidate.identity_bit) == best_candidate.identity_bit)
341 {
342 break;
343 }
344 } else {
345 best_candidate.commits_in_its_future += 1;
346 }
347
348 parents_by_date_onto_queue_and_track_names(graph, &mut queue, commit, flags, first_parent)?;
349 }
350 Ok(commits_seen)
351 }
352
353 #[derive(Debug)]
354 struct Candidate<'a> {
355 name: Cow<'a, BStr>,
356 commits_in_its_future: Flags,
357 identity_bit: Flags,
359 order: usize,
361 }
362}
363
364type CommitTime = u32;