diff --git a/src/main.rs b/src/main.rs index 53bbad23..522db4dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -174,12 +174,15 @@ fn run(cli: Cli) -> anyhow::Result<()> { let git_submodules = cli.globals.git_submodules; let cwd = std::env::current_dir()?; if cwd.join(zed_cli::transaction::STAGING_DIR).is_dir() { - // Every live project transaction already owns this kernel-backed - // install lock. Recover under the same lock so a concurrent process - // cannot mistake an in-flight rollback journal for an abandoned one. - let store = Store::new(&cfg.home); - let _recovery_lock = store.install_lock()?; - zed_cli::transaction::recover_pending(&cwd)?; + // Recovery mutates checkout-owned paths. Acquire the canonical + // checkout operation lock before the store lock so another process + // cannot mistake a live transaction journal for an abandoned one. + zed_cli::project_lock::with_lock(&cwd, "recover interrupted project transaction", || { + let store = Store::new(&cfg.home); + let _recovery_lock = store.install_lock()?; + zed_cli::transaction::recover_pending(&cwd)?; + Ok(()) + })?; } match cli.cmd { Cmd::Init { org, name } => ops::init(&cwd, org, name, cfg.interactive), diff --git a/tests/mise_export_reserved_paths_cli.rs b/tests/mise_export_reserved_paths_cli.rs index 584eca58..a3ec66e3 100644 --- a/tests/mise_export_reserved_paths_cli.rs +++ b/tests/mise_export_reserved_paths_cli.rs @@ -51,6 +51,28 @@ fn run_zed(project: &Path, home: &Path, output: &str) -> Output { run_zed_with_plan(project, home, "zed-env.json", output) } +fn remove_operation_rendezvous(project: &Path) { + let zed_dir = project.join(".zed"); + let operation_lock = zed_dir.join("operation.lock"); + let metadata = fs::symlink_metadata(&operation_lock) + .expect("CLI mutation attempts should retain the operation rendezvous"); + assert!(metadata.is_file()); + assert!(!metadata.file_type().is_symlink()); + + let unexpected = fs::read_dir(&zed_dir) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .filter(|name| name != "operation.lock") + .collect::>(); + assert!( + unexpected.is_empty(), + "reserved-path rejection left native adapter state: {unexpected:?}" + ); + + fs::remove_file(operation_lock).unwrap(); + fs::remove_dir(zed_dir).unwrap(); +} + #[test] fn real_cli_refuses_source_state_and_transaction_staging_destinations() { let temp = tempfile::tempdir().unwrap(); @@ -94,7 +116,7 @@ fn real_cli_refuses_source_state_and_transaction_staging_destinations() { ); assert!(!project.join("mise.toml").exists()); fs::remove_file(project.join(".zed/mise-export-state.json")).unwrap(); - fs::remove_dir(project.join(".zed")).unwrap(); + remove_operation_rendezvous(&project); assert_eq!( fs::read(project.join("zed-env.json")).unwrap(),