fix(dev-infra): guard dormant worktree configs

This commit is contained in:
Tianyi Cui
2026-07-27 21:31:12 +08:00
parent 6914b04107
commit 19b8794966
8 changed files with 87 additions and 18 deletions
@@ -12,13 +12,13 @@ Lefthook-generated hooks prefer an absolute binary path captured from the instal
## Decision
Hook installation is worktree-scoped. The installer requires Git 2.26 or newer for configuration-scope provenance, upgrades a format-0 repository to format 1, enables `extensions.worktreeConfig`, and assigns the current worktree an absolute `core.hooksPath` at `$GIT_DIR/dsh-hooks`. The main worktree receives `$GIT_COMMON_DIR/dsh-hooks`; each linked worktree receives the corresponding directory under `$GIT_COMMON_DIR/worktrees/<id>`. A repository-scoped lock serializes configuration migration and hook writes, including repeated concurrent installs. Each lock records a process ID and random ownership token; release verifies the same file identity and exact record. A dead or invalid lock is never broken automatically, so the diagnostic requires the contributor to confirm no installer is running and remove the lock manually.
Hook installation is worktree-scoped. The installer requires Git 2.26 or newer for configuration-scope provenance, upgrades a format-0 repository to format 1, enables `extensions.worktreeConfig`, and assigns the current worktree an absolute `core.hooksPath` at `$GIT_DIR/dsh-hooks`. Before first enabling the repository-wide extension, it inspects the dormant `config.worktree` file for the main worktree and every registered linked worktree, then refuses any settings whose activation would change the current or a sibling worktree. The main worktree receives `$GIT_COMMON_DIR/dsh-hooks`; each linked worktree receives the corresponding directory under `$GIT_COMMON_DIR/worktrees/<id>`. A repository-scoped lock serializes configuration migration and hook writes, including repeated concurrent installs. Each lock records a process ID and random ownership token; release verifies the same file identity and exact record. A dead or invalid lock is never broken automatically, so the diagnostic requires the contributor to confirm no installer is running and remove the lock manually.
The installer recognizes its hook directory with a private ownership marker and updates it idempotently. It inspects the effective scope, origin, and value of `core.hooksPath`, then refuses an unowned directory, every command-scoped path, and every non-owned worktree-scoped path, including values loaded through `config.worktree` includes. It follows conditional includes with Git's parser and refuses a command- or worktree-scoped include whose target provides, or cannot safely be shown not to provide, a hook path; an inactive condition therefore cannot later hide a user-owned path behind the installer's direct value. The same risk in an inherited system, global, or common-repository include requires `DSH_LEFTHOOK_ALLOW_HOOKS_PATH_OVERRIDE=1`, which explicitly opts only the current worktree into Lefthook while other worktrees retain the inherited path. Unrelated conditional includes remain valid. Command-scoped Git configuration is removed from the Lefthook subprocess environment after validation. This opt-in does not attempt to chain arbitrary hook managers.
Enabling worktree config removes the standard redundant `core.bare=false` value from the common config because false remains Git's default; an explicit `core.worktree` or `core.bare=true`, whether direct or loaded through an active common-config include, is refused for manual migration. Before enabling the extension, the installer follows common-config conditional includes and refuses a target that provides, or cannot safely be shown not to provide, either migration-sensitive key; unrelated conditional includes remain valid. If Lefthook fails during a first install, the installer removes the new worktree override so the prior inherited or common hooks remain active. Legacy files in `$GIT_COMMON_DIR/hooks` are never removed or rewritten by the worktree-local installer.
[`install-lefthook.spec.ts`](../../../../scripts/install-lefthook.spec.ts) exercises main and linked worktrees, removal independence, repeated and concurrent installs, stale and replaced lock ownership, the Git version boundary, migration keys loaded through active and conditional common-config includes, scoped custom-path refusal and opt-in, active and inactive worktree includes, inherited conditional paths, command-environment isolation, legacy common-hook preservation, and failed-install rollback.
[`install-lefthook.spec.ts`](../../../../scripts/install-lefthook.spec.ts) exercises main and linked worktrees, removal independence, repeated and concurrent installs, stale and replaced lock ownership, the Git version boundary, dormant sibling-config refusal, migration keys loaded through active and conditional common-config includes, scoped custom-path refusal and opt-in, active and inactive worktree includes, inherited conditional paths, command-environment isolation, legacy common-hook preservation, and failed-install rollback.
## Alternatives considered