This repository has no description
1use std::path::Path;
2
3use knot_git::{CommitRange, EntryKind, FileChange, Layout, LineCount, LogLimit, LogSkip, Repo};
4use knot_types::{Listing, Oid, RefName, RepoDid, RepoPath};
5
6fn rp(path: &str) -> RepoPath {
7 RepoPath::new(path).unwrap()
8}
9
10mod common;
11use common::{commit_file, contains, git_ok as git};
12
13#[test]
14fn typed_reads_over_a_seeded_repo() {
15 let scan = tempfile::tempdir().unwrap();
16 let layout = Layout::new(scan.path());
17 let did = RepoDid::new("did:plc:squid").unwrap();
18 let bare = layout.create(&did).unwrap();
19 let bare_path = layout.repo_path(&did).unwrap();
20 let bare_str = bare_path.to_str().unwrap();
21
22 let work_dir = tempfile::tempdir().unwrap();
23 let work = work_dir.path();
24 git(work, &["init", "-q", "-b", "main"]);
25 commit_file(work, "a.txt", "one\n", "first");
26 git(work, &["push", "-q", bare_str, "main"]);
27 commit_file(work, "b.txt", "two\n", "second");
28 std::fs::write(work.join("a.txt"), "one updated\n").unwrap();
29 git(work, &["add", "-A"]);
30 git(work, &["commit", "-q", "-m", "third"]);
31 git(work, &["push", "-q", bare_str, "main"]);
32 git(
33 bare_path.as_path(),
34 &["symbolic-ref", "HEAD", "refs/heads/main"],
35 );
36
37 let head = Oid::from_hex(&git(work, &["rev-parse", "HEAD"])).unwrap();
38 let parent = Oid::from_hex(&git(work, &["rev-parse", "HEAD~1"])).unwrap();
39 let root = Oid::from_hex(&git(work, &["rev-parse", "HEAD~2"])).unwrap();
40 let tree = Oid::from_hex(&git(work, &["rev-parse", "HEAD^{tree}"])).unwrap();
41 let blob_b = Oid::from_hex(&git(work, &["rev-parse", "HEAD:b.txt"])).unwrap();
42
43 let head_ref = bare.head().expect("HEAD resolves");
44 assert_eq!(head_ref.name.as_str(), "refs/heads/main");
45 assert_eq!(head_ref.target, head);
46
47 let commit = bare.find_commit(head).unwrap();
48 assert_eq!(commit.id, head);
49 assert_eq!(commit.tree, tree);
50 assert_eq!(commit.parents, vec![parent]);
51 assert_eq!(commit.author.name.as_str(), "nel");
52 assert!(commit.message.starts_with("third"));
53
54 let tree_entries = bare.find_tree(tree).unwrap();
55 let names: Vec<&str> = tree_entries
56 .entries
57 .iter()
58 .map(|entry| entry.name.as_str())
59 .collect();
60 assert!(names.contains(&"a.txt"));
61 assert!(names.contains(&"b.txt"));
62 assert!(
63 tree_entries
64 .entries
65 .iter()
66 .all(|entry| entry.kind == EntryKind::Blob)
67 );
68
69 assert_eq!(bare.read_blob(blob_b).unwrap(), b"two\n");
70
71 let changes = bare.diff(CommitRange { base: root, head }).unwrap();
72 assert!(changes.iter().any(
73 |change| matches!(change, FileChange::Added { path, .. } if path.as_str() == "b.txt")
74 ));
75 assert!(changes.iter().any(
76 |change| matches!(change, FileChange::Modified { path, .. } if path.as_str() == "a.txt")
77 ));
78
79 let comparison = bare.compare(CommitRange { base: root, head }).unwrap();
80 assert_eq!(comparison.commits.len(), 2);
81 assert!(comparison.commits.contains(&head));
82 assert!(comparison.commits.contains(&parent));
83 assert_eq!(comparison.changes, changes);
84}
85
86fn seed_main() -> (tempfile::TempDir, Layout, RepoDid, std::path::PathBuf, Oid) {
87 let scan = tempfile::tempdir().unwrap();
88 let layout = Layout::new(scan.path());
89 let did = RepoDid::new("did:plc:squid").unwrap();
90 layout.create(&did).unwrap();
91 let bare_path = layout.repo_path(&did).unwrap();
92
93 let work_dir = tempfile::tempdir().unwrap();
94 let work = work_dir.path();
95 git(work, &["init", "-q", "-b", "main"]);
96 commit_file(work, "a.txt", "one\n", "first");
97 git(work, &["push", "-q", bare_path.to_str().unwrap(), "main"]);
98 let head = Oid::from_hex(&git(work, &["rev-parse", "HEAD"])).unwrap();
99 (scan, layout, did, bare_path, head)
100}
101
102fn seed_rich() -> (tempfile::TempDir, tempfile::TempDir, Layout, RepoDid) {
103 let scan = tempfile::tempdir().unwrap();
104 let layout = Layout::new(scan.path());
105 let did = RepoDid::new("did:plc:squid").unwrap();
106 layout.create(&did).unwrap();
107 let bare_path = layout.repo_path(&did).unwrap();
108
109 let work_dir = tempfile::tempdir().unwrap();
110 let work = work_dir.path();
111 git(work, &["init", "-q", "-b", "main"]);
112 commit_file(work, "a.txt", "one\ntwo\nthree\n", "first");
113 std::fs::create_dir_all(work.join("src")).unwrap();
114 commit_file(work, "src/lib.rs", "pub fn nel() {}\n", "add lib");
115 commit_file(work, "a.txt", "one\ntwo\nthree\nfour\n", "extend a");
116 git(work, &["tag", "light"]);
117 git(work, &["tag", "-a", "v1.0.0", "-m", "release one"]);
118 commit_file(
119 work,
120 "src/lib.rs",
121 "pub fn nel() {}\npub fn teq() {}\n",
122 "extend lib",
123 );
124 git(
125 work,
126 &["push", "-q", "--tags", bare_path.to_str().unwrap(), "main"],
127 );
128 git(
129 bare_path.as_path(),
130 &["symbolic-ref", "HEAD", "refs/heads/main"],
131 );
132 (scan, work_dir, layout, did)
133}
134
135#[test]
136fn typed_reads_over_a_rich_repo() {
137 let (_scan, work_dir, layout, did) = seed_rich();
138 let work = work_dir.path();
139 let bare = layout.open(&did).unwrap();
140 let head = Oid::from_hex(&git(work, &["rev-parse", "HEAD"])).unwrap();
141 let parent = Oid::from_hex(&git(work, &["rev-parse", "HEAD~1"])).unwrap();
142 let base = Oid::from_hex(&git(work, &["rev-parse", "HEAD~2"])).unwrap();
143 let root = Oid::from_hex(&git(work, &["rev-parse", "HEAD~3"])).unwrap();
144
145 assert_eq!(bare.resolve_revision("main"), Some(head));
146 assert_eq!(bare.resolve_revision("HEAD"), Some(head));
147 assert_eq!(bare.resolve_revision(&head.to_hex()), Some(head));
148 assert_eq!(bare.resolve_revision("does-not-exist"), None);
149 let tag_object = bare.resolve_revision("v1.0.0").unwrap();
150 assert_eq!(
151 bare.peel_to_commit(tag_object).unwrap(),
152 Oid::from_hex(&git(work, &["rev-parse", "v1.0.0^{commit}"])).unwrap(),
153 "annotated tag peels to its commit"
154 );
155
156 let expected: Vec<String> = git(work, &["rev-list", "HEAD"])
157 .lines()
158 .map(str::to_string)
159 .collect();
160 let (walked, total) = bare
161 .log_window(head, LogSkip::new(0), LogLimit::new(100))
162 .unwrap();
163 let walked: Vec<String> = walked.iter().map(|commit| commit.id.to_hex()).collect();
164 assert_eq!(walked, expected, "log order matches git rev-list");
165 assert_eq!(total, expected.len());
166
167 let (page, total) = bare
168 .log_window(head, LogSkip::new(1), LogLimit::new(2))
169 .unwrap();
170 assert_eq!(page.len(), 2);
171 assert_eq!(page[0].id.to_hex(), expected[1]);
172 assert_eq!(page[1].id.to_hex(), expected[2]);
173 assert_eq!(total, expected.len(), "window still reports full count");
174
175 let between: Vec<String> = bare
176 .commits_between(CommitRange { base, head }, LogLimit::new(100))
177 .unwrap()
178 .iter()
179 .map(|oid| oid.to_hex())
180 .collect();
181 let expected_between: Vec<String> =
182 git(work, &["rev-list", &format!("{}..HEAD", base.to_hex())])
183 .lines()
184 .map(str::to_string)
185 .collect();
186 assert_eq!(between, expected_between);
187 assert_eq!(
188 bare.commits_between(CommitRange { base, head }, LogLimit::new(1))
189 .unwrap()
190 .len(),
191 1,
192 "walk stops at limit instead of collecting full range"
193 );
194 assert_eq!(bare.merge_base(head, base).unwrap(), Some(base));
195
196 let commit = bare.find_commit(head).unwrap();
197 assert_eq!(commit.author.name.as_str(), "nel");
198 assert!(commit.pgp_signature.is_none());
199 assert!(commit.extra_headers.is_empty());
200 assert!(commit.change_id().is_none());
201
202 let branches = bare.branch_list().unwrap();
203 assert_eq!(branches.len(), 1);
204 assert_eq!(branches[0].name.as_str(), "main");
205 assert_eq!(branches[0].tip.id(), head);
206 assert!(matches!(branches[0].tip, knot_git::BranchTip::Commit(_)));
207
208 let tags = bare.tag_list().unwrap();
209 assert_eq!(tags.len(), 2);
210 let light = tags
211 .iter()
212 .find(|tag| tag.name.as_str() == "light")
213 .unwrap();
214 assert!(light.annotated.is_none());
215 assert!(light.message.starts_with("extend a"));
216 let annotated = tags
217 .iter()
218 .find(|tag| tag.name.as_str() == "v1.0.0")
219 .unwrap();
220 let detail = annotated.annotated.as_ref().unwrap();
221 assert_eq!(annotated.message, "release one\n");
222 assert_eq!(detail.tagger.as_ref().unwrap().name.as_str(), "nel");
223 assert_eq!(
224 detail.target,
225 Oid::from_hex(&git(work, &["rev-parse", "v1.0.0^{commit}"])).unwrap()
226 );
227 assert_eq!(
228 annotated.id,
229 Oid::from_hex(&git(work, &["rev-parse", "v1.0.0"])).unwrap(),
230 "tag info id is tag object itself"
231 );
232
233 let tree_root = bare.tree_entries_at(head, None).unwrap().unwrap();
234 let names: Vec<&str> = tree_root.iter().map(|entry| entry.name.as_str()).collect();
235 assert_eq!(names, vec!["a.txt", "src"]);
236 let a = tree_root
237 .iter()
238 .find(|entry| entry.name == "a.txt")
239 .unwrap();
240 assert_eq!(a.size, "one\ntwo\nthree\nfour\n".len() as u64);
241 assert_eq!(a.kind.mode_octal(), "0100644");
242 let src = tree_root.iter().find(|entry| entry.name == "src").unwrap();
243 assert_eq!(src.kind, EntryKind::Tree);
244 assert_eq!(src.size, 0);
245
246 let sub = bare
247 .tree_entries_at(head, Some(&rp("src")))
248 .unwrap()
249 .unwrap();
250 assert_eq!(sub.len(), 1);
251 assert_eq!(sub[0].name, "lib.rs");
252 assert_eq!(
253 bare.tree_entries_at(head, Some(&rp("a.txt")))
254 .unwrap()
255 .unwrap(),
256 Vec::new(),
257 "file path lists as empty"
258 );
259 assert!(
260 bare.tree_entries_at(head, Some(&rp("missing")))
261 .unwrap()
262 .is_none()
263 );
264 assert!(RepoPath::new("../escape").is_err());
265
266 let entry = bare.entry_at(head, &rp("src/lib.rs")).unwrap().unwrap();
267 assert_eq!(
268 entry.oid,
269 Oid::from_hex(&git(work, &["rev-parse", "HEAD:src/lib.rs"])).unwrap()
270 );
271
272 let deadline = Some(std::time::Instant::now() + std::time::Duration::from_secs(10));
273 let names: Vec<String> = tree_root.iter().map(|entry| entry.name.clone()).collect();
274 let attributed = bare.last_commits(head, None, &names, deadline).unwrap();
275 assert_eq!(
276 attributed["a.txt"].id.to_hex(),
277 git(work, &["log", "-1", "--format=%H", "--", "a.txt"])
278 );
279 assert_eq!(
280 attributed["src"].id.to_hex(),
281 git(work, &["log", "-1", "--format=%H", "--", "src"])
282 );
283 assert_eq!(attributed["a.txt"].subject, "extend a");
284 let nested = bare
285 .last_commits(head, Some(&rp("src")), &["lib.rs".to_string()], deadline)
286 .unwrap();
287 assert_eq!(
288 nested["lib.rs"].id.to_hex(),
289 git(work, &["log", "-1", "--format=%H", "--", "src/lib.rs"])
290 );
291
292 let patches = bare
293 .commit_patches(knot_git::PatchRange {
294 base: Some(parent),
295 head,
296 })
297 .unwrap();
298 assert_eq!(patches.len(), 1);
299 let patch = &patches[0];
300 assert_eq!(patch.path.as_str(), "src/lib.rs");
301 assert_eq!(patch.status, knot_git::PatchStatus::Modified);
302 assert!(!patch.is_binary);
303 assert_eq!(patch.hunks.len(), 1);
304 let hunk = &patch.hunks[0];
305 assert_eq!(
306 (
307 hunk.old_start.get(),
308 hunk.old_lines.get(),
309 hunk.new_start.get(),
310 hunk.new_lines.get()
311 ),
312 (1, 1, 1, 2)
313 );
314 assert_eq!(hunk.added(), LineCount::new(1));
315 assert_eq!(hunk.deleted(), LineCount::new(0));
316 assert_eq!(
317 hunk.lines
318 .iter()
319 .map(|line| String::from_utf8_lossy(&line.text).into_owned())
320 .collect::<Vec<_>>(),
321 vec!["pub fn nel() {}\n", "pub fn teq() {}\n"]
322 );
323
324 let initial = bare
325 .commit_patches(knot_git::PatchRange {
326 base: None,
327 head: root,
328 })
329 .unwrap();
330 assert_eq!(initial.len(), 1);
331 assert_eq!(initial[0].status, knot_git::PatchStatus::Added);
332 assert_eq!(
333 initial[0].hunks[0].old_start.get(),
334 0,
335 "added file hunk starts at -0,0"
336 );
337 assert_eq!(initial[0].hunks[0].old_lines.get(), 0);
338
339 let tag_commit = bare.peel_to_commit(tag_object).unwrap();
340 assert_eq!(
341 bare.changed_paths(knot_git::PatchRange {
342 base: None,
343 head: tag_object,
344 })
345 .unwrap(),
346 bare.changed_paths(knot_git::PatchRange {
347 base: None,
348 head: tag_commit,
349 })
350 .unwrap(),
351 "an annotated tag peels to its commit before the trees are diffed"
352 );
353 let created = bare
354 .changed_paths(knot_git::PatchRange { base: None, head })
355 .unwrap();
356 assert_eq!(
357 created.paths(),
358 [rp("a.txt"), rp("src/lib.rs")],
359 "a ref creation lists every blob in the tree and no directory of them"
360 );
361 assert_eq!(created.listing(), Listing::Complete);
362}
363
364type TopoRow = (fn(&Path, &Oid), fn(&Repo, &Oid));
365
366#[test]
367fn ref_topology_reads_are_total() {
368 let rows: &[TopoRow] = &[
369 (
370 |bare, _head| {
371 git(bare, &["update-ref", "-d", "refs/heads/main"]);
372 },
373 |bare, _head| {
374 assert!(bare.head().is_none());
375 assert_eq!(bare.default_branch().unwrap().as_str(), "refs/heads/main");
376 assert!(bare.references().unwrap().is_empty());
377 assert!(bare.branches().unwrap().is_empty());
378 assert!(bare.tags().unwrap().is_empty());
379 assert!(bare.advertised_refs().unwrap().is_empty());
380 },
381 ),
382 (
383 |bare, head| {
384 git(bare, &["update-ref", "--no-deref", "HEAD", &head.to_hex()]);
385 },
386 |bare, head| {
387 assert!(bare.head().is_none());
388 assert!(bare.default_branch().is_none());
389 let branches = bare.branches().unwrap();
390 assert_eq!(branches.len(), 1);
391 assert_eq!(branches[0].target, *head);
392 assert_eq!(
393 bare.find_ref(&RefName::new("refs/heads/main").unwrap())
394 .unwrap(),
395 Some(*head)
396 );
397 },
398 ),
399 (
400 |bare, _head| {
401 git(bare, &["symbolic-ref", "HEAD", "refs/heads/nursery"]);
402 },
403 |bare, _head| {
404 assert!(bare.head().is_none());
405 assert_eq!(
406 bare.default_branch().unwrap().as_str(),
407 "refs/heads/nursery"
408 );
409 let branches = bare.branches().unwrap();
410 assert_eq!(branches.len(), 1);
411 assert_eq!(branches[0].name.as_str(), "refs/heads/main");
412 },
413 ),
414 (
415 |bare, _head| {
416 git(
417 bare,
418 &["symbolic-ref", "refs/heads/mirror", "refs/heads/gone"],
419 );
420 },
421 |bare, head| {
422 let refs = bare.references().unwrap();
423 assert!(
424 refs.iter()
425 .all(|record| record.name.as_str() != "refs/heads/mirror"),
426 "symref to missing target must be dropped, not panic or resolve"
427 );
428 assert!(
429 refs.iter()
430 .any(|record| record.name.as_str() == "refs/heads/main"
431 && record.target == *head)
432 );
433 assert_eq!(
434 bare.find_ref(&RefName::new("refs/heads/mirror").unwrap())
435 .unwrap(),
436 None
437 );
438 },
439 ),
440 (
441 |bare, _head| {
442 git(bare, &["pack-refs", "--all"]);
443 assert!(!bare.join("refs/heads/main").exists());
444 assert!(bare.join("packed-refs").exists());
445 },
446 |bare, head| {
447 let refs = bare.references().unwrap();
448 assert!(
449 refs.iter()
450 .any(|record| record.name.as_str() == "refs/heads/main"
451 && record.target == *head)
452 );
453 assert_eq!(
454 bare.find_ref(&RefName::new("refs/heads/main").unwrap())
455 .unwrap(),
456 Some(*head)
457 );
458 },
459 ),
460 ];
461
462 rows.iter().for_each(|(setup, check)| {
463 let (_scan, layout, did, bare_path, head) = seed_main();
464 setup(bare_path.as_path(), &head);
465 let bare = layout.open(&did).unwrap();
466 check(&bare, &head);
467 });
468}
469
470#[test]
471fn reachable_from_public_excludes_cob_only_commits() {
472 let (_scan, work_dir, layout, did) = seed_rich();
473 let work = work_dir.path();
474 let bare_path = layout.repo_path(&did).unwrap();
475 let bare = layout.open(&did).unwrap();
476
477 let head = Oid::from_hex(&git(work, &["rev-parse", "HEAD"])).unwrap();
478 let ancestor = Oid::from_hex(&git(work, &["rev-parse", "HEAD~2"])).unwrap();
479 let tag_commit = Oid::from_hex(&git(work, &["rev-parse", "v1.0.0^{commit}"])).unwrap();
480 assert!(bare.reachable_from_public(head).unwrap());
481 assert!(bare.reachable_from_public(ancestor).unwrap());
482 assert!(bare.reachable_from_public(tag_commit).unwrap());
483
484 commit_file(work, "hidden.txt", "secret\n", "hidden");
485 let hidden = Oid::from_hex(&git(work, &["rev-parse", "HEAD"])).unwrap();
486 git(
487 work,
488 &[
489 "push",
490 "-q",
491 bare_path.to_str().unwrap(),
492 "HEAD:refs/cobs/sh.tangled.repo.collaborator/secret",
493 ],
494 );
495
496 let bare = layout.open(&did).unwrap();
497 assert!(bare.contains(hidden), "object lives in odb");
498 assert!(
499 !bare.reachable_from_public(hidden).unwrap(),
500 "commit held only by cob ref isn't reachable from any public ref"
501 );
502}
503
504#[test]
505fn a_branch_tipped_by_a_tag_object_lists_opaquely() {
506 let (_scan, work_dir, layout, did) = seed_rich();
507 let work = work_dir.path();
508 let bare_path = layout.repo_path(&did).unwrap();
509 let tag_object = git(work, &["rev-parse", "v1.0.0"]);
510 std::fs::write(
511 bare_path.join("refs/heads/tagtip"),
512 format!("{tag_object}\n"),
513 )
514 .unwrap();
515
516 let bare = layout.open(&did).unwrap();
517 let branches = bare.branch_list().unwrap();
518 assert_eq!(branches.len(), 2);
519 let tagtip = branches
520 .iter()
521 .find(|branch| branch.name.as_str() == "tagtip")
522 .unwrap();
523 match &tagtip.tip {
524 knot_git::BranchTip::Opaque {
525 id,
526 message,
527 created_at,
528 } => {
529 assert_eq!(*id, Oid::from_hex(&tag_object).unwrap());
530 assert_eq!(message, "release one\n");
531 assert!(created_at.get() > 0, "annotated tag records tagger time");
532 }
533 other => panic!("expected opaque tip, got {other:?}"),
534 }
535}
536
537#[test]
538fn extended_history_reads() {
539 let (_scan, work_dir, layout, did) = seed_rich();
540 let work = work_dir.path();
541 let bare_path = layout.repo_path(&did).unwrap();
542
543 let seed_head = git(work, &["rev-parse", "HEAD"]);
544 git(
545 work,
546 &[
547 "update-index",
548 "--add",
549 "--cacheinfo",
550 &format!("160000,{seed_head},vendor/dep"),
551 ],
552 );
553 git(work, &["commit", "-q", "-m", "add gitlink"]);
554 git(work, &["push", "-q", bare_path.to_str().unwrap(), "main"]);
555 let bare = layout.open(&did).unwrap();
556 let linked = Oid::from_hex(&git(work, &["rev-parse", "HEAD"])).unwrap();
557 assert!(
558 bare.tree_entries_at(linked, Some(&rp("vendor/dep")))
559 .unwrap()
560 .is_none(),
561 "submodule path isn't found"
562 );
563 assert!(
564 bare.tree_entries_at(linked, Some(&rp("vendor")))
565 .unwrap()
566 .is_some(),
567 "directory holding gitlink still lists"
568 );
569
570 commit_file(
571 work,
572 ".gitmodules",
573 "# top comment\n[submodule \"kelp\"]\n\tpath = libs/kelp ; trailing comment\n\turl = \"https://oyster.cafe/kelp.git\"\n\tbranch = main\n[submodule \"whelk\"]\n\tpath = libs/whelk\n\turl = https://nel.pet/whelk.git # mirror\n",
574 "add submodules",
575 );
576 git(work, &["push", "-q", bare_path.to_str().unwrap(), "main"]);
577 let bare = layout.open(&did).unwrap();
578 let with_mods = Oid::from_hex(&git(work, &["rev-parse", "HEAD"])).unwrap();
579 let submodules = bare.submodules(with_mods).unwrap();
580 assert_eq!(submodules.len(), 2);
581 assert_eq!(submodules[0].name, "kelp");
582 assert_eq!(submodules[0].path.as_str(), "libs/kelp");
583 assert_eq!(submodules[0].url, "https://oyster.cafe/kelp.git");
584 assert_eq!(
585 submodules[0].branch,
586 Some(knot_types::BranchName::new("main").unwrap())
587 );
588 assert_eq!(submodules[1].branch, None);
589
590 std::fs::write(work.join("blob.bin"), [0u8, 159, 146, 150, 0, 1]).unwrap();
591 std::fs::write(work.join("noeol.txt"), "no newline at end").unwrap();
592 git(work, &["add", "-A"]);
593 git(work, &["commit", "-q", "-m", "binary and noeol"]);
594 git(work, &["push", "-q", bare_path.to_str().unwrap(), "main"]);
595 let bare = layout.open(&did).unwrap();
596 let head = Oid::from_hex(&git(work, &["rev-parse", "HEAD"])).unwrap();
597 let parent = Oid::from_hex(&git(work, &["rev-parse", "HEAD~1"])).unwrap();
598 let patches = bare
599 .commit_patches(knot_git::PatchRange {
600 base: Some(parent),
601 head,
602 })
603 .unwrap();
604 let binary = patches
605 .iter()
606 .find(|patch| patch.path.as_str() == "blob.bin")
607 .unwrap();
608 assert!(binary.is_binary);
609 assert!(binary.hunks.is_empty());
610 let noeol = patches
611 .iter()
612 .find(|patch| patch.path.as_str() == "noeol.txt")
613 .unwrap();
614 let last = noeol.hunks[0].lines.last().unwrap();
615 assert_eq!(last.text, b"no newline at end".to_vec());
616}
617
618#[test]
619fn archives_round_trip_through_tar() {
620 let (_scan, work_dir, layout, did) = seed_rich();
621 let work = work_dir.path();
622 let bare = layout.open(&did).unwrap();
623 let head = Oid::from_hex(&git(work, &["rev-parse", "HEAD"])).unwrap();
624 let tree = bare.peel_to_tree(head).unwrap();
625
626 let mut out = std::io::Cursor::new(Vec::new());
627 bare.write_archive(
628 tree,
629 knot_git::ArchiveFormat::TarGz,
630 Some(&knot_git::ArchivePrefix::new("squid-main/").unwrap()),
631 &mut out,
632 )
633 .unwrap();
634 let compressed = out.into_inner();
635 assert_eq!(
636 &compressed[..2],
637 &[0x1f, 0x8b],
638 "tar.gz starts with gzip magic"
639 );
640
641 let mut decoder = flate2::read::GzDecoder::new(compressed.as_slice());
642 let mut tar = Vec::new();
643 std::io::Read::read_to_end(&mut decoder, &mut tar).unwrap();
644 assert!(
645 contains(&tar, b"squid-main/src/lib.rs"),
646 "tar contains prefixed entries"
647 );
648}
649
650#[test]
651fn a_filename_with_a_backslash_is_addressable() {
652 let (_scan, layout, did, bare_path, _head) = seed_main();
653 let work_dir = tempfile::tempdir().unwrap();
654 let work = work_dir.path();
655 git(
656 work,
657 &["clone", "-q", bare_path.to_str().unwrap(), "checkout"],
658 );
659 let clone = work.join("checkout");
660 commit_file(&clone, "back\\slash.txt", "escaped\n", "backslash name");
661 git(&clone, &["push", "-q", "origin", "main"]);
662
663 let bare = layout.open(&did).unwrap();
664 let head = Oid::from_hex(&git(&clone, &["rev-parse", "HEAD"])).unwrap();
665 let entry = bare
666 .entry_at(head, &rp("back\\slash.txt"))
667 .unwrap()
668 .expect("backslash in filename is legal and addressable");
669 assert_eq!(bare.read_blob(entry.oid).unwrap(), b"escaped\n");
670}
671
672#[test]
673fn an_oversized_blob_diffs_as_binary_without_loading_it() {
674 let (_scan, layout, did, bare_path, _head) = seed_main();
675 let work_dir = tempfile::tempdir().unwrap();
676 let work = work_dir.path();
677 git(
678 work,
679 &["clone", "-q", bare_path.to_str().unwrap(), "checkout"],
680 );
681 let clone = work.join("checkout");
682 let oversized = vec![b'a'; (knot_git::MAX_DIFF_BLOB_BYTES + 1) as usize];
683 std::fs::write(clone.join("huge.txt"), &oversized).unwrap();
684 git(&clone, &["add", "-A"]);
685 git(&clone, &["commit", "-q", "-m", "huge text file"]);
686 git(&clone, &["push", "-q", "origin", "main"]);
687
688 let bare = layout.open(&did).unwrap();
689 let head = Oid::from_hex(&git(&clone, &["rev-parse", "HEAD"])).unwrap();
690 let parent = Oid::from_hex(&git(&clone, &["rev-parse", "HEAD~1"])).unwrap();
691 let patches = bare
692 .commit_patches(knot_git::PatchRange {
693 base: Some(parent),
694 head,
695 })
696 .unwrap();
697 let huge = patches
698 .iter()
699 .find(|patch| patch.path.as_str() == "huge.txt")
700 .unwrap();
701 assert!(
702 huge.is_binary,
703 "blob past diff budget falls back to binary instead of being loaded"
704 );
705 assert!(huge.hunks.is_empty());
706}