From c220b09257d6682d4d1aab8f5794f51bef347833 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 27 Jun 2025 10:42:22 -0700 Subject: [PATCH 01/16] Download all trees for non-prefetched commits when root tree is downloaded --- GVFS/GVFS.Common/Git/GitRepo.cs | 11 +++--- GVFS/GVFS.Common/Git/LibGit2Repo.cs | 17 +++------ GVFS/GVFS.Mount/InProcessMount.cs | 36 ++++++++++++++++--- .../Mock/Git/MockLibGit2Repo.cs | 3 +- GVFS/GVFS/CommandLine/GVFSVerb.cs | 2 +- 5 files changed, 47 insertions(+), 22 deletions(-) diff --git a/GVFS/GVFS.Common/Git/GitRepo.cs b/GVFS/GVFS.Common/Git/GitRepo.cs index 4aa4b945a7..ee9d8b96d2 100644 --- a/GVFS/GVFS.Common/Git/GitRepo.cs +++ b/GVFS/GVFS.Common/Git/GitRepo.cs @@ -4,6 +4,7 @@ using System.IO; using System.IO.Compression; using System.Linq; +using static GVFS.Common.Git.LibGit2Repo; namespace GVFS.Common.Git { @@ -60,9 +61,9 @@ public void OpenRepo() this.libgit2RepoInvoker?.InitializeSharedRepo(); } - public bool TryGetIsBlob(string sha, out bool isBlob) + public bool TryGetObjectType(string sha, out Native.ObjectTypes? objectType) { - return this.libgit2RepoInvoker.TryInvoke(repo => repo.IsBlob(sha), out isBlob); + return this.libgit2RepoInvoker.TryInvoke(repo => repo.GetObjectType(sha), out objectType); } public virtual bool TryCopyBlobContentStream(string blobSha, Action writeAction) @@ -86,10 +87,12 @@ public virtual bool TryCopyBlobContentStream(string blobSha, Action repo.CommitAndRootTreeExists(commitSha), out output); + string treeShaLocal = null; + this.libgit2RepoInvoker.TryInvoke(repo => repo.CommitAndRootTreeExists(commitSha, out treeShaLocal), out output); + rootTreeSha = treeShaLocal; return output; } diff --git a/GVFS/GVFS.Common/Git/LibGit2Repo.cs b/GVFS/GVFS.Common/Git/LibGit2Repo.cs index c1056def32..0849dd6b7f 100644 --- a/GVFS/GVFS.Common/Git/LibGit2Repo.cs +++ b/GVFS/GVFS.Common/Git/LibGit2Repo.cs @@ -41,24 +41,17 @@ protected LibGit2Repo() protected ITracer Tracer { get; } protected IntPtr RepoHandle { get; private set; } - public bool IsBlob(string sha) + public Native.ObjectTypes? GetObjectType(string sha) { IntPtr objHandle; if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.SuccessCode) { - return false; + return null; } try { - switch (Native.Object.GetType(objHandle)) - { - case Native.ObjectTypes.Blob: - return true; - - default: - return false; - } + return Native.Object.GetType(objHandle); } finally { @@ -91,9 +84,9 @@ public virtual string GetTreeSha(string commitish) return null; } - public virtual bool CommitAndRootTreeExists(string commitish) + public virtual bool CommitAndRootTreeExists(string commitish, out string treeSha) { - string treeSha = this.GetTreeSha(commitish); + treeSha = this.GetTreeSha(commitish); if (treeSha == null) { return false; diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index a6f9710a86..79fa29e9e7 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -16,6 +16,7 @@ using System.IO; using System.Text; using System.Threading; +using static GVFS.Common.Git.LibGit2Repo; namespace GVFS.Mount { @@ -44,6 +45,8 @@ public class InProcessMount private HeartbeatThread heartbeat; private ManualResetEvent unmountEvent; + private readonly Dictionary treesWithRecentlyDownloadedCommits = new Dictionary(); + // True if InProcessMount is calling git reset as part of processing // a folder dehydrate request private volatile bool resetForDehydrateInProgress; @@ -504,7 +507,21 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name else { Stopwatch downloadTime = Stopwatch.StartNew(); - if (this.gitObjects.TryDownloadAndSaveObject(objectSha, GVFSGitObjects.RequestSource.NamedPipeMessage) == GitObjects.DownloadAndSaveObjectResult.Success) + + /* If this is the root tree for a commit that was was just downloaded, assume that more + * trees will be needed soon and download them as well by using the download commit API. + * + * Otherwise, or as a fallback if the commit download fails, download the object directly. + */ + if (this.treesWithRecentlyDownloadedCommits.TryGetValue(objectSha, out string commitSha) + && this.gitObjects.TryDownloadCommit(commitSha)) + { + this.treesWithRecentlyDownloadedCommits.Remove(objectSha); + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult); + // FUTURE: Should the stats be updated to reflect all the trees in the pack? + // FUTURE: Should we try to clean up duplicate trees or increase depth of the commit download? + } + else if (this.gitObjects.TryDownloadAndSaveObject(objectSha, GVFSGitObjects.RequestSource.NamedPipeMessage) == GitObjects.DownloadAndSaveObjectResult.Success) { response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult); } @@ -513,9 +530,20 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.DownloadFailed); } - bool isBlob; - this.context.Repository.TryGetIsBlob(objectSha, out isBlob); - this.context.Repository.GVFSLock.Stats.RecordObjectDownload(isBlob, downloadTime.ElapsedMilliseconds); + Native.ObjectTypes? objectType; + this.context.Repository.TryGetObjectType(objectSha, out objectType); + this.context.Repository.GVFSLock.Stats.RecordObjectDownload(objectType == Native.ObjectTypes.Blob, downloadTime.ElapsedMilliseconds); + + if (objectType == Native.ObjectTypes.Commit + && !this.context.Repository.CommitAndRootTreeExists(objectSha, out var treeSha) + && !string.IsNullOrEmpty(treeSha)) + { + /* If a commit is downloaded but its tree doesn't exist, it means the commit hadn't been prefetched and all its trees + * may be needed soon depending on the context. e.g. git log (without a pathspec) doesn't need trees, but git checkout does. + * save the tree/commit so if the tree is requested soon we can download all the trees for the commit in a batch. + */ + this.treesWithRecentlyDownloadedCommits[treeSha] = objectSha; + } } } diff --git a/GVFS/GVFS.UnitTests/Mock/Git/MockLibGit2Repo.cs b/GVFS/GVFS.UnitTests/Mock/Git/MockLibGit2Repo.cs index c2494ba3f9..9901995617 100644 --- a/GVFS/GVFS.UnitTests/Mock/Git/MockLibGit2Repo.cs +++ b/GVFS/GVFS.UnitTests/Mock/Git/MockLibGit2Repo.cs @@ -12,8 +12,9 @@ public MockLibGit2Repo(ITracer tracer) { } - public override bool CommitAndRootTreeExists(string commitish) + public override bool CommitAndRootTreeExists(string commitish, out string treeSha) { + treeSha = string.Empty; return false; } diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index cdf9e920e6..a9fcb95876 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -663,7 +663,7 @@ protected bool TryDownloadCommit( out string error, bool checkLocalObjectCache = true) { - if (!checkLocalObjectCache || !repo.CommitAndRootTreeExists(commitId)) + if (!checkLocalObjectCache || !repo.CommitAndRootTreeExists(commitId, out _)) { if (!gitObjects.TryDownloadCommit(commitId)) { From fb77a4759f1f6ba519e5a45cdbfac255b1412aeb Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 27 Jun 2025 14:01:34 -0700 Subject: [PATCH 02/16] Prefetch in background when cloning if pack indexes are not trusted --- GVFS/GVFS.Common/Enlistment.cs | 13 +++++++++ GVFS/GVFS.Common/Git/GitObjects.cs | 7 +---- GVFS/GVFS/CommandLine/CloneVerb.cs | 44 ++++++++++++++++++++++-------- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/GVFS/GVFS.Common/Enlistment.cs b/GVFS/GVFS.Common/Enlistment.cs index 3f061257fb..090a81c9e4 100644 --- a/GVFS/GVFS.Common/Enlistment.cs +++ b/GVFS/GVFS.Common/Enlistment.cs @@ -109,5 +109,18 @@ public virtual GitProcess CreateGitProcess() { return new GitProcess(this); } + + public bool GetTrustPackIndexesConfig() + { + var gitProcess = this.CreateGitProcess(); + bool trustPackIndexes = true; + if (gitProcess.TryGetFromConfig(GVFSConstants.GitConfig.TrustPackIndexes, forceOutsideEnlistment: false, out var valueString) + && bool.TryParse(valueString, out var trustPackIndexesConfig)) + { + trustPackIndexes = trustPackIndexesConfig; + } + + return trustPackIndexes; + } } } diff --git a/GVFS/GVFS.Common/Git/GitObjects.cs b/GVFS/GVFS.Common/Git/GitObjects.cs index 6614bd5734..e21d4a1d1c 100644 --- a/GVFS/GVFS.Common/Git/GitObjects.cs +++ b/GVFS/GVFS.Common/Git/GitObjects.cs @@ -135,12 +135,7 @@ public virtual bool TryDownloadPrefetchPacks(GitProcess gitProcess, long latestT * pack file and an index file that do not match. * Eventually we will make this the default, but it has a high performance cost for the first prefetch after * cloning a large repository, so it must be explicitly enabled for now. */ - bool trustPackIndexes = true; - if (gitProcess.TryGetFromConfig(GVFSConstants.GitConfig.TrustPackIndexes, forceOutsideEnlistment: false, out var valueString) - && bool.TryParse(valueString, out var trustPackIndexesConfig)) - { - trustPackIndexes = trustPackIndexesConfig; - } + bool trustPackIndexes = this.Enlistment.GetTrustPackIndexesConfig(); metadata.Add("trustPackIndexes", trustPackIndexes); long requestId = HttpRequestor.GetNewRequestId(); diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index 47d7d3f978..e810308377 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -215,20 +215,40 @@ public override void Execute() { if (!this.NoPrefetch) { - ReturnCode result = this.Execute( - enlistment, - verb => + bool trustPackIndexes = enlistment.GetTrustPackIndexesConfig(); + /* If pack indexes are not trusted, the prefetch can take a long time. + * We will run the prefetch command in the background. + */ + if (trustPackIndexes) + { + ReturnCode result = this.Execute( + enlistment, + verb => + { + verb.Commits = true; + verb.SkipVersionCheck = true; + verb.ResolvedCacheServer = cacheServer; + verb.ServerGVFSConfig = serverGVFSConfig; + }); + + if (result != ReturnCode.Success) { - verb.Commits = true; - verb.SkipVersionCheck = true; - verb.ResolvedCacheServer = cacheServer; - verb.ServerGVFSConfig = serverGVFSConfig; - }); + this.Output.WriteLine("\r\nError during prefetch @ {0}", fullEnlistmentRootPathParameter); + exitCode = (int)result; + } + } - if (result != ReturnCode.Success) + else { - this.Output.WriteLine("\r\nError during prefetch @ {0}", fullEnlistmentRootPathParameter); - exitCode = (int)result; + Process.Start(new ProcessStartInfo( + fileName: "gvfs", + arguments: "prefetch --commits") + { + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden, + WorkingDirectory = enlistment.EnlistmentRoot + }); + this.Output.WriteLine("\r\nPrefetch of commit graph has been started as a background process. Git operations involving history may be slower until prefetch has completed.\r\n"); } } @@ -247,7 +267,7 @@ public override void Execute() verb.SkipVersionCheck = true; verb.ResolvedCacheServer = cacheServer; verb.DownloadedGVFSConfig = serverGVFSConfig; - }); + }); } } else From 8ac9ad03b73fa1ea9a4ae669c3ab5770b017dfa4 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Thu, 10 Jul 2025 11:04:03 -0700 Subject: [PATCH 03/16] Revise commit batch - limit to once per 5 minutes - disable if any prefetch of commit graph has already succeeded --- GVFS/GVFS.Mount/InProcessMount.cs | 49 ++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index 79fa29e9e7..a9dd95b708 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using System.Threading; using static GVFS.Common.Git.LibGit2Repo; @@ -45,7 +46,8 @@ public class InProcessMount private HeartbeatThread heartbeat; private ManualResetEvent unmountEvent; - private readonly Dictionary treesWithRecentlyDownloadedCommits = new Dictionary(); + private readonly Dictionary treesWithDownloadedCommits = new Dictionary(); + private DateTime lastCommitPackDownloadTime = DateTime.MinValue; // True if InProcessMount is calling git reset as part of processing // a folder dehydrate request @@ -513,10 +515,10 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name * * Otherwise, or as a fallback if the commit download fails, download the object directly. */ - if (this.treesWithRecentlyDownloadedCommits.TryGetValue(objectSha, out string commitSha) + if (this.ShouldDownloadCommitPack(objectSha, out string commitSha) && this.gitObjects.TryDownloadCommit(commitSha)) { - this.treesWithRecentlyDownloadedCommits.Remove(objectSha); + this.DownloadedCommitPack(objectSha: objectSha, commitSha: commitSha); response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult); // FUTURE: Should the stats be updated to reflect all the trees in the pack? // FUTURE: Should we try to clean up duplicate trees or increase depth of the commit download? @@ -530,19 +532,25 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.DownloadFailed); } + Native.ObjectTypes? objectType; this.context.Repository.TryGetObjectType(objectSha, out objectType); this.context.Repository.GVFSLock.Stats.RecordObjectDownload(objectType == Native.ObjectTypes.Blob, downloadTime.ElapsedMilliseconds); if (objectType == Native.ObjectTypes.Commit + && !this.PrefetchHasBeenDone() && !this.context.Repository.CommitAndRootTreeExists(objectSha, out var treeSha) && !string.IsNullOrEmpty(treeSha)) { - /* If a commit is downloaded but its tree doesn't exist, it means the commit hadn't been prefetched and all its trees - * may be needed soon depending on the context. e.g. git log (without a pathspec) doesn't need trees, but git checkout does. - * save the tree/commit so if the tree is requested soon we can download all the trees for the commit in a batch. + /* If a commit is downloaded, it wasn't prefetched. + * If any prefetch has been done, there is probably a commit in the prefetch packs that is close enough that + * loose object download of missing trees will be faster than downloading a pack of all the trees for the commit. + * Otherwise, the trees for the commit may be needed soon depending on the context. + * e.g. git log (without a pathspec) doesn't need trees, but git checkout does. + * + * Save the tree/commit so if the tree is requested soon we can download all the trees for the commit in a batch. */ - this.treesWithRecentlyDownloadedCommits[treeSha] = objectSha; + this.treesWithDownloadedCommits[treeSha] = objectSha; } } } @@ -550,6 +558,33 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name connection.TrySendResponse(response.CreateMessage()); } + private bool PrefetchHasBeenDone() + { + var prefetchPacks = this.gitObjects.ReadPackFileNames(this.enlistment.GitPackRoot, GVFSConstants.PrefetchPackPrefix); + return prefetchPacks.Length > 0; + } + + private bool ShouldDownloadCommitPack(string objectSha, out string commitSha) + { + + if (!this.treesWithDownloadedCommits.TryGetValue(objectSha, out commitSha) + || this.PrefetchHasBeenDone()) + { + return false; + } + + /* This is a heuristic to prevent downloading multiple packs related to git history commands, + * since commits downloaded close together likely have similar trees. */ + var timePassed = DateTime.UtcNow - this.lastCommitPackDownloadTime; + return (timePassed > TimeSpan.FromMinutes(5)); + } + + private void DownloadedCommitPack(string objectSha, string commitSha) + { + this.lastCommitPackDownloadTime = DateTime.UtcNow; + this.treesWithDownloadedCommits.Remove(objectSha); + } + private void HandlePostFetchJobRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection) { NamedPipeMessages.RunPostFetchJob.Request request = new NamedPipeMessages.RunPostFetchJob.Request(message); From 240f73ee5b987fa4e400251229582b64b3924d56 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 23 Jul 2025 11:08:08 -0700 Subject: [PATCH 04/16] Update to windows-2025, upgrade to .NET 4.7.1 window-2019 agent has been deprecated and is no longer available. The newer agents don't include support for .NET Framework 4.6.1 or the Windows SDK that the native projects targeted. I tried some workarounds to install support for them as part of the action YML, but failed. Newer versions of .NET Framework are available on the agent images, but 4.7.2 and later start failing one of the functional tests. So I've updated the projects to target .NET Framework 4.7.1 and the latest Windows SDK. --- .github/workflows/build.yaml | 4 ++-- GVFS/FastFetch/FastFetch.csproj | 2 +- GVFS/GVFS.Common/GVFS.Common.csproj | 3 ++- .../GVFS.FunctionalTests.LockHolder.csproj | 2 +- GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj | 2 +- GVFS/GVFS.GVFlt/GVFS.GVFlt.csproj | 2 +- GVFS/GVFS.Hooks/GVFS.Hooks.csproj | 2 +- GVFS/GVFS.Installers/GVFS.Installers.csproj | 2 +- GVFS/GVFS.Mount/GVFS.Mount.csproj | 2 +- GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj | 6 +++--- GVFS/GVFS.Payload/GVFS.Payload.csproj | 2 +- GVFS/GVFS.Payload/layout.bat | 2 +- GVFS/GVFS.PerfProfiling/GVFS.PerfProfiling.csproj | 2 +- GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj | 2 +- .../GVFS.PostIndexChangedHook.vcxproj | 6 +++--- GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj | 6 +++--- GVFS/GVFS.Service.UI/GVFS.Service.UI.csproj | 2 +- GVFS/GVFS.Service/GVFS.Service.csproj | 2 +- GVFS/GVFS.Tests/GVFS.Tests.csproj | 2 +- GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj | 7 ++++++- .../GVFS.VirtualFileSystemHook.vcxproj | 6 +++--- GVFS/GVFS.Virtualization/GVFS.Virtualization.csproj | 2 +- GVFS/GVFS/GVFS.csproj | 2 +- GVFS/GitHooksLoader/GitHooksLoader.vcxproj | 6 +++--- scripts/CreateBuildArtifacts.bat | 4 ++-- scripts/RunFunctionalTests.bat | 2 +- scripts/RunUnitTests.bat | 2 +- 27 files changed, 45 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2dc811ea47..f3e650f20b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: windows-2019 + runs-on: windows-2025 name: Build and Unit Test strategy: @@ -66,7 +66,7 @@ jobs: path: artifacts\NuGetPackages functional_test: - runs-on: windows-2019 + runs-on: windows-2025 name: Functional Tests needs: build diff --git a/GVFS/FastFetch/FastFetch.csproj b/GVFS/FastFetch/FastFetch.csproj index 1a2bdaae99..df25afb5b9 100644 --- a/GVFS/FastFetch/FastFetch.csproj +++ b/GVFS/FastFetch/FastFetch.csproj @@ -2,7 +2,7 @@ Exe - net461 + net471 x64 true diff --git a/GVFS/GVFS.Common/GVFS.Common.csproj b/GVFS/GVFS.Common/GVFS.Common.csproj index e19e8ee2ec..daacd2ca22 100644 --- a/GVFS/GVFS.Common/GVFS.Common.csproj +++ b/GVFS/GVFS.Common/GVFS.Common.csproj @@ -1,7 +1,7 @@  - net461 + net471 true @@ -13,6 +13,7 @@ + diff --git a/GVFS/GVFS.FunctionalTests.LockHolder/GVFS.FunctionalTests.LockHolder.csproj b/GVFS/GVFS.FunctionalTests.LockHolder/GVFS.FunctionalTests.LockHolder.csproj index 290a12b469..bb03a41716 100644 --- a/GVFS/GVFS.FunctionalTests.LockHolder/GVFS.FunctionalTests.LockHolder.csproj +++ b/GVFS/GVFS.FunctionalTests.LockHolder/GVFS.FunctionalTests.LockHolder.csproj @@ -1,7 +1,7 @@  - net461 + net471 Exe diff --git a/GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj b/GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj index 0a64e74400..f170451f4e 100644 --- a/GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj +++ b/GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj @@ -1,7 +1,7 @@  - net461 + net471 Exe diff --git a/GVFS/GVFS.GVFlt/GVFS.GVFlt.csproj b/GVFS/GVFS.GVFlt/GVFS.GVFlt.csproj index a436520f9d..97e2973e30 100644 --- a/GVFS/GVFS.GVFlt/GVFS.GVFlt.csproj +++ b/GVFS/GVFS.GVFlt/GVFS.GVFlt.csproj @@ -1,7 +1,7 @@ - net461 + net471 diff --git a/GVFS/GVFS.Hooks/GVFS.Hooks.csproj b/GVFS/GVFS.Hooks/GVFS.Hooks.csproj index efe7842a7d..23dd6ea97a 100644 --- a/GVFS/GVFS.Hooks/GVFS.Hooks.csproj +++ b/GVFS/GVFS.Hooks/GVFS.Hooks.csproj @@ -2,7 +2,7 @@ Exe - net461 + net471 diff --git a/GVFS/GVFS.Installers/GVFS.Installers.csproj b/GVFS/GVFS.Installers/GVFS.Installers.csproj index bf0c5ec7f0..13cf3e6018 100644 --- a/GVFS/GVFS.Installers/GVFS.Installers.csproj +++ b/GVFS/GVFS.Installers/GVFS.Installers.csproj @@ -1,7 +1,7 @@ - net461 + net471 false $(RepoOutPath)GVFS.Payload\bin\$(Configuration)\win-x64\ diff --git a/GVFS/GVFS.Mount/GVFS.Mount.csproj b/GVFS/GVFS.Mount/GVFS.Mount.csproj index 6e41c00081..83d89be63e 100644 --- a/GVFS/GVFS.Mount/GVFS.Mount.csproj +++ b/GVFS/GVFS.Mount/GVFS.Mount.csproj @@ -2,7 +2,7 @@ Exe - net461 + net471 diff --git a/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj b/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj index 87352d6112..ae216cfaef 100644 --- a/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj +++ b/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj @@ -17,19 +17,19 @@ {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5} Win32Proj GVFSNativeTests - 10.0.16299.0 + 10.0 DynamicLibrary true - v142 + v143 NotSet DynamicLibrary false - v142 + v143 true NotSet diff --git a/GVFS/GVFS.Payload/GVFS.Payload.csproj b/GVFS/GVFS.Payload/GVFS.Payload.csproj index 1df5a32108..1311bc87d6 100644 --- a/GVFS/GVFS.Payload/GVFS.Payload.csproj +++ b/GVFS/GVFS.Payload/GVFS.Payload.csproj @@ -1,7 +1,7 @@  - net461 + net471 false diff --git a/GVFS/GVFS.Payload/layout.bat b/GVFS/GVFS.Payload/layout.bat index de1c0df81d..ebdae19c22 100644 --- a/GVFS/GVFS.Payload/layout.bat +++ b/GVFS/GVFS.Payload/layout.bat @@ -39,7 +39,7 @@ SET OUTPUT=%5 SET ROOT=%~dp0..\.. SET BUILD_OUT="%ROOT%\..\out" -SET MANAGED_OUT_FRAGMENT=bin\%CONFIGURATION%\net461\win-x64 +SET MANAGED_OUT_FRAGMENT=bin\%CONFIGURATION%\net471\win-x64 SET NATIVE_OUT_FRAGMENT=bin\x64\%CONFIGURATION% ECHO Copying files... diff --git a/GVFS/GVFS.PerfProfiling/GVFS.PerfProfiling.csproj b/GVFS/GVFS.PerfProfiling/GVFS.PerfProfiling.csproj index de092f981c..bf3ce68509 100644 --- a/GVFS/GVFS.PerfProfiling/GVFS.PerfProfiling.csproj +++ b/GVFS/GVFS.PerfProfiling/GVFS.PerfProfiling.csproj @@ -2,7 +2,7 @@ Exe - net461 + net471 diff --git a/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj b/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj index 5cbd7e1748..f8fb565972 100644 --- a/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj +++ b/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj @@ -1,7 +1,7 @@ - net461 + net471 diff --git a/GVFS/GVFS.PostIndexChangedHook/GVFS.PostIndexChangedHook.vcxproj b/GVFS/GVFS.PostIndexChangedHook/GVFS.PostIndexChangedHook.vcxproj index 0537dfaf1e..3808ff0277 100644 --- a/GVFS/GVFS.PostIndexChangedHook/GVFS.PostIndexChangedHook.vcxproj +++ b/GVFS/GVFS.PostIndexChangedHook/GVFS.PostIndexChangedHook.vcxproj @@ -14,7 +14,7 @@ {24D161E9-D1F0-4299-BBD3-5D940BEDD535} Win32Proj GVFSPostIndexChangedHook - 10.0.16299.0 + 10.0 GVFS.PostIndexChangedHook GVFS.PostIndexChangedHook @@ -22,13 +22,13 @@ Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte diff --git a/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj b/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj index 613e9d9a7c..09e9e6616e 100644 --- a/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj +++ b/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj @@ -14,7 +14,7 @@ {5A6656D5-81C7-472C-9DC8-32D071CB2258} Win32Proj readobject - 10.0.16299.0 + 10.0 GVFS.ReadObjectHook GVFS.ReadObjectHook @@ -22,13 +22,13 @@ Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte diff --git a/GVFS/GVFS.Service.UI/GVFS.Service.UI.csproj b/GVFS/GVFS.Service.UI/GVFS.Service.UI.csproj index 90589d9330..48e5c16050 100644 --- a/GVFS/GVFS.Service.UI/GVFS.Service.UI.csproj +++ b/GVFS/GVFS.Service.UI/GVFS.Service.UI.csproj @@ -2,7 +2,7 @@ Exe - net461 + net471 diff --git a/GVFS/GVFS.Service/GVFS.Service.csproj b/GVFS/GVFS.Service/GVFS.Service.csproj index 8211413262..c24eb6505c 100644 --- a/GVFS/GVFS.Service/GVFS.Service.csproj +++ b/GVFS/GVFS.Service/GVFS.Service.csproj @@ -2,7 +2,7 @@ Exe - net461 + net471 true diff --git a/GVFS/GVFS.Tests/GVFS.Tests.csproj b/GVFS/GVFS.Tests/GVFS.Tests.csproj index c02954c262..c8c173ebfa 100644 --- a/GVFS/GVFS.Tests/GVFS.Tests.csproj +++ b/GVFS/GVFS.Tests/GVFS.Tests.csproj @@ -1,7 +1,7 @@ - net461 + net471 diff --git a/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj b/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj index fe7648aacd..890714857b 100644 --- a/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj +++ b/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj @@ -1,7 +1,7 @@  - net461 + net471 Exe true @@ -21,6 +21,11 @@ + + + + + diff --git a/GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.vcxproj b/GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.vcxproj index 8fb0cdb176..9390120db3 100644 --- a/GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.vcxproj +++ b/GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.vcxproj @@ -14,7 +14,7 @@ {2D23AB54-541F-4ABC-8DCA-08C199E97ABB} Win32Proj GVFSVirtualFileSystemHook - 10.0.16299.0 + 10.0 GVFS.VirtualFileSystemHook GVFS.VirtualFileSystemHook @@ -22,13 +22,13 @@ Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte diff --git a/GVFS/GVFS.Virtualization/GVFS.Virtualization.csproj b/GVFS/GVFS.Virtualization/GVFS.Virtualization.csproj index bc5fa77f87..91772d2692 100644 --- a/GVFS/GVFS.Virtualization/GVFS.Virtualization.csproj +++ b/GVFS/GVFS.Virtualization/GVFS.Virtualization.csproj @@ -1,7 +1,7 @@ - net461 + net471 true diff --git a/GVFS/GVFS/GVFS.csproj b/GVFS/GVFS/GVFS.csproj index 6d9d579f9c..892bc13867 100644 --- a/GVFS/GVFS/GVFS.csproj +++ b/GVFS/GVFS/GVFS.csproj @@ -2,7 +2,7 @@ Exe - net461 + net471 diff --git a/GVFS/GitHooksLoader/GitHooksLoader.vcxproj b/GVFS/GitHooksLoader/GitHooksLoader.vcxproj index 78a51bf7f8..0c4d4fbdfc 100644 --- a/GVFS/GitHooksLoader/GitHooksLoader.vcxproj +++ b/GVFS/GitHooksLoader/GitHooksLoader.vcxproj @@ -14,19 +14,19 @@ {798DE293-6EDA-4DC4-9395-BE7A71C563E3} Win32Proj GitHooksLoader - 10.0.16299.0 + 10.0 Application true - v142 + v143 Unicode Application false - v142 + v143 true Unicode diff --git a/scripts/CreateBuildArtifacts.bat b/scripts/CreateBuildArtifacts.bat index a7ba9795e5..5cca08d5a5 100644 --- a/scripts/CreateBuildArtifacts.bat +++ b/scripts/CreateBuildArtifacts.bat @@ -49,7 +49,7 @@ ECHO ^************************ ECHO Collecting FastFetch... mkdir %OUTROOT%\FastFetch xcopy /S /Y ^ - %VFS_OUTDIR%\FastFetch\bin\%CONFIGURATION%\net461\win-x64 ^ + %VFS_OUTDIR%\FastFetch\bin\%CONFIGURATION%\net471\win-x64 ^ %OUTROOT%\FastFetch\ || GOTO ERROR ECHO ^*********************************** @@ -57,7 +57,7 @@ ECHO ^* Collecting GVFS.FunctionalTests * ECHO ^*********************************** mkdir %OUTROOT%\GVFS.FunctionalTests xcopy /S /Y ^ - %VFS_OUTDIR%\GVFS.FunctionalTests\bin\%CONFIGURATION%\net461\win-x64 ^ + %VFS_OUTDIR%\GVFS.FunctionalTests\bin\%CONFIGURATION%\net471\win-x64 ^ %OUTROOT%\GVFS.FunctionalTests\ || GOTO ERROR ECHO ^************************************* diff --git a/scripts/RunFunctionalTests.bat b/scripts/RunFunctionalTests.bat index 80ab7e1e4c..ef86a74f89 100644 --- a/scripts/RunFunctionalTests.bat +++ b/scripts/RunFunctionalTests.bat @@ -27,7 +27,7 @@ IF NOT %ERRORLEVEL% == 0 ( ECHO error: unable to locate Git on the PATH (has it been installed?) ) -%VFS_OUTDIR%\GVFS.FunctionalTests\bin\%CONFIGURATION%\net461\win-x64\GVFS.FunctionalTests.exe /result:TestResult.xml %2 %3 %4 %5 +%VFS_OUTDIR%\GVFS.FunctionalTests\bin\%CONFIGURATION%\net471\win-x64\GVFS.FunctionalTests.exe /result:TestResult.xml %2 %3 %4 %5 SET error=%ERRORLEVEL% CALL %VFS_SCRIPTSDIR%\StopAllServices.bat diff --git a/scripts/RunUnitTests.bat b/scripts/RunUnitTests.bat index f9f6845806..3424825cae 100644 --- a/scripts/RunUnitTests.bat +++ b/scripts/RunUnitTests.bat @@ -5,6 +5,6 @@ IF "%1"=="" (SET "CONFIGURATION=Debug") ELSE (SET "CONFIGURATION=%1") SET RESULT=0 -%VFS_OUTDIR%\GVFS.UnitTests\bin\%CONFIGURATION%\net461\win-x64\GVFS.UnitTests.exe || SET RESULT=1 +%VFS_OUTDIR%\GVFS.UnitTests\bin\%CONFIGURATION%\net471\win-x64\GVFS.UnitTests.exe || SET RESULT=1 EXIT /b %RESULT% From d6f30efdd6f7b8cc18665dfffa317064384d1ff8 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Tue, 12 Aug 2025 11:01:37 -0700 Subject: [PATCH 05/16] Make expected versions consistent --- .azure-pipelines/release.yml | 2 +- .github/workflows/build.yaml | 2 +- .vsconfig | 13 +++++++------ Readme.md | 14 ++++---------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index d098c5a70f..f754ce9d1f 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -28,7 +28,7 @@ jobs: displayName: Install .NET SDK inputs: packageType: sdk - version: 5.0.201 + version: 8.0.413 - task: CmdLine@2 displayName: Build VFS for Git diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f3e650f20b..92e9ab6756 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -24,7 +24,7 @@ jobs: - name: Install .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 5.0.201 + dotnet-version: 8.0.413 - name: Add MSBuild to PATH uses: microsoft/setup-msbuild@v2.0.0 diff --git a/.vsconfig b/.vsconfig index e91f31e691..095606afa1 100644 --- a/.vsconfig +++ b/.vsconfig @@ -2,12 +2,13 @@ "version": "1.0", "components": [ "Microsoft.Component.MSBuild", - "Microsoft.VisualStudio.Workload.NativeDesktop" + "Microsoft.Net.Component.4.7.1.TargetingPack", + "Microsoft.Net.Component.4.7.1.SDK", + "Microsoft.Net.Core.Component.SDK.8.0", + "Microsoft.VisualStudio.Component.VC.v143.x86.x64", + "Microsoft.VisualStudio.Component.Windows11SDK.26100", + "Microsoft.VisualStudio.Workload.NativeDesktop", "Microsoft.VisualStudio.Workload.ManagedDesktop", - "Microsoft.VisualStudio.Workload.NetCoreTools", - "Microsoft.Net.Core.Component.SDK.2.1", - "Microsoft.VisualStudio.Component.VC.v141.x86.x64", - "Microsoft.Net.Component.4.6.1.TargetingPack", - "Microsoft.Net.Component.4.6.1.SDK", + "Microsoft.VisualStudio.Workload.NetCoreTools" ] } \ No newline at end of file diff --git a/Readme.md b/Readme.md index 2ee0517b93..e388634e5f 100644 --- a/Readme.md +++ b/Readme.md @@ -42,15 +42,15 @@ will notify you when new versions are available. ## Building VFS for Git If you'd like to build your own VFS for Git Windows installer: -* Install Visual Studio 2017 Community Edition or higher (https://www.visualstudio.com/downloads/). +* Install Visual Studio 2022 Community Edition or higher (https://www.visualstudio.com/downloads/). * Include the following workloads: * .NET desktop development * Desktop development with C++ * .NET Core cross-platform development * Include the following additional components: * .NET Core runtime - * Windows 10 SDK (10.0.10240.0) -* Install the .NET Core 2.1 SDK (https://www.microsoft.com/net/download/dotnet-core/2.1) + * Windows 10 or 11 SDK (10.0+) +* Install the .NET Core 8 SDK (https://www.microsoft.com/net/download/dotnet-core/8) * Install [`nuget.exe`](https://www.nuget.org/downloads) * Create a folder to clone into, e.g. `C:\Repos\VFSForGit` * Clone this repo into the `src` subfolder, e.g. `C:\Repos\VFSForGit\src` @@ -59,13 +59,7 @@ If you'd like to build your own VFS for Git Windows installer: build will fail, and the second and subsequent builds will succeed. This is because the build requires a prebuild code generation step. For details, see the build script in the previous step. -You can also use Visual Studio 2019. There are a couple of options for getting all the dependencies. -* You can install Visual Studio 2017 side by side with Visual Studio 2019, and make sure that you have all the dependencies from Visual Studio 2017 installed -* Alternatively, if you only want to have Visual Studio 2019 installed, install the following extra dependencies: - * MSVC v141 VS 2017 C++ build tools via the optional components in the Visual Studio 2019 installer. It is under the "Desktop Development with C++" heading. - * Windows 10 SDK (10.0.10240.0) via the archived SDK page: https://developer.microsoft.com/en-us/windows/downloads/sdk-archive - -Visual Studio 2019 will [automatically prompt you to install these dependencies](https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/) when you open the solution. The .vsconfig file that is present in the root of the repository specifies all required components _except_ the Windows 10 SDK (10.0.10240.0) as this component is no longer shipped with VS2019 - **you'll still need to install that separately**. +Visual Studio 2022 will [automatically prompt you to install these dependencies](https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/) when you open the solution. The .vsconfig file that is present in the root of the repository specifies all required components. The installer can now be found at `C:\Repos\VFSForGit\BuildOutput\GVFS.Installer.Windows\bin\x64\[Debug|Release]\SetupGVFS..exe` From 170745b8b87fbf36232b49e212521094befd15e1 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 7 Aug 2025 12:11:41 +0100 Subject: [PATCH 06/16] GitVersion: support release candidate versions Add support for parsing Git versions that include a release candidate 'tag' such as 2.51.0-rc0, which will have VFS version strings like: "2.51.0-rc0.vfs.0.0". Signed-off-by: Matthew John Cheetham --- GVFS/GVFS.Common/Git/GitVersion.cs | 73 ++++++++++++++++--- GVFS/GVFS.UnitTests/Common/GitVersionTests.cs | 57 +++++++++++++++ 2 files changed, 119 insertions(+), 11 deletions(-) diff --git a/GVFS/GVFS.Common/Git/GitVersion.cs b/GVFS/GVFS.Common/Git/GitVersion.cs index aecd02465a..8c3f0f053d 100644 --- a/GVFS/GVFS.Common/Git/GitVersion.cs +++ b/GVFS/GVFS.Common/Git/GitVersion.cs @@ -1,15 +1,18 @@ using System; -using System.Text; namespace GVFS.Common.Git { public class GitVersion { public GitVersion(int major, int minor, int build, string platform = null, int revision = 0, int minorRevision = 0) + : this(major, minor, build, null, platform, revision, minorRevision) { } + + public GitVersion(int major, int minor, int build, int? releaseCandidate = null, string platform = null, int revision = 0, int minorRevision = 0) { this.Major = major; this.Minor = minor; this.Build = build; + this.ReleaseCandidate = releaseCandidate; this.Platform = platform; this.Revision = revision; this.MinorRevision = minorRevision; @@ -18,6 +21,7 @@ public GitVersion(int major, int minor, int build, string platform = null, int r public int Major { get; private set; } public int Minor { get; private set; } public int Build { get; private set; } + public int? ReleaseCandidate { get; private set; } public string Platform { get; private set; } public int Revision { get; private set; } public int MinorRevision { get; private set; } @@ -62,6 +66,7 @@ public static bool TryParseVersion(string input, out GitVersion version) version = null; int major, minor, build, revision = 0, minorRevision = 0; + int? releaseCandidate = null; string platform = null; if (string.IsNullOrWhiteSpace(input)) @@ -73,10 +78,10 @@ public static bool TryParseVersion(string input, out GitVersion version) int numComponents = parsedComponents.Length; // We minimally accept the official Git version number format which - // consists of three components: "major.minor.build". + // consists of three components: "major.minor.build[.rcN]". // // The other supported formats are the Git for Windows and Microsoft Git - // formats which look like: "major.minor.build.platform.revision.minorRevision" + // formats which look like: "major.minor.build[.rcN].platform.revision.minorRevision" // 0 1 2 3 4 5 // len 1 2 3 4 5 6 // @@ -103,25 +108,54 @@ public static bool TryParseVersion(string input, out GitVersion version) return false; } - // Take the platform component verbatim + // Release candidate and/or platform + // Both of these are optional, but the release candidate is expected to be of the format 'rcN' + // where N is a number, helping us distinguish it from a platform string. + int platformIdx = 3; if (numComponents >= 4) { - platform = parsedComponents[3]; + string tag = parsedComponents[3]; + + // Release candidate 'rcN' + if (tag.StartsWith("rc", StringComparison.OrdinalIgnoreCase) && + tag.Length > 2 && int.TryParse(tag.Substring(2), out int rc) && rc >= 0) + { + releaseCandidate = rc; + + // The next component will now be the (optional) platform. + // Subsequent components will be revision and minor revision so we need to adjust + // the platform index to account for the release candidate. + platformIdx = 4; + if (numComponents >= 5) + { + platform = parsedComponents[4]; + } + } + else // Platform string only + { + platform = tag; + } } // Platform revision - if (numComponents < 5 || !TryParseComponent(parsedComponents[4], out revision)) + if (numComponents > platformIdx + 1) { - revision = 0; + if (!TryParseComponent(parsedComponents[platformIdx + 1], out revision)) + { + revision = 0; + } } // Minor platform revision - if (numComponents < 6 || !TryParseComponent(parsedComponents[5], out minorRevision)) + if (numComponents > platformIdx + 2) { - minorRevision = 0; + if (!TryParseComponent(parsedComponents[platformIdx + 2], out minorRevision)) + { + minorRevision = 0; + } } - version = new GitVersion(major, minor, build, platform, revision, minorRevision); + version = new GitVersion(major, minor, build, releaseCandidate, platform, revision, minorRevision); return true; } @@ -142,7 +176,12 @@ public bool IsLessThan(GitVersion other) public override string ToString() { - return string.Format("{0}.{1}.{2}.{3}.{4}.{5}", this.Major, this.Minor, this.Build, this.Platform, this.Revision, this.MinorRevision); + if (ReleaseCandidate is null) + { + return $"{Major}.{Minor}.{Build}.{Platform}.{Revision}.{MinorRevision}"; + } + + return $"{Major}.{Minor}.{Build}.rc{ReleaseCandidate}.{Platform}.{Revision}.{MinorRevision}"; } private static bool TryParseComponent(string component, out int parsedComponent) @@ -182,6 +221,18 @@ private int CompareVersionNumbers(GitVersion other) return this.Build.CompareTo(other.Build); } + if (this.ReleaseCandidate != other.ReleaseCandidate) + { + if (this.ReleaseCandidate.HasValue && other.ReleaseCandidate.HasValue) + { + return this.ReleaseCandidate.Value.CompareTo(other.ReleaseCandidate.Value); + } + + // If one version has a release candidate and the other does not, + // the one without a release candidate is considered "greater than" the one with. + return other.ReleaseCandidate.HasValue ? 1 : -1; + } + if (this.Revision != other.Revision) { return this.Revision.CompareTo(other.Revision); diff --git a/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs b/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs index a69bd8f7e5..54c47cca16 100644 --- a/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs +++ b/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs @@ -118,6 +118,46 @@ public void Compare_Version_Minor_Greater() version1.IsEqualTo(version2).ShouldEqual(false); } + [TestCase] + public void Compare_ReleaseCandidate_Less() + { + GitVersion version1 = new GitVersion(1, 2, 3, 1, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, 2, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_ReleaseCandidate_Greater() + { + GitVersion version1 = new GitVersion(1, 2, 3, 2, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, 1, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_ReleaseCandidate_NonRC_Less() + { + GitVersion version1 = new GitVersion(1, 2, 3, 0, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, null, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_ReleaseCandidate_NonRC_Greater() + { + GitVersion version1 = new GitVersion(1, 2, 3, null, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, 0, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + version1.IsEqualTo(version2).ShouldEqual(false); + } + [TestCase] public void Compare_Version_Build_Less() { @@ -187,6 +227,7 @@ public void Allow_Blank_Minor_Revision() version.Major.ShouldEqual(1); version.Minor.ShouldEqual(2); version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(null); version.Platform.ShouldEqual("test"); version.Revision.ShouldEqual(4); version.MinorRevision.ShouldEqual(0); @@ -201,11 +242,27 @@ public void Allow_Invalid_Minor_Revision() version.Major.ShouldEqual(1); version.Minor.ShouldEqual(2); version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(null); version.Platform.ShouldEqual("test"); version.Revision.ShouldEqual(4); version.MinorRevision.ShouldEqual(0); } + [TestCase] + public void Allow_ReleaseCandidates() + { + GitVersion version; + GitVersion.TryParseVersion("1.2.3.rc2.test.4.5", out version).ShouldEqual(true); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(2); + version.Platform.ShouldEqual("test"); + version.Revision.ShouldEqual(4); + version.MinorRevision.ShouldEqual(5); + } + private void ParseAndValidateInstallerVersion(string installerName) { GitVersion version; From e30e98f1a12ada0b293ea18b9497c06c87ec22f9 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 7 Aug 2025 12:13:33 +0100 Subject: [PATCH 07/16] GitVersion: drop unused installer version parsing Drop unused (except by tests) methods for parsing the Git installer file name to extract a version number. The installer file name in the tests was out-of-date anyway (no longer contains "gvfs"). Signed-off-by: Matthew John Cheetham --- GVFS/GVFS.Common/Git/GitVersion.cs | 20 --------------- GVFS/GVFS.UnitTests/Common/GitVersionTests.cs | 25 +------------------ 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/GVFS/GVFS.Common/Git/GitVersion.cs b/GVFS/GVFS.Common/Git/GitVersion.cs index 8c3f0f053d..a3c6461a72 100644 --- a/GVFS/GVFS.Common/Git/GitVersion.cs +++ b/GVFS/GVFS.Common/Git/GitVersion.cs @@ -41,26 +41,6 @@ public static bool TryParseGitVersionCommandResult(string input, out GitVersion return TryParseVersion(input, out version); } - public static bool TryParseInstallerName(string input, string installerExtension, out GitVersion version) - { - // Installer name is of the form - // Git-2.14.1.gvfs.1.1.gb16030b-64-bit.exe - - version = null; - - if (!input.StartsWith("Git-", StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - if (!input.EndsWith("-64-bit" + installerExtension, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - return TryParseVersion(input.Substring(4, input.Length - 15), out version); - } - public static bool TryParseVersion(string input, out GitVersion version) { version = null; diff --git a/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs b/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs index 54c47cca16..3d349ad004 100644 --- a/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs +++ b/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs @@ -1,5 +1,4 @@ -using GVFS.Common; -using GVFS.Common.Git; +using GVFS.Common.Git; using GVFS.Tests.Should; using NUnit.Framework; @@ -8,14 +7,6 @@ namespace GVFS.UnitTests.Common [TestFixture] public class GitVersionTests { - [TestCase] - public void TryParseInstallerName() - { - this.ParseAndValidateInstallerVersion("Git-1.2.3.gvfs.4.5.gb16030b-64-bit" + GVFSPlatform.Instance.Constants.InstallerExtension); - this.ParseAndValidateInstallerVersion("git-1.2.3.gvfs.4.5.gb16030b-64-bit" + GVFSPlatform.Instance.Constants.InstallerExtension); - this.ParseAndValidateInstallerVersion("Git-1.2.3.gvfs.4.5.gb16030b-64-bit" + GVFSPlatform.Instance.Constants.InstallerExtension); - } - [TestCase] public void Version_Data_Null_Returns_False() { @@ -262,19 +253,5 @@ public void Allow_ReleaseCandidates() version.Revision.ShouldEqual(4); version.MinorRevision.ShouldEqual(5); } - - private void ParseAndValidateInstallerVersion(string installerName) - { - GitVersion version; - bool success = GitVersion.TryParseInstallerName(installerName, GVFSPlatform.Instance.Constants.InstallerExtension, out version); - success.ShouldBeTrue(); - - version.Major.ShouldEqual(1); - version.Minor.ShouldEqual(2); - version.Build.ShouldEqual(3); - version.Platform.ShouldEqual("gvfs"); - version.Revision.ShouldEqual(4); - version.MinorRevision.ShouldEqual(5); - } } } From 8a98e517c68a1bcda92be2a80da09981700d4ff9 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 28 Jul 2025 09:54:21 -0700 Subject: [PATCH 08/16] build: stop creating/uploading GVFS.Installers nupkg Stop creating and uploading (for the functional tests) NuGet packages for the GVFS.Installers project. We no longer maintain the packages: * GitForWindows.GVFS.Installer * GitForWindows.GVFS.Portable Signed-off-by: Matthew John Cheetham --- .github/workflows/build.yaml | 6 ------ GVFS/GVFS.Installers/GVFS.Installers.csproj | 11 ---------- .../GVFS.Installers.template.nuspec | 15 -------------- Version.props | 7 ++----- scripts/CreateBuildArtifacts.bat | 20 ------------------- 5 files changed, 2 insertions(+), 57 deletions(-) delete mode 100644 GVFS/GVFS.Installers/GVFS.Installers.template.nuspec diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 92e9ab6756..4f3f308418 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -59,12 +59,6 @@ jobs: name: Installers_${{ matrix.configuration }} path: artifacts\GVFS.Installers - - name: Upload NuGet packages - uses: actions/upload-artifact@v4 - with: - name: NuGetPackages_${{ matrix.configuration }} - path: artifacts\NuGetPackages - functional_test: runs-on: windows-2025 name: Functional Tests diff --git a/GVFS/GVFS.Installers/GVFS.Installers.csproj b/GVFS/GVFS.Installers/GVFS.Installers.csproj index 13cf3e6018..4470aeaba9 100644 --- a/GVFS/GVFS.Installers/GVFS.Installers.csproj +++ b/GVFS/GVFS.Installers/GVFS.Installers.csproj @@ -13,8 +13,6 @@ - - @@ -24,13 +22,8 @@ - - - @@ -44,10 +37,6 @@ - - - - diff --git a/GVFS/GVFS.Installers/GVFS.Installers.template.nuspec b/GVFS/GVFS.Installers/GVFS.Installers.template.nuspec deleted file mode 100644 index 8991bd81e0..0000000000 --- a/GVFS/GVFS.Installers/GVFS.Installers.template.nuspec +++ /dev/null @@ -1,15 +0,0 @@ - - - - GVFS.Installers - $version$ - Microsoft - false - GVFS and G4W installers - - - - - - - diff --git a/Version.props b/Version.props index 1547245cec..d1f6f341b7 100644 --- a/Version.props +++ b/Version.props @@ -5,10 +5,8 @@ 0.2.173.2 - 2.20220414.4 v2.31.0.vfs.0.1 diff --git a/scripts/CreateBuildArtifacts.bat b/scripts/CreateBuildArtifacts.bat index 5cca08d5a5..797ed0bf92 100644 --- a/scripts/CreateBuildArtifacts.bat +++ b/scripts/CreateBuildArtifacts.bat @@ -14,13 +14,6 @@ IF "%~2"=="" ( SET OUTROOT=%2 ) -REM Check NuGet is on the PATH -where /q nuget.exe -IF ERRORLEVEL 1 ( - ECHO ERROR: Could not find nuget.exe on the PATH - EXIT /B 1 -) - IF EXIST %OUTROOT% ( rmdir /s /q %OUTROOT% ) @@ -60,19 +53,6 @@ xcopy /S /Y ^ %VFS_OUTDIR%\GVFS.FunctionalTests\bin\%CONFIGURATION%\net471\win-x64 ^ %OUTROOT%\GVFS.FunctionalTests\ || GOTO ERROR -ECHO ^************************************* -ECHO ^* Creating Installers NuGet Package * -ECHO ^************************************* -mkdir %OUTROOT%\NuGetPackages -nuget.exe pack ^ - %VFS_OUTDIR%\GVFS.Installers\bin\%CONFIGURATION%\win-x64\GVFS.Installers.nuspec ^ - -BasePath %VFS_OUTDIR%\GVFS.Installers\bin\%CONFIGURATION%\win-x64 ^ - -OutputDirectory %OUTROOT%\NuGetPackages || GOTO ERROR - -REM Move the nuspec file to the NuGetPackages artifact directory -move %OUTROOT%\GVFS.Installers\GVFS.Installers.nuspec ^ - %OUTROOT%\NuGetPackages - GOTO :EOF :USAGE From f215829cc7ce5b3e913e2db9924dcc16e7e62613 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 28 Jul 2025 09:56:24 -0700 Subject: [PATCH 09/16] build: download microsoft/git installers from GitHub Download the Microsoft Git installers from GitHub releases via the `gh` CLI tool. The installers (both portable and Inno Setup based) are included in the Installers_${{ matrix.configuration }} build artifact that is consumed by the functional test jobs. Signed-off-by: Matthew John Cheetham --- .github/workflows/build.yaml | 34 +++++ .../workflows/scripts/validate_release.ps1 | 124 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 .github/workflows/scripts/validate_release.ps1 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4f3f308418..d4af2ab391 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -5,11 +5,38 @@ on: branches: [ master, releases/shipped ] push: branches: [ master, releases/shipped ] + workflow_dispatch: + inputs: + git_version: + description: 'Microsoft Git version tag to include in the build (leave empty for default)' + required: false + type: string + +env: + GIT_VERSION: ${{ github.event.inputs.git_version || 'v2.50.1.vfs.0.1' }} jobs: + validate: + runs-on: windows-2025 + name: Validation + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Validate Microsoft Git version + shell: pwsh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + & "$env:GITHUB_WORKSPACE\.github\workflows\scripts\validate_release.ps1" ` + -Repository microsoft/git ` + -Tag $env:GIT_VERSION && ` + Write-Host ::notice title=Validation::Using microsoft/git version $env:GIT_VERSION + build: runs-on: windows-2025 name: Build and Unit Test + needs: validate strategy: matrix: @@ -41,6 +68,13 @@ jobs: shell: cmd run: src\scripts\CreateBuildArtifacts.bat ${{ matrix.configuration }} artifacts + - name: Download microsoft/git installers + shell: cmd + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release download %GIT_VERSION% --repo microsoft/git --pattern "Git*.exe" --dir artifacts\GVFS.Installers + - name: Upload functional tests drop uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/scripts/validate_release.ps1 b/.github/workflows/scripts/validate_release.ps1 new file mode 100644 index 0000000000..b9ee8c04db --- /dev/null +++ b/.github/workflows/scripts/validate_release.ps1 @@ -0,0 +1,124 @@ +param( + [Parameter(Mandatory=$true)] + [string]$Tag, + + [Parameter(Mandatory=$true)] + [string]$Repository +) + +function Write-GitHubActionsCommand { + param( + [Parameter(Mandatory=$true)] + [string]$Command, + + [Parameter(Mandatory=$true)] + [string]$Message, + + [Parameter(Mandatory=$true)] + [string]$Title + ) + + Write-Host "::$Command title=$Title::$Message" +} + + +function Write-GitHubActionsWarning { + param( + [Parameter(Mandatory=$true)] + [string]$Message, + + [Parameter(Mandatory=$false)] + [string]$Title = "Warning" + ) + + if ($env:GITHUB_ACTIONS -eq "true") { + Write-GitHubActionsCommand -Command "warning" -Message $Message -Title $Title + } else { + Write-Host "! Warning: $Message" -ForegroundColor Yellow + } +} + +function Write-GitHubActionsError { + param( + [Parameter(Mandatory=$true)] + [string]$Message, + + [Parameter(Mandatory=$false)] + [string]$Title = "Error" + ) + + if ($env:GITHUB_ACTIONS -eq "true") { + Write-GitHubActionsCommand -Command "error" -Message $Message -Title $Title + } else { + Write-Host "x Error: $Message" -ForegroundColor Red + } +} + +if ([string]::IsNullOrWhiteSpace($Tag)) { + Write-GitHubActionsError -Message "Tag parameter is required" + exit 1 +} + +if ([string]::IsNullOrWhiteSpace($Repository)) { + Write-GitHubActionsError -Message "Repository parameter is required" + exit 1 +} + +Write-Host "Validating $Repository release '$Tag'..." + +# Prepare headers for GitHub API +$headers = @{ + 'Accept' = 'application/vnd.github.v3+json' + 'User-Agent' = 'VFSForGit-Build' +} + +if ($env:GITHUB_TOKEN) { + $headers['Authorization'] = "Bearer $env:GITHUB_TOKEN" +} + +# Check if the tag exists in microsoft/git repository +try { + $releaseResponse = Invoke-RestMethod ` + -Uri "https://api.github.com/repos/$Repository/releases/tags/$Tag" ` + -Headers $headers + + Write-Host "✓ Tag '$Tag' found in $Repository" -ForegroundColor Green + Write-Host " Release : $($releaseResponse.name)" + Write-Host " Published : $($releaseResponse.published_at.ToString('u'))" + + # Check if this a pre-release + if ($releaseResponse.prerelease -eq $true) { + Write-GitHubActionsWarning ` + -Message "Using a pre-released version of $Repository" ` + -Title "Pre-release $Repository version" + } + + # Get the latest release for comparison + try { + $latestResponse = Invoke-RestMethod ` + -Uri "https://api.github.com/repos/$Repository/releases/latest" ` + -Headers $headers + $latestTag = $latestResponse.tag_name + + # Check if this is the latest release + if ($Tag -eq $latestTag) { + Write-Host "✓ Using the latest release" -ForegroundColor Green + exit 0 + } + + # Not the latest! + $warningTitle = "Outdated $Repository release" + $warningMsg = "Not using latest release of $Repository (latest: $latestTag)" + Write-GitHubActionsWarning -Message $warningMsg -Title $warningTitle + } catch { + Write-GitHubActionsWarning -Message "Could not check latest release info for ${Repository}: $($_.Exception.Message)" + } +} catch { + if ($_.Exception.Response.StatusCode -eq 404) { + Write-GitHubActionsError -Message "Tag '$Tag' does not exist in $Repository" + exit 1 + } else { + Write-GitHubActionsError -Message "Error validating release '$Tag': $($_.Exception.Message)" + exit 1 + } +} From 3d6f13d8a051ac8514d447c80b451cf342792156 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 28 Jul 2025 09:58:26 -0700 Subject: [PATCH 10/16] ft: install arch-specific msftgit and print OS version Install the correct version of Microsoft Git in the functional tests for the OS architecture, and print more OS/CPU information for good measure. Signed-off-by: Matthew John Cheetham --- GVFS/GVFS.Installers/info.bat | 12 ++++++++++++ GVFS/GVFS.Installers/install.bat | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/GVFS/GVFS.Installers/info.bat b/GVFS/GVFS.Installers/info.bat index b068f9c692..ba61fbc7a8 100644 --- a/GVFS/GVFS.Installers/info.bat +++ b/GVFS/GVFS.Installers/info.bat @@ -9,19 +9,31 @@ SET VFS_BUND_PROJFSLIB=C:\Program Files\VFS for Git\ProjFS\ProjectedFSLib.dll SET VFS_EXEC=C:\Program Files\VFS for Git\GVFS.exe SET GIT_EXEC=C:\Program Files\Git\cmd\git.exe +REM Lookup the current Windows version +FOR /F "tokens=*" %%i IN ('powershell -Command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"') DO SET OS_VER=%%i + +ECHO Print system information... +ECHO OS version: %OS_VER% +ECHO CPU architecture: %PROCESSOR_ARCHITECTURE% + +ECHO. ECHO Checking ProjFS Windows feature... powershell -Command "Get-WindowsOptionalFeature -Online -FeatureName Client-ProjFS" +ECHO. ECHO Checking ProjFS and GVFS services... ECHO GVFS.Service: sc query GVFS.Service +ECHO. ECHO Test.GVFS.Service: sc query Test.GVFS.Service +ECHO. ECHO prjflt: sc query prjflt +ECHO. ECHO Checking ProjFS files... IF EXIST "%SYS_PRJFLT%" ( ECHO [ FOUND ] %SYS_PRJFLT% diff --git a/GVFS/GVFS.Installers/install.bat b/GVFS/GVFS.Installers/install.bat index c629c75bc1..8375be193f 100644 --- a/GVFS/GVFS.Installers/install.bat +++ b/GVFS/GVFS.Installers/install.bat @@ -1,8 +1,18 @@ @ECHO OFF SETLOCAL +REM Determine the correct architecture for the installer +IF "%PROCESSOR_ARCHITECTURE%"=="AMD64" ( + SET GIT_ARCH=64-bit +) ELSE IF "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + SET GIT_ARCH=arm64 +) ELSE ( + ECHO Unknown architecture: %PROCESSOR_ARCHITECTURE% + exit 1 +) + REM Lookup full paths to Git and VFS for Git installers -FOR /F "tokens=* USEBACKQ" %%F IN ( `where /R %~dp0 Git*.exe` ) DO SET GIT_INSTALLER=%%F +FOR /F "tokens=* USEBACKQ" %%F IN ( `where /R %~dp0 Git*-%GIT_ARCH%.exe` ) DO SET GIT_INSTALLER=%%F FOR /F "tokens=* USEBACKQ" %%F IN ( `where /R %~dp0 SetupGVFS*.exe` ) DO SET GVFS_INSTALLER=%%F REM Create new empty directory for logs @@ -12,7 +22,7 @@ IF EXIST %LOGDIR% ( ) mkdir %LOGDIR% -ECHO Installing Git for Windows... +ECHO Installing Git (%GIT_ARCH%)... %GIT_INSTALLER% /LOG="%LOGDIR%\git.log" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /ALLOWDOWNGRADE=1 ECHO Installing VFS for Git... From e66befe32f7830c76368ae9b85c6b80218e8e7b0 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 28 Jul 2025 10:25:42 -0700 Subject: [PATCH 11/16] ft: run functional tests on ARM64 Let's use the ARM64 Windows images (currently in public preview) on GitHub hosted runners to run the functional tests on ARM. VFS for Git is still only built for x86_64, but we do now include a natively compiled Git for ARM64 - so let's exercise that! Signed-off-by: Matthew John Cheetham --- .github/workflows/build.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d4af2ab391..79a4e4cc8b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -94,13 +94,14 @@ jobs: path: artifacts\GVFS.Installers functional_test: - runs-on: windows-2025 + runs-on: ${{ matrix.architecture == 'arm64' && 'windows-11-arm' || 'windows-2025' }} name: Functional Tests needs: build strategy: matrix: configuration: [ Debug, Release ] + architecture: [ x86_64, arm64 ] steps: - name: Download installers @@ -131,7 +132,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: InstallationLogs_${{ matrix.configuration }} + name: InstallationLogs_${{ matrix.configuration }}_${{ matrix.architecture }} path: install\logs - name: Run functional tests @@ -145,14 +146,14 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: FunctionalTests_Results_${{ matrix.configuration }} + name: FunctionalTests_Results_${{ matrix.configuration }}_${{ matrix.architecture }} path: TestResult.xml - name: Upload Git trace2 output if: always() uses: actions/upload-artifact@v4 with: - name: GitTrace2_${{ matrix.configuration }} + name: GitTrace2_${{ matrix.configuration }}_${{ matrix.architecture }} path: C:\temp\git-trace2.log - name: ProjFS details (post-test) From 3b6c22387e2e1fea713a86cf90f24bcba73406ad Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 28 Jul 2025 11:04:47 -0700 Subject: [PATCH 12/16] Functional Tests: refactor out output filtering In preparing to accounting for the "You are in a partially-hydrated checkout with %d% of tracked files present" message that was added in Microsoft Git v2.XX.Y, let's refactor the existing output filtering (which is so far only done for `stderr`) into its own method. Signed-off-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs index 8aacac84a7..452bb14432 100644 --- a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs +++ b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs @@ -66,12 +66,23 @@ public static ProcessResult InvokeGitAgainstGVFSRepo( bool removeUpgradeMessages = true) { ProcessResult result = GitProcess.InvokeProcess(gvfsRepoRoot, command, environmentVariables); - string errors = result.Errors; + string errors = FilterMessages(result.Errors, removeWaitingMessages, removeUpgradeMessages); - if (!string.IsNullOrEmpty(errors) && (removeWaitingMessages || removeUpgradeMessages)) + return new ProcessResult( + result.Output, + errors, + result.ExitCode); + } + + private static string FilterMessages( + string input, + bool removeWaitingMessages, + bool removeUpgradeMessages) + { + if (!string.IsNullOrEmpty(input) && (removeWaitingMessages || removeUpgradeMessages)) { - IEnumerable errorLines = errors.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); - IEnumerable filteredErrorLines = errorLines.Where(line => + IEnumerable lines = input.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + IEnumerable filteredLines = lines.Where(line => { if (string.IsNullOrWhiteSpace(line) || (removeUpgradeMessages && line.StartsWith("A new version of VFS for Git is available.")) || @@ -85,13 +96,10 @@ public static ProcessResult InvokeGitAgainstGVFSRepo( } }); - errors = filteredErrorLines.Any() ? string.Join(Environment.NewLine, filteredErrorLines) : string.Empty; + return filteredLines.Any() ? string.Join(Environment.NewLine, filteredLines) : string.Empty; } - return new ProcessResult( - result.Output, - errors, - result.ExitCode); + return input; } public static void ValidateGitCommand( From 7e4e345aeaa2d69788c03c4ee76e2a0094e58edf Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Aug 2025 15:05:24 +0200 Subject: [PATCH 13/16] Functional Tests: when filtering Git output, retain newlines faithfully There are tests that find multi-line needles in the Git output's haystack. So far, those tests work on `stdout` only. We are about to filter `stdout` using the new `FilterMessages()` method, therefore that method must stop its current practice where it splits by newlines and then joins using `Environment.Newline` (even if that is different from what the input had before). Signed-off-by: Johannes Schindelin --- GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs index 452bb14432..c9f9768c2d 100644 --- a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs +++ b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs @@ -74,6 +74,23 @@ public static ProcessResult InvokeGitAgainstGVFSRepo( result.ExitCode); } + private static IEnumerable SplitLinesKeepingNewlines(string input) + { + for (int start = 0; start < input.Length; ) + { + int nextLine = input.IndexOf('\n', start) + 1; + + if (nextLine == 0) + { + // No more newlines, yield the rest + nextLine = input.Length; + } + + yield return input.Substring(start, nextLine - start); + start = nextLine; + } + } + private static string FilterMessages( string input, bool removeWaitingMessages, @@ -81,7 +98,7 @@ private static string FilterMessages( { if (!string.IsNullOrEmpty(input) && (removeWaitingMessages || removeUpgradeMessages)) { - IEnumerable lines = input.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + IEnumerable lines = SplitLinesKeepingNewlines(input); IEnumerable filteredLines = lines.Where(line => { if (string.IsNullOrWhiteSpace(line) || @@ -96,7 +113,7 @@ private static string FilterMessages( } }); - return filteredLines.Any() ? string.Join(Environment.NewLine, filteredLines) : string.Empty; + return filteredLines.Any() ? string.Join("", filteredLines) : string.Empty; } return input; From 23001d596cc3a2a25b24290a87e9009d8c057c4b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Aug 2025 15:09:04 +0200 Subject: [PATCH 14/16] Functional Tests: support retaining empty lines in FilterMessages() Currently, we only filter `stderr`, but we're about to do that with `stdout`, too, where we do not want to skip empty lines. Signed-off-by: Johannes Schindelin --- GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs index c9f9768c2d..19249ba79c 100644 --- a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs +++ b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs @@ -66,7 +66,7 @@ public static ProcessResult InvokeGitAgainstGVFSRepo( bool removeUpgradeMessages = true) { ProcessResult result = GitProcess.InvokeProcess(gvfsRepoRoot, command, environmentVariables); - string errors = FilterMessages(result.Errors, removeWaitingMessages, removeUpgradeMessages); + string errors = FilterMessages(result.Errors, true, removeWaitingMessages, removeUpgradeMessages); return new ProcessResult( result.Output, @@ -93,6 +93,7 @@ private static IEnumerable SplitLinesKeepingNewlines(string input) private static string FilterMessages( string input, + bool removeEmptyLines, bool removeWaitingMessages, bool removeUpgradeMessages) { @@ -101,7 +102,7 @@ private static string FilterMessages( IEnumerable lines = SplitLinesKeepingNewlines(input); IEnumerable filteredLines = lines.Where(line => { - if (string.IsNullOrWhiteSpace(line) || + if ((removeEmptyLines && string.IsNullOrWhiteSpace(line)) || (removeUpgradeMessages && line.StartsWith("A new version of VFS for Git is available.")) || (removeWaitingMessages && line.StartsWith("Waiting for "))) { From 3c364c349ddb4f8e0fa50ed0272f950572e41c81 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 28 Jul 2025 11:04:47 -0700 Subject: [PATCH 15/16] Functional Tests: account for 'partially-hydrated checkout' messages Let the functional tests account for the "You are in a partially-hydrated checkout with %d% of tracked files present" message that was added in Microsoft Git v2.45.2.vfs.0.2. Do this by filtering out those messages in the same way we do for the old 'A new version of VFS for Git is available.' upgrader messages, and the 'Waiting for %s' lock waiting message. Note that the 'hydration' state message is printed to standard output and not standard error as the other messages, since this is the output from the `status` command. We filter both stdout and stderr. Signed-off-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs index 19249ba79c..784b10a0e4 100644 --- a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs +++ b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs @@ -63,13 +63,15 @@ public static ProcessResult InvokeGitAgainstGVFSRepo( string command, Dictionary environmentVariables = null, bool removeWaitingMessages = true, - bool removeUpgradeMessages = true) + bool removeUpgradeMessages = true, + bool removePartialHydrationMessages = true) { ProcessResult result = GitProcess.InvokeProcess(gvfsRepoRoot, command, environmentVariables); - string errors = FilterMessages(result.Errors, true, removeWaitingMessages, removeUpgradeMessages); + string output = FilterMessages(result.Output, false, false, false, removePartialHydrationMessages); + string errors = FilterMessages(result.Errors, true, removeWaitingMessages, removeUpgradeMessages, removePartialHydrationMessages); return new ProcessResult( - result.Output, + output, errors, result.ExitCode); } @@ -95,16 +97,18 @@ private static string FilterMessages( string input, bool removeEmptyLines, bool removeWaitingMessages, - bool removeUpgradeMessages) + bool removeUpgradeMessages, + bool removePartialHydrationMessages) { - if (!string.IsNullOrEmpty(input) && (removeWaitingMessages || removeUpgradeMessages)) + if (!string.IsNullOrEmpty(input) && (removeWaitingMessages || removeUpgradeMessages || removePartialHydrationMessages)) { IEnumerable lines = SplitLinesKeepingNewlines(input); IEnumerable filteredLines = lines.Where(line => { if ((removeEmptyLines && string.IsNullOrWhiteSpace(line)) || (removeUpgradeMessages && line.StartsWith("A new version of VFS for Git is available.")) || - (removeWaitingMessages && line.StartsWith("Waiting for "))) + (removeWaitingMessages && line.StartsWith("Waiting for ")) || + (removePartialHydrationMessages && line.StartsWith("You are in a partially-hydrated checkout with "))) { return false; } From c69ba8e8036a62c3d4dd8f2ee68cda4529187c0f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Aug 2025 14:12:30 +0200 Subject: [PATCH 16/16] Functional Tests: heed Postel's law regarding `core.FSMonitor` When running the functional tests locally, it is quite possible that the `core.fsmonitor` setting is enabled globally. Let's avoid chalking that up as a test failure. Signed-off-by: Johannes Schindelin --- GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs index 784b10a0e4..76a77eb64e 100644 --- a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs +++ b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs @@ -64,11 +64,12 @@ public static ProcessResult InvokeGitAgainstGVFSRepo( Dictionary environmentVariables = null, bool removeWaitingMessages = true, bool removeUpgradeMessages = true, - bool removePartialHydrationMessages = true) + bool removePartialHydrationMessages = true, + bool removeFSMonitorMessages = true) { ProcessResult result = GitProcess.InvokeProcess(gvfsRepoRoot, command, environmentVariables); - string output = FilterMessages(result.Output, false, false, false, removePartialHydrationMessages); - string errors = FilterMessages(result.Errors, true, removeWaitingMessages, removeUpgradeMessages, removePartialHydrationMessages); + string output = FilterMessages(result.Output, false, false, false, removePartialHydrationMessages, removeFSMonitorMessages); + string errors = FilterMessages(result.Errors, true, removeWaitingMessages, removeUpgradeMessages, removePartialHydrationMessages, removeFSMonitorMessages); return new ProcessResult( output, @@ -98,9 +99,10 @@ private static string FilterMessages( bool removeEmptyLines, bool removeWaitingMessages, bool removeUpgradeMessages, - bool removePartialHydrationMessages) + bool removePartialHydrationMessages, + bool removeFSMonitorMessages) { - if (!string.IsNullOrEmpty(input) && (removeWaitingMessages || removeUpgradeMessages || removePartialHydrationMessages)) + if (!string.IsNullOrEmpty(input) && (removeWaitingMessages || removeUpgradeMessages || removePartialHydrationMessages || removeFSMonitorMessages)) { IEnumerable lines = SplitLinesKeepingNewlines(input); IEnumerable filteredLines = lines.Where(line => @@ -108,7 +110,8 @@ private static string FilterMessages( if ((removeEmptyLines && string.IsNullOrWhiteSpace(line)) || (removeUpgradeMessages && line.StartsWith("A new version of VFS for Git is available.")) || (removeWaitingMessages && line.StartsWith("Waiting for ")) || - (removePartialHydrationMessages && line.StartsWith("You are in a partially-hydrated checkout with "))) + (removePartialHydrationMessages && line.StartsWith("You are in a partially-hydrated checkout with ")) || + (removeFSMonitorMessages && line.TrimEnd().EndsWith(" is incompatible with fsmonitor"))) { return false; }