1use crate::error::{Error, Result};
15use crate::objects::{parse_commit, parse_tag, CommitData, ObjectId, ObjectKind};
16use crate::refs;
17use crate::repo::Repository;
18use std::collections::{HashMap, VecDeque};
19
20const MERGE_TRAVERSAL_WEIGHT: u32 = 65_535;
22
23#[derive(Clone, Debug)]
25struct RevName {
26 tip_name: String,
31 taggerdate: i64,
33 generation: u32,
35 distance: u32,
37 from_tag: bool,
39}
40
41#[derive(Debug, Default, Clone)]
43pub struct NameRevOptions {
44 pub tags_only: bool,
46 pub shorten_tags: bool,
51 pub ref_filters: Vec<String>,
54 pub exclude_filters: Vec<String>,
56}
57
58pub fn build_name_map(
70 repo: &Repository,
71 options: &NameRevOptions,
72) -> Result<HashMap<ObjectId, String>> {
73 let tips = collect_tips(repo, options)?;
74 let mut names: HashMap<ObjectId, RevName> = HashMap::new();
75
76 let mut commit_cache: HashMap<ObjectId, CommitData> = HashMap::new();
77
78 for tip in &tips {
79 let Some(commit_oid) = tip.commit_oid else {
80 continue;
81 };
82 name_from_tip(
83 repo,
84 &mut names,
85 &mut commit_cache,
86 commit_oid,
87 &tip.display_name,
88 tip.taggerdate,
89 tip.from_tag,
90 tip.deref,
91 )?;
92 }
93
94 Ok(names
95 .into_iter()
96 .map(|(oid, name)| (oid, format_name(&name)))
97 .collect())
98}
99
100fn format_name(name: &RevName) -> String {
106 if name.generation == 0 {
107 return name.tip_name.clone();
108 }
109 let base = name.tip_name.strip_suffix("^0").unwrap_or(&name.tip_name);
110 format!("{}~{}", base, name.generation)
111}
112
113fn effective_distance(distance: u32, generation: u32) -> u32 {
119 distance.saturating_add(if generation > 0 {
120 MERGE_TRAVERSAL_WEIGHT
121 } else {
122 0
123 })
124}
125
126fn is_better_name(
133 existing: &RevName,
134 taggerdate: i64,
135 generation: u32,
136 distance: u32,
137 from_tag: bool,
138) -> bool {
139 let existing_eff = effective_distance(existing.distance, existing.generation);
140 let new_eff = effective_distance(distance, generation);
141
142 if from_tag && existing.from_tag {
144 return existing_eff > new_eff;
145 }
146 if existing.from_tag != from_tag {
147 return from_tag;
148 }
149
150 if existing_eff != new_eff {
152 return existing_eff > new_eff;
153 }
154
155 if existing.taggerdate != taggerdate {
157 return existing.taggerdate > taggerdate;
158 }
159
160 false
161}
162
163fn get_parent_name(current: &RevName, parent_number: u32) -> String {
168 let base = current
169 .tip_name
170 .strip_suffix("^0")
171 .unwrap_or(¤t.tip_name);
172 if current.generation > 0 {
173 format!("{}~{}^{}", base, current.generation, parent_number)
174 } else {
175 format!("{}^{}", base, parent_number)
176 }
177}
178
179fn name_from_tip(
183 repo: &Repository,
184 names: &mut HashMap<ObjectId, RevName>,
185 commit_cache: &mut HashMap<ObjectId, CommitData>,
186 start_oid: ObjectId,
187 tip_name: &str,
188 taggerdate: i64,
189 from_tag: bool,
190 deref: bool,
191) -> Result<()> {
192 let actual_tip_name = if deref {
193 format!("{}^0", tip_name)
194 } else {
195 tip_name.to_owned()
196 };
197
198 let should_start = match names.get(&start_oid) {
200 None => true,
201 Some(existing) => is_better_name(existing, taggerdate, 0, 0, from_tag),
202 };
203 if !should_start {
204 return Ok(());
205 }
206 names.insert(
207 start_oid,
208 RevName {
209 tip_name: actual_tip_name,
210 taggerdate,
211 generation: 0,
212 distance: 0,
213 from_tag,
214 },
215 );
216
217 let mut stack: Vec<ObjectId> = vec![start_oid];
219
220 while let Some(oid) = stack.pop() {
221 let current = match names.get(&oid) {
222 Some(n) => n.clone(),
223 None => continue,
224 };
225
226 let commit = match load_commit_cached(repo, commit_cache, oid) {
227 Ok(c) => c,
228 Err(_) => continue,
229 };
230 let parents = commit.parents.clone();
232
233 let mut to_push: Vec<ObjectId> = Vec::new();
234
235 for (idx, parent_oid) in parents.iter().enumerate() {
236 let parent_number = (idx + 1) as u32;
237
238 let (parent_gen, parent_dist) = if parent_number > 1 {
239 (
240 0u32,
241 current.distance.saturating_add(MERGE_TRAVERSAL_WEIGHT),
242 )
243 } else {
244 (
245 current.generation.saturating_add(1),
246 current.distance.saturating_add(1),
247 )
248 };
249
250 let should_update = match names.get(parent_oid) {
251 None => true,
252 Some(existing) => {
253 is_better_name(existing, taggerdate, parent_gen, parent_dist, from_tag)
254 }
255 };
256
257 if should_update {
258 let parent_tip_name = if parent_number > 1 {
259 get_parent_name(¤t, parent_number)
260 } else {
261 current.tip_name.clone()
262 };
263
264 names.insert(
265 *parent_oid,
266 RevName {
267 tip_name: parent_tip_name,
268 taggerdate,
269 generation: parent_gen,
270 distance: parent_dist,
271 from_tag,
272 },
273 );
274 to_push.push(*parent_oid);
275 }
276 }
277
278 for parent in to_push.into_iter().rev() {
279 stack.push(parent);
280 }
281 }
282
283 Ok(())
284}
285
286struct TipEntry {
288 display_name: String,
290 commit_oid: Option<ObjectId>,
292 taggerdate: i64,
294 from_tag: bool,
296 deref: bool,
298}
299
300fn collect_tips(repo: &Repository, options: &NameRevOptions) -> Result<Vec<TipEntry>> {
302 let all_refs = refs::list_refs(&repo.git_dir, "refs/")?;
303 let mut tips: Vec<TipEntry> = Vec::new();
304
305 for (refname, oid) in all_refs {
306 if options.tags_only && !refname.starts_with("refs/tags/") {
307 continue;
308 }
309
310 if options
312 .exclude_filters
313 .iter()
314 .any(|pat| subpath_matches(&refname, pat))
315 {
316 continue;
317 }
318
319 let can_abbreviate = if !options.ref_filters.is_empty() {
321 let mut matched = false;
322 let mut subpath_match = false;
323 for pat in &options.ref_filters {
324 match subpath_match_kind(&refname, pat) {
325 SubpathMatch::Full => matched = true,
326 SubpathMatch::Sub => {
327 matched = true;
328 subpath_match = true;
329 }
330 SubpathMatch::None => {}
331 }
332 }
333 if !matched {
334 continue;
335 }
336 subpath_match
337 } else {
338 false
341 };
342
343 let from_tag = refname.starts_with("refs/tags/");
344 let display_name = shorten_refname(&refname, can_abbreviate || options.shorten_tags);
345
346 let (commit_oid, taggerdate, deref) = peel_to_commit(repo, oid)?;
348
349 tips.push(TipEntry {
350 display_name,
351 commit_oid,
352 taggerdate,
353 from_tag,
354 deref,
355 });
356 }
357
358 tips.sort_by(|a, b| {
360 let tag_cmp = b.from_tag.cmp(&a.from_tag); if tag_cmp != std::cmp::Ordering::Equal {
362 return tag_cmp;
363 }
364 a.taggerdate.cmp(&b.taggerdate)
365 });
366
367 Ok(tips)
368}
369
370fn peel_to_commit(repo: &Repository, mut oid: ObjectId) -> Result<(Option<ObjectId>, i64, bool)> {
377 let mut deref = false;
378 let mut taggerdate: Option<i64> = None;
379
380 loop {
381 let obj = match repo.odb.read(&oid) {
382 Ok(o) => o,
383 Err(_) => return Ok((None, taggerdate.unwrap_or(0), deref)),
384 };
385
386 match obj.kind {
387 ObjectKind::Commit => {
388 let ts = if let Ok(c) = parse_commit(&obj.data) {
389 parse_signature_time(&c.committer)
390 } else {
391 0
392 };
393 let date = taggerdate.unwrap_or(ts);
394 return Ok((Some(oid), date, deref));
395 }
396 ObjectKind::Tag => {
397 let tag = match parse_tag(&obj.data) {
398 Ok(t) => t,
399 Err(_) => return Ok((None, taggerdate.unwrap_or(0), deref)),
400 };
401 if taggerdate.is_none() {
403 taggerdate = Some(tag.tagger.as_deref().map(parse_signature_time).unwrap_or(0));
404 }
405 oid = tag.object;
406 deref = true;
407 }
408 _ => return Ok((None, taggerdate.unwrap_or(0), deref)),
409 }
410 }
411}
412
413pub fn all_reachable_commits(repo: &Repository) -> Result<Vec<ObjectId>> {
421 let all_refs = refs::list_refs(&repo.git_dir, "refs/")?;
422 let mut seen: std::collections::HashSet<ObjectId> = std::collections::HashSet::new();
423 let mut queue: VecDeque<ObjectId> = VecDeque::new();
424
425 for (_, oid) in all_refs {
426 let (commit_oid, _, _) = peel_to_commit(repo, oid)?;
428 if let Some(c) = commit_oid {
429 if seen.insert(c) {
430 queue.push_back(c);
431 }
432 }
433 }
434
435 while let Some(oid) = queue.pop_front() {
436 let commit = match load_commit(repo, oid) {
437 Ok(c) => c,
438 Err(_) => continue,
439 };
440 for parent in commit.parents {
441 if seen.insert(parent) {
442 queue.push_back(parent);
443 }
444 }
445 }
446
447 let mut result: Vec<ObjectId> = seen.into_iter().collect();
448 result.sort();
449 Ok(result)
450}
451
452fn shorten_refname(refname: &str, can_abbreviate: bool) -> String {
459 if can_abbreviate {
460 if let Some(rest) = refname.strip_prefix("refs/heads/") {
461 return rest.to_owned();
462 }
463 if let Some(rest) = refname.strip_prefix("refs/tags/") {
464 return rest.to_owned();
465 }
466 if let Some(rest) = refname.strip_prefix("refs/") {
467 return rest.to_owned();
468 }
469 return refname.to_owned();
470 }
471 if let Some(rest) = refname.strip_prefix("refs/heads/") {
473 return rest.to_owned();
474 }
475 if let Some(rest) = refname.strip_prefix("refs/") {
476 return rest.to_owned();
477 }
478 refname.to_owned()
479}
480
481#[derive(PartialEq, Eq)]
483enum SubpathMatch {
484 Full,
486 Sub,
488 None,
490}
491
492fn subpath_match_kind(path: &str, pattern: &str) -> SubpathMatch {
497 if glob_matches(pattern, path) {
499 return SubpathMatch::Full;
500 }
501 let mut rest = path;
503 while let Some(pos) = rest.find('/') {
504 rest = &rest[pos + 1..];
505 if glob_matches(pattern, rest) {
506 return SubpathMatch::Sub;
507 }
508 }
509 SubpathMatch::None
510}
511
512fn subpath_matches(path: &str, pattern: &str) -> bool {
514 subpath_match_kind(path, pattern) != SubpathMatch::None
515}
516
517fn glob_matches(pattern: &str, text: &str) -> bool {
521 let pat: Vec<char> = pattern.chars().collect();
522 let txt: Vec<char> = text.chars().collect();
523 glob_match_inner(&pat, &txt)
524}
525
526fn glob_match_inner(pat: &[char], txt: &[char]) -> bool {
527 match (pat.first(), txt.first()) {
528 (None, None) => true,
529 (None, Some(_)) => false,
530 (Some('*'), _) => {
531 glob_match_inner(&pat[1..], txt)
533 || (!txt.is_empty() && glob_match_inner(pat, &txt[1..]))
534 }
535 (Some('?'), Some(_)) => glob_match_inner(&pat[1..], &txt[1..]),
536 (Some('?'), None) => false,
537 (Some(p), Some(t)) => p == t && glob_match_inner(&pat[1..], &txt[1..]),
538 (Some(_), None) => false,
539 }
540}
541
542pub(crate) fn parse_signature_time(sig: &str) -> i64 {
548 let parts: Vec<&str> = sig.split_whitespace().collect();
549 if parts.len() < 2 {
550 return 0;
551 }
552 parts[parts.len().saturating_sub(2)]
553 .parse::<i64>()
554 .unwrap_or(0)
555}
556
557fn load_commit_cached<'c>(
559 repo: &Repository,
560 cache: &'c mut HashMap<ObjectId, CommitData>,
561 oid: ObjectId,
562) -> Result<&'c CommitData> {
563 if let std::collections::hash_map::Entry::Vacant(e) = cache.entry(oid) {
564 let obj = repo.odb.read(&oid)?;
565 if obj.kind != ObjectKind::Commit {
566 return Err(Error::CorruptObject(format!(
567 "object {oid} is not a commit"
568 )));
569 }
570 let commit = parse_commit(&obj.data)?;
571 e.insert(commit);
572 }
573 Ok(cache.get(&oid).unwrap())
574}
575
576fn load_commit(repo: &Repository, oid: ObjectId) -> Result<CommitData> {
578 let obj = repo.odb.read(&oid)?;
579 if obj.kind != ObjectKind::Commit {
580 return Err(Error::CorruptObject(format!(
581 "object {oid} is not a commit"
582 )));
583 }
584 parse_commit(&obj.data)
585}
586
587pub fn resolve_oid(repo: &Repository, spec: &str) -> Result<ObjectId> {
596 crate::rev_parse::resolve_revision(repo, spec)
597}
598
599pub fn lookup_name<'m>(
611 repo: &Repository,
612 name_map: &'m HashMap<ObjectId, String>,
613 oid: ObjectId,
614) -> Result<Option<&'m String>> {
615 if let Some(name) = name_map.get(&oid) {
617 return Ok(Some(name));
618 }
619
620 let obj = match repo.odb.read(&oid) {
622 Ok(o) => o,
623 Err(_) => return Ok(None),
624 };
625 if obj.kind == ObjectKind::Tag {
626 let (commit_oid, _, _) = peel_to_commit(repo, oid)?;
627 if let Some(c) = commit_oid {
628 return Ok(name_map.get(&c));
629 }
630 }
631 Ok(None)
632}
633
634pub fn annotate_line(
641 repo: &Repository,
642 name_map: &HashMap<ObjectId, String>,
643 line: &str,
644 name_only: bool,
645) -> Result<String> {
646 let mut out = String::with_capacity(line.len() + 32);
647 let chars: Vec<char> = line.chars().collect();
648 let hex_len = 40usize;
649 let mut i = 0usize;
650 let mut flush_start = 0usize;
651
652 while i + hex_len <= chars.len() {
653 let slice: String = chars[i..i + hex_len].iter().collect();
655 let after_is_hex = chars
656 .get(i + hex_len)
657 .map(|c| c.is_ascii_hexdigit())
658 .unwrap_or(false);
659 if !after_is_hex && slice.chars().all(|c| c.is_ascii_hexdigit()) {
660 if let Ok(oid) = slice.parse::<ObjectId>() {
662 if let Ok(Some(name)) = lookup_name(repo, name_map, oid) {
663 let prefix: String = chars[flush_start..i].iter().collect();
665 out.push_str(&prefix);
666
667 if name_only {
668 out.push_str(name);
669 } else {
670 out.push_str(&slice);
671 out.push_str(" (");
672 out.push_str(name);
673 out.push(')');
674 }
675 flush_start = i + hex_len;
676 i += hex_len;
677 continue;
678 }
679 }
680 }
681 i += 1;
682 }
683
684 let tail: String = chars[flush_start..].iter().collect();
686 out.push_str(&tail);
687 Ok(out)
688}
689
690#[must_use]
692pub fn abbrev_oid(oid: ObjectId, len: usize) -> String {
693 let hex = oid.to_hex();
694 let n = len.clamp(4, 40).min(hex.len());
695 hex[..n].to_owned()
696}
697
698pub use self::all_reachable_commits as walk_all_commits;
702
703pub fn object_exists(repo: &Repository, oid: ObjectId) -> bool {
705 repo.odb.exists(&oid)
706}