forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
10.1 KiB
1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use forge_git::browse::{
5 EntryKind, RefKind, default_branch_commit, list_refs, read_blob, read_commit, read_tree,
6 resolve_commit, split_ref_and_path, validate_repo_path,
7};
8
9struct Fixture {
10 _dir: tempfile::TempDir,
11 bare: PathBuf,
12 head: String,
13 first: String,
14}
15
16fn git(dir: &Path, args: &[&str]) -> String {
17 let out = Command::new("git")
18 .args(args)
19 .current_dir(dir)
20 .env("GIT_AUTHOR_NAME", "Ada Lovelace")
21 .env("GIT_AUTHOR_EMAIL", "ada@example.com")
22 .env("GIT_COMMITTER_NAME", "Grace Hopper")
23 .env("GIT_COMMITTER_EMAIL", "grace@example.com")
24 .env("GIT_AUTHOR_DATE", "1700000000 +0000")
25 .env("GIT_COMMITTER_DATE", "1700000100 +0000")
26 .env("GIT_CONFIG_NOSYSTEM", "1")
27 .env("HOME", dir)
28 .output()
29 .expect("git must be installed");
30 assert!(
31 out.status.success(),
32 "git {args:?} failed: {}",
33 String::from_utf8_lossy(&out.stderr)
34 );
35 String::from_utf8_lossy(&out.stdout).trim().to_string()
36}
37
38/// Builds a work repo with the real git CLI, then clones it bare so tests read a bare repo.
39fn fixture() -> Fixture {
40 let dir = tempfile::tempdir().unwrap();
41 let work = dir.path().join("work");
42 std::fs::create_dir(&work).unwrap();
43 git(&work, &["init", "-q", "-b", "main"]);
44
45 std::fs::write(work.join("README.md"), "# Hello\nsecond line\n").unwrap();
46 std::fs::create_dir_all(work.join("dir/sub")).unwrap();
47 std::fs::write(work.join("dir/sub/nested.txt"), "nested\n").unwrap();
48 std::fs::write(work.join("dir/file.txt"), "file body\n").unwrap();
49 std::fs::create_dir(work.join("bin")).unwrap();
50 std::fs::write(work.join("bin/run.sh"), "#!/bin/sh\necho hi\n").unwrap();
51 {
52 use std::os::unix::fs::PermissionsExt;
53 std::fs::set_permissions(
54 work.join("bin/run.sh"),
55 std::fs::Permissions::from_mode(0o755),
56 )
57 .unwrap();
58 }
59 std::os::unix::fs::symlink("README.md", work.join("link")).unwrap();
60 std::fs::write(work.join("image.bin"), [0x89, b'P', b'N', b'G', 0, 1, 2, 3]).unwrap();
61
62 git(&work, &["add", "-A"]);
63 git(&work, &["commit", "-q", "-m", "first commit\n\nbody text"]);
64 let first = git(&work, &["rev-parse", "HEAD"]);
65
66 std::fs::write(work.join("dir/file.txt"), "file body v2\n").unwrap();
67 git(&work, &["commit", "-q", "-am", "second commit"]);
68 let head = git(&work, &["rev-parse", "HEAD"]);
69
70 git(&work, &["tag", "-a", "v1.0", "-m", "release 1.0"]);
71 git(&work, &["branch", "feature/x"]);
72
73 let bare = dir.path().join("bare.git");
74 git(
75 dir.path(),
76 &[
77 "clone",
78 "-q",
79 "--bare",
80 work.to_str().unwrap(),
81 bare.to_str().unwrap(),
82 ],
83 );
84
85 Fixture {
86 _dir: dir,
87 bare,
88 head,
89 first,
90 }
91}
92
93#[test]
94fn list_refs_has_branches_and_peeled_tags() {
95 let f = fixture();
96 let refs = list_refs(&f.bare).unwrap();
97
98 let main = refs
99 .iter()
100 .find(|r| r.full_name == "refs/heads/main")
101 .unwrap();
102 assert_eq!(main.name, "main");
103 assert_eq!(main.kind, RefKind::Branch);
104 assert_eq!(main.target, f.head);
105
106 let feature = refs
107 .iter()
108 .find(|r| r.name == "feature/x")
109 .expect("feature/x listed");
110 assert_eq!(feature.full_name, "refs/heads/feature/x");
111 assert_eq!(feature.kind, RefKind::Branch);
112
113 let tag = refs.iter().find(|r| r.name == "v1.0").unwrap();
114 assert_eq!(tag.full_name, "refs/tags/v1.0");
115 assert_eq!(tag.kind, RefKind::Tag);
116 assert_eq!(tag.target, f.head, "annotated tag must peel to its commit");
117}
118
119#[test]
120fn default_branch_commit_works() {
121 let f = fixture();
122 assert_eq!(
123 default_branch_commit(&f.bare, "main").unwrap(),
124 Some(f.head.clone())
125 );
126 assert_eq!(default_branch_commit(&f.bare, "nope").unwrap(), None);
127 assert_eq!(default_branch_commit(&f.bare, "../main").unwrap(), None);
128}
129
130#[test]
131fn resolve_commit_accepts_branch_tag_and_short_sha() {
132 let f = fixture();
133 assert_eq!(
134 resolve_commit(&f.bare, "main").unwrap(),
135 Some(f.head.clone())
136 );
137 assert_eq!(
138 resolve_commit(&f.bare, "feature/x").unwrap(),
139 Some(f.head.clone())
140 );
141 assert_eq!(
142 resolve_commit(&f.bare, "v1.0").unwrap(),
143 Some(f.head.clone()),
144 "tag peels to commit"
145 );
146 assert_eq!(
147 resolve_commit(&f.bare, &f.first[..8]).unwrap(),
148 Some(f.first.clone())
149 );
150 assert_eq!(
151 resolve_commit(&f.bare, &f.head.to_uppercase()).unwrap(),
152 Some(f.head.clone()),
153 "full uppercase hex resolves"
154 );
155}
156
157#[test]
158fn resolve_commit_rejects_revision_syntax_and_unknown() {
159 let f = fixture();
160 for rev in [
161 "HEAD~1",
162 "-x",
163 "a..b",
164 "@{0}",
165 "HEAD",
166 "main^",
167 "main:README.md",
168 "ma in",
169 "",
170 "zzzz",
171 "nope",
172 ] {
173 assert_eq!(resolve_commit(&f.bare, rev).unwrap(), None, "rev {rev:?}");
174 }
175 // 3 hex chars is below the minimum prefix length.
176 assert_eq!(resolve_commit(&f.bare, &f.head[..3]).unwrap(), None);
177}
178
179#[test]
180fn split_ref_and_path_prefers_longest_ref() {
181 let f = fixture();
182 let got = split_ref_and_path(&f.bare, "feature/x/dir/file.txt").unwrap();
183 assert_eq!(
184 got,
185 Some((
186 "feature/x".to_string(),
187 f.head.clone(),
188 "dir/file.txt".to_string()
189 ))
190 );
191 assert_eq!(
192 split_ref_and_path(&f.bare, "main").unwrap(),
193 Some(("main".to_string(), f.head.clone(), String::new()))
194 );
195 let by_oid = split_ref_and_path(&f.bare, &format!("{}/README.md", &f.first[..10])).unwrap();
196 assert_eq!(
197 by_oid,
198 Some((
199 f.first[..10].to_string(),
200 f.first.clone(),
201 "README.md".to_string()
202 ))
203 );
204 assert_eq!(split_ref_and_path(&f.bare, "unknown/x").unwrap(), None);
205 assert_eq!(split_ref_and_path(&f.bare, "main/../etc").unwrap(), None);
206}
207
208#[test]
209fn read_tree_orders_dirs_first_and_reports_kinds() {
210 let f = fixture();
211 let root = read_tree(&f.bare, &f.head, "").unwrap().unwrap();
212 let names: Vec<(&str, EntryKind)> = root.iter().map(|e| (e.name.as_str(), e.kind)).collect();
213 assert_eq!(
214 names,
215 vec![
216 ("bin", EntryKind::Tree),
217 ("dir", EntryKind::Tree),
218 ("README.md", EntryKind::Blob),
219 ("image.bin", EntryKind::Blob),
220 ("link", EntryKind::Symlink),
221 ]
222 );
223 let dir = root.iter().find(|e| e.name == "dir").unwrap();
224 assert_eq!(dir.size, None);
225 let readme = root.iter().find(|e| e.name == "README.md").unwrap();
226 assert_eq!(readme.size, Some("# Hello\nsecond line\n".len() as u64));
227 let link = root.iter().find(|e| e.name == "link").unwrap();
228 assert_eq!(link.size, Some("README.md".len() as u64));
229
230 let sub = read_tree(&f.bare, &f.head, "dir").unwrap().unwrap();
231 let names: Vec<&str> = sub.iter().map(|e| e.name.as_str()).collect();
232 assert_eq!(names, vec!["sub", "file.txt"]);
233
234 let bin = read_tree(&f.bare, &f.head, "bin").unwrap().unwrap();
235 assert_eq!(bin[0].kind, EntryKind::Executable);
236
237 assert_eq!(read_tree(&f.bare, &f.head, "README.md").unwrap(), None);
238 assert_eq!(read_tree(&f.bare, &f.head, "missing").unwrap(), None);
239 assert!(read_tree(&f.bare, &f.head, "../x").is_err());
240}
241
242#[test]
243fn read_blob_truncates_and_detects_binary_and_symlink() {
244 let f = fixture();
245 let readme = read_blob(&f.bare, &f.head, "README.md", 5)
246 .unwrap()
247 .unwrap();
248 assert_eq!(readme.kind, EntryKind::Blob);
249 assert_eq!(readme.data, b"# Hel");
250 assert!(readme.truncated);
251 assert!(!readme.is_binary);
252 assert_eq!(readme.size, "# Hello\nsecond line\n".len() as u64);
253
254 let full = read_blob(&f.bare, &f.head, "README.md", 1024)
255 .unwrap()
256 .unwrap();
257 assert!(!full.truncated);
258 assert_eq!(full.data, b"# Hello\nsecond line\n");
259
260 let bin = read_blob(&f.bare, &f.head, "image.bin", 1024)
261 .unwrap()
262 .unwrap();
263 assert!(bin.is_binary);
264
265 let exec = read_blob(&f.bare, &f.head, "bin/run.sh", 1024)
266 .unwrap()
267 .unwrap();
268 assert_eq!(exec.kind, EntryKind::Executable);
269
270 let link = read_blob(&f.bare, &f.head, "link", 1024).unwrap().unwrap();
271 assert_eq!(link.kind, EntryKind::Symlink);
272 assert_eq!(link.data, b"README.md");
273
274 assert_eq!(read_blob(&f.bare, &f.head, "dir", 1024).unwrap(), None);
275 assert_eq!(read_blob(&f.bare, &f.head, "nope", 1024).unwrap(), None);
276}
277
278#[test]
279fn read_commit_fields() {
280 let f = fixture();
281 let head = read_commit(&f.bare, &f.head).unwrap().unwrap();
282 assert_eq!(head.oid, f.head);
283 assert_eq!(head.parents, vec![f.first.clone()]);
284 assert_eq!(head.author_name, "Ada Lovelace");
285 assert_eq!(head.author_email, "ada@example.com");
286 assert_eq!(head.author_time, 1_700_000_000);
287 assert_eq!(head.committer_name, "Grace Hopper");
288 assert_eq!(head.committer_email, "grace@example.com");
289 assert_eq!(head.committer_time, 1_700_000_100);
290 assert_eq!(head.summary, "second commit");
291
292 let first = read_commit(&f.bare, &f.first).unwrap().unwrap();
293 assert!(first.parents.is_empty());
294 assert_eq!(first.summary, "first commit");
295 assert_eq!(first.message, "first commit\n\nbody text\n");
296
297 assert_eq!(
298 read_commit(&f.bare, "0123456789012345678901234567890123456789").unwrap(),
299 None
300 );
301}
302
303#[test]
304fn validate_repo_path_cases() {
305 assert!(validate_repo_path("").is_ok());
306 assert!(validate_repo_path("a").is_ok());
307 assert!(validate_repo_path("a/b.txt").is_ok());
308 assert!(validate_repo_path(".hidden/x").is_ok());
309 assert!(validate_repo_path("a/".repeat(2047).trim_end_matches('/')).is_ok());
310 assert!(validate_repo_path("/a").is_err());
311 assert!(validate_repo_path("a/").is_err());
312 assert!(validate_repo_path("a//b").is_err());
313 assert!(validate_repo_path("a/./b").is_err());
314 assert!(validate_repo_path("a/../b").is_err());
315 assert!(validate_repo_path("..").is_err());
316 assert!(validate_repo_path("a\0b").is_err());
317 assert!(validate_repo_path(&"a".repeat(4097)).is_err());
318}