Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ updates:
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
cooldown:
default-days: 7
20 changes: 10 additions & 10 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Look for prior successful runs
id: check
if: github.event.inputs.git_version == ''
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GITHUB_TOKEN}}
result-encoding: string
Expand Down Expand Up @@ -199,7 +199,7 @@ jobs:

- name: Checkout source
if: steps.check.outputs.result == ''
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Validate Microsoft Git version
if: steps.check.outputs.result == ''
Expand Down Expand Up @@ -249,7 +249,7 @@ jobs:

- name: Upload microsoft/git installers
if: steps.check.outputs.result == ''
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: MicrosoftGit
path: MicrosoftGit
Expand All @@ -269,27 +269,27 @@ jobs:
- name: Skip this job if there is a previous successful run
if: needs.validate.outputs.skip != ''
id: skip
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
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@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: src

- name: Install .NET SDK
if: steps.skip.outputs.result != 'true'
uses: actions/setup-dotnet@v6
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: src/global.json

- name: Add MSBuild to PATH
if: steps.skip.outputs.result != 'true'
uses: microsoft/setup-msbuild@v3.0.0
uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0

- name: Build VFS for Git
if: steps.skip.outputs.result != 'true'
Expand All @@ -308,21 +308,21 @@ jobs:

- name: Upload functional tests drop
if: steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: FunctionalTests_${{ matrix.configuration }}_${{ matrix.architecture }}
path: artifacts\GVFS.FunctionalTests

- name: Upload FastFetch drop
if: steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: FastFetch_${{ matrix.configuration }}_${{ matrix.architecture }}
path: artifacts\FastFetch

- name: Upload GVFS installer
if: steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: GVFS_${{ matrix.configuration }}_${{ matrix.architecture }}
path: artifacts\GVFS.Installers
Expand Down
22 changes: 11 additions & 11 deletions .github/workflows/functional-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
- name: Skip this job if there is a previous successful run
if: inputs.skip != ''
id: skip
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
core.info(`Skipping: There already is a successful run: ${{ inputs.skip }}`)
Expand All @@ -80,7 +80,7 @@ jobs:
id: download-git
if: steps.skip.outputs.result != 'true'
continue-on-error: true
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.git_artifact_name }}
path: git
Expand All @@ -90,7 +90,7 @@ jobs:

- name: Download Git installer (retry)
if: steps.skip.outputs.result != 'true' && steps.download-git.outcome == 'failure'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.git_artifact_name }}
path: git
Expand All @@ -102,7 +102,7 @@ jobs:
id: download-gvfs
if: steps.skip.outputs.result != 'true'
continue-on-error: true
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: GVFS_${{ matrix.configuration }}_${{ matrix.architecture }}
path: gvfs
Expand All @@ -112,7 +112,7 @@ jobs:

- name: Download GVFS installer (retry)
if: steps.skip.outputs.result != 'true' && steps.download-gvfs.outcome == 'failure'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: GVFS_${{ matrix.configuration }}_${{ matrix.architecture }}
path: gvfs
Expand All @@ -124,7 +124,7 @@ jobs:
id: download-ft
if: steps.skip.outputs.result != 'true'
continue-on-error: true
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: FunctionalTests_${{ matrix.configuration }}_${{ matrix.architecture }}
path: ft
Expand All @@ -134,7 +134,7 @@ jobs:

- name: Download functional tests drop (retry)
if: steps.skip.outputs.result != 'true' && steps.download-ft.outcome == 'failure'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: FunctionalTests_${{ matrix.configuration }}_${{ matrix.architecture }}
path: ft
Expand All @@ -145,7 +145,7 @@ jobs:
- name: Download FastFetch drop
if: steps.skip.outputs.result != 'true'
continue-on-error: true
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: FastFetch_${{ matrix.configuration }}_${{ matrix.architecture }}
path: ft
Expand Down Expand Up @@ -189,7 +189,7 @@ jobs:

- name: Upload installation logs
if: always() && steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
continue-on-error: true
with:
name: ${{ env.ARTIFACT_PREFIX }}InstallationLogs_${{ env.FT_MATRIX_NAME }}
Expand All @@ -208,14 +208,14 @@ jobs:

- name: Upload functional test results
if: always() && steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.ARTIFACT_PREFIX }}FunctionalTests_Results_${{ env.FT_MATRIX_NAME }}
path: TestResult.xml

- name: Upload Git trace2 output
if: always() && steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.ARTIFACT_PREFIX }}GitTrace2_${{ env.FT_MATRIX_NAME }}
path: C:\temp\git-trace2.log
Expand Down
33 changes: 25 additions & 8 deletions .github/workflows/upgrade-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: Skip this job if there is a previous successful run
if: inputs.skip != ''
id: skip
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
core.info(`Skipping: There already is a successful run: ${{ inputs.skip }}`)
Expand All @@ -66,14 +66,14 @@ jobs:
id: download-git
if: steps.skip.outputs.result != 'true'
continue-on-error: true
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: MicrosoftGit
path: git

- name: Download Git installer (retry)
if: steps.skip.outputs.result != 'true' && steps.download-git.outcome == 'failure'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: MicrosoftGit
path: git
Expand All @@ -82,14 +82,14 @@ jobs:
id: download-gvfs
if: steps.skip.outputs.result != 'true'
continue-on-error: true
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: GVFS_${{ matrix.configuration }}_x64
path: gvfs-new

- name: Download current GVFS installer (retry)
if: steps.skip.outputs.result != 'true' && steps.download-gvfs.outcome == 'failure'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: GVFS_${{ matrix.configuration }}_x64
path: gvfs-new
Expand Down Expand Up @@ -118,8 +118,25 @@ jobs:
run: |
$ErrorActionPreference = 'Stop'

$lkgInstaller = (Get-ChildItem gvfs-lkg\SetupGVFS*.exe).FullName
$newInstaller = (Get-ChildItem gvfs-new\SetupGVFS*.exe).FullName
# Releases publish both x64 and arm64 installers. The x64 installer
# keeps the historical suffix-less name "SetupGVFS.<version>.exe"; other
# architectures add a suffix (e.g. "SetupGVFS.<version>-arm64.exe").
# These tests run on an x64 runner, so select the x64 installer by its
# suffix-less name and require exactly one match, rather than picking an
# arbitrary file when the directory holds more than one installer.
# NOTE: arm64 upgrade is not exercised here because the runner is x64;
# arm64 upgrade coverage is a known gap for when arm64 runners exist.
function Select-X64Installer($directory) {
$installers = @(Get-ChildItem "$directory\SetupGVFS*.exe" |
Where-Object { $_.Name -match '^SetupGVFS\.[\d.]+\.exe$' })
if ($installers.Count -ne 1) {
throw "Expected exactly one x64 installer in '$directory', found $($installers.Count): $($installers.Name -join ', ')"
}
return $installers[0].FullName
}

$lkgInstaller = Select-X64Installer "gvfs-lkg"
$newInstaller = Select-X64Installer "gvfs-new"
$installDir = "C:\Program Files\VFS for Git"
$testRepo = "https://dev.azure.com/gvfs/ci/_git/ForTests"
$enlistment = "C:\gvfs-upgrade-test"
Expand Down Expand Up @@ -370,7 +387,7 @@ jobs:

- name: Upload service logs
if: always() && steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
continue-on-error: true
with:
name: UpgradeTest_Logs_${{ matrix.scenario }}
Expand Down
7 changes: 7 additions & 0 deletions GVFS/GVFS.Common/GVFSConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public static class GitConfig
public const string TrustPackIndexes = GVFSPrefix + "trust-pack-indexes";
public const bool TrustPackIndexesDefault = true;

/* Kill switch for the destructive part of packfile-maintenance corruption recovery: when
* false, GVFS still detects and reports corrupt packs but does not delete them (or later
* prefetch packs) and does not request a restoring prefetch. Detection/telemetry is
* unaffected; the non-destructive multi-pack-index rewrite still runs. */
public const string EnablePackfileRecovery = GVFSPrefix + "enable-packfile-recovery";
public const bool EnablePackfileRecoveryDefault = true;

public const string ShowHydrationStatus = GVFSPrefix + "show-hydration-status";
public const bool ShowHydrationStatusDefault = false;

Expand Down
11 changes: 11 additions & 0 deletions GVFS/GVFS.Common/Git/GitProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,17 @@ public Result VerifyMultiPackIndex(string objectDir)
return this.InvokeGitAgainstDotGitFolder("-c core.multiPackIndex=true multi-pack-index verify --object-dir=\"" + objectDir + "\" --no-progress");
}

/// <summary>
/// Verifies the integrity of a single packfile via its .idx. Returns a failure exit code if the
/// pack is truncated or otherwise unreadable. Used by pack maintenance recovery to determine
/// which pack is corrupt - the "could not load pack N" ordinal reported by the multi-pack-index
/// is an internal position, not a filename, so it cannot be mapped to a file directly.
/// </summary>
public Result VerifyPack(string packIndexPath)
{
return this.InvokeGitAgainstDotGitFolder("verify-pack \"" + packIndexPath + "\"");
}

public Result RemoteAdd(string remoteName, string url)
{
return this.InvokeGitAgainstDotGitFolder("remote add " + remoteName + " " + url);
Expand Down
54 changes: 53 additions & 1 deletion GVFS/GVFS.Common/Http/HttpRequestor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ protected GitEndPointResponseData SendRequest(
shouldRetry = false;
errorMessage = "Anonymous request was rejected with a 401";
}
else if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.BadRequest || response.StatusCode == HttpStatusCode.Redirect)
else if (ShouldRejectCredentials(response.StatusCode, errorMessage))
{
this.authentication.RejectCredentials(this.Tracer, authString);
if (!this.authentication.IsBackingOff)
Expand Down Expand Up @@ -326,6 +326,58 @@ private static bool ShouldRetry(HttpStatusCode statusCode)
return false;
}

/// <summary>
/// The message the Azure DevOps GVFS cache server returns in a 400 (Bad Request)
/// body when the request carried no parseable Basic Authorization header - i.e.
/// the one 400 that genuinely means "authentication required".
/// </summary>
/// <remarks>
/// Mirrors the cache server's own message, emitted by
/// GvfsHttpHandler.PrepareContextAsync as
/// $"A valid {scheme} {header} header is required." with scheme="Basic" and
/// header="Authorization". Kept as a literal (not a format) so a substring match
/// stays robust if the server text is wrapped or prefixed.
/// </remarks>
internal const string CacheServerAuthRequiredBadRequestMessage = "A valid Basic Authorization header is required.";

/// <summary>
/// Determines whether an HTTP response indicates an authentication failure
/// that warrants rejecting (erasing) the stored credential.
/// </summary>
/// <remarks>
/// 401 (Unauthorized) and 302 (Redirect to the Azure DevOps sign-in page) are
/// always genuine authentication failures. A 400 (Bad Request) is usually NOT an
/// auth failure - a present-but-expired/invalid credential returns 401, and a
/// malformed request (e.g. a corrupt object SHA in the loose-object URL) returns a
/// 400 that has nothing to do with credentials. Rejecting credentials on every 400
/// erased valid credentials and caused a storm of credential-manager popups.
///
/// The one exception: the GVFS cache server returns a 400 (instead of a 401) when
/// the request carried no parseable Basic Authorization header. That single 400 is
/// genuinely "authentication required", and microsoft/git's git-gvfs-helper maps it
/// to a 401 for the same reason (its normalize step notes the cache server "sends a
/// somewhat bogus 400 instead of the normal 401 when AUTH is required", and its TODO
/// asks to confirm the response body - which is exactly what we do here). We only
/// treat a 400 as an auth failure when the body matches that specific message.
/// </remarks>
internal static bool ShouldRejectCredentials(HttpStatusCode statusCode, string responseBody)
{
if (statusCode == HttpStatusCode.Unauthorized ||
statusCode == HttpStatusCode.Redirect)
{
return true;
}

if (statusCode == HttpStatusCode.BadRequest &&
responseBody != null &&
responseBody.IndexOf(CacheServerAuthRequiredBadRequestMessage, StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}

return false;
}

private static string GetSingleHeaderOrEmpty(HttpHeaders headers, string headerName)
{
IEnumerable<string> values;
Expand Down
14 changes: 12 additions & 2 deletions GVFS/GVFS.Common/Maintenance/GitMaintenanceScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ private void ScheduleRecurringSteps()
return;
}

if (this.gitObjects.IsUsingCacheServer())
bool usingCacheServer = this.gitObjects.IsUsingCacheServer();

if (usingCacheServer)
{
TimeSpan prefetchPeriod = TimeSpan.FromMinutes(15);
this.stepTimers.Add(new Timer(
Expand All @@ -70,8 +72,16 @@ private void ScheduleRecurringSteps()
dueTime: this.looseObjectsDueTime,
period: this.looseObjectsPeriod));

// When packfile-maintenance recovery removes a corrupt prefetch pack (and the later prefetch
// packs that depend on it), it needs a prefetch to re-download them and rebuild the
// commit-graph. This is only meaningful when a cache server is in use; otherwise the objects
// are restored on demand.
Action requestPrefetch = usingCacheServer
? () => this.queue.TryEnqueue(new PrefetchStep(this.context, this.gitObjects))
: (Action)null;

this.stepTimers.Add(new Timer(
(state) => this.queue.TryEnqueue(new PackfileMaintenanceStep(this.context)),
(state) => this.queue.TryEnqueue(new PackfileMaintenanceStep(this.context, requestPrefetch: requestPrefetch)),
state: null,
dueTime: this.packfileDueTime,
period: this.packfilePeriod));
Expand Down
Loading
Loading