Skip to content

fix(vcs): bound git subprocesses with deadlines and actionable timeout diagnostics - #86

Open
mvanhorn wants to merge 4 commits into
kunchenguid:mainfrom
mvanhorn:fix/64-git-command-timeouts
Open

fix(vcs): bound git subprocesses with deadlines and actionable timeout diagnostics#86
mvanhorn wants to merge 4 commits into
kunchenguid:mainfrom
mvanhorn:fix/64-git-command-timeouts

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Aug 13, 2026

Copy link
Copy Markdown

Intent

Bound stalled git commands in treehouse with actionable diagnostics: centralize subprocess construction in internal/git around exec.CommandContext, apply a conservative default deadline to every git command including the merge-ancestor path that bypassed runGit, surface DeadlineExceeded as a timeout-specific error naming the git subcommand and working directory with stale index-lock guidance, and keep IsHeadMergedIntoRef's exit-code-1 unmerged-head meaning intact. Re-pushing through no-mistakes so PR #86 carries the pipeline attestation the maintainer requires to merge.

What Changed

  • Centralized every git subprocess in internal/vcs/gitvcs on exec.CommandContext with a deadline, including the merge-ancestor path that previously bypassed runGit. Two budgets apply: a 2-minute standard budget for metadata commands and a 30-minute long budget for size- and network-bound commands (fetch, ls-remote, worktree add/remove, checkout, read-tree, clean), overridable via TREEHOUSE_GIT_TIMEOUT and TREEHOUSE_GIT_LONG_TIMEOUT (an unparseable or non-positive value warns and falls back to the default). A deadline hit now reports the git subcommand, the working directory, stale index-lock / credential-prompt / network guidance, and the env var that raises the budget; IsHeadMergedIntoRef's exit-code-1 unmerged-head meaning is preserved, and non-ExitError failures (including cmd.WaitDelay expiry from a background helper holding the output pipe) are wrapped with the same subcommand and directory context.
  • Added a PruneWorktreeAt operation to the vcs.Backend seam (git implementation plus a documented jj no-op) that prunes stale registrations and additionally unlocks the initializing lock git leaves on an interrupted worktree add, which plain git worktree prune skips. Only that lock reason is cleared, and only while the slot directory is gone, so a user-taken lock is untouched. get now routes its pre-add prune through it, and on a failed AddWorktree removes the partial slot directory it just created and re-prunes, so a timed-out creation no longer wedges the pool on that slot name; the cleanup is skipped when the slot directory already existed.
  • Documented the timeout budgets and their overrides in a new README "Git command timeouts" section, noted that jj subprocesses are not bounded yet, and expanded the self-healing get documentation in README and AGENTS.md. Test coverage added in internal/vcs/gitvcs/gitvcs_test.go and internal/pool/pool_test.go for budget selection, timeout error content, lock clearing, and slot reuse after a failed creation.

Risk Assessment

✅ Low: The follow-up commit resolves all three actionable round-1 findings with narrowly scoped, documented, and test-pinned changes - split budgets with env overrides, a pre-existence-guarded partial-slot cleanup, and context-carrying error wrapping - while preserving the exit-code-1 unmerged semantics and the origin-unreachable classification the surrounding code depends on.

Testing

Ran the targeted gitvcs and pool test packages for the timeout and partial-worktree-cleanup paths (20 tests, all passing) and then drove the built CLI through four real scenarios: a stalled git worktree add under a shortened long budget, recovery of the pool afterwards, a stalled git merge-base --is-ancestor compared side by side against a binary built from the base commit, and an unmerged pooled worktree. The timeout diagnostics appear verbatim to the end user with the subcommand, working directory, stale-index-lock guidance, and the right overridable env var for each budget; the base binary hung indefinitely on the merge-ancestor stall while the target binary exits promptly and fails closed. The pool-wedge reported in the previous round no longer reproduces: the interrupted creation's registration is cleared and the freed slot is reused. Scratch repos, shims, and the baseline binary were deleted and the working tree is clean.

Evidence: CLI transcript: stalled worktree add times out and the pool recovers

STEP 1 treehouse get --lease with a stalled checkout filter 🌳 Setting up worktree... failed to create worktree: git worktree add --detach .../1/repo main timed out in "/private/tmp/treehouse-e2e/run/repo"; check for a stale index lock (locate it with 'git rev-parse --git-path index.lock'), blocked credential prompts, or network connectivity. Raise TREEHOUSE_GIT_LONG_TIMEOUT if this repository legitimately needs longer --- exit code: 1 (elapsed 13s) STEP 2 git's own view of the pool after the timeout worktree /private/tmp/treehouse-e2e/run/repo HEAD 7f9063936b3ef76bbf862c24acf7dc4848e9cb5b branch refs/heads/main STEP 3 treehouse status 🌳 No worktrees in pool. STEP 4 the stall is over; treehouse get --lease again 🌳 Leased worktree at ~/.treehouse/repo-6a9505/1/repo. --- exit code: 0

==============================================================
STEP 1  treehouse get --lease with a stalled checkout filter
        (TREEHOUSE_GIT_LONG_TIMEOUT=10s instead of the 30m default)
==============================================================
🌳 Setting up worktree...
failed to create worktree: git worktree add --detach /tmp/treehouse-e2e/run/home/.treehouse/repo-6a9505/1/repo main timed out in "/private/tmp/treehouse-e2e/run/repo"; check for a stale index lock (locate it with 'git rev-parse --git-path index.lock'), blocked credential prompts, or network connectivity. Raise TREEHOUSE_GIT_LONG_TIMEOUT if this repository legitimately needs longer
--- exit code: 1  (elapsed 13s)

==============================================================
STEP 2  git's own view of the pool after the timeout
==============================================================
worktree /private/tmp/treehouse-e2e/run/repo
HEAD 7f9063936b3ef76bbf862c24acf7dc4848e9cb5b
branch refs/heads/main


==============================================================
STEP 3  treehouse status
==============================================================
🌳 No worktrees in pool.

==============================================================
STEP 4  the stall is over; treehouse get --lease again
==============================================================
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-6a9505/1/repo. Run 'treehouse return ~/.treehouse/repo-6a9505/1/repo' to release it.
/tmp/treehouse-e2e/run/home/.treehouse/repo-6a9505/1/repo
--- exit code: 0

==============================================================
STEP 5  the pool is healthy: status + a second slot
==============================================================
1     leased       ~/.treehouse/repo-6a9505/1/repo
                   git (54073), sleep (54074)
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-6a9505/2/repo. Run 'treehouse return ~/.treehouse/repo-6a9505/2/repo' to release it.
/tmp/treehouse-e2e/run/home/.treehouse/repo-6a9505/2/repo
--- exit code: 0
/private/tmp/treehouse-e2e/run/repo                                7f90639 [main]
/private/tmp/treehouse-e2e/run/home/.treehouse/repo-6a9505/1/repo  7f90639 (detached HEAD)
/private/tmp/treehouse-e2e/run/home/.treehouse/repo-6a9505/2/repo  7f90639 (detached HEAD)
Evidence: Before/after: stalled merge-ancestor probe (base 5aafb9e vs ee4a57f)

BEFORE base commit 5aafb9e (merge-ancestor used exec.Command, no context, no deadline) ----- BEFORE : treehouse prune --verbose with a stalled merge-ancestor probe STILL RUNNING after 30s -- killed (no deadline; the real stall is 600s) AFTER ee4a57f ----- AFTER : treehouse prune --verbose with a stalled merge-ancestor probe exited after 17s 🌳 Skipped 2 unsafe idle worktrees: cannot verify worktree: 1 cannot prove HEAD is merged into default branch ~/.treehouse/repo-96b6b9/1/repo detail: git merge-base --is-ancestor HEAD refs/heads/main timed out in "..."; check for a stale index lock (locate it with 'git rev-parse --git-path index.lock'), blocked credential prompts, or network connectivity. Raise TREEHOUSE_GIT_TIMEOUT if this repository legitimately needs longer

==============================================================
BEFORE  base commit 5aafb9e (merge-ancestor used exec.Command,
        no context, no deadline)
==============================================================
----- BEFORE : treehouse prune --verbose with a stalled merge-ancestor probe
STILL RUNNING after 30s -- killed (no deadline; the real stall is 600s)

==============================================================
AFTER   ee4a57f
==============================================================
----- AFTER  : treehouse prune --verbose with a stalled merge-ancestor probe
exited after 17s
🌳 No stale worktrees to prune.
🌳 Skipped 2 unsafe idle worktrees:
  cannot verify worktree:
  1     cannot prove HEAD is merged into default branch  ~/.treehouse/repo-96b6b9/1/repo
        detail: git merge-base --is-ancestor HEAD refs/heads/main timed out in "/tmp/treehouse-e2e/mb/home/.treehouse/repo-96b6b9/1/repo"; check for a stale index lock (locate it with 'git rev-parse --git-path index.lock'), blocked credential prompts, or network connectivity. Raise TREEHOUSE_GIT_TIMEOUT if this repository legitimately needs longer
  2     cannot prove HEAD is merged into default branch  ~/.treehouse/repo-96b6b9/2/repo
        detail: git merge-base --is-ancestor HEAD refs/heads/main timed out in "/tmp/treehouse-e2e/mb/home/.treehouse/repo-96b6b9/2/repo"; check for a stale index lock (locate it with 'git rev-parse --git-path index.lock'), blocked credential prompts, or network connectivity. Raise TREEHOUSE_GIT_TIMEOUT if this repository legitimately needs longer
Evidence: CLI transcript: merge-ancestor timeout through get and prune
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-96b6b9/1/repo. Run 'treehouse return ~/.treehouse/repo-96b6b9/1/repo' to release it.
🌳 Worktree returned to pool.
==============================================================
treehouse get with a stalled 'git merge-base --is-ancestor'
(TREEHOUSE_GIT_TIMEOUT=8s instead of the 2m metadata default)
==============================================================
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-96b6b9/2/repo. Run 'treehouse return ~/.treehouse/repo-96b6b9/2/repo' to release it.
/tmp/treehouse-e2e/mb/home/.treehouse/repo-96b6b9/2/repo
--- exit code: 0  (elapsed 8s)

==============================================================
the same stall surfaced as a diagnostic: treehouse prune -v
==============================================================
🌳 Worktree returned to pool.
🌳 Worktree returned to pool.
🌳 No stale worktrees to prune.
🌳 Skipped 2 unsafe idle worktrees:
  cannot verify worktree:
  1     cannot prove HEAD is merged into default branch  ~/.treehouse/repo-96b6b9/1/repo
        detail: git merge-base --is-ancestor HEAD refs/heads/main timed out in "/tmp/treehouse-e2e/mb/home/.treehouse/repo-96b6b9/1/repo"; check for a stale index lock (locate it with 'git rev-parse --git-path index.lock'), blocked credential prompts, or network connectivity. Raise TREEHOUSE_GIT_TIMEOUT if this repository legitimately needs longer
  2     cannot prove HEAD is merged into default branch  ~/.treehouse/repo-96b6b9/2/repo
        detail: git merge-base --is-ancestor HEAD refs/heads/main timed out in "/tmp/treehouse-e2e/mb/home/.treehouse/repo-96b6b9/2/repo"; check for a stale index lock (locate it with 'git rev-parse --git-path index.lock'), blocked credential prompts, or network connectivity. Raise TREEHOUSE_GIT_TIMEOUT if this repository legitimately needs longer
--- exit code: 0  (elapsed 17s)
Evidence: CLI transcript: exit-code-1 still means unmerged, slot preserved

unmerged HEAD in a pooled worktree (merge-base exit code 1) 🌳 No stale worktrees to prune. 🌳 Skipped 1 unsafe idle worktree: unmerged: 1 HEAD not merged into refs/heads/main ~/.treehouse/repo-62341d/1/repo --- prune exit: 0 --- treehouse get must NOT reuse the unmerged slot: 🌳 Leased worktree at ~/.treehouse/repo-62341d/2/repo.

🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-62341d/1/repo. Run 'treehouse return ~/.treehouse/repo-62341d/1/repo' to release it.
🌳 Worktree returned to pool.
==============================================================
unmerged HEAD in a pooled worktree (merge-base exit code 1)
==============================================================
🌳 No stale worktrees to prune.
🌳 Skipped 1 unsafe idle worktree:
  unmerged:
  1     HEAD not merged into refs/heads/main  ~/.treehouse/repo-62341d/1/repo
--- prune exit: 0

--- treehouse get must NOT reuse the unmerged slot:
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-62341d/2/repo. Run 'treehouse return ~/.treehouse/repo-62341d/2/repo' to release it.
/tmp/treehouse-e2e/um/home/.treehouse/repo-62341d/2/repo
/private/tmp/treehouse-e2e/um/repo                                59a609a [main]
/private/tmp/treehouse-e2e/um/home/.treehouse/repo-62341d/1/repo  caa72fa (detached HEAD)
/private/tmp/treehouse-e2e/um/home/.treehouse/repo-62341d/2/repo  59a609a (detached HEAD)
- Outcome: 🔧 1 issue found → auto-fixed ✅ across 2 runs (41m32s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 2 issues found → auto-fixed ✅
  • ⚠️ internal/git/git.go - merge conflict rebasing onto origin/main
  • ⚠️ internal/git/git_test.go - merge conflict rebasing onto origin/main

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Review** - 2 infos
  • ⚠️ internal/vcs/gitvcs/gitvcs.go:16 - The single 2-minute budget now also bounds the only genuinely unbounded git commands: fetch origin (gitvcs.go:205), ls-remote --symref (gitvcs.go:401), worktree add --detach (gitvcs.go:176) and read-tree --reset -u (gitvcs.go:271), with no config/env override. Concrete failing sequence: on a git-lfs or very large repo, treehouse get calls vcs.AddWorktree (internal/pool/pool.go:235); git worktree add runs the LFS smudge filter and is SIGKILLed at 120s after it has already created <pool>/<n>/<repo> and registered it in .git/worktrees. WriteState is never reached, so no state entry exists. On the next get, nextName (internal/pool/pool.go:547) derives the slot name only from state and returns the same n, PruneWorktrees cannot clear a registration whose directory still exists, and git worktree add fails with "already exists" - every subsequent get on that pool is wedged until a human deletes the directory. A slow fetch similarly turns get into a hard "fetch failed" where it previously succeeded slowly. Suggest either a longer (or overridable) budget for the network/checkout commands, or removing the partial wtPath when AddWorktree fails so a timeout is self-healing.
  • ⚠️ internal/vcs/gitvcs/gitvcs.go:589 - The non-ExitError fallback returns the bare err with no git subcommand and no working directory, and setting cmd.WaitDelay (gitvcs.go:596) makes exec.ErrWaitDelay newly reachable on this path. Failing sequence: git status --porcelain --untracked-files=all exits 0 in a repo where a descendant (credential-cache daemon, LFS/filter process, fsmonitor daemon) still holds the inherited stdout pipe; after 250ms Wait returns ErrWaitDelay, the already-copied output is discarded, and IsDirty fails with the opaque "exec: WaitDelay expired before I/O complete" - the user cannot tell which git command or which worktree produced it, which is exactly the diagnosability this change is meant to add. Wrap this return with fmt.Errorf("git %s in %q: %w", strings.Join(args, " "), dir, err); optionally treat ErrWaitDelay with a zero exit status as success so a lingering descendant does not convert a completed command into a failure.
  • ℹ️ internal/vcs/gitvcs/gitvcs.go:573 - runGitRaw has no callers left anywhere in the repo (its only caller, readTree, became readTreeContext and calls runGitRawContext directly), and its new doc comment claims it "routes [the ls-tree caller] through the same timeout budget" - a caller it no longer has. go vet will not flag it, so it will silently rot. Delete it, or drop the misleading comment if it is intentionally kept as an API entry point.
  • ℹ️ internal/vcs/jjvcs/jjvcs.go:551 - Noting the remaining boundary, not asking for it in this change: the jj backend still builds subprocesses with plain exec.Command and no deadline, and jj shells out to git for network work, so the same stall class remains reachable with vcs = "jj". The stated intent scopes this fix to the git backend, so this is deliberate residual scope rather than a gap in the fix.

🔧 Fix: separate long git budgets, self-heal partial worktrees, wrap exec errors
2 infos still open:

  • ℹ️ internal/vcs/gitvcs/gitvcs.go:24 - Tradeoff worth a conscious decision, not a blocker: fetch and ls-remote now sit on the 30-minute budget, but a blocked credential prompt - one of the three hang causes the timeout message itself names - is not a slow transfer; git blocks on /dev/tty indefinitely, so treehouse get stalls for a full 30 minutes before the actionable error appears (internal/pool/pool.go:125 returns "fetch failed"). If interactive credential entry during a pool fetch is not a supported flow, setting GIT_TERMINAL_PROMPT=0 on the command environment in gitCommandContext would make that class fail in seconds while genuinely slow transfers keep the long budget. Leaving it as-is is defensible since the command is still bounded and the override env var is documented.
  • ℹ️ internal/pool/pool.go:559 - Acknowledging the deliberate limits of the new self-heal, no action needed: cleanup runs only when the process reaches the error return and only when slotDirIsNew. Two paths still leave <pool>/<n> behind - a SIGINT/SIGKILL during git worktree add (no deferred cleanup runs), and a RemoveAll that fails because a descendant the context kill did not terminate (git-lfs filter-process, notably on Windows where open files block deletion) still holds files. Both are pre-existing rather than introduced, both now emit a warning naming the slot path, and the resulting git worktree add failure quotes the conflicting path, so recovery is discoverable. The slotDirIsNew guard correctly prioritizes never deleting pre-existing content over always self-healing.
🔧 **Test** - 1 issue found → auto-fixed ✅
  • 🚨 internal/pool/pool.go:559 - Timing out git worktree add leaves git's registration locked, and the new self-heal cannot clear it, so the pool is permanently wedged. Repro (full transcript in pool-wedged-after-worktree-add-timeout.txt): a repo with a stalling required smudge filter, TREEHOUSE_GIT_LONG_TIMEOUT=10s treehouse get --lease -> the expected actionable timeout error; git worktree list then shows the slot registered and locked, because git locks a worktree while checking it out and only unlocks on success. cleanupPartialWorktree removes the slot directory and calls vcs.PruneWorktrees, but git worktree prune skips locked entries, so nextName hands out the same slot name and every subsequent treehouse get dies with "is a missing but locked worktree" even after the stall is gone. treehouse status reports an empty pool, treehouse prune --yes and treehouse prune --prune-orphans --yes both report nothing to prune; only raw git worktree unlock + git worktree prune restores the pool. This is the exact case AGENTS.md says the self-heal covers ("A failed vcs.AddWorktree ... (timeout, interrupted checkout, full disk)"), and the new timeout makes it a routine outcome rather than a rare interrupt. Deciding the fix (unlock before pruning, or route cleanup through git worktree remove --force) is a product change outside this test phase.
  • go test ./internal/vcs/gitvcs/ -run 'TestRunGit|TestGitCommandTimeoutFor|TestGitTimeoutError|TestIsHeadMergedIntoRef|TestIsHeadContentMergedIntoRefContextReportsTimeout' -v (20 tests, all pass)
  • go test ./internal/pool/ -run 'TestCleanupPartialWorktreeRemovesSlotAndRegistration|TestAcquireReusesSlotNameAfterFailedWorktreeCreation|TestAcquireKeepsPreexistingSlotContentWhenCreationFails' -v (3 tests, all pass)
  • Manual before/after: built the base commit (5aafb9e) via git archive and the target commit, ran treehouse get --lease in a repo whose required smudge filter sleeps forever; base hung until an external 40s cap killed it, target failed at TREEHOUSE_GIT_LONG_TIMEOUT=10s with the actionable diagnostic
  • Manual metadata-budget check: git config core.fsmonitor <hanging hook> then TREEHOUSE_GIT_TIMEOUT=5s treehouse prune -v — bounded at 5s, classified "cannot verify", detail names git status --porcelain --untracked-files=all, the worktree dir, and TREEHOUSE_GIT_TIMEOUT
  • Manual acquire check: TREEHOUSE_GIT_TIMEOUT=5s TREEHOUSE_GIT_LONG_TIMEOUT=20s treehouse get --lease with a stalled fsmonitor — unverifiable slot skipped (fail closed), whole command bounded
  • Manual override validation: TREEHOUSE_GIT_TIMEOUT=banana treehouse status warns once and falls back to 2m0s
  • Manual unmerged-head check: worktree HEAD carrying a commit not on main, treehouse prune classifies it unmerged (merge-base exit 1 semantics intact)
  • Manual defect repro: TREEHOUSE_GIT_LONG_TIMEOUT=10s treehouse get --lease -> git worktree list (locked) -> repeated treehouse get --lease, treehouse status, treehouse prune --yes, treehouse prune --prune-orphans --yes, then git worktree unlock + git worktree prune
  • grep -n 'CombinedOutput()|.Output()|.Run()|.Start()' internal/vcs/gitvcs/gitvcs.go — all three execution sites go through gitCommandContext

🔧 Fix: clear git's initializing lock when worktree creation fails
✅ Re-checked - no issues remain.

  • go build -o /tmp/treehouse-e2e/treehouse .
  • go test ./internal/vcs/gitvcs/ -run 'Timeout|Git|PruneWorktreeAt|Merged' -v (16 tests, all pass)
  • go test ./internal/pool/ -run 'PartialWorktree|FailedWorktreeCreation|PreexistingSlot|InitializingLock' -v (4 tests, all pass)
  • Manual E2E: repo with a hanging required smudge filter, TREEHOUSE_GIT_LONG_TIMEOUT=10s treehouse get --lease -> actionable timeout in 13s
  • Manual E2E: post-timeout recovery - git worktree list --porcelain, treehouse status, then treehouse get --lease reuses slot 1 and creates slot 2 (round-1 wedge regression check)
  • Manual E2E: treehouse return + re-get with orphaned filter processes still alive (lingering processes terminated, slot reused)
  • Manual E2E: git shim stalling only on merge-base --is-ancestor, TREEHOUSE_GIT_TIMEOUT=8s treehouse get --lease (fails closed, skips the slot) and treehouse prune --verbose (surfaces the timeout detail)
  • Manual E2E before/after: same stalled merge-ancestor probe against a binary built from base commit 5aafb9e (still running after 30s, killed) vs. ee4a57f (exits in 17s with the diagnostic)
  • Manual E2E: unmerged commit in a pooled worktree -> treehouse prune --verbose classifies it unmerged, treehouse get refuses to reuse it
  • grep -rn 'exec\.Command' internal/vcs/gitvcs/gitvcs.go internal/vcs/vcs.go internal/pool cmd (single exec.CommandContext call site)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks for this. internal/git now uses CommandContext with a 2-minute deadline and an actionable timeout error, and merge-base exit 1 is still unmerged rather than a hang. Windows test (windows-latest) is red (macos was cancelled). Please push a fix or a rerun so we can continue.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: no new author push. Windows test (windows-latest) is still red and the branch is dirty vs main. Not merging.

@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported fallback deadline and linked-worktree lock guidance issues are fixed in the current code.

Reviews (3): Last reviewed commit: "no-mistakes(document): update VCS seam c..." | Re-trigger Greptile

Comment thread internal/git/git.go Outdated
Comment thread internal/git/git.go Outdated
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: no new author push (still 89b18de). Greptile is 4/5 and blocking.

VISION (per rule):

  • Isolation and reuse: aligns. Timeouts do not recycle or reassign worktrees.
  • Safe lifecycle operations: not aligned yet. IsHeadMergedIntoRef still drops into isHeadContentMergedIntoRef with unbounded exec.Command on the ordinary unmerged exit, so prune/destroy merge checks can still hang past the two-minute bound this PR is meant to add.
  • Ownership and interface: mixed. Timeout errors name the command and cwd, but the .git/index.lock hint is wrong for linked worktrees (lock lives in the main repo's worktree metadata).
  • Scope and evaluation: aligns as a bounded git-adapter fix, not a workflow manager.

Please put the merge-content fallback under the same CommandContext deadline and fix the linked-worktree lock guidance. Not merging. Waiting on author.

@mvanhorn

Copy link
Copy Markdown
Author

Pushed 0fb398c. The remaining Greptile blocker was that the merge-base and read-tree helpers still used exec.Command directly, so a stalled git there could not be cancelled and did not produce the timeout diagnostic the rest of the path does. They now take a context and go through gitCommandContext, and a DeadlineExceeded surfaces as gitTimeoutError like the other calls.

go build ./... clean and go test ./internal/git/... passes locally.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks for 0fb398c. I inspected the new commit: isHeadContentMergedIntoRef and readTree now take a context and go through gitCommandContext, so a stall on that merge-content fallback is a timeout rather than an unbounded hang. The index-lock hint now points at git rev-parse --git-path index.lock. Greptile is 5/5. I approved fork CI (32483209093).

Remaining blocker: PR must be raised via no-mistakes failed because this body has no ## Pipeline / git push no-mistakes signature. Please re-push through https://github.com/kunchenguid/no-mistakes so the body gets the attestation. Not merging until that is green.

@mvanhorn
mvanhorn force-pushed the fix/64-git-command-timeouts branch from 0fb398c to 095ea9d Compare August 24, 2026 10:22
@mvanhorn mvanhorn changed the title fix: bound stalled git commands with actionable diagnostics fix(vcs): bound git subprocesses with deadlines and actionable timeout diagnostics Aug 24, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks for the re-raise at 095ea9d2. I inspected the timeout path before re-approving fork CI.

Inspected:

  • Every git subprocess goes through gitCommandContext / CommandContext.
  • IsHeadMergedIntoRef and the content fallback share that deadline; exit code 1 stays unmerged.
  • PruneWorktreeAt only unlocks git's initializing lock, and only when the slot directory is gone.
  • acquire cleanup RemoveAll only when slotDirIsNew.

VISION (per rule):

  • Isolation and reuse: aligns. Timeouts fail closed and do not recycle a dirty or in-use worktree.
  • Safe lifecycle operations: aligns. Unverifiable slots are skipped. Partial-slot cleanup is gated on a new directory, and a user-taken worktree lock is not cleared.
  • Ownership and interface: aligns. Timeout errors name the subcommand, cwd, and override env var. No PID/lease inference change.
  • Scope and evaluation: aligns as a bounded git-adapter fix. jj subprocesses remain unbounded; that is documented residual scope.

Greptile is 5/5 on this SHA. I approved CI 32716452288 and Require no-mistakes 32716452260 / 32716551861. Ubuntu/macOS/check are green; Windows is still running.

Require no-mistakes is red: "This PR was not raised through no-mistakes." The body has a ## Pipeline section, but there is no HMAC/signature comment. I will not forge a marker. Please git push no-mistakes again so this head gets a valid signature. This is waiting on you, not a captain hold. Not merging until that check is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants