//! Git stays responsive through the shell, and cancellation removes only its own index lock. use sausage::{ exec::{Command, Exec, Options}, process::Runtime, world::World, }; use sausage_vfs::{Layer, Open}; use std::{ path::{Path, PathBuf}, sync::{Arc, OnceLock}, time::Duration, }; fn guests() -> &'static Path { static BUILT: OnceLock = OnceLock::new(); BUILT.get_or_init(|| { let repo = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); let target = PathBuf::from(std::env::var("CARGO_TARGET_DIR").unwrap()); for script in ["userland/cwd-init/build.nu", "userland/brush/build.nu"] { if script == "userland/cwd-init/build.nu" && repo.join("userland/cwd-init/init.o").is_file() { continue; } assert!( std::process::Command::new("nu") .args(["--no-config-file", script]) .current_dir(repo) .status() .unwrap() .success() ); } assert!( std::process::Command::new("cargo") .args([ "build", "--release", "--locked", "--target", "wasm32-wasip2", "-p", "git" ]) .current_dir(repo.join("userland")) .status() .unwrap() .success() ); target.join("wasm32-wasip2/release") }) } fn native(cwd: &Path, args: &[&str]) { let output = std::process::Command::new("git") .args(args) .current_dir(cwd) .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); } struct Fixture { _temp: tempfile::TempDir, runtime: Arc, root: Arc, processes: sausage::process::Processes, } fn fixture() -> Fixture { let programs = guests(); let temp = tempfile::tempdir().unwrap(); let root = temp.path().join("world"); for path in ["bin", "src/demo/src", "worktrees"] { std::fs::create_dir_all(root.join(path)).unwrap(); } for (source, name) in [("brush", "sh"), ("git", "git")] { std::fs::copy( programs.join(format!("{source}.wasm")), root.join(format!("bin/{name}.wasm")), ) .unwrap(); } let repo = root.join("src/demo"); std::fs::write(repo.join("src/main.rs"), "old\n").unwrap(); native(&repo, &["init", "-b", "main"]); native(&repo, &["add", "."]); native( &repo, &[ "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--no-gpg-sign", "-m", "initial", ], ); native( &repo, &[ "worktree", "add", "-b", "worker", root.join("worktrees/160").to_str().unwrap(), ], ); std::fs::write( root.join("worktrees/160/.git"), "gitdir: /src/demo/.git/worktrees/160\n", ) .unwrap(); std::fs::write( repo.join(".git/worktrees/160/gitdir"), "/worktrees/160/.git\n", ) .unwrap(); let world = World::open(temp.path()).unwrap(); let root = world.root.clone(); let processes = world.processes.clone(); let runtime = Runtime::new(world).unwrap(); Fixture { _temp: temp, runtime, root, processes, } } fn options() -> Options { Options { cwd: "/worktrees/160".into(), env: [ ("GIT_AUTHOR_NAME", "Test"), ("GIT_AUTHOR_EMAIL", "test@example.com"), ("GIT_COMMITTER_NAME", "Test"), ("GIT_COMMITTER_EMAIL", "test@example.com"), ] .map(|(k, v)| (k.into(), v.into())) .into(), ..Default::default() } } async fn run(fixture: &Fixture, script: &str) -> String { let mut output = Vec::new(); let code = fixture .runtime .exec(Command::Shell(script.into()), options(), &mut |_, bytes| { output.extend_from_slice(bytes) }) .await .unwrap(); assert_eq!(code, 0, "{}", String::from_utf8_lossy(&output)); String::from_utf8(output).unwrap() } async fn edit(fixture: &Fixture) { let file = fixture .root .open( "/worktrees/160/src/main.rs", Open { write: true, truncate: true, ..Default::default() }, ) .await .unwrap(); file.write(0, b"changed\n".to_vec()).await.unwrap(); } #[cfg(not(debug_assertions))] #[tokio::test(flavor = "current_thread")] async fn linked_worktree_commands_stay_under_two_seconds() { let fixture = fixture(); run(&fixture, "git status --short").await; edit(&fixture).await; for script in [ "git status --short", "git log -1", "git add src/main.rs && git commit --no-gpg-sign -m changed", ] { let start = std::time::Instant::now(); let output = run(&fixture, script).await; let elapsed = start.elapsed(); println!("{script}: {elapsed:?}\n{output}"); assert!(elapsed < Duration::from_secs(2), "{script}: {elapsed:?}"); } } #[tokio::test(flavor = "current_thread")] async fn cancelled_add_releases_its_index_lock() { let fixture = fixture(); run(&fixture, "git status --short").await; edit(&fixture).await; let path = "/src/demo/.git/worktrees/160/index.lock"; let mut changes = fixture .root .observer() .unwrap() .subscribe(vec![path.into()]); let mut output = Vec::new(); let mut collect = |_, bytes: &[u8]| output.extend_from_slice(bytes); let mut command = Box::pin(fixture.runtime.exec( Command::Argv(vec![ "/bin/git.wasm".into(), "add".into(), "src/main.rs".into(), ]), options(), &mut collect, )); tokio::time::timeout(Duration::from_secs(30), async { tokio::select! { biased; change = changes.recv() => { assert_eq!(change.unwrap().0, path); }, result = &mut command => panic!("add completed before cancellation: {result:?}"), } }) .await .unwrap(); assert!( !fixture.processes.lock().unwrap().is_empty(), "Git must still run when cancellation occurs" ); drop(command); tokio::time::timeout(Duration::from_secs(30), async { while !fixture.processes.lock().unwrap().is_empty() { tokio::task::yield_now().await; } }) .await .unwrap(); assert!(fixture.root.stat(path, false).await.is_err()); run( &fixture, "git add src/main.rs && git commit --no-gpg-sign -m recovered", ) .await; }