Keep test git from following a hook's GIT_DIR into this repo - #133
Merged
Merged
Conversation
Git hooks, `git rebase -x` and `git bisect run` export GIT_DIR and GIT_INDEX_FILE, and in a linked worktree those are absolute paths to the real repository. Several tests run `git init`, `git add --all` and `git config` in temp dirs with only `current_dir` set, so under those variables they act on the enclosing repo instead. Committing from a worktree ran the pre-commit hook's `cargo test`, which flipped the shared .git/config to `core.bare = true` (breaking the primary clone), replaced the worktree's index with test fixtures, and, with --no-fail-fast, wrote `user.email=test@example.com` into the shared config and made fixture commits on the real branch. A regular clone isn't fully safe either: `git commit -a` and `git commit <path>` export an absolute GIT_INDEX_FILE there too. Two test-only layers: - A cargo runner (.cargo/config.toml -> dev/without-git-repo-env) clears every variable `git rev-parse --local-env-vars` lists, then execs the test binary. It runs before the test process starts, so it also covers what per-command scrubbing can't: in-process production `ls-files`/`git add` reached from unit tests, and the CLI children tests launch. It fails closed if git can't produce the list. - A tripwire, `assert_git_env_isolated()`, runs before every test git write in tests/common, the tracked_files unit tests, and runner_api's in-process stage. It refuses if any of those variables survive, covering runs that bypass the runner (a test binary run directly, `--manifest-path` from outside the repo, non-unix hosts, an overriding runner). Production code is unchanged and keeps honoring GIT_DIR/GIT_INDEX_FILE: users run `codeownership validate` from their own hooks, where `git_stage` must stage into the index being committed. The runner only affects what cargo launches inside this repo. Five regression tests in tests/git_env_isolation_test.rs cover the runner stripping the variables, failing closed without git, and preserving the binary's exit status, and the tripwire refusing both the worktree shape and a lone GIT_INDEX_FILE. Each fails when the behavior it guards is removed.
Merged
- Move the tripwire to tests/support/git_env.rs, spliced with include!() into tests/common and the tracked_files unit tests instead of duplicated. - Guard build_run_config too, since its callers stage in-process. - Re-run each tests/common helper in a child under an inherited GIT_DIR, so dropping any one helper's tripwire call fails a test. - Make the child an ignored test, so an ambient marker variable can't turn the parent test into a silent no-op. - Fix the tripwire message for runs that overrode the runner, and document the conflict with a user's own cfg(...) runner in .cargo/config.toml.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Git hooks export
GIT_DIRandGIT_INDEX_FILE, and in a linked worktree they're absolute paths to the real repository. Several tests rungit init,git add --all, andgit configin temp dirs with onlycurrent_dirset, so under those variables they act on the enclosing repo instead. The repo's own pre-commit hook runscargo test, so committing from a worktree corrupts the repo.What happens today
Measured with a probe hook (git 2.55.0):
GIT_INDEX_FILE.git/index(relative, resolves inside each temp repo)GIT_DIROne commit from a worktree on
main:.git/configtocore.bare = true, which broke the primary clone (git status: "this operation must be run in a work tree");--no-fail-fast, also wroteuser.email=test@example.cominto the shared config and made fixture commits on the real branch.The hook fails partway, so nothing gets committed, but the damage is done. A regular clone isn't fully safe either:
git commit -aandgit commit <path>export an absoluteGIT_INDEX_FILEthere as well. The same exposure applies togit rebase -x 'cargo test'andgit bisect run cargo testin a worktree.Fix — two test-only layers
A cargo runner.
.cargo/config.tomlroutes every binary cargo launches throughdev/without-git-repo-env:git rev-parse --local-env-varsis git's own list of repository-local variables, the same set it clears when running in a different repo. The runner fails closed: if git can't produce the list, the tests don't run. The simplerunset $(git rev-parse --local-env-vars)fails open, because a failed git leavesunsetwith no arguments. Clearing the environment before the test process starts also covers what per-command scrubbing can't reach: in-process productionls-filesandgit addcalled from unit tests, and every CLI childassert_cmdlaunches.A tripwire.
assert_git_env_isolated()runs inside everytests/commonhelper that touches a repo (init_git_repo,git_add_all_files,git_reset_all,is_file_staged, andbuild_run_config, whose callers stage in-process), in thetracked_filesunit tests, and beforerunner_api's in-process stage. It refuses to proceed if any of those variables survive. That covers runs that bypass the runner: a test binary run directly,--manifest-pathfrom outside the repo, non-unix hosts, or an overriding target runner. It guards the shared helpers, not each git call: tests that stage or rungenerateare covered because they build their repo through those helpers first, so a new test that skips them should call it too. It lives intests/support/git_env.rsand is spliced withinclude!()into bothtests/commonand thetracked_filesunit tests, because unit tests can't importtests/commonand integration tests link the library built withoutcfg(test). That shares one copy without adding test support to the crate's public API.Why not change production code or the hook
Production behavior is unchanged, deliberately. Users run
codeownership validatefrom their own pre-commit hooks, wheregit_stage()must stage CODEOWNERS into the index being committed, andls-filesmust see that index. The runner only affects what cargo launches inside this repo; the library and the released binary never see it..rusty-hook.tomlis untouched. rusty-hook runs its command withsh -cand inherits the environment, so the existing hook'scargo testalready goes through the runner. Fixing only the hook would have left manual runs,rebase -x,bisect run, and other hook managers exposed, and any shell syntax in the hook string breaks Windows'scmd /C.Verification
main, a worktree commit reproduces the corruption above;GIT_DIRandGIT_INDEX_FILEexported by hand at a victim repo leave it byte-identical with this change. Onmain, the same run corrupts it.tests/git_env_isolation_test.rs. They check that the runner strips the variables, fails closed without git, and preserves the test binary's exit status. They also re-run eachtests/commonhelper in a child process under the worktree shape, andinit_git_repounder a loneGIT_INDEX_FILE, and check that it refuses. The child is an#[ignore]d test, so a stray marker variable can't turn the parent into a no-op. Each fails when the behavior it guards is removed; for example, a runner that swallows exit status fails withleft: Some(0), right: Some(3).cargo fmt --checkandcargo clippy --all-targets --all-features -- -D warningsare clean;cargo testpasses 168/0 across 27 binaries, and five runs at--test-threads=16pass 840/0. Removing the tripwire call from any single helper failstest_git_helpers_refuse_inherited_repo_env. Confirmed under Linux dash and macOS/bin/sh.Worth knowing
cargo runinside this repo also goes through the runner, so it doesn't see these variables either. One consequence:cargo run -- generate --stagefrom a hook duringgit commit -aorgit commit <path>stages into.git/indexrather than the hook's temporary index. The released binary is unaffected.[target.'cfg(...)'] runner, cargo refuses to choose between the two (several matching instances of target.'cfg(..)'.runner). Override withCARGO_TARGET_<TRIPLE>_RUNNER; the tripwire still guards git..cargo/config.tomlsays the same.cfg(unix). Elsewhere, the tripwire still refuses before any git write made through the shared test helpers.--local-env-varslist, so when the runner is bypassed, harmless variables likeGIT_CONFIG_PARAMETERS(set by agit -cparent) also make it refuse. That's deliberate: it fails closed.rebase -x-ing across commits older than this one, from a worktree, is still exposed, since those commits don't have the runner.git rev-parseoutside both layers (cargo runs build scripts directly). It's read-only and fails harmlessly under an inheritedGIT_DIR.Sent with Claude Code