This repository has no description
7.3 kB
248 lines
1use std::collections::HashMap;
2use std::path::Path;
3
4use knot_git::Repo;
5use knot_maintenance::run_repo;
6
7mod common;
8use common::{git_available, now, options};
9
10fn skip() -> bool {
11 if git_available() {
12 return false;
13 }
14 eprintln!("skipping commit-graph differential: git unavailable");
15 true
16}
17
18fn git_at(dir: &Path, date: i64, args: &[&str]) {
19 let stamp = format!("{date} +0000");
20 let out = knot_fixtures::command_at(dir, &stamp)
21 .args(args)
22 .output()
23 .expect("git runs");
24 assert!(
25 out.status.success(),
26 "git {args:?}: {}",
27 String::from_utf8_lossy(&out.stderr)
28 );
29}
30
31fn git(dir: &Path, args: &[&str]) {
32 git_at(dir, 1_700_000_000, args);
33}
34
35fn write_file(dir: &Path, rel: &str, contents: &str) {
36 let path = dir.join(rel);
37 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
38 std::fs::write(path, contents).unwrap();
39}
40
41fn commit(dir: &Path, rel: &str, contents: &str, message: &str) {
42 write_file(dir, rel, contents);
43 git(dir, &["add", "-A"]);
44 git(dir, &["commit", "-m", message]);
45}
46
47fn seed_history(dir: &Path, format: &str) {
48 git(dir, &["init", "--object-format", format, "-b", "main"]);
49 write_file(dir, "dir1/a.txt", "a1");
50 write_file(dir, "dir1/sub/b.txt", "b1");
51 commit(dir, "c.txt", "c1", "root");
52 commit(dir, "dir1/sub/b.txt", "b2", "edit nested");
53 git(dir, &["checkout", "-b", "feature"]);
54 commit(dir, "c.txt", "c2", "feature edit");
55 git(dir, &["checkout", "main"]);
56 commit(dir, "dir2/d.txt", "d1", "add dir2");
57 git(dir, &["merge", "--no-ff", "-m", "merge feature", "feature"]);
58 git(dir, &["tag", "v1"]);
59}
60
61fn read_chunks(bytes: &[u8]) -> HashMap<[u8; 4], Vec<u8>> {
62 let count = bytes[6] as usize;
63 let table = &bytes[8..8 + (count + 1) * 12];
64 let entry = |index: usize| -> ([u8; 4], u64) {
65 let base = index * 12;
66 let id = table[base..base + 4].try_into().unwrap();
67 (
68 id,
69 u64::from_be_bytes(table[base + 4..base + 12].try_into().unwrap()),
70 )
71 };
72 (0..count)
73 .map(|index| {
74 let (id, start) = entry(index);
75 let (_, end) = entry(index + 1);
76 (id, bytes[start as usize..end as usize].to_vec())
77 })
78 .collect()
79}
80
81fn graph_verify(root: &Path) -> (bool, String) {
82 let out = knot_fixtures::command(root)
83 .args(["commit-graph", "verify"])
84 .output()
85 .expect("git commit-graph verify runs");
86 (
87 out.status.success(),
88 String::from_utf8_lossy(&out.stderr).into_owned(),
89 )
90}
91
92fn path_log(dir: &Path, path: &str, use_graph: bool) -> String {
93 let out = knot_fixtures::command(dir)
94 .args([
95 "-c",
96 &format!("core.commitGraph={use_graph}"),
97 "-c",
98 "commitGraph.readChangedPaths=true",
99 "log",
100 "--format=%H",
101 "--",
102 path,
103 ])
104 .output()
105 .expect("git log runs");
106 assert!(out.status.success());
107 String::from_utf8(out.stdout).unwrap()
108}
109
110fn build_and_compare(root: &Path, format: &str, chunks: &[&[u8; 4]]) {
111 git(
112 root,
113 &[
114 "-c",
115 "commitGraph.changedPathsVersion=2",
116 "commit-graph",
117 "write",
118 "--reachable",
119 "--changed-paths",
120 ],
121 );
122 let graph_file = root.join(".git/objects/info/commit-graph");
123 let canonical = read_chunks(&std::fs::read(&graph_file).unwrap());
124
125 assert!(
126 run_repo(&Repo::open(root).unwrap(), now(), &options())
127 .unwrap()
128 .commit_graph,
129 "knot wrote a graph ({format})"
130 );
131 let ours = read_chunks(&std::fs::read(&graph_file).unwrap());
132 chunks.iter().for_each(|id| {
133 assert_eq!(
134 ours.get(*id),
135 canonical.get(*id),
136 "chunk {} differs ({format})",
137 String::from_utf8_lossy(*id)
138 );
139 });
140 let (ok, stderr) = graph_verify(root);
141 assert!(ok, "git commit-graph verify failed ({format}): {stderr}");
142}
143
144fn check_against_git(format: &str) {
145 if skip() {
146 return;
147 }
148 let dir = tempfile::tempdir().unwrap();
149 let root = dir.path();
150 seed_history(root, format);
151 build_and_compare(root, format, &[b"OIDL", b"CDAT", b"GDA2", b"BIDX", b"BDAT"]);
152
153 ["dir1/sub/b.txt", "dir1/sub", "dir1", "c.txt", "dir2/d.txt"]
154 .iter()
155 .for_each(|path| {
156 assert_eq!(
157 path_log(root, path, true),
158 path_log(root, path, false),
159 "changed-path bloom altered `git log -- {path}` ({format})"
160 );
161 });
162}
163
164#[test]
165fn matches_canonical_git_sha1() {
166 check_against_git("sha1");
167}
168
169#[test]
170fn matches_canonical_git_sha256() {
171 check_against_git("sha256");
172}
173
174#[test]
175fn writing_the_graph_clears_a_pre_existing_split_chain() {
176 if skip() {
177 return;
178 }
179 let dir = tempfile::tempdir().unwrap();
180 let root = dir.path();
181 seed_history(root, "sha1");
182
183 git(root, &["commit-graph", "write", "--reachable", "--split"]);
184 let chain = root.join(".git/objects/info/commit-graphs");
185 assert!(chain.exists(), "git wrote a split commit-graph chain");
186
187 assert!(
188 run_repo(&Repo::open(root).unwrap(), now(), &options())
189 .unwrap()
190 .commit_graph,
191 "knot wrote a monolithic graph"
192 );
193 assert!(
194 !chain.exists(),
195 "the stale split chain is removed so it cannot shadow the fresh graph"
196 );
197 assert!(root.join(".git/objects/info/commit-graph").exists());
198 let (ok, stderr) = graph_verify(root);
199 assert!(
200 ok,
201 "git verify passes after the chain is replaced: {stderr}"
202 );
203}
204
205#[test]
206fn large_filter_sentinel_matches_git_when_dirs_overflow_the_limit() {
207 if skip() {
208 return;
209 }
210 let dir = tempfile::tempdir().unwrap();
211 let root = dir.path();
212 git(root, &["init", "--object-format", "sha1", "-b", "main"]);
213 (0..200).for_each(|i| write_file(root, &format!("a{i}/b{i}/c.txt"), "x"));
214 git(root, &["add", "-A"]);
215 git(root, &["commit", "-m", "wide refactor"]);
216 build_and_compare(root, "sha1", &[b"BIDX", b"BDAT"]);
217}
218
219#[test]
220fn corrected_date_overflow_matches_git() {
221 if skip() {
222 return;
223 }
224 let dir = tempfile::tempdir().unwrap();
225 let root = dir.path();
226 git(root, &["init", "--object-format", "sha1", "-b", "main"]);
227 write_file(root, "a.txt", "1");
228 git_at(root, 4_000_000_000, &["add", "-A"]);
229 git_at(root, 4_000_000_000, &["commit", "-m", "far-future root"]);
230 write_file(root, "a.txt", "2");
231 git_at(root, 1_000_000_000, &["add", "-A"]);
232 git_at(root, 1_000_000_000, &["commit", "-m", "past child"]);
233 build_and_compare(root, "sha1", &[b"CDAT", b"GDA2", b"GDO2"]);
234}
235
236#[test]
237fn changed_path_filter_matches_git_for_high_byte_names() {
238 if skip() {
239 return;
240 }
241 let dir = tempfile::tempdir().unwrap();
242 let root = dir.path();
243 git(root, &["init", "--object-format", "sha1", "-b", "main"]);
244 write_file(root, "café/résumé.txt", "1");
245 commit(root, "naïve.md", "2", "non-ascii paths");
246 commit(root, "café/résumé.txt", "2", "edit non-ascii");
247 build_and_compare(root, "sha1", &[b"BIDX", b"BDAT"]);
248}