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
164pub fn all_reflog_oids_ordered(git_dir: &Path) -> Result<Vec<ObjectId>> {
173 if crate::reftable::is_reftable_repo(git_dir) {
174 let mut out = Vec::new();
176 let mut seen = HashSet::new();
177 let z = zero_oid();
178 let mut names = list_reflog_refs(git_dir).unwrap_or_default();
179 names.sort();
180 for refname in names {
181 for entry in read_reflog(git_dir, &refname).unwrap_or_default() {
182 for oid in [entry.old_oid, entry.new_oid] {
183 if oid != z && seen.insert(oid) {
184 out.push(oid);
185 }
186 }
187 }
188 }
189 return Ok(out);
190 }
191 let mut out = Vec::new();
192 let mut seen = HashSet::new();
193 let logs = git_dir.join("logs");
194 if !logs.is_dir() {
195 return Ok(out);
196 }
197 let z = zero_oid();
198 let mut names = list_reflog_refs(git_dir).unwrap_or_default();
199 names.sort();
200 for refname in names {
201 let path = reflog_path(git_dir, &refname);
202 let Ok(content) = fs::read_to_string(&path) else {
203 continue;
204 };
205 for line in content.lines() {
206 let Some(e) = parse_reflog_line(line) else {
207 continue;
208 };
209 for oid in [e.old_oid, e.new_oid] {
210 if oid != z && seen.insert(oid) {
211 out.push(oid);
212 }
213 }
214 }
215 }
216 Ok(out)
217}
218
219fn walk_reflog_files(dir: &Path, out: &mut HashSet<ObjectId>, zero: &ObjectId) -> Result<()> {
220 for entry in fs::read_dir(dir).map_err(Error::Io)? {
221 let entry = entry.map_err(Error::Io)?;
222 let path = entry.path();
223 if path.is_dir() {
224 walk_reflog_files(&path, out, zero)?;
225 } else if path.is_file() {
226 let content = fs::read_to_string(&path).map_err(Error::Io)?;
227 for line in content.lines() {
228 if let Some(e) = parse_reflog_line(line) {
229 if e.old_oid != *zero {
230 out.insert(e.old_oid);
231 }
232 if e.new_oid != *zero {
233 out.insert(e.new_oid);
234 }
235 }
236 }
237 }
238 }
239 Ok(())
240}
241
242pub fn delete_reflog_entries(git_dir: &Path, refname: &str, indices: &[usize]) -> Result<()> {
246 let mut entries = read_reflog(git_dir, refname)?;
247 if entries.is_empty() {
248 return Ok(());
249 }
250
251 entries.reverse();
254
255 let indices_set: std::collections::HashSet<usize> = indices.iter().copied().collect();
256
257 let remaining: Vec<&ReflogEntry> = entries
258 .iter()
259 .enumerate()
260 .filter(|(i, _)| !indices_set.contains(i))
261 .map(|(_, e)| e)
262 .collect();
263
264 let mut lines = Vec::new();
266 for entry in remaining.iter().rev() {
267 lines.push(format_reflog_entry(entry));
268 }
269
270 if crate::reftable::is_reftable_repo(git_dir) {
271 let kept: Vec<ReflogEntry> = remaining
272 .iter()
273 .rev()
274 .map(|entry| (*entry).clone())
275 .collect();
276 return crate::reftable::reftable_replace_reflog(git_dir, refname, &kept);
277 }
278
279 let path = reflog_path(git_dir, refname);
280 fs::write(&path, lines.join(""))?;
281 Ok(())
282}
283
284pub fn expire_reflog(git_dir: &Path, refname: &str, expire_time: Option<i64>) -> Result<usize> {
288 let entries = read_reflog(git_dir, refname)?;
289 if entries.is_empty() {
290 return Ok(0);
291 }
292
293 let mut kept = Vec::new();
294 let mut kept_entries = Vec::new();
295 let mut pruned = 0usize;
296
297 for entry in &entries {
298 let ts = parse_timestamp_from_identity(&entry.identity);
299 let dominated = match (expire_time, ts) {
300 (Some(cutoff), Some(t)) => t < cutoff,
301 (None, _) => true, (Some(_), None) => false, };
304 if dominated {
305 pruned += 1;
306 } else {
307 kept_entries.push(entry.clone());
308 kept.push(format_reflog_entry(entry));
309 }
310 }
311
312 if crate::reftable::is_reftable_repo(git_dir) {
313 crate::reftable::reftable_replace_reflog(git_dir, refname, &kept_entries)?;
314 return Ok(pruned);
315 }
316 let path = reflog_path(git_dir, refname);
317 fs::write(&path, kept.join(""))?;
318 Ok(pruned)
319}
320
321pub fn expire_reflog_unreachable(
330 repo: &Repository,
331 git_dir: &Path,
332 refname: &str,
333 cutoff: Option<i64>,
334) -> Result<usize> {
335 let Some(cutoff) = cutoff else {
336 return Ok(0);
337 };
338 if crate::reftable::is_reftable_repo(git_dir) {
339 return Ok(0);
340 }
341 let tip = match refs::resolve_ref(git_dir, refname) {
342 Ok(o) => o,
343 Err(_) => return Ok(0),
344 };
345 let ancestors = match merge_base::ancestor_closure(repo, tip) {
346 Ok(a) => a,
347 Err(_) => return Ok(0),
348 };
349
350 let entries = read_reflog(git_dir, refname)?;
351 if entries.is_empty() {
352 return Ok(0);
353 }
354
355 let path = reflog_path(git_dir, refname);
356 let mut kept = Vec::new();
357 let mut pruned = 0usize;
358
359 for entry in &entries {
360 let ts = parse_timestamp_from_identity(&entry.identity);
361 let unreachable = !entry.new_oid.is_zero() && !ancestors.contains(&entry.new_oid);
362 let should_prune = unreachable && matches!(ts, Some(t) if t < cutoff);
363 if should_prune {
364 pruned += 1;
365 } else {
366 kept.push(format_reflog_entry(entry));
367 }
368 }
369
370 fs::write(&path, kept.join(""))?;
371 Ok(pruned)
372}
373
374fn format_reflog_entry(entry: &ReflogEntry) -> String {
376 if entry.message.is_empty() {
377 format!("{} {} {}\n", entry.old_oid, entry.new_oid, entry.identity)
378 } else {
379 format!(
380 "{} {} {}\t{}\n",
381 entry.old_oid, entry.new_oid, entry.identity, entry.message
382 )
383 }
384}
385
386fn parse_timestamp_from_identity(identity: &str) -> Option<i64> {
390 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
392 if parts.len() >= 2 {
393 parts[1].parse::<i64>().ok()
394 } else {
395 None
396 }
397}
398
399pub fn mirror_branch_reflog_to_head(git_dir: &Path, branch_refname: &str) -> Result<()> {
402 if crate::reftable::is_reftable_repo(git_dir) {
403 return Ok(());
404 }
405 let src = reflog_path(git_dir, branch_refname);
406 if !src.is_file() {
407 return Ok(());
408 }
409 let content = fs::read_to_string(&src).map_err(Error::Io)?;
410 let dst = reflog_path(git_dir, "HEAD");
411 if let Some(parent) = dst.parent() {
412 fs::create_dir_all(parent).map_err(Error::Io)?;
413 }
414 fs::write(&dst, content).map_err(Error::Io)?;
415 Ok(())
416}
417
418pub fn list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
420 if crate::reftable::is_reftable_repo(git_dir) {
421 return crate::reftable::reftable_list_reflog_refs(git_dir);
422 }
423 let mut refs = Vec::new();
424 let mut seen = HashSet::new();
425
426 fn collect_from_logs_root(
427 logs_dir: &Path,
428 out: &mut Vec<String>,
429 seen: &mut HashSet<String>,
430 skip_per_worktree_refs: bool,
431 ) -> Result<()> {
432 if logs_dir.join("HEAD").is_file() && seen.insert("HEAD".to_string()) {
433 out.push("HEAD".to_string());
434 }
435 let refs_logs = logs_dir.join("refs");
436 if refs_logs.is_dir() {
437 collect_reflog_refs(&refs_logs, "refs", out, seen, skip_per_worktree_refs)?;
438 }
439 Ok(())
440 }
441
442 collect_from_logs_root(&git_dir.join("logs"), &mut refs, &mut seen, false)?;
443 if let Some(common) = refs::common_dir(git_dir) {
444 if common != git_dir {
445 collect_from_logs_root(&common.join("logs"), &mut refs, &mut seen, true)?;
446 }
447 }
448
449 Ok(refs)
450}
451
452fn collect_reflog_refs(
453 dir: &Path,
454 prefix: &str,
455 out: &mut Vec<String>,
456 seen: &mut HashSet<String>,
457 skip_per_worktree_refs: bool,
458) -> Result<()> {
459 let read_dir = match fs::read_dir(dir) {
460 Ok(rd) => rd,
461 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
462 Err(e) => return Err(Error::Io(e)),
463 };
464
465 for entry in read_dir {
466 let entry = entry.map_err(Error::Io)?;
467 let name = entry.file_name().to_string_lossy().to_string();
468 let full_name = format!("{prefix}/{name}");
469 if skip_per_worktree_refs && crate::worktree_ref::is_per_worktree_ref(&full_name) {
470 continue;
471 }
472 let ft = entry.file_type().map_err(Error::Io)?;
473 if ft.is_dir() {
474 collect_reflog_refs(&entry.path(), &full_name, out, seen, skip_per_worktree_refs)?;
475 } else if ft.is_file() && seen.insert(full_name.clone()) {
476 out.push(full_name);
477 }
478 }
479 Ok(())
480}
481
482#[derive(Debug, Clone)]
486pub struct ReflogExpireParams {
487 pub stale_fix: bool,
489 pub dry_run: bool,
490 pub verbose: bool,
491}
492
493#[derive(Debug, Clone)]
495pub struct GcReflogPattern {
496 pattern: String,
497 expire_total: Option<i64>,
498 expire_unreachable: Option<i64>,
499}
500
501fn collect_gc_reflog_patterns(config: &ConfigSet, now: i64) -> Vec<GcReflogPattern> {
502 let mut by_pattern: HashMap<String, GcReflogPattern> = HashMap::new();
503 for e in config.entries() {
504 let key = e.key.as_str();
505 let Some(rest) = key.strip_prefix("gc.") else {
506 continue;
507 };
508 let lower = rest.to_ascii_lowercase();
511 let (pat, is_total) = if lower.ends_with(".reflogexpireunreachable") {
512 (
513 &rest[..rest.len() - ".reflogexpireunreachable".len()],
514 false,
515 )
516 } else if lower.ends_with(".reflogexpire") {
517 (&rest[..rest.len() - ".reflogexpire".len()], true)
518 } else {
519 continue;
520 };
521 if pat.is_empty() {
522 continue;
523 }
524 let Some(val) = e.value.as_deref() else {
525 continue;
526 };
527 let Ok(ts) = parse_gc_reflog_expiry(val, now) else {
528 continue;
529 };
530 let ent = by_pattern
531 .entry(pat.to_string())
532 .or_insert(GcReflogPattern {
533 pattern: pat.to_string(),
534 expire_total: None,
535 expire_unreachable: None,
536 });
537 if is_total {
538 ent.expire_total = Some(ts);
539 } else {
540 ent.expire_unreachable = Some(ts);
541 }
542 }
543 by_pattern.into_values().collect()
544}
545
546fn global_gc_reflog_expiry(config: &ConfigSet, now: i64) -> (Option<i64>, Option<i64>) {
547 let total = config
548 .get("gc.reflogExpire")
549 .and_then(|v| parse_gc_reflog_expiry(&v, now).ok());
550 let unreach = config
551 .get("gc.reflogExpireUnreachable")
552 .and_then(|v| parse_gc_reflog_expiry(&v, now).ok());
553 (total, unreach)
554}
555
556fn parse_gc_reflog_expiry(raw: &str, now: i64) -> Result<i64> {
558 let s = raw.trim();
559 if s.eq_ignore_ascii_case("never") || s.eq_ignore_ascii_case("false") {
560 return Ok(0);
561 }
562 if s.eq_ignore_ascii_case("now") || s.eq_ignore_ascii_case("all") {
563 return Ok(i64::MAX);
564 }
565 if let Ok(days) = s.parse::<u64>() {
566 if days == 0 {
567 return Ok(0);
568 }
569 return Ok(now - (days as i64 * 86400));
570 }
571 s.parse::<i64>()
572 .map_err(|_| Error::Message(format!("invalid reflog expiry: {raw:?}")))
573}
574
575fn default_expire_total(now: i64) -> i64 {
576 now - 30 * 86400
577}
578
579fn default_expire_unreachable(now: i64) -> i64 {
580 now - 90 * 86400
581}
582
583fn resolve_expire_for_ref(
584 refname: &str,
585 explicit_total: Option<i64>,
586 explicit_unreachable: Option<i64>,
587 patterns: &[GcReflogPattern],
588 default_total: i64,
589 default_unreachable: i64,
590) -> (i64, i64) {
591 let mut expire_total = explicit_total.unwrap_or(default_total);
592 let mut expire_unreachable = explicit_unreachable.unwrap_or(default_unreachable);
593 if explicit_total.is_some() && explicit_unreachable.is_some() {
594 return (expire_total, expire_unreachable);
595 }
596 for ent in patterns {
597 let wildcard_prefix_matches = ent
598 .pattern
599 .split_once('*')
600 .is_some_and(|(prefix, _)| refname.starts_with(prefix));
601 if wildmatch(ent.pattern.as_bytes(), refname.as_bytes(), WM_PATHNAME)
602 || wildmatch(ent.pattern.as_bytes(), refname.as_bytes(), 0)
603 || wildcard_prefix_matches
604 {
605 if explicit_total.is_none() {
607 if let Some(total) = ent.expire_total {
608 expire_total = total;
609 }
610 }
611 if explicit_unreachable.is_none() {
612 if let Some(unreachable) = ent.expire_unreachable {
613 expire_unreachable = unreachable;
614 }
615 }
616 return (expire_total, expire_unreachable);
617 }
618 }
619 if refname == "refs/stash" {
620 if explicit_total.is_none() {
621 expire_total = 0;
622 }
623 if explicit_unreachable.is_none() {
624 expire_unreachable = 0;
625 }
626 }
627 (expire_total, expire_unreachable)
628}
629
630fn tree_fully_complete(repo: &Repository, oid: ObjectId, depth: usize) -> bool {
631 if depth > 65536 {
632 return false;
633 }
634 let Ok(obj) = repo.odb.read(&oid) else {
635 return false;
636 };
637 match obj.kind {
638 ObjectKind::Blob => true,
639 ObjectKind::Tree => {
640 let Ok(entries) = parse_tree(&obj.data) else {
641 return false;
642 };
643 for e in entries {
644 if !tree_fully_complete(repo, e.oid, depth + 1) {
645 return false;
646 }
647 }
648 true
649 }
650 _ => false,
651 }
652}
653
654fn commit_chain_complete(repo: &Repository, oid: ObjectId, depth: usize) -> bool {
655 if oid.is_zero() {
656 return true;
657 }
658 if depth > 65536 {
659 return false;
660 }
661 let Ok(obj) = repo.odb.read(&oid) else {
662 return false;
663 };
664 if obj.kind != ObjectKind::Commit {
665 return false;
666 }
667 let Ok(c) = parse_commit(&obj.data) else {
668 return false;
669 };
670 if !tree_fully_complete(repo, c.tree, depth + 1) {
671 return false;
672 }
673 for p in &c.parents {
674 if !commit_chain_complete(repo, *p, depth + 1) {
675 return false;
676 }
677 }
678 true
679}
680
681#[derive(Debug, Clone, Copy, PartialEq, Eq)]
682enum UnreachableKind {
683 Always,
684 Normal,
685 Head,
686}
687
688fn is_head_ref(refname: &str) -> bool {
689 refname == "HEAD" || refname.ends_with("/HEAD")
690}
691
692fn tip_commits_for_reflog(repo: &Repository, git_dir: &Path, refname: &str) -> Vec<ObjectId> {
693 let mut tips = Vec::new();
694 if is_head_ref(refname) {
695 if let Ok(oid) = refs::resolve_ref(git_dir, "HEAD") {
696 tips.push(oid);
697 }
698 if let Ok(refs) = refs::list_refs(git_dir, "refs/") {
699 for (_, oid) in refs {
700 tips.push(oid);
701 }
702 }
703 } else if let Ok(oid) = refs::resolve_ref(git_dir, refname) {
704 tips.push(oid);
705 }
706 tips.sort();
707 tips.dedup();
708 tips.retain(|o| commit_chain_complete(repo, *o, 0));
709 tips
710}
711
712fn reachable_commit_set(repo: &Repository, tips: &[ObjectId]) -> HashSet<ObjectId> {
713 let mut acc = HashSet::new();
714 for t in tips {
715 if let Ok(cl) = merge_base::ancestor_closure(repo, *t) {
716 acc.extend(cl);
717 }
718 }
719 acc
720}
721
722fn is_unreachable_oid(
723 repo: &Repository,
724 reachable: &HashSet<ObjectId>,
725 kind: UnreachableKind,
726 oid: ObjectId,
727) -> bool {
728 if oid.is_zero() {
729 return false;
730 }
731 if reachable.contains(&oid) {
732 return false;
733 }
734 if kind == UnreachableKind::Always {
735 return true;
736 }
737 let Ok(obj) = repo.odb.read(&oid) else {
738 return true;
739 };
740 obj.kind == ObjectKind::Commit
741}
742
743fn should_drop_reflog_entry(
744 repo: &Repository,
745 entry: &ReflogEntry,
746 expire_total: i64,
747 expire_unreachable: i64,
748 unreachable_kind: UnreachableKind,
749 reachable: &HashSet<ObjectId>,
750 stale_fix: bool,
751) -> bool {
752 let ts = parse_timestamp_from_identity(&entry.identity).unwrap_or(i64::MAX);
753 if expire_total > 0 && ts < expire_total {
754 return true;
755 }
756 if stale_fix
757 && (!commit_chain_complete(repo, entry.old_oid, 0)
758 || !commit_chain_complete(repo, entry.new_oid, 0))
759 {
760 return true;
761 }
762 if expire_unreachable > 0 && ts < expire_unreachable {
763 match unreachable_kind {
764 UnreachableKind::Always => return true,
765 UnreachableKind::Normal | UnreachableKind::Head => {
766 if is_unreachable_oid(repo, reachable, unreachable_kind, entry.old_oid)
767 || is_unreachable_oid(repo, reachable, unreachable_kind, entry.new_oid)
768 {
769 return true;
770 }
771 }
772 }
773 }
774 false
775}
776
777pub fn expire_reflog_git(
779 repo: &Repository,
780 git_dir: &Path,
781 refname: &str,
782 params: &ReflogExpireParams,
783 explicit_total: Option<i64>,
784 explicit_unreachable: Option<i64>,
785 gc_patterns: &[GcReflogPattern],
786 gc_global_total: Option<i64>,
787 gc_global_unreachable: Option<i64>,
788 now: i64,
789) -> Result<usize> {
790 let is_reftable = crate::reftable::is_reftable_repo(git_dir);
791 let base_total = gc_global_total.unwrap_or_else(|| default_expire_total(now));
792 let base_unreachable = gc_global_unreachable.unwrap_or_else(|| default_expire_unreachable(now));
793 let (expire_total, expire_unreachable) = resolve_expire_for_ref(
794 refname,
795 explicit_total,
796 explicit_unreachable,
797 gc_patterns,
798 base_total,
799 base_unreachable,
800 );
801
802 let unreachable_kind = if expire_unreachable <= expire_total {
803 UnreachableKind::Always
804 } else if expire_unreachable == 0 || is_head_ref(refname) {
805 UnreachableKind::Head
806 } else {
807 match refs::resolve_ref(git_dir, refname) {
808 Ok(t) if commit_chain_complete(repo, t, 0) => UnreachableKind::Normal,
809 _ => UnreachableKind::Always,
810 }
811 };
812
813 let tips = tip_commits_for_reflog(repo, git_dir, refname);
814 let reachable = if matches!(unreachable_kind, UnreachableKind::Always) {
815 HashSet::new()
816 } else {
817 reachable_commit_set(repo, &tips)
818 };
819
820 let entries = read_reflog(git_dir, refname)?;
821 if entries.is_empty() {
822 return Ok(0);
823 }
824 let mut kept = Vec::new();
825 let mut kept_entries = Vec::new();
826 let mut pruned = 0usize;
827
828 for entry in &entries {
829 let drop = should_drop_reflog_entry(
830 repo,
831 entry,
832 expire_total,
833 expire_unreachable,
834 unreachable_kind,
835 &reachable,
836 params.stale_fix,
837 );
838 if drop {
839 pruned += 1;
840 if params.verbose {
841 if params.dry_run {
842 println!("would prune {}", entry.message);
843 } else {
844 println!("prune {}", entry.message);
845 }
846 }
847 } else {
848 if params.verbose {
849 println!("keep {}", entry.message);
850 }
851 kept_entries.push(entry.clone());
852 kept.push(format_reflog_entry(entry));
853 }
854 }
855
856 if !params.dry_run && pruned > 0 {
857 if is_reftable {
858 crate::reftable::reftable_replace_reflog(git_dir, refname, &kept_entries)?;
859 } else {
860 let path = reflog_path(git_dir, refname);
866 fs::write(&path, kept.join(""))?;
867 adjust_reflog_shared_perm(git_dir, &path);
868 }
869 }
870 Ok(pruned)
871}
872
873#[derive(Debug, Clone)]
875pub struct GcReflogExpireConfig {
876 pub patterns: Vec<GcReflogPattern>,
877 pub global_total: Option<i64>,
878 pub global_unreachable: Option<i64>,
879}
880
881#[must_use]
883pub fn load_gc_reflog_expire_config(config: &ConfigSet, now: i64) -> GcReflogExpireConfig {
884 let (global_total, global_unreachable) = global_gc_reflog_expiry(config, now);
885 GcReflogExpireConfig {
886 patterns: collect_gc_reflog_patterns(config, now),
887 global_total,
888 global_unreachable,
889 }
890}
891
892pub fn mark_stalefix_reachable(repo: &Repository, git_dir: &Path) -> Result<HashSet<ObjectId>> {
894 let mut seeds: Vec<ObjectId> = Vec::new();
895 if let Ok(oid) = refs::resolve_ref(git_dir, "HEAD") {
896 seeds.push(oid);
897 }
898 if let Ok(refs) = refs::list_refs(git_dir, "refs/") {
899 for (_, oid) in refs {
900 seeds.push(oid);
901 }
902 }
903 if let Ok(names) = list_reflog_refs(git_dir) {
904 for r in names {
905 if let Ok(ent) = read_reflog(git_dir, &r) {
906 for e in ent {
907 if !e.old_oid.is_zero() {
908 seeds.push(e.old_oid);
909 }
910 if !e.new_oid.is_zero() {
911 seeds.push(e.new_oid);
912 }
913 }
914 }
915 }
916 }
917 seeds.sort();
918 seeds.dedup();
919
920 let mut seen = HashSet::new();
921 let mut queue: std::collections::VecDeque<ObjectId> = seeds.into_iter().collect();
922 while let Some(oid) = queue.pop_front() {
923 if oid.is_zero() || !seen.insert(oid) {
924 continue;
925 }
926 let Ok(obj) = repo.odb.read(&oid) else {
927 continue;
928 };
929 match obj.kind {
930 ObjectKind::Commit => {
931 if let Ok(c) = parse_commit(&obj.data) {
932 queue.push_back(c.tree);
933 for p in c.parents {
934 queue.push_back(p);
935 }
936 }
937 }
938 ObjectKind::Tree => {
939 if let Ok(entries) = parse_tree(&obj.data) {
940 for te in entries {
941 queue.push_back(te.oid);
942 }
943 }
944 }
945 ObjectKind::Tag | ObjectKind::Blob => {}
946 }
947 }
948 Ok(seen)
949}