This repository has no description
1#![allow(dead_code, unused_imports)]
2
3use std::collections::BTreeSet;
4use std::ops::Range;
5use std::path::Path;
6
7use knot_git::{
8 EntryKind, Identity, Layout, NewCommit, RefUpdate, Repo, StagedAction, StagedChange,
9};
10use knot_maintenance::{GeometricFactor, ObjectCount, Options, PruneGrace, ReflogRetention};
11use knot_types::{AuthorName, BranchName, Email, ObjectFormat, Oid, RefName, RepoDid, UnixSeconds};
12
13pub const EMPTY_TREE_SHA1: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
14pub const EMPTY_TREE_SHA256: &str =
15 "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321";
16
17pub fn empty_tree(format: ObjectFormat) -> &'static str {
18 if format == ObjectFormat::SHA256 {
19 EMPTY_TREE_SHA256
20 } else {
21 EMPTY_TREE_SHA1
22 }
23}
24
25pub fn now() -> UnixSeconds {
26 UnixSeconds::new(1_700_000_500)
27}
28
29pub use knot_fixtures::available as git_available;
30
31pub fn identity() -> Identity {
32 Identity {
33 name: AuthorName::new("nel"),
34 email: Email::new("nel@oyster.cafe"),
35 time: UnixSeconds::new(1_700_000_000),
36 offset_seconds: 0,
37 }
38}
39
40pub fn options() -> Options {
41 Options {
42 repack_max_objects: ObjectCount::new(1_000_000),
43 geometric_factor: GeometricFactor::full_repack(),
44 prune_grace: PruneGrace::from_secs(0),
45 reflog_floor: ReflogRetention::from_secs(i64::MAX as u64 / 4),
46 commit_graph: true,
47 multi_pack_index: true,
48 bitmap: true,
49 }
50}
51
52pub fn create_repo(scan: &Path, format: ObjectFormat, did: &str) -> Repo {
53 Layout::new(scan)
54 .with_object_format(format)
55 .with_default_branch(BranchName::new("main").unwrap())
56 .create(&RepoDid::new(did).unwrap())
57 .unwrap()
58}
59
60pub fn commit_on(repo: &Repo, empty_tree: &str, body: u8, parents: Vec<Oid>) -> Oid {
61 let tree = repo
62 .write_staged_tree(
63 Oid::from_hex(empty_tree).unwrap(),
64 &[StagedChange {
65 path: knot_types::RepoPath::new(format!("file{body}.txt")).unwrap(),
66 action: StagedAction::Put {
67 content: vec![body, body, body],
68 kind: EntryKind::Blob,
69 },
70 }],
71 )
72 .unwrap();
73 repo.write_commit(&NewCommit {
74 tree,
75 parents,
76 author: identity(),
77 committer: identity(),
78 message: format!("commit {body}"),
79 extra_headers: Vec::new(),
80 })
81 .unwrap()
82}
83
84pub fn commit(repo: &Repo, body: u8, parents: Vec<Oid>) -> Oid {
85 commit_on(repo, EMPTY_TREE_SHA1, body, parents)
86}
87
88pub fn chain(repo: &Repo, empty_tree: &str, bodies: Range<u8>, start: Option<Oid>) -> Oid {
89 bodies
90 .fold(start, |parent, body| {
91 let parents = parent.map(|tip| vec![tip]).unwrap_or_default();
92 Some(commit_on(repo, empty_tree, body, parents))
93 })
94 .expect("a non-empty body range yields a tip")
95}
96
97pub fn set_ref(repo: &Repo, name: &str, new: Oid) {
98 let refname = RefName::new(name).unwrap();
99 let update = match repo.find_ref(&refname).unwrap() {
100 Some(old) => RefUpdate::Update {
101 name: refname,
102 old,
103 new,
104 },
105 None => RefUpdate::Create { name: refname, new },
106 };
107 repo.update_ref(&update).unwrap();
108}
109
110pub fn delete_ref(repo: &Repo, name: &str) {
111 let refname = RefName::new(name).unwrap();
112 let old = repo.find_ref(&refname).unwrap().unwrap();
113 repo.update_ref(&RefUpdate::Delete { name: refname, old })
114 .unwrap();
115}
116
117pub fn set_reflog_seconds(repo: &Repo, name: &str, seconds: &[i64]) {
118 let path = repo.git().git_dir().join("logs").join(name);
119 let text = std::fs::read_to_string(&path).expect("reflog file exists");
120 let lines: Vec<&str> = text.lines().collect();
121 assert_eq!(
122 lines.len(),
123 seconds.len(),
124 "set_reflog_seconds needs one timestamp per reflog line"
125 );
126 let rewritten = lines
127 .iter()
128 .zip(seconds)
129 .map(|(line, secs)| rewrite_reflog_seconds(line, *secs))
130 .collect::<Vec<_>>()
131 .join("\n");
132 std::fs::write(&path, format!("{rewritten}\n")).expect("rewrite reflog");
133}
134
135fn rewrite_reflog_seconds(line: &str, secs: i64) -> String {
136 let (meta, message) = line
137 .split_once('\t')
138 .expect("reflog line has a message tab");
139 let tokens: Vec<&str> = meta.split(' ').collect();
140 let tz = tokens.last().expect("reflog line has a timezone");
141 let head = tokens[..tokens.len() - 2].join(" ");
142 format!("{head} {secs} {tz}\t{message}")
143}
144
145pub fn git(repo: &Repo, args: &[&str]) -> (bool, String) {
146 let out = knot_fixtures::command(repo.git().git_dir())
147 .args(args)
148 .output()
149 .expect("git runs");
150 (
151 out.status.success(),
152 String::from_utf8_lossy(&out.stderr).into_owned(),
153 )
154}
155
156pub fn fsck(repo: &Repo) -> (bool, String) {
157 git(repo, &["fsck", "--no-dangling", "--no-progress"])
158}
159
160pub fn fsck_clean(repo: &Repo) -> bool {
161 fsck(repo).0
162}
163
164pub fn assert_fsck_clean(repo: &Repo) {
165 let (clean, stderr) = fsck(repo);
166 assert!(clean, "fsck failed: {stderr}");
167}
168
169pub fn reachable_objects(repo: &Repo) -> BTreeSet<String> {
170 let out = knot_fixtures::command(repo.git().git_dir())
171 .args(["rev-list", "--objects", "--all"])
172 .output()
173 .expect("git rev-list runs");
174 assert!(out.status.success());
175 String::from_utf8_lossy(&out.stdout)
176 .lines()
177 .filter_map(|line| line.split_whitespace().next())
178 .map(str::to_string)
179 .collect()
180}
181
182pub fn midx_verifies(repo: &Repo) -> Option<(bool, String)> {
183 let midx = repo.git().git_dir().join("objects/pack/multi-pack-index");
184 midx.exists()
185 .then(|| git(repo, &["multi-pack-index", "verify"]))
186}
187
188fn pack_entries(repo: &Repo) -> impl Iterator<Item = std::path::PathBuf> {
189 std::fs::read_dir(repo.git().git_dir().join("objects/pack"))
190 .into_iter()
191 .flatten()
192 .filter_map(Result::ok)
193 .map(|entry| entry.path())
194}
195
196pub fn idx_stems(repo: &Repo) -> BTreeSet<String> {
197 pack_entries(repo)
198 .filter(|path| path.extension().is_some_and(|ext| ext == "idx"))
199 .filter_map(|path| {
200 path.file_stem()
201 .and_then(|stem| stem.to_str())
202 .map(str::to_string)
203 })
204 .collect()
205}
206
207pub fn has_cruft_pack(repo: &Repo) -> bool {
208 pack_entries(repo).any(|path| path.extension().is_some_and(|ext| ext == "mtimes"))
209}
210
211pub fn has_bitmap(repo: &Repo) -> bool {
212 pack_entries(repo).any(|path| path.extension().is_some_and(|ext| ext == "bitmap"))
213}
214
215pub fn has_midx_bitmap(repo: &Repo) -> bool {
216 pack_entries(repo)
217 .filter_map(|path| {
218 path.file_name()
219 .and_then(|n| n.to_str())
220 .map(str::to_string)
221 })
222 .any(|name| name.starts_with("multi-pack-index-") && name.ends_with(".bitmap"))
223}