Fix corruption when unstaging changes made by git commands#1856
Fix corruption when unstaging changes made by git commands#1856tyrielv wants to merge 1 commit intomicrosoft:masterfrom
Conversation
e122e0d to
ef5f575
Compare
When git commands like cherry-pick -n or merge stage changes directly (as opposed to user edits followed by git add), those staged files have skip-worktree set and are not in the GVFS ModifiedPaths database. A subsequent 'restore --staged' then fails to properly unstage them: - Modified/deleted files become invisible to git status - Added files (ProjFS placeholders) vanish on projection changes Fix by sending a PreUnstage pipe message from the pre-command hook before 'restore --staged' or 'checkout HEAD --' runs: 1. Query staged files via 'git diff --cached --name-status -z' matching the command's pathspec, and add them to ModifiedPaths so git clears skip-worktree and detects their working tree state 2. For staged-added files, write their content to disk via batched 'git checkout-index --force' (with hooks bypassed to avoid deadlock) so they persist as full files across projection changes Pathspecs are forwarded from the hook args to scope the operation, avoiding unnecessary ModifiedPaths additions during large merge conflict resolutions. On failure, the hook blocks with an actionable error message rather than allowing silent corruption. Components: - GVFS.Hooks/Program.Unstage.cs: detect unstage operations, extract pathspecs, send PreUnstage message, block on failure - GVFS.Mount/InProcessMount.cs: handle PreUnstage message - GVFS.Virtualization/FileSystemCallbacks.cs: AddStagedFilesToModifiedPaths queries staged files, adds to ModifiedPaths, hydrates added files - GVFS.Common/Git/GitProcess.cs: DiffCachedNameStatus, batched CheckoutIndexForFiles, QuoteGitPath for safe path escaping - GVFS.Common/NamedPipes/UnstageNamedPipeMessages.cs: message type - CorruptionReproTests.cs: functional test - GitProcessTests.cs: QuoteGitPath unit + integration tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ef5f575 to
9aa9eda
Compare
Fix corruption when unstaging changes made by git commandsProblemWhen git commands like
See #1855 for the original issue report. SolutionAdd a
Pathspecs from the user's command are forwarded to scope the operation — this avoids adding all staged files to ModifiedPaths when only unstaging specific files (important for performance during large merge conflict resolutions). Key Design Decisions
Files Changed
TestingManual verificationTested on a GVFS enlistment ( Unit tests
Functional test
Review GuidanceAreas to pay particular attention to:
|
KeithIsSleeping
left a comment
There was a problem hiding this comment.
Multi-Model Code Review
Reviewed with Claude Opus 4.6, GPT-5.1-Codex-Max, GPT-5.3-Codex, and GPT-5.4. Findings below are consensus items where multiple models independently identified the same issue.
| // Query all staged files in one call using --name-status -z. | ||
| // Output format: "A\0path1\0M\0path2\0D\0path3\0" | ||
| GitProcess.Result result = gitProcess.DiffCachedNameStatus(pathspecs); | ||
| if (result.ExitCodeIsSuccess && !string.IsNullOrEmpty(result.Output)) |
There was a problem hiding this comment.
HIGH: Fail-open — AddStagedFilesToModifiedPaths returns success when git diff --cached fails (3/3 models agree)
success is initialized to true (line 382) and is only set to false inside the ExitCodeIsSuccess block. If DiffCachedNameStatus fails (non-zero exit — index locked, repo not ready, disk error), the method falls through and returns true with addedCount = 0.
The caller in HandlePrepareForUnstageRequest converts this to a success response, the hook allows the unstage to proceed, and no paths were added to ModifiedPaths. This re-opens the exact corruption this PR is meant to fix.
Recommendation: Treat !result.ExitCodeIsSuccess as hard failure:
else if (!result.ExitCodeIsSuccess)
{
success = false;
// Log result.Errors for diagnostics
}| string arg = args[i]; | ||
|
|
||
| if (arg.StartsWith("--git-pid=")) | ||
| continue; |
There was a problem hiding this comment.
MEDIUM: Tree-ish arguments incorrectly forwarded as pathspecs (2/4 models agree)
The parser adds every non-option token before -- to the path list. For git checkout HEAD -- foo.txt, the extracted list becomes ["HEAD", "foo.txt"]. If the repo has a file or directory matching the tree-ish name, the mount side will add/hydrate unrelated staged paths.
At OS repo scale (2.5M files), accidental scope widening is dangerous.
Recommendation: Parse checkout and restore separately — strip the checkout tree-ish (HEAD) and any --source value before collecting pathspecs.
| /// Returns null-separated pathspecs, or empty string for all staged files. | ||
| /// </summary> | ||
| private static string GetRestorePathspec(string command, string[] args) | ||
| { |
There was a problem hiding this comment.
MEDIUM: --pathspec-from-file ignored, falls back to all-staged scan (2/4 models agree)
GetRestorePathspec() skips all --prefixed args without interpreting them. git restore --staged --pathspec-from-file=list.txt is treated as no pathspec, which downstream means "all staged files" (DiffCachedNameStatus(null)). On the OS repo, a narrowly-scoped unstage becomes a full-index walk.
Recommendation: Explicitly support --pathspec-from-file/--pathspec-file-nul, or fail closed with an error when those options are detected.
Adds a test to reproduce corruption of cherry-pick followed by restore. This should remove all staged changes in the index but leave the working directory unchanged.
It still leaves the working directory unchanged, but the index is in a corrupted state and git status no longer accurately reflects the working directory.
See #1855. It hasn't been determined yet whether microsoft/git or VFSForGit is the cause of the error - it could be both.