This repository has no description
1use std::collections::HashSet;
2use std::io::Read;
3use std::path::{Path, PathBuf};
4
5use knot_git::{
6 Haves, Repo, Wants, reachable_via_bitmap, verbatim_clone_pack, write_bitmap, write_midx_bitmap,
7};
8use knot_types::Oid;
9
10mod common;
11use common::{commit_file as commit, git, git_available, git_ok as ok};
12
13fn test_bitmap(path: &Path) {
14 let (ok, report) = git(path, &["rev-list", "--test-bitmap", "HEAD"]);
15 assert!(ok, "git rejected the bitmap: {report}");
16}
17
18fn seed(format: &str) -> tempfile::TempDir {
19 let dir = tempfile::tempdir().unwrap();
20 let path = dir.path();
21 ok(
22 path,
23 &[
24 "init",
25 "-q",
26 "--object-format",
27 format,
28 "--initial-branch",
29 "main",
30 ],
31 );
32 commit(path, "a.txt", "alpha\n", "root");
33 commit(path, "b.txt", "beta\n", "second");
34 ok(path, &["checkout", "-q", "-b", "feature"]);
35 commit(path, "c.txt", "gamma\n", "feature work");
36 ok(path, &["checkout", "-q", "main"]);
37 commit(path, "d.txt", "delta\n", "more main");
38 ok(path, &["tag", "-a", "v1", "-m", "release one"]);
39 ok(path, &["repack", "-adq"]);
40 dir
41}
42
43fn find_pack(path: &Path, suffix: &str) -> PathBuf {
44 std::fs::read_dir(path.join(".git/objects/pack"))
45 .unwrap()
46 .filter_map(Result::ok)
47 .map(|e| e.path())
48 .find(|p| {
49 p.file_name()
50 .and_then(|n| n.to_str())
51 .is_some_and(|n| n.ends_with(suffix))
52 })
53 .unwrap_or_else(|| panic!("a pack file ending {suffix}"))
54}
55
56fn idx_count(path: &Path) -> usize {
57 std::fs::read_dir(path.join(".git/objects/pack"))
58 .unwrap()
59 .filter_map(Result::ok)
60 .filter(|e| e.path().extension().is_some_and(|ext| ext == "idx"))
61 .count()
62}
63
64fn rev_parse(path: &Path, name: &str) -> Oid {
65 Oid::from_hex(ok(path, &["rev-parse", name]).trim()).unwrap()
66}
67
68fn rev_list(path: &Path, revs: &[&str]) -> HashSet<Oid> {
69 let args: Vec<&str> = ["rev-list", "--objects"]
70 .into_iter()
71 .chain(revs.iter().copied())
72 .collect();
73 ok(path, &args)
74 .lines()
75 .filter_map(|line| line.split_whitespace().next())
76 .filter_map(|token| Oid::from_hex(token).ok())
77 .collect()
78}
79
80fn closure(repo: &Repo, wants: &[Oid], haves: &[Oid]) -> HashSet<Oid> {
81 reachable_via_bitmap(repo, Wants::new(wants), Haves::new(haves))
82 .unwrap()
83 .expect("bitmap fast path resolves")
84 .into_iter()
85 .collect()
86}
87
88fn walked(repo: &Repo, want: Oid) -> HashSet<Oid> {
89 repo.select_pack_objects(Wants::new(&[want]), Haves::new(&[]))
90 .unwrap()
91 .into_iter()
92 .collect()
93}
94
95fn reject_corruption(repo: &Repo, bitmap: &Path, head: Oid) {
96 let intact = std::fs::read(bitmap).unwrap();
97 let mut flipped = intact.clone();
98 *flipped.last_mut().unwrap() ^= 0xff;
99 std::fs::write(bitmap, &flipped).unwrap();
100 assert!(
101 reachable_via_bitmap(repo, Wants::new(&[head]), Haves::new(&[])).is_err(),
102 "a corrupt checksum is rejected"
103 );
104 std::fs::write(bitmap, &intact[..intact.len() / 2]).unwrap();
105 assert!(
106 reachable_via_bitmap(repo, Wants::new(&[head]), Haves::new(&[])).is_err(),
107 "a truncated bitmap is rejected without a panic"
108 );
109}
110
111fn run_lifecycle(format: &str) {
112 if !git_available() {
113 eprintln!("skipping bitmap lifecycle: git unavailable");
114 return;
115 }
116 let dir = seed(format);
117 let path = dir.path();
118 let repo = Repo::open(path).unwrap();
119 let idx = find_pack(path, ".idx");
120 assert!(
121 write_bitmap(&repo, &idx).unwrap(),
122 "a single-pack repo gets a bitmap"
123 );
124 test_bitmap(path);
125
126 let head = rev_parse(path, "HEAD");
127 let ours = closure(&repo, &[head], &[]);
128 assert_eq!(
129 ours,
130 rev_list(path, &["HEAD"]),
131 "closure equals rev-list --objects HEAD"
132 );
133 assert_eq!(ours, walked(&repo, head), "closure equals the walk closure");
134 assert_eq!(
135 ours,
136 closure(&repo, &vec![head; 2048], &[]),
137 "duplicate wants fold to one closure"
138 );
139
140 let feature = rev_parse(path, "feature");
141 let main = rev_parse(path, "main");
142 assert_eq!(
143 closure(&repo, &[feature], &[main]),
144 rev_list(path, &["feature", "^main"]),
145 "feature minus main"
146 );
147
148 let tag = rev_parse(path, "v1");
149 let mut whole = verbatim_clone_pack(&repo, Wants::new(&[main, feature, tag]))
150 .unwrap()
151 .expect("full clone reuses whole pack");
152 let mut header = [0u8; 12];
153 whole.read_exact(&mut header).unwrap();
154 assert_eq!(
155 &header[..4],
156 b"PACK",
157 "verbatim reuse returns the on-disk pack"
158 );
159 let count = u32::from_be_bytes([header[8], header[9], header[10], header[11]]) as usize;
160 assert_eq!(
161 count,
162 rev_list(path, &["--all"]).len(),
163 "verbatim reuse streams every object"
164 );
165 assert!(
166 verbatim_clone_pack(&repo, Wants::new(&[main]))
167 .unwrap()
168 .is_none(),
169 "a partial closure never reuses the whole pack"
170 );
171
172 ok(path, &["tag", "-a", "v2", "-m", "release two", "HEAD"]);
173 let v2 = rev_parse(path, "v2");
174 assert_ne!(v2, head);
175 assert!(
176 reachable_via_bitmap(&repo, Wants::new(&[v2]), Haves::new(&[]))
177 .unwrap()
178 .is_none(),
179 "a want outside the bitmapped pack fails closed"
180 );
181 assert!(
182 walked(&repo, v2).contains(&v2),
183 "the fallback walk includes the wanted tag"
184 );
185
186 std::fs::remove_file(idx.with_extension("bitmap")).unwrap();
187 commit(path, "e.txt", "epsilon\n", "post-bitmap growth");
188 commit(path, "f.txt", "zeta\n", "more growth");
189 ok(path, &["repack", "-dq"]);
190 ok(path, &["multi-pack-index", "write"]);
191 assert!(idx_count(path) >= 2, "the repo now spans multiple packs");
192
193 let repo = Repo::open(path).unwrap();
194 assert!(
195 write_midx_bitmap(&repo).unwrap(),
196 "a multi-pack repo gets a midx bitmap"
197 );
198 test_bitmap(path);
199 let head = rev_parse(path, "HEAD");
200 assert_eq!(
201 closure(&repo, &[head], &[]),
202 rev_list(path, &["HEAD"]),
203 "midx closure equals rev-list HEAD"
204 );
205
206 reject_corruption(&repo, &find_pack(path, ".bitmap"), head);
207}
208
209#[test]
210fn bitmap_single_pack_then_midx_round_trips_against_canonical_git_sha1() {
211 run_lifecycle("sha1");
212}
213
214#[test]
215fn bitmap_single_pack_then_midx_round_trips_against_canonical_git_sha256() {
216 run_lifecycle("sha256");
217}