Skip to content

Merge master into vnext (20260827) - #2098

Open
tyrielv wants to merge 11 commits into
microsoft:vnextfrom
tyrielv:tyrielv/merge-master-to-vnext-20260827
Open

Merge master into vnext (20260827)#2098
tyrielv wants to merge 11 commits into
microsoft:vnextfrom
tyrielv:tyrielv/merge-master-to-vnext-20260827

Conversation

@tyrielv

@tyrielv tyrielv commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Routine forward-merge of master into vnext to keep vnext current so master's fixes flow down and the two lines do not diverge. Merge commit only (no squash/rebase).

master was 10 commits ahead of vnext. Commits brought down:

The merge was clean with no conflicts.

tyrielv and others added 11 commits August 7, 2026 15:43
When a packfile in the shared object cache is corrupt or truncated (e.g. from a
past disk-full event), 'git multi-pack-index write/verify' fails with "could not
load pack N". The existing self-heal only deletes and rewrites the
multi-pack-index (MIDX), which does not fix the underlying pack: the rewrite
re-scans the same bad pack and keeps failing. The corruption then recurs
indefinitely.

PackfileMaintenanceStep now routes write/verify failures through a recovery path
that, when git reports a pack-load failure:

  - Detection (always runs, even with recovery disabled): verifies each pack in
    the object cache with 'git verify-pack' and reports every unreadable pack via
    telemetry (Operation=FoundCorruptPack). The "could not load pack N" ordinal is
    an internal MIDX position, not a filename, so per-pack verification is how we
    find the actual bad file.
  - Removal (gated, see kill switch below): deletes each corrupt pack's files
    (.pack/.idx/.keep/.rev; Operation=DeletedCorruptPack), then deletes and
    regenerates the MIDX from the packs that remain (fast path, no full repack).
    Missing objects are re-fetched on demand.
  - Corrupt prefetch pack (special case): prefetch packs are incremental and
    ordered by timestamp, so a corrupt one invalidates every later prefetch pack
    too - leaving a hole would let the newest surviving timestamp advance past it
    so a later prefetch never backfills the gap. Recovery removes the corrupt
    prefetch pack and every later prefetch pack
    (Operation=DeletedHealthyPrefetchPack for the healthy ones removed purely due
    to ordering), then requests a prefetch (via a callback GitMaintenanceScheduler
    wires to a PrefetchStep, only when using a cache server) to re-download them
    and rebuild the commit-graph.

Kill switch: the destructive pack removal is gated by a new git config,
gvfs.enable-packfile-recovery (default true). When false, GVFS still detects and
reports corrupt packs (Operation=FoundCorruptPack, then
CorruptPackRecoverySkipped) but deletes nothing and does not request a prefetch;
the non-destructive MIDX rewrite still runs, so behavior degrades to today's.
This gives a field kill switch without a redeploy if the destructive path ever
misbehaves.

This is stacked on the git-output bounding change: recovery runs additional git
commands (verify-pack, MIDX rewrites) against the corrupt repo, so it relies on
that change to keep a noisy stderr from OOM-ing the mount mid-recovery.

Review follow-ups:
  - prefetchRestoreNeeded is now set only after a corrupt prefetch pack is
    actually removed (RemovePackFileSet returns whether the .pack file was
    deleted), instead of as soon as one is detected. If deletion is blocked,
    the restore no longer runs while the corrupt pack is still present.
  - DetectAndRemoveCorruptPacks now remembers, for the lifetime of a single
    maintenance run, that it already reported corrupt packs with recovery
    disabled, and skips the redundant per-pack verify-pack rescan on later
    MIDX failures in that same run.
  - DetectAndRemoveCorruptPacks now parses the corrupt pack's filename directly
    out of the write/verify failure's stderr when git includes it (e.g.
    "packfile pack-1234.pack does not match index" / "wrong index v2 file size
    in pack-1234.idx"), and verifies only that candidate instead of every pack
    in the object cache. This only helps when git actually names the file,
    which it does for the verify-triggered failures this code mostly handles
    (not for the rarer write-path "could not load pack N", which is genuinely
    an unresolvable internal ordinal - confirmed by reading git's midx-write.c).
    Falls back to verifying every pack whenever no candidate can be parsed, or
    the parsed candidate turns out to be healthy, so detection is never less
    thorough than before.

Tests:
  - A verify failure reporting a pack-load error removes the corrupt pack and
    rewrites the MIDX from the remaining good packs (recovery enabled).
  - With recovery disabled, the same failure still verifies each pack and reports
    the corrupt one but deletes nothing.
  - A corrupt prefetch pack removes it and every later prefetch pack, keeps the
    earlier healthy one, and requests a prefetch.
  - A verify failure that names the corrupt pack directly verifies only that
    pack (fast path).
  - A verify failure that names a pack which turns out to be healthy falls back
    to verifying every pack (fallback path).

Assisted-by: Claude Sonnet 5
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
GVFS erased a valid credential when an object-download response was HTTP 400,
which triggered a storm of Git Credential Manager popups.

A 400 is a request or formatting problem, not an authentication failure. An
expired or invalid credential always returns 401 (Unauthorized) or 302 (the
Azure DevOps sign-in redirect), never 400. Treating 400 as an auth failure
erased good credentials and produced the misleading "Your PAT may be expired"
message.

Remove BadRequest (400) from the credential-rejection branch in SendRequest.
Only 401 and 302 now reject credentials; 400 flows through the generic,
non-auth error path (unchanged retry / circuit-breaker behavior, and 400
remains non-retryable). Extract the decision into ShouldRejectCredentials so it
is unit tested: 400 does not reject credentials, while 401 and 302 still do.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…"auth required" body

An earlier change dropped HTTP 400 from the credential-rejection branch entirely,
on the premise that a 400 is never an authentication failure. That premise is
incomplete. The Azure DevOps GVFS cache server returns a 400 (not a 401) in one
genuine authentication case: when the request carried no parseable Basic
Authorization header. Its response body is "A valid Basic Authorization header is
required." microsoft/git's git-gvfs-helper maps that same cache-server 400 to a
401 for this reason, and its own TODO says to confirm the response body - which is
what this change does.

A present-but-expired or invalid credential still returns 401, and a malformed
request (for example a corrupt object SHA in the loose-object URL) returns a 400
that has nothing to do with credentials. So the decision is now body-aware:

- 401 and 302 always reject credentials.
- 400 rejects credentials only when the body matches the cache server's
  auth-required message (case-insensitive substring).
- Every other 400 (and 404/5xx/timeouts) does not reject credentials.

This stops the credential-manager popup storm caused by rejecting a valid
credential on a non-auth 400, while preserving credential refresh for the one 400
that really does mean "authentication required", keeping the behavior consistent
with git-gvfs-helper.

Tests updated for the new body-aware signature and cases.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…-actions

Pin GitHub Actions to full-length commit SHAs
…-credentials-on-400

HttpRequestor: do not reject credentials on HTTP 400
…ption-recovery

Auto-recover corrupt packfiles in packfile maintenance
The upgrade test job selected the last-known-good installer with
`(Get-ChildItem gvfs-lkg\SetupGVFS*.exe).FullName`. Releases now publish
both an x64 and an arm64 installer, so the glob matches two files and
`.FullName` returns an array. `Start-Process -FilePath` then fails with
"Cannot convert 'System.Object[]' to the type 'System.String'".

Select the x64 installer explicitly. The x64 installer has no
architecture suffix; the arm64 one is named `SetupGVFS.<version>-arm64.exe`.
These tests run on an x64 runner and download the x64 "new" installer, so
the x64 LKG installer is the correct match. Apply the same guard to the
"new" installer selection and throw a clear error if no x64 installer is
present.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Address self-review feedback on the multi-installer fix. Replace the
`-arm64` denylist plus `Select-Object -First 1` with a shared
`Select-X64Installer` helper that positively matches the x64 asset by its
suffix-less name (`SetupGVFS.<version>.exe`) and requires exactly one match.

The denylist would still pass a future non-x64 asset (for example a `-x86`
or `-arm` installer) and `-First 1` would then pick an arbitrary file. The
positive allowlist matches the documented x64 naming contract and fails
loudly when the directory holds an unexpected number of installers. The
helper also removes the duplicated filter across the LKG and new installer
selection, and its error message reports the directory and the files found.

Note in a comment that arm64 upgrade is not exercised here because the
runner is x64.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…g-arm64

Fix upgrade tests when the LKG release has multiple installers
@tyrielv
tyrielv enabled auto-merge August 27, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants