ref:main

Test suite corrupts the developer's real repository: pre-push hook leaks GIT_DIR into mix test #75

closed Opened by cole.christensen@gmail.com

Links

No links yet.

.githooks/pre-push runs mix test. Git exports GIT_DIR (and GIT_WORK_TREE, GIT_INDEX_FILE, …) into hook processes, so every git push runs the whole test suite pointed at the repository being pushed. Tests that shell out to git — 20 test files do — then operate on that repo instead of their own fixture.

Reproduced deliberately

mix test -> 1030 passed
mix test (under the pre-push hook) -> 74 failed
GIT_DIR=<throwaway repo> mix test -> 74 failed # same count, on purpose

Damage observed on a real developer checkout

All four of the outcomes the existing test_helper.exs comment warns about actually happened here:

  • core.bare set to true — git then refused to treat the checkout as a work tree at all (fatal: this operation must be run in a work tree)
  • remote.origin.url rewritten to a fixture’s local HTTP server (http://127.0.0.1:61703/repo)
  • origin/main force-updated to a fixture commit (9190f30)
  • a spurious .git/shallow written, truncating a branch’s history to 1 commit and making git push fail with remote unpack failed: invalid_command: shallow …
  • ~140 fixture commits (first, second, a, b, delta-friendly, pushed, rewritten, commit 1commit 10) committed onto a real branch, per its reflog

A stray side branch with commits dated 2026-07-10 is still sitting in the repo, so this has happened at least once before.

All of the above was repaired by hand; git fsck is clean.

Why the existing guard doesn’t cover it

test_helper.exs sets GIT_CEILING_DIRECTORIES to the project root, which stops git’s upward discovery from escaping into the project’s .git. That guard is correct and necessary — but GIT_DIR bypasses discovery entirely, so the ceiling never gets consulted.

A scrub alone is not the fix — verified

The obvious patch is to System.delete_env the redirecting variables in test_helper.exs before ExUnit.start/1. I wrote it and measured it:

GIT_DIR=<repo> mix test -> 1032 passed (was 74 failed)

…and it moved the corruption rather than stopping it. With GIT_DIR gone, the same tests fell back to discovery and wrote their fixture commits into the worktree’s own repository instead — because the fixture directories live under the project root, and GIT_CEILING_DIRECTORIES only blocks discovery from above the ceiling, not from below it. That is exactly how the ~140 commits above got written.

So the scrub was deliberately left out of fangorn/ex_git_objectstore#48; shipping half a fix here would just relocate the blast radius.

What a real fix probably looks like

Every System.cmd("git", …) in the suite should be unable to reach any repository but its own fixture, by construction rather than by hoping discovery fails:

  • pass env: [{"GIT_DIR", fixture_git_dir}, {"GIT_WORK_TREE", fixture_work_tree}, …] explicitly per invocation, via one shared helper rather than 20 call sites, and
  • scrub the inherited variables in test_helper.exs so nothing leaks in, and
  • keep fixture directories outside the project tree (System.tmp_dir!/0) so discovery-from-below cannot reach the project’s .git even if a cd: target is missing.

The existing TestIsolationTest is the right place for regression coverage; it needs cases for an inherited GIT_DIR and for discovery-from-below.

Interim

Until this is fixed, git push in this repo is unsafe. Pushing with --no-verify after running mix test manually is the safe order of operations, which is what fangorn/ex_git_objectstore#48 did.

Found while profiling the diff engine for fangorn/anvil#367.