What's wrong
RunCommandGitProcessRunner.EnvironmentOverlay (GitIntegration/Execution/RunCommandGitProcessRunner.cs:51-56) adds only GIT_TERMINAL_PROMPT=0 and LC_ALL=C. Everything else in the caller's environment is inherited by every git child process. Two groups of inherited variables beat the arguments the library passes.
1. The repository-locating variables beat -C <LocalPath>
The library chooses the repository with -C. GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY and GIT_COMMON_DIR take priority over repository discovery. Git exports these variables to hooks, and githooks(5) tells hooks to clear them before touching another repository. So a .NET tool that runs from a hook (Husky.NET and similar) and opens a different repository with this library gets the wrong data.
Reproduced with git 2.43, using repo A (contains A.txt) and repo B (contains a modified B.txt). The program calls client.OpenAsync(B), then repo.Status() and repo.Patch():
| Environment |
Status() |
Patch() |
| clean |
B.txt Modified ✅ |
B.txt ✅ |
GIT_DIR=$PWD/A/.git |
A.txt Deleted, B.txt Untracked ❌ |
A.txt ❌ |
real pre-commit hook in A, during git commit -a (git sets GIT_INDEX_FILE=…/A/.git/index.lock) |
throws GitCommandException: fatal: unable to read <sha> ❌ |
— |
In the GIT_DIR row, LocalPath still says B, but git is comparing A's index against B's working tree. Mutating verbs (Add, Apply().ToIndex(), Commit) resolve the same way, so they would write into the other repository's index or object store. The reproduction above covered only the read verbs.
2. GIT_DIFF_OPTS beats the pinned -U{n} in Patch()
GitPatchBuilder always emits -U{n} (GitPatchBuilder.cs:181-183), so that a hostile configuration can't produce a zero-context patch (#121/#128), and WithContext(0) is refused for the same reason. The git docs say GIT_DIFF_OPTS=--unified=N / -uN overrides any -U/--unified on the command line, so an environment variable can defeat that guarantee:
var f = (await repo.Patch().WithContext(3).ForPath(RelativeFilePath.Create("n.txt")).ExecuteAsync()).Files[0];
var r = await repo.Apply(f.PatchFor(f.Hunks)).ToIndex().TryExecuteAsync();
The setup is n.txt committed as lines 1..5, with line 3 changed to X in the working tree:
- Clean environment: the hunk is
@@ -1,5 +1,5 @@, and the apply succeeds.
GIT_DIFF_OPTS=-u0: the hunk is @@ -3 +3 @@ with no context. The apply fails with patch failed: n.txt:3 … patch does not apply, because Apply doesn't pass --unidiff-zero.
Suggested fix
Add these keys to EnvironmentOverlay with null values. RunCommand ≥ 1.5.0 removes a variable whose value is null.
GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY,
GIT_ALTERNATE_OBJECT_DIRECTORIES, GIT_COMMON_DIR, GIT_NAMESPACE, GIT_PREFIX,
GIT_DIFF_OPTS
GIT_EXTERNAL_DIFF is already neutralised by --no-ext-diff.
Acceptance criteria
- A runner test sets
GIT_DIR (and GIT_INDEX_FILE) to point at a different repository, and Status() and Patch() still report the repository passed to OpenAsync.
- A test sets
GIT_DIFF_OPTS=-u0, and Patch().WithContext(3) still returns 3-line context hunks that Apply().ToIndex() accepts.
What's wrong
RunCommandGitProcessRunner.EnvironmentOverlay(GitIntegration/Execution/RunCommandGitProcessRunner.cs:51-56) adds onlyGIT_TERMINAL_PROMPT=0andLC_ALL=C. Everything else in the caller's environment is inherited by every git child process. Two groups of inherited variables beat the arguments the library passes.1. The repository-locating variables beat
-C <LocalPath>The library chooses the repository with
-C.GIT_DIR,GIT_WORK_TREE,GIT_INDEX_FILE,GIT_OBJECT_DIRECTORYandGIT_COMMON_DIRtake priority over repository discovery. Git exports these variables to hooks, and githooks(5) tells hooks to clear them before touching another repository. So a .NET tool that runs from a hook (Husky.NET and similar) and opens a different repository with this library gets the wrong data.Reproduced with git 2.43, using repo A (contains
A.txt) and repo B (contains a modifiedB.txt). The program callsclient.OpenAsync(B), thenrepo.Status()andrepo.Patch():Status()Patch()B.txt Modified✅B.txt✅GIT_DIR=$PWD/A/.gitA.txt Deleted,B.txt Untracked❌A.txt❌pre-commithook in A, duringgit commit -a(git setsGIT_INDEX_FILE=…/A/.git/index.lock)GitCommandException: fatal: unable to read <sha>❌In the
GIT_DIRrow,LocalPathstill says B, but git is comparing A's index against B's working tree. Mutating verbs (Add,Apply().ToIndex(),Commit) resolve the same way, so they would write into the other repository's index or object store. The reproduction above covered only the read verbs.2.
GIT_DIFF_OPTSbeats the pinned-U{n}in Patch()GitPatchBuilderalways emits-U{n}(GitPatchBuilder.cs:181-183), so that a hostile configuration can't produce a zero-context patch (#121/#128), andWithContext(0)is refused for the same reason. The git docs sayGIT_DIFF_OPTS=--unified=N/-uNoverrides any-U/--unifiedon the command line, so an environment variable can defeat that guarantee:The setup is
n.txtcommitted as lines1..5, with line 3 changed toXin the working tree:@@ -1,5 +1,5 @@, and the apply succeeds.GIT_DIFF_OPTS=-u0: the hunk is@@ -3 +3 @@with no context. The apply fails withpatch failed: n.txt:3 … patch does not apply, because Apply doesn't pass--unidiff-zero.Suggested fix
Add these keys to
EnvironmentOverlaywithnullvalues. RunCommand ≥ 1.5.0 removes a variable whose value is null.GIT_EXTERNAL_DIFFis already neutralised by--no-ext-diff.Acceptance criteria
GIT_DIR(andGIT_INDEX_FILE) to point at a different repository, andStatus()andPatch()still report the repository passed toOpenAsync.GIT_DIFF_OPTS=-u0, andPatch().WithContext(3)still returns 3-line context hunks thatApply().ToIndex()accepts.