[package-deps-hash] Fix build cache failures in git linked worktrees caused by GIT_DIR set by pre-commit hooks#5815
Conversation
7b20309 to
7bfddde
Compare
|
@microsoft-github-policy-service agree company="Squarespace" |
… calls to fix build cache in linked worktrees
When a git pre-commit hook runs in a linked worktree, git sets GIT_DIR to the
per-worktree metadata directory (.git/worktrees/{name}) without setting
GIT_WORK_TREE. With GIT_DIR set this way, `git rev-parse --show-toplevel`
returns the CWD (e.g. the rushJsonFolder subdirectory) instead of the actual
worktree root, causing all subsequent git calls to use the wrong root directory.
This makes `git status -u` miss the top-level .gitignore, surfacing node_modules
symlinks as untracked files, which then causes `git hash-object` to fail on
symlink-to-directory entries and ultimately breaks the build cache.
Fix: strip GIT_DIR and GIT_WORK_TREE from the environment in getRepoRoot,
spawnGitAsync, and getRepoChanges so git auto-discovers the correct repo root
from the working directory regardless of hook-injected env vars.
7bfddde to
ef318f0
Compare
| { | ||
| currentWorkingDirectory | ||
| currentWorkingDirectory, | ||
| environment: getCleanGitEnvironment() |
There was a problem hiding this comment.
Should we strip those env vars iff they point to nonexistent locations/locations without a rush.json?
Also note that AFAIK rush.json doesn't have to be at the repo root.
There was a problem hiding this comment.
the GIT_DIR directory always points to the metadata directory (./.git/, not the root of the git repository - so when GIT_DIR is set, it will never point to a directory with a rush.json file in it. So only stripping the vars if they point to locations without a rush.json would effectively be the same as always stripping them.
As for when they point to nonexistent locations - that wouldn't apply in the bug case, because the GIT_DIR value points to the metadata directory, which does exist. I believe GIT_DIR is not set in the node env when you are working in the "main worktree" (i.e. you haven't created a separate worktree at all), so if that var is set at all, it should always point to a directory that does exist. In older git versions, GIT_DIR is set to the default .git location, but stripping it still forces git to navigate upwards to find the worktree root naturally, resulting in the same logic.
The only users that might be incidentally affected by this would be those who are explicitly setting GIT_DIR to some unexpected value - but even in that case, I think stripping the variables is the better choice. The changed utils are all given a currentWorkingDirectory value, and I would expect that git would follow its typical logic to find the repo root by navigating up from the given directory. When GIT_DIR is set and GIT_WORK_TREE is missing, the logic for git rev-parse --show-toplevel changes, and it treats the cwd as the root without navigating upwards. Stripping GIT_DIR fixes the git environment for subprocesses created within hooks (such as when running rush commands in a pre-commit hook, which then run their own git commands to determine repo state)
The way this works right now is that it strips variables that are only defined in the case that we need to fix - so stripping them unconditionally feels like the right move, even though it might appear overly aggressive. I'm open to making the change more limited if you have concerns.
Also note that AFAIK rush.json doesn't have to be at the repo root.
To be clear, our use case is exactly that - our rush.json file lives in a subdirectory one level deeper than the repo root (./our-special-directory/rush.json instead of ./rush.json). Part of the intention of this fix is specifically to handle cases where the rush.json is not at the repo root, where the currentWorkingDirectory value is the subdirectory where the rush.json file lives. In the "standard" case, where the rush.json file does live at the repo root, users wouldn't be affected by this bug anyway.
…ee-hook_2026-06-03-00-00.json Co-authored-by: Ian Clanton-Thuon <iclanton@users.noreply.github.com>
Co-authored-by: Ian Clanton-Thuon <iclanton@users.noreply.github.com>
Co-authored-by: Ian Clanton-Thuon <iclanton@users.noreply.github.com>
| "changes": [ | ||
| { | ||
| "packageName": "@microsoft/rush", | ||
| "comment": "No-op change to trigger changeset for rush publish for package-deps-hash changes.", |
There was a problem hiding this comment.
Not sure if we need something more descriptive here, if it ends up in a changelog or anything.
There was a problem hiding this comment.
It does end up in the changelog. Can you describe the fix from a Rush user's perspective here?
There was a problem hiding this comment.
Actually, the text you have in the @rushstack/package-deps-hash changefile would be better here. That changefile should be updated to describe what will change for consumers of that package (without independent of Rush).
There was a problem hiding this comment.
I've updated this changefile to use what was previously the description in the package-deps-hash changefile, and updated the other changefile's description to now read Strip GIT_DIR and GIT_WORK_TREE Node env variables to fix issues with miscalculating the git repo root when working in a linked worktree.
How's that read to you?
…undary The previous test set process.env.GIT_DIR and shelled out to git, but the Jest environment does not propagate in-process env writes to child processes, so git never saw the variable and the test passed with or without the fix. Assert instead that getRepoRoot invokes Executable.spawnSync with an explicit environment that omits GIT_DIR/GIT_WORK_TREE. This fails against the pre-fix code (no environment passed) and passes with the fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Fixes build cache failures when Rush commands that trigger incremental build analysis run inside a git linked worktree via a pre-commit hook.
Fixes #5479
When git invokes a pre-commit hook in a linked worktree, it sets
GIT_DIRto the per-worktree metadata directory (e.g..git/worktrees/{name}) but does not setGIT_WORK_TREE. Child processes inherit this environment.getRepoRoot()callsgit rev-parse --show-topleveland inherits thisGIT_DIR, causing git to return thecurrentWorkingDirectoryargument (e.g. therushJsonFoldersubdirectory) instead of the actual worktree root. All subsequent git calls use this wrong root, causinggit status -uto miss the top-level.gitignore, surfacingnode_modules/symlinks as untracked files, whichgit hash-objectcannot hash — ultimately reported as "Build cache is only supported if running in a Git repository."Details
The fix strips
GIT_DIRandGIT_WORK_TREEfrom the environment before spawning any git subprocess ingetRepoState.ts. This lets git auto-discover the correct repo root by scanning up fromcurrentWorkingDirectory, which correctly resolves to the linked worktree root regardless of the hook-injected environment. Three call sites are patched:getRepoRoot,spawnGitAsync(used bygetDetailedRepoStateAsyncandhashFilesAsync), andgetRepoChanges.This regression was introduced in #5500, which switched from
git ls-tree -r HEAD(reads committed objects, never surfacesnode_modules/) togit ls-files --cached+git status -u(scans the work tree, exposing the broken.gitignorecontext).The diff also includes prettier reformatting the
WINDOWS_RESERVED_BASENAMESarray from a compact multi-line form to one-entry-per-line — this is an unrelated side effect of the project's pre-commit hook. Happy to add a// prettier-ignorecomment to suppress it if preferred.How it was tested
Added a unit test to
getRepoDeps.test.tsthat setsGIT_DIRto a nonexistent path (simulating hook interference) and verifies thatgetRepoRootstill returns the correct repo root. Without the fix,git rev-parseexits 128 ("not a git repository") and the function throws; with the fix,GIT_DIRis stripped and git auto-discovers the root correctly.Also manually reproduced the original failure by running a Rush build command from within a git linked worktree via a pre-commit hook and confirmed the build cache error no longer occurs with this fix applied.
Reproduction
The bug can be reproduced with this shell script: https://gist.github.com/istateside/e8b0c5f694424a423ae29fe9203ec895
The script bootstraps a Rush monorepo with all of the requisite contributing factors to recreate the bug:
rush.jsonis not in the root directory of the git repo)In that environment, the bug is triggered if you are in a linked worktree and make a commit, to trigger the pre-commit rush command.