1use std::cell::{OnceCell, RefCell};
12use std::collections::{HashMap, HashSet};
13use std::fs;
14use std::fs::File;
15use std::io;
16use std::io::{Read, Seek, SeekFrom};
17use std::ops::ControlFlow;
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21use crate::commit_graph_file::CommitGraphChain;
22use crate::config::ConfigSet;
23use crate::error::{Error, Result};
24use crate::ident::Signature;
25use crate::index::MODE_TREE;
26use crate::merge_base;
27use crate::objects::{parse_commit, parse_tree, CommitRef, ObjectId, ObjectKind, OidHashMap};
28use crate::refs::store::{RefStore as _, RefStoreError};
29use crate::refs::{self, reflog_file_path};
30use crate::repo::Repository;
31use crate::wildmatch::{wildmatch, WM_PATHNAME};
32
33mod selector;
34pub use selector::{parse_reflog_ref, ReflogSelector};
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ReflogEntry {
39 pub old_oid: ObjectId,
41 pub new_oid: ObjectId,
43 pub identity: String,
45 pub message: String,
47}
48
49impl ReflogEntry {
50 #[must_use]
54 pub fn signature(&self) -> Option<Signature> {
55 crate::ident::parse_signature(&self.identity)
56 }
57
58 #[must_use]
64 pub fn action(&self) -> Option<&str> {
65 self.message.split_once(": ").map(|(prefix, _)| prefix)
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct MessageMatcher {
72 needle: String,
73}
74
75impl MessageMatcher {
76 #[must_use]
78 pub fn new(pattern: impl Into<String>) -> Self {
79 Self {
80 needle: pattern.into(),
81 }
82 }
83
84 #[must_use]
86 pub fn matches(&self, message: &str) -> bool {
87 message.contains(&self.needle)
88 }
89}
90
91pub fn reflog_path(git_dir: &Path, refname: &str) -> PathBuf {
96 reflog_file_path(git_dir, refname)
97}
98
99pub fn truncate_last_reflog_line(git_dir: &Path, refname: &str) -> Result<()> {
107 if crate::refs::store::open_ref_store(git_dir)?.format()
108 != crate::refs::store::RefStorageFormat::Files
109 {
110 return Ok(());
111 }
112 let path = reflog_path(git_dir, refname);
113 let Ok(mut content) = fs::read_to_string(&path) else {
114 return Ok(());
115 };
116 if content.is_empty() {
117 return Ok(());
118 }
119 if content.ends_with('\n') {
120 content.pop();
121 if content.ends_with('\r') {
122 content.pop();
123 }
124 }
125 if let Some(pos) = content.rfind('\n') {
126 content.truncate(pos + 1);
127 } else {
128 content.clear();
129 }
130 if content.is_empty() {
131 let _ = fs::remove_file(&path);
132 } else {
133 fs::write(&path, content)?;
134 }
135 Ok(())
136}
137
138fn adjust_reflog_shared_perm(git_dir: &Path, path: &Path) {
141 let Ok(config) = ConfigSet::load(
142 &crate::environment::Environment::empty(),
143 Some(git_dir),
144 true,
145 ) else {
146 return;
147 };
148 let raw = config.get("core.sharedRepository");
149 let Ok(perm) = crate::shared_repo::shared_repository_from_config_value(raw.as_deref()) else {
150 return;
151 };
152 if perm != 0 {
153 let _ = crate::shared_repo::adjust_shared_perm_path(perm, path);
154 }
155}
156
157pub fn reflog_exists(git_dir: &Path, refname: &str) -> bool {
159 if let Ok(store) = crate::refs::store::open_ref_store(git_dir) {
160 if store.format() != crate::refs::store::RefStorageFormat::Files {
161 return store.reflog_exists(refname).unwrap_or(false);
162 }
163 }
164 let path = reflog_path(git_dir, refname);
165 path.is_file()
166}
167
168pub fn read_reflog_dwim(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
173 let mut entries = read_reflog(git_dir, refname)?;
174 if !entries.is_empty() {
175 return Ok(entries);
176 }
177 if !refname.starts_with("refs/") {
178 entries = read_reflog(git_dir, &format!("refs/{refname}"))?;
179 if !entries.is_empty() {
180 return Ok(entries);
181 }
182 entries = read_reflog(git_dir, &format!("refs/heads/{refname}"))?;
183 }
184 Ok(entries)
185}
186
187pub fn read_reflog(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
191 if crate::refs::store::open_ref_store(git_dir)?.format()
192 != crate::refs::store::RefStorageFormat::Files
193 {
194 let store = crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?;
195 let mut entries = Vec::new();
196 store.for_each_reflog_entry(refname, false, &mut |entry| {
197 entries.push(entry.clone());
198 std::ops::ControlFlow::Continue(())
199 })?;
200 return Ok(entries);
201 }
202 let path = reflog_path(git_dir, refname);
203 let content = match fs::read_to_string(&path) {
204 Ok(c) => c,
205 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
206 Err(e) => return Err(Error::Io(e)),
207 };
208
209 let mut entries = Vec::new();
210 for line in content.lines() {
211 if line.is_empty() {
212 continue;
213 }
214 if let Some(entry) = parse_reflog_line(line) {
215 entries.push(entry);
216 }
217 }
218 Ok(entries)
219}
220
221pub(crate) fn reflog_entry_count(git_dir: &Path, refname: &str) -> Result<usize> {
225 if crate::refs::store::open_ref_store(git_dir)?.format()
226 != crate::refs::store::RefStorageFormat::Files
227 {
228 let store = crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?;
229 let mut count = 0usize;
230 store.for_each_reflog_entry(refname, false, &mut |_| {
231 count += 1;
232 std::ops::ControlFlow::Continue(())
233 })?;
234 return Ok(count);
235 }
236 let path = reflog_path(git_dir, refname);
237 let content = match fs::read_to_string(&path) {
238 Ok(c) => c,
239 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(0),
240 Err(e) => return Err(Error::Io(e)),
241 };
242 Ok(content.lines().filter(|line| !line.is_empty()).count())
243}
244
245const REFLOG_REVERSE_READ_CHUNK: usize = 64 * 1024;
246
247pub(crate) struct ReverseReflogLineReader {
249 file: File,
250 loaded_from: u64,
251 buffer: Vec<u8>,
252}
253
254impl ReverseReflogLineReader {
255 pub fn open(path: &Path) -> Result<Option<Self>> {
262 let file = match File::open(path) {
263 Ok(f) => f,
264 Err(e)
265 if matches!(
266 e.kind(),
267 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
268 ) =>
269 {
270 return Ok(None)
271 }
272 Err(e) => return Err(Error::Io(e)),
273 };
274 let meta = file.metadata().map_err(Error::Io)?;
275 let len = meta.len();
276 if !meta.is_file() || len == 0 {
277 return Ok(None);
278 }
279 Ok(Some(Self {
280 file,
281 loaded_from: len,
282 buffer: Vec::new(),
283 }))
284 }
285
286 pub fn next_nonempty_line(&mut self) -> Result<Option<Vec<u8>>> {
292 loop {
293 if let Some(line) = take_newest_line(&mut self.buffer)? {
294 if line.is_empty() || line == b"\r" {
295 continue;
296 }
297 return Ok(Some(line));
298 }
299 if self.loaded_from == 0 {
300 if self.buffer.is_empty() {
301 return Ok(None);
302 }
303 let mut line = std::mem::take(&mut self.buffer);
304 while line.last() == Some(&b'\r') {
305 line.pop();
306 }
307 if line.is_empty() {
308 return Ok(None);
309 }
310 return Ok(Some(line));
311 }
312 let read_end = self.loaded_from;
313 let read_start = read_end.saturating_sub(REFLOG_REVERSE_READ_CHUNK as u64);
314 let read_len = (read_end - read_start) as usize;
315 self.file
316 .seek(SeekFrom::Start(read_start))
317 .map_err(Error::Io)?;
318 let mut chunk = vec![0u8; read_len];
319 self.file.read_exact(&mut chunk).map_err(Error::Io)?;
320 self.loaded_from = read_start;
321 let mut merged = chunk;
322 merged.append(&mut self.buffer);
323 self.buffer = merged;
324 }
325 }
326}
327
328fn take_newest_line(buffer: &mut Vec<u8>) -> Result<Option<Vec<u8>>> {
329 loop {
330 if buffer.is_empty() {
331 return Ok(None);
332 }
333 let Some(newline_at) = buffer.iter().rposition(|&b| b == b'\n') else {
334 return Ok(None);
335 };
336 let mut line = buffer.split_off(newline_at + 1);
337 buffer.truncate(newline_at);
338 while line.last() == Some(&b'\r') {
339 line.pop();
340 }
341 if line.is_empty() {
342 continue;
343 }
344 return Ok(Some(line));
345 }
346}
347
348pub(crate) fn parse_reflog_line_for_store(line: &str) -> Option<ReflogEntry> {
353 parse_reflog_line(line)
354}
355
356fn parse_reflog_line(line: &str) -> Option<ReflogEntry> {
357 let (before_tab, message) = if let Some(pos) = line.find('\t') {
359 (&line[..pos], line[pos + 1..].to_string())
360 } else {
361 (line, String::new())
362 };
363
364 let mut parts = before_tab.splitn(3, ' ');
368 let old_hex = parts.next()?;
369 let new_hex = parts.next()?;
370 let identity = parts.next()?.to_string();
371
372 let old_oid = old_hex.parse::<ObjectId>().ok()?;
373 let new_oid = new_hex.parse::<ObjectId>().ok()?;
374
375 Some(ReflogEntry {
376 old_oid,
377 new_oid,
378 identity,
379 message,
380 })
381}
382
383pub fn all_reflog_oids(git_dir: &Path) -> Result<HashSet<ObjectId>> {
387 if crate::refs::store::open_ref_store(git_dir)?.format()
388 != crate::refs::store::RefStorageFormat::Files
389 {
390 return Ok(all_reflog_oids_ordered(git_dir)?.into_iter().collect());
391 }
392 let mut out = HashSet::new();
393 let logs = git_dir.join("logs");
394 if !logs.is_dir() {
395 return Ok(out);
396 }
397 walk_reflog_files(&logs, &mut out)?;
398 Ok(out)
399}
400
401pub fn all_reflog_oids_ordered(git_dir: &Path) -> Result<Vec<ObjectId>> {
410 if crate::refs::store::open_ref_store(git_dir)?.format()
411 != crate::refs::store::RefStorageFormat::Files
412 {
413 let mut out = Vec::new();
415 let mut seen = HashSet::new();
416 let mut names = list_reflog_refs(git_dir).unwrap_or_default();
417 names.sort();
418 for refname in names {
419 for entry in read_reflog(git_dir, &refname).unwrap_or_default() {
420 for oid in [entry.old_oid, entry.new_oid] {
421 if !oid.is_zero() && seen.insert(oid) {
422 out.push(oid);
423 }
424 }
425 }
426 }
427 return Ok(out);
428 }
429 let mut out = Vec::new();
430 let mut seen = HashSet::new();
431 let logs = git_dir.join("logs");
432 if !logs.is_dir() {
433 return Ok(out);
434 }
435 let mut names = list_reflog_refs(git_dir).unwrap_or_default();
436 names.sort();
437 for refname in names {
438 let path = reflog_path(git_dir, &refname);
439 let Ok(content) = fs::read_to_string(&path) else {
440 continue;
441 };
442 for line in content.lines() {
443 let Some(e) = parse_reflog_line(line) else {
444 continue;
445 };
446 for oid in [e.old_oid, e.new_oid] {
447 if !oid.is_zero() && seen.insert(oid) {
448 out.push(oid);
449 }
450 }
451 }
452 }
453 Ok(out)
454}
455
456fn walk_reflog_files(dir: &Path, out: &mut HashSet<ObjectId>) -> Result<()> {
457 for entry in fs::read_dir(dir).map_err(Error::Io)? {
458 let entry = entry.map_err(Error::Io)?;
459 let path = entry.path();
460 if path.is_dir() {
461 walk_reflog_files(&path, out)?;
462 } else if path.is_file() {
463 let content = fs::read_to_string(&path).map_err(Error::Io)?;
464 for line in content.lines() {
465 if let Some(e) = parse_reflog_line(line) {
466 if !e.old_oid.is_zero() {
467 out.insert(e.old_oid);
468 }
469 if !e.new_oid.is_zero() {
470 out.insert(e.new_oid);
471 }
472 }
473 }
474 }
475 }
476 Ok(())
477}
478
479pub fn delete_reflog_entries(git_dir: &Path, refname: &str, indices: &[usize]) -> Result<()> {
484 let store = crate::refs::store::open_ref_store(git_dir)?;
485 delete_reflog_entries_on_store(store.as_ref(), refname, indices)
486}
487
488fn filter_reflog_without_indices(
491 entries: Vec<ReflogEntry>,
492 indices: &[usize],
493) -> Option<(Vec<ReflogEntry>, bool)> {
494 if entries.is_empty() {
495 return None;
496 }
497 let len = entries.len();
498 let indices_set: HashSet<usize> = indices.iter().copied().collect();
499 let removed_oldest = indices_set.contains(&(len - 1));
500 let kept = entries
501 .into_iter()
502 .enumerate()
503 .filter(|(oldest_first, _)| !indices_set.contains(&(len - 1 - oldest_first)))
504 .map(|(_, e)| e)
505 .collect();
506 Some((kept, removed_oldest))
507}
508
509fn rechain_reflog_after_delete(
510 mut kept: Vec<ReflogEntry>,
511 removed_oldest: bool,
512) -> Vec<ReflogEntry> {
513 if removed_oldest {
514 if let Some(first) = kept.first_mut() {
515 first.old_oid = ObjectId::zero();
516 }
517 }
518 for i in 1..kept.len() {
519 kept[i].old_oid = kept[i - 1].new_oid;
520 }
521 kept
522}
523
524pub(crate) fn delete_reflog_entries_on_store(
527 store: &dyn crate::refs::store::RefStore,
528 refname: &str,
529 indices: &[usize],
530) -> Result<()> {
531 let indices: Vec<usize> = indices.to_vec();
532 store.rewrite_reflog(refname, &mut move |current| {
533 Ok(filter_reflog_without_indices(current, &indices)
534 .map(|(kept, _)| kept)
535 .unwrap_or_default())
536 })
537}
538
539pub(crate) fn delete_reflog_entries_rechain_on_store(
542 store: &dyn crate::refs::store::RefStore,
543 refname: &str,
544 indices: &[usize],
545) -> Result<()> {
546 let indices: Vec<usize> = indices.to_vec();
547 store.rewrite_reflog(refname, &mut move |current| {
548 Ok(filter_reflog_without_indices(current, &indices)
549 .map(|(kept, removed_oldest)| rechain_reflog_after_delete(kept, removed_oldest))
550 .unwrap_or_default())
551 })
552}
553
554pub(crate) fn read_reflog_on_store(
560 store: &dyn crate::refs::store::RefStore,
561 refname: &str,
562) -> Result<Vec<ReflogEntry>> {
563 if let Some(files) = store.as_files() {
564 return files.read_reflog_entries(refname);
565 }
566 let mut entries = Vec::new();
567 store.for_each_reflog_entry(refname, false, &mut |entry| {
568 entries.push(entry.clone());
569 std::ops::ControlFlow::Continue(())
570 })?;
571 Ok(entries)
572}
573
574pub fn expire_reflog(git_dir: &Path, refname: &str, expire_time: Option<i64>) -> Result<usize> {
578 let store = crate::refs::store::open_ref_store(git_dir)?;
579 let mut pruned = 0usize;
580 store.rewrite_reflog(refname, &mut |current| {
581 if current.is_empty() {
582 return Ok(current);
583 }
584 let mut kept = Vec::with_capacity(current.len());
585 for entry in current {
586 let ts = parse_timestamp_from_identity(&entry.identity);
587 let dominated = match (expire_time, ts) {
588 (Some(cutoff), Some(t)) => t < cutoff,
589 (None, _) => true,
590 (Some(_), None) => false,
591 };
592 if dominated {
593 pruned += 1;
594 } else {
595 kept.push(entry);
596 }
597 }
598 Ok(kept)
599 })?;
600 Ok(pruned)
601}
602
603pub fn expire_reflog_unreachable(
611 repo: &Repository,
612 _git_dir: &Path,
613 refname: &str,
614 cutoff: Option<i64>,
615) -> Result<usize> {
616 let Some(cutoff) = cutoff else {
617 return Ok(0);
618 };
619 let tip = match refs::resolve_ref_in(repo, refname) {
620 Ok(o) => o,
621 Err(_) => return Ok(0),
622 };
623 let ancestors = match merge_base::ancestor_closure(repo, tip) {
624 Ok(a) => a,
625 Err(_) => return Ok(0),
626 };
627
628 let store = repo.refs();
629 let snapshot = read_reflog_on_store(store, refname)?;
630 if snapshot.is_empty() {
631 return Ok(0);
632 }
633
634 let drop_flags: Vec<bool> = snapshot
635 .iter()
636 .map(|entry| {
637 let ts = parse_timestamp_from_identity(&entry.identity);
638 let unreachable = !entry.new_oid.is_zero() && !ancestors.contains(&entry.new_oid);
639 unreachable && matches!(ts, Some(t) if t < cutoff)
640 })
641 .collect();
642 let pruned = drop_flags.iter().filter(|&&drop| drop).count();
643 if pruned == 0 {
644 return Ok(0);
645 }
646 rewrite_reflog_from_snapshot(store, refname, &snapshot, &drop_flags)?;
647 Ok(pruned)
648}
649
650#[cfg_attr(not(test), allow(dead_code))]
652fn format_reflog_entry(entry: &ReflogEntry) -> String {
653 if entry.message.is_empty() {
654 format!("{} {} {}\n", entry.old_oid, entry.new_oid, entry.identity)
655 } else {
656 format!(
657 "{} {} {}\t{}\n",
658 entry.old_oid, entry.new_oid, entry.identity, entry.message
659 )
660 }
661}
662
663pub fn reflog_identity_timestamp(identity: &str) -> Option<i64> {
668 parse_timestamp_from_identity(identity)
669}
670
671fn parse_timestamp_from_identity(identity: &str) -> Option<i64> {
672 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
674 if parts.len() >= 2 {
675 parts[1].parse::<i64>().ok()
676 } else {
677 None
678 }
679}
680
681pub fn mirror_branch_reflog_to_head(git_dir: &Path, branch_refname: &str) -> Result<()> {
684 if crate::refs::store::open_ref_store(git_dir)?.format()
685 != crate::refs::store::RefStorageFormat::Files
686 {
687 return Ok(());
688 }
689 let src = reflog_path(git_dir, branch_refname);
690 if !src.is_file() {
691 return Ok(());
692 }
693 let content = fs::read_to_string(&src).map_err(Error::Io)?;
694 let dst = reflog_path(git_dir, "HEAD");
695 if let Some(parent) = dst.parent() {
696 fs::create_dir_all(parent).map_err(Error::Io)?;
697 }
698 fs::write(&dst, content).map_err(Error::Io)?;
699 Ok(())
700}
701
702pub(crate) fn reflog_refs_on_store(
708 store: &dyn crate::refs::store::RefStore,
709) -> Result<Vec<String>> {
710 let mut names = Vec::new();
711 store.for_each_reflog_ref(&mut |name| {
712 names.push(name.to_owned());
713 ControlFlow::Continue(())
714 })?;
715 names.sort();
716 names.dedup();
717 Ok(names)
718}
719
720pub fn list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
722 if crate::refs::store::open_ref_store(git_dir)?.format()
723 != crate::refs::store::RefStorageFormat::Files
724 {
725 let store = crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?;
726 let mut refs = Vec::new();
727 store.for_each_reflog_ref(&mut |name| {
728 refs.push(name.to_owned());
729 std::ops::ControlFlow::Continue(())
730 })?;
731 refs.sort();
732 return Ok(refs);
733 }
734 let mut refs = Vec::new();
735 let mut seen = HashSet::new();
736
737 fn collect_from_logs_root(
738 logs_dir: &Path,
739 out: &mut Vec<String>,
740 seen: &mut HashSet<String>,
741 skip_per_worktree_refs: bool,
742 ) -> Result<()> {
743 if logs_dir.join("HEAD").is_file() && seen.insert("HEAD".to_string()) {
744 out.push("HEAD".to_string());
745 }
746 let refs_logs = crate::refs::store::paths::logs_refs_from_logs_root(logs_dir);
747 if refs_logs.is_dir() {
748 collect_reflog_refs(&refs_logs, "refs", out, seen, skip_per_worktree_refs)?;
749 }
750 Ok(())
751 }
752
753 collect_from_logs_root(
754 &crate::refs::store::paths::logs_dir_path(git_dir),
755 &mut refs,
756 &mut seen,
757 false,
758 )?;
759 if let Some(common) = refs::common_dir(git_dir) {
760 if common != git_dir {
761 collect_from_logs_root(
762 &crate::refs::store::paths::logs_dir_path(&common),
763 &mut refs,
764 &mut seen,
765 true,
766 )?;
767 }
768 }
769
770 Ok(refs)
771}
772
773fn collect_reflog_refs(
774 dir: &Path,
775 prefix: &str,
776 out: &mut Vec<String>,
777 seen: &mut HashSet<String>,
778 skip_per_worktree_refs: bool,
779) -> Result<()> {
780 let read_dir = match fs::read_dir(dir) {
781 Ok(rd) => rd,
782 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
783 Err(e) => return Err(Error::Io(e)),
784 };
785
786 for entry in read_dir {
787 let entry = entry.map_err(Error::Io)?;
788 let name = entry.file_name().to_string_lossy().to_string();
789 let full_name = format!("{prefix}/{name}");
790 if skip_per_worktree_refs && crate::worktree_ref::is_per_worktree_ref(&full_name) {
791 continue;
792 }
793 let ft = entry.file_type().map_err(Error::Io)?;
794 if ft.is_dir() {
795 collect_reflog_refs(&entry.path(), &full_name, out, seen, skip_per_worktree_refs)?;
796 } else if ft.is_file() && seen.insert(full_name.clone()) {
797 out.push(full_name);
798 }
799 }
800 Ok(())
801}
802
803#[derive(Debug, Clone)]
807pub struct ReflogExpireParams {
808 pub stale_fix: bool,
811 pub dry_run: bool,
813 pub verbose: bool,
815}
816
817#[derive(Debug, Clone, Copy, PartialEq, Eq)]
819pub enum ReflogExpireActionKind {
820 Keep,
822 Prune,
824 WouldPrune,
826}
827
828#[derive(Debug, Clone, PartialEq, Eq)]
830pub struct ReflogExpireAction {
831 pub refname: String,
833 pub entry: ReflogEntry,
835 pub action: ReflogExpireActionKind,
837}
838
839#[derive(Debug, Clone, PartialEq, Eq)]
841pub struct ReflogExpireResult {
842 pub pruned: usize,
844 pub actions: Vec<ReflogExpireAction>,
846}
847
848#[derive(Debug, Clone)]
850pub struct GcReflogPattern {
851 pattern: String,
852 expire_total: Option<i64>,
853 expire_unreachable: Option<i64>,
854}
855
856fn collect_gc_reflog_patterns(config: &ConfigSet, now: i64) -> Vec<GcReflogPattern> {
857 let mut by_pattern: HashMap<String, GcReflogPattern> = HashMap::new();
858 let mut pattern_order: Vec<String> = Vec::new();
859 for e in config.entries() {
860 let key = e.key.as_str();
861 let Some(rest) = key.strip_prefix("gc.") else {
862 continue;
863 };
864 let lower = rest.to_ascii_lowercase();
867 let (pat, is_total) = if lower.ends_with(".reflogexpireunreachable") {
868 (
869 &rest[..rest.len() - ".reflogexpireunreachable".len()],
870 false,
871 )
872 } else if lower.ends_with(".reflogexpire") {
873 (&rest[..rest.len() - ".reflogexpire".len()], true)
874 } else {
875 continue;
876 };
877 if pat.is_empty() {
878 continue;
879 }
880 let Some(val) = e.value.as_deref() else {
881 continue;
882 };
883 let Ok(ts) = parse_reflog_expire_spec(val, now) else {
884 continue;
885 };
886 let pat_owned = pat.to_string();
887 if !by_pattern.contains_key(&pat_owned) {
888 pattern_order.push(pat_owned.clone());
889 }
890 let ent = by_pattern.entry(pat_owned).or_insert(GcReflogPattern {
891 pattern: pat.to_string(),
892 expire_total: None,
893 expire_unreachable: None,
894 });
895 if is_total {
896 ent.expire_total = Some(ts);
897 } else {
898 ent.expire_unreachable = Some(ts);
899 }
900 }
901 pattern_order
902 .into_iter()
903 .filter_map(|p| by_pattern.remove(&p))
904 .collect()
905}
906
907fn global_gc_reflog_expiry(config: &ConfigSet, now: i64) -> (Option<i64>, Option<i64>) {
908 let total = config
909 .get("gc.reflogExpire")
910 .and_then(|v| parse_reflog_expire_spec(&v, now).ok());
911 let unreach = config
912 .get("gc.reflogExpireUnreachable")
913 .and_then(|v| parse_reflog_expire_spec(&v, now).ok());
914 (total, unreach)
915}
916
917pub fn parse_reflog_expire_spec(raw: &str, now: i64) -> Result<i64> {
919 let s = raw.trim();
920 if s.eq_ignore_ascii_case("never") || s.eq_ignore_ascii_case("false") {
921 return Ok(0);
922 }
923 if s.eq_ignore_ascii_case("now") || s.eq_ignore_ascii_case("all") {
924 return Ok(i64::MAX);
925 }
926 crate::git_date::parse_date_cutoff(s, now)
927 .map_err(|_| Error::Message(format!("invalid reflog expiry: {raw:?}")))
928}
929
930const REFLOG_DEFAULT_EXPIRE_REACHABLE_DAYS: i64 = 90;
932const REFLOG_DEFAULT_EXPIRE_UNREACHABLE_DAYS: i64 = 30;
934
935fn default_expire_cutoff(now: i64, days: i64) -> i64 {
936 now.saturating_sub(time::Duration::days(days).whole_seconds())
937}
938
939fn default_expire_total(now: i64) -> i64 {
940 default_expire_cutoff(now, REFLOG_DEFAULT_EXPIRE_REACHABLE_DAYS)
941}
942
943fn default_expire_unreachable(now: i64) -> i64 {
944 default_expire_cutoff(now, REFLOG_DEFAULT_EXPIRE_UNREACHABLE_DAYS)
945}
946
947fn resolve_expire_for_ref(
948 refname: &str,
949 explicit_total: Option<i64>,
950 explicit_unreachable: Option<i64>,
951 patterns: &[GcReflogPattern],
952 default_total: i64,
953 default_unreachable: i64,
954) -> (i64, i64) {
955 let mut expire_total = explicit_total.unwrap_or(default_total);
956 let mut expire_unreachable = explicit_unreachable.unwrap_or(default_unreachable);
957 if explicit_total.is_some() && explicit_unreachable.is_some() {
958 return (expire_total, expire_unreachable);
959 }
960 for ent in patterns {
961 if wildmatch(ent.pattern.as_bytes(), refname.as_bytes(), WM_PATHNAME)
962 || wildmatch(ent.pattern.as_bytes(), refname.as_bytes(), 0)
963 {
964 if explicit_total.is_none() {
966 if let Some(total) = ent.expire_total {
967 expire_total = total;
968 }
969 }
970 if explicit_unreachable.is_none() {
971 if let Some(unreachable) = ent.expire_unreachable {
972 expire_unreachable = unreachable;
973 }
974 }
975 return (expire_total, expire_unreachable);
976 }
977 }
978 if refname == "refs/stash" {
979 if explicit_total.is_none() {
980 expire_total = 0;
981 }
982 if explicit_unreachable.is_none() {
983 expire_unreachable = 0;
984 }
985 }
986 (expire_total, expire_unreachable)
987}
988
989#[derive(Debug, Default)]
995pub(crate) struct ReflogExpiryCache {
996 promised: OnceCell<HashSet<ObjectId>>,
997 commits: RefCell<CommitLinks>,
999 complete: RefCell<HashMap<ObjectId, bool>>,
1000}
1001
1002#[derive(Debug, Default)]
1005struct CommitLinks {
1006 index: OidHashMap<u32>,
1007 nodes: Vec<Linkage>,
1008}
1009
1010#[derive(Debug)]
1011enum Linkage {
1012 Unread(ObjectId),
1013 Absent,
1015 Read {
1016 time: i64,
1017 parents: Box<[u32]>,
1018 },
1019}
1020
1021impl CommitLinks {
1022 fn intern(&mut self, oid: ObjectId) -> u32 {
1023 let next = u32::try_from(self.nodes.len()).unwrap_or(u32::MAX);
1024 let node = *self.index.entry(oid).or_insert(next);
1025 if node == next {
1026 self.nodes.push(Linkage::Unread(oid));
1027 }
1028 node
1029 }
1030
1031 fn lookup(&self, oid: &ObjectId) -> Option<u32> {
1032 self.index.get(oid).copied()
1033 }
1034
1035 fn len(&self) -> usize {
1036 self.nodes.len()
1037 }
1038
1039 fn linkage(
1042 &mut self,
1043 node: u32,
1044 read: impl FnOnce(ObjectId) -> Option<(Vec<ObjectId>, i64)>,
1045 ) -> Option<(i64, &[u32])> {
1046 let idx = node as usize;
1047 if let Linkage::Unread(oid) = self.nodes[idx] {
1048 self.nodes[idx] = match read(oid) {
1049 Some((parents, time)) => Linkage::Read {
1050 time,
1051 parents: parents.into_iter().map(|p| self.intern(p)).collect(),
1052 },
1053 None => Linkage::Absent,
1054 };
1055 }
1056 match &self.nodes[idx] {
1057 Linkage::Read { time, parents } => Some((*time, parents)),
1058 Linkage::Unread(_) | Linkage::Absent => None,
1059 }
1060 }
1061}
1062
1063impl ReflogExpiryCache {
1064 fn promised(&self, repo: &Repository) -> Result<&HashSet<ObjectId>> {
1067 if let Some(promised) = self.promised.get() {
1068 return Ok(promised);
1069 }
1070 let promised = crate::promisor::maintenance_promise_boundary(repo.odb())?;
1071 Ok(self.promised.get_or_init(|| promised))
1072 }
1073
1074 fn commit_complete(
1081 &self,
1082 repo: &Repository,
1083 promised: &HashSet<ObjectId>,
1084 root: ObjectId,
1085 ) -> bool {
1086 enum Step {
1087 Enter(ObjectId, ObjectKind),
1088 Finish(ObjectId, Vec<ObjectId>),
1089 }
1090 let is_boundary = |oid: &ObjectId| oid.is_zero() || promised.contains(oid);
1091 if is_boundary(&root) {
1092 return true;
1093 }
1094 let mut memo = self.complete.borrow_mut();
1095 let mut stack = vec![Step::Enter(root, ObjectKind::Commit)];
1096 while let Some(step) = stack.pop() {
1097 match step {
1098 Step::Enter(oid, kind) => {
1099 if is_boundary(&oid) || memo.contains_key(&oid) {
1100 continue;
1101 }
1102 memo.insert(oid, false);
1105 let Some(children) = object_children(repo, promised, oid, kind) else {
1106 continue;
1107 };
1108 let next = children
1109 .iter()
1110 .map(|&(child, kind)| Step::Enter(child, kind))
1111 .collect::<Vec<_>>();
1112 stack.push(Step::Finish(
1113 oid,
1114 children.into_iter().map(|(child, _)| child).collect(),
1115 ));
1116 stack.extend(next);
1117 }
1118 Step::Finish(oid, children) => {
1119 let complete = children
1120 .iter()
1121 .all(|child| is_boundary(child) || memo.get(child) == Some(&true));
1122 memo.insert(oid, complete);
1123 }
1124 }
1125 }
1126 memo.get(&root) == Some(&true)
1127 }
1128}
1129
1130fn object_children(
1135 repo: &Repository,
1136 promised: &HashSet<ObjectId>,
1137 oid: ObjectId,
1138 kind: ObjectKind,
1139) -> Option<Vec<(ObjectId, ObjectKind)>> {
1140 let obj = repo.odb().try_read_once(&oid).ok()??;
1141 if obj.kind != kind {
1142 return None;
1143 }
1144 match kind {
1145 ObjectKind::Commit => {
1146 let commit = parse_commit(&obj.data).ok()?;
1147 Some(
1148 std::iter::once((commit.tree, ObjectKind::Tree))
1149 .chain(commit.parents.into_iter().map(|p| (p, ObjectKind::Commit)))
1150 .collect(),
1151 )
1152 }
1153 ObjectKind::Tree => {
1154 let mut subtrees = Vec::new();
1155 for entry in parse_tree(&obj.data).ok()? {
1156 if entry.mode == MODE_TREE {
1157 subtrees.push((entry.oid, ObjectKind::Tree));
1158 } else if !promised.contains(&entry.oid) && !repo.odb().exists(&entry.oid) {
1159 return None;
1160 }
1161 }
1162 Some(subtrees)
1163 }
1164 ObjectKind::Blob | ObjectKind::Tag => None,
1165 }
1166}
1167
1168fn peel_tip_to_commit(
1173 repo: &Repository,
1174 promised: &HashSet<ObjectId>,
1175 oid: ObjectId,
1176) -> Option<ObjectId> {
1177 if oid.is_zero() {
1178 return None;
1179 }
1180 if promised.contains(&oid) {
1181 return Some(oid);
1182 }
1183 let target = repo
1184 .odb()
1185 .peel_stored_tag_target(oid)
1186 .ok()
1187 .flatten()
1188 .unwrap_or(oid);
1189 if promised.contains(&target) {
1190 return Some(target);
1191 }
1192 match repo.odb().try_read_once(&target) {
1193 Ok(Some(obj)) if obj.kind == ObjectKind::Commit => Some(target),
1194 _ => None,
1195 }
1196}
1197
1198fn is_head_ref(refname: &str) -> bool {
1199 refname == "HEAD" || refname.ends_with("/HEAD")
1200}
1201
1202fn head_reflog_tips(repo: &Repository, promised: &HashSet<ObjectId>) -> Vec<ObjectId> {
1205 let store = repo.refs();
1206 let mut tips = Vec::new();
1207 if let Ok(oid) = store.resolve("HEAD") {
1208 tips.push(oid);
1209 }
1210 if let Ok(refs) = refs::collect_resolved_refs(store, "refs/") {
1211 tips.extend(refs.into_iter().map(|(_, oid)| oid));
1212 }
1213 tips.sort();
1214 tips.dedup();
1215 tips.into_iter()
1216 .filter_map(|oid| peel_tip_to_commit(repo, promised, oid))
1217 .collect()
1218}
1219
1220struct ReachableCommits<'r> {
1229 repo: &'r Repository,
1230 cache: &'r ReflogExpiryCache,
1231 graph: Option<Arc<CommitGraphChain>>,
1232 shallow: Vec<u32>,
1234 seen: Vec<bool>,
1236 seen_count: usize,
1237 pending: Vec<u32>,
1238 deferred: Vec<u32>,
1240}
1241
1242impl<'r> ReachableCommits<'r> {
1243 fn new(
1245 repo: &'r Repository,
1246 cache: &'r ReflogExpiryCache,
1247 tips: Vec<ObjectId>,
1248 cutoff: i64,
1249 ) -> Self {
1250 let mut reachable = Self {
1251 repo,
1252 cache,
1253 graph: None,
1254 shallow: Vec::new(),
1255 seen: Vec::new(),
1256 seen_count: 0,
1257 pending: Vec::new(),
1258 deferred: Vec::new(),
1259 };
1260 if tips.is_empty() {
1261 return reachable;
1262 }
1263 reachable.graph = repo.caches().commit_graph_chain(repo.odb().objects_dir());
1264 {
1265 let mut commits = cache.commits.borrow_mut();
1266 let mut shallow = crate::shallow::load_shallow_boundaries(repo.git_dir())
1267 .into_iter()
1268 .map(|oid| commits.intern(oid))
1269 .collect::<Vec<_>>();
1270 shallow.sort_unstable();
1271 reachable.shallow = shallow;
1272 reachable.pending = tips.into_iter().map(|oid| commits.intern(oid)).collect();
1273 }
1274 reachable.walk(cutoff);
1275 reachable
1276 }
1277
1278 fn walk(&mut self, cutoff: i64) {
1279 let repo = self.repo;
1280 let graph = self.graph.as_deref();
1281 let mut commits = self.cache.commits.borrow_mut();
1282 while let Some(node) = self.pending.pop() {
1283 let idx = node as usize;
1284 if idx >= self.seen.len() {
1285 self.seen.resize(commits.len(), false);
1286 }
1287 if std::mem::replace(&mut self.seen[idx], true) {
1288 continue;
1289 }
1290 self.seen_count += 1;
1291 if !self.shallow.is_empty() && self.shallow.binary_search(&node).is_ok() {
1292 continue;
1293 }
1294 let Some((time, parents)) =
1295 commits.linkage(node, |oid| read_commit_linkage(repo, graph, oid))
1296 else {
1297 continue;
1298 };
1299 if cutoff > 0 && time < cutoff {
1300 self.deferred.extend_from_slice(parents);
1301 } else {
1302 self.pending.extend_from_slice(parents);
1303 }
1304 }
1305 }
1306
1307 fn is_seen(&self, oid: &ObjectId) -> bool {
1308 self.cache
1309 .commits
1310 .borrow()
1311 .lookup(oid)
1312 .is_some_and(|node| self.seen.get(node as usize) == Some(&true))
1313 }
1314
1315 fn contains(&mut self, oid: &ObjectId) -> bool {
1316 if !self.is_seen(oid) && !self.deferred.is_empty() {
1317 self.pending.append(&mut self.deferred);
1318 self.walk(0);
1319 }
1320 self.is_seen(oid)
1321 }
1322}
1323
1324fn read_commit_linkage(
1327 repo: &Repository,
1328 graph: Option<&CommitGraphChain>,
1329 oid: ObjectId,
1330) -> Option<(Vec<ObjectId>, i64)> {
1331 if let Some(linkage) = graph.and_then(|g| g.graph_commit(&oid)) {
1332 return Some(linkage);
1333 }
1334 let obj = repo.odb().try_read_once(&oid).ok()??;
1335 if obj.kind != ObjectKind::Commit {
1336 return None;
1337 }
1338 let commit = CommitRef::parse(&obj.data).ok()?;
1339 Some((commit.parents().to_vec(), commit.committer_timestamp()))
1340}
1341
1342fn is_unreachable_oid(
1345 repo: &Repository,
1346 reachable: &mut ReachableCommits<'_>,
1347 oid: ObjectId,
1348) -> bool {
1349 if oid.is_zero() || reachable.is_seen(&oid) {
1350 return false;
1351 }
1352 match repo.odb().try_read_once(&oid) {
1353 Ok(Some(obj)) if obj.kind == ObjectKind::Commit => !reachable.contains(&oid),
1354 Ok(Some(_)) => false,
1355 _ => true,
1356 }
1357}
1358
1359fn reflog_entry_timestamp(entry: &ReflogEntry) -> i64 {
1360 parse_timestamp_from_identity(&entry.identity).unwrap_or(i64::MAX)
1361}
1362
1363struct StaleFix<'a> {
1365 cache: &'a ReflogExpiryCache,
1366 promised: &'a HashSet<ObjectId>,
1367}
1368
1369impl StaleFix<'_> {
1370 fn entry_is_stale(&self, repo: &Repository, entry: &ReflogEntry) -> bool {
1371 !self
1372 .cache
1373 .commit_complete(repo, self.promised, entry.old_oid)
1374 || !self
1375 .cache
1376 .commit_complete(repo, self.promised, entry.new_oid)
1377 }
1378}
1379
1380fn should_drop_reflog_entry(
1381 repo: &Repository,
1382 entry: &ReflogEntry,
1383 expire_total: i64,
1384 expire_unreachable: i64,
1385 reachable: Option<&mut ReachableCommits<'_>>,
1386 stale_fix: Option<&StaleFix<'_>>,
1387) -> bool {
1388 let ts = reflog_entry_timestamp(entry);
1389 if expire_total > 0 && ts < expire_total {
1390 return true;
1391 }
1392 if stale_fix.is_some_and(|fix| fix.entry_is_stale(repo, entry)) {
1393 return true;
1394 }
1395 if expire_unreachable == 0 || ts >= expire_unreachable {
1396 return false;
1397 }
1398 let Some(reachable) = reachable else {
1400 return true;
1401 };
1402 is_unreachable_oid(repo, reachable, entry.old_oid)
1403 || is_unreachable_oid(repo, reachable, entry.new_oid)
1404}
1405
1406#[expect(clippy::too_many_arguments)]
1425pub fn expire_reflog_git(
1426 repo: &Repository,
1427 refname: &str,
1428 params: &ReflogExpireParams,
1429 explicit_total: Option<i64>,
1430 explicit_unreachable: Option<i64>,
1431 gc_patterns: &[GcReflogPattern],
1432 gc_global_total: Option<i64>,
1433 gc_global_unreachable: Option<i64>,
1434 now: i64,
1435) -> Result<ReflogExpireResult> {
1436 expire_reflog_git_with_cache(
1437 repo,
1438 refname,
1439 params,
1440 explicit_total,
1441 explicit_unreachable,
1442 gc_patterns,
1443 gc_global_total,
1444 gc_global_unreachable,
1445 now,
1446 &ReflogExpiryCache::default(),
1447 )
1448}
1449
1450#[expect(clippy::too_many_arguments)]
1453pub(crate) fn expire_reflog_git_with_cache(
1454 repo: &Repository,
1455 refname: &str,
1456 params: &ReflogExpireParams,
1457 explicit_total: Option<i64>,
1458 explicit_unreachable: Option<i64>,
1459 gc_patterns: &[GcReflogPattern],
1460 gc_global_total: Option<i64>,
1461 gc_global_unreachable: Option<i64>,
1462 now: i64,
1463 cache: &ReflogExpiryCache,
1464) -> Result<ReflogExpireResult> {
1465 let store = repo.refs();
1466 let base_total = gc_global_total.unwrap_or_else(|| default_expire_total(now));
1467 let base_unreachable = gc_global_unreachable.unwrap_or_else(|| default_expire_unreachable(now));
1468 let (expire_total, expire_unreachable) = resolve_expire_for_ref(
1469 refname,
1470 explicit_total,
1471 explicit_unreachable,
1472 gc_patterns,
1473 base_total,
1474 base_unreachable,
1475 );
1476
1477 let entries = read_reflog_on_store(store, refname)?;
1478 if entries.is_empty() {
1479 return Ok(ReflogExpireResult {
1480 pruned: 0,
1481 actions: Vec::new(),
1482 });
1483 }
1484
1485 let may_drop_total = expire_total > 0
1486 && entries
1487 .iter()
1488 .any(|e| reflog_entry_timestamp(e) < expire_total);
1489 let may_drop_unreachable = expire_unreachable > 0
1490 && entries
1491 .iter()
1492 .any(|e| reflog_entry_timestamp(e) < expire_unreachable);
1493 let stale_fix = if params.stale_fix {
1494 Some(StaleFix {
1495 cache,
1496 promised: cache.promised(repo)?,
1497 })
1498 } else {
1499 None
1500 };
1501 let may_drop_stale = stale_fix
1502 .as_ref()
1503 .is_some_and(|fix| entries.iter().any(|e| fix.entry_is_stale(repo, e)));
1504
1505 if !may_drop_total && !may_drop_stale && !may_drop_unreachable {
1506 return Ok(ReflogExpireResult {
1507 pruned: 0,
1508 actions: entries
1509 .iter()
1510 .map(|entry| ReflogExpireAction {
1511 refname: refname.to_string(),
1512 entry: entry.clone(),
1513 action: ReflogExpireActionKind::Keep,
1514 })
1515 .collect(),
1516 });
1517 }
1518
1519 let tips = if !may_drop_unreachable || expire_unreachable <= expire_total {
1522 None
1523 } else if is_head_ref(refname) {
1524 Some(head_reflog_tips(repo, cache.promised(repo)?))
1525 } else {
1526 let promised = cache.promised(repo)?;
1527 store
1528 .resolve(refname)
1529 .ok()
1530 .and_then(|tip| peel_tip_to_commit(repo, promised, tip))
1531 .map(|tip| vec![tip])
1532 };
1533 let mut reachable = tips.map(|tips| ReachableCommits::new(repo, cache, tips, expire_total));
1534 let snapshot = entries;
1535 let mut drop_flags = Vec::with_capacity(snapshot.len());
1536 let mut pruned = 0usize;
1537 let mut actions = Vec::with_capacity(snapshot.len());
1538
1539 for entry in &snapshot {
1540 let drop = should_drop_reflog_entry(
1541 repo,
1542 entry,
1543 expire_total,
1544 expire_unreachable,
1545 reachable.as_mut(),
1546 stale_fix.as_ref(),
1547 );
1548 drop_flags.push(drop);
1549 let action = if drop {
1550 pruned += 1;
1551 if params.dry_run {
1552 ReflogExpireActionKind::WouldPrune
1553 } else {
1554 ReflogExpireActionKind::Prune
1555 }
1556 } else {
1557 ReflogExpireActionKind::Keep
1558 };
1559 actions.push(ReflogExpireAction {
1560 refname: refname.to_string(),
1561 entry: entry.clone(),
1562 action,
1563 });
1564 }
1565
1566 if !params.dry_run && pruned > 0 {
1567 rewrite_reflog_from_snapshot(store, refname, &snapshot, &drop_flags)?;
1568 if let Some(files) = store.as_files() {
1570 adjust_reflog_shared_perm(repo.git_dir(), &files.reflog_path(refname));
1571 }
1572 }
1573 Ok(ReflogExpireResult { pruned, actions })
1574}
1575
1576const REFLOG_REWRITE_RETRIES: u32 = 128;
1577
1578fn rewrite_reflog_from_snapshot(
1579 store: &dyn crate::refs::store::RefStore,
1580 refname: &str,
1581 snapshot: &[ReflogEntry],
1582 drop_flags: &[bool],
1583) -> Result<()> {
1584 debug_assert_eq!(snapshot.len(), drop_flags.len());
1585 for attempt in 0..REFLOG_REWRITE_RETRIES {
1586 let snap = snapshot.to_vec();
1587 let drops = drop_flags.to_vec();
1588 let name = refname.to_owned();
1589 match store.rewrite_reflog(refname, &mut move |current| {
1590 if current.len() < snap.len() || current[..snap.len()] != snap[..] {
1591 return Err(Error::RefStore(RefStoreError::ReflogRewriteStale {
1592 name: name.clone(),
1593 }));
1594 }
1595 let mut kept = Vec::new();
1596 for (i, entry) in current.into_iter().enumerate() {
1597 if i < drops.len() {
1598 if !drops[i] {
1599 kept.push(entry);
1600 }
1601 } else {
1602 kept.push(entry);
1603 }
1604 }
1605 Ok(kept)
1606 }) {
1607 Ok(()) => return Ok(()),
1608 Err(Error::RefStore(RefStoreError::ReflogRewriteStale { .. }))
1609 if attempt + 1 < REFLOG_REWRITE_RETRIES =>
1610 {
1611 continue;
1612 }
1613 Err(err) => return Err(err),
1614 }
1615 }
1616 Err(Error::RefStore(RefStoreError::Corrupt(format!(
1617 "reflog '{refname}' kept changing during rewrite"
1618 ))))
1619}
1620
1621#[derive(Debug, Clone)]
1623pub struct GcReflogExpireConfig {
1624 pub patterns: Vec<GcReflogPattern>,
1626 pub global_total: Option<i64>,
1628 pub global_unreachable: Option<i64>,
1630}
1631
1632#[must_use]
1634pub fn load_gc_reflog_expire_config(config: &ConfigSet, now: i64) -> GcReflogExpireConfig {
1635 let (global_total, global_unreachable) = global_gc_reflog_expiry(config, now);
1636 GcReflogExpireConfig {
1637 patterns: collect_gc_reflog_patterns(config, now),
1638 global_total,
1639 global_unreachable,
1640 }
1641}
1642
1643pub fn mark_stalefix_reachable(repo: &Repository, git_dir: &Path) -> Result<HashSet<ObjectId>> {
1645 let mut seeds: Vec<ObjectId> = Vec::new();
1646 if let Ok(oid) = refs::resolve_ref_in(repo, "HEAD") {
1647 seeds.push(oid);
1648 }
1649 if let Ok(refs) = refs::list_refs_for_repository(repo, "refs/") {
1650 for (_, oid) in refs {
1651 seeds.push(oid);
1652 }
1653 }
1654 if let Ok(names) = list_reflog_refs(git_dir) {
1655 for r in names {
1656 if let Ok(ent) = read_reflog(git_dir, &r) {
1657 for e in ent {
1658 if !e.old_oid.is_zero() {
1659 seeds.push(e.old_oid);
1660 }
1661 if !e.new_oid.is_zero() {
1662 seeds.push(e.new_oid);
1663 }
1664 }
1665 }
1666 }
1667 }
1668 seeds.sort();
1669 seeds.dedup();
1670
1671 let mut seen = HashSet::new();
1672 let mut queue: std::collections::VecDeque<ObjectId> = seeds.into_iter().collect();
1673 while let Some(oid) = queue.pop_front() {
1674 if oid.is_zero() || !seen.insert(oid) {
1675 continue;
1676 }
1677 let Ok(Some(obj)) = repo.odb().try_read_once(&oid) else {
1678 continue;
1679 };
1680 match obj.kind {
1681 ObjectKind::Commit => {
1682 if let Ok(c) = parse_commit(&obj.data) {
1683 queue.push_back(c.tree);
1684 for p in c.parents {
1685 queue.push_back(p);
1686 }
1687 }
1688 }
1689 ObjectKind::Tree => {
1690 if let Ok(entries) = parse_tree(&obj.data) {
1691 for te in entries {
1692 queue.push_back(te.oid);
1693 }
1694 }
1695 }
1696 ObjectKind::Tag | ObjectKind::Blob => {}
1697 }
1698 }
1699 Ok(seen)
1700}
1701
1702#[cfg(test)]
1703mod reverse_scan_tests {
1704 use std::fs;
1705 use std::io::Write;
1706
1707 use super::{reflog_path, ReverseReflogLineReader, REFLOG_REVERSE_READ_CHUNK};
1708 use crate::objects::ObjectId;
1709
1710 fn sample_line(n: usize) -> String {
1711 let oid = ObjectId::zero();
1712 format!(
1713 "{oid} {oid} T <t@example.com> {ts} +0000\tcommit: line{n}\n",
1714 ts = 1_700_000_000 + n as i64
1715 )
1716 }
1717
1718 #[test]
1719 fn reverse_reader_returns_newest_first() {
1720 let dir = tempfile::tempdir().expect("tempdir");
1721 let git_dir = dir.path().join(".git");
1722 let path = reflog_path(&git_dir, "refs/heads/main");
1723 fs::create_dir_all(path.parent().expect("reflog parent")).expect("mkdir");
1724 let mut f = fs::File::create(&path).expect("create");
1725 write!(f, "{}", sample_line(1)).expect("w");
1726 write!(f, "{}", sample_line(2)).expect("w");
1727 let mut reader = ReverseReflogLineReader::open(&path)
1728 .expect("open")
1729 .expect("nonempty");
1730 let first = reader.next_nonempty_line().expect("read").expect("line");
1731 assert!(first.ends_with(b"line2"));
1732 let second = reader.next_nonempty_line().expect("read").expect("line");
1733 assert!(second.ends_with(b"line1"));
1734 assert!(reader.next_nonempty_line().expect("read").is_none());
1735 }
1736
1737 #[test]
1738 fn reverse_reader_spans_chunk_boundaries() {
1739 let dir = tempfile::tempdir().expect("tempdir");
1740 let git_dir = dir.path().join(".git");
1741 fs::create_dir_all(git_dir.join("logs")).expect("mkdir");
1742 let path = reflog_path(&git_dir, "HEAD");
1743 let mut f = fs::File::create(&path).expect("create");
1744 let pad = "x".repeat(REFLOG_REVERSE_READ_CHUNK + 128);
1745 write!(f, "{pad}\n").expect("pad");
1746 write!(f, "{}", sample_line(99)).expect("newest");
1747 let mut reader = ReverseReflogLineReader::open(&path)
1748 .expect("open")
1749 .expect("nonempty");
1750 let line = reader.next_nonempty_line().expect("read").expect("line");
1751 assert!(line.ends_with(b"line99"));
1752 }
1753}
1754
1755#[cfg(test)]
1756mod default_expire_tests {
1757 use std::fs;
1758 use std::path::Path;
1759
1760 use tempfile::TempDir;
1761
1762 use crate::config::ConfigSet;
1763 use crate::environment::Environment;
1764 use crate::objects::ObjectId;
1765 use crate::refs::{append_reflog, write_ref};
1766 use crate::repo::{init_repository, Repository};
1767
1768 use super::{
1769 all_reflog_oids, all_reflog_oids_ordered, default_expire_total, default_expire_unreachable,
1770 delete_reflog_entries, expire_reflog, expire_reflog_git, list_reflog_refs,
1771 load_gc_reflog_expire_config, mark_stalefix_reachable, read_reflog, read_reflog_dwim,
1772 truncate_last_reflog_line, ReflogExpireActionKind, ReflogExpireParams,
1773 };
1774 use super::{parse_reflog_expire_spec, parse_reflog_line};
1775
1776 #[test]
1777 fn gc_main_suffix_pattern_does_not_match_main_ref() {
1778 let now = 1_700_000_000_i64;
1779 let (tmp, _repo) = seed_repo();
1780 let git_dir = tmp.path().join(".git");
1781 std::fs::write(
1782 git_dir.join("config"),
1783 "[core]\nrepositoryformatversion = 0\n[gc \"refs/heads/main*suffix\"]\n\treflogExpire = now\n",
1784 )
1785 .expect("write config");
1786 let cfg = ConfigSet::load(&Environment::empty(), Some(&git_dir), true).expect("cfg");
1787 let gc = load_gc_reflog_expire_config(&cfg, now);
1788 assert_eq!(gc.patterns.len(), 1);
1789 assert_eq!(gc.patterns[0].pattern, "refs/heads/main*suffix");
1790 let (total, _) = super::resolve_expire_for_ref(
1791 "refs/heads/main",
1792 None,
1793 None,
1794 &gc.patterns,
1795 super::default_expire_total(now),
1796 super::default_expire_unreachable(now),
1797 );
1798 assert_ne!(
1799 total,
1800 i64::MAX,
1801 "refs/heads/main must not pick up main*suffix reflogExpire=now"
1802 );
1803 assert_eq!(total, super::default_expire_total(now));
1804 }
1805
1806 #[test]
1807 fn gc_reflog_expiry_uses_git_approxidate_threshold() {
1808 let now = 1_700_000_000_i64;
1809 assert_eq!(parse_reflog_expire_spec("never", now).expect("never"), 0);
1810 assert_eq!(parse_reflog_expire_spec("now", now).expect("now"), i64::MAX);
1811 assert_eq!(
1813 parse_reflog_expire_spec("100000000", now).expect("100M"),
1814 100_000_000
1815 );
1816 assert_ne!(
1818 parse_reflog_expire_spec("90000", now).expect("90000"),
1819 90_000
1820 );
1821 }
1822
1823 #[test]
1824 fn default_expire_total_is_ninety_days_before_now() {
1825 let now = 1_700_000_000_i64;
1826 let cutoff = default_expire_total(now);
1827 assert_eq!(cutoff, now - 90 * 86_400);
1828 }
1829
1830 #[test]
1831 fn default_expire_unreachable_is_thirty_days_before_now() {
1832 let now = 1_700_000_000_i64;
1833 let cutoff = default_expire_unreachable(now);
1834 assert_eq!(cutoff, now - 30 * 86_400);
1835 }
1836
1837 fn seed_repo() -> (TempDir, Repository) {
1838 let tmp = TempDir::new().expect("tempdir");
1839 let repo = init_repository(
1840 tmp.path(),
1841 false,
1842 "main",
1843 None,
1844 crate::RefStorageFormat::Files,
1845 )
1846 .expect("init");
1847 (tmp, repo)
1848 }
1849
1850 fn write_loose_reflog(git_dir: &Path, refname: &str, body: &str) {
1851 let path = super::reflog_path(git_dir, refname);
1852 if let Some(p) = path.parent() {
1853 fs::create_dir_all(p).expect("mkdir logs");
1854 }
1855 fs::write(path, body).expect("write reflog");
1856 }
1857
1858 #[test]
1859 fn parse_and_dwim_read_reflog_lines() {
1860 let (tmp, repo) = seed_repo();
1861 let git_dir = &repo.git_dir();
1862 let oid = crate::refs::test_commit(git_dir, "aaaaaaaa");
1863 write_ref(git_dir, "refs/heads/dwim", &oid).expect("write ref");
1864 let line = format!(
1865 "0000000000000000000000000000000000000000 {oid} Tester <t@e.com> 100 +0000\tmsg\n"
1866 );
1867 write_loose_reflog(git_dir, "refs/heads/dwim", &line);
1868 let parsed = parse_reflog_line(line.trim_end()).expect("parse");
1869 assert_eq!(parsed.message, "msg");
1870 let dwim = read_reflog_dwim(git_dir, "dwim").expect("dwim");
1871 assert_eq!(dwim.len(), 1);
1872 assert_eq!(dwim[0].message, "msg");
1873 assert!(read_reflog(git_dir, "missing").expect("read").is_empty());
1874 let _ = tmp;
1875 }
1876
1877 #[test]
1878 fn truncate_delete_and_expire_loose_reflog() {
1879 let (tmp, repo) = seed_repo();
1880 let git_dir = &repo.git_dir();
1881 let refname = "refs/heads/main";
1882 let o1 = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").expect("o1");
1883 let o2 = crate::refs::test_commit(git_dir, "cccccccc");
1884 write_ref(git_dir, refname, &o2).expect("tip");
1885 append_reflog(
1886 git_dir,
1887 refname,
1888 &ObjectId::zero(),
1889 &o1,
1890 "T <t@e.com> 1000 +0000",
1891 "a",
1892 true,
1893 )
1894 .expect("log1");
1895 append_reflog(
1896 git_dir,
1897 refname,
1898 &o1,
1899 &o2,
1900 "T <t@e.com> 2000 +0000",
1901 "b",
1902 false,
1903 )
1904 .expect("log2");
1905
1906 delete_reflog_entries(git_dir, refname, &[0]).expect("delete newest");
1907 assert_eq!(read_reflog(git_dir, refname).expect("read").len(), 1);
1908
1909 truncate_last_reflog_line(git_dir, refname).expect("truncate");
1910 assert!(read_reflog(git_dir, refname).expect("read2").is_empty());
1911
1912 append_reflog(
1913 git_dir,
1914 refname,
1915 &ObjectId::zero(),
1916 &o1,
1917 "T <t@e.com> 500 +0000",
1918 "old",
1919 true,
1920 )
1921 .expect("log old");
1922 let pruned = expire_reflog(git_dir, refname, Some(1_000)).expect("expire");
1923 assert_eq!(pruned, 1);
1924 let _ = tmp;
1925 }
1926
1927 #[test]
1928 fn expire_reflog_git_dry_run_and_stash_gc_never() {
1929 let (tmp, repo) = seed_repo();
1930 let git_dir = &repo.git_dir();
1931 let now = 1_700_000_000_i64;
1932 let refname = "refs/stash";
1933 let oid = crate::refs::test_commit(git_dir, "dddddddd");
1934 write_ref(git_dir, refname, &oid).expect("stash ref");
1935 append_reflog(
1936 git_dir,
1937 refname,
1938 &ObjectId::zero(),
1939 &oid,
1940 &format!("T <t@e.com> {} +0000", now - 86_400),
1941 "stash",
1942 true,
1943 )
1944 .expect("stash log");
1945
1946 let cfg = ConfigSet::load(&Environment::empty(), Some(git_dir), true).expect("cfg");
1947 let gc = load_gc_reflog_expire_config(&cfg, now);
1948 let dry = expire_reflog_git(
1949 &repo,
1950 refname,
1951 &ReflogExpireParams {
1952 stale_fix: false,
1953 dry_run: true,
1954 verbose: true,
1955 },
1956 None,
1957 None,
1958 &gc.patterns,
1959 gc.global_total,
1960 gc.global_unreachable,
1961 now,
1962 )
1963 .expect("dry");
1964 assert!(!dry.actions.is_empty());
1965 assert!(read_reflog(git_dir, refname).expect("stash kept").len() == 1);
1966
1967 let real = expire_reflog_git(
1968 &repo,
1969 refname,
1970 &ReflogExpireParams {
1971 stale_fix: false,
1972 dry_run: false,
1973 verbose: false,
1974 },
1975 None,
1976 None,
1977 &gc.patterns,
1978 gc.global_total,
1979 gc.global_unreachable,
1980 now,
1981 )
1982 .expect("real");
1983 assert_eq!(real.pruned, 0, "refs/stash uses gc never by default");
1984 assert_eq!(read_reflog(git_dir, refname).expect("stash still").len(), 1);
1985 let _ = tmp;
1986 }
1987
1988 #[test]
1989 fn mark_stalefix_reachable_walks_reflog_mentions() {
1990 let (tmp, repo) = seed_repo();
1991 let git_dir = &repo.git_dir();
1992 let oid = crate::refs::test_commit(git_dir, "eeeeeeee");
1993 write_ref(git_dir, "refs/heads/main", &oid).expect("write");
1994 append_reflog(
1995 git_dir,
1996 "refs/heads/main",
1997 &ObjectId::zero(),
1998 &oid,
1999 "T <t@e.com> 1000 +0000",
2000 "m",
2001 true,
2002 )
2003 .expect("log");
2004 let seen = mark_stalefix_reachable(&repo, git_dir).expect("stalefix");
2005 assert!(seen.contains(&oid));
2006 let _ = tmp;
2007 }
2008
2009 #[test]
2010 fn all_reflog_oids_and_list_includes_head() {
2011 let (tmp, repo) = seed_repo();
2012 let git_dir = &repo.git_dir();
2013 let oid = crate::refs::test_commit(git_dir, "12121212");
2014 write_ref(git_dir, "refs/heads/main", &oid).expect("write");
2015 append_reflog(
2016 git_dir,
2017 "HEAD",
2018 &ObjectId::zero(),
2019 &oid,
2020 "T <t@e.com> 1000 +0000",
2021 "head",
2022 true,
2023 )
2024 .expect("head log");
2025 let refs = list_reflog_refs(git_dir).expect("list");
2026 assert!(refs.iter().any(|r| r == "HEAD"));
2027 let oids = all_reflog_oids(git_dir).expect("all oids");
2028 assert!(oids.contains(&oid));
2029 let ordered = all_reflog_oids_ordered(git_dir).expect("ordered");
2030 assert!(ordered.contains(&oid));
2031 let _ = tmp;
2032 }
2033
2034 #[test]
2035 fn mirror_branch_reflog_to_head_copies_bytes() {
2036 let (tmp, repo) = seed_repo();
2037 let git_dir = &repo.git_dir();
2038 let branch = "refs/heads/main";
2039 let oid = crate::refs::test_commit(git_dir, "13131313");
2040 write_ref(git_dir, branch, &oid).expect("write");
2041 append_reflog(
2042 git_dir,
2043 branch,
2044 &ObjectId::zero(),
2045 &oid,
2046 "T <t@e.com> 1000 +0000",
2047 "branch",
2048 true,
2049 )
2050 .expect("branch log");
2051 super::mirror_branch_reflog_to_head(git_dir, branch).expect("mirror");
2052 let head = read_reflog(git_dir, "HEAD").expect("head log");
2053 assert_eq!(head.len(), 1);
2054 assert_eq!(head[0].message, "branch");
2055 let _ = tmp;
2056 }
2057
2058 #[test]
2059 fn read_reflog_skips_blank_lines_and_no_message_tab() {
2060 let (tmp, repo) = seed_repo();
2061 let git_dir = &repo.git_dir();
2062 let oid = ObjectId::from_hex("1414141414141414141414141414141414141414").expect("oid");
2063 let body =
2064 format!("\n0000000000000000000000000000000000000000 {oid} T <t@e.com> 1000 +0000\n");
2065 write_loose_reflog(git_dir, "refs/heads/x", &body);
2066 let entries = read_reflog(git_dir, "refs/heads/x").expect("read");
2067 assert_eq!(entries.len(), 1);
2068 assert!(entries[0].message.is_empty());
2069 let _ = tmp;
2070 }
2071
2072 #[test]
2073 fn reflog_entry_action_prefixes() {
2074 let cases = [
2075 ("commit: init", Some("commit")),
2076 ("commit (initial): init", Some("commit (initial)")),
2077 ("commit (amend): tweak", Some("commit (amend)")),
2078 ("checkout: moving from main to topic", Some("checkout")),
2079 ("reset: moving to HEAD", Some("reset")),
2080 (
2081 "rebase (finish): refs/heads/main onto abc",
2082 Some("rebase (finish)"),
2083 ),
2084 ("branch: Created from HEAD", Some("branch")),
2085 ("branch: Reset to origin/main", Some("branch")),
2086 ("merge topic: Merge branch 'topic'", Some("merge topic")),
2087 ("pull: Fast-forward", Some("pull")),
2088 ("pull: rebase onto origin/main", Some("pull")),
2089 ("cherry-pick: subject", Some("cherry-pick")),
2090 ("revert: subject", Some("revert")),
2091 ("history undo", None),
2092 ("rewrite", None),
2093 ("prune-me", None),
2094 ];
2095 for (message, want) in cases {
2096 let entry = super::ReflogEntry {
2097 old_oid: ObjectId::zero(),
2098 new_oid: ObjectId::zero(),
2099 identity: String::new(),
2100 message: message.into(),
2101 };
2102 assert_eq!(entry.action(), want, "message={message:?}");
2103 }
2104 }
2105
2106 #[test]
2107 fn expire_git_prunes_old_entry_and_reports_action() {
2108 let (tmp, repo) = seed_repo();
2109 let git_dir = &repo.git_dir();
2110 let refname = "refs/heads/main";
2111 let oid = crate::refs::test_commit(git_dir, "ffffffff");
2112 write_ref(git_dir, refname, &oid).expect("write");
2113 append_reflog(
2114 git_dir,
2115 refname,
2116 &ObjectId::zero(),
2117 &oid,
2118 "T <t@e.com> 1000 +0000",
2119 "prune-me",
2120 true,
2121 )
2122 .expect("log");
2123 let now = 2_000_000_i64;
2124 let cfg = ConfigSet::load(&Environment::empty(), Some(git_dir), true).expect("cfg");
2125 let gc = load_gc_reflog_expire_config(&cfg, now);
2126 let result = expire_reflog_git(
2127 &repo,
2128 refname,
2129 &ReflogExpireParams {
2130 stale_fix: false,
2131 dry_run: false,
2132 verbose: false,
2133 },
2134 Some(1_500_000),
2135 None,
2136 &gc.patterns,
2137 gc.global_total,
2138 gc.global_unreachable,
2139 now,
2140 )
2141 .expect("expire");
2142 assert_eq!(result.pruned, 1);
2143 assert!(result
2144 .actions
2145 .iter()
2146 .any(|a| a.action == ReflogExpireActionKind::Prune && a.entry.message == "prune-me"));
2147 assert!(read_reflog(git_dir, refname).expect("empty").is_empty());
2148 let _ = tmp;
2149 }
2150}
2151
2152#[cfg(test)]
2153mod rewrite_concurrency_tests {
2154 use std::sync::{Arc, Barrier};
2155 use std::thread;
2156
2157 use crate::lockfile::LockWait;
2158 use crate::objects::ObjectId;
2159 use crate::refs::prepare_with_lock_wait;
2160 use crate::refs::store::{
2161 Expected, MemoryRefStore, RawRef, RefStore, RefTransaction, RefUpdate, RefUpdateFlags,
2162 ReflogUpdate,
2163 };
2164
2165 use super::{
2166 delete_reflog_entries_on_store, delete_reflog_entries_rechain_on_store, ReflogEntry,
2167 };
2168
2169 fn oid(byte: u8) -> ObjectId {
2170 let mut bytes = [0u8; 20];
2171 bytes[19] = byte;
2172 ObjectId::from_bytes(&bytes).expect("oid")
2173 }
2174
2175 fn append(
2176 store: &dyn RefStore,
2177 refname: &str,
2178 oid: ObjectId,
2179 ts: i64,
2180 ) -> crate::error::Result<()> {
2181 let txn = RefTransaction::new().update(RefUpdate {
2182 name: refname.to_owned(),
2183 new_value: Some(RawRef::Direct(oid)),
2184 expected: Expected::Any,
2185 reflog: Some(ReflogUpdate {
2186 identity: format!("T <t@example.com> {ts} +0000"),
2187 message: "x".into(),
2188 time: time::OffsetDateTime::from_unix_timestamp(ts).expect("ts"),
2189 old_oid: None,
2190 new_oid: Some(oid),
2191 }),
2192 flags: RefUpdateFlags::default(),
2193 })?;
2194 prepare_with_lock_wait(store, txn, LockWait::Indefinitely)?.commit()?;
2195 Ok(())
2196 }
2197
2198 fn read_entries(store: &dyn RefStore, refname: &str) -> Vec<ReflogEntry> {
2199 let mut out = Vec::new();
2200 store
2201 .for_each_reflog_entry(refname, false, &mut |e| {
2202 out.push(e.clone());
2203 std::ops::ControlFlow::Continue(())
2204 })
2205 .expect("read");
2206 out
2207 }
2208
2209 #[test]
2210 fn delete_on_store_uses_locked_entries_not_stale_snapshot() {
2211 let store = Arc::new(MemoryRefStore::new());
2212 let refname = "refs/heads/r";
2213 let o1 = oid(1);
2214 let o2 = oid(2);
2215 let o3 = oid(3);
2216 store
2217 .replace_reflog(
2218 refname,
2219 vec![
2220 ReflogEntry {
2221 old_oid: ObjectId::zero(),
2222 new_oid: o1,
2223 identity: "T <t@e.com> 100 +0000".into(),
2224 message: String::new(),
2225 },
2226 ReflogEntry {
2227 old_oid: o1,
2228 new_oid: o2,
2229 identity: "T <t@e.com> 200 +0000".into(),
2230 message: String::new(),
2231 },
2232 ],
2233 )
2234 .expect("seed");
2235 let barrier = Arc::new(Barrier::new(2));
2236 let s_delete = store.clone();
2237 let b_delete = barrier.clone();
2238 let deleter = thread::spawn(move || {
2239 b_delete.wait();
2240 delete_reflog_entries_on_store(s_delete.as_ref(), refname, &[1]).expect("delete");
2241 });
2242 let b_append = barrier.clone();
2243 let s_append = store.clone();
2244 let appender = thread::spawn(move || {
2245 b_append.wait();
2246 append(s_append.as_ref(), refname, o3, 9_000_000_000).expect("append");
2247 });
2248 deleter.join().expect("deleter");
2249 appender.join().expect("appender");
2250 let entries = read_entries(store.as_ref(), refname);
2251 assert!(
2252 entries.iter().any(|e| e.new_oid == o3),
2253 "concurrent append missing after delete rewrite"
2254 );
2255 assert_eq!(entries.len(), 2, "expected oldest removed and append kept");
2256 }
2257
2258 #[test]
2259 fn delete_rechain_on_store_uses_locked_entries_not_stale_snapshot() {
2260 let store = Arc::new(MemoryRefStore::new());
2261 let refname = "refs/heads/r";
2262 let o1 = oid(1);
2263 let o2 = oid(2);
2264 let o3 = oid(3);
2265 store
2266 .replace_reflog(
2267 refname,
2268 vec![
2269 ReflogEntry {
2270 old_oid: ObjectId::zero(),
2271 new_oid: o1,
2272 identity: "T <t@e.com> 100 +0000".into(),
2273 message: String::new(),
2274 },
2275 ReflogEntry {
2276 old_oid: o1,
2277 new_oid: o2,
2278 identity: "T <t@e.com> 200 +0000".into(),
2279 message: String::new(),
2280 },
2281 ],
2282 )
2283 .expect("seed");
2284 let barrier = Arc::new(Barrier::new(2));
2285 let s_delete = store.clone();
2286 let b_delete = barrier.clone();
2287 let deleter = thread::spawn(move || {
2288 b_delete.wait();
2289 delete_reflog_entries_rechain_on_store(s_delete.as_ref(), refname, &[1])
2290 .expect("rechain delete");
2291 });
2292 let b_append = barrier.clone();
2293 let s_append = store.clone();
2294 let appender = thread::spawn(move || {
2295 b_append.wait();
2296 append(s_append.as_ref(), refname, o3, 9_000_000_000).expect("append");
2297 });
2298 deleter.join().expect("deleter");
2299 appender.join().expect("appender");
2300 let entries = read_entries(store.as_ref(), refname);
2301 assert!(entries.iter().any(|e| e.new_oid == o3));
2302 }
2303}
2304
2305#[cfg(test)]
2306mod completeness_tests {
2307 use std::collections::HashSet;
2308
2309 use tempfile::TempDir;
2310
2311 use crate::objects::{serialize_tree, ObjectId, ObjectKind, TreeEntry};
2312 use crate::repo::{init_repository, Repository};
2313
2314 use super::{Linkage, ReachableCommits, ReflogExpiryCache};
2315
2316 fn seed_repo() -> (TempDir, Repository) {
2317 let tmp = TempDir::new().expect("tempdir");
2318 let repo = init_repository(
2319 tmp.path(),
2320 false,
2321 "main",
2322 None,
2323 crate::RefStorageFormat::Files,
2324 )
2325 .expect("init");
2326 (tmp, repo)
2327 }
2328
2329 fn tree(repo: &Repository, entries: &[(u32, &str, ObjectId)]) -> ObjectId {
2330 let entries = entries
2331 .iter()
2332 .map(|&(mode, name, oid)| TreeEntry {
2333 mode,
2334 name: name.as_bytes().to_vec(),
2335 oid,
2336 })
2337 .collect::<Vec<_>>();
2338 repo.odb()
2339 .write(ObjectKind::Tree, &serialize_tree(&entries))
2340 .expect("write tree")
2341 }
2342
2343 fn commit(repo: &Repository, tree: ObjectId, parents: &[ObjectId]) -> ObjectId {
2344 commit_at(repo, tree, parents, 1)
2345 }
2346
2347 fn commit_at(repo: &Repository, tree: ObjectId, parents: &[ObjectId], when: i64) -> ObjectId {
2348 let mut body = format!("tree {tree}\n");
2349 for p in parents {
2350 body.push_str(&format!("parent {p}\n"));
2351 }
2352 body.push_str(&format!(
2353 "author A <a@example.com> {when} +0000\ncommitter A <a@example.com> {when} +0000\n\nc\n"
2354 ));
2355 repo.odb()
2356 .write(ObjectKind::Commit, body.as_bytes())
2357 .expect("write commit")
2358 }
2359
2360 #[test]
2361 fn completeness_walk_is_memoised_and_stops_at_missing_objects_and_promises() {
2362 let (_tmp, repo) = seed_repo();
2363 let blob = repo.odb().write(ObjectKind::Blob, b"a\n").expect("blob");
2364 let sub = tree(&repo, &[(0o100644, "b", blob)]);
2365 let root = tree(&repo, &[(0o100644, "a", blob), (0o040000, "sub", sub)]);
2366 let base = commit(&repo, root, &[]);
2367 let left = commit(&repo, root, &[base]);
2368 let right = commit(&repo, sub, &[base]);
2369 let merge = commit(&repo, root, &[left, right]);
2370
2371 let none = HashSet::new();
2372 let cache = ReflogExpiryCache::default();
2373 assert!(cache.commit_complete(&repo, &none, ObjectId::zero()));
2374 let probes = repo.odb().exists_probe_count();
2375 assert!(cache.commit_complete(&repo, &none, merge));
2376 assert_eq!(
2377 repo.odb().exists_probe_count() - probes,
2378 2,
2379 "each blob entry of each distinct tree is probed once"
2380 );
2381 let probes = repo.odb().exists_probe_count();
2382 assert!(cache.commit_complete(&repo, &none, left));
2383 assert!(cache.commit_complete(&repo, &none, merge));
2384 assert_eq!(
2385 repo.odb().exists_probe_count(),
2386 probes,
2387 "answers are reused"
2388 );
2389
2390 let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").expect("oid");
2391 let broken_tree = tree(&repo, &[(0o100644, "gone", missing)]);
2392 let broken = commit(&repo, broken_tree, &[merge]);
2393 let child = commit(&repo, root, &[broken]);
2394 assert!(!cache.commit_complete(&repo, &none, broken));
2395 assert!(!cache.commit_complete(&repo, &none, child));
2396 assert!(!cache.commit_complete(&repo, &none, missing));
2397 assert!(!cache.commit_complete(&repo, &none, blob), "not a commit");
2398
2399 let promised = HashSet::from([missing]);
2400 let fresh = ReflogExpiryCache::default();
2401 assert!(fresh.commit_complete(&repo, &promised, child));
2402 assert!(fresh.commit_complete(&repo, &promised, missing));
2403 }
2404
2405 #[test]
2406 fn reachability_walk_stops_at_the_cutoff_until_a_lookup_misses() {
2407 let (_tmp, repo) = seed_repo();
2408 let tree = tree(&repo, &[]);
2409 let mut chain = Vec::new();
2410 for when in 1..=10 {
2411 let parents = chain.last().copied().into_iter().collect::<Vec<_>>();
2412 chain.push(commit_at(&repo, tree, &parents, when));
2413 }
2414 let unrelated = commit_at(&repo, tree, &[], 5);
2415
2416 let cache = ReflogExpiryCache::default();
2417 let mut reachable = ReachableCommits::new(&repo, &cache, vec![chain[9]], 8);
2418 assert_eq!(
2419 reachable.seen_count, 4,
2420 "commits from time 10 down to the first one older than the cutoff"
2421 );
2422 assert!(reachable.contains(&chain[7]));
2423 assert_eq!(reachable.seen_count, 4, "a hit does not walk further");
2424 assert!(reachable.contains(&chain[0]));
2425 assert_eq!(reachable.seen_count, 10, "a miss walks the rest once");
2426 assert!(!reachable.contains(&unrelated));
2427
2428 let mut everything = ReachableCommits::new(&repo, &cache, vec![chain[9]], 0);
2429 assert_eq!(everything.seen_count, 10);
2430 assert!(!everything.contains(&unrelated));
2431 assert_eq!(
2432 cache.commits.borrow().len(),
2433 10,
2434 "both walks share one interned commit per commit"
2435 );
2436
2437 {
2438 let mut commits = cache.commits.borrow_mut();
2439 let tip = commits.intern(chain[9]) as usize;
2440 let parent = commits.intern(unrelated);
2441 commits.nodes[tip] = Linkage::Read {
2442 time: 10,
2443 parents: Box::new([parent]),
2444 };
2445 }
2446 let mut reused = ReachableCommits::new(&repo, &cache, vec![chain[9]], 0);
2447 assert!(
2448 reused.contains(&unrelated),
2449 "a later walk in the same pass takes linkage from the cache"
2450 );
2451 assert_eq!(
2452 ReachableCommits::new(&repo, &cache, Vec::new(), 0).seen_count,
2453 0
2454 );
2455 }
2456
2457 #[test]
2458 fn reachability_walk_counts_shallow_boundaries_as_reachable_without_walking_past_them() {
2459 let (_tmp, repo) = seed_repo();
2460 let tree = tree(&repo, &[]);
2461 let root = commit_at(&repo, tree, &[], 1);
2462 let boundary = commit_at(&repo, tree, &[root], 2);
2463 let tip = commit_at(&repo, tree, &[boundary], 3);
2464 std::fs::write(repo.git_dir().join("shallow"), format!("{boundary}\n")).expect("shallow");
2465
2466 let cache = ReflogExpiryCache::default();
2467 let mut reachable = ReachableCommits::new(&repo, &cache, vec![tip], 0);
2468 assert!(reachable.contains(&boundary));
2469 assert!(!reachable.contains(&root));
2470 assert_eq!(reachable.seen_count, 2);
2471 }
2472}