Skip to content

fix(windows): avoid subprocess git calls that hang in pip .exe wrappers#1

Open
hamb3r wants to merge 3 commits intoMikeRecognex:mainfrom
hamb3r:fix/windows-subprocess-git-hang
Open

fix(windows): avoid subprocess git calls that hang in pip .exe wrappers#1
hamb3r wants to merge 3 commits intoMikeRecognex:mainfrom
hamb3r:fix/windows-subprocess-git-hang

Conversation

@hamb3r
Copy link

@hamb3r hamb3r commented Feb 26, 2026

Problem

On Windows, pip-installed console_scripts (.exe wrappers) inherit a reduced PATH that often does not include git. Every call to subprocess.run(["git", ...]) either raises FileNotFoundError or hangs until the 10-second timeout.

Since _ensure_index() and _maybe_incremental_update() make multiple git subprocess calls (rev-parse, diff, ls-files), a single tool invocation takes 20+ seconds before returning a result.

Reproduction

  1. pip install "mcp-codebase-index[mcp]" on Windows 10/11
  2. Configure as MCP server in Claude Code
  3. Call any tool → hangs for 20+ seconds on first call

Root cause

is_git_repo() calls git rev-parse --is-inside-work-tree and get_head_commit() calls git rev-parse HEAD via subprocess.run. Inside the .exe wrapper process, git is not on PATH, so these calls hang until timeout.

Solution

Two source files changed, zero new dependencies.

git_tracker.py

Function Before After
_resolve_git_dir() (new) Unified git-dir resolver: walks up parents, supports .git dir (regular repos) and .git file with gitdir: pointer (worktrees/submodules)
is_git_repo() subprocess.run(["git", "rev-parse", ...]) Delegates to _resolve_git_dir() — pure filesystem, no subprocess
get_head_commit() subprocess.run(["git", "rev-parse", "HEAD"]) Uses _resolve_git_dir(), then reads .git/HEAD directly. Supports symbolic refs, detached HEAD, packed-refs, SHA-1 (40 hex) and SHA-256 (64 hex)
_find_git() (new) Resolves git binary path once at import time via shutil.which() + Windows fallback paths
get_changed_files() ["git", "diff", ...] [_GIT_CMD, "diff", ...] using resolved path; new skip_committed kwarg to skip since_ref..HEAD diff while still checking working tree

server.py

Function Change
_maybe_incremental_update() Uses skip_committed=True when HEAD hasn't moved instead of early return — working tree changes (unstaged, staged, untracked) are always detected

test_git_tracker.py

Tests fully rewritten for the new filesystem-based implementations:

Test class Tests Coverage
TestResolveGitDir 7 .git dir, parent walk, .git file with absolute/relative gitdir:, non-git, invalid .git file, missing target dir
TestIsGitRepo 5 Regular repo, nested path, worktree .git file, non-git, empty path
TestGetHeadCommit 8 Symbolic ref, detached HEAD (SHA-1), detached HEAD (SHA-256), packed-refs, worktree .git file, subdirectory root, no .git dir, missing ref, invalid hash
TestCommitHashRe 5 SHA-1, SHA-256, short hash, non-hex, wrong length
TestFindGit 3 shutil.which success, fallback, bare "git"
TestGetChangedFiles 10 None ref, M/A/D parsing, rename, overlap resolution, untracked, skip_committed skips committed diff, skip_committed still checks working tree, FileNotFoundError
TestGitChangeSet 4 Empty, modified, added, deleted
Total 43
$ python -m pytest tests/test_git_tracker.py -v
======================== 43 passed in 0.10s ========================

Cross-platform safety

  • Linux/macOS/Docker: shutil.which("git") resolves immediately; Windows-specific fallback paths are skipped (files don't exist). _resolve_git_dir() filesystem walk is the same logic on all platforms. No behavior change.
  • Windows: filesystem-based is_git_repo() and get_head_commit() eliminate the subprocess dependency on hot path. _find_git() resolves git for diff operations.
  • Worktrees/Submodules: _resolve_git_dir() follows .git files with gitdir: pointers (both absolute and relative paths).

Result

Metric Before After
First tool call (with cache) ~21s 0.2s
First tool call (cold, no cache) ~21s ~1s (indexing only)

Tested on Windows 10 Pro, Python 3.14, pip editable install, Claude Code MCP integration.

@hamb3r hamb3r force-pushed the fix/windows-subprocess-git-hang branch from 046c498 to 80e0fc7 Compare February 26, 2026 22:47
On Windows, pip-installed .exe console_scripts inherit a reduced PATH
that does not include git. This causes subprocess.run(["git", ...])
calls to hang indefinitely.

Changes:
- Add _resolve_git_dir() that walks up parents and supports both .git
  directory (regular repos) and .git file (worktrees/submodules with
  gitdir: pointer)
- Rewrite is_git_repo() to use _resolve_git_dir() instead of calling
  git rev-parse --is-inside-work-tree via subprocess
- Rewrite get_head_commit() to use _resolve_git_dir() and read
  .git/HEAD directly; supports symbolic refs, detached HEAD, packed-refs,
  and both SHA-1 (40 hex) and SHA-256 (64 hex) object IDs
- Add _find_git() helper that resolves the git binary path once at
  import time via shutil.which() with Windows fallback paths
- Use resolved _GIT_CMD for all remaining subprocess-based git calls
  (diff, ls-files) instead of bare "git"
- Add skip_committed parameter to get_changed_files() so callers can
  skip the expensive since_ref..HEAD diff when HEAD hasn't moved while
  still checking the working tree (unstaged, staged, untracked)
- Update _maybe_incremental_update() to use skip_committed instead of
  early return, so working tree changes are always detected
- Rewrite tests for filesystem-based behavior with 43 test cases
  covering worktrees, submodules, subdirectory roots, SHA-256, and
  skip_committed
@hamb3r hamb3r force-pushed the fix/windows-subprocess-git-hang branch from 80e0fc7 to 418b095 Compare February 26, 2026 22:52
@hamb3r
Copy link
Author

hamb3r commented Feb 28, 2026

cc @MikeRecognex

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.

1 participant