ReleaseLock::acquire (src/monorepo/run/lock.rs:38) hard-requires a classic .git directory:
let git_dir = repo_root.join(".git");
if !git_dir.is_dir() {
return Err(anyhow!(
"release lock cannot acquire — {} is not a regular .git directory \
(worktrees and submodules currently unsupported by the lock)", ...
In a linked worktree .git is a file containing gitdir: /path/to/main/.git/worktrees/<name>, so is_dir() is false and every mutating command (release, and anything else taking the lock) aborts before doing any work. Same for a submodule. The error message states the limitation honestly, but it means the tool simply cannot be used in a worktree-based flow — which is a normal way to run releases for a maintenance branch alongside main, and how several repos in this org are laid out.
The rest of the codebase already handles this correctly. cache.rs:39 uses repo.git_dir(), which gix resolves through the .git file to the real per-worktree git dir. The lock is the only place that reimplements the path by hand.
Fix
Take the Repository (or the resolved git dir) instead of repo_root and use repo.git_dir(), matching cache_dir. That lands the lock in .git/worktrees/<name>/ferrflow.lock for a worktree.
That raises a design question worth settling in the same change: which lock does a worktree release want? Two worktrees of the same repo push to the same remote and compete on the same refs, so a per-worktree lock does not actually prevent the race the lock is for. gix exposes the common dir (repo.common_dir()), which is shared across worktrees — putting the lock there gives one lock per repository, which is the correct semantics. Suggest common_dir(), with the per-worktree path only if there is a reason to allow parallel worktree releases.
Bare repos should keep erroring, but with a message that says "bare" rather than pointing at worktrees.
Tests
git worktree add a second checkout, acquire the lock from it, assert it succeeds and lands in the expected path.
- Acquire from the main checkout while the worktree holds it, assert it is refused (confirms the common-dir choice).
- Bare repo still errors, with the bare-specific message.
ReleaseLock::acquire(src/monorepo/run/lock.rs:38) hard-requires a classic.gitdirectory:In a linked worktree
.gitis a file containinggitdir: /path/to/main/.git/worktrees/<name>, sois_dir()is false and every mutating command (release, and anything else taking the lock) aborts before doing any work. Same for a submodule. The error message states the limitation honestly, but it means the tool simply cannot be used in a worktree-based flow — which is a normal way to run releases for a maintenance branch alongside main, and how several repos in this org are laid out.The rest of the codebase already handles this correctly.
cache.rs:39usesrepo.git_dir(), which gix resolves through the.gitfile to the real per-worktree git dir. The lock is the only place that reimplements the path by hand.Fix
Take the
Repository(or the resolved git dir) instead ofrepo_rootand userepo.git_dir(), matchingcache_dir. That lands the lock in.git/worktrees/<name>/ferrflow.lockfor a worktree.That raises a design question worth settling in the same change: which lock does a worktree release want? Two worktrees of the same repo push to the same remote and compete on the same refs, so a per-worktree lock does not actually prevent the race the lock is for.
gixexposes the common dir (repo.common_dir()), which is shared across worktrees — putting the lock there gives one lock per repository, which is the correct semantics. Suggestcommon_dir(), with the per-worktree path only if there is a reason to allow parallel worktree releases.Bare repos should keep erroring, but with a message that says "bare" rather than pointing at worktrees.
Tests
git worktree adda second checkout, acquire the lock from it, assert it succeeds and lands in the expected path.