Skip to main content

max / makenotwork

Drop libgit2: the server now links no GPL code The last git2 callers move to gitoxide: the push-refs walk that turns commit messages into issue actions, repo creation and its receive.maxInputSize cap, the admin backfill command, and the test fixtures across the unit and integration suites. git2 leaves Cargo.toml, and with it libgit2-sys. Repository creation writes the pack-size cap straight into the repo's config file. gitoxide's in-memory config edits are not persisted by commit(), and receive.maxInputSize is not a key its typed config tree knows, so the file is parsed, amended and rewritten. GitError::Git2 becomes GitError::Git(String), since nothing needs to match on libgit2 error classes any more; the read paths already map their own not-found cases onto the specific variants. The vendored OpenSSL stays: cargo tree still shows webauthn-rs and async-stripe's native-tls pulling it in, so only the comment narrows. Credits page updated in both places it named git2.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-01 21:35 UTC
Signed with PGP, not checked
Commit: fcf2aa6eeaf78f67ae681cbf4527dfa3e74074b4
Parent: c25c65f
13 files changed, +313 insertions, -387 deletions
@@ -3244,18 +3244,6 @@
3244 3244 "stable_deref_trait",
3245 3245 ]
3246 3246
3247 - [[package]]
3248 - name = "git2"
3249 - version = "0.21.0"
3250 - source = "registry+https://github.com/rust-lang/crates.io-index"
3251 - checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e"
3252 - dependencies = [
3253 - "bitflags 2.13.0",
3254 - "libc",
3255 - "libgit2-sys",
3256 - "log",
3257 - ]
3258 -
3259 3247 [[package]]
3260 3248 name = "gix"
3261 3249 version = "0.86.0"
@@ -4975,18 +4963,6 @@
4975 4963 source = "registry+https://github.com/rust-lang/crates.io-index"
4976 4964 checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
4977 4965
4978 - [[package]]
4979 - name = "libgit2-sys"
4980 - version = "0.18.5+1.9.4"
4981 - source = "registry+https://github.com/rust-lang/crates.io-index"
4982 - checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2"
4983 - dependencies = [
4984 - "cc",
4985 - "libc",
4986 - "libz-sys",
4987 - "pkg-config",
4988 - ]
4989 -
4990 4966 [[package]]
4991 4967 name = "libm"
4992 4968 version = "0.2.16"
@@ -5015,18 +4991,6 @@
5015 4991 "vcpkg",
5016 4992 ]
5017 4993
5018 - [[package]]
5019 - name = "libz-sys"
5020 - version = "1.1.29"
5021 - source = "registry+https://github.com/rust-lang/crates.io-index"
5022 - checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
5023 - dependencies = [
5024 - "cc",
5025 - "libc",
5026 - "pkg-config",
5027 - "vcpkg",
5028 - ]
5029 -
5030 4994 [[package]]
5031 4995 name = "lightningcss"
5032 4996 version = "1.0.0-alpha.71"
@@ -5229,7 +5193,6 @@
5229 5193 "email_address",
5230 5194 "flate2",
5231 5195 "fs2",
5232 - "git2",
5233 5196 "gix",
5234 5197 "goblin 0.10.7",
5235 5198 "governor",
@@ -66,7 +66,8 @@
66 66 webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "conditional-ui"] }
67 67 webauthn-rs-proto = "0.5"
68 68
69 - # OpenSSL (transitive dep from git2, webauthn-rs: vendored for cross-compilation)
69 + # OpenSSL (transitive dep from webauthn-rs and async-stripe's native-tls:
70 + # vendored for cross-compilation)
70 71 openssl = { version = "0.10", features = ["vendored"] }
71 72
72 73 # Security
@@ -127,10 +128,7 @@
127 128 # Shared theme palette + the bundled theme set (Tier 0 creator theming).
128 129 makeover = "2.4.1"
129 130
130 - # Git source browser. Migrating off git2 (libgit2, GPL-2.0 with linking
131 - # exception) onto gix, which is MIT/Apache-2.0. Both are linked while the port
132 - # is in flight; git2 goes away when the last read path moves over.
133 - git2 = { version = "0.21", features = ["vendored-libgit2"] }
131 + # Git source browser
134 132 gix = { version = "0.86", default-features = false, features = ["blame", "revision", "sha1", "max-performance-safe"] }
135 133 syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "html", "regex-fancy"] }
136 134 regex = "1"
@@ -408,7 +408,7 @@
408 408
409 409 ensure_bare_repo_on_disk(root.path(), "max", "shop").unwrap();
410 410 assert!(
411 - git2::Repository::open_bare(&repo_dir).is_ok(),
411 + gix::open(&repo_dir).is_ok(),
412 412 "a push to a name with no repo has somewhere to write",
413 413 );
414 414
@@ -416,7 +416,7 @@
416 416 // It is also what repairs a repo registered before the fix: the row is
417 417 // there, the directory is not, and the branch above makes it.
418 418 ensure_bare_repo_on_disk(root.path(), "max", "shop").unwrap();
419 - assert!(git2::Repository::open_bare(&repo_dir).is_ok());
419 + assert!(gix::open(&repo_dir).is_ok());
420 420 }
421 421
422 422 #[test]
@@ -757,7 +757,7 @@
757 757 if !is_bare || !repo_path.is_dir() {
758 758 continue;
759 759 }
760 - match git2::Repository::open_bare(&repo_path) {
760 + match gix::open(&repo_path) {
761 761 Ok(repo) => {
762 762 makenotwork::git::apply_bare_repo_limits(&repo)?;
763 763 updated += 1;
M server/src/git/mod.rs +154 -201
@@ -1,5 +1,4 @@
1 - //! Git abstraction layer, a thin wrapper with no HTTP/Axum concerns. Reads go
2 - //! through gitoxide; repository creation is still libgit2.
1 + //! Git abstraction layer over gitoxide, with no HTTP/Axum concerns.
3 2 //!
4 3 //! All functions take a filesystem path and return `Result<T>`.
5 4 //! Repository is opened per-request (cheap, just file descriptors).
@@ -14,7 +13,6 @@
14 13
15 14 use std::path::{Path, PathBuf};
16 15
17 - use git2::Repository;
18 16 use syntect::highlighting::ThemeSet;
19 17 use syntect::html::{ClassStyle, ClassedHTMLGenerator};
20 18 use syntect::parsing::SyntaxSet;
@@ -236,41 +234,14 @@
236 234
237 235 /// Open a bare repository with path traversal validation.
238 236 ///
239 - /// `pub(crate)` by design: `git2::Repository` is `!Send` and blocks, so every
240 - /// caller must hold it inside a `spawn_blocking` closure (see
241 - /// `routes::git::GitState::with_repo`). Keeping the opener crate-private marks
242 - /// it as an internal primitive, not a general-purpose entry point.
237 + /// `pub(crate)` by design: opening blocks on disk I/O, so every caller must
238 + /// hold it inside a `spawn_blocking` closure (see
239 + /// `routes::git::ResolvedRepo::with_repo`). Keeping the opener crate-private
240 + /// marks it as an internal primitive, not a general-purpose entry point.
243 241 pub(crate) fn open_repo(
244 242 repos_root: &Path,
245 243 owner: &str,
246 244 repo: &str,
247 - ) -> Result<Repository, GitError> {
248 - validate_segment(owner)?;
249 - validate_segment(repo)?;
250 -
251 - let repo_path = repos_root.join(owner).join(format!("{repo}.git"));
252 -
253 - // Canonicalize both paths to prevent symlink-based traversal
254 - let canonical_root = repos_root
255 - .canonicalize()
256 - .map_err(|_| GitError::RepoNotFound)?;
257 - let canonical_repo = repo_path
258 - .canonicalize()
259 - .map_err(|_| GitError::RepoNotFound)?;
260 -
261 - if !canonical_repo.starts_with(&canonical_root) {
262 - return Err(GitError::RepoNotFound);
263 - }
264 -
265 - Repository::open_bare(&canonical_repo).map_err(|_| GitError::RepoNotFound)
266 - }
267 -
268 - /// Open a bare repository with gitoxide, with the same path traversal
269 - /// validation [`open_repo`] applies.
270 - pub(crate) fn open_gix_repo(
271 - repos_root: &Path,
272 - owner: &str,
273 - repo: &str,
274 245 ) -> Result<gix::Repository, GitError> {
275 246 open_gix_repo_at(&repo_disk_path(repos_root, owner, repo)?)
276 247 }
@@ -372,20 +343,30 @@
372 343 /// `receive.maxInputSize`, which makes git-receive-pack abort a push whose pack
373 344 /// exceeds the cap. Shared by repo creation and the one-time backfill command so
374 345 /// the limit can never drift between the two.
375 - pub fn apply_bare_repo_limits(repo: &Repository) -> Result<(), git2::Error> {
376 - let mut cfg = repo.config()?;
377 - cfg.set_i64(
378 - "receive.maxInputSize",
379 - crate::constants::GIT_SSH_MAX_PACK_BYTES,
380 - )?;
346 + /// Written straight into the repository's own config file. gitoxide's in-memory
347 + /// config edits are not persisted by `commit()`, and `receive.maxInputSize` is
348 + /// not a key its typed config tree knows, so the file is parsed, amended and
349 + /// rewritten instead.
350 + pub fn apply_bare_repo_limits(repo: &gix::Repository) -> Result<(), GitError> {
351 + let config_path = repo.path().join("config");
352 + let mut config =
353 + gix::config::File::from_path_no_includes(config_path.clone(), gix::config::Source::Local)
354 + .map_err(|e| GitError::Git(e.to_string()))?;
355 + config
356 + .set_raw_value(
357 + "receive.maxInputSize",
358 + crate::constants::GIT_SSH_MAX_PACK_BYTES.to_string(),
359 + )
360 + .map_err(|e| GitError::Git(e.to_string()))?;
361 + std::fs::write(&config_path, config.to_bstring()).map_err(|e| GitError::Git(e.to_string()))?;
381 362 Ok(())
382 363 }
383 364
384 365 /// Initialize a bare repository on disk with the standard resource limits
385 366 /// applied. This is the single production path for creating a bare repo, so a
386 367 /// new repo can never be created without its pack-size cap.
387 - pub fn init_bare_repo(repo_dir: &Path) -> Result<Repository, GitError> {
388 - let repo = Repository::init_bare(repo_dir)?;
368 + pub fn init_bare_repo(repo_dir: &Path) -> Result<gix::Repository, GitError> {
369 + let repo = gix::init_bare(repo_dir).map_err(|e| GitError::Git(e.to_string()))?;
389 370 apply_bare_repo_limits(&repo)?;
390 371 Ok(repo)
391 372 }
@@ -405,7 +386,7 @@
405 386 #[error("Path not found")]
406 387 PathNotFound,
407 388 #[error("Git error: {0}")]
408 - Git2(#[from] git2::Error),
389 + Git(String),
409 390 }
410 391
411 392 impl From<GitError> for crate::error::AppError {
@@ -415,16 +396,7 @@
415 396 GitError::RefNotFound | GitError::TreeNotFound | GitError::PathNotFound => {
416 397 crate::error::AppError::NotFound
417 398 }
418 - GitError::Git2(ref git_err)
419 - if git_err.code() == git2::ErrorCode::NotFound
420 - && matches!(
421 - git_err.class(),
422 - git2::ErrorClass::Reference | git2::ErrorClass::Object
423 - ) =>
424 - {
425 - crate::error::AppError::NotFound
426 - }
427 - GitError::Git2(git_err) => crate::error::AppError::Internal(anyhow::anyhow!(git_err)),
399 + GitError::Git(message) => crate::error::AppError::Internal(anyhow::anyhow!(message)),
428 400 }
429 401 }
430 402 }
@@ -467,60 +439,88 @@
467 439 assert!(validate_segment(&long).is_err());
468 440 }
469 441
442 + /// Open a test repo through gitoxide.
443 + fn open_gix(bare_path: &std::path::Path) -> gix::Repository {
444 + open_gix_repo_at(bare_path).unwrap()
445 + }
446 +
447 + /// Create an empty bare repo under `<tmp>/owner/<name>.git`.
448 + fn init_test_repo(name: &str) -> (tempfile::TempDir, std::path::PathBuf, gix::Repository) {
449 + let tmp = tempfile::TempDir::new().unwrap();
450 + let bare_path = tmp.path().join("owner").join(format!("{name}.git"));
451 + std::fs::create_dir_all(&bare_path).unwrap();
452 + let repo = gix::init_bare(&bare_path).unwrap();
453 + (tmp, bare_path, repo)
454 + }
455 +
456 + /// Write a tree from `(name, id, kind)` entries. Git requires tree entries
457 + /// in sorted order, which `gix_object::Tree` handles on write.
458 + fn write_tree(
459 + repo: &gix::Repository,
460 + entries: &[(&str, gix::ObjectId, gix::objs::tree::EntryKind)],
461 + ) -> gix::ObjectId {
462 + let mut tree = gix::objs::Tree::empty();
463 + for (name, oid, kind) in entries {
464 + tree.entries.push(gix::objs::tree::Entry {
465 + mode: (*kind).into(),
466 + filename: (*name).into(),
467 + oid: *oid,
468 + });
469 + }
470 + tree.entries.sort();
471 + repo.write_object(&tree).unwrap().detach()
472 + }
473 +
474 + /// Commit `tree` onto `refs/heads/main` with a fixed test identity.
475 + fn commit_main(
476 + repo: &gix::Repository,
477 + message: &str,
478 + tree: gix::ObjectId,
479 + parents: Vec<gix::ObjectId>,
480 + ) -> gix::ObjectId {
481 + let signature = gix::actor::SignatureRef {
482 + name: "Test".into(),
483 + email: "test@example.com".into(),
484 + time: "1700000000 +0000",
485 + };
486 + repo.commit_as(
487 + signature,
488 + signature,
489 + "refs/heads/main",
490 + message,
491 + tree,
492 + parents,
493 + )
494 + .unwrap()
495 + .detach()
496 + }
497 +
470 498 /// Create a temporary bare repo with some commits for testing.
471 499 /// Returns (tmpdir, bare_path). The branch name is "main".
472 500 fn make_test_repo() -> (tempfile::TempDir, std::path::PathBuf) {
473 - let tmp = tempfile::TempDir::new().unwrap();
474 - let bare_path = tmp.path().join("owner").join("testrepo.git");
475 - std::fs::create_dir_all(&bare_path).unwrap();
476 - let bare_repo = Repository::init_bare(&bare_path).unwrap();
501 + use gix::objs::tree::EntryKind;
477 502
478 - // Build tree directly in the bare repo (no clone/push needed)
479 - let sig = git2::Signature::now("Test", "test@example.com").unwrap();
503 + let (tmp, bare_path, repo) = init_test_repo("testrepo");
480 504
481 - // Create blobs
482 - let readme_oid = bare_repo.blob(b"# Test Repo\n\nHello world.").unwrap();
483 - let main_rs_oid = bare_repo
484 - .blob(b"fn main() {\n println!(\"hello\");\n}\n")
505 + let readme = repo.write_blob(b"# Test Repo\n\nHello world.").unwrap();
506 + let main_rs = repo
507 + .write_blob(b"fn main() {\n println!(\"hello\");\n}\n")
485 508 .unwrap();
486 509
487 - // Build src/ subtree
488 - let mut src_tb = bare_repo.treebuilder(None).unwrap();
489 - src_tb.insert("main.rs", main_rs_oid, 0o100_644).unwrap();
490 - let src_tree_oid = src_tb.write().unwrap();
510 + let src_tree = write_tree(&repo, &[("main.rs", main_rs.detach(), EntryKind::Blob)]);
511 + let root_tree = write_tree(
512 + &repo,
513 + &[
514 + ("README.md", readme.detach(), EntryKind::Blob),
515 + ("src", src_tree, EntryKind::Tree),
516 + ],
517 + );
491 518
492 - // Build root tree
493 - let mut root_tb = bare_repo.treebuilder(None).unwrap();
494 - root_tb.insert("README.md", readme_oid, 0o100_644).unwrap();
495 - root_tb.insert("src", src_tree_oid, 0o040_000).unwrap();
496 - let root_tree_oid = root_tb.write().unwrap();
497 - let root_tree = bare_repo.find_tree(root_tree_oid).unwrap();
498 -
499 - // Create commit on refs/heads/main
500 - bare_repo
501 - .commit(
502 - Some("refs/heads/main"),
503 - &sig,
504 - &sig,
505 - "Initial commit",
506 - &root_tree,
507 - &[],
508 - )
509 - .unwrap();
510 -
511 - // Set HEAD to point to main
512 - bare_repo.set_head("refs/heads/main").unwrap();
519 + commit_main(&repo, "Initial commit", root_tree, Vec::new());
513 520
514 521 (tmp, bare_path)
515 522 }
516 523
517 - /// Open the same test repo through gitoxide. The fixtures are still built
518 - /// with git2 (write paths are not part of this migration), while the ported
519 - /// read paths take a `gix::Repository`.
520 - fn open_gix(bare_path: &std::path::Path) -> gix::Repository {
521 - open_gix_repo_at(bare_path).unwrap()
522 - }
523 -
524 524 #[test]
525 525 fn open_repo_valid() {
526 526 let (tmp, _) = make_test_repo();
@@ -545,26 +545,21 @@
545 545 fn blame_gates_oversize_file_before_walk() {
546 546 // R6-Sec-L3: an over-cap file is rejected by the size gate before the
547 547 // expensive blame walk; a small file still blames normally.
548 - let tmp = tempfile::TempDir::new().unwrap();
549 - let bare_path = tmp.path().join("owner").join("blame.git");
550 - std::fs::create_dir_all(&bare_path).unwrap();
551 - let repo = Repository::init_bare(&bare_path).unwrap();
552 - let sig = git2::Signature::now("Test", "test@example.com").unwrap();
548 + use gix::objs::tree::EntryKind;
549 +
550 + let (_tmp, _bare_path, repo) = init_test_repo("blame");
553 551
554 552 let big = vec![b'a'; crate::constants::GIT_MAX_FILE_SIZE_BYTES + 1024];
555 - let big_oid = repo.blob(&big).unwrap();
556 - let small_oid = repo.blob(b"line one\nline two\n").unwrap();
557 - let mut tb = repo.treebuilder(None).unwrap();
558 - tb.insert("big.txt", big_oid, 0o100_644).unwrap();
559 - tb.insert("small.txt", small_oid, 0o100_644).unwrap();
560 - let tree_oid = tb.write().unwrap();
561 - let tree = repo.find_tree(tree_oid).unwrap();
562 - let commit_oid = repo
563 - .commit(Some("refs/heads/main"), &sig, &sig, "c", &tree, &[])
564 - .unwrap();
565 -
566 - let repo = open_gix(&bare_path);
567 - let commit_oid = gix::ObjectId::from_bytes_or_panic(commit_oid.as_bytes());
553 + let big_oid = repo.write_blob(&big).unwrap().detach();
554 + let small_oid = repo.write_blob(b"line one\nline two\n").unwrap().detach();
555 + let tree = write_tree(
556 + &repo,
557 + &[
558 + ("big.txt", big_oid, EntryKind::Blob),
559 + ("small.txt", small_oid, EntryKind::Blob),
560 + ],
561 + );
562 + let commit_oid = commit_main(&repo, "c", tree, Vec::new());
568 563
569 564 assert!(matches!(
570 565 blame_file(&repo, commit_oid, "big.txt"),
@@ -684,56 +679,37 @@
684 679 /// Create a test repo with two commits: first adds README.md + src/main.rs,
685 680 /// second modifies only src/main.rs. Used for file_commit_log tests.
686 681 fn make_two_commit_repo() -> (tempfile::TempDir, std::path::PathBuf) {
687 - let tmp = tempfile::TempDir::new().unwrap();
688 - let bare_path = tmp.path().join("owner").join("testrepo.git");
689 - std::fs::create_dir_all(&bare_path).unwrap();
690 - let bare_repo = Repository::init_bare(&bare_path).unwrap();
691 - let sig = git2::Signature::now("Test", "test@example.com").unwrap();
682 + use gix::objs::tree::EntryKind;
683 +
684 + let (tmp, bare_path, repo) = init_test_repo("testrepo");
692 685
693 686 // Commit 1: README.md + src/main.rs
694 - let readme_oid = bare_repo.blob(b"# Test Repo\n").unwrap();
695 - let main_rs_oid = bare_repo.blob(b"fn main() {}\n").unwrap();
696 - let mut src_tb = bare_repo.treebuilder(None).unwrap();
697 - src_tb.insert("main.rs", main_rs_oid, 0o100_644).unwrap();
698 - let src_tree_oid = src_tb.write().unwrap();
699 - let mut root_tb = bare_repo.treebuilder(None).unwrap();
700 - root_tb.insert("README.md", readme_oid, 0o100_644).unwrap();
701 - root_tb.insert("src", src_tree_oid, 0o040_000).unwrap();
702 - let root_tree = bare_repo.find_tree(root_tb.write().unwrap()).unwrap();
703 - let c1 = bare_repo
704 - .commit(
705 - Some("refs/heads/main"),
706 - &sig,
707 - &sig,
708 - "Initial commit",
709 - &root_tree,
710 - &[],
711 - )
712 - .unwrap();
713 - bare_repo.set_head("refs/heads/main").unwrap();
687 + let readme = repo.write_blob(b"# Test Repo\n").unwrap().detach();
688 + let main_rs = repo.write_blob(b"fn main() {}\n").unwrap().detach();
689 + let src_tree = write_tree(&repo, &[("main.rs", main_rs, EntryKind::Blob)]);
690 + let root_tree = write_tree(
691 + &repo,
692 + &[
693 + ("README.md", readme, EntryKind::Blob),
694 + ("src", src_tree, EntryKind::Tree),
695 + ],
696 + );
697 + let first = commit_main(&repo, "Initial commit", root_tree, Vec::new());
714 698
715 699 // Commit 2: modify only src/main.rs
716 - let first = bare_repo.find_commit(c1).unwrap();
717 - let main_rs_v2 = bare_repo
718 - .blob(b"fn main() { println!(\"hello\"); }\n")
719 - .unwrap();
720 - let mut src_tb2 = bare_repo.treebuilder(None).unwrap();
721 - src_tb2.insert("main.rs", main_rs_v2, 0o100_644).unwrap();
722 - let src2 = src_tb2.write().unwrap();
723 - let mut root_tb2 = bare_repo.treebuilder(None).unwrap();
724 - root_tb2.insert("README.md", readme_oid, 0o100_644).unwrap();
725 - root_tb2.insert("src", src2, 0o040_000).unwrap();
726 - let root_tree2 = bare_repo.find_tree(root_tb2.write().unwrap()).unwrap();
727 - bare_repo
728 - .commit(
729 - Some("refs/heads/main"),
730 - &sig,
731 - &sig,
732 - "Update main.rs",
733 - &root_tree2,
734 - &[&first],
735 - )
736 - .unwrap();
700 + let main_rs_v2 = repo
701 + .write_blob(b"fn main() { println!(\"hello\"); }\n")
702 + .unwrap()
703 + .detach();
704 + let src_tree2 = write_tree(&repo, &[("main.rs", main_rs_v2, EntryKind::Blob)]);
705 + let root_tree2 = write_tree(
706 + &repo,
707 + &[
708 + ("README.md", readme, EntryKind::Blob),
709 + ("src", src_tree2, EntryKind::Tree),
710 + ],
711 + );
712 + commit_main(&repo, "Update main.rs", root_tree2, vec![first]);
737 713
738 714 (tmp, bare_path)
739 715 }
@@ -808,39 +784,21 @@
808 784 // Rename tracking comes from gitoxide's diff configuration. libgit2 was
809 785 // called without `find_similar`, so a rename used to render as an
810 786 // addition plus a deletion and the Renamed status never appeared.
811 - let tmp = tempfile::TempDir::new().unwrap();
812 - let bare_path = tmp.path().join("owner").join("rename.git");
813 - std::fs::create_dir_all(&bare_path).unwrap();
814 - let bare = Repository::init_bare(&bare_path).unwrap();
815 - let sig = git2::Signature::now("Test", "test@example.com").unwrap();
787 + use gix::objs::tree::EntryKind;
816 788
817 - let body = b"one\ntwo\nthree\nfour\nfive\nsix\n";
818 - let blob = bare.blob(body).unwrap();
789 + let (_tmp, _bare_path, repo) = init_test_repo("rename");
819 790
820 - let mut tb = bare.treebuilder(None).unwrap();
821 - tb.insert("before.txt", blob, 0o100_644).unwrap();
822 - let tree = bare.find_tree(tb.write().unwrap()).unwrap();
823 - let first = bare
824 - .commit(Some("refs/heads/main"), &sig, &sig, "add", &tree, &[])
825 - .unwrap();
826 - bare.set_head("refs/heads/main").unwrap();
791 + let blob = repo
792 + .write_blob(b"one\ntwo\nthree\nfour\nfive\nsix\n")
793 + .unwrap()
794 + .detach();
795 +
796 + let tree = write_tree(&repo, &[("before.txt", blob, EntryKind::Blob)]);
797 + let first = commit_main(&repo, "add", tree, Vec::new());
827 798
828 799 // Same blob, new name.
829 - let mut tb2 = bare.treebuilder(None).unwrap();
830 - tb2.insert("after.txt", blob, 0o100_644).unwrap();
831 - let tree2 = bare.find_tree(tb2.write().unwrap()).unwrap();
832 - let parent = bare.find_commit(first).unwrap();
833 - bare.commit(
834 - Some("refs/heads/main"),
835 - &sig,
836 - &sig,
837 - "rename",
838 - &tree2,
839 - &[&parent],
840 - )
841 - .unwrap();
842 -
843 - let repo = open_gix(&bare_path);
800 + let tree2 = write_tree(&repo, &[("after.txt", blob, EntryKind::Blob)]);
801 + commit_main(&repo, "rename", tree2, vec![first]);
844 802 let head = resolve_ref(&repo, "main").unwrap();
845 803 let files = commit_diff(&repo, head, 10, 1000).unwrap();
846 804
@@ -858,21 +816,16 @@
858 816 fn commit_diff_escapes_html_in_content() {
859 817 // Diff lines are rendered into the commit page unescaped by the
860 818 // template, so the escaping has to happen here.
861 - let tmp = tempfile::TempDir::new().unwrap();
862 - let bare_path = tmp.path().join("owner").join("escape.git");
863 - std::fs::create_dir_all(&bare_path).unwrap();
864 - let bare = Repository::init_bare(&bare_path).unwrap();
865 - let sig = git2::Signature::now("Test", "test@example.com").unwrap();
819 + use gix::objs::tree::EntryKind;
866 820
867 - let blob = bare.blob(b"<script>alert('x') && y</script>\n").unwrap();
868 - let mut tb = bare.treebuilder(None).unwrap();
869 - tb.insert("x.html", blob, 0o100_644).unwrap();
870 - let tree = bare.find_tree(tb.write().unwrap()).unwrap();
871 - bare.commit(Some("refs/heads/main"), &sig, &sig, "add", &tree, &[])
872 - .unwrap();
873 - bare.set_head("refs/heads/main").unwrap();
821 + let (_tmp, _bare_path, repo) = init_test_repo("escape");
874 822
875 - let repo = open_gix(&bare_path);
823 + let blob = repo
824 + .write_blob(b"<script>alert('x') && y</script>\n")
825 + .unwrap()
826 + .detach();
827 + let tree = write_tree(&repo, &[("x.html", blob, EntryKind::Blob)]);
828 + commit_main(&repo, "add", tree, Vec::new());
876 829 let head = resolve_ref(&repo, "main").unwrap();
877 830 let files = commit_diff(&repo, head, 10, 1000).unwrap();
878 831
@@ -3,6 +3,7 @@
3 3 pub(crate) mod client;
4 4 pub(crate) mod db;
5 5 pub(crate) mod email;
6 + pub(crate) mod gitfixture;
6 7 pub(crate) mod storage;
7 8 pub(crate) mod stripe;
8 9
@@ -1,6 +1,6 @@
1 1 //! Git browser route tests: repo overview, tree, file, commits, raw, 404s.
2 2 //!
3 - //! Creates temp bare repos with git2 to test the actual HTTP routes.
3 + //! Creates temp bare repos with gitoxide to test the actual HTTP routes.
4 4
5 5 use crate::harness::TestHarness;
6 6
@@ -8,67 +8,38 @@
8 8 /// Commit 1 (root): README.md, src/main.rs
9 9 /// Commit 2: modifies src/main.rs (adds a line)
10 10 fn make_test_repo(dir: &std::path::Path) {
11 - let bare_path = dir.join("testowner").join("testrepo.git");
12 - std::fs::create_dir_all(&bare_path).unwrap();
13 - let bare_repo = git2::Repository::init_bare(&bare_path).unwrap();
11 + use crate::harness::gitfixture::{blob, commit, init_bare, tree};
12 + use gix::objs::tree::EntryKind;
14 13
15 - let sig = git2::Signature::now("Test", "test@example.com").unwrap();
14 + let repo = init_bare(dir, "testowner", "testrepo");
16 15
17 - // Create blobs
18 - let readme_oid = bare_repo.blob(b"# Test Repo\n\nHello world.").unwrap();
19 - let main_rs_oid = bare_repo
20 - .blob(b"fn main() {\n println!(\"hello\");\n}\n")
21 - .unwrap();
16 + // Commit 1: README.md + src/main.rs
17 + let readme = blob(&repo, b"# Test Repo\n\nHello world.");
18 + let main_rs = blob(&repo, b"fn main() {\n println!(\"hello\");\n}\n");
19 + let src = tree(&repo, &[("main.rs", main_rs, EntryKind::Blob)]);
20 + let root = tree(
21 + &repo,
22 + &[
23 + ("README.md", readme, EntryKind::Blob),
24 + ("src", src, EntryKind::Tree),
25 + ],
26 + );
27 + let first = commit(&repo, "Initial commit", root, Vec::new());
22 28
23 - // Build src/ subtree
24 - let mut src_tb = bare_repo.treebuilder(None).unwrap();
25 - src_tb.insert("main.rs", main_rs_oid, 0o100_644).unwrap();
26 - let src_tree_oid = src_tb.write().unwrap();
27 -
28 - // Build root tree
29 - let mut root_tb = bare_repo.treebuilder(None).unwrap();
30 - root_tb.insert("README.md", readme_oid, 0o100_644).unwrap();
31 - root_tb.insert("src", src_tree_oid, 0o040_000).unwrap();
32 - let root_tree_oid = root_tb.write().unwrap();
33 - let root_tree = bare_repo.find_tree(root_tree_oid).unwrap();
34 -
35 - let first_commit_oid = bare_repo
36 - .commit(
37 - Some("refs/heads/main"),
38 - &sig,
39 - &sig,
40 - "Initial commit",
41 - &root_tree,
42 - &[],
43 - )
44 - .unwrap();
45 - bare_repo.set_head("refs/heads/main").unwrap();
46 -
47 - // Second commit: modify src/main.rs
48 - let first_commit = bare_repo.find_commit(first_commit_oid).unwrap();
49 - let main_rs_oid_v2 = bare_repo
50 - .blob(b"fn main() {\n println!(\"hello\");\n println!(\"world\");\n}\n")
51 - .unwrap();
52 - let mut src_tb2 = bare_repo.treebuilder(None).unwrap();
53 - src_tb2
54 - .insert("main.rs", main_rs_oid_v2, 0o100_644)
55 - .unwrap();
56 - let src_tree_oid2 = src_tb2.write().unwrap();
57 - let mut root_tb2 = bare_repo.treebuilder(None).unwrap();
58 - root_tb2.insert("README.md", readme_oid, 0o100_644).unwrap();
59 - root_tb2.insert("src", src_tree_oid2, 0o040_000).unwrap();
60 - let root_tree_oid2 = root_tb2.write().unwrap();
61 - let root_tree2 = bare_repo.find_tree(root_tree_oid2).unwrap();
62 - bare_repo
63 - .commit(
64 - Some("refs/heads/main"),
65 - &sig,
66 - &sig,
67 - "Add world output",
68 - &root_tree2,
69 - &[&first_commit],
70 - )
71 - .unwrap();
29 + // Commit 2: modify src/main.rs
30 + let main_rs_v2 = blob(
31 + &repo,
32 + b"fn main() {\n println!(\"hello\");\n println!(\"world\");\n}\n",
33 + );
34 + let src2 = tree(&repo, &[("main.rs", main_rs_v2, EntryKind::Blob)]);
35 + let root2 = tree(
36 + &repo,
37 + &[
38 + ("README.md", readme, EntryKind::Blob),
39 + ("src", src2, EntryKind::Tree),
40 + ],
41 + );
42 + commit(&repo, "Add world output", root2, vec![first]);
72 43 }
73 44
74 45 /// Set up a harness with git repos and a user matching the disk owner.
@@ -1103,9 +1074,7 @@
1103 1074
1104 1075 /// Tip commit sha of refs/heads/main from the on-disk bare repo.
1105 1076 fn main_tip_sha(dir: &std::path::Path) -> String {
1106 - let bare = dir.join("testowner").join("testrepo.git");
1107 - let repo = git2::Repository::open_bare(&bare).unwrap();
1108 - repo.refname_to_id("refs/heads/main").unwrap().to_string()
1077 + crate::harness::gitfixture::main_tip_sha(dir, "testowner", "testrepo")
1109 1078 }
1110 1079
1111 1080 #[tokio::test]
@@ -15,29 +15,13 @@
15 15
16 16 /// Create a temp bare repo at `{dir}/testowner/testrepo.git` with one commit on "main".
17 17 fn make_test_repo(dir: &std::path::Path) {
18 - let bare_path = dir.join("testowner").join("testrepo.git");
19 - std::fs::create_dir_all(&bare_path).unwrap();
20 - let bare_repo = git2::Repository::init_bare(&bare_path).unwrap();
18 + use crate::harness::gitfixture::{blob, commit, init_bare, tree};
19 + use gix::objs::tree::EntryKind;
21 20
22 - let sig = git2::Signature::now("Test", "test@example.com").unwrap();
23 - let readme_oid = bare_repo.blob(b"# Test Repo\n").unwrap();
24 -
25 - let mut root_tb = bare_repo.treebuilder(None).unwrap();
26 - root_tb.insert("README.md", readme_oid, 0o100_644).unwrap();
27 - let root_tree_oid = root_tb.write().unwrap();
28 - let root_tree = bare_repo.find_tree(root_tree_oid).unwrap();
29 -
30 - bare_repo
31 - .commit(
32 - Some("refs/heads/main"),
33 - &sig,
34 - &sig,
35 - "Initial commit",
36 - &root_tree,
37 - &[],
38 - )
39 - .unwrap();
40 - bare_repo.set_head("refs/heads/main").unwrap();
21 + let repo = init_bare(dir, "testowner", "testrepo");
22 + let readme = blob(&repo, b"# Test Repo\n");
23 + let root = tree(&repo, &[("README.md", readme, EntryKind::Blob)]);
24 + commit(&repo, "Initial commit", root, Vec::new());
41 25 }
42 26
43 27 /// Set up harness with git repos and owner user logged in.
@@ -113,26 +97,17 @@
113 97 /// Add a commit with the given message to the bare repo, returning (before_oid, after_oid).
114 98 fn add_commit_to_repo(dir: &std::path::Path, message: &str) -> (String, String) {
115 99 let bare_path = dir.join("testowner").join("testrepo.git");
116 - let repo = git2::Repository::open_bare(&bare_path).unwrap();
100 + let repo = gix::open(&bare_path).unwrap();
117 101
118 - let head = repo.head().unwrap();
119 - let parent = head.peel_to_commit().unwrap();
102 + let parent = repo.head_commit().unwrap();
120 103 let before = parent.id().to_string();
104 + let tree = parent.tree().unwrap().id;
121 105
122 - let sig = git2::Signature::now("Test", "test@example.com").unwrap();
123 - let tree = parent.tree().unwrap();
124 - let after_oid = repo
125 - .commit(
126 - Some("refs/heads/main"),
127 - &sig,
128 - &sig,
129 - message,
130 - &tree,
131 - &[&parent],
132 - )
133 - .unwrap();
106 + // Same tree as the parent: these tests only care about the message.
107 + let after =
108 + crate::harness::gitfixture::commit(&repo, message, tree, vec![parent.id().detach()]);
134 109
135 - (before, after_oid.to_string())
110 + (before, after.to_string())
136 111 }
137 112
138 113 // ══════════════════════════════════════════════════════════════════════