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