From 77ce10d9d1bbf59d114f786a729d6fa6a9463828 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 11:14:11 -0500 Subject: [PATCH 01/13] feat(DEN-2038): expose reusable project operation guard --- src/project_lock.rs | 68 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/src/project_lock.rs b/src/project_lock.rs index 564d05ec..811a0b4b 100644 --- a/src/project_lock.rs +++ b/src/project_lock.rs @@ -12,7 +12,7 @@ use std::fs; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; -use zed_lock::{LockClass, LockManager, LockRequest}; +use zed_lock::{LockClass, LockGuard, LockManager, LockRequest}; pub const OPERATION_LOCK_RELATIVE_PATH: &str = ".zed/operation.lock"; @@ -56,6 +56,19 @@ impl Drop for HeldMarker { } } +/// Owned checkout-local mutation authority. +/// +/// A nested same-thread acquisition carries only a depth marker because the +/// outer guard already owns the descriptor lock. Independent threads and +/// processes always contend through `zed-lock`. +#[must_use = "dropping the operation guard releases project mutation ownership"] +pub struct OperationGuard { + // Drop the thread-local marker before the descriptor lock. That keeps the + // reentrancy view conservative for the entire lifetime of kernel ownership. + _marker: HeldMarker, + _guard: Option, +} + /// Canonical descriptor-lock identity for one checkout. /// /// Canonicalizing the project before appending the stable relative path makes @@ -71,24 +84,21 @@ fn is_held_on_this_thread(path: &Path) -> bool { HELD_LOCKS.with(|held| held.borrow().contains_key(path)) } -/// Run a complete project mutation while owning the checkout-local lock. +/// Acquire checkout-local mutation ownership and return an RAII guard. /// -/// Nested calls on the same thread and canonical checkout reuse the outer -/// ownership. Independent threads and processes still go through `zed-lock`, -/// whose descriptor lock is the sole local ownership authority and is released -/// automatically if the process exits. -pub fn with_lock( - project: &Path, - operation: &str, - action: impl FnOnce() -> Result, -) -> Result { +/// This form is useful when callers must span multiple library calls under one +/// project mutation boundary. Nested calls on the same thread reuse the outer +/// descriptor ownership rather than issuing a second kernel lock request. +pub fn acquire(project: &Path, operation: &str) -> Result { let path = operation_lock_path(project)?; if is_held_on_this_thread(&path) { - let _marker = HeldMarker::enter(path); - return action(); + return Ok(OperationGuard { + _marker: HeldMarker::enter(path), + _guard: None, + }); } - let _guard = LockManager::global() + let guard = LockManager::global() .acquire_blocking( LockRequest::exclusive(&path) .operation(operation) @@ -100,7 +110,24 @@ pub fn with_lock( path.display() ) })?; - let _marker = HeldMarker::enter(path); + Ok(OperationGuard { + _marker: HeldMarker::enter(path), + _guard: Some(guard), + }) +} + +/// Run a complete project mutation while owning the checkout-local lock. +/// +/// Nested calls on the same thread and canonical checkout reuse the outer +/// ownership. Independent threads and processes still go through `zed-lock`, +/// whose descriptor lock is the sole local ownership authority and is released +/// automatically if the process exits. +pub fn with_lock( + project: &Path, + operation: &str, + action: impl FnOnce() -> Result, +) -> Result { + let _guard = acquire(project, operation)?; action() } @@ -108,7 +135,7 @@ pub fn with_lock( mod tests { use anyhow::Result; - use super::{OPERATION_LOCK_RELATIVE_PATH, operation_lock_path, with_lock}; + use super::{OPERATION_LOCK_RELATIVE_PATH, acquire, operation_lock_path, with_lock}; #[test] fn canonical_aliases_share_one_lock_identity() -> Result<()> { @@ -135,4 +162,13 @@ mod tests { assert!(operation_lock_path(project.path())?.is_file()); Ok(()) } + + #[test] + fn explicit_guard_allows_nested_facade_reuse() -> Result<()> { + let project = tempfile::tempdir()?; + let _guard = acquire(project.path(), "multi-step mutation")?; + with_lock(project.path(), "nested facade", || Ok(()))?; + assert!(operation_lock_path(project.path())?.is_file()); + Ok(()) + } } From fd09b1ce5c8d2a1b8159a6271d52c4e23078cca8 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 11:14:30 -0500 Subject: [PATCH 02/13] feat(DEN-2038): lock Git submodule takeover by checkout --- src/git_submodules/cli.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/git_submodules/cli.rs b/src/git_submodules/cli.rs index f7ecd25f..fae30df7 100644 --- a/src/git_submodules/cli.rs +++ b/src/git_submodules/cli.rs @@ -111,7 +111,11 @@ fn run_cli(args: Vec) -> Result { "no takeover source selected; pass `--git-submodules` or set ZED_PKG_GIT_SUBMODULES=1" ); } - let report = super::overtake(&cwd, &cfg)?; + let report = crate::project_lock::with_lock( + &cwd, + "adopt Git submodules into Zed package graph", + || super::overtake(&cwd, &cfg), + )?; println!( "overtook {} Git submodule package(s) in {}", report.adopted, From 33458e38ef7b19a4bd1e8ed093b8ecc1740e157d Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 11:53:23 -0500 Subject: [PATCH 03/13] fix(DEN-2038): lock takeover by discovered superproject --- src/git_submodules/cli.rs | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/git_submodules/cli.rs b/src/git_submodules/cli.rs index fae30df7..13f8e20d 100644 --- a/src/git_submodules/cli.rs +++ b/src/git_submodules/cli.rs @@ -1,5 +1,6 @@ use std::env; use std::ffi::{OsStr, OsString}; +use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; use clap::{Args, Parser, Subcommand}; @@ -90,6 +91,19 @@ fn print_root_help() -> Result<()> { Ok(()) } +/// Resolve the authority root before acquiring checkout-local mutation +/// ownership. `overtake` supports invocation below the superproject, so locking +/// the raw current directory would create a second `.zed/operation.lock` and +/// allow a root-level lifecycle command to mutate the same checkout concurrently. +fn takeover_project_root(requested: &Path) -> Result { + super::find_root(requested).with_context(|| { + format!( + "`zed overtake --git-submodules` requires .gitmodules at or above {}", + requested.display() + ) + }) +} + fn run_cli(args: Vec) -> Result { crate::flags::normalize_global_boolean_environment(&args)?; let cli = match OvertakeCli::try_parse_from(args) { @@ -111,10 +125,11 @@ fn run_cli(args: Vec) -> Result { "no takeover source selected; pass `--git-submodules` or set ZED_PKG_GIT_SUBMODULES=1" ); } + let project = takeover_project_root(&cwd)?; let report = crate::project_lock::with_lock( - &cwd, + &project, "adopt Git submodules into Zed package graph", - || super::overtake(&cwd, &cfg), + || super::overtake(&project, &cfg), )?; println!( "overtook {} Git submodule package(s) in {}", @@ -202,9 +217,12 @@ fn global_option_takes_value(token: &str) -> bool { #[cfg(test)] mod tests { + use std::fs; + + use anyhow::Result; use clap::Parser; - use super::OvertakeCli; + use super::{OvertakeCli, takeover_project_root}; #[test] fn takeover_accepts_the_global_switch_before_and_after_the_command() { @@ -220,4 +238,19 @@ mod tests { OvertakeCli::try_parse_from(["zed", "overtake", "--git-submodules=false"]).unwrap(); assert!(!cli.globals.git_submodules); } + + #[test] + fn nested_takeover_uses_the_superproject_operation_lock() -> Result<()> { + let root = tempfile::tempdir()?; + fs::write(root.path().join(".gitmodules"), b"")?; + let nested = root.path().join("packages/client/src"); + fs::create_dir_all(&nested)?; + + let selected = takeover_project_root(&nested)?; + assert_eq!( + crate::project_lock::operation_lock_path(&selected)?, + crate::project_lock::operation_lock_path(root.path())? + ); + Ok(()) + } } From 35f3f238deac1ab5d13a310bf6b1a089f8e3ec54 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 11:55:34 -0500 Subject: [PATCH 04/13] fix(DEN-2038): keep operation guards thread-affine --- src/project_lock.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/project_lock.rs b/src/project_lock.rs index 811a0b4b..252e271a 100644 --- a/src/project_lock.rs +++ b/src/project_lock.rs @@ -9,7 +9,9 @@ use std::cell::RefCell; use std::collections::HashMap; use std::fs; +use std::marker::PhantomData; use std::path::{Path, PathBuf}; +use std::rc::Rc; use anyhow::{Context, Result}; use zed_lock::{LockClass, LockGuard, LockManager, LockRequest}; @@ -61,12 +63,25 @@ impl Drop for HeldMarker { /// A nested same-thread acquisition carries only a depth marker because the /// outer guard already owns the descriptor lock. Independent threads and /// processes always contend through `zed-lock`. +/// +/// Reentrancy bookkeeping is thread-local, so this guard is intentionally +/// neither [`Send`] nor [`Sync`]. It must be dropped on the thread that acquired +/// it; moving it would otherwise detach marker cleanup from the originating +/// thread. +/// +/// ```compile_fail +/// fn assert_send() {} +/// assert_send::(); +/// ``` #[must_use = "dropping the operation guard releases project mutation ownership"] pub struct OperationGuard { // Drop the thread-local marker before the descriptor lock. That keeps the // reentrancy view conservative for the entire lifetime of kernel ownership. _marker: HeldMarker, _guard: Option, + // `Rc` is deliberately !Send + !Sync. The marker itself lives in a + // thread-local map and therefore cannot be cleaned up safely elsewhere. + _thread_affinity: PhantomData>, } /// Canonical descriptor-lock identity for one checkout. @@ -95,6 +110,7 @@ pub fn acquire(project: &Path, operation: &str) -> Result { return Ok(OperationGuard { _marker: HeldMarker::enter(path), _guard: None, + _thread_affinity: PhantomData, }); } @@ -113,6 +129,7 @@ pub fn acquire(project: &Path, operation: &str) -> Result { Ok(OperationGuard { _marker: HeldMarker::enter(path), _guard: Some(guard), + _thread_affinity: PhantomData, }) } From 6716bd10be51c0022700d59ad97ee2a48dd51228 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 12:01:58 -0500 Subject: [PATCH 05/13] fix(DEN-2038): share descriptor ownership across nested guards --- src/project_lock.rs | 139 +++++++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 60 deletions(-) diff --git a/src/project_lock.rs b/src/project_lock.rs index 252e271a..46b93df0 100644 --- a/src/project_lock.rs +++ b/src/project_lock.rs @@ -9,79 +9,53 @@ use std::cell::RefCell; use std::collections::HashMap; use std::fs; -use std::marker::PhantomData; use std::path::{Path, PathBuf}; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use anyhow::{Context, Result}; use zed_lock::{LockClass, LockGuard, LockManager, LockRequest}; pub const OPERATION_LOCK_RELATIVE_PATH: &str = ".zed/operation.lock"; +/// One thread's reentrant ownership cache. +/// +/// The map stores weak references so it never extends descriptor ownership. +/// Stale entries are removed lazily on the next acquisition of that canonical +/// checkout. Every live [`OperationGuard`] holds a strong reference to the same +/// [`OwnedLock`], so nested handles may be dropped in any order without +/// releasing the kernel lock before the final handle. thread_local! { - /// Same-thread facade composition is intentionally reentrant. For example, - /// the CLI may hold the checkout lock around Git-submodule synchronization - /// and then call the public installer facade, which must reuse that exact - /// ownership rather than deadlock on a second descriptor request. - static HELD_LOCKS: RefCell> = RefCell::new(HashMap::new()); -} - -struct HeldMarker { - path: PathBuf, + static HELD_LOCKS: RefCell>> = RefCell::new(HashMap::new()); } -impl HeldMarker { - fn enter(path: PathBuf) -> Self { - HELD_LOCKS.with(|held| { - *held.borrow_mut().entry(path.clone()).or_insert(0) += 1; - }); - Self { path } - } -} - -impl Drop for HeldMarker { - fn drop(&mut self) { - HELD_LOCKS.with(|held| { - let mut held = held.borrow_mut(); - let remove = match held.get_mut(&self.path) { - Some(count) if *count > 1 => { - *count -= 1; - false - } - Some(_) => true, - None => false, - }; - if remove { - held.remove(&self.path); - } - }); - } +struct OwnedLock { + _guard: LockGuard, } /// Owned checkout-local mutation authority. /// -/// A nested same-thread acquisition carries only a depth marker because the -/// outer guard already owns the descriptor lock. Independent threads and -/// processes always contend through `zed-lock`. +/// A nested same-thread acquisition clones the strong reference to the outer +/// descriptor ownership. The operating-system lock therefore remains held until +/// the final nested or outer handle is dropped, regardless of drop order. +/// Independent threads and processes always contend through `zed-lock`. /// /// Reentrancy bookkeeping is thread-local, so this guard is intentionally /// neither [`Send`] nor [`Sync`]. It must be dropped on the thread that acquired -/// it; moving it would otherwise detach marker cleanup from the originating -/// thread. +/// it; moving it would otherwise detach the ownership cache from the originating +/// thread. The `Rc` field enforces both type-level constraints. /// /// ```compile_fail /// fn assert_send() {} /// assert_send::(); /// ``` -#[must_use = "dropping the operation guard releases project mutation ownership"] +/// +/// ```compile_fail +/// fn assert_sync() {} +/// assert_sync::(); +/// ``` +#[must_use = "dropping the operation guard releases one project-ownership handle"] pub struct OperationGuard { - // Drop the thread-local marker before the descriptor lock. That keeps the - // reentrancy view conservative for the entire lifetime of kernel ownership. - _marker: HeldMarker, - _guard: Option, - // `Rc` is deliberately !Send + !Sync. The marker itself lives in a - // thread-local map and therefore cannot be cleaned up safely elsewhere. - _thread_affinity: PhantomData>, + _ownership: Rc, } /// Canonical descriptor-lock identity for one checkout. @@ -95,22 +69,27 @@ pub fn operation_lock_path(project: &Path) -> Result { Ok(canonical.join(OPERATION_LOCK_RELATIVE_PATH)) } -fn is_held_on_this_thread(path: &Path) -> bool { - HELD_LOCKS.with(|held| held.borrow().contains_key(path)) +fn ownership_on_this_thread(path: &Path) -> Option> { + HELD_LOCKS.with(|held| { + let mut held = held.borrow_mut(); + let ownership = held.get(path).and_then(Weak::upgrade); + if ownership.is_none() { + held.remove(path); + } + ownership + }) } /// Acquire checkout-local mutation ownership and return an RAII guard. /// /// This form is useful when callers must span multiple library calls under one -/// project mutation boundary. Nested calls on the same thread reuse the outer +/// project mutation boundary. Nested calls on the same thread share the outer /// descriptor ownership rather than issuing a second kernel lock request. pub fn acquire(project: &Path, operation: &str) -> Result { let path = operation_lock_path(project)?; - if is_held_on_this_thread(&path) { + if let Some(ownership) = ownership_on_this_thread(&path) { return Ok(OperationGuard { - _marker: HeldMarker::enter(path), - _guard: None, - _thread_affinity: PhantomData, + _ownership: ownership, }); } @@ -126,10 +105,15 @@ pub fn acquire(project: &Path, operation: &str) -> Result { path.display() ) })?; + let ownership = Rc::new(OwnedLock { _guard: guard }); + HELD_LOCKS.with(|held| { + let previous = held + .borrow_mut() + .insert(path, Rc::downgrade(&ownership)); + debug_assert!(previous.and_then(|entry| entry.upgrade()).is_none()); + }); Ok(OperationGuard { - _marker: HeldMarker::enter(path), - _guard: Some(guard), - _thread_affinity: PhantomData, + _ownership: ownership, }) } @@ -150,7 +134,10 @@ pub fn with_lock( #[cfg(test)] mod tests { + use std::rc::Rc; + use anyhow::Result; + use zed_lock::{LockClass, LockManager, LockRequest}; use super::{OPERATION_LOCK_RELATIVE_PATH, acquire, operation_lock_path, with_lock}; @@ -188,4 +175,36 @@ mod tests { assert!(operation_lock_path(project.path())?.is_file()); Ok(()) } + + #[test] + fn nested_guards_keep_descriptor_ownership_until_the_last_drop() -> Result<()> { + let project = tempfile::tempdir()?; + let path = operation_lock_path(project.path())?; + let outer = acquire(project.path(), "outer mutation")?; + let inner = acquire(project.path(), "inner mutation")?; + assert!(Rc::ptr_eq(&outer._ownership, &inner._ownership)); + assert_eq!(Rc::strong_count(&inner._ownership), 2); + + // Public guards are independent values and callers can drop them out of + // lexical order. The inner handle must continue owning the descriptor. + drop(outer); + assert_eq!(Rc::strong_count(&inner._ownership), 1); + let contender = LockManager::global().try_acquire( + LockRequest::exclusive(&path) + .operation("same-process contention probe") + .class(LockClass::ProjectMutation) + .queue_same_process(), + )?; + assert!(contender.is_none()); + + drop(inner); + let acquired = LockManager::global().try_acquire( + LockRequest::exclusive(&path) + .operation("post-release probe") + .class(LockClass::ProjectMutation) + .queue_same_process(), + )?; + assert!(acquired.is_some()); + Ok(()) + } } From 2c5787f1c3c4f43cc2535415d2fad11b681c2f15 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 12:06:29 -0500 Subject: [PATCH 06/13] style(DEN-2038): apply rustfmt to ownership cache --- src/project_lock.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/project_lock.rs b/src/project_lock.rs index 46b93df0..eadb30d7 100644 --- a/src/project_lock.rs +++ b/src/project_lock.rs @@ -107,9 +107,7 @@ pub fn acquire(project: &Path, operation: &str) -> Result { })?; let ownership = Rc::new(OwnedLock { _guard: guard }); HELD_LOCKS.with(|held| { - let previous = held - .borrow_mut() - .insert(path, Rc::downgrade(&ownership)); + let previous = held.borrow_mut().insert(path, Rc::downgrade(&ownership)); debug_assert!(previous.and_then(|entry| entry.upgrade()).is_none()); }); Ok(OperationGuard { From c1435af21d15a359853df3c51687df67d72af24a Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 12:16:28 -0500 Subject: [PATCH 07/13] fix(DEN-2038): keep thread-local cache notes lint-clean --- src/project_lock.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/project_lock.rs b/src/project_lock.rs index eadb30d7..d4dd2ebe 100644 --- a/src/project_lock.rs +++ b/src/project_lock.rs @@ -17,13 +17,13 @@ use zed_lock::{LockClass, LockGuard, LockManager, LockRequest}; pub const OPERATION_LOCK_RELATIVE_PATH: &str = ".zed/operation.lock"; -/// One thread's reentrant ownership cache. -/// -/// The map stores weak references so it never extends descriptor ownership. -/// Stale entries are removed lazily on the next acquisition of that canonical -/// checkout. Every live [`OperationGuard`] holds a strong reference to the same -/// [`OwnedLock`], so nested handles may be dropped in any order without -/// releasing the kernel lock before the final handle. +// One thread's reentrant ownership cache. +// +// The map stores weak references so it never extends descriptor ownership. +// Stale entries are removed lazily on the next acquisition of that canonical +// checkout. Every live `OperationGuard` holds a strong reference to the same +// `OwnedLock`, so nested handles may be dropped in any order without releasing +// the kernel lock before the final handle. thread_local! { static HELD_LOCKS: RefCell>> = RefCell::new(HashMap::new()); } From d163e98b9151340b368301b022c3a45bdf5bd70e Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 12:18:03 -0500 Subject: [PATCH 08/13] fix(DEN-2038): recover transactions under checkout ownership --- src/main.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 67f25141..acabc754 100644 --- a/src/main.rs +++ b/src/main.rs @@ -173,12 +173,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 the checkout and must use the same canonical + // descriptor-lock boundary as every new lifecycle mutation. Unlike the + // old Store install lock, this also coordinates processes using + // different ZED_PKG_HOME values without serializing unrelated projects. + zed_cli::project_lock::with_lock( + &cwd, + "recover interrupted Zed project transaction", + || zed_cli::transaction::recover_pending(&cwd).map(|_| ()), + )?; } match cli.cmd { Cmd::Init { org, name } => ops::init(&cwd, org, name, cfg.interactive), From e0ec6e7f8d50104f22c5719b4236c24cb333a304 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 12:24:04 -0500 Subject: [PATCH 09/13] fix(DEN-2038): recover before takeover Git transport --- src/git_submodules/cli.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/git_submodules/cli.rs b/src/git_submodules/cli.rs index 13f8e20d..63d78dc5 100644 --- a/src/git_submodules/cli.rs +++ b/src/git_submodules/cli.rs @@ -129,7 +129,14 @@ fn run_cli(args: Vec) -> Result { let report = crate::project_lock::with_lock( &project, "adopt Git submodules into Zed package graph", - || super::overtake(&project, &cfg), + || { + // Modular takeover dispatch bypasses the ordinary `run` + // entrypoint. Recover under the already-owned project lock + // before reading `.gitmodules` or executing Git transport, + // not only later when the first transaction begins. + crate::transaction::recover_pending(&project)?; + super::overtake(&project, &cfg) + }, )?; println!( "overtook {} Git submodule package(s) in {}", From 2b5897f6a5b5cf33fee7a5934a60d7aa3e70e0a0 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 12:37:50 -0500 Subject: [PATCH 10/13] fix(DEN-2038): hold project ownership across submodule install --- src/main.rs | 57 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index acabc754..eabad1de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -198,21 +198,50 @@ fn run(cli: Cli) -> anyhow::Result<()> { allow_ecosystem_mismatch, } => { if git_submodules { - submodules::sync(&cwd)?; + // Git synchronization mutates the submodule worktrees and must + // share one descriptor lifetime with manifest/lock resolution, + // materialization, adapter wiring, and Git-lock finalization. + // Resolve the superproject first so nested invocations and root + // invocations converge on one checkout-local ownership path. + let project = submodules::find_root(&cwd).unwrap_or_else(|| cwd.clone()); + let operation = if frozen { + "synchronize Git submodules and restore frozen Zed dependency graph" + } else { + "synchronize Git submodules and install Zed dependency graph" + }; + let _guard = zed_cli::project_lock::acquire(&project, operation)?; + // Close the journal-created-between-startup-and-lock window and + // recover a superproject journal when invoked below the root. + zed_cli::transaction::recover_pending(&project)?; + submodules::sync(&project)?; + managed_install::install( + &cwd, + &cfg, + &specs, + frozen, + install_mode, + adapter, + allow_build, + target.as_deref(), + allow_no_manifest, + allow_ecosystem_mismatch, + ) + .map(|_| ()) + } else { + managed_install::install( + &cwd, + &cfg, + &specs, + frozen, + install_mode, + adapter, + allow_build, + target.as_deref(), + allow_no_manifest, + allow_ecosystem_mismatch, + ) + .map(|_| ()) } - managed_install::install( - &cwd, - &cfg, - &specs, - frozen, - install_mode, - adapter, - allow_build, - target.as_deref(), - allow_no_manifest, - allow_ecosystem_mismatch, - ) - .map(|_| ()) } Cmd::Uninstall { specs } => ops::uninstall(&cwd, &cfg, &specs), Cmd::Env { cmd } => match cmd { From 160be5fb4b729bfad450f86e43639f1a68be6d92 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 12:54:55 -0500 Subject: [PATCH 11/13] chore(DEN-2038): stage cooperative install root finalizer --- ...den-2038-superproject-install-finalize.yml | 52 +++++++++++++++++++ .../den-2038-superproject-install-finalize.py | 25 +++++++++ 2 files changed, 77 insertions(+) create mode 100644 .github/workflows/den-2038-superproject-install-finalize.yml create mode 100644 scripts/den-2038-superproject-install-finalize.py diff --git a/.github/workflows/den-2038-superproject-install-finalize.yml b/.github/workflows/den-2038-superproject-install-finalize.yml new file mode 100644 index 00000000..420612df --- /dev/null +++ b/.github/workflows/den-2038-superproject-install-finalize.yml @@ -0,0 +1,52 @@ +name: DEN-2038 cooperative install root finalizer + +on: + push: + branches: + - agent/den-2038-overtake-operation-lock + +permissions: + contents: write + +concurrency: + group: den-2038-cooperative-install-root-finalizer + cancel-in-progress: false + +jobs: + finalize: + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - name: Check out the exact feature branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + + - name: Install stable Rust + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: stable + components: rustfmt, clippy + + - name: Apply and validate the superproject installer root + shell: bash + run: | + set -euo pipefail + python3 scripts/den-2038-superproject-install-finalize.py + cargo fmt --all -- --check + cargo check --locked --bin zed + cargo test --locked --lib project_lock + cargo test --locked --lib git_submodules + cargo clippy --locked --bin zed -- -D warnings + git diff --check + + - name: Publish the validated source correction + shell: bash + run: | + set -euo pipefail + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add -A + git commit -m 'fix(DEN-2038): install from the selected superproject' + git push origin "HEAD:${GITHUB_REF_NAME}" diff --git a/scripts/den-2038-superproject-install-finalize.py b/scripts/den-2038-superproject-install-finalize.py new file mode 100644 index 00000000..02189046 --- /dev/null +++ b/scripts/den-2038-superproject-install-finalize.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Keep cooperative submodule installation on the selected superproject.""" + +from pathlib import Path + +source = Path("src/main.rs") +text = source.read_text(encoding="utf-8") +old = ''' submodules::sync(&project)?; + managed_install::install( + &cwd, +''' +new = ''' submodules::sync(&project)?; + managed_install::install( + &project, +''' +if old in text: + if text.count(old) != 1: + raise SystemExit("expected exactly one cooperative-install cwd call") + text = text.replace(old, new, 1) +elif new not in text: + raise SystemExit("cooperative-install call is neither the old nor expected form") +source.write_text(text, encoding="utf-8") + +Path("scripts/den-2038-superproject-install-finalize.py").unlink() +Path(".github/workflows/den-2038-superproject-install-finalize.yml").unlink() From 28c68124e87301f562a8d423410943cf2de61064 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:02:12 +0000 Subject: [PATCH 12/13] fix(DEN-2038): install from the selected superproject --- ...den-2038-superproject-install-finalize.yml | 52 ------------------- .../den-2038-superproject-install-finalize.py | 25 --------- src/main.rs | 2 +- 3 files changed, 1 insertion(+), 78 deletions(-) delete mode 100644 .github/workflows/den-2038-superproject-install-finalize.yml delete mode 100644 scripts/den-2038-superproject-install-finalize.py diff --git a/.github/workflows/den-2038-superproject-install-finalize.yml b/.github/workflows/den-2038-superproject-install-finalize.yml deleted file mode 100644 index 420612df..00000000 --- a/.github/workflows/den-2038-superproject-install-finalize.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: DEN-2038 cooperative install root finalizer - -on: - push: - branches: - - agent/den-2038-overtake-operation-lock - -permissions: - contents: write - -concurrency: - group: den-2038-cooperative-install-root-finalizer - cancel-in-progress: false - -jobs: - finalize: - runs-on: ubuntu-24.04 - timeout-minutes: 25 - steps: - - name: Check out the exact feature branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.ref_name }} - fetch-depth: 0 - - - name: Install stable Rust - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 - with: - toolchain: stable - components: rustfmt, clippy - - - name: Apply and validate the superproject installer root - shell: bash - run: | - set -euo pipefail - python3 scripts/den-2038-superproject-install-finalize.py - cargo fmt --all -- --check - cargo check --locked --bin zed - cargo test --locked --lib project_lock - cargo test --locked --lib git_submodules - cargo clippy --locked --bin zed -- -D warnings - git diff --check - - - name: Publish the validated source correction - shell: bash - run: | - set -euo pipefail - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - git add -A - git commit -m 'fix(DEN-2038): install from the selected superproject' - git push origin "HEAD:${GITHUB_REF_NAME}" diff --git a/scripts/den-2038-superproject-install-finalize.py b/scripts/den-2038-superproject-install-finalize.py deleted file mode 100644 index 02189046..00000000 --- a/scripts/den-2038-superproject-install-finalize.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -"""Keep cooperative submodule installation on the selected superproject.""" - -from pathlib import Path - -source = Path("src/main.rs") -text = source.read_text(encoding="utf-8") -old = ''' submodules::sync(&project)?; - managed_install::install( - &cwd, -''' -new = ''' submodules::sync(&project)?; - managed_install::install( - &project, -''' -if old in text: - if text.count(old) != 1: - raise SystemExit("expected exactly one cooperative-install cwd call") - text = text.replace(old, new, 1) -elif new not in text: - raise SystemExit("cooperative-install call is neither the old nor expected form") -source.write_text(text, encoding="utf-8") - -Path("scripts/den-2038-superproject-install-finalize.py").unlink() -Path(".github/workflows/den-2038-superproject-install-finalize.yml").unlink() diff --git a/src/main.rs b/src/main.rs index eabad1de..a343a782 100644 --- a/src/main.rs +++ b/src/main.rs @@ -215,7 +215,7 @@ fn run(cli: Cli) -> anyhow::Result<()> { zed_cli::transaction::recover_pending(&project)?; submodules::sync(&project)?; managed_install::install( - &cwd, + &project, &cfg, &specs, frozen, From 591b40846da73f7540bd36add33645d11cd33ab0 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 13:12:17 -0500 Subject: [PATCH 13/13] docs(DEN-2038): clarify one checkout authority boundary --- src/project_lock.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/project_lock.rs b/src/project_lock.rs index d4dd2ebe..719b8f09 100644 --- a/src/project_lock.rs +++ b/src/project_lock.rs @@ -5,6 +5,8 @@ //! generated adapter wiring, Git-submodule projections, and transaction //! recovery. The lock lives inside the canonical project directory so two Zed //! processes using different `ZED_PKG_HOME` values still coordinate. +//! Git-submodule synchronization, installer finalization, and transaction +//! recovery all use this same authority root. use std::cell::RefCell; use std::collections::HashMap;