From 5905e0e7751c7c7f6c737298cc940b87c927b544 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 10 Jun 2026 15:49:03 -0700 Subject: [PATCH 1/2] Add repro test for reset --mixed skip-worktree bug Adds ReproResetMixedSkipWorktree to CorruptionReproTests. The test hydrates Readme.md via blame (materializes on disk but not in ModifiedPaths, so skip-worktree stays set), then runs reset HEAD~1. On the FunctionalTests/20201014 branch, Readme.md is the only file that differs between HEAD and HEAD~1. The control repo correctly reports it as modified after the reset; the GVFS repo does not because skip-worktree hides the working-tree vs index mismatch. This test is expected to FAIL until the fix is applied. Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella --- .../Tests/GitCommands/CorruptionReproTests.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionReproTests.cs b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionReproTests.cs index 9e7eec0bb..362367f5f 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionReproTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionReproTests.cs @@ -76,6 +76,47 @@ public void ReproCherryPickRestoreCorruption() this.FilesShouldMatchCheckoutOfSourceBranch(); } + /// + /// Reproduces a bug where "git reset --mixed" fails to report hydrated files + /// as modified when skip-worktree hides the working-tree vs index mismatch. + /// + /// After a mixed reset, the index is updated to the target commit's tree, but + /// the working tree is left untouched. For non-hydrated placeholders this is + /// invisible (ProjFS serves the new content transparently). But for hydrated + /// files — files that have been read and materialized on disk — the on-disk + /// content still matches the OLD HEAD. Because the file was never modified + /// (just read), it is not in ModifiedPaths, so skip-worktree remains set. + /// This causes "git status" to skip the file entirely, hiding the mismatch + /// between the stale on-disk content and the new index entry. + /// + /// On the FunctionalTests/20201014 branch, Readme.md is the only file that + /// differs between HEAD and HEAD~1, making it a clean single-file repro. + /// + /// Expected: reset output includes "M Readme.md", status shows it as modified. + /// Actual (bug): reset output is empty, status reports clean. + /// + [TestCase] + public void ReproResetMixedSkipWorktree() + { + // Hydrate Readme.md by reading it via blame. In GVFS, this triggers a + // ProjFS callback that materializes the file from the object store. The + // file is now a full file on disk, but NOT in ModifiedPaths (read-only + // access doesn't modify it), so skip-worktree stays set. + this.ValidateGitCommand("blame Readme.md"); + + this.ValidateGitCommand("checkout -b tests/functional/ReproResetMixedSkipWorktree"); + + // Mixed reset to HEAD~1. Readme.md's blob differs between HEAD and HEAD~1 + // on this branch, so the index entry is updated. But the on-disk content + // still has HEAD's version (mixed reset doesn't touch the working tree). + // + // Control repo: reports "M Readme.md" in reset output and status. + // GVFS (bug): skip-worktree is set and file isn't in ModifiedPaths, so + // git skips the working-tree check. Reset output omits Readme.md, and + // status incorrectly reports clean. + this.ValidateGitCommand("reset HEAD~1"); + } + /// /// Reproduction of a reported issue: /// Restoring a file after its parent directory was deleted fails with From 7d27d35dd7f61aac6c732b938010d17b4acfb9c9 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 10 Jun 2026 15:49:19 -0700 Subject: [PATCH 2/2] Fix reset --mixed skipping hydrated files due to skip-worktree Adds PrepareForReset pre-command hook (mirroring PrepareForUnstage) that diffs HEAD against the target commit and adds changed files to ModifiedPaths before the reset runs. This ensures skip-worktree is cleared for hydrated files so git correctly reports them as modified. Components: - ResetCommandParser: detects mixed resets, extracts target commit - Program.Reset: sends PrepareForReset IPC to mount process - FileSystemCallbacks.AddResetDiffToModifiedPaths: runs git diff-tree, adds changed paths to ModifiedPaths - InProcessMount: handles PrepareForReset messages - ResetCommandParserTests: 12 unit tests for arg parsing Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella --- GVFS/GVFS.Common/Git/GitProcess.cs | 12 ++ .../NamedPipes/ResetNamedPipeMessages.cs | 27 ++++ GVFS/GVFS.Hooks/GVFS.Hooks.csproj | 3 + GVFS/GVFS.Hooks/Program.Reset.cs | 83 +++++++++++ GVFS/GVFS.Hooks/Program.cs | 3 + GVFS/GVFS.Hooks/ResetCommandParser.cs | 134 ++++++++++++++++++ GVFS/GVFS.Mount/InProcessMount.cs | 52 +++++++ GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj | 3 + .../Hooks/ResetCommandParserTests.cs | 132 +++++++++++++++++ .../FileSystemCallbacks.cs | 63 ++++++++ 10 files changed, 512 insertions(+) create mode 100644 GVFS/GVFS.Common/NamedPipes/ResetNamedPipeMessages.cs create mode 100644 GVFS/GVFS.Hooks/Program.Reset.cs create mode 100644 GVFS/GVFS.Hooks/ResetCommandParser.cs create mode 100644 GVFS/GVFS.UnitTests/Hooks/ResetCommandParserTests.cs diff --git a/GVFS/GVFS.Common/Git/GitProcess.cs b/GVFS/GVFS.Common/Git/GitProcess.cs index b818fd915..1264e5e2b 100644 --- a/GVFS/GVFS.Common/Git/GitProcess.cs +++ b/GVFS/GVFS.Common/Git/GitProcess.cs @@ -473,6 +473,18 @@ public Result DiffTree(string sourceTreeish, string targetTreeish, Action + /// Runs "git diff-tree --name-only -r -z" between two tree-ish references, + /// returning only the file paths that differ. The output is null-byte separated + /// for safe parsing of paths containing special characters. + /// + public Result DiffTreeNameOnly(string sourceTreeish, string targetTreeish) + { + return this.InvokeGitInWorkingDirectoryRoot( + "diff-tree --name-only -r -z " + sourceTreeish + " " + targetTreeish, + useReadObjectHook: false); + } + public Result CreateBranchWithUpstream(string branchToCreate, string upstreamBranch) { return this.InvokeGitAgainstDotGitFolder("branch " + branchToCreate + " --track " + upstreamBranch); diff --git a/GVFS/GVFS.Common/NamedPipes/ResetNamedPipeMessages.cs b/GVFS/GVFS.Common/NamedPipes/ResetNamedPipeMessages.cs new file mode 100644 index 000000000..261e801c7 --- /dev/null +++ b/GVFS/GVFS.Common/NamedPipes/ResetNamedPipeMessages.cs @@ -0,0 +1,27 @@ +namespace GVFS.Common.NamedPipes +{ + public static partial class NamedPipeMessages + { + public static class PrepareForReset + { + public const string Request = "PreReset"; + public const string SuccessResult = "S"; + public const string FailureResult = "F"; + + public class Response + { + public Response(string result) + { + this.Result = result; + } + + public string Result { get; } + + public Message CreateMessage() + { + return new Message(this.Result, null); + } + } + } + } +} diff --git a/GVFS/GVFS.Hooks/GVFS.Hooks.csproj b/GVFS/GVFS.Hooks/GVFS.Hooks.csproj index c960ac430..00b349ad2 100644 --- a/GVFS/GVFS.Hooks/GVFS.Hooks.csproj +++ b/GVFS/GVFS.Hooks/GVFS.Hooks.csproj @@ -47,6 +47,9 @@ Common\NamedPipes\UnstageNamedPipeMessages.cs + + Common\NamedPipes\ResetNamedPipeMessages.cs + Common\NamedPipes\HydrationStatusNamedPipeMessages.cs diff --git a/GVFS/GVFS.Hooks/Program.Reset.cs b/GVFS/GVFS.Hooks/Program.Reset.cs new file mode 100644 index 000000000..c7b3c9883 --- /dev/null +++ b/GVFS/GVFS.Hooks/Program.Reset.cs @@ -0,0 +1,83 @@ +using GVFS.Common.NamedPipes; +using System; + +namespace GVFS.Hooks +{ + /// + /// Partial class for reset-related pre-command handling. + /// Detects "git reset --mixed" operations and sends a PrepareForReset + /// message to the GVFS mount process so it can add files that differ + /// between HEAD and the target commit to ModifiedPaths before git + /// clears skip-worktree. + /// + public partial class Program + { + /// + /// Sends a PrepareForReset message to the GVFS mount process, which will + /// diff HEAD against the target commit and add changed files to ModifiedPaths + /// so that git will clear skip-worktree and process them during the reset. + /// + private static void SendPrepareForResetMessage(string[] args) + { + ResetCommandParser.ResetParseResult parseResult = ResetCommandParser.Parse(args); + + if (!parseResult.IsMixedReset) + { + return; + } + + // Message body is the target commit (or empty for HEAD). + // For path-based resets (git reset HEAD -- path), we still send the + // target to let the mount process diff the right trees. + string body = parseResult.TargetCommit ?? string.Empty; + + string message = string.IsNullOrEmpty(body) + ? NamedPipeMessages.PrepareForReset.Request + : NamedPipeMessages.PrepareForReset.Request + "|" + body; + + bool succeeded = false; + string failureMessage = null; + + try + { + using (NamedPipeClient pipeClient = new NamedPipeClient(enlistmentPipename)) + { + if (pipeClient.Connect()) + { + pipeClient.SendRequest(message); + string rawResponse = pipeClient.ReadRawResponse(); + if (rawResponse != null && rawResponse.StartsWith(NamedPipeMessages.PrepareForReset.SuccessResult)) + { + succeeded = true; + } + else + { + failureMessage = "GVFS mount process returned failure for PrepareForReset."; + } + } + else + { + failureMessage = "Unable to connect to GVFS mount process."; + } + } + } + catch (Exception e) + { + failureMessage = "Exception communicating with GVFS: " + e.Message; + } + + if (!succeeded && failureMessage != null) + { + ExitWithError( + failureMessage, + "The reset operation cannot safely proceed because GVFS was unable to", + "prepare the index entries. This could lead to stale index state where", + "skip-worktree files retain incorrect blob SHAs after the reset.", + "", + "To resolve:", + " 1. Run 'gvfs unmount' and 'gvfs mount' to reset the GVFS state", + " 2. Retry the reset command"); + } + } + } +} diff --git a/GVFS/GVFS.Hooks/Program.cs b/GVFS/GVFS.Hooks/Program.cs index aee260928..794234c6c 100644 --- a/GVFS/GVFS.Hooks/Program.cs +++ b/GVFS/GVFS.Hooks/Program.cs @@ -130,6 +130,9 @@ private static void RunPreCommands(string[] args) SendPrepareForUnstageMessage(command, args); } break; + case "reset": + SendPrepareForResetMessage(args); + break; case "worktree": RunWorktreePreCommand(args); break; diff --git a/GVFS/GVFS.Hooks/ResetCommandParser.cs b/GVFS/GVFS.Hooks/ResetCommandParser.cs new file mode 100644 index 000000000..c04a9bf8b --- /dev/null +++ b/GVFS/GVFS.Hooks/ResetCommandParser.cs @@ -0,0 +1,134 @@ +using System; + +namespace GVFS.Hooks +{ + /// + /// Pure parsing logic for detecting mixed resets and extracting the + /// target commit. Separated from Program.Reset.cs for testability. + /// + public static class ResetCommandParser + { + /// + /// Result of parsing a git reset command. + /// + public class ResetParseResult + { + /// Whether this is a mixed reset that needs PrepareForReset handling. + public bool IsMixedReset { get; set; } + + /// + /// The target commit (ref, SHA, HEAD~N, etc.), or null if resetting to HEAD. + /// For path-based resets (git reset HEAD -- path), this is the tree-ish. + /// + public string TargetCommit { get; set; } + + /// Whether this is a path-based reset (git reset [commit] -- paths). + public bool HasPaths { get; set; } + } + + /// + /// Determines whether the git reset command is a mixed reset that may be + /// affected by skip-worktree, and extracts the target commit. + /// + /// Mixed resets include: + /// git reset HEAD~1 (implicit --mixed) + /// git reset --mixed HEAD~1 (explicit --mixed) + /// git reset HEAD -- path (path-based, also affected) + /// + /// Not affected: + /// git reset --soft HEAD~1 (doesn't touch index) + /// git reset --hard HEAD~1 (overwrites working tree, handles skip-worktree) + /// + public static ResetParseResult Parse(string[] args) + { + // args[0] = hook type, args[1] = "reset", rest are arguments + bool isSoft = false; + bool isHard = false; + bool isMerge = false; + bool isKeep = false; + bool pastDashDash = false; + string targetCommit = null; + bool hasPositionalAfterDashDash = false; + + for (int i = 2; i < args.Length; i++) + { + string arg = args[i]; + + if (arg.StartsWith("--git-pid=")) + { + continue; + } + + if (arg == "--") + { + pastDashDash = true; + continue; + } + + if (pastDashDash) + { + hasPositionalAfterDashDash = true; + continue; + } + + if (arg.Equals("--soft", StringComparison.OrdinalIgnoreCase)) + { + isSoft = true; + continue; + } + + if (arg.Equals("--hard", StringComparison.OrdinalIgnoreCase)) + { + isHard = true; + continue; + } + + if (arg.Equals("--mixed", StringComparison.OrdinalIgnoreCase)) + { + // Explicit --mixed, no-op since mixed is the default + continue; + } + + if (arg.Equals("--merge", StringComparison.OrdinalIgnoreCase)) + { + isMerge = true; + continue; + } + + if (arg.Equals("--keep", StringComparison.OrdinalIgnoreCase)) + { + isKeep = true; + continue; + } + + // Skip flags that don't affect mode + if (arg.StartsWith("-")) + { + continue; + } + + // First positional argument before -- is the target commit + if (targetCommit == null) + { + targetCommit = arg; + } + else + { + // Second positional argument = path (git reset ) + hasPositionalAfterDashDash = true; + } + } + + // Only handle mixed resets. Soft doesn't touch index, hard and + // merge/keep have their own working tree handling. + bool isMixedReset = !isSoft && !isHard && !isMerge && !isKeep; + + return new ResetParseResult + { + IsMixedReset = isMixedReset, + TargetCommit = targetCommit, + HasPaths = hasPositionalAfterDashDash, + }; + } + } +} diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index 1272eb876..68755d495 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -508,6 +508,10 @@ private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Conne this.HandlePrepareForUnstageRequest(message, connection); break; + case NamedPipeMessages.PrepareForReset.Request: + this.HandlePrepareForResetRequest(message, connection); + break; + case NamedPipeMessages.RunPostFetchJob.PostFetchJob: this.HandlePostFetchJobRequest(message, connection); break; @@ -826,6 +830,54 @@ private void HandlePrepareForUnstageRequest(NamedPipeMessages.Message message, N connection.TrySendResponse(response.CreateMessage()); } + /// + /// Handles a request to prepare for a mixed reset operation. + /// Diffs HEAD against the target commit and adds changed files to ModifiedPaths + /// so that git will clear skip-worktree for them. Without this, hydrated files + /// (read but not modified) retain skip-worktree after the reset, hiding the + /// working-tree vs index mismatch from git status. + /// + private void HandlePrepareForResetRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection) + { + NamedPipeMessages.PrepareForReset.Response response; + + if (this.currentState != MountState.Ready) + { + response = new NamedPipeMessages.PrepareForReset.Response(NamedPipeMessages.MountNotReadyResult); + } + else + { + try + { + string targetCommit = message.Body; + bool success = this.fileSystemCallbacks.AddResetDiffToModifiedPaths(targetCommit, out int addedCount); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("addedToModifiedPaths", addedCount); + metadata.Add("targetCommit", targetCommit ?? "(HEAD)"); + metadata.Add("success", success); + this.tracer.RelatedEvent( + EventLevel.Informational, + nameof(this.HandlePrepareForResetRequest), + metadata); + + response = new NamedPipeMessages.PrepareForReset.Response( + success + ? NamedPipeMessages.PrepareForReset.SuccessResult + : NamedPipeMessages.PrepareForReset.FailureResult); + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", e.ToString()); + this.tracer.RelatedError(metadata, nameof(this.HandlePrepareForResetRequest) + " failed"); + response = new NamedPipeMessages.PrepareForReset.Response(NamedPipeMessages.PrepareForReset.FailureResult); + } + } + + connection.TrySendResponse(response.CreateMessage()); + } + private void HandleModifiedPathsListRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection) { NamedPipeMessages.ModifiedPaths.Response response; diff --git a/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj b/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj index 0517e82f1..616a4c4ac 100644 --- a/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj +++ b/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj @@ -30,6 +30,9 @@ Hooks\UnstageCommandParser.cs + + Hooks\ResetCommandParser.cs + diff --git a/GVFS/GVFS.UnitTests/Hooks/ResetCommandParserTests.cs b/GVFS/GVFS.UnitTests/Hooks/ResetCommandParserTests.cs new file mode 100644 index 000000000..2bdce26f7 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Hooks/ResetCommandParserTests.cs @@ -0,0 +1,132 @@ +using GVFS.Hooks; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.UnitTests.Hooks +{ + [TestFixture] + public class ResetCommandParserTests + { + [TestCase] + public void Parse_ImplicitMixed() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "HEAD~1" }); + + result.IsMixedReset.ShouldBeTrue(); + result.TargetCommit.ShouldEqual("HEAD~1"); + result.HasPaths.ShouldBeFalse(); + } + + [TestCase] + public void Parse_ExplicitMixed() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "--mixed", "HEAD~1" }); + + result.IsMixedReset.ShouldBeTrue(); + result.TargetCommit.ShouldEqual("HEAD~1"); + result.HasPaths.ShouldBeFalse(); + } + + [TestCase] + public void Parse_SoftNotMixed() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "--soft", "HEAD~1" }); + + result.IsMixedReset.ShouldBeFalse(); + } + + [TestCase] + public void Parse_HardNotMixed() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "--hard", "HEAD~1" }); + + result.IsMixedReset.ShouldBeFalse(); + } + + [TestCase] + public void Parse_MergeNotMixed() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "--merge", "HEAD~1" }); + + result.IsMixedReset.ShouldBeFalse(); + } + + [TestCase] + public void Parse_KeepNotMixed() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "--keep", "HEAD~1" }); + + result.IsMixedReset.ShouldBeFalse(); + } + + [TestCase] + public void Parse_NoTarget_ResetToHead() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset" }); + + result.IsMixedReset.ShouldBeTrue(); + result.TargetCommit.ShouldBeNull(); + result.HasPaths.ShouldBeFalse(); + } + + [TestCase] + public void Parse_PathBasedReset() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "HEAD", "--", "path/to/file.txt" }); + + result.IsMixedReset.ShouldBeTrue(); + result.TargetCommit.ShouldEqual("HEAD"); + result.HasPaths.ShouldBeTrue(); + } + + [TestCase] + public void Parse_PathBasedResetWithoutDashDash() + { + // git reset HEAD path/to/file.txt (second positional = path) + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "HEAD", "path/to/file.txt" }); + + result.IsMixedReset.ShouldBeTrue(); + result.TargetCommit.ShouldEqual("HEAD"); + result.HasPaths.ShouldBeTrue(); + } + + [TestCase] + public void Parse_CommitSha() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "abc123def" }); + + result.IsMixedReset.ShouldBeTrue(); + result.TargetCommit.ShouldEqual("abc123def"); + } + + [TestCase] + public void Parse_GitPidArgIgnored() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "--git-pid=12345", "HEAD~2" }); + + result.IsMixedReset.ShouldBeTrue(); + result.TargetCommit.ShouldEqual("HEAD~2"); + } + + [TestCase] + public void Parse_ExplicitMixedNoTarget() + { + ResetCommandParser.ResetParseResult result = ResetCommandParser.Parse( + new[] { "pre-command", "reset", "--mixed" }); + + result.IsMixedReset.ShouldBeTrue(); + result.TargetCommit.ShouldBeNull(); + } + } +} diff --git a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs index ab2ff36d7..533425cc2 100644 --- a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs +++ b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs @@ -492,6 +492,69 @@ public bool AddStagedFilesToModifiedPaths(string messageBody, out int addedCount return success; } + /// + /// Finds files that differ between HEAD and the given target commit and adds + /// them to ModifiedPaths. This prepares for a mixed reset operation by ensuring + /// git will clear skip-worktree for these entries, allowing it to detect + /// working-tree mismatches for files that were hydrated (read from ProjFS but + /// not modified, so not yet in ModifiedPaths). + /// + /// + /// The commit to diff against HEAD. If null or empty, defaults to HEAD (no-op). + /// + /// Number of paths added to ModifiedPaths. + /// True if the operation succeeded, false on failure. + public bool AddResetDiffToModifiedPaths(string targetCommit, out int addedCount) + { + addedCount = 0; + + if (string.IsNullOrEmpty(targetCommit)) + { + // Resetting to HEAD is a no-op for the index — nothing to diff. + return true; + } + + GitProcess gitProcess = new GitProcess(this.context.Enlistment); + + GitProcess.Result result = gitProcess.DiffTreeNameOnly("HEAD", targetCommit); + if (result.ExitCodeIsSuccess && !string.IsNullOrEmpty(result.Output)) + { + // Output is null-separated paths. The first entry from diff-tree is + // the target commit SHA (when given a commit, not a tree), skip it. + string[] parts = result.Output.Split(new[] { '\0' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string gitPath in parts) + { + // diff-tree with a commit (not tree) prefixes output with the + // commit SHA on the first line. Skip anything that looks like a + // hex SHA (40+ chars, no path separators). + if (gitPath.Length >= 40 && !gitPath.Contains('/') && !gitPath.Contains('\\')) + { + continue; + } + + string platformPath = gitPath.Replace(GVFSConstants.GitPathSeparator, Path.DirectorySeparatorChar); + if (this.modifiedPaths.TryAdd(platformPath, isFolder: false, isRetryable: out _)) + { + addedCount++; + } + } + } + else if (!result.ExitCodeIsSuccess) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("ExitCode", result.ExitCode); + metadata.Add("Errors", result.Errors ?? string.Empty); + metadata.Add("targetCommit", targetCommit); + this.context.Tracer.RelatedError( + metadata, + nameof(this.AddResetDiffToModifiedPaths) + ": git diff-tree failed"); + return false; + } + + return true; + } + /// /// Writes the staged (index) versions of files to the working directory as /// full files, bypassing ProjFS. Uses "git checkout-index --force" with