Skip to content

wt step prune: Windows .git/config Permission-denied race (parallel integration checks vs background removals) #2801

@max-sixty

Description

@max-sixty

Summary

On Windows, wt step prune can fail with unable to access '.git/config': Permission denied because prune runs branch-integration checks concurrently with background worktree removals, and Windows has no shared-read locking on .git/config.

Observed on PR #2788 CI (test (windows)): integration_tests::step_prune::test_prune_locally_merged_when_upstream_diverged panicked prune should succeed, with stderr:

✗ checking branch integration
  warning: unable to access '.git/config': Permission denied
  fatal: unknown error occurred while reading the configuration files
◎ Removing feature-prune-local worktree & branch in background

Linux/macOS pass the same test; main's Windows CI is green.

Root cause

src/commands/step/prune.rs (~line 512) spawns a thread running integration_reason over a rayon into_par_iter() while the main loop concurrently spawns background worktree removals (handlers.rsexecute_instant_removal_or_fallback: rename + git worktree prune + detached rm -rf). The integration thread reads .git/config; the background removal writes git metadata/config. On Windows this read/write overlap intermittently yields Permission denied.

This race is inherent to prune's parallel design and predates PR #2788. PR #2788's dbcc73ade (revalidate_worktree_before_removal) added two more .git/config-opening subprocesses (git status --porcelain, git rev-parse HEAD) into the main-thread removal path, widening the contention window enough to surface it.

Why it wasn't "fixed" in #2788

Both checks in revalidate_worktree_before_removal are load-bearing data-safety guards and must not be dropped to dodge the race:

  • ensure_clean is the only dirty-work guard on the fast removal path — execute_instant_removal_or_fallback does rename + prune + rm -rf, which (unlike non-force git worktree remove) never refuses a dirty worktree.
  • The HEAD-moved check is the only guard against a post-snapshot commit being destroyed by the branch -D that runs off the stale pre_computed_integration.

So the fix belongs at the source.

Proposed fix

Make prune not run integration checks concurrently with background removals — e.g. join/collect the integration-check thread before starting any removal, or serialize the two phases. (Code path: src/commands/step/prune.rs around the std::thread::spawn + into_par_iter at ~512 and the rx consumption loop that drives removals.)

A TODO(prune-windows-git-race) is left on revalidate_worktree_before_removal in src/output/handlers.rs pointing here.

Ref PR #2788.

This was written by Claude Code on behalf of Maximilian Roos

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions