From 2568836a816a0ccf58608c80c2ffc28bec6e3e47 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 11 Sep 2025 15:07:27 +0200 Subject: [PATCH 01/18] ci(build): use Unix line endings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Git simply has so strong an opinion about line endings that it is not worth fighting. Git will passive-aggressively color the Carriage Return in the diff output until you give up, and that's what I'm doing here. Der Klügere gibt nach. Technically, this is far from the only file that has this issue. But I do not want to interfere with other ongoing work in the CI area, so I'll refrain from touching files other than the one I am about to modify. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 324 +++++++++++++++++------------------ 1 file changed, 162 insertions(+), 162 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 12bdf560d..7f1172aa7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,162 +1,162 @@ -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 + +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 From c5f1ca3cc7cafc51b88924d47da9c162a516e289 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 12 Sep 2025 14:17:04 +0200 Subject: [PATCH 02/18] ci: explicitly restrict `GITHUB_TOKEN` permissions Yes, yes, CodeQL. It is totally _possible_ that someone with admin privileges will change the default back to the unsafe `write` permission. Even if unlikely, let's make it explicit that the `build` workflow only requires `read` permission. Even in a PR that is only modifying that workflow, and whose purpose is a totally different one. Let's conflate separations of concerns. Whatever. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7f1172aa7..9067c0b6f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,6 +12,9 @@ on: required: false type: string +permissions: + contents: read + env: GIT_VERSION: ${{ github.event.inputs.git_version || 'v2.50.1.vfs.0.1' }} From 5ca1b9972e051f9408da3d6c2744493c671735a9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 11 Sep 2025 11:32:50 +0200 Subject: [PATCH 03/18] ci: avoid failing fast in the matrix jobs Most of the failures we encounter in VFSforGit's CI runs are flakes. In those instances, it is not helpful but rather annoying when long-running jobs are canceled after more than half an hour when they could have run to completion and succeeded instead. Because when those runs fail, the first thing I do is to hit "Re-run" to see whether it _is_ a flake. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9067c0b6f..f9e26ed89 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -44,6 +44,7 @@ jobs: strategy: matrix: configuration: [ Debug, Release ] + fail-fast: false steps: - name: Checkout source @@ -105,6 +106,7 @@ jobs: matrix: configuration: [ Debug, Release ] architecture: [ x86_64, arm64 ] + fail-fast: false # most failures are flaky tests, no need to stop the other jobs from succeeding steps: - name: Download installers From a08ef4c43901dbb18f97d20ac9665a451bf8b5fb Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 12 Sep 2025 12:28:28 +0200 Subject: [PATCH 04/18] ci: skip running if the same commit has already seen a successful run GitHub Actions sadly lacks the feature where you can reuse prior runs that were successful on the same `head_sha`. Similar to what I did in git/git@99fe06cbfd6 (ci: avoid building from the same commit in parallel, 2023-08-23), let's simply skip the CI run if the same commit has been tested successfully before. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 46 +++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f9e26ed89..90863bbaf 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,11 +22,53 @@ jobs: validate: runs-on: windows-2025 name: Validation + outputs: + skip: ${{ steps.check.outputs.result }} + steps: + - name: Look for prior successful runs + id: check + uses: actions/github-script@v7 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + /* + * Look for any successful run for the same commit. No need to run it again, right? + */ + 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 + const { data: runs } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 500, + workflow_id, + status: 'success', + }); + for (const run of runs.workflow_runs) { + if (head_sha === run.head_sha) return true + } + return false + } catch (e) { + core.error(e) + core.warning(e) + } + - name: Checkout source + if: steps.check.outputs.result != 'true' uses: actions/checkout@v5 - name: Validate Microsoft Git version + if: steps.check.outputs.result != 'true' shell: pwsh env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -42,6 +84,7 @@ jobs: needs: validate strategy: + if: needs.validate.outputs.skip != 'true' matrix: configuration: [ Debug, Release ] fail-fast: false @@ -100,9 +143,10 @@ jobs: functional_test: runs-on: ${{ matrix.architecture == 'arm64' && 'windows-11-arm' || 'windows-2025' }} name: Functional Tests - needs: build + needs: [validate, build] strategy: + if: needs.validate.outputs.skip != 'true' matrix: configuration: [ Debug, Release ] architecture: [ x86_64, arm64 ] From 2edc6c182001d3c01180000b5126303357c4c1be Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 12 Sep 2025 12:33:11 +0200 Subject: [PATCH 05/18] ci: skip also for successful runs for _tree-same_ commits As far as VFSforGit's CI is concerned, there is no difference between commits when their _files_ have not changed (it's not like the tip commit's SHA is persisted in some version number or some such). Therefore, if there is already a successful run for another commit whose top-level tree is identical to the one that is about to be tested, skip the current run in favor of that one. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 90863bbaf..36e4ead5a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -34,6 +34,8 @@ jobs: script: | /* * Look for any successful run for the same commit. No need to run it again, right? + * Also allow for _tree-same_ commits, i.e. commits whose SHA is different, but whose + * top-level trees' SHA isn't. */ try { // Figure out workflow ID, commit and tree @@ -46,7 +48,7 @@ jobs: const head_sha = run.head_sha; const tree_id = run.head_commit.tree_id; - // See whether there is a successful run for that commit + // 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, @@ -56,6 +58,7 @@ jobs: }); for (const run of runs.workflow_runs) { if (head_sha === run.head_sha) return true + else if (tree_id === run.head_commit?.tree_id) return true } return false } catch (e) { From 4ee8c1c9a176d98ca9b3fcfe0013b3dd93d20bbb Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 12 Sep 2025 12:41:57 +0200 Subject: [PATCH 06/18] ci: when skipping runs, do not skip the required jobs To increase confidence in Pull Requests, a couple of Checks are required, for example the PR build must pass. However, due to one of GitHub Actions' limitations that seem to stay with us forever, it is impossible to require a specific _workflow_ to succeed for a PR before it can be merged. You have to specify workflow _jobs_ that are required. Therefore, the changes I just made to skip runs when there already exists a successful run for the same commit (or at least a tree-same one), would eternally prevent affected PRs from being merged. While the _workflow_ succeeds in such instances, the (required) workflow _jobs_ would be skipped (and therefore the required checks would not be fulfilled). Let's bite the bullet and patch through the information whether to skip re-running the tests _to the individual jobs_, so that they do run, and the do get marked as successful, and the Pull Requests can proceed. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 60 ++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 36e4ead5a..74bda369a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,9 +33,19 @@ jobs: github-token: ${{secrets.GITHUB_TOKEN}} script: | /* - * Look for any successful run for the same commit. No need to run it again, right? - * Also allow for _tree-same_ commits, i.e. commits whose SHA is different, but whose - * top-level trees' SHA isn't. + * 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 @@ -87,38 +97,53 @@ jobs: needs: validate strategy: - if: needs.validate.outputs.skip != 'true' matrix: configuration: [ Debug, Release ] fail-fast: false steps: + - name: Skip this job if there is a previous successful run + if: needs.validate.outputs.skip == 'true' + id: skip + uses: actions/github-script@v7 + with: + script: | + core.info(`Skipping: There already is a successful run`) + 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 }} @@ -126,18 +151,21 @@ jobs: 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 }} @@ -149,45 +177,59 @@ jobs: needs: [validate, build] strategy: - if: needs.validate.outputs.skip != 'true' matrix: configuration: [ Debug, Release ] architecture: [ x86_64, arm64 ] 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 == 'true' + id: skip + uses: actions/github-script@v7 + with: + script: | + core.info(`Skipping: There already is a successful run`) + 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() + if: always() && steps.skip.outputs.result != 'true' uses: actions/upload-artifact@v4 with: name: InstallationLogs_${{ matrix.configuration }}_${{ matrix.architecture }} 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% @@ -195,20 +237,20 @@ jobs: ft\GVFS.FunctionalTests.exe /result:TestResult.xml --ci - name: Upload functional test results - if: always() + if: always() && steps.skip.outputs.result != 'true' uses: actions/upload-artifact@v4 with: name: FunctionalTests_Results_${{ matrix.configuration }}_${{ matrix.architecture }} path: TestResult.xml - name: Upload Git trace2 output - if: always() + if: always() && steps.skip.outputs.result != 'true' 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() + if: always() && steps.skip.outputs.result != 'true' shell: cmd run: install\info.bat From 5099500912f2026915cbc54e7439f2cd64ddf262 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 12 Sep 2025 13:01:38 +0200 Subject: [PATCH 07/18] ci: when skipping runs, report the previous successful one It is always nice when one is given a good reason for something. For example, when a CI run is skipped, it is nice to not only to know that there _has_ been a previous successful run, but also _which one_. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 74bda369a..ee0c17bb1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -31,6 +31,7 @@ jobs: 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 @@ -67,21 +68,21 @@ jobs: status: 'success', }); for (const run of runs.workflow_runs) { - if (head_sha === run.head_sha) return true - else if (tree_id === run.head_commit?.tree_id) return true + if (head_sha === run.head_sha) return run.html_url + else if (tree_id === run.head_commit?.tree_id) return run.html_url } - return false + return '' } catch (e) { core.error(e) core.warning(e) } - name: Checkout source - if: steps.check.outputs.result != 'true' + if: steps.check.outputs.result == '' uses: actions/checkout@v5 - name: Validate Microsoft Git version - if: steps.check.outputs.result != 'true' + if: steps.check.outputs.result == '' shell: pwsh env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -103,12 +104,12 @@ jobs: steps: - name: Skip this job if there is a previous successful run - if: needs.validate.outputs.skip == 'true' + if: needs.validate.outputs.skip != '' id: skip uses: actions/github-script@v7 with: script: | - core.info(`Skipping: There already is a successful run`) + core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`) return true - name: Checkout source @@ -184,12 +185,12 @@ jobs: steps: - name: Skip this job if there is a previous successful run - if: needs.validate.outputs.skip == 'true' + if: needs.validate.outputs.skip != '' id: skip uses: actions/github-script@v7 with: script: | - core.info(`Skipping: There already is a successful run`) + core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`) return true - name: Download installers From a9fc9a57c696a9b4514ec8f324a370dc6f70508f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 12 Sep 2025 13:40:26 +0200 Subject: [PATCH 08/18] ci: when looking for successful workflow runs, wait for in-progress ones The Functional Test jobs takes long enough (35 minutes for x86_64, 1 hour 15 minutes for ARM64) that it is worth waiting for them to finish before kicking off a matrix build that tests the exact same code. So let's do that, wait for any in-progress run that might result in some saved time. Granted, this spends cycles of GitHub runners waiting idly, but GitHub does not provide for a better way. Note: The `concurrency` attribute _might_ look as helpful in this context at first sight. But is comically limited: It can only let a _single_ job wait in a queue, subsequent jobs get canceled (and if you have notifications turned on, in a loud manner!). And even letting the job wait would not help here because we also want to handle _tree-same_ commits, not only identical commits, and GitHub does not provide for a way to identify them in GitHub workflow expressions, either. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ee0c17bb1..ea1579d61 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -65,11 +65,30 @@ jobs: repo: context.repo.repo, per_page: 500, workflow_id, - status: 'success', }); for (const run of runs.workflow_runs) { - if (head_sha === run.head_sha) return run.html_url - else if (tree_id === run.head_commit?.tree_id) return run.html_url + 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.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 === 'success') return run.html_url } return '' } catch (e) { From 65755726a33be3c829d864a3a9209d66559fb35b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 12 Sep 2025 13:40:26 +0200 Subject: [PATCH 09/18] ci: when looking for successful workflow jobs, use a smart order There might be runs for the very same commit, those should be looked at first. Then there might be runs that have not yet finished; Let's first look at the finished ones. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ea1579d61..92a850693 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -66,7 +66,10 @@ jobs: per_page: 500, workflow_id, }); - for (const run of runs.workflow_runs) { + // 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 ;-) From 45580086fb34570b48ef2f4b8d8c990aacf47450 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 15 Sep 2025 20:19:34 +0200 Subject: [PATCH 10/18] ci: exclude `workflow_dispatch` runs from the "skip same run" logic In `workflow_dispatch`-triggered runs, the user can specify a Git version to use for testing, which means that something different is actually tested than in regular, non-`workflow_dispatch` runs. So let's neither skip the build/test when triggered thusly, nor reuse any results from thusly-triggered runs. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 92a850693..74c6f8e47 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -28,6 +28,7 @@ jobs: 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}} @@ -72,6 +73,7 @@ jobs: 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 From 095becccc21684fef77f4d9d39bdaff80b61d21a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 12 Sep 2025 20:06:50 +0200 Subject: [PATCH 11/18] Prepare for running the Functional Tests in parallel This teaches a new command-line option to `GVFS.FunctionalTests.exe` that allows running only a slice of all tests at a time, via `--slice=,` where `m` is the zero-based index of the slice to run, and `n` is the total number of slices to split the tests into. The idea is to distribute the test cases evenly across the slices, so that running all slices in parallel will finish in about the same time as running all tests in a single slice. This is slightly tricky because the test cases of classes within the `EnlistmentPerFixture` namespace are known to rely on side effects of previous test cases within the same class. Therefore, all test cases of such a class must be run in the same slice. Signed-off-by: Johannes Schindelin --- GVFS/GVFS.FunctionalTests/Program.cs | 21 +++++- GVFS/GVFS.Tests/NUnitRunner.cs | 102 ++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) 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); From 64d82bd93d4388e09829454873cfd472d8f9d797 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 16 Sep 2025 12:55:30 +0200 Subject: [PATCH 12/18] ci: fix skipping logic In https://github.com/microsoft/VFSForGit/pull/1866 I introduced logic to skip builds and tests when a previous workflow run already succeeded for the same commit. However, the original revision of that Pull Request tried something _even more_ elaborate, and when I dropped that elaborate logic (because it is a bit fragile), I made a mistake in the replacement logic. The `run.status` can never be `success`, it can be `completed` and _`run.conclusion`_ can be `success`. So let's test for that instead ;-) Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 74c6f8e47..dbcea1640 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -93,7 +93,7 @@ jobs: } } - if (run.status === 'success') return run.html_url + if (run.status === 'completed' && run.conclusion === 'success') return run.html_url } return '' } catch (e) { From 687c2486bb65c8668551b5f5c74aefc9d726b711 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 15 Sep 2025 17:48:49 +0200 Subject: [PATCH 13/18] ci: run Functional Tests in parallel Following the example of git/git, which runs the Windows tests in parallel to compensate for a very long run time, we split the Functional Tests into 10 buckets and run them in parallel. This is particularly useful in light of some flaky tests that frequently need to be re-run. That way, the cost of re-running a failed test is reduced by virtue of only having to re-run a _slice_ of the tests. Signed-off-by: Johannes Schindelin --- .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 dbcea1640..af6e39b2a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -205,6 +205,7 @@ jobs: 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: @@ -250,7 +251,7 @@ jobs: if: always() && steps.skip.outputs.result != 'true' uses: actions/upload-artifact@v4 with: - name: InstallationLogs_${{ matrix.configuration }}_${{ matrix.architecture }} + name: InstallationLogs_${{ matrix.configuration }}_${{ matrix.architecture }}-${{ matrix.nr }} path: install\logs - name: Run functional tests @@ -259,20 +260,20 @@ jobs: 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 + 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 }} + 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 }} + name: GitTrace2_${{ matrix.configuration }}_${{ matrix.architecture }}-${{ matrix.nr }} path: C:\temp\git-trace2.log - name: ProjFS details (post-test) From 9e8e7ce54f3ed32b747a7d779bf2386f896b5784 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 16 Sep 2025 09:41:55 +0200 Subject: [PATCH 14/18] ci: do provide the job names expected by the Required PR Checks In the GitHub UI, it is possible to specify Checks that need to be successful before a PR can be merged. These "Checks" are actually the names of jobs in the workflow. It would be much better to specify a required _workflow_, but only workflow _jobs_ can be specified. The Required Checks are currently: - Functional Tests (Debug, arm64) - Functional Tests (Debug, x86_64) - Functional Tests (Release, arm64) - Functional Tests (Release, x86_64) So let's make sure that there are job names that match the recorded names and that indicate a successful workflow run. An alternative would be to add a single job with a defined name that depends on all the other jobs, and that can compensate for the missing GitHub feature to require a whole workflow instead of individual jobs to have run successfully. But that would interfere with other PRs that are currently in flight, and will therefore have to wait its turn in a separate PR. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index af6e39b2a..298e81df5 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -280,3 +280,17 @@ jobs: 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 }}!" \ No newline at end of file From 040240bb1875f1226805a0ad366b39a0e90b4ccf Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 16 Sep 2025 09:49:36 +0200 Subject: [PATCH 15/18] ci: prepare for a single Required Check GitHub has a feature called "Require status checks before merging": https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging What they actually mean by "status checks" is "successful workflow job runs". And what they _really_ actually mean is "_display labels_ of successful workflow job runs". That is, workflow job runs are identified by the label they display. Example: In VFSforGit, there is currently a "required status check" that reads "Functional Test (Debug, arm64)". This is the label that is displayed for the matrix job with vector {configuration: Debug, architecture: arm64} in the "functional_test" workflow job. This is quite restrictive! In VFSforGit, specifying those four matrix job labels is a _work-around_ for the _actual_ requirement, namely that the workflow defined in `build.yaml` succeeds. It just so happens that those four matrix jobs are the leaf jobs, i.e. when they all succeed, the workflow _run_ has succeeded. And vice versa, if the workflow run failed, at least one of those four matrix jobs must have failed or not even run. Now that I multiplied the matrix jobs even further by running the Functional Tests in parallel, the _display labels_ (and the number) of the matrix jobs has changed. As a consequence, to appease the "Require status checks before merging" rule, I had to add _another_ set of matrix jobs just to guarantee that the same four matrix job labels exist. This is silly, because those four matrix jobs are not needed at all for actually testing the code. They are just there to make GitHub happy. This commit prepares to change that. It adds a new workflow job that fits the bill "if the job succeeded, the workflow run must have succeeded as a hole, and vice versa". This new job will be made the only "required status check", once this here PR has been merged, and then we can remove the silly "duplicate" matrix jobs again. Signed-off-by: Johannes Schindelin --- .github/workflows/build.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 298e81df5..fe238c532 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -293,4 +293,13 @@ jobs: 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 }}!" \ No newline at end of file + 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 From a60e8063142b2aab2c514163105426b7c727b13c Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 18 Sep 2025 11:47:51 +0100 Subject: [PATCH 16/18] Directory.Build.props: don't include commit ID in version Since .NET SDK 8, the Informational Version attribute includes the commit ID of the current HEAD at build time. The product code relies on this value to be System.Version-compliant, so we should opt out of this new behaviour. https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/8.0/source-link Signed-off-by: Matthew John Cheetham --- Directory.Build.targets | 6 ++++++ 1 file changed, 6 insertions(+) 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 From fcee3edbd71c33ff0ee6d18c491875aaceacbfb4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 18 Sep 2025 13:56:44 +0200 Subject: [PATCH 17/18] IsDevelopmentVersion(): tighten the check It is not _any_ version that starts with `0.` that is a development version, instead it is the version hard-coded in `Version.props` when no `GVFSVersion` has been specified. Signed-off-by: Johannes Schindelin --- GVFS/GVFS.Common/ProcessHelper.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) 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) From 436bab46b91fa1836f339f2989df1547f621e3f5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 18 Sep 2025 13:59:45 +0200 Subject: [PATCH 18/18] TryValidateGVFSVersion: allow for the version to include a commit ID In .NET SDK 8, the Informational Version attribute of the assembly includes the commit ID of the current HEAD at build time: https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/8.0/source-link This kind of version might be compliant with SemVer v2.0, but it is _not_ System.Version-compliant. Therefore, we opted out of this new behavior _really quickly_ after upgrading from SDK 3 to SDK 9, in order to fix cloning from repositories where `gvfs/config` requires certain minimal VFSforGit versions. In this here commit, we adapt the logic to strip off the commit ID if present. That way, the version check would continue to work even if we opted back in to this new behavior that includes the commit SHA in `gvfs version`'s output. Signed-off-by: Johannes Schindelin --- GVFS/GVFS/CommandLine/GVFSVerb.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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