This repository has no description
0

Configure Feed

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

core / knot2 / crates / knot-git / tests / reads.rs
25 kB 706 lines
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}