1 Commits
Author SHA1 Message Date
Bohan Jiang 0a54725fe6 MUL-6567 fix(execenv): lock a local_directory repo's git section across processes (#7462)
The lock meant to serialise git admin work on one repository was an in-process
sync.Mutex, and no two contenders were ever in one process: a daemon prepares
every task in a short-lived helper, and worktree mode deliberately skips the
per-path task mutex, so N concurrent tasks on one local_directory were N
processes issuing git commands at the same repo with nothing between them.

`git stash create`, which captures the user's uncommitted work before the
worktree is built, needs the repository's index lock and lost that race
routinely — reproducibly in about two runs out of three for two concurrent
prepares on a dirty repo. git reports the loss as exit status 1 with nothing
on stderr, and nothing retries a failed task, so the run died undiagnosable
and the issue rolled back to todo.

- lockGitRoot now takes an advisory file lock in the repository's COMMON git
  dir alongside the mutex, reusing the flock/LockFileEx primitives the env-root
  claim relies on. The common dir is what makes one lock cover one repository:
  the guarded section contends both for the per-worktree index.lock and for the
  refs / worktrees/ state every linked worktree shares. The file lock is taken
  before the mutex so waiting is not itself serialised. A git dir that cannot
  hold a lock file falls back to the mutex; a BUSY lock fails the caller.
- The capture retries, 5 attempts over ~1.5s, for the index-lock races owned by
  the user's own tools. `stash create` writes a commit object and returns its
  sha without touching the index, refs or working tree, so retrying repeats
  nothing.
- runGitStdout renders git's stderr into its error instead of collecting it
  into ExitError.Stderr and dropping it, and the capture names .git/index.lock
  when git itself says nothing.

Finalize and Discard take the same lock, so a prepare in a helper and a
finalize in the daemon parent no longer overlap. Finalize reports its worktree
as preserved if it cannot get the lock.

Seven tests, each verified to fail against the code it fixes, including a
second PROCESS holding the lock and two concurrent PrepareIsolated runs on one
repo. The timeout message is pinned never to advise deleting a held lock file:
the lock binds to an open file description, not a path, so unlinking it would
let two tasks into the same section at once.

Refs #7434
2026-08-24 01:12:39 +08:00