diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 12bdf560d..fe238c532 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -1,162 +1,305 @@
-name: VFS for Git
-
-on:
- pull_request:
- 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@v5
-
- - 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:
- configuration: [ Debug, Release ]
-
- steps:
- - name: Checkout source
- uses: actions/checkout@v5
- with:
- path: src
-
- - name: Install .NET SDK
- uses: actions/setup-dotnet@v5
- with:
- dotnet-version: 8.0.413
-
- - name: Add MSBuild to PATH
- uses: microsoft/setup-msbuild@v2.0.0
-
- - name: Build VFS for Git
- shell: cmd
- run: src\scripts\Build.bat ${{ matrix.configuration }}
-
- - name: Run unit tests
- shell: cmd
- run: src\scripts\RunUnitTests.bat ${{ matrix.configuration }}
-
- - name: Create build artifacts
- 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:
- name: FunctionalTests_${{ matrix.configuration }}
- path: artifacts\GVFS.FunctionalTests
-
- - name: Upload FastFetch drop
- uses: actions/upload-artifact@v4
- with:
- name: FastFetch_${{ matrix.configuration }}
- path: artifacts\FastFetch
-
- - name: Upload installers
- uses: actions/upload-artifact@v4
- with:
- name: Installers_${{ matrix.configuration }}
- path: artifacts\GVFS.Installers
-
- functional_test:
- 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
- uses: actions/download-artifact@v5
- with:
- name: Installers_${{ matrix.configuration }}
- path: install
-
- - name: Download functional tests drop
- uses: actions/download-artifact@v5
- with:
- name: FunctionalTests_${{ matrix.configuration }}
- path: ft
-
- - name: ProjFS details (pre-install)
- shell: cmd
- run: install\info.bat
-
- - name: Install product
- shell: cmd
- run: install\install.bat
-
- - name: ProjFS details (post-install)
- shell: cmd
- run: install\info.bat
-
- - name: Upload installation logs
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: InstallationLogs_${{ matrix.configuration }}_${{ matrix.architecture }}
- path: install\logs
-
- - name: Run functional tests
- shell: cmd
- run: |
- SET PATH=C:\Program Files\VFS for Git;%PATH%
- SET GIT_TRACE2_PERF=C:\temp\git-trace2.log
- ft\GVFS.FunctionalTests.exe /result:TestResult.xml --ci
-
- - name: Upload functional test results
- if: always()
- uses: actions/upload-artifact@v4
- with:
- 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 }}_${{ matrix.architecture }}
- path: C:\temp\git-trace2.log
-
- - name: ProjFS details (post-test)
- if: always()
- shell: cmd
- run: install\info.bat
+name: VFS for Git
+
+on:
+ pull_request:
+ 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
+
+permissions:
+ contents: read
+
+env:
+ GIT_VERSION: ${{ github.event.inputs.git_version || 'v2.50.1.vfs.0.1' }}
+
+jobs:
+ validate:
+ runs-on: windows-2025
+ name: Validation
+ outputs:
+ skip: ${{ steps.check.outputs.result }}
+
+ steps:
+ - name: Look for prior successful runs
+ id: check
+ if: github.event.inputs.git_version == ''
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{secrets.GITHUB_TOKEN}}
+ result-encoding: string
+ script: |
+ /*
+ * It would be nice if GitHub Actions offered a convenient way to avoid running
+ * successful workflow runs _again_ for the respective commit (or for a tree-same one):
+ * We would expect the same outcome in those cases, right?
+ *
+ * Let's check for such a scenario: Look for previous runs that have been successful
+ * and that correspond to the same commit, or at least a tree-same one. If there is
+ * one, skip running the build and tests _again_.
+ *
+ * There are challenges, though: We need to require those _jobs_ to succeed before PRs
+ * can be merged. You can mark workflow _jobs_ as required on GitHub, but not
+ * _workflows_. So if those jobs are now simply skipped, the requirement isn't met and
+ * the PR cannot be merged. We can't just skip the job. Instead, we need to run the job
+ * _but skip every single step_ so that the job can "succeed".
+ */
+ try {
+ // Figure out workflow ID, commit and tree
+ const { data: run } = await github.rest.actions.getWorkflowRun({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: context.runId,
+ });
+ const workflow_id = run.workflow_id;
+ const head_sha = run.head_sha;
+ const tree_id = run.head_commit.tree_id;
+
+ // See whether there is a successful run for that commit or tree
+ const { data: runs } = await github.rest.actions.listWorkflowRuns({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ per_page: 500,
+ workflow_id,
+ });
+ // first look at commit-same runs, then at finished ones, then at in-progress ones
+ const rank = (a) => (a.status === 'in_progress' ? 0 : (head_sha === a.head_sha ? 2 : 1))
+ const demoteInProgressToEnd = (a, b) => (rank(b) - rank(a))
+ for (const run of runs.workflow_runs.sort(demoteInProgressToEnd)) {
+ if (head_sha !== run.head_sha && tree_id !== run.head_commit?.tree_id) continue
+ if (context.runId === run.id) continue // do not wait for the current run to finish ;-)
+ if (run.event === 'workflow_dispatch') continue // skip runs that were started manually: they can override the Git version
+
+ if (run.status === 'in_progress') {
+ // poll until the run is done
+ const pollIntervalInSeconds = 30
+ let seconds = 0
+ for (;;) {
+ console.log(`Found existing, in-progress run at ${run.html_url}; Waiting for it to finish (waited ${seconds} seconds so far)...`)
+ await new Promise((resolve) => { setTimeout(resolve, pollIntervalInSeconds * 1000) })
+ seconds += pollIntervalInSeconds
+
+ const { data: polledRun } = await github.rest.actions.getWorkflowRun({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: run.id
+ })
+ if (polledRun.status !== 'in_progress') break
+ }
+ }
+
+ if (run.status === 'completed' && run.conclusion === 'success') return run.html_url
+ }
+ return ''
+ } catch (e) {
+ core.error(e)
+ core.warning(e)
+ }
+
+ - name: Checkout source
+ if: steps.check.outputs.result == ''
+ uses: actions/checkout@v5
+
+ - name: Validate Microsoft Git version
+ if: steps.check.outputs.result == ''
+ 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:
+ configuration: [ Debug, Release ]
+ fail-fast: false
+
+ steps:
+ - name: Skip this job if there is a previous successful run
+ if: needs.validate.outputs.skip != ''
+ id: skip
+ uses: actions/github-script@v7
+ with:
+ script: |
+ core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`)
+ return true
+
+ - name: Checkout source
+ if: steps.skip.outputs.result != 'true'
+ uses: actions/checkout@v5
+ with:
+ path: src
+
+ - name: Install .NET SDK
+ if: steps.skip.outputs.result != 'true'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 8.0.413
+
+ - name: Add MSBuild to PATH
+ if: steps.skip.outputs.result != 'true'
+ uses: microsoft/setup-msbuild@v2.0.0
+
+ - name: Build VFS for Git
+ if: steps.skip.outputs.result != 'true'
+ shell: cmd
+ run: src\scripts\Build.bat ${{ matrix.configuration }}
+
+ - name: Run unit tests
+ if: steps.skip.outputs.result != 'true'
+ shell: cmd
+ run: src\scripts\RunUnitTests.bat ${{ matrix.configuration }}
+
+ - name: Create build artifacts
+ if: steps.skip.outputs.result != 'true'
+ shell: cmd
+ run: src\scripts\CreateBuildArtifacts.bat ${{ matrix.configuration }} artifacts
+
+ - name: Download microsoft/git installers
+ if: steps.skip.outputs.result != 'true'
+ 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
+ if: steps.skip.outputs.result != 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: FunctionalTests_${{ matrix.configuration }}
+ path: artifacts\GVFS.FunctionalTests
+
+ - name: Upload FastFetch drop
+ if: steps.skip.outputs.result != 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: FastFetch_${{ matrix.configuration }}
+ path: artifacts\FastFetch
+
+ - name: Upload installers
+ if: steps.skip.outputs.result != 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: Installers_${{ matrix.configuration }}
+ path: artifacts\GVFS.Installers
+
+ functional_test:
+ runs-on: ${{ matrix.architecture == 'arm64' && 'windows-11-arm' || 'windows-2025' }}
+ name: Functional Tests
+ needs: [validate, build]
+
+ strategy:
+ matrix:
+ configuration: [ Debug, Release ]
+ architecture: [ x86_64, arm64 ]
+ nr: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 10 parallel jobs to speed up the tests
+ fail-fast: false # most failures are flaky tests, no need to stop the other jobs from succeeding
+
+ steps:
+ - name: Skip this job if there is a previous successful run
+ if: needs.validate.outputs.skip != ''
+ id: skip
+ uses: actions/github-script@v7
+ with:
+ script: |
+ core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`)
+ return true
+
+ - name: Download installers
+ if: steps.skip.outputs.result != 'true'
+ uses: actions/download-artifact@v5
+ with:
+ name: Installers_${{ matrix.configuration }}
+ path: install
+
+ - name: Download functional tests drop
+ if: steps.skip.outputs.result != 'true'
+ uses: actions/download-artifact@v5
+ with:
+ name: FunctionalTests_${{ matrix.configuration }}
+ path: ft
+
+ - name: ProjFS details (pre-install)
+ if: steps.skip.outputs.result != 'true'
+ shell: cmd
+ run: install\info.bat
+
+ - name: Install product
+ if: steps.skip.outputs.result != 'true'
+ shell: cmd
+ run: install\install.bat
+
+ - name: ProjFS details (post-install)
+ if: steps.skip.outputs.result != 'true'
+ shell: cmd
+ run: install\info.bat
+
+ - name: Upload installation logs
+ if: always() && steps.skip.outputs.result != 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: InstallationLogs_${{ matrix.configuration }}_${{ matrix.architecture }}-${{ matrix.nr }}
+ path: install\logs
+
+ - name: Run functional tests
+ if: steps.skip.outputs.result != 'true'
+ shell: cmd
+ run: |
+ SET PATH=C:\Program Files\VFS for Git;%PATH%
+ SET GIT_TRACE2_PERF=C:\temp\git-trace2.log
+ ft\GVFS.FunctionalTests.exe /result:TestResult.xml --ci --slice=${{ matrix.nr }},10
+
+ - name: Upload functional test results
+ if: always() && steps.skip.outputs.result != 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: FunctionalTests_Results_${{ matrix.configuration }}_${{ matrix.architecture }}-${{ matrix.nr }}
+ path: TestResult.xml
+
+ - name: Upload Git trace2 output
+ if: always() && steps.skip.outputs.result != 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: GitTrace2_${{ matrix.configuration }}_${{ matrix.architecture }}-${{ matrix.nr }}
+ path: C:\temp\git-trace2.log
+
+ - name: ProjFS details (post-test)
+ if: always() && steps.skip.outputs.result != 'true'
+ shell: cmd
+ run: install\info.bat
+
+ ft_results:
+ runs-on: ubuntu-latest # quickest runners
+ name: Functional Tests
+ needs: [functional_test]
+
+ strategy:
+ matrix:
+ configuration: [ Debug, Release ]
+ architecture: [ x86_64, arm64 ]
+
+ steps:
+ - name: Success! # for easier identification of successful runs in the Checks Required for Pull Requests
+ run: echo "All functional test jobs successful for ${{ matrix.configuration }} / ${{ matrix.architecture }}!"
+
+ result:
+ runs-on: ubuntu-latest
+ name: Build, Unit and Functional Tests Successful
+ needs: [functional_test]
+
+ steps:
+ - name: Success! # for easier identification of successful runs in the Checks Required for Pull Requests
+ run: echo "Workflow run is successful!"
\ No newline at end of file
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 94263857e..578902043 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -3,6 +3,12 @@
$(GVFSVersion)
+
+
+ false
diff --git a/GVFS/GVFS.Common/ProcessHelper.cs b/GVFS/GVFS.Common/ProcessHelper.cs
index ad24a6434..4fa57fbaf 100644
--- a/GVFS/GVFS.Common/ProcessHelper.cs
+++ b/GVFS/GVFS.Common/ProcessHelper.cs
@@ -58,17 +58,7 @@ public static string GetCurrentProcessVersion()
public static bool IsDevelopmentVersion()
{
string version = ProcessHelper.GetCurrentProcessVersion();
- /* When debugging local version with VS, the version will include +{commitId} suffix,
- * which is not valid for Version class. */
- var plusIndex = version.IndexOf('+');
- if (plusIndex >= 0)
- {
- version = version.Substring(0, plusIndex);
- }
-
- Version currentVersion = new Version(version);
-
- return currentVersion.Major == 0;
+ return version.Equals("0.2.173.2") || version.StartsWith("0.2.173.2+");
}
public static string GetProgramLocation(string programLocaterCommand, string processName)
diff --git a/GVFS/GVFS.FunctionalTests/Program.cs b/GVFS/GVFS.FunctionalTests/Program.cs
index 276a7d701..f00d9496a 100644
--- a/GVFS/GVFS.FunctionalTests/Program.cs
+++ b/GVFS/GVFS.FunctionalTests/Program.cs
@@ -89,6 +89,25 @@ public static void Main(string[] args)
GVFSTestConfig.FileSystemRunners = FileSystemRunners.FileSystemRunner.DefaultRunners;
}
+ (uint, uint)? testSlice = null;
+ string testSliceArg = runner.GetCustomArgWithParam("--slice");
+ if (testSliceArg != null)
+ {
+ // split `testSliceArg` on a comma and parse the two values as uints
+ string[] parts = testSliceArg.Split(',');
+ uint sliceNumber;
+ uint totalSlices;
+ if (parts.Length != 2 ||
+ !uint.TryParse(parts[0], out sliceNumber) ||
+ !uint.TryParse(parts[1], out totalSlices) ||
+ totalSlices == 0 ||
+ sliceNumber >= totalSlices)
+ {
+ throw new Exception("Invalid argument to --slice. Expected format: X,Y where X is the slice number and Y is the total number of slices");
+ }
+ testSlice = (sliceNumber, totalSlices);
+ }
+
GVFSTestConfig.DotGVFSRoot = ".gvfs";
GVFSTestConfig.RepoToClone =
@@ -96,7 +115,7 @@ public static void Main(string[] args)
?? Properties.Settings.Default.RepoToClone;
RunBeforeAnyTests();
- Environment.ExitCode = runner.RunTests(includeCategories, excludeCategories);
+ Environment.ExitCode = runner.RunTests(includeCategories, excludeCategories, testSlice);
if (Debugger.IsAttached)
{
diff --git a/GVFS/GVFS.Tests/NUnitRunner.cs b/GVFS/GVFS.Tests/NUnitRunner.cs
index 1f1e3e99c..e0861eea9 100644
--- a/GVFS/GVFS.Tests/NUnitRunner.cs
+++ b/GVFS/GVFS.Tests/NUnitRunner.cs
@@ -1,8 +1,10 @@
using NUnitLite;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Reflection;
+using System.Text.RegularExpressions;
namespace GVFS.Tests
{
@@ -42,10 +44,106 @@ public void AddGlobalSetupIfNeeded(string globalSetup)
}
}
- public int RunTests(ICollection includeCategories, ICollection excludeCategories)
+ public void PrepareTestSlice(string filters, (uint, uint) testSlice)
{
- string filters = GetFiltersArgument(includeCategories, excludeCategories);
+ IEnumerable args = this.args.Concat(new[] { "--explore" });
if (filters.Length > 0)
+ {
+ args = args.Concat(new[] { "--where", filters });
+ }
+
+ // Temporarily redirect Console.Out to capture the output of --explore
+ var stringWriter = new StringWriter();
+ var originalOut = Console.Out;
+
+ string[] list;
+ try
+ {
+ Console.SetOut(stringWriter);
+ int exploreResult = new AutoRun(Assembly.GetEntryAssembly()).Execute(args.ToArray());
+ if (exploreResult != 0)
+ {
+ throw new Exception("--explore failed with " + exploreResult);
+ }
+
+ list = stringWriter.ToString().Split(new[] { "\n" }, StringSplitOptions.None);
+ }
+ finally
+ {
+ Console.SetOut(originalOut); // Ensure we restore Console.Out
+ }
+
+ // Sort the test cases into roughly equal-sized buckets;
+ // Care must be taken to ensure that all test cases for a given
+ // EnlistmentPerFixture class go into the same bucket, as they
+ // may very well be dependent on each other.
+
+ // First, create the buckets
+ List[] buckets = new List[testSlice.Item2];
+ // There is no PriorityQueue in .NET Framework; Emulate one via
+ // a sorted set that contains tuples of (bucket index, bucket size).
+ var priorityQueue = new SortedSet<(int, int)>(
+ Comparer<(int, int)>.Create((x, y) =>
+ {
+ if (x.Item2 != y.Item2)
+ {
+ return x.Item2.CompareTo(y.Item2);
+ }
+ return x.Item1.CompareTo(y.Item1);
+ }));
+ for (int i = 0; i < buckets.Length; i++)
+ {
+ buckets[i] = new List();
+ priorityQueue.Add((i, buckets[i].Count));
+ }
+
+ // Now distribute the tests into the buckets
+ Regex perFixtureRegex = new Regex(
+ @"^.*\.EnlistmentPerFixture\..+\.",
+ // @"^.*\.",
+ RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+ for (uint i = 0; i < list.Length; i++)
+ {
+ var test = list[i].Trim();
+ if (!test.StartsWith("GVFS.")) continue;
+
+ var bucket = priorityQueue.Min;
+ priorityQueue.Remove(bucket);
+
+ buckets[bucket.Item1].Add(test);
+
+ // Ensure that EnlistmentPerFixture tests of the same class are all in the same bucket
+ var match = perFixtureRegex.Match(test);
+ if (match.Success)
+ {
+ string prefix = match.Value;
+ while (i + 1 < list.Length && list[i + 1].StartsWith(prefix))
+ {
+ buckets[bucket.Item1].Add(list[++i].Trim());
+ }
+ }
+
+ bucket.Item2 = buckets[bucket.Item1].Count;
+ priorityQueue.Add(bucket);
+ }
+
+ // Write the respective bucket's contents to a file
+ string listFile = $"GVFS_test_slice_{testSlice.Item1}_of_{testSlice.Item2}.txt";
+ File.WriteAllLines(listFile, buckets[testSlice.Item1]);
+ Console.WriteLine($"Wrote {buckets[testSlice.Item1].Count} test cases to {listFile}");
+
+ this.args.Add($"--testlist={listFile}");
+ }
+
+ public int RunTests(ICollection includeCategories, ICollection excludeCategories, (uint, uint)? testSlice = null)
+ {
+ string filters = GetFiltersArgument(includeCategories, excludeCategories);
+
+ if (testSlice.HasValue && testSlice.Value.Item2 != 1)
+ {
+ this.PrepareTestSlice(filters, testSlice.Value);
+ }
+ else if (filters.Length > 0)
{
this.args.Add("--where");
this.args.Add(filters);
diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs
index a9fcb9587..fa183c7a3 100644
--- a/GVFS/GVFS/CommandLine/GVFSVerb.cs
+++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs
@@ -933,7 +933,11 @@ private bool TryValidateGVFSVersion(GVFSEnlistment enlistment, ITracer tracer, S
return true;
}
- Version currentVersion = new Version(ProcessHelper.GetCurrentProcessVersion());
+ string recordedVersion = ProcessHelper.GetCurrentProcessVersion();
+ // Work around the default behavior in .NET SDK 8 where the revision ID
+ // is appended after a '+' character, which cannot be parsed by `System.Version`.
+ int plus = recordedVersion.IndexOf('+');
+ Version currentVersion = new Version(plus < 0 ? recordedVersion : recordedVersion.Substring(0, plus));
IEnumerable allowedGvfsClientVersions =
config != null
? config.AllowedGVFSClientVersions