Skip to content

Prevent git rebase --exec test runs from corrupting the enclosing worktree#2157

Merged
dscho merged 5 commits intomainfrom
fix-tests-during-rebase
Mar 4, 2026
Merged

Prevent git rebase --exec test runs from corrupting the enclosing worktree#2157
dscho merged 5 commits intomainfrom
fix-tests-during-rebase

Conversation

@dscho
Copy link
Copy Markdown
Member

@dscho dscho commented Feb 28, 2026

Running git rebase -x 'npm test' from a worktree wreaked havoc on my
real repository: core.bare = true and test url.*.insteadOf settings
ended up in the shared .git/config, test commits landed on the real
HEAD, and a bogus refs/notes/gitgitgadget ref appeared out of nowhere.
For a terrifying moment I thought I had lost actual work.

The root cause turned out to be git rebase --exec setting GIT_DIR in
the environment. This leaked into the Node.js test processes, causing
git init <target> to silently reinitialize the real repository
instead of creating a fresh one in the target directory -- git simply
ignores the target argument when GIT_DIR is set.

Diagnosing this was a bit trickier than I hoped for. Adding
GIT_CEILING_DIRECTORIES alone did not help because GIT_DIR takes
precedence over repository discovery, making ceiling directories
irrelevant. A secondary leak hid in misc-helper.test.ts which
captured process.env at module load time, before testCreateRepo had
a chance to clear the offending variables.

This series clears GIT_DIR/GIT_WORK_TREE early, adds
GIT_CEILING_DIRECTORIES as defense-in-depth, introduces a
validateWorkDir() safety net that fails loudly if git would operate
outside the test directory, and fixes a pre-existing test that
accidentally depended on the enclosing repo's config. It also tells Jest
to ignore .test-dir/ so stale files from corrupted runs don't cause
confusing failures on the next run.

Copy link
Copy Markdown
Contributor

@webstech webstech left a comment

Choose a reason for hiding this comment

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

Hoping this comment shows up correctly in the PR. ed: it did not! This relates to d893220.

The commit explanation may be misleading with (which is not the case for non-bare repositories). core.bare is present and false (at least in new repos). The test only works because the test is normally run in a git repo. If the working directory/tree is copied to another directory without the .git directory, the test will fail.

@webstech
Copy link
Copy Markdown
Contributor

webstech commented Mar 4, 2026

This relates to d2113d3. It is unfortunate that this and the previous commit are needed. It would be nice if you could just tell git not to search beyond the directory.

Comment thread package.json
dscho added 5 commits March 4, 2026 09:00
The "finds core.bare" test was a gitConfig() smoke test added in
a2ad7db (Start testing the new classes, 2018-05-08) that happened to
read core.bare from the enclosing repository. This is exactly the kind
of accidental access to the enclosing repo that we want to prevent, and
it also only passed by accident: While `git init` _does_ set that config
variable, users are at liberty to unset it (because Git considers it to
default to "false").

Replace the `core.bare` test with one that reads one of the GIT_CONFIG_*
environment variables already set up at the top of the same file,
which still exercises gitConfig() without depending on the enclosing
repository.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
When tests are run from within `git rebase --exec` (e.g. via
`exec rm -rf node_modules/ build/ && npm ci && npm run build && npm test`),
git sets GIT_DIR in the environment, pointing to the worktree's gitdir.
This leaked into child processes spawned by the test infrastructure, causing
`git init <target>` to silently reinitialize the *real* repository instead
of creating a new one in the target directory. The result was `core.bare`
getting flipped in the shared config and test commits, refs and notes
landing on the real HEAD.

The fix is to delete GIT_DIR and GIT_WORK_TREE from the environment at the
very start of testCreateRepo, before any git commands are executed.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
When running `npm test` from a Git worktree, test-spawned git processes
that happened to operate at or above `.test-dir/` would walk up the
directory tree and discover the enclosing real repository. In a worktree
setup this is particularly dangerous because the `.git/config` is shared
across all worktrees, so any accidental config write (e.g. `core.bare`)
affects every checkout.

Prevent this by setting `GIT_CEILING_DIRECTORIES` to every ancestor of
the `.test-dir/` temporary directory. The temp dir itself is excluded so
that git can still discover the test repositories created inside it.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
The GIT_CEILING_DIRECTORIES fix in the preceding commit should be
sufficient to prevent test-spawned git processes from discovering the
enclosing repository. However, when that protection was put to the test
by running `rm -rf node_modules/ build/ && npm ci && npm run build &&
npm test` from a worktree, the test commits still ended up on the real
worktree's HEAD for reasons that could not be fully diagnosed. Since the
consequences of this happening are severe (the primary worktree was
marked as `core.bare = true` and test commits, notes refs, and fake
upstream refs all landed on the real repository), an additional layer of
defense is warranted.

Introduce a `validateWorkDir()` function in `lib/git.ts` that is called
from every function that spawns a git process (`git()`, `revParse()`,
`revListCount()`, `gitConfig()`, `gitConfigForEach()`,
`gitCommandExists()`). When the `GIT_WORK_DIR_PREFIX` environment
variable is set, the function verifies that the resolved `workDir` falls
inside the expected directory, and throws otherwise. For `git init
<path>` and `git --git-dir=<path>` invocations, the target path from
the arguments is validated instead of the cwd, since those commands
legitimately operate on a path specified as an argument rather than
through the `workDir` option.

In `testCreateRepo()`, the guard is suspended during the setup phase
(which needs to run `git init` and `git config --global` without a test
repo `workDir`), then activated by setting `GIT_WORK_DIR_PREFIX` to the
`.test-dir/` path before returning. This ensures that all subsequent
`git()` calls in the test process must use a `workDir` inside the test
directory, or fail loudly.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
When a previous test run leaves stale files inside .test-dir/ (e.g. copies
of the real worktree after a corrupted run that operated on the wrong
repository), Jest would discover and attempt to run .test.ts files found
there, producing confusing ENOENT failures. Tell Jest to ignore .test-dir/
entirely.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@dscho
Copy link
Copy Markdown
Member Author

dscho commented Mar 4, 2026

Hoping this comment shows up correctly in the PR. ed: it did not! This relates to d893220.

The commit explanation may be misleading with (which is not the case for non-bare repositories). core.bare is present and false (at least in new repos). The test only works because the test is normally run in a git repo. If the working directory/tree is copied to another directory without the .git directory, the test will fail.

Indeed, you're right: https://github.com/git/git/blob/v2.53.0/setup.c#L2463-L2466. I must have suffered a side effect when I tried to confirm this behavior. I fixed the commit message!

Range-diff
  • 1: d893220 ! 1: 5aaf6f9 tests(git): stop reading enclosing repo's config

    @@ Metadata
      ## Commit message ##
         tests(git): stop reading enclosing repo's config
     
    -    The "finds core.bare" test was a gitConfig() smoke test added in a2ad7db1
    -    (Start testing the new classes, 2018-05-08) that happened to read
    -    core.bare from the enclosing repository. This is exactly the kind of
    -    accidental access to the enclosing repo that we want to prevent, and it
    -    also only passed by accident when core.bare was set (which is not the
    -    case for non-bare repositories).
    +    The "finds core.bare" test was a gitConfig() smoke test added in
    +    a2ad7db1 (Start testing the new classes, 2018-05-08) that happened to
    +    read core.bare from the enclosing repository. This is exactly the kind
    +    of accidental access to the enclosing repo that we want to prevent, and
    +    it also only passed by accident: While `git init` _does_ set that config
    +    variable, users are at liberty to unset it (because Git considers it to
    +    default to "false").
     
    -    Replace it with a test that reads one of the GIT_CONFIG_* environment
    -    variables already set up at the top of the same file, which still
    -    exercises gitConfig() without depending on the enclosing repository.
    +    Replace the `core.bare` test with one that reads one of the GIT_CONFIG_*
    +    environment variables already set up at the top of the same file,
    +    which still exercises gitConfig() without depending on the enclosing
    +    repository.
     
         Assisted-by: Claude Opus 4.6
         Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
  • 2: 2dc9767 = 2: 810cb3d tests: unset GIT_DIR and GIT_WORK_TREE in testCreateRepo

  • 3: 7c94d9d = 3: 0fda24f tests: set GIT_CEILING_DIRECTORIES to prevent repo discovery

  • 4: d2113d3 = 4: d51f9ae tests: add a safety net to prevent git from operating on the wrong repo

  • 5: 0d593b3 = 5: bfa3684 jest: exclude .test-dir/ from test discovery

This relates to d2113d3. It is unfortunate that this and the previous commit are needed. It would be nice if you could just tell git not to search beyond the directory.

Yes. The GIT_CEILING_DIRECTORIES feature errs on the side of the Git developers' convenience.

@dscho dscho force-pushed the fix-tests-during-rebase branch from 0d593b3 to bfa3684 Compare March 4, 2026 08:01
Copy link
Copy Markdown
Contributor

@webstech webstech left a comment

Choose a reason for hiding this comment

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

Thanks. The git rebase --exec was new to me.

Comment thread package.json
@dscho dscho merged commit a9499c1 into main Mar 4, 2026
6 checks passed
@dscho dscho deleted the fix-tests-during-rebase branch March 4, 2026 19:11
github-actions bot pushed a commit that referenced this pull request Mar 4, 2026
…rupting the enclosing worktree (#2157), 2026-03-04))
github-actions bot pushed a commit to Bassamx93/gitgitgadget that referenced this pull request Mar 4, 2026
…rupting the enclosing worktree (gitgitgadget#2157), 2026-03-04))
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.

2 participants