Skip to content

Use native git CLI for worktree status on hook hot paths (45.9s -> 193ms on large worktrees)#1643

Open
sam-fakhreddine wants to merge 4 commits into
entireio:mainfrom
sam-fakhreddine:fix/native-git-status-hot-paths
Open

Use native git CLI for worktree status on hook hot paths (45.9s -> 193ms on large worktrees)#1643
sam-fakhreddine wants to merge 4 commits into
entireio:mainfrom
sam-fakhreddine:fix/native-git-status-hot-paths

Conversation

@sam-fakhreddine

Copy link
Copy Markdown

Summary

Fixes #1642 — on worktrees with large gitignored trees, the Claude Code user-prompt-submit hook takes 40–55s per prompt and every turn-end checkpoint pays the same cost again. Root cause: three hot paths still used go-git's worktree.Status(), which stats and hashes every file on disk including gitignored directories. Several sibling call sites had already been converted to the native git CLI for exactly this reason (see the existing comments in checkpoint/ephemeral.go and getStagedFiles in strategy/manual_commit_hooks.go); this PR finishes the job.

Changes

  • strategy/common.go: new exported GitCLIStatus(ctx) helper — runs git status --porcelain=v1 -z -uall --no-renames at the worktree root and parses the output into a go-git-compatible git.Status map. Porcelain v1 status letters are byte-identical to go-git's StatusCode constants, so this is a drop-in replacement for worktree.Status().
  • state.go: getUntrackedFilesForState (→ capture_pre_prompt_state) and DetectFileChanges (→ turn-end checkpointing) now use GitCLIStatus.
  • strategy/manual_commit_hooks.go: calculatePromptAttributionAtStart (→ init_session) now uses GitCLIStatus.
  • state_test.go: the hand-rolled test repos gain a .git/refs/ directory — the native git CLI requires it to recognize a repository (go-git did not).

Measurements

61GB / ~686k-file worktree (~34GB in gitignored SwiftPM .build/ dirs), measured with entire doctor trace + ENTIRE_LOG_LEVEL=DEBUG:

before after
user-prompt-submit total 45,954ms 193ms
capture_pre_prompt_state 24,397ms 102ms
init_session 21,540ms 79ms

Turn-end shadow-branch commits went from 20–50s to sub-second on the same repo.

Testing

  • mise run lint — clean
  • mise run test — 989 passed; the single failure (TestRunUninstall_Force_NothingInstalled) is pre-existing and environment-dependent — it fails identically on pristine main on any machine where the Entire CLI is installed, because the uninstall path finds a real installation.
  • Dogfooded: this patched build has been my daily entire binary on the affected repo since building it.

Notes

go-git's worktree.Status() stats and hashes every file on disk, including
gitignored directories. On repos with large ignored trees (node_modules/,
SwiftPM .build/, ...) this costs tens of seconds per hook invocation:
user-prompt-submit measured at 45954ms on a 61GB/686k-file worktree
(capture_pre_prompt_state 24397ms + init_session 21540ms), and every
turn-end paid the same walk again in DetectFileChanges before the shadow
branch commit.

Add strategy.GitCLIStatus(), which runs
'git status --porcelain=v1 -z -uall --no-renames' and parses the output
into a go-git-compatible git.Status map (porcelain v1 status letters are
byte-identical to go-git StatusCode values), and use it in the three
remaining hot call sites:

- getUntrackedFilesForState (capture_pre_prompt_state)
- calculatePromptAttributionAtStart (init_session)
- DetectFileChanges (turn-end checkpoint)

Same repo, same trace tooling after the change: user-prompt-submit 193ms
(capture 102ms, init_session 79ms) — ~238x faster.

The hand-rolled test repos in state_test.go gain a .git/refs/ directory,
required by the native CLI to recognize a repository.

Fixes entireio#1642
@sam-fakhreddine sam-fakhreddine requested a review from a team as a code owner July 5, 2026 23:55
Copilot AI review requested due to automatic review settings July 5, 2026 23:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses severe performance regressions on large worktrees by replacing remaining hot-path uses of go-git’s worktree.Status() with a native git status --porcelain implementation, avoiding expensive scans/hashing across large gitignored directories during hooks and checkpointing.

Changes:

  • Adds strategy.GitCLIStatus(ctx) to obtain a go-git-compatible git.Status map via native git status --porcelain=v1 -z -uall --no-renames.
  • Switches DetectFileChanges, getUntrackedFilesForState, and prompt attribution startup logic to use GitCLIStatus.
  • Updates unit test scaffolding for minimal repos to include .git/refs/, which native git requires for repo recognition.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
cmd/entire/cli/strategy/common.go Introduces GitCLIStatus helper to replace go-git status calls with native git CLI parsing.
cmd/entire/cli/state.go Uses strategy.GitCLIStatus in state capture and change detection hot paths.
cmd/entire/cli/strategy/manual_commit_hooks.go Uses GitCLIStatus in calculatePromptAttributionAtStart to avoid expensive status walks.
cmd/entire/cli/state_test.go Adjusts hand-rolled repo fixtures to be recognized by native git (adds .git/refs).

Comment thread cmd/entire/cli/state.go Outdated
Comment thread cmd/entire/cli/strategy/common.go
sam-fakhreddine and others added 2 commits July 5, 2026 18:09
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Match the pattern in checkpoint/git_common_dir.go: an ExitError with
empty stderr previously produced the awkward message
"git status failed: : exit status 128".
@sam-fakhreddine

Copy link
Copy Markdown
Author

Addressed the error-formatting finding in ae2b4c4: stderr detail is now only appended when non-empty, matching the existing pattern in checkpoint/git_common_dir.go. Lint and the strategy test suite pass.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread cmd/entire/cli/state_test.go Outdated
Comment on lines 68 to 78
// Initialize git repo. refs/ is required alongside objects/ and HEAD for
// the native git CLI (used by GitCLIStatus) to recognize the directory.
if err := os.MkdirAll(".git/objects", 0o755); err != nil {
t.Fatalf("Failed to create .git: %v", err)
}
if err := os.MkdirAll(".git/refs", 0o755); err != nil {
t.Fatalf("Failed to create .git/refs: %v", err)
}
if err := os.WriteFile(".git/HEAD", []byte("ref: refs/heads/main\n"), 0o644); err != nil {
t.Fatalf("Failed to create HEAD: %v", err)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6b86259 — both hand-rolled setups now use testutil.InitRepo, matching the convention used elsewhere in this file.

Comment thread cmd/entire/cli/state_test.go Outdated
Comment on lines 354 to 364
// Initialize git repo. refs/ is required alongside objects/ and HEAD for
// the native git CLI (used by GitCLIStatus) to recognize the directory.
if err := os.MkdirAll(".git/objects", 0o755); err != nil {
t.Fatalf("Failed to create .git: %v", err)
}
if err := os.MkdirAll(".git/refs", 0o755); err != nil {
t.Fatalf("Failed to create .git/refs: %v", err)
}
if err := os.WriteFile(".git/HEAD", []byte("ref: refs/heads/main\n"), 0o644); err != nil {
t.Fatalf("Failed to create HEAD: %v", err)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6b86259setupTestRepoWithTranscript now uses testutil.InitRepo.

…e tests

The manually constructed .git layout (objects/, refs/, HEAD) is brittle
across git versions now that production code shells out to the git CLI
for rev-parse and status. testutil.InitRepo creates a real repository
layout and matches the convention used elsewhere in this file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Prompt hooks take 40-55s on large worktrees: go-git worktree.Status() walks gitignored directories (git status: 0.02s; patched locally to 193ms)

2 participants