Skip to content

Commit 532722c

Browse files
committed
Tests: Build git fixtures via gix instead of CLI
- The git module's test fixtures used to chain `fork+exec` of `git` for every init / commit / branch / checkout. A modest fixture like `build_repo_with_branches(&[("a", 5), ("b", 1), ("c", 2)])` spawned ~31 `git` processes; under `./scripts/check.sh` parallel-check contention the per-spawn cost climbed enough to push two tests over the intentional 8 s nextest cap. - New `file_system/git/test_fixtures.rs` exposes a `Fixture` wrapper around `gix::Repository` plus `build_simple_repo` / `build_repo_with_branches` helpers. Init, commit, branch, checkout, and lightweight tag creation all run in-process; the index file is synced after each commit so `list_status` and `git checkout` follow-ups still work. `commit_files_with_modes` lets the executable-bit test pin `EntryKind::BlobExecutable` directly. - Migrated `tests.rs`, `m2_tests.rs`, `m3_tests.rs`, `m4_tests.rs`, `snapshot_dates_tests.rs` end-to-end. The handful of operations gix 0.81 doesn't expose publicly (stash creation, `git worktree add`, `git submodule add`, detach HEAD, bare init) keep a thin `git_cli` shell-out — one or two calls per affected test, no longer the dominant cost. - Enabled the `tree-editor` feature on the existing `gix` dep (already 56 sub-crates in lockfile; pure API addition, no new transitive deps). - `bench.rs` stays on CLI (all `#[ignore]`-gated, fixture cached). `status.rs::cache_tests` stays on CLI (uses `git add` without commit, needs raw index manipulation, low value to migrate). - Result: 91 git module tests now total ~1.7 s wall-clock (was tens of seconds). The cancellation test that had timed out at 8 s runs in 73 ms.
1 parent f4c0b5a commit 532722c

8 files changed

Lines changed: 668 additions & 486 deletions

File tree

apps/desktop/src-tauri/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,13 @@ rand = "0.10"
107107
# Pinned at 0.81.0 (published 2026-03-22, ~5 weeks old at landing) per
108108
# project rule on avoiding 0-day vulns. Default features include `status`,
109109
# `basic`, and `extras` which cover discover + ahead/behind + status walk.
110-
gix = "0.81"
110+
# `tree-editor` opts into the in-memory `Repository::edit_tree()` API used
111+
# by `file_system/git/test_fixtures.rs` so test fixtures build via gix
112+
# instead of dozens of `git` CLI `fork+exec` calls (each ~50-150 ms on a
113+
# loaded machine; one fixture chain used to spawn ~31 of them and time
114+
# out the 8 s nextest cap under check.sh contention). Pure API addition,
115+
# no extra runtime cost on the prod read paths.
116+
gix = { version = "0.81", features = ["tree-editor"] }
111117
# Multi-provider LLM client. Normalizes OpenAI / OpenAI Responses / Anthropic / Gemini /
112118
# xAI / Groq / DeepSeek / Ollama / OpenRouter / etc. Auto-routes `gpt-5*` / `*-codex` /
113119
# `*-pro` to the Responses API; uses native protocols for Anthropic + Gemini. Replaces

apps/desktop/src-tauri/src/file_system/git/m2_tests.rs

Lines changed: 66 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,71 @@
11
//! Integration tests for M2 – virtual `.git/` portal.
22
//!
3-
//! Builds tiny fixture repos with the `git` CLI (already a system requirement
4-
//! for M1) and exercises the volume hooks end-to-end.
3+
//! Fixtures go through `test_fixtures::Fixture` (in-process gix). The
4+
//! one test that asserts byte-for-byte parity with `git show` still
5+
//! shells out for that comparison (no gix-side equivalent that's
6+
//! cheaper than just opening the blob).
57
68
#![cfg(test)]
79

810
use std::os::unix::fs::PermissionsExt;
911
use std::path::{Path, PathBuf};
10-
use std::process::{Command, Stdio};
1112

1213
use super::path::{Cat, VirtualGitPath, classify, is_virtual, to_path};
1314
use super::read_blob::GitBlobReadStream;
1415
use super::repo::discover_repo;
16+
use super::test_fixtures::{EntryKind, Fixture, cleanup, git_cli_capture, temp_dir};
1517
use super::{tree, virtual_listing};
1618
use crate::file_system::volume::VolumeReadStream;
1719

18-
fn temp_dir(name: &str) -> PathBuf {
19-
let dir = std::env::temp_dir().join(format!(
20-
"cmdr_git_m2_{}_{}_{}",
21-
name,
22-
std::process::id(),
23-
std::time::SystemTime::now()
24-
.duration_since(std::time::UNIX_EPOCH)
25-
.map(|d| d.as_nanos())
26-
.unwrap_or_default()
27-
));
28-
let _ = std::fs::remove_dir_all(&dir);
29-
std::fs::create_dir_all(&dir).expect("create temp dir");
30-
dir
31-
}
32-
33-
fn cleanup(dir: &Path) {
34-
let _ = std::fs::remove_dir_all(dir);
35-
}
36-
37-
fn git(dir: &Path, args: &[&str]) {
38-
let status = Command::new("git")
39-
.current_dir(dir)
40-
.args(args)
41-
.env("GIT_AUTHOR_NAME", "Cmdr Test")
42-
.env("GIT_AUTHOR_EMAIL", "test@cmdr.local")
43-
.env("GIT_COMMITTER_NAME", "Cmdr Test")
44-
.env("GIT_COMMITTER_EMAIL", "test@cmdr.local")
45-
.stdout(Stdio::null())
46-
.stderr(Stdio::null())
47-
.status()
48-
.expect("git command");
49-
assert!(status.success(), "git {:?} failed in {}", args, dir.display());
50-
}
51-
5220
fn git_show_bytes(dir: &Path, spec: &str) -> Vec<u8> {
53-
Command::new("git")
54-
.current_dir(dir)
55-
.args(["show", spec])
56-
.stdout(Stdio::piped())
57-
.stderr(Stdio::piped())
58-
.output()
59-
.expect("git show")
60-
.stdout
21+
git_cli_capture(dir, &["show", spec])
6122
}
6223

6324
fn build_fixture_repo() -> PathBuf {
64-
let dir = temp_dir("portal");
65-
git(&dir, &["init", "-q", "-b", "main"]);
66-
git(&dir, &["config", "user.name", "Cmdr Test"]);
67-
git(&dir, &["config", "user.email", "test@cmdr.local"]);
25+
let dir = temp_dir("m2", "portal");
26+
let mut f = Fixture::init(dir.clone());
6827

28+
// Set executable bit on `scripts/run.sh` before the commit so the
29+
// tree records mode 0o755 (`BlobExecutable`). The on-disk perm
30+
// assignment also matches what a user would see in a checked-out
31+
// working tree.
6932
std::fs::create_dir_all(dir.join("scripts")).unwrap();
70-
std::fs::write(dir.join("README.md"), "hello\n").unwrap();
7133
std::fs::write(dir.join("scripts").join("run.sh"), "#!/bin/sh\necho hi\n").unwrap();
72-
let perm = std::fs::Permissions::from_mode(0o755);
73-
std::fs::set_permissions(dir.join("scripts").join("run.sh"), perm).unwrap();
74-
75-
git(&dir, &["add", "."]);
76-
git(&dir, &["commit", "-q", "-m", "initial"]);
77-
78-
// Create a branch with a slash in its name so the path classifier has
79-
// a non-trivial case to handle.
80-
git(&dir, &["branch", "feature/foo"]);
34+
std::fs::set_permissions(
35+
dir.join("scripts").join("run.sh"),
36+
std::fs::Permissions::from_mode(0o755),
37+
)
38+
.unwrap();
39+
40+
f.commit_files_with_modes(
41+
&[
42+
("README.md", b"hello\n", EntryKind::Blob),
43+
("scripts/run.sh", b"#!/bin/sh\necho hi\n", EntryKind::BlobExecutable),
44+
],
45+
"initial",
46+
1_700_000_000,
47+
);
8148

82-
// Tag the initial commit (lightweight is enough for M2).
83-
git(&dir, &["tag", "v1.0"]);
49+
// Create a branch with a slash in its name so the path classifier
50+
// has a non-trivial case to handle.
51+
f.create_branch("feature/foo");
52+
53+
// Lightweight tag at HEAD.
54+
let head_id = f
55+
.repo
56+
.find_reference("refs/heads/main")
57+
.unwrap()
58+
.peel_to_id()
59+
.unwrap()
60+
.detach();
61+
f.repo
62+
.reference(
63+
"refs/tags/v1.0",
64+
head_id,
65+
gix::refs::transaction::PreviousValue::MustNotExist,
66+
"test_fixtures: lightweight tag",
67+
)
68+
.expect("create tag ref");
8469

8570
dir
8671
}
@@ -335,7 +320,7 @@ async fn cross_volume_copy_preserves_executable_bit() {
335320
let repo_dir = build_fixture_repo();
336321
let (_, root) = discover_repo(&repo_dir).unwrap();
337322

338-
let dest_dir = temp_dir("copy_dest");
323+
let dest_dir = temp_dir("m2", "copy_dest");
339324

340325
let src = LocalPosixVolume::new("src", root.clone());
341326
let dst = LocalPosixVolume::new("dst", dest_dir.clone());
@@ -426,11 +411,25 @@ fn watcher_invalidates_branches_listing_on_new_branch() {
426411
);
427412
}
428413

429-
// Make the watcher see a "ref change" by adding a new branch and
430-
// calling the invalidation directly. We don't drive the notify-rs
431-
// event loop here – the unit-level contract is "given a repo root,
432-
// invalidate matching listings."
433-
git(&dir, &["branch", "added-after-init"]);
414+
// Make the watcher see a "ref change" by adding a new branch via
415+
// gix, then run the invalidation entry point directly. The unit-
416+
// level contract is "given a repo root, invalidate matching
417+
// listings" — driving notify-rs isn't needed.
418+
let new_handle = handle.to_thread_local();
419+
let head_id = new_handle
420+
.find_reference("refs/heads/main")
421+
.unwrap()
422+
.peel_to_id()
423+
.unwrap()
424+
.detach();
425+
new_handle
426+
.reference(
427+
"refs/heads/added-after-init",
428+
head_id,
429+
gix::refs::transaction::PreviousValue::MustNotExist,
430+
"m2_tests: new branch",
431+
)
432+
.expect("create branch ref");
434433
super::watcher::invalidate_for_test(&root);
435434

436435
// Assert the listing is still in the cache (we full-refresh, not evict).

0 commit comments

Comments
 (0)