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 name_from_tip(
84 repo,
85 &mut names,
86 &mut commit_cache,
87 commit_oid,
88 &tip.display_name,
89 tip.taggerdate,
90 tip.from_tag,
91 tip.deref,
92 )?;
93 }
94
95 Ok(names
96 .into_iter()
97 .map(|(oid, name)| (oid, format_name(&name)))
98 .collect())
99}
100
101fn format_name(name: &RevName) -> String {
107 if name.generation == 0 {
108 return name.tip_name.clone();
109 }
110 let base = name.tip_name.strip_suffix("^0").unwrap_or(&name.tip_name);
111 format!("{}~{}", base, name.generation)
112}
113
114fn effective_distance(distance: u32, generation: u32) -> u32 {
120 distance.saturating_add(if generation > 0 {
121 MERGE_TRAVERSAL_WEIGHT
122 } else {
123 0
124 })
125}
126
127fn is_better_name(
134 existing: &RevName,
135 taggerdate: i64,
136 generation: u32,
137 distance: u32,
138 from_tag: bool,
139) -> bool {
140 let existing_eff = effective_distance(existing.distance, existing.generation);
141 let new_eff = effective_distance(distance, generation);
142
143 if from_tag && existing.from_tag {
145 return existing_eff > new_eff;
146 }
147 if existing.from_tag != from_tag {
148 return from_tag;
149 }
150
151 if existing_eff != new_eff {
153 return existing_eff > new_eff;
154 }
155
156 if existing.taggerdate != taggerdate {
158 return existing.taggerdate > taggerdate;
159 }
160
161 false
162}
163
164fn get_parent_name(current: &RevName, parent_number: u32) -> String {
169 let base = current
170 .tip_name
171 .strip_suffix("^0")
172 .unwrap_or(¤t.tip_name);
173 if current.generation > 0 {
174 format!("{}~{}^{}", base, current.generation, parent_number)
175 } else {
176 format!("{}^{}", base, parent_number)
177 }
178}
179
180fn name_from_tip(
184 repo: &Repository,
185 names: &mut HashMap<ObjectId, RevName>,
186 commit_cache: &mut HashMap<ObjectId, CommitData>,
187 start_oid: ObjectId,
188 tip_name: &str,
189 taggerdate: i64,
190 from_tag: bool,
191 deref: bool,
192) -> Result<()> {
193 let actual_tip_name = if deref {
194 format!("{}^0", tip_name)
195 } else {
196 tip_name.to_owned()
197 };
198
199 let should_start = match names.get(&start_oid) {
201 None => true,
202 Some(existing) => is_better_name(existing, taggerdate, 0, 0, from_tag),
203 };
204 if !should_start {
205 return Ok(());
206 }
207 names.insert(
208 start_oid,
209 RevName {
210 tip_name: actual_tip_name,
211 taggerdate,
212 generation: 0,
213 distance: 0,
214 from_tag,
215 },
216 );
217
218 let mut stack: Vec<ObjectId> = vec![start_oid];
220
221 while let Some(oid) = stack.pop() {
222 let current = match names.get(&oid) {
223 Some(n) => n.clone(),
224 None => continue,
225 };
226
227 let commit = match load_commit_cached(repo, commit_cache, oid) {
228 Ok(c) => c,
229 Err(_) => continue,
230 };
231 let parents = commit.parents.clone();
233
234 let mut to_push: Vec<ObjectId> = Vec::new();
235
236 for (idx, parent_oid) in parents.iter().enumerate() {
237 let parent_number = (idx + 1) as u32;
238
239 let (parent_gen, parent_dist) = if parent_number > 1 {
240 (
241 0u32,
242 current.distance.saturating_add(MERGE_TRAVERSAL_WEIGHT),
243 )
244 } else {
245 (
246 current.generation.saturating_add(1),
247 current.distance.saturating_add(1),
248 )
249 };
250
251 let should_update = match names.get(parent_oid) {
252 None => true,
253 Some(existing) => {
254 is_better_name(existing, taggerdate, parent_gen, parent_dist, from_tag)
255 }
256 };
257
258 if should_update {
259 let parent_tip_name = if parent_number > 1 {
260 get_parent_name(¤t, parent_number)
261 } else {
262 current.tip_name.clone()
263 };
264
265 names.insert(
266 *parent_oid,
267 RevName {
268 tip_name: parent_tip_name,
269 taggerdate,
270 generation: parent_gen,
271 distance: parent_dist,
272 from_tag,
273 },
274 );
275 to_push.push(*parent_oid);
276 }
277 }
278
279 for parent in to_push.into_iter().rev() {
280 stack.push(parent);
281 }
282 }
283
284 Ok(())
285}
286
287struct TipEntry {
289 display_name: String,
291 commit_oid: Option<ObjectId>,
293 taggerdate: i64,
295 from_tag: bool,
297 deref: bool,
299}
300
301fn collect_tips(repo: &Repository, options: &NameRevOptions) -> Result<Vec<TipEntry>> {
303 let all_refs = refs::list_refs(&repo.git_dir, "refs/")?;
304 let mut tips: Vec<TipEntry> = Vec::new();
305
306 for (refname, oid) in all_refs {
307 if options.tags_only && !refname.starts_with("refs/tags/") {
308 continue;
309 }
310
311 if options
313 .exclude_filters
314 .iter()
315 .any(|pat| subpath_matches(&refname, pat))
316 {
317 continue;
318 }
319
320 let can_abbreviate = if !options.ref_filters.is_empty() {
322 let mut matched = false;
323 let mut subpath_match = false;
324 for pat in &options.ref_filters {
325 match subpath_match_kind(&refname, pat) {
326 SubpathMatch::Full => matched = true,
327 SubpathMatch::Sub => {
328 matched = true;
329 subpath_match = true;
330 }
331 SubpathMatch::None => {}
332 }
333 }
334 if !matched {
335 continue;
336 }
337 subpath_match
338 } else {
339 false
342 };
343
344 let from_tag = refname.starts_with("refs/tags/");
345 let display_name = shorten_refname(&refname, can_abbreviate || options.shorten_tags);
346
347 let (commit_oid, taggerdate, deref) = peel_to_commit(repo, oid)?;
349
350 tips.push(TipEntry {
351 display_name,
352 commit_oid,
353 taggerdate,
354 from_tag,
355 deref,
356 });
357 }
358
359 tips.sort_by(|a, b| {
361 let tag_cmp = b.from_tag.cmp(&a.from_tag); if tag_cmp != std::cmp::Ordering::Equal {
363 return tag_cmp;
364 }
365 a.taggerdate.cmp(&b.taggerdate)
366 });
367
368 Ok(tips)
369}
370
371fn peel_to_commit(repo: &Repository, mut oid: ObjectId) -> Result<(Option<ObjectId>, i64, bool)> {
378 let mut deref = false;
379 let mut taggerdate: Option<i64> = None;
380
381 loop {
382 let obj = match repo.odb.read(&oid) {
383 Ok(o) => o,
384 Err(_) => return Ok((None, taggerdate.unwrap_or(0), deref)),
385 };
386
387 match obj.kind {
388 ObjectKind::Commit => {
389 let ts = if let Ok(c) = parse_commit(&obj.data) {
390 parse_signature_time(&c.committer)
391 } else {
392 0
393 };
394 let date = taggerdate.unwrap_or(ts);
395 return Ok((Some(oid), date, deref));
396 }
397 ObjectKind::Tag => {
398 let tag = match parse_tag(&obj.data) {
399 Ok(t) => t,
400 Err(_) => return Ok((None, taggerdate.unwrap_or(0), deref)),
401 };
402 if taggerdate.is_none() {
404 taggerdate = Some(tag.tagger.as_deref().map(parse_signature_time).unwrap_or(0));
405 }
406 oid = tag.object;
407 deref = true;
408 }
409 _ => return Ok((None, taggerdate.unwrap_or(0), deref)),
410 }
411 }
412}
413
414pub fn all_reachable_commits(repo: &Repository) -> Result<Vec<ObjectId>> {
422 let all_refs = refs::list_refs(&repo.git_dir, "refs/")?;
423 let mut seen: std::collections::HashSet<ObjectId> = std::collections::HashSet::new();
424 let mut queue: VecDeque<ObjectId> = VecDeque::new();
425
426 for (_, oid) in all_refs {
427 let (commit_oid, _, _) = peel_to_commit(repo, oid)?;
429 if let Some(c) = commit_oid {
430 if seen.insert(c) {
431 queue.push_back(c);
432 }
433 }
434 }
435
436 while let Some(oid) = queue.pop_front() {
437 let commit = match load_commit(repo, oid) {
438 Ok(c) => c,
439 Err(_) => continue,
440 };
441 for parent in commit.parents {
442 if seen.insert(parent) {
443 queue.push_back(parent);
444 }
445 }
446 }
447
448 let mut result: Vec<ObjectId> = seen.into_iter().collect();
449 result.sort();
450 Ok(result)
451}
452
453fn shorten_refname(refname: &str, can_abbreviate: bool) -> String {
460 if can_abbreviate {
461 if let Some(rest) = refname.strip_prefix("refs/heads/") {
462 return rest.to_owned();
463 }
464 if let Some(rest) = refname.strip_prefix("refs/tags/") {
465 return rest.to_owned();
466 }
467 if let Some(rest) = refname.strip_prefix("refs/") {
468 return rest.to_owned();
469 }
470 return refname.to_owned();
471 }
472 if let Some(rest) = refname.strip_prefix("refs/heads/") {
474 return rest.to_owned();
475 }
476 if let Some(rest) = refname.strip_prefix("refs/") {
477 return rest.to_owned();
478 }
479 refname.to_owned()
480}
481
482#[derive(PartialEq, Eq)]
484enum SubpathMatch {
485 Full,
487 Sub,
489 None,
491}
492
493fn subpath_match_kind(path: &str, pattern: &str) -> SubpathMatch {
498 if glob_matches(pattern, path) {
500 return SubpathMatch::Full;
501 }
502 let mut rest = path;
504 while let Some(pos) = rest.find('/') {
505 rest = &rest[pos + 1..];
506 if glob_matches(pattern, rest) {
507 return SubpathMatch::Sub;
508 }
509 }
510 SubpathMatch::None
511}
512
513fn subpath_matches(path: &str, pattern: &str) -> bool {
515 subpath_match_kind(path, pattern) != SubpathMatch::None
516}
517
518fn glob_matches(pattern: &str, text: &str) -> bool {
522 let pat: Vec<char> = pattern.chars().collect();
523 let txt: Vec<char> = text.chars().collect();
524 glob_match_inner(&pat, &txt)
525}
526
527fn glob_match_inner(pat: &[char], txt: &[char]) -> bool {
528 match (pat.first(), txt.first()) {
529 (None, None) => true,
530 (None, Some(_)) => false,
531 (Some('*'), _) => {
532 glob_match_inner(&pat[1..], txt)
534 || (!txt.is_empty() && glob_match_inner(pat, &txt[1..]))
535 }
536 (Some('?'), Some(_)) => glob_match_inner(&pat[1..], &txt[1..]),
537 (Some('?'), None) => false,
538 (Some(p), Some(t)) => p == t && glob_match_inner(&pat[1..], &txt[1..]),
539 (Some(_), None) => false,
540 }
541}
542
543pub(crate) fn parse_signature_time(sig: &str) -> i64 {
549 committer_unix_seconds_for_ordering(sig)
550}
551
552fn load_commit_cached<'c>(
554 repo: &Repository,
555 cache: &'c mut HashMap<ObjectId, CommitData>,
556 oid: ObjectId,
557) -> Result<&'c CommitData> {
558 if let std::collections::hash_map::Entry::Vacant(e) = cache.entry(oid) {
559 let obj = repo.odb.read(&oid)?;
560 if obj.kind != ObjectKind::Commit {
561 return Err(Error::CorruptObject(format!(
562 "object {oid} is not a commit"
563 )));
564 }
565 let commit = parse_commit(&obj.data)?;
566 e.insert(commit);
567 }
568 Ok(cache.get(&oid).unwrap())
569}
570
571fn load_commit(repo: &Repository, oid: ObjectId) -> Result<CommitData> {
573 let obj = repo.odb.read(&oid)?;
574 if obj.kind != ObjectKind::Commit {
575 return Err(Error::CorruptObject(format!(
576 "object {oid} is not a commit"
577 )));
578 }
579 parse_commit(&obj.data)
580}
581
582pub fn resolve_oid(repo: &Repository, spec: &str) -> Result<ObjectId> {
591 crate::rev_parse::resolve_revision(repo, spec)
592}
593
594pub fn lookup_name<'m>(
606 repo: &Repository,
607 name_map: &'m HashMap<ObjectId, String>,
608 oid: ObjectId,
609) -> Result<Option<&'m String>> {
610 if let Some(name) = name_map.get(&oid) {
612 return Ok(Some(name));
613 }
614
615 let obj = match repo.odb.read(&oid) {
617 Ok(o) => o,
618 Err(_) => return Ok(None),
619 };
620 if obj.kind == ObjectKind::Tag {
621 let (commit_oid, _, _) = peel_to_commit(repo, oid)?;
622 if let Some(c) = commit_oid {
623 return Ok(name_map.get(&c));
624 }
625 }
626 Ok(None)
627}
628
629pub fn annotate_line(
636 repo: &Repository,
637 name_map: &HashMap<ObjectId, String>,
638 line: &str,
639 name_only: bool,
640) -> Result<String> {
641 let mut out = String::with_capacity(line.len() + 32);
642 let chars: Vec<char> = line.chars().collect();
643 let hex_len = 40usize;
644 let mut i = 0usize;
645 let mut flush_start = 0usize;
646
647 while i + hex_len <= chars.len() {
648 let slice: String = chars[i..i + hex_len].iter().collect();
650 let after_is_hex = chars
651 .get(i + hex_len)
652 .map(|c| c.is_ascii_hexdigit())
653 .unwrap_or(false);
654 if !after_is_hex && slice.chars().all(|c| c.is_ascii_hexdigit()) {
655 if let Ok(oid) = slice.parse::<ObjectId>() {
657 if let Ok(Some(name)) = lookup_name(repo, name_map, oid) {
658 let prefix: String = chars[flush_start..i].iter().collect();
660 out.push_str(&prefix);
661
662 if name_only {
663 out.push_str(name);
664 } else {
665 out.push_str(&slice);
666 out.push_str(" (");
667 out.push_str(name);
668 out.push(')');
669 }
670 flush_start = i + hex_len;
671 i += hex_len;
672 continue;
673 }
674 }
675 }
676 i += 1;
677 }
678
679 let tail: String = chars[flush_start..].iter().collect();
681 out.push_str(&tail);
682 Ok(out)
683}
684
685#[must_use]
687pub fn abbrev_oid(oid: ObjectId, len: usize) -> String {
688 let hex = oid.to_hex();
689 let n = len.clamp(4, 40).min(hex.len());
690 hex[..n].to_owned()
691}
692
693pub use self::all_reachable_commits as walk_all_commits;
697
698pub fn object_exists(repo: &Repository, oid: ObjectId) -> bool {
700 repo.odb.exists(&oid)
701}