Expose the git work tree to hooks running outside the git root - #2579
Conversation
Committing from a linked worktree makes Git export an absolute `GIT_DIR` without a `GIT_WORK_TREE`, which tells Git to treat the current directory as the work tree root. Workspace hooks run in their own project directory, so a Git command inside such a hook resolved the repository root to that subdirectory and rewrote the index as if the sub-project were the whole repository. Pass the derived work tree to hook subprocesses that start outside the git root. Hooks that run at the git root already resolve the root correctly, so they keep inheriting the environment Git gave us.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2579 +/- ##
=======================================
Coverage 93.78% 93.78%
=======================================
Files 140 140
Lines 29036 29058 +22
=======================================
+ Hits 27230 27251 +21
- Misses 1806 1807 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5e1271ddec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`Julia::run` builds its own command instead of going through `ExecutionEnvironment::command`, so Julia hooks in a workspace kept inheriting `GIT_DIR` without the derived `GIT_WORK_TREE` and could still rewrite the index from a linked worktree. Move the environment handling into `git::apply_hook_work_tree` and call it from both runners. The other backends that build their own commands are unaffected: `docker` and `docker_image` only forward the variables they pass with `-e`, `pygrep` runs a script prek ships itself, and `fail` starts no process at all.
📦 Cargo Bloat ComparisonBinary size change: +0.00% (30.3 MiB → 30.3 MiB) Expand for cargo-bloat outputHead Branch ResultsBase Branch Results |
⚡️ Hyperfine BenchmarksSummary: 0 regressions, 0 improvements above the 10% threshold. Environment
CLI CommandsBenchmarking basic commands in the main repo:
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base --version |
2.2 ± 0.1 | 2.1 | 2.6 | 1.02 ± 0.15 |
prek-head --version |
2.2 ± 0.3 | 2.1 | 5.0 | 1.00 |
prek list
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base list |
9.3 ± 0.3 | 9.0 | 11.6 | 1.00 |
prek-head list |
9.3 ± 0.2 | 9.0 | 10.0 | 1.00 ± 0.04 |
prek validate-config .pre-commit-config.yaml
⏭️ Skipped: .pre-commit-config.yaml not found
prek sample-config
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base sample-config |
2.6 ± 0.3 | 2.4 | 4.9 | 1.01 ± 0.14 |
prek-head sample-config |
2.5 ± 0.0 | 2.5 | 2.7 | 1.00 |
Cold vs Warm Runs
Comparing first run (cold) vs subsequent runs (warm cache):
prek run --all-files (cold - no cache)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --all-files |
40.2 ± 0.9 | 38.7 | 41.2 | 1.00 |
prek-head run --all-files |
40.4 ± 1.3 | 37.7 | 42.2 | 1.01 ± 0.04 |
prek run --all-files (warm - with cache)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --all-files |
41.0 ± 1.2 | 38.8 | 42.8 | 1.02 ± 0.04 |
prek-head run --all-files |
40.1 ± 1.0 | 37.8 | 41.8 | 1.00 |
Full Hook Suite
Running the builtin hook suite on the benchmark workspace:
prek run --all-files (full builtin hook suite)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --all-files |
40.1 ± 1.6 | 36.7 | 44.4 | 1.00 ± 0.05 |
prek-head run --all-files |
39.9 ± 1.0 | 37.7 | 42.5 | 1.00 |
Individual Hook Performance
Benchmarking each hook individually on the test repo:
prek run trailing-whitespace --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run trailing-whitespace --all-files |
13.5 ± 0.4 | 12.7 | 14.4 | 1.00 |
prek-head run trailing-whitespace --all-files |
13.7 ± 0.6 | 12.8 | 15.4 | 1.02 ± 0.05 |
prek run end-of-file-fixer --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run end-of-file-fixer --all-files |
11.5 ± 0.5 | 10.5 | 13.3 | 1.00 |
prek-head run end-of-file-fixer --all-files |
11.7 ± 0.5 | 10.8 | 12.7 | 1.02 ± 0.07 |
prek run check-json --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-json --all-files |
8.0 ± 0.3 | 7.6 | 8.5 | 1.00 |
prek-head run check-json --all-files |
8.7 ± 1.2 | 7.5 | 13.6 | 1.08 ± 0.15 |
prek run check-yaml --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-yaml --all-files |
8.2 ± 0.2 | 7.8 | 8.7 | 1.00 |
prek-head run check-yaml --all-files |
8.6 ± 0.2 | 8.1 | 8.9 | 1.05 ± 0.04 |
prek run check-toml --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-toml --all-files |
8.5 ± 0.3 | 8.0 | 9.4 | 1.00 |
prek-head run check-toml --all-files |
8.5 ± 0.4 | 8.0 | 9.7 | 1.00 ± 0.06 |
prek run check-xml --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-xml --all-files |
8.3 ± 0.4 | 7.9 | 9.6 | 1.02 ± 0.08 |
prek-head run check-xml --all-files |
8.1 ± 0.6 | 7.4 | 9.7 | 1.00 |
prek run detect-private-key --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run detect-private-key --all-files |
11.8 ± 0.6 | 10.8 | 13.7 | 1.02 ± 0.07 |
prek-head run detect-private-key --all-files |
11.6 ± 0.6 | 10.7 | 12.6 | 1.00 |
prek run fix-byte-order-marker --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run fix-byte-order-marker --all-files |
13.5 ± 0.9 | 12.5 | 15.3 | 1.00 |
prek-head run fix-byte-order-marker --all-files |
13.9 ± 1.0 | 12.4 | 15.9 | 1.03 ± 0.10 |
Installation Performance
Benchmarking hook installation (fast path hooks skip Python setup):
prek install-hooks (cold - no cache)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base install-hooks |
4.6 ± 0.1 | 4.5 | 4.7 | 1.01 ± 0.03 |
prek-head install-hooks |
4.5 ± 0.1 | 4.4 | 4.6 | 1.00 |
prek install-hooks (warm - with cache)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base install-hooks |
4.5 ± 0.0 | 4.5 | 4.5 | 1.00 |
prek-head install-hooks |
4.6 ± 0.1 | 4.4 | 4.7 | 1.00 ± 0.02 |
File Filtering/Scoping Performance
Testing different file selection modes:
prek run (staged files only)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run |
24.1 ± 0.5 | 23.3 | 25.1 | 1.00 |
prek-head run |
24.7 ± 0.5 | 23.8 | 25.7 | 1.03 ± 0.03 |
prek run --files '*.json' (specific file type)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --files '*.json' |
5.4 ± 0.2 | 5.2 | 5.9 | 1.00 |
prek-head run --files '*.json' |
5.5 ± 0.1 | 5.3 | 5.6 | 1.02 ± 0.03 |
Workspace Discovery & Initialization
Benchmarking hook discovery and initialization overhead:
prek run --dry-run --all-files (measures init overhead)
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run --dry-run --all-files |
7.0 ± 0.2 | 6.8 | 7.4 | 1.02 ± 0.03 |
prek-head run --dry-run --all-files |
6.8 ± 0.1 | 6.7 | 7.0 | 1.00 |
Meta Hooks Performance
Benchmarking meta hooks separately:
prek run check-hooks-apply --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-hooks-apply --all-files |
8.2 ± 0.2 | 7.9 | 8.5 | 1.00 |
prek-head run check-hooks-apply --all-files |
8.3 ± 0.2 | 8.1 | 8.6 | 1.02 ± 0.03 |
prek run check-useless-excludes --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run check-useless-excludes --all-files |
8.3 ± 0.1 | 8.0 | 8.6 | 1.00 ± 0.02 |
prek-head run check-useless-excludes --all-files |
8.2 ± 0.1 | 8.0 | 8.5 | 1.00 |
prek run identity --all-files
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
prek-base run identity --all-files |
7.2 ± 0.1 | 7.0 | 7.4 | 1.00 ± 0.02 |
prek-head run identity --all-files |
7.1 ± 0.1 | 7.0 | 7.2 | 1.00 |
80c4e5a to
7c3cc5a
Compare
Documents the Git environment contract for hooks and how to isolate Git commands that operate on a separate repository. Stacked on #2579.
|
Thanks! |
Committing from a linked worktree (
git worktree add) makes Git export anabsolute
GIT_DIRto hooks without aGIT_WORK_TREE. Git then treats thecurrent directory as the work tree root. Committing from a main worktree
exports neither, so only linked worktrees are affected.
prek runs each workspace project's hooks with the cwd set to that project's
directory. In a linked worktree that made a hook's own Git command resolve the
repository root to the sub-project directory:
git add -uinside asub-project hook rewrote the whole index as if the sub-project were the
repository, staging every path outside it as deleted and giving the surviving
paths the sub-project's blobs.
prek already derives the correct work tree in
init_git_work_tree, but #2356scoped it to prek's own Git commands so hooks do not inherit an environment Git
did not expose. Keep that scoping, with one exception: hook subprocesses that
start outside the git root, which is exactly the sub-project case. Hooks
running at the git root are unaffected, so #2293 stays fixed.
Related: #2505 isolated the same environment leak for dependency installs, but
hook execution still inherits it.