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