This repository has no description
7.0 kB
200 lines
1use std::collections::HashSet;
2
3use knot_git::Repo;
4use knot_maintenance::{Options, PruneGrace, RepackStatus, run_repo};
5use knot_types::{ObjectFormat, Oid};
6
7mod common;
8use common::{
9 commit, create_repo, delete_ref, git, has_bitmap, has_midx_bitmap, now, options, set_ref,
10};
11
12fn create(scan: &std::path::Path, did: &str) -> Repo {
13 create_repo(scan, ObjectFormat::SHA1, did)
14}
15
16fn loose_count(repo: &Repo) -> usize {
17 walkdir::WalkDir::new(repo.git().git_dir().join("objects"))
18 .into_iter()
19 .filter_map(Result::ok)
20 .filter(|e| e.file_type().is_file())
21 .filter(|e| {
22 e.path()
23 .parent()
24 .and_then(|p| p.file_name())
25 .and_then(|n| n.to_str())
26 .is_some_and(|n| n.len() == 2)
27 })
28 .count()
29}
30
31#[test]
32fn a_single_pack_maintenance_pass_packs_graphs_bitmaps_prunes_then_settles() {
33 let scan = tempfile::tempdir().unwrap();
34 let repo = create(scan.path(), "did:plc:squid");
35
36 let base = commit(&repo, 0, Vec::new());
37 let main_tip = commit(&repo, 1, vec![base]);
38 set_ref(&repo, "refs/heads/main", main_tip);
39 set_ref(&repo, "refs/heads/feature", commit(&repo, 2, vec![base]));
40 let orphan = commit(&repo, 9, vec![main_tip]);
41
42 let info = repo.git().git_dir().join("objects/info");
43 std::fs::create_dir_all(&info).unwrap();
44 let leaked = info.join("commit-graph.knot-tmp.999999");
45 std::fs::write(&leaked, b"a graph write that a kill -9 interrupted").unwrap();
46 let long_ago = std::time::SystemTime::now() - std::time::Duration::from_secs(7 * 3600);
47 std::fs::File::options()
48 .write(true)
49 .open(&leaked)
50 .unwrap()
51 .set_times(std::fs::FileTimes::new().set_modified(long_ago))
52 .unwrap();
53 assert!(loose_count(&repo) >= 4 && repo.contains(orphan));
54
55 let report = run_repo(&repo, now(), &options()).unwrap();
56 assert_eq!(report.repack.status, RepackStatus::Repacked);
57 assert!(
58 report.repack.removed_loose.get() >= 4,
59 "loose objects folded into the pack"
60 );
61 assert!(report.commit_graph && info.join("commit-graph").exists());
62 assert!(
63 !leaked.exists(),
64 "maintenance sweeps a crashed graph write's temp once it is too old to still have a writer"
65 );
66 assert!(report.bitmap && has_bitmap(&repo));
67 let (ok, stderr) = git(&repo, &["rev-list", "--test-bitmap", "main"]);
68 assert!(ok, "canonical git accepts our bitmap: {stderr}");
69 assert!(report.packed_refs.packed >= 1);
70 assert!(
71 report.prune.ran && report.prune.removed.get() >= 1,
72 "the orphan is pruned"
73 );
74
75 let reopened = Repo::open(repo.git().git_dir()).unwrap();
76 assert!(!reopened.contains(orphan) && reopened.contains(main_tip));
77 let (clean, stderr) = git(&reopened, &["fsck", "--no-progress"]);
78 assert!(clean, "fsck-clean after the pass: {stderr}");
79
80 let settled = run_repo(&reopened, now(), &options()).unwrap();
81 assert_eq!(settled.repack.status, RepackStatus::Clean);
82 assert!(
83 !settled.prune.ran && !settled.commit_graph && !settled.bitmap,
84 "a settled repo is a no-op"
85 );
86 assert_eq!(settled.packed_refs.packed, 0);
87}
88
89#[test]
90fn the_written_graph_verifies_and_accelerates_selection_through_an_octopus_merge() {
91 let scan = tempfile::tempdir().unwrap();
92 let repo = create(scan.path(), "did:plc:cuttle");
93
94 let a = commit(&repo, 1, Vec::new());
95 let b = commit(&repo, 2, Vec::new());
96 let c = commit(&repo, 3, Vec::new());
97 let pair = commit(&repo, 4, vec![a, b]);
98 let octopus = commit(&repo, 5, vec![a, b, c]);
99 let tip = commit(&repo, 6, vec![pair, octopus]);
100 set_ref(&repo, "refs/heads/main", tip);
101
102 let graph = repo.git().git_dir().join("objects/info/commit-graph");
103 let closure = |r: &Repo| -> HashSet<Oid> {
104 r.select_pack_objects(knot_git::Wants::new(&[tip]), knot_git::Haves::new(&[]))
105 .unwrap()
106 .into_iter()
107 .collect()
108 };
109 let decode_closure = closure(&repo);
110 assert!(!graph.exists());
111
112 assert!(run_repo(&repo, now(), &options()).unwrap().commit_graph && graph.exists());
113 let (ok, stderr) = git(&repo, &["commit-graph", "verify"]);
114 assert!(
115 ok,
116 "git accepts the hand-written graph with an octopus: {stderr}"
117 );
118
119 let reopened = Repo::open(repo.git().git_dir()).unwrap();
120 assert_eq!(
121 closure(&reopened),
122 decode_closure,
123 "graph-accelerated selection equals the decode walk"
124 );
125}
126
127#[test]
128fn commit_graph_knob_lifecycle_skips_backfills_and_sweeps() {
129 let scan = tempfile::tempdir().unwrap();
130 let repo = create(scan.path(), "did:plc:limpet");
131
132 let base = commit(&repo, 0, Vec::new());
133 set_ref(&repo, "refs/heads/main", commit(&repo, 1, vec![base]));
134
135 let off = Options {
136 commit_graph: false,
137 ..options()
138 };
139 let graph = repo.git().git_dir().join("objects/info/commit-graph");
140
141 let first = run_repo(&repo, now(), &off).unwrap();
142 assert_eq!(first.repack.status, RepackStatus::Repacked);
143 assert!(
144 !first.commit_graph && !graph.exists(),
145 "no graph is written when the knob is off"
146 );
147
148 let reopened = Repo::open(repo.git().git_dir()).unwrap();
149 let backfill = run_repo(&reopened, now(), &options()).unwrap();
150 assert!(
151 backfill.commit_graph && graph.exists(),
152 "a packed repo with no graph backfills it"
153 );
154 assert_eq!(backfill.repack.status, RepackStatus::Clean);
155
156 let settled = Repo::open(repo.git().git_dir()).unwrap();
157 assert!(
158 !run_repo(&settled, now(), &options()).unwrap().commit_graph,
159 "a present graph settles to no-op"
160 );
161
162 let swept = Repo::open(repo.git().git_dir()).unwrap();
163 assert!(!run_repo(&swept, now(), &off).unwrap().commit_graph);
164 assert!(
165 !graph.exists(),
166 "flipping the knob off sweeps the orphan graph"
167 );
168}
169
170#[test]
171fn cruft_second_pack_gets_a_midx_bitmap_canonical_git_accepts() {
172 let scan = tempfile::tempdir().unwrap();
173 let repo = create(scan.path(), "did:plc:mussel");
174
175 let base = commit(&repo, 0, Vec::new());
176 let doomed = commit(&repo, 1, vec![base]);
177 set_ref(&repo, "refs/heads/main", base);
178 set_ref(&repo, "refs/heads/feature", doomed);
179
180 let opts = Options {
181 prune_grace: PruneGrace::from_secs(86_400),
182 ..options()
183 };
184 run_repo(&repo, now(), &opts).unwrap();
185
186 delete_ref(&repo, "refs/heads/feature");
187 set_ref(&repo, "refs/heads/main", commit(&repo, 2, vec![base]));
188
189 let report = run_repo(&repo, now(), &opts).unwrap();
190 assert!(
191 report.prune.crufted.get() >= 1,
192 "young unreachable object is crufted"
193 );
194 assert!(
195 report.bitmap && has_midx_bitmap(&repo),
196 "the multi-pack repo gets a midx bitmap"
197 );
198 let (ok, stderr) = git(&repo, &["rev-list", "--test-bitmap", "main"]);
199 assert!(ok, "canonical git accepts our midx bitmap: {stderr}");
200}