1use crate::error::{Error, Result};
15use crate::ident::committer_unix_seconds_for_ordering;
16use crate::objects::{parse_commit, parse_tag, CommitData, ObjectId, ObjectKind};
17use crate::refs;
18use crate::repo::Repository;
19use std::collections::{HashMap, VecDeque};
20
21const MERGE_TRAVERSAL_WEIGHT: u32 = 65_535;
23
24#[derive(Clone, Debug)]
26struct RevName {
27 tip_name: String,
32 taggerdate: i64,
34 generation: u32,
36 distance: u32,
38 from_tag: bool,
40}
41
42#[derive(Debug, Default, Clone)]
44pub struct NameRevOptions {
45 pub tags_only: bool,
47 pub shorten_tags: bool,
52 pub ref_filters: Vec<String>,
55 pub exclude_filters: Vec<String>,
57}
58
59pub fn build_name_map(
71 repo: &Repository,
72 options: &NameRevOptions,
73) -> Result<HashMap<ObjectId, String>> {
74 let tips = collect_tips(repo, options)?;
75 let mut names: HashMap<ObjectId, RevName> = HashMap::new();
76
77 let mut commit_cache: HashMap<ObjectId, CommitData> = HashMap::new();
78
79 for tip in &tips {
80 let Some(commit_oid) = tip.commit_oid else {
81 continue;
82 };
83 if tip.deref {
84 names.insert(
85 tip.object_oid,
86 RevName {
87 tip_name: tip.display_name.clone(),
88 taggerdate: tip.taggerdate,
89 generation: 0,
90 distance: 0,
91 from_tag: tip.from_tag,
92 },
93 );
94 }
95 name_from_tip(
96 repo,
97 &mut names,
98 &mut commit_cache,
99 commit_oid,
100 &tip.display_name,
101 tip.taggerdate,
102 tip.from_tag,
103 tip.deref,
104 )?;
105 }
106
107 Ok(names
108 .into_iter()
109 .map(|(oid, name)| (oid, format_name(&name)))
110 .collect())
111}
112
113fn format_name(name: &RevName) -> String {
119 if name.generation == 0 {
120 return name.tip_name.clone();
121 }
122 let base = name.tip_name.strip_suffix("^0").unwrap_or(&name.tip_name);
123 format!("{}~{}", base, name.generation)
124}
125
126fn effective_distance(distance: u32, generation: u32) -> u32 {
132 distance.saturating_add(if generation > 0 {
133 MERGE_TRAVERSAL_WEIGHT
134 } else {
135 0
136 })
137}
138
139fn is_better_name(
146 existing: &RevName,
147 taggerdate: i64,
148 generation: u32,
149 distance: u32,
150 from_tag: bool,
151) -> bool {
152 let existing_eff = effective_distance(existing.distance, existing.generation);
153 let new_eff = effective_distance(distance, generation);
154
155 if from_tag && existing.from_tag {
157 return existing_eff > new_eff;
158 }
159 if existing.from_tag != from_tag {
160 return from_tag;
161 }
162
163 if existing_eff != new_eff {
165 return existing_eff > new_eff;
166 }
167
168 if existing.taggerdate != taggerdate {
170 return existing.taggerdate > taggerdate;
171 }
172
173 false
174}
175
176fn get_parent_name(current: &RevName, parent_number: u32) -> String {
181 let base = current
182 .tip_name
183 .strip_suffix("^0")
184 .unwrap_or(¤t.tip_name);
185 if current.generation > 0 {
186 format!("{}~{}^{}", base, current.generation, parent_number)
187 } else {
188 format!("{}^{}", base, parent_number)
189 }
190}
191
192fn name_from_tip(
196 repo: &Repository,
197 names: &mut HashMap<ObjectId, RevName>,
198 commit_cache: &mut HashMap<ObjectId, CommitData>,
199 start_oid: ObjectId,
200 tip_name: &str,
201 taggerdate: i64,
202 from_tag: bool,
203 deref: bool,
204) -> Result<()> {
205 let actual_tip_name = if deref {
206 format!("{}^0", tip_name)
207 } else {
208 tip_name.to_owned()
209 };
210
211 let should_start = match names.get(&start_oid) {
213 None => true,
214 Some(existing) => is_better_name(existing, taggerdate, 0, 0, from_tag),
215 };
216 if !should_start {
217 return Ok(());
218 }
219 names.insert(
220 start_oid,
221 RevName {
222 tip_name: actual_tip_name,
223 taggerdate,
224 generation: 0,
225 distance: 0,
226 from_tag,
227 },
228 );
229
230 let mut stack: Vec<ObjectId> = vec![start_oid];
232
233 while let Some(oid) = stack.pop() {
234 let current = match names.get(&oid) {
235 Some(n) => n.clone(),
236 None => continue,
237 };
238
239 let commit = match load_commit_cached(repo, commit_cache, oid) {
240 Ok(c) => c,
241 Err(_) => continue,
242 };
243 let parents = commit.parents.clone();
245
246 let mut to_push: Vec<ObjectId> = Vec::new();
247
248 for (idx, parent_oid) in parents.iter().enumerate() {
249 let parent_number = (idx + 1) as u32;
250
251 let (parent_gen, parent_dist) = if parent_number > 1 {
252 (
253 0u32,
254 current.distance.saturating_add(MERGE_TRAVERSAL_WEIGHT),
255 )
256 } else {
257 (
258 current.generation.saturating_add(1),
259 current.distance.saturating_add(1),
260 )
261 };
262
263 let should_update = match names.get(parent_oid) {
264 None => true,
265 Some(existing) => {
266 is_better_name(existing, taggerdate, parent_gen, parent_dist, from_tag)
267 }
268 };
269
270 if should_update {
271 let parent_tip_name = if parent_number > 1 {
272 get_parent_name(¤t, parent_number)
273 } else {
274 current.tip_name.clone()
275 };
276
277 names.insert(
278 *parent_oid,
279 RevName {
280 tip_name: parent_tip_name,
281 taggerdate,
282 generation: parent_gen,
283 distance: parent_dist,
284 from_tag,
285 },
286 );
287 to_push.push(*parent_oid);
288 }
289 }
290
291 for parent in to_push.into_iter().rev() {
292 stack.push(parent);
293 }
294 }
295
296 Ok(())
297}
298
299struct TipEntry {
301 object_oid: ObjectId,
303 display_name: String,
305 commit_oid: Option<ObjectId>,
307 taggerdate: i64,
309 from_tag: bool,
311 deref: bool,
313}
314
315fn collect_tips(repo: &Repository, options: &NameRevOptions) -> Result<Vec<TipEntry>> {
317 let all_refs = refs::list_refs(&repo.git_dir, "refs/")?;
318 let mut tips: Vec<TipEntry> = Vec::new();
319
320 for (refname, oid) in all_refs {
321 if options.tags_only && !refname.starts_with("refs/tags/") {
322 continue;
323 }
324
325 if options
327 .exclude_filters
328 .iter()
329 .any(|pat| subpath_matches(&refname, pat))
330 {
331 continue;
332 }
333
334 let can_abbreviate = if !options.ref_filters.is_empty() {
336 let mut matched = false;
337 let mut subpath_match = false;
338 for pat in &options.ref_filters {
339 match subpath_match_kind(&refname, pat) {
340 SubpathMatch::Full => matched = true,
341 SubpathMatch::Sub => {
342 matched = true;
343 subpath_match = true;
344 }
345 SubpathMatch::None => {}
346 }
347 }
348 if !matched {
349 continue;
350 }
351 subpath_match
352 } else {
353 false
356 };
357
358 let from_tag = refname.starts_with("refs/tags/");
359 let display_name = shorten_refname(&refname, can_abbreviate || options.shorten_tags);
360
361 let (commit_oid, taggerdate, deref) = peel_to_commit(repo, oid)?;
363
364 tips.push(TipEntry {
365 object_oid: oid,
366 display_name,
367 commit_oid,
368 taggerdate,
369 from_tag,
370 deref,
371 });
372 }
373
374 tips.sort_by(|a, b| {
376 let tag_cmp = b.from_tag.cmp(&a.from_tag); if tag_cmp != std::cmp::Ordering::Equal {
378 return tag_cmp;
379 }
380 a.taggerdate.cmp(&b.taggerdate)
381 });
382
383 Ok(tips)
384}
385
386fn peel_to_commit(repo: &Repository, mut oid: ObjectId) -> Result<(Option<ObjectId>, i64, bool)> {
393 let mut deref = false;
394 let mut taggerdate: Option<i64> = None;
395
396 loop {
397 let obj = match repo.odb.read(&oid) {
398 Ok(o) => o,
399 Err(_) => return Ok((None, taggerdate.unwrap_or(0), deref)),
400 };
401
402 match obj.kind {
403 ObjectKind::Commit => {
404 let ts = if let Ok(c) = parse_commit(&obj.data) {
405 parse_signature_time(&c.committer)
406 } else {
407 0
408 };
409 let date = taggerdate.unwrap_or(ts);
410 return Ok((Some(oid), date, deref));
411 }
412 ObjectKind::Tag => {
413 let tag = match parse_tag(&obj.data) {
414 Ok(t) => t,
415 Err(_) => return Ok((None, taggerdate.unwrap_or(0), deref)),
416 };
417 if taggerdate.is_none() {
419 taggerdate = Some(tag.tagger.as_deref().map(parse_signature_time).unwrap_or(0));
420 }
421 oid = tag.object;
422 deref = true;
423 }
424 _ => return Ok((None, taggerdate.unwrap_or(0), deref)),
425 }
426 }
427}
428
429pub fn all_reachable_commits(repo: &Repository) -> Result<Vec<ObjectId>> {
437 let all_refs = refs::list_refs(&repo.git_dir, "refs/")?;
438 let mut seen: std::collections::HashSet<ObjectId> = std::collections::HashSet::new();
439 let mut queue: VecDeque<ObjectId> = VecDeque::new();
440
441 for (_, oid) in all_refs {
442 let (commit_oid, _, _) = peel_to_commit(repo, oid)?;
444 if let Some(c) = commit_oid {
445 if seen.insert(c) {
446 queue.push_back(c);
447 }
448 }
449 }
450
451 while let Some(oid) = queue.pop_front() {
452 let commit = match load_commit(repo, oid) {
453 Ok(c) => c,
454 Err(_) => continue,
455 };
456 for parent in commit.parents {
457 if seen.insert(parent) {
458 queue.push_back(parent);
459 }
460 }
461 }
462
463 let mut result: Vec<ObjectId> = seen.into_iter().collect();
464 result.sort();
465 Ok(result)
466}
467
468fn shorten_refname(refname: &str, can_abbreviate: bool) -> String {
475 if can_abbreviate {
476 if let Some(rest) = refname.strip_prefix("refs/heads/") {
477 return rest.to_owned();
478 }
479 if let Some(rest) = refname.strip_prefix("refs/tags/") {
480 return rest.to_owned();
481 }
482 if let Some(rest) = refname.strip_prefix("refs/") {
483 return rest.to_owned();
484 }
485 return refname.to_owned();
486 }
487 if let Some(rest) = refname.strip_prefix("refs/heads/") {
489 return rest.to_owned();
490 }
491 if let Some(rest) = refname.strip_prefix("refs/") {
492 return rest.to_owned();
493 }
494 refname.to_owned()
495}
496
497#[derive(PartialEq, Eq)]
499enum SubpathMatch {
500 Full,
502 Sub,
504 None,
506}
507
508fn subpath_match_kind(path: &str, pattern: &str) -> SubpathMatch {
513 if glob_matches(pattern, path) {
515 return SubpathMatch::Full;
516 }
517 let mut rest = path;
519 while let Some(pos) = rest.find('/') {
520 rest = &rest[pos + 1..];
521 if glob_matches(pattern, rest) {
522 return SubpathMatch::Sub;
523 }
524 }
525 SubpathMatch::None
526}
527
528fn subpath_matches(path: &str, pattern: &str) -> bool {
530 subpath_match_kind(path, pattern) != SubpathMatch::None
531}
532
533fn glob_matches(pattern: &str, text: &str) -> bool {
537 let pat: Vec<char> = pattern.chars().collect();
538 let txt: Vec<char> = text.chars().collect();
539 glob_match_inner(&pat, &txt)
540}
541
542fn glob_match_inner(pat: &[char], txt: &[char]) -> bool {
543 match (pat.first(), txt.first()) {
544 (None, None) => true,
545 (None, Some(_)) => false,
546 (Some('*'), _) => {
547 glob_match_inner(&pat[1..], txt)
549 || (!txt.is_empty() && glob_match_inner(pat, &txt[1..]))
550 }
551 (Some('?'), Some(_)) => glob_match_inner(&pat[1..], &txt[1..]),
552 (Some('?'), None) => false,
553 (Some(p), Some(t)) => p == t && glob_match_inner(&pat[1..], &txt[1..]),
554 (Some(_), None) => false,
555 }
556}
557
558pub(crate) fn parse_signature_time(sig: &str) -> i64 {
564 committer_unix_seconds_for_ordering(sig)
565}
566
567fn load_commit_cached<'c>(
569 repo: &Repository,
570 cache: &'c mut HashMap<ObjectId, CommitData>,
571 oid: ObjectId,
572) -> Result<&'c CommitData> {
573 if let std::collections::hash_map::Entry::Vacant(e) = cache.entry(oid) {
574 let obj = repo.odb.read(&oid)?;
575 if obj.kind != ObjectKind::Commit {
576 return Err(Error::CorruptObject(format!(
577 "object {oid} is not a commit"
578 )));
579 }
580 let commit = parse_commit(&obj.data)?;
581 e.insert(commit);
582 }
583 cache
584 .get(&oid)
585 .ok_or_else(|| Error::CorruptObject(format!("commit {oid} missing from cache")))
586}
587
588fn load_commit(repo: &Repository, oid: ObjectId) -> Result<CommitData> {
590 let obj = repo.odb.read(&oid)?;
591 if obj.kind != ObjectKind::Commit {
592 return Err(Error::CorruptObject(format!(
593 "object {oid} is not a commit"
594 )));
595 }
596 parse_commit(&obj.data)
597}
598
599pub fn resolve_oid(repo: &Repository, spec: &str) -> Result<ObjectId> {
608 crate::rev_parse::resolve_revision(repo, spec)
609}
610
611pub fn lookup_name<'m>(
623 repo: &Repository,
624 name_map: &'m HashMap<ObjectId, String>,
625 oid: ObjectId,
626) -> Result<Option<&'m String>> {
627 if let Some(name) = name_map.get(&oid) {
629 return Ok(Some(name));
630 }
631
632 let obj = match repo.odb.read(&oid) {
634 Ok(o) => o,
635 Err(_) => return Ok(None),
636 };
637 if obj.kind == ObjectKind::Tag {
638 let (commit_oid, _, _) = peel_to_commit(repo, oid)?;
639 if let Some(c) = commit_oid {
640 return Ok(name_map.get(&c));
641 }
642 }
643 Ok(None)
644}
645
646pub fn annotate_line(
653 repo: &Repository,
654 name_map: &HashMap<ObjectId, String>,
655 line: &str,
656 name_only: bool,
657) -> Result<String> {
658 let mut out = String::with_capacity(line.len() + 32);
659 let chars: Vec<char> = line.chars().collect();
660 let hex_len = 40usize;
661 let mut i = 0usize;
662 let mut flush_start = 0usize;
663
664 while i + hex_len <= chars.len() {
665 let slice: String = chars[i..i + hex_len].iter().collect();
667 let after_is_hex = chars
668 .get(i + hex_len)
669 .map(|c| c.is_ascii_hexdigit())
670 .unwrap_or(false);
671 if !after_is_hex && slice.chars().all(|c| c.is_ascii_hexdigit()) {
672 if let Ok(oid) = slice.parse::<ObjectId>() {
674 if let Ok(Some(name)) = lookup_name(repo, name_map, oid) {
675 let prefix: String = chars[flush_start..i].iter().collect();
677 out.push_str(&prefix);
678
679 if name_only {
680 out.push_str(name);
681 } else {
682 out.push_str(&slice);
683 out.push_str(" (");
684 out.push_str(name);
685 out.push(')');
686 }
687 flush_start = i + hex_len;
688 i += hex_len;
689 continue;
690 }
691 }
692 }
693 i += 1;
694 }
695
696 let tail: String = chars[flush_start..].iter().collect();
698 out.push_str(&tail);
699 Ok(out)
700}
701
702#[must_use]
704pub fn abbrev_oid(oid: ObjectId, len: usize) -> String {
705 let hex = oid.to_hex();
706 let n = len.clamp(4, 40).min(hex.len());
707 hex[..n].to_owned()
708}
709
710pub use self::all_reachable_commits as walk_all_commits;
714
715pub fn object_exists(repo: &Repository, oid: ObjectId) -> bool {
717 repo.odb.exists(&oid)
718}