This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-maintenance / tests / commitgraph.rs
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}