1use std::collections::{HashMap, HashSet};
12use std::fs;
13use std::io;
14use std::path::{Path, PathBuf};
15
16use crate::config::ConfigSet;
17use crate::diff::zero_oid;
18use crate::error::{Error, Result};
19use crate::merge_base;
20use crate::objects::{parse_commit, parse_tree, ObjectId, ObjectKind};
21use crate::refs::{self, reflog_file_path};
22use crate::repo::Repository;
23use crate::wildmatch::{wildmatch, WM_PATHNAME};
24
25#[derive(Debug, Clone)]
27pub struct ReflogEntry {
28 pub old_oid: ObjectId,
30 pub new_oid: ObjectId,
32 pub identity: String,
34 pub message: String,
36}
37
38pub fn reflog_path(git_dir: &Path, refname: &str) -> PathBuf {
43 reflog_file_path(git_dir, refname)
44}
45
46pub fn reflog_exists(git_dir: &Path, refname: &str) -> bool {
48 if crate::reftable::is_reftable_repo(git_dir) {
49 return crate::reftable::reftable_reflog_exists(git_dir, refname);
50 }
51 let path = reflog_path(git_dir, refname);
52 path.is_file()
53}
54
55pub fn read_reflog_dwim(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
60 let mut entries = read_reflog(git_dir, refname)?;
61 if !entries.is_empty() {
62 return Ok(entries);
63 }
64 if !refname.starts_with("refs/") {
65 entries = read_reflog(git_dir, &format!("refs/{refname}"))?;
66 if !entries.is_empty() {
67 return Ok(entries);
68 }
69 entries = read_reflog(git_dir, &format!("refs/heads/{refname}"))?;
70 }
71 Ok(entries)
72}
73
74pub fn read_reflog(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
78 if crate::reftable::is_reftable_repo(git_dir) {
79 return crate::reftable::reftable_read_reflog(git_dir, refname);
80 }
81 let path = reflog_path(git_dir, refname);
82 let content = match fs::read_to_string(&path) {
83 Ok(c) => c,
84 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
85 Err(e) => return Err(Error::Io(e)),
86 };
87
88 let mut entries = Vec::new();
89 for line in content.lines() {
90 if line.is_empty() {
91 continue;
92 }
93 if let Some(entry) = parse_reflog_line(line) {
94 entries.push(entry);
95 }
96 }
97 Ok(entries)
98}
99
100fn parse_reflog_line(line: &str) -> Option<ReflogEntry> {
104 let (before_tab, message) = if let Some(pos) = line.find('\t') {
106 (&line[..pos], line[pos + 1..].to_string())
107 } else {
108 (line, String::new())
109 };
110
111 if before_tab.len() < 83 {
113 return None;
115 }
116
117 let old_hex = &before_tab[..40];
118 let new_hex = &before_tab[41..81];
119 let identity = before_tab[82..].to_string();
120
121 let old_oid = old_hex.parse::<ObjectId>().ok()?;
122 let new_oid = new_hex.parse::<ObjectId>().ok()?;
123
124 Some(ReflogEntry {
125 old_oid,
126 new_oid,
127 identity,
128 message,
129 })
130}
131
132pub fn all_reflog_oids(git_dir: &Path) -> Result<HashSet<ObjectId>> {
136 if crate::reftable::is_reftable_repo(git_dir) {
137 return Ok(HashSet::new());
138 }
139 let mut out = HashSet::new();
140 let logs = git_dir.join("logs");
141 if !logs.is_dir() {
142 return Ok(out);
143 }
144 let z = zero_oid();
145 walk_reflog_files(&logs, &mut out, &z)?;
146 Ok(out)
147}
148
149fn walk_reflog_files(dir: &Path, out: &mut HashSet<ObjectId>, zero: &ObjectId) -> Result<()> {
150 for entry in fs::read_dir(dir).map_err(Error::Io)? {
151 let entry = entry.map_err(Error::Io)?;
152 let path = entry.path();
153 if path.is_dir() {
154 walk_reflog_files(&path, out, zero)?;
155 } else if path.is_file() {
156 let content = fs::read_to_string(&path).map_err(Error::Io)?;
157 for line in content.lines() {
158 if let Some(e) = parse_reflog_line(line) {
159 if e.old_oid != *zero {
160 out.insert(e.old_oid);
161 }
162 if e.new_oid != *zero {
163 out.insert(e.new_oid);
164 }
165 }
166 }
167 }
168 }
169 Ok(())
170}
171
172pub fn delete_reflog_entries(git_dir: &Path, refname: &str, indices: &[usize]) -> Result<()> {
176 let mut entries = read_reflog(git_dir, refname)?;
177 if entries.is_empty() {
178 return Ok(());
179 }
180
181 entries.reverse();
184
185 let indices_set: std::collections::HashSet<usize> = indices.iter().copied().collect();
186
187 let remaining: Vec<&ReflogEntry> = entries
188 .iter()
189 .enumerate()
190 .filter(|(i, _)| !indices_set.contains(i))
191 .map(|(_, e)| e)
192 .collect();
193
194 let mut lines = Vec::new();
196 for entry in remaining.iter().rev() {
197 lines.push(format_reflog_entry(entry));
198 }
199
200 if crate::reftable::is_reftable_repo(git_dir) {
201 let kept: Vec<ReflogEntry> = remaining
202 .iter()
203 .rev()
204 .map(|entry| (*entry).clone())
205 .collect();
206 return crate::reftable::reftable_replace_reflog(git_dir, refname, &kept);
207 }
208
209 let path = reflog_path(git_dir, refname);
210 fs::write(&path, lines.join(""))?;
211 Ok(())
212}
213
214pub fn expire_reflog(git_dir: &Path, refname: &str, expire_time: Option<i64>) -> Result<usize> {
218 let entries = read_reflog(git_dir, refname)?;
219 if entries.is_empty() {
220 return Ok(0);
221 }
222
223 let mut kept = Vec::new();
224 let mut kept_entries = Vec::new();
225 let mut pruned = 0usize;
226
227 for entry in &entries {
228 let ts = parse_timestamp_from_identity(&entry.identity);
229 let dominated = match (expire_time, ts) {
230 (Some(cutoff), Some(t)) => t < cutoff,
231 (None, _) => true, (Some(_), None) => false, };
234 if dominated {
235 pruned += 1;
236 } else {
237 kept_entries.push(entry.clone());
238 kept.push(format_reflog_entry(entry));
239 }
240 }
241
242 if crate::reftable::is_reftable_repo(git_dir) {
243 crate::reftable::reftable_replace_reflog(git_dir, refname, &kept_entries)?;
244 return Ok(pruned);
245 }
246 let path = reflog_path(git_dir, refname);
247 fs::write(&path, kept.join(""))?;
248 Ok(pruned)
249}
250
251pub fn expire_reflog_unreachable(
260 repo: &Repository,
261 git_dir: &Path,
262 refname: &str,
263 cutoff: Option<i64>,
264) -> Result<usize> {
265 let Some(cutoff) = cutoff else {
266 return Ok(0);
267 };
268 if crate::reftable::is_reftable_repo(git_dir) {
269 return Ok(0);
270 }
271 let tip = match refs::resolve_ref(git_dir, refname) {
272 Ok(o) => o,
273 Err(_) => return Ok(0),
274 };
275 let ancestors = match merge_base::ancestor_closure(repo, tip) {
276 Ok(a) => a,
277 Err(_) => return Ok(0),
278 };
279
280 let entries = read_reflog(git_dir, refname)?;
281 if entries.is_empty() {
282 return Ok(0);
283 }
284
285 let path = reflog_path(git_dir, refname);
286 let mut kept = Vec::new();
287 let mut pruned = 0usize;
288
289 for entry in &entries {
290 let ts = parse_timestamp_from_identity(&entry.identity);
291 let unreachable = !entry.new_oid.is_zero() && !ancestors.contains(&entry.new_oid);
292 let should_prune = unreachable && matches!(ts, Some(t) if t < cutoff);
293 if should_prune {
294 pruned += 1;
295 } else {
296 kept.push(format_reflog_entry(entry));
297 }
298 }
299
300 fs::write(&path, kept.join(""))?;
301 Ok(pruned)
302}
303
304fn format_reflog_entry(entry: &ReflogEntry) -> String {
306 if entry.message.is_empty() {
307 format!("{} {} {}\n", entry.old_oid, entry.new_oid, entry.identity)
308 } else {
309 format!(
310 "{} {} {}\t{}\n",
311 entry.old_oid, entry.new_oid, entry.identity, entry.message
312 )
313 }
314}
315
316fn parse_timestamp_from_identity(identity: &str) -> Option<i64> {
320 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
322 if parts.len() >= 2 {
323 parts[1].parse::<i64>().ok()
324 } else {
325 None
326 }
327}
328
329pub fn mirror_branch_reflog_to_head(git_dir: &Path, branch_refname: &str) -> Result<()> {
332 if crate::reftable::is_reftable_repo(git_dir) {
333 return Ok(());
334 }
335 let src = reflog_path(git_dir, branch_refname);
336 if !src.is_file() {
337 return Ok(());
338 }
339 let content = fs::read_to_string(&src).map_err(Error::Io)?;
340 let dst = reflog_path(git_dir, "HEAD");
341 if let Some(parent) = dst.parent() {
342 fs::create_dir_all(parent).map_err(Error::Io)?;
343 }
344 fs::write(&dst, content).map_err(Error::Io)?;
345 Ok(())
346}
347
348pub fn list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
350 if crate::reftable::is_reftable_repo(git_dir) {
351 return crate::reftable::reftable_list_reflog_refs(git_dir);
352 }
353 let mut refs = Vec::new();
354 let mut seen = HashSet::new();
355
356 fn collect_from_logs_root(
357 logs_dir: &Path,
358 out: &mut Vec<String>,
359 seen: &mut HashSet<String>,
360 ) -> Result<()> {
361 if logs_dir.join("HEAD").is_file() && seen.insert("HEAD".to_string()) {
362 out.push("HEAD".to_string());
363 }
364 let refs_logs = logs_dir.join("refs");
365 if refs_logs.is_dir() {
366 collect_reflog_refs(&refs_logs, "refs", out, seen)?;
367 }
368 Ok(())
369 }
370
371 collect_from_logs_root(&git_dir.join("logs"), &mut refs, &mut seen)?;
372 if let Some(common) = refs::common_dir(git_dir) {
373 if common != git_dir {
374 collect_from_logs_root(&common.join("logs"), &mut refs, &mut seen)?;
375 }
376 }
377
378 Ok(refs)
379}
380
381fn collect_reflog_refs(
382 dir: &Path,
383 prefix: &str,
384 out: &mut Vec<String>,
385 seen: &mut HashSet<String>,
386) -> Result<()> {
387 let read_dir = match fs::read_dir(dir) {
388 Ok(rd) => rd,
389 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
390 Err(e) => return Err(Error::Io(e)),
391 };
392
393 for entry in read_dir {
394 let entry = entry.map_err(Error::Io)?;
395 let name = entry.file_name().to_string_lossy().to_string();
396 let full_name = format!("{prefix}/{name}");
397 let ft = entry.file_type().map_err(Error::Io)?;
398 if ft.is_dir() {
399 collect_reflog_refs(&entry.path(), &full_name, out, seen)?;
400 } else if ft.is_file() && seen.insert(full_name.clone()) {
401 out.push(full_name);
402 }
403 }
404 Ok(())
405}
406
407#[derive(Debug, Clone)]
411pub struct ReflogExpireParams {
412 pub stale_fix: bool,
414 pub dry_run: bool,
415 pub verbose: bool,
416}
417
418#[derive(Debug, Clone)]
420pub struct GcReflogPattern {
421 pattern: String,
422 expire_total: i64,
423 expire_unreachable: i64,
424}
425
426fn collect_gc_reflog_patterns(config: &ConfigSet, now: i64) -> Vec<GcReflogPattern> {
427 let mut by_pattern: HashMap<String, GcReflogPattern> = HashMap::new();
428 for e in config.entries() {
429 let key = e.key.as_str();
430 let Some(rest) = key.strip_prefix("gc.") else {
431 continue;
432 };
433 let Some((pat, suffix)) = rest.rsplit_once('.') else {
436 continue;
437 };
438 if !suffix.eq_ignore_ascii_case("reflogexpire")
439 && !suffix.eq_ignore_ascii_case("reflogexpireunreachable")
440 {
441 continue;
442 }
443 let Some(val) = e.value.as_deref() else {
444 continue;
445 };
446 let Ok(ts) = parse_gc_reflog_expiry(val, now) else {
447 continue;
448 };
449 let ent = by_pattern
450 .entry(pat.to_string())
451 .or_insert(GcReflogPattern {
452 pattern: pat.to_string(),
453 expire_total: i64::MAX,
454 expire_unreachable: i64::MAX,
455 });
456 if suffix.eq_ignore_ascii_case("reflogexpire") {
457 ent.expire_total = ts;
458 } else {
459 ent.expire_unreachable = ts;
460 }
461 }
462 by_pattern.into_values().collect()
463}
464
465fn global_gc_reflog_expiry(config: &ConfigSet, now: i64) -> (Option<i64>, Option<i64>) {
466 let total = config
467 .get("gc.reflogExpire")
468 .and_then(|v| parse_gc_reflog_expiry(&v, now).ok());
469 let unreach = config
470 .get("gc.reflogExpireUnreachable")
471 .and_then(|v| parse_gc_reflog_expiry(&v, now).ok());
472 (total, unreach)
473}
474
475fn parse_gc_reflog_expiry(raw: &str, now: i64) -> Result<i64> {
477 let s = raw.trim();
478 if s.eq_ignore_ascii_case("never") || s.eq_ignore_ascii_case("false") {
479 return Ok(0);
480 }
481 if let Ok(days) = s.parse::<u64>() {
482 if days == 0 {
483 return Ok(0);
484 }
485 return Ok(now - (days as i64 * 86400));
486 }
487 s.parse::<i64>()
488 .map_err(|_| Error::Message(format!("invalid reflog expiry: {raw:?}")))
489}
490
491fn default_expire_total(now: i64) -> i64 {
492 now - 30 * 86400
493}
494
495fn default_expire_unreachable(now: i64) -> i64 {
496 now - 90 * 86400
497}
498
499fn resolve_expire_for_ref(
500 refname: &str,
501 explicit_total: Option<i64>,
502 explicit_unreachable: Option<i64>,
503 patterns: &[GcReflogPattern],
504 default_total: i64,
505 default_unreachable: i64,
506) -> (i64, i64) {
507 let mut expire_total = explicit_total.unwrap_or(default_total);
508 let mut expire_unreachable = explicit_unreachable.unwrap_or(default_unreachable);
509 if explicit_total.is_some() && explicit_unreachable.is_some() {
510 return (expire_total, expire_unreachable);
511 }
512 for ent in patterns {
513 if wildmatch(ent.pattern.as_bytes(), refname.as_bytes(), WM_PATHNAME) {
514 if explicit_total.is_none() && ent.expire_total != i64::MAX {
516 expire_total = ent.expire_total;
517 }
518 if explicit_unreachable.is_none() && ent.expire_unreachable != i64::MAX {
519 expire_unreachable = ent.expire_unreachable;
520 }
521 return (expire_total, expire_unreachable);
522 }
523 }
524 if refname == "refs/stash" {
525 if explicit_total.is_none() {
526 expire_total = 0;
527 }
528 if explicit_unreachable.is_none() {
529 expire_unreachable = 0;
530 }
531 }
532 (expire_total, expire_unreachable)
533}
534
535fn tree_fully_complete(repo: &Repository, oid: ObjectId, depth: usize) -> bool {
536 if depth > 65536 {
537 return false;
538 }
539 let Ok(obj) = repo.odb.read(&oid) else {
540 return false;
541 };
542 match obj.kind {
543 ObjectKind::Blob => true,
544 ObjectKind::Tree => {
545 let Ok(entries) = parse_tree(&obj.data) else {
546 return false;
547 };
548 for e in entries {
549 if !tree_fully_complete(repo, e.oid, depth + 1) {
550 return false;
551 }
552 }
553 true
554 }
555 _ => false,
556 }
557}
558
559fn commit_chain_complete(repo: &Repository, oid: ObjectId, depth: usize) -> bool {
560 if oid.is_zero() {
561 return true;
562 }
563 if depth > 65536 {
564 return false;
565 }
566 let Ok(obj) = repo.odb.read(&oid) else {
567 return false;
568 };
569 if obj.kind != ObjectKind::Commit {
570 return false;
571 }
572 let Ok(c) = parse_commit(&obj.data) else {
573 return false;
574 };
575 if !tree_fully_complete(repo, c.tree, depth + 1) {
576 return false;
577 }
578 for p in &c.parents {
579 if !commit_chain_complete(repo, *p, depth + 1) {
580 return false;
581 }
582 }
583 true
584}
585
586#[derive(Debug, Clone, Copy, PartialEq, Eq)]
587enum UnreachableKind {
588 Always,
589 Normal,
590 Head,
591}
592
593fn is_head_ref(refname: &str) -> bool {
594 refname == "HEAD" || refname.ends_with("/HEAD")
595}
596
597fn tip_commits_for_reflog(repo: &Repository, git_dir: &Path, refname: &str) -> Vec<ObjectId> {
598 let mut tips = Vec::new();
599 if is_head_ref(refname) {
600 if let Ok(oid) = refs::resolve_ref(git_dir, "HEAD") {
601 tips.push(oid);
602 }
603 if let Ok(refs) = refs::list_refs(git_dir, "refs/") {
604 for (_, oid) in refs {
605 tips.push(oid);
606 }
607 }
608 } else if let Ok(oid) = refs::resolve_ref(git_dir, refname) {
609 tips.push(oid);
610 }
611 tips.sort();
612 tips.dedup();
613 tips.retain(|o| commit_chain_complete(repo, *o, 0));
614 tips
615}
616
617fn reachable_commit_set(repo: &Repository, tips: &[ObjectId]) -> HashSet<ObjectId> {
618 let mut acc = HashSet::new();
619 for t in tips {
620 if let Ok(cl) = merge_base::ancestor_closure(repo, *t) {
621 acc.extend(cl);
622 }
623 }
624 acc
625}
626
627fn is_unreachable_oid(
628 repo: &Repository,
629 reachable: &HashSet<ObjectId>,
630 kind: UnreachableKind,
631 oid: ObjectId,
632) -> bool {
633 if oid.is_zero() {
634 return false;
635 }
636 if reachable.contains(&oid) {
637 return false;
638 }
639 if kind == UnreachableKind::Always {
640 return true;
641 }
642 let Ok(obj) = repo.odb.read(&oid) else {
643 return true;
644 };
645 obj.kind == ObjectKind::Commit
646}
647
648fn should_drop_reflog_entry(
649 repo: &Repository,
650 entry: &ReflogEntry,
651 expire_total: i64,
652 expire_unreachable: i64,
653 unreachable_kind: UnreachableKind,
654 reachable: &HashSet<ObjectId>,
655 stale_fix: bool,
656) -> bool {
657 let ts = parse_timestamp_from_identity(&entry.identity).unwrap_or(i64::MAX);
658 if expire_total > 0 && ts < expire_total {
659 return true;
660 }
661 if stale_fix
662 && (!commit_chain_complete(repo, entry.old_oid, 0)
663 || !commit_chain_complete(repo, entry.new_oid, 0))
664 {
665 return true;
666 }
667 if expire_unreachable > 0 && ts < expire_unreachable {
668 match unreachable_kind {
669 UnreachableKind::Always => return true,
670 UnreachableKind::Normal | UnreachableKind::Head => {
671 if is_unreachable_oid(repo, reachable, unreachable_kind, entry.old_oid)
672 || is_unreachable_oid(repo, reachable, unreachable_kind, entry.new_oid)
673 {
674 return true;
675 }
676 }
677 }
678 }
679 false
680}
681
682pub fn expire_reflog_git(
684 repo: &Repository,
685 git_dir: &Path,
686 refname: &str,
687 params: &ReflogExpireParams,
688 explicit_total: Option<i64>,
689 explicit_unreachable: Option<i64>,
690 gc_patterns: &[GcReflogPattern],
691 gc_global_total: Option<i64>,
692 gc_global_unreachable: Option<i64>,
693 now: i64,
694) -> Result<usize> {
695 let is_reftable = crate::reftable::is_reftable_repo(git_dir);
696 let base_total = gc_global_total.unwrap_or_else(|| default_expire_total(now));
697 let base_unreachable = gc_global_unreachable.unwrap_or_else(|| default_expire_unreachable(now));
698 let (expire_total, expire_unreachable) = resolve_expire_for_ref(
699 refname,
700 explicit_total,
701 explicit_unreachable,
702 gc_patterns,
703 base_total,
704 base_unreachable,
705 );
706
707 let unreachable_kind = if expire_unreachable <= expire_total {
708 UnreachableKind::Always
709 } else if expire_unreachable == 0 || is_head_ref(refname) {
710 UnreachableKind::Head
711 } else {
712 match refs::resolve_ref(git_dir, refname) {
713 Ok(t) if commit_chain_complete(repo, t, 0) => UnreachableKind::Normal,
714 _ => UnreachableKind::Always,
715 }
716 };
717
718 let tips = tip_commits_for_reflog(repo, git_dir, refname);
719 let reachable = if matches!(unreachable_kind, UnreachableKind::Always) {
720 HashSet::new()
721 } else {
722 reachable_commit_set(repo, &tips)
723 };
724
725 let entries = read_reflog(git_dir, refname)?;
726 if entries.is_empty() {
727 return Ok(0);
728 }
729 let mut kept = Vec::new();
730 let mut kept_entries = Vec::new();
731 let mut pruned = 0usize;
732
733 for entry in &entries {
734 let drop = should_drop_reflog_entry(
735 repo,
736 entry,
737 expire_total,
738 expire_unreachable,
739 unreachable_kind,
740 &reachable,
741 params.stale_fix,
742 );
743 if drop {
744 pruned += 1;
745 if params.verbose {
746 if params.dry_run {
747 println!("would prune {}", entry.message);
748 } else {
749 println!("prune {}", entry.message);
750 }
751 }
752 } else {
753 if params.verbose {
754 println!("keep {}", entry.message);
755 }
756 kept_entries.push(entry.clone());
757 kept.push(format_reflog_entry(entry));
758 }
759 }
760
761 if !params.dry_run && pruned > 0 {
762 if is_reftable {
763 crate::reftable::reftable_replace_reflog(git_dir, refname, &kept_entries)?;
764 } else if kept.is_empty() {
765 let path = reflog_path(git_dir, refname);
766 let _ = fs::remove_file(&path);
767 } else {
768 let path = reflog_path(git_dir, refname);
769 fs::write(&path, kept.join(""))?;
770 }
771 }
772 Ok(pruned)
773}
774
775#[derive(Debug, Clone)]
777pub struct GcReflogExpireConfig {
778 pub patterns: Vec<GcReflogPattern>,
779 pub global_total: Option<i64>,
780 pub global_unreachable: Option<i64>,
781}
782
783#[must_use]
785pub fn load_gc_reflog_expire_config(config: &ConfigSet, now: i64) -> GcReflogExpireConfig {
786 let (global_total, global_unreachable) = global_gc_reflog_expiry(config, now);
787 GcReflogExpireConfig {
788 patterns: collect_gc_reflog_patterns(config, now),
789 global_total,
790 global_unreachable,
791 }
792}
793
794pub fn mark_stalefix_reachable(repo: &Repository, git_dir: &Path) -> Result<HashSet<ObjectId>> {
796 let mut seeds: Vec<ObjectId> = Vec::new();
797 if let Ok(oid) = refs::resolve_ref(git_dir, "HEAD") {
798 seeds.push(oid);
799 }
800 if let Ok(refs) = refs::list_refs(git_dir, "refs/") {
801 for (_, oid) in refs {
802 seeds.push(oid);
803 }
804 }
805 if let Ok(names) = list_reflog_refs(git_dir) {
806 for r in names {
807 if let Ok(ent) = read_reflog(git_dir, &r) {
808 for e in ent {
809 if !e.old_oid.is_zero() {
810 seeds.push(e.old_oid);
811 }
812 if !e.new_oid.is_zero() {
813 seeds.push(e.new_oid);
814 }
815 }
816 }
817 }
818 }
819 seeds.sort();
820 seeds.dedup();
821
822 let mut seen = HashSet::new();
823 let mut queue: std::collections::VecDeque<ObjectId> = seeds.into_iter().collect();
824 while let Some(oid) = queue.pop_front() {
825 if oid.is_zero() || !seen.insert(oid) {
826 continue;
827 }
828 let Ok(obj) = repo.odb.read(&oid) else {
829 continue;
830 };
831 match obj.kind {
832 ObjectKind::Commit => {
833 if let Ok(c) = parse_commit(&obj.data) {
834 queue.push_back(c.tree);
835 for p in c.parents {
836 queue.push_back(p);
837 }
838 }
839 }
840 ObjectKind::Tree => {
841 if let Ok(entries) = parse_tree(&obj.data) {
842 for te in entries {
843 queue.push_back(te.oid);
844 }
845 }
846 }
847 ObjectKind::Tag | ObjectKind::Blob => {}
848 }
849 }
850 Ok(seen)
851}