Skip to content

Release M277.2#2067

Merged
tyrielv merged 45 commits into
releases/shippedfrom
milestones/M277.2
Jul 15, 2026
Merged

Release M277.2#2067
tyrielv merged 45 commits into
releases/shippedfrom
milestones/M277.2

Conversation

@tyrielv

@tyrielv tyrielv commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Changes:

tyrielv and others added 30 commits July 1, 2026 10:46
git invokes core.virtualfilesystem via the shell (use_shell=1 in
virtualfilesystem.c), so on Windows the shell has to probe PATHEXT
candidates (.COM, .EXE, ...) to resolve the bare "virtual-filesystem"
path to the actual "virtual-filesystem.exe" file that HooksInstaller
writes to disk. Since VFSForGit controls both the config value and the
file it points to, append GVFSPlatform.Instance.Constants.ExecutableExtension
so the path matches the file exactly, avoiding repeated PATHEXT probing
on every hook invocation. This is a no-op on platforms where
ExecutableExtension is empty (macOS/Linux).

Assisted-by: Claude Sonnet 5
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Include executable extension in core.virtualfilesystem path
The greedy bucket slicer keeps consecutive tests from the same fixture
class together when they share state. The grouping regex was overly
broad, using whole namespaces instead of specific class names:

  OLD: ^.*\.(?:EnlistmentPerFixture|MultiEnlistmentTests)\..+\.
  NEW: ^.*\.(?:EnlistmentPerFixture\..+|MultiEnlistmentTests\.ConfigVerbTests)\.

Two problems fixed:

1. GitCommands namespace (previous PR run): included all GitCommands
   tests, causing all 38 CheckoutTests(Full) methods (~16 min) to land
   in one slice.  GitCommands tests are safe to split: they use either a
   per-test enlistment or a git-checkout reset in [SetUp].

2. MultiEnlistmentTests namespace (this change): included all classes
   in the namespace, keeping SharedCacheTests (10 tests, ~7 min) and
   ServiceVerbTests together unnecessarily.
   - SharedCacheTests: [SetUp] generates a Guid-based cache path per
     test — no shared state across test methods.  Safe to split.
   - ServiceVerbTests: [NonParallelizable] blocks within-process
     concurrency; different slices run on different machines.  Safe.
   - ConfigVerbTests: uses [Order(1..8)] with each test reading config
     written by the previous.  Must stay together — kept explicitly.
   - EnlistmentPerFixture: still grouped per-class as before.

From PR #2028 baseline: critical path was 18.9 min.
After GitCommands fix: 9.85 min.
Expected after this change: ~6-7 min.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
fix: tighten FT slicer grouping regex to class-level
….vfs.0.4

Update default Microsoft Git version to v2.54.0.vfs.0.4
Under Windows "Administrator protection" (AP), an elevated process runs as a
hidden, profile-separated shadow admin account (MACHINE\admin_<user>) whose SID
is a local account (S-1-5-21-...) that differs from the real (non-elevated)
user's SID.

When `gvfs clone` runs elevated, the enlistment's `src` and `.git` directories
are created owned by the Administrators group. EnsureDirectoryIsOwnedByCurrentUser
then reassigned ownership to the current user to satisfy git's dubious-ownership
check. Under AP the "current user" is the shadow admin, so ownership was set to
admin_<user> — which the real user does not match. The real (non-elevated) user
then hit `fatal: detected dubious ownership`, because git's Administrators-group
membership grace does not cover another specific user's SID, and the shadow admin
cannot SetOwner to the real user's SID either.

Detect the AP shadow admin from the running process's own token — its effective
elevation identity — and, when present, leave the directory owner as the
Administrators group instead of reassigning it to the shadow admin. An
Administrators-owned directory is accepted by modern git and the libgit2
non-elevated-admin-owner overlay for any Administrators member — the real user,
the shadow admin, and SYSTEM (for automount) alike.

Detection inspects the token rather than the TypeOfAdminApprovalMode policy value
so it stays correct across an AP policy change that has not yet been rebooted
(where the policy value is pending but elevation still yields a shadow admin). A
shadow admin is identified as a local account SID (S-1-5-21-...) whose ProfileList
profile directory is ADMIN_-prefixed; the real interactive user (a domain or
Entra ID account) is excluded.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The `non-elevated-admin-owner.diff` overlay patch was copied verbatim from
libgit2 PR #7200. That PR version had four bugs that maintainers fixed
before merging to libgit2 `main`:

- `linked_token` HANDLE from TokenLinkedToken was never CloseHandle'd,
  leaking a handle in the long-lived GVFS mount service (SYSTEM).
- `current_user_sid` used `static HANDLE linked_token` — not thread-safe
  and retained a stale handle across calls; replaced with a per-call
  local initialized to NULL by the caller.
- The TokenLinkedToken failure path nulled the local pointer parameter
  (`linked_token = NULL`) instead of `*linked_token = NULL` (+CloseHandle).
- `current_user_sid()` was only invoked in the CURRENT_USER branch, but
  the admin-membership branch also reads `linked_token`; a caller passing
  only USER_IS_ADMINISTRATOR would read an uninitialized handle. It is now
  called unconditionally before either branch.

Regenerate the patch so that, applied to the pinned libgit2 source, it
produces the same corrected `src/util/fs_path.c` as upstream `main`
(commits cc477ee + f9f36a6 + 44c05e5, merge e805a16, merged 2026-06-07).
The patch is not yet in any libgit2 release, so the overlay remains
necessary.

Also bump the pinned libgit2 version 1.9.3 -> 1.9.4 (matching the official
vcpkg port). fs_path.c is byte-identical between 1.9.3 and 1.9.4, so the
patched region is unaffected and the diff applies cleanly to both.

Verified: Build.bat Debug rebuilds libgit2 for both x64-windows-static-aot
and x64-windows-dynamic triplets, applying non-elevated-admin-owner.diff
with no hunk failures, and the full solution builds to an installer.

Complements #2039 (the GVFS-side AP clone-ownership fix).
Work item: AB#62918022.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Fix clone ownership under Windows Administrator Protection
…ner-overlay

Re-sync libgit2 overlay admin-owner patch with merged upstream (bump to 1.9.4)
GVFS.Mount could crash with OutOfMemoryException. GitProcess.InvokeGitImpl
buffered all of a git process's stdout and stderr into unbounded StringBuilders
via the DataReceived handlers. A corrupt or truncated packfile in the shared
object cache makes 'git multi-pack-index' stream a large volume of "could not
load pack" errors to stderr, growing the buffer until the mount OOM-crashes.

Bound each captured stream with its own cap:

  - stderr: ~20 MB (10M chars). stderr is diagnostic, so truncating it is safe;
    this is the cap that stops the corrupt-packfile crash.
  - stdout: ~256 MB (128M chars) when the caller buffers the whole result.
    Sized to hold the machine-readable (-z) output of the largest real
    repositories - on the order of 2.8M status/diff records on the biggest
    os.2020 branches - with headroom, while staying far below the .NET maximum
    array/string size so the buffer itself cannot OOM. Commands that stream
    their output line-by-line are unaffected.

Truncation is surfaced on GitProcess.Result via OutputTruncated and
ErrorsTruncated so callers can react:

  - stdout carries correctness-critical data for two commands, which now fail
    safe on truncation rather than act on a partial result:
      - AddStagedFilesToModifiedPaths (git diff --cached --name-status -z):
        refuses to update ModifiedPaths from a partial staged-file list, which
        would otherwise leave staged files with skip-worktree set and stale
        placeholders.
      - SparseVerb's git status check (status --porcelain -z): aborts sparse
        rather than risk proceeding over uncommitted changes it failed to see.
    Both emit telemetry on truncation.
  - stderr truncation is expected and non-fatal; GitMaintenanceStep.RunGitCommand
    logs it for diagnostics but does not fail the command for it.

This is the first of a series: a follow-up converts those two -z commands to
stream their output (removing the truncation exposure entirely), and packfile
corruption auto-recovery is a separate change.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
SQLITE_IOERR is excluded from the retry-eligible set because it can fire
during connection disposal *after* a write has already committed to the WAL.
In ExecuteReadThenWrite (used by RemoveAllEntriesForFolder), this produces
a stale empty result on retry: the SELECT finds zero rows (DELETE already
committed), so the returned list of removed placeholders is wrong.  If the
dehydrate operation subsequently fails, the rollback loop re-adds nothing
and placeholder entries are silently lost.

BUSY and LOCKED are safe to retry: both mean the operation was blocked
before executing and was never committed.  SQLite guarantees this.

IOERR on a genuinely failed write is also rolled back atomically by SQLite,
but we cannot distinguish a post-commit IOERR (from Dispose) from a
pre-commit IOERR (from ExecuteNonQuery) at the catch site.  The safe
choice is to surface IOERR immediately and let the caller decide.

The root cause of the production LOCKED errors (#2031) was Cache=Shared,
which is already removed.  IOERR tolerance was speculative; removing it
eliminates a real (if rare) correctness hazard with no practical loss.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
PR #1980 made TryAttachToVolume tolerate ACCESS_DENIED from FilterAttach
whenever the ProjFS service is running. Because both callers share this
method, the elevated GVFS.Service (running as LocalSystem) also began
swallowing ACCESS_DENIED — but the service holds SE_LOAD_DRIVER_PRIVILEGE,
so ACCESS_DENIED there is never the benign "unprivileged caller" case. It
signals a real problem (AV/EDR lock, Group Policy, or driver corruption),
and tolerating it lets a broken mount proceed instead of surfacing an
actionable error. Before #1980 the service called raw TryAttach and
reported that failure.

FilterAttach's ACCESS_DENIED is a privilege gate, not a transient
contention error: an unelevated caller always gets it (regardless of the
actual attach state), while an elevated caller only gets it when something
is genuinely wrong. Narrow the tolerance to unelevated callers so the
unelevated `gvfs mount` fix is preserved while the elevated service once
again surfaces genuine attach failures. This also aligns the code with the
existing comment's stated intent ("when the caller lacks this privilege").

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Change the outer try-catch in HandleRequest from swallowing exceptions
to catch-log-rethrow. The original catch (PR #2021) suppressed all
exceptions except OutOfMemoryException, which could mask corruption in
state-mutating handlers like PostIndexChanged, DehydrateFolders, and
ReleaseLock — leaving the mount serving in an inconsistent state.

The catch now logs the message header and exception (context that
OnNewConnection's generic catch in NamedPipeServer does not capture),
then rethrows so fail-fast via Environment.Exit still occurs for
unhandled errors in state-mutating handlers.

The inner try-catch in HandleDownloadObjectRequest remains unchanged —
it correctly catches and returns DownloadFailed for transient
network/disk errors in the download path, which is safe because no
critical mount state is mutated.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
PR #2005 restructured mount startup so the named pipe server starts
before the parallel auth+validation wait. That early-pipe move is a
reliability improvement -- it widens the WaitUntilMounted connect
window, avoiding "GVFS.Mount process is not responding" failures when
auth is slow (including the SYSTEM-service automount path) -- and is
separable from the progress-display feature layered on top of it.

Add a disabled-by-default gvfs.mount-progress config flag that gates
only the display layer. When off, the mount process no longer surfaces
MountProgress phase strings over the named pipe, and the CLI falls back
to its existing static spinner (ConsoleHelper already renders just the
base message when progress is empty). The reliability infrastructure --
early pipe start, HandleRequest Mounting-state guard, volatile
currentState, and null-safe HandleGetStatusRequest -- is always on
regardless of the flag, so stabilization builds keep the automount
robustness without shipping the newer progress-display behavior.

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

Add two sections to AGENTS.md capturing repo facts that routinely mislead
agents and aren't obvious from a fresh clone:

- "What ships in a public release": FastFetch is built/signed but NOT
  attached to the GitHub release, and Git is not bundled — "default
  Microsoft Git version" bumps only change CI GIT_VERSION, while the shipped
  floor is MinimumGitVersion in Version.props. Both matter when scoping a
  release or writing changelog notes.
- "Feature flags": product flags are git config keys under the gvfs. prefix,
  declared in GVFSConstants.cs and read via GetConfigBoolOrDefault, mirroring
  gvfs.show-hydration-status. Gate the runtime entry point, default false.

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

ProjFS does not always deliver EndDirectoryEnumeration for a
StartDirectoryEnumeration (for example when an enumeration is cancelled), which
leaks the corresponding ActiveEnumeration - and the projected item list it pins -
in this.activeEnumerations. Over long-lived GVFS.Mount processes this unbounded
growth is a suspected contributor to the memory pressure behind the native ProjFS
command-completion crashes that surface downstream as
STATUS_FILE_SYSTEM_VIRTUALIZATION_UNAVAILABLE (0xC000CE01).

"Suspected" is the operative word: Watson shows radar_high_memory in
projectedfslib.dll!prjcompletecommand, but we do not yet have direct evidence that
the never-ended-enumeration leak is a dominant driver of that memory. This change
is therefore split into two parts so we can measure before we change the
enumeration hot path anywhere:

1. Observation telemetry (always on). ActiveEnumeration records a monotonic
   LastActivityTickCount (Environment.TickCount64) on creation and on every
   GetDirectoryEnumeration read. WindowsFileSystemVirtualizer now contributes
   ActiveEnumerationCount and ActiveCommandCount to the periodic Heartbeat event
   via a new FileSystemVirtualizer.AddHeartbeatMetadata hook. This ships the
   signal needed to confirm (or refute) that activeEnumerations grows without
   bound on real machines - independent of whether eviction is enabled.

2. Eviction (off by default). When the gvfs.max-active-enumerations git config is
   set to a positive value, a throttled sweep (at most once per minute, and only
   once the collection exceeds the configured count) evicts enumerations idle
   longer than a 5 minute timeout. Config unset or <= 0 disables eviction
   entirely, so the enumeration hot path is unchanged by default. Monotonic clocks
   are used for both the throttle and the staleness cutoff so wall-clock
   adjustments cannot disturb them, and the throttle claim uses an
   Interlocked.CompareExchange keyed on the previously-read tick so two threads
   entering the same interval cannot both sweep.

Evicting a live-but-idle enumeration is safe: the next GetDirectoryEnumeration
for that id misses activeEnumerations and returns HResult.InternalError - it fails
that one directory listing loudly rather than returning truncated results as if
complete - and every eviction is reported via telemetry.

This complements the stabilization-safe "do not crash the mount on a native ProjFS
failure" change (#2042), which is intentionally kept separate and does not touch
the enumeration hot path. Eviction stays behind the flag until the Heartbeat
telemetry confirms the leak is worth acting on.

Tests: ActiveEnumeration records activity time; Heartbeat metadata reports the
live enumeration count; with eviction disabled stale enumerations are retained;
with eviction enabled stale enumerations are evicted (and their subsequent End
fails with InternalError) while freshly-active ones are kept. Full unit suite
passes.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The in-product org auto-upgrade mechanism (OrgInfo server + upgrade ring +
NuGet feed) has been dead for years: `gvfs upgrade` is already a no-op stub
and nothing consumes these paths. Fleet upgrades are driven externally from
GitHub Releases.

Remove the dead code while keeping the `gvfs upgrade` verb in its current
no-op stub state:

- OrgInfoApiClient / VersionResponse (and its GVFSJsonContext registration)
- AzDevOpsOrgFromNuGetFeed (NuGet-feed org-parse helper, only referenced by
  the dead upgrader path and its own test)
- LocalGVFSConfig keys upgrade.ring / upgrade.feedpackagename /
  upgrade.feedurl / upgrade.orgInfoServerUrl
- the now-unreferenced GVFSConstants.UpgradeVerbMessages class
- MockLocalGVFSConfigBuilder and the removed types' unit tests
- installer ring->NuGet-feed plumbing (SetNuGetFeedIfNecessary,
  GetConfiguredUpgradeRing, IsConfigured, SetIfNotConfigured, UpgradeRing)

Build (including the Inno Setup installer) and all unit tests pass.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Bound git stdout/stderr capture to prevent GVFS.Mount OOM
Narrow IsTransientError to BUSY+LOCKED only; exclude IOERR
…e (0xC000CE01)

GVFS.Mount crashes are being reported that manifest downstream as
STATUS_FILE_SYSTEM_VIRTUALIZATION_UNAVAILABLE (0xC000CE01): once the mount
process dies, the ProjFS virtualization root is orphaned and every subsequent
placeholder access fails (for example GetOfficialBranch/libgit2 reading the
virtualized .git). Watson attributes the crashes to radar_high_memory
projectedfslib.dll!prjcompletecommand, with the dominant managed signatures
being NullReferenceException surfaced from the native ProjFS command-completion
(PrjCompleteCommand) and delete (PrjDeleteFile) paths under memory pressure.
VFS for Git is already on the latest Microsoft.Windows.ProjFS package, so the
native projectedfslib.dll fault cannot be fixed here directly.

This is not a v2.0 regression: the unguarded call sites date to the 2018
.NET Framework era. The v2.0 pure-C# ProjFS rewrite made the same latent native
fault legible as a managed NRE in GVFS telemetry, which is why reports increased
recently.

Do not crash the mount on a native ProjFS failure.

A single boundary helper, TryInvokeProjFS, wraps each GVFS-initiated native ProjFS
call: it catches the exception, emits high-signal *_NativeFailure telemetry
(including live enumeration/command counts and GC memory), and returns
HResult.InternalError so the caller maps it to a failure result and keeps serving
the mount. Routed through it: CompleteCommand (via TryCompleteCommand, covering
the completion leg of all async callbacks), DeleteFile, WritePlaceholderFile,
WritePlaceholderDirectory, UpdatePlaceholderIfNeeded, and ClearNegativePathCache.

MarkDirectoryAsPlaceholder mutates projection state, so swallowing is unsafe; it
uses a sibling helper InvokeProjFSOrThrow that emits the same telemetry and then
rethrows, preserving fail-fast. GetFileStreamHandlerAsyncHandler already fails a
single hydration on any exception, so it is left as-is.

When a native CompleteCommand fault is swallowed for a directory-enumeration
start, ProjFS never accepted the completion and will not send the matching
EndDirectoryEnumeration callback. TryCompleteCommand now reports the command as
not completed in that case, so StartDirectoryEnumerationAsyncHandler removes the
ActiveEnumeration it just registered instead of leaking it.

A companion change to bound the activeEnumerations leak (a suspected contributor
to the memory pressure) is deliberately kept out of this stabilization-safe fix
and tracked separately behind an off-by-default config flag, so that the
enumeration hot path is not altered until telemetry confirms the leak is a real
driver.

Tests: DeleteFile and the other outbound operations return IOError when the
virtualization instance throws; a directory-enumeration start whose native
completion faults removes its ActiveEnumeration rather than leaking it. Full unit
suite passes.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
`gvfs service --unmount-all` intermittently failed with exit code 3 and
"Already unmounting, please wait" when the service registry still listed a
repo whose GVFS.Mount process was in the transient Unmounting state (a
concurrent or prior unmount still shutting it down). The single IsRepoMounted
helper returned true whenever the mount pipe merely answered a GetStatus
request, regardless of the actual MountState, so --unmount-all would attempt
to unmount a repo already on its way out and treat that transient state as a
hard failure.

Replace IsRepoMounted with three intent-specific predicates built on a
TryGetRepoMountStatus primitive that reads the actual MountState:
  - IsRepoReady (Ready)                          -> --list-mounted
  - IsRepoAvailableToMount (no live process)     -> --mount-all
  - IsRepoAvailableToUnmount (Ready|MountFailed) -> --unmount-all

--unmount-all now skips repos that are Unmounting (already reaching the
desired state) or Mounting (an unmount request would be rejected anyway),
eliminating the spurious failure. --list-mounted now reports only fully-Ready
repos. --mount-all behavior is unchanged (it still mounts only when no live
mount process is answering).

StatusVerb exposes the "Mount status: " output prefix as a shared constant so
ServiceVerb recovers the MountState without re-implementing the pipe protocol.

Fixes the flaky
GVFS.FunctionalTests.Tests.MultiEnlistmentTests.ServiceVerbTests.ServiceCommandsWithNoRepos.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
PR #1989 fixed a PID-recycling race in orphan-lock detection by capturing the
holder's process start time at acquire and comparing it at each orphan check.
That fix is correct for the recycling case, but it collapsed every failure to
read the start time into a single bool "false" that the caller treats as "the
holder is gone" and releases the lock.

The dangerous consequence is on the transient-failure path. The Windows
implementation returns false the moment OpenProcess(QUERY_LIMITED_INFORMATION)
yields an invalid handle. Under momentary resource pressure that OpenProcess can
fail for a process that is genuinely still alive, so the orphan check can release
a lock that is still legitimately held -- admitting a second writer to the index.
That is a worse failure class (potential corruption) than the flaky hang #1989
set out to fix, which is exactly the kind of regression a stabilization release
must not ship. (Note the pre-#1989 code did not have this exposure: its liveness
check fell back to Process.GetProcessById when OpenProcess failed; #1989 dropped
that fallback on the identity path.)

This change makes the release decision reason-aware so that a lock is only
released on positive evidence that the holder is gone:

  * TryGetActiveProcessStartTime now returns a ProcessStartTimeResult enum
    (Success / ProcessNotFound / Inaccessible / Indeterminate) instead of bool.
    The Windows implementation classifies an invalid OpenProcess handle by the
    Win32 error: ERROR_INVALID_PARAMETER -> ProcessNotFound (no such PID),
    ERROR_ACCESS_DENIED -> Inaccessible, anything else (e.g.
    ERROR_NOT_ENOUGH_MEMORY / ERROR_NO_SYSTEM_RESOURCES) -> Indeterminate. An
    opened-but-exited process (exit code != STILL_ACTIVE) maps to ProcessNotFound.

  * The orphan check in GVFSLock releases the lock only for:
      - Success with a different start time  (PID recycled -- the #1989 case)
      - ProcessNotFound                       (positive: holder is gone)
      - Inaccessible                          (see gate argument below)
    and deliberately KEEPS the lock for Indeterminate, letting the existing
    250 ms wait-loop poll re-evaluate. A transient read failure can no longer
    release a live holder's lock.

  * Inaccessible is safe to treat as "holder gone" because of an acquire-time
    gate: we only enter the identity-check path for a holder whose start time we
    successfully read at acquire, i.e. one this mount could open. OpenProcess
    access is a stable function of the caller token and the target's protection
    level / DACL (git and the hooks never rewrite their own DACL), so a live
    original holder we could open before cannot later become inaccessible. An
    access-denied result therefore means the PID now refers to a different
    process, so releasing is correct. The pre-existing null-start-time fallback
    (used when we could not read the start time at acquire) is unchanged.

Telemetry: the Indeterminate hold emits an ExternalHolderLivenessIndeterminate
event so we can measure whether these transient failures ever occur in the field.

This is in-memory mount-side state only; the named-pipe lock protocol is
unchanged, so there is no cross-version or wire-format impact.

Tests: adds unit coverage for the new outcomes -- ProcessNotFound and
Inaccessible each release the orphaned lock, and Indeterminate keeps it (the
anti-false-release guard). Existing PID-recycle and start-time-match tests are
retained. Full GVFS.UnitTests suite passes (885 passed, 0 failed).

Assisted-by: Claude Opus 4.7
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Run ServiceVerbTests against a dedicated GVFS service name so --mount-all/--unmount-all only touch repos created in that fixture.

This wires service-name overrides through functional-test helpers and adds fixture-local service lifecycle setup/teardown for ServiceVerbTests.

Assisted-by: GPT-5.3-Codex
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
HealthTests read placeholder/modified-path counts via 'gvfs health'
immediately after hydrating files via File.OpenWrite().  Those writes
trigger ProjFS callbacks that GVFS processes on a background queue, so
the modified-paths database may not yet reflect the full set of
transitions when the health verb samples it.

The resulting race manifests as: fast-file count is still 5 (some
Scripts/*.bat entries still counted as placeholders) when the test
expects 3 after TurnPlaceholdersIntoModifiedPaths.

Fix: call WaitForBackgroundOperations() in ValidateHealthOutputValues
before invoking the health verb, consistent with every other functional
test that asserts modified-path state after a file operation (e.g.
GitFilesTests, ModifiedPathsTests, CheckoutTests).

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
When a cache server is configured locally, mount starts virtualization
without waiting for the background auth/config query (PR #2034). During that
window, TryInitializeAndQueryGVFSConfig has already published IsAnonymous=false
but has not yet set isInitialized (the credential fetch runs in between). A
directory enumeration that arrives in this window flows through
HttpRequestor.SendRequest -> GitAuthentication.TryGetCredentials, which threw
InvalidOperationException("must be initialized"). Thrown into the ProjFS
StartDirectoryEnumerationAsyncHandler callback, this crashed the mount process:

  StartDirectoryEnumerationAsyncHandler caught unhandled exception, exiting process

Fix: instead of throwing when called before initialization, TryGetCredentials
now waits (bounded by InitializationWaitTimeoutMs, defaulting to the background
credential timeout) for initialization to complete, then proceeds normally. If
initialization never completes within the timeout it returns a retryable
failure rather than crashing. All initialization terminal paths signal a
ManualResetEventSlim via a new MarkInitialized() helper.

This is isolated to GitAuthentication.cs so it can land independently of the
separate change that will gate the background cache-auth behavior behind a
config flag.

Adds deterministic unit tests that reproduce the race: reintroducing the throw
fails them; the fix makes them pass.

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

PR #2034 changed mount so that, when a cache server URL is already configured
in local git config, authentication and the /gvfs/config query run as a
fire-and-forget background task instead of blocking mount startup. As part of
GVFS 2.0 stabilization we want that behavioral change to be off by default and
opt-in, while keeping the associated diagnostic/reliability improvements (the
distinct startup exit codes, the bounded credential timeout, and the
cache-server fallback) always on.

This adds a git-config flag, gvfs.background-cache-auth (default false), that
gates only the fire-and-forget behavior:

- Flag off (default): mount blocks on auth + /gvfs/config synchronously before
  starting virtualization, even when a cache server is configured. Because the
  network task is awaited, the fetched config is used and a synchronous
  (Default) credential timeout applies.
- Flag on: the #2034 background behavior — mount proceeds without waiting on
  auth, using the local cache server URL, with the longer background credential
  timeout.

The gate is a single derived condition, useBackgroundAuth = hasCacheServer &&
flag, applied to the wait decision, the credential-timeout selection, and the
source of serverGVFSConfig. The pre-existing cache-server fallback, the startup
exit codes, and ValidateGVFSVersion leniency remain keyed on hasCacheServer and
are unchanged.

The flag is read via libgit2 in-process (a short-lived LibGit2Repo, since the
GVFSContext is not created until later in mount), defaulting to off on any
failure.

Isolated to GVFSConstants.cs and InProcessMount.cs; does not touch
GitAuthentication.cs, so it does not conflict with the separate fix that hardens
TryGetCredentials against being called before initialization completes.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Mount: gate background cache-server auth behind gvfs.background-cache-auth
Mount: don't crash when auth is used before background init completes
HealthTests: wait for background ops before gvfs health
tyrielv and others added 15 commits July 14, 2026 14:35
Harden orphan-lock detection against false-release on transient failures
…n-leak

Bound leaked ActiveEnumeration collection behind an off-by-default flag + Heartbeat observation telemetry
…status

Split ServiceVerb repo-state check into intent-specific predicates
Default gvfs.prefetch-offload to false and skip mount offload runtime paths unless enabled. Add heartbeat FeatureFlags.PrefetchOffload telemetry and unit assertions.

Assisted-by: GPT-5.3-Codex
Copilot-Session: 85dced45-8638-4b33-a1d1-8af4c8e37fb0
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The gvfs.prefetch-offload flag state is already captured fleet-wide by the
git Trace2 collector via trace2.configparams (scalar.*,gvfs.*,...), surfaced
as Summary.payload.params. Re-emitting it in VFS.Heartbeat is redundant and
client-side telemetry adopts slowly across the GVFS fleet.

Drop the FeatureFlags heartbeat metadata and the GitRepo.GetConfigBoolOrDefault
wrapper added only to feed it. The gvfs.prefetch-offload gate in PrefetchVerb
is unaffected.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Scope ServiceVerbTests to a fixture-local GVFS service
Gate prefetch offload behind gvfs.prefetch-offload
Harden GVFS.Mount against native ProjFS failures under memory pressure (0xC000CE01)
docs(agents): document release-shipping scope and feature-flag convention
…-gate

ProjFS: only tolerate FilterAttach ACCESS_DENIED for unelevated callers
Mount: remove broad HandleRequest exception catch
Gate CLI mount-progress display behind gvfs.mount-progress config
@tyrielv tyrielv enabled auto-merge July 15, 2026 19:55
@tyrielv tyrielv merged commit 2b7a99b into releases/shipped Jul 15, 2026
79 checks passed
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.

3 participants