Skip to content

Release M277#2036

Merged
tyrielv merged 56 commits into
releases/shippedfrom
milestones/M277
Jun 30, 2026
Merged

Release M277#2036
tyrielv merged 56 commits into
releases/shippedfrom
milestones/M277

Conversation

@tyrielv

@tyrielv tyrielv commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Changes:

tyrielv added 30 commits June 4, 2026 10:43
…tion

The previous IsConsoleOutputRedirectedToFile() used Win32 GetFileType to
check if stdout was redirected to a disk file. This missed the pipe case
(GetFileType returns Pipe, not Disk), so the console spinner would still
run when stdout was captured via Process.Start with RedirectStandardOutput
(e.g. in functional tests and CI), polluting captured output with \r and
spinner characters.

Replace with .NET's Console.IsOutputRedirected which returns true for any
non-console handle (files, pipes, NUL). Remove the now-unused P/Invoke
declarations (GetStdHandle, GetFileType) and the platform abstraction
(IsConsoleOutputRedirectedToFile) from GVFSPlatform, WindowsPlatform,
GVFSHooksPlatform, and MockPlatform.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Replace the single-entry LastBlobPrefetch.dat cache with a multi-entry
BlobPrefetchCache.dat that stores up to N entries (default 100), keyed
by SHA256 hash of (files, folders, hydrate) and storing the commit ID.

This avoids redundant diff+download work when users cycle through a
small set of prefetch patterns (e.g. 3 different file/folder combos),
which previously caused 2/3 of calls to miss the single-entry cache.

Changes:
- BlobPrefetcher: replace flat 4-key dictionary with hash-keyed cache
- BlobPrefetcher.ComputeCacheKey: canonical, order-independent hashing
- BlobPrefetcher.SavePrefetchArgs: single-entry eviction when at capacity
- PrefetchVerb: read gvfs.prefetchCacheSize config (0=disabled, max 1000)
- PrefetchVerb: use BlobPrefetchCache.dat instead of LastBlobPrefetch.dat
- 12 unit tests covering key determinism, order independence, cache
  hit/miss, multi-entry support, and null/empty edge cases

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The multi-entry prefetch cache persists across ordered tests, causing
cache hits where the tests expect fresh prefetch work. Delete
BlobPrefetchCache.dat in [SetUp] so each test starts with a clean cache.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Introduce IObjectExistenceChecker strategy pattern to decouple blob
prefetch from libgit2's git_revparse_single, which is extremely slow
for missing objects (~2.8ms/op with 14 packs in a large GVFS cache).

New PackIndexObjectExistenceChecker reads MIDX and supplemental .idx
files directly in managed code via memory-mapped IO (~5us/op), with
loose-object File.Exists fallback. Gated on gvfs.prefetch-use-idx
git config (default: false).

Components:
- IObjectExistenceChecker: strategy interface
- RevParseObjectExistenceChecker: wraps existing LibGit2Repo.ObjectExists
- PackIndexObjectExistenceChecker: MIDX + pack idx + loose fallback
- MidxReader: memory-mapped MIDX v1 parser with binary search
- PackIndexReader: memory-mapped pack index v2 parser with binary search
- FindBlobsStage: accepts optional checker factory (backward compatible)
- BlobPrefetcher: reads config, creates appropriate checker factory

Searches both LocalObjectsRoot and GitObjectsRoot (shared cache),
detects supplemental packs not yet in MIDX via PNAM chunk diffing,
and safely falls back to revparse on initialization errors.

Unit tests cover: MIDX/idx hit and miss, all 256 fanout buckets,
supplemental pack detection, loose objects, empty/missing pack dirs,
multiple object roots, corrupt file handling, and deduplication.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Two related fixes that together let `gvfs mount` succeed against an
enlistment whose pre-command hook is stale - typically because the GVFS
install location baked into .git/hooks/pre-command.hooks at clone time
has since moved (re-install, version-junction swap, system-to-user
migration, etc.).

Before this change, a stale .hooks text file causes every git invocation
that fires the pre-command hook to fail with:

  fatal: pre-command hook aborted command

which makes the mount path unrecoverable.

Changes:

  HooksInstaller.TryUpdateHooks
    Also refresh the .hooks text files. Previously TryUpdateHooks only
    refreshed the .exe copies of GitHooksLoader; the .hooks text file
    (containing the absolute path of GVFS.Hooks.exe that the loader
    execs) was only written at clone time by InstallHooks. When the
    GVFS install moves, the .exe copies stay valid but the .hooks
    path goes stale - and gvfs.mount.exe's existing TryUpdateHooks
    call didn't repair it.

    The new TryInstallGitCommandHooks calls are idempotent: when the
    GVFS install path hasn't changed, the file is rewritten with the
    same content.

  GitProcess
    Pass usePreCommandHook:false on all git operations that run during
    the mount bootstrap path. These calls happen before gvfs.mount.exe
    reaches TryUpdateHooks, so without this flag they trip over the
    very stale-hook config we're trying to repair.

    Affected: SetInLocalConfig, AddInLocalConfig, DeleteFromLocalConfig,
    TryGetAllConfig, TryGetConfigUrlMatch, TryGetCredential,
    TryGetCertificatePassword, TryDeleteCredential, TryStoreCredential.

    GetFromConfig, GetFromLocalConfig and IsValidRepo also gain the
    flag (some via the existing GetOriginUrl pattern, some new). None
    of these operations mutate the working tree, so pre-command hook
    is semantically inappropriate anyway - skipping it is correct
    independent of the stale-hook scenario.

    The mechanism: usePreCommandHook:false sets the COMMAND_HOOK_LOCK
    environment variable, which Microsoft Git itself reads to suppress
    pre-command hook invocation. So the failure is bypassed at the
    git layer, not just inside GVFS.Hooks.exe.

Testing:

  - 818/818 unit tests pass
  - Manually verified end-to-end with a real enlistment whose
    pre-command.hooks was corrupted to point at a non-existent path
    (C:\NonExistent\Path\GVFS.Hooks.exe). Before this change, `gvfs
    mount` failed with "pre-command hook aborted command". After this
    change, mount succeeds and the .hooks file is rewritten to point
    at the currently-running GVFS install.

Assisted-by: Claude Opus 4.7
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The Yes/No/Cancel MsgBox shown when mounted repos are detected during
install was confusing -- the Yes option meant "keep repos mounted"
(the less-common, advanced staging case), which inverted user
expectations and required reading the message body carefully to map
button semantics to outcomes.

Replace it with a custom modal containing two radio buttons and
Continue/Cancel:

  (*) Remount repos as part of the installation
      They will be temporarily unavailable.
  ( ) Keep repos mounted
      The upgrade will complete automatically when all repos are
      unmounted, or at next reboot.

The remount option is selected by default, matching the previous IDYES
default's intent (proceed with the common path).

Silent-mode STAGEIFMOUNTED=true|false behavior is unchanged.

Assisted-by: Claude Opus 4.7
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
When no previous commit exists to diff against (sourceTreeSha == null),
DiffHelper.PerformDiff previously ran 'git ls-tree -r -t HEAD' which walks
all tree objects. On a large repo with ~2.5M files, this takes ~24s.

Replace with 'git ls-files -s' which reads the index instead of walking
tree objects. Benchmarked at ~6.5s on the same repo — a 3.7x speedup.

The optimization is only applied when targetTreeSha matches HEAD's tree,
since ls-files reads the index (which reflects HEAD). When they differ
(e.g., FastFetch checking out a non-HEAD commit), falls back to ls-tree
to preserve correctness.

Also falls back to ls-tree if ls-files fails (e.g., index does not exist
on fresh git init before first checkout).

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The modified-paths database file is an append-only log of "A path" /
"D path" entries that ModifiedPathsDatabase compacts at the end of each
background-op batch via WriteAllEntriesAndFlush in PostBackgroundOperation.
BackgroundOperationCount, however, drops to 0 inside DequeueAndFlush for
the last task -- before the post-callback runs. WaitForBackgroundOperations
polls until count == 0 and can therefore return between dequeue and
compaction, leaving the file in a state like:

    A temp.txt
    D temp.txt

ModifiedPathsShouldNotContain matched both lines, fed two results into
EnumerableShouldExtensions.ShouldNotContain, and the SingleOrDefault call
threw "Sequence contains more than one matching element" -- masking the
underlying race with a confusing exception.

The product code is correct: FileBasedCollection.TryLoadFromDisk replays
the A/D log on mount, so the on-disk state is always recoverable. This
change fixes the tests:

  * GVFSHelpers.ModifiedPathsShouldContain / ModifiedPathsShouldNotContain
    now build the current set by replaying the A/D log (same algorithm as
    TryLoadFromDisk) and check membership in that set. This matches the
    semantic intent of every existing caller and is robust to the
    flush-race window.

  * ModifiedPathsContentsShouldEqual is renamed to
    ModifiedPathsRawFileContentsShouldEqual and gains an XML doc comment
    pointing callers at the semantic helpers unless they specifically need
    to validate the compacted file layout. The two existing callers in
    CheckoutTests are updated.

  * EnumerableShouldExtensions.ShouldNotContain uses Where(predicate)
    instead of SingleOrDefault(predicate) so multi-match cases produce a
    useful Assert.Fail message instead of InvalidOperationException.

  * A new ModifiedPathsDatabaseTests.AddFollowedByDeleteIsRecoveredOnLoad
    unit test pins down the recovery contract -- loading
    "A temp.txt\r\nD temp.txt\r\n" produces an empty set
    (only the auto-added .gitattributes entry).

Assisted-by: Claude Opus 4.7
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Per Keith's review, the two CheckoutTests callsites that used
ModifiedPathsRawFileContentsShouldEqual were exposed to the same
compaction race the rest of this PR fixes -- WaitForBackgroundOperations
can return before PostBackgroundOperation rewrites the file, so any
transient "A path / D path" cycle (today: none; future: easy to
introduce) would break the exact-equals raw-bytes assertion.

Those callers' real intent is semantic: "after this checkout sequence,
the only modified path is .gitattributes". Replace the raw helper with
a new ModifiedPathsShouldOnlyContain that compares against the replayed
A/D log as a set. The raw helper now has zero callers and is removed.

Assisted-by: Claude Opus 4.7
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Fix flaky ModifiedPathsTests by replaying the on-disk log
…ged entries

Fix three issues raised in PR review:

1. TargetMatchesHeadTree compared HEAD's tree SHA against a commit SHA
   (callers pass commit IDs, not tree IDs). The comparison never matched,
   so the optimization silently fell back to ls-tree every time. Fix by
   resolving targetTreeSha to its tree SHA via GetTreeSha() before
   comparing.

2. Add comments clarifying that the ls-files path intentionally skips
   directory operations. This is safe because the path only fires for
   gvfs prefetch on GVFS-mounted repos where directories are virtualized
   by PrjFlt. FastFetch force-checkout (which needs directory ops) targets
   a non-HEAD commit and falls back to ls-tree.

3. Filter out non-zero stage entries (unmerged) in the ls-files parser.
   During merge conflicts the same path appears at stages 1/2/3 with
   different SHAs. While GVFS repos shouldn't have conflicts, filtering
   defensively avoids duplicate adds with wrong blob SHAs.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Installer: replace confusing mount prompt with radio dialog
Use git ls-files -s instead of ls-tree for full-tree enumeration
Mount: self-heal stale hook configurations
Replace the heavyweight git_revparse_single call in LibGit2Repo.ObjectExists
with git_odb_exists, a purpose-built existence check that skips revparse
expression parsing and git_object handle allocation.

Benchmarked on an os.2020 enlistment (59.7M objects, 14 packs):
- Existing objects: ~800 ns/op (comparable)
- Missing objects: 1.3ms vs 2.8ms (2.1x faster)

The ODB handle is lazily acquired on first ObjectExists call via
git_repository_odb (returns the repo's internal ODB, ref-counted)
and freed in Dispose. Concurrent first-time calls are safe via
Interlocked.CompareExchange — the loser frees its duplicate handle.
Falls back to revparse if ODB acquisition fails.

Add ObjectCanBeParsed method that retains the old revparse behavior
for callers that need corruption detection (LooseObjectsStep) or that
may receive refs/abbreviated SHAs rather than full 40-char hex
(BlobPrefetcher.DownloadMissingCommit, which takes raw CLI input).

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyler Vella <tyrielv@gmail.com>
Replace Stream.Read with Stream.ReadExactly where the caller expects
all requested bytes (EnlistmentHydrationSummary, ReusableMemoryStream).

In GitRepo.ReadLooseObjectHeader, check the Read return value instead
of switching to ReadExactly — ReadExactly would throw EndOfStreamException
on a truncated header, routing to the IOException catch (LooseBlobState.Unknown)
instead of the header-mismatch path (LooseBlobState.Corrupt) that quarantines
the file.

Suppress CA2022 in GitIndexParser.ReadNextPage where partial last-page
reads are intentional and the parser stops after entryCount entries.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Use git_odb_exists instead of git_revparse_single for ObjectExists
Fix CA2022 warnings: avoid inexact Stream.Read calls
When TryDownloadCommit finds the commit via CommitAndRootTreeExists,
it now checks whether the commit is a loose object. Loose commits
(e.g., from a prior 'git show' or 'git log' in a mounted enlistment)
do not include reachable trees. Skipping the download in this case
causes 'git checkout -f' to fail with 'unable to read tree', followed
by an expensive fallback that re-downloads and retries checkout.

If the commit is in a pack file (prefetch or commit pack), trees are
included by the GVFS protocol, so the download can safely be skipped.

Added GitRepo.LooseObjectExists() to check whether a SHA exists as a
loose object file in the shared cache or local object store.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
When a GVFS mount is running, all prefetch operations now offload to the
mount process via named pipe IPC, using its already-warm authentication
to skip the slow cold-auth path (anonymous HTTP probe + git credential
helper invocation).

Commits prefetch (--commits):
  PrefetchCommits IPC message tells the mount to run PrefetchStep with
  its warm GitObjectsHttpRequestor. A post-fetch callback is injected
  to avoid re-entrant named pipe IPC when SchedulePostFetchJob would
  otherwise call back into the same mount.

Blob prefetch (--files/--folders):
  PrefetchBlobs IPC message carries file/folder lists, HEAD commit ID,
  and hydrate flag. The mount creates a fresh GitObjectsHttpRequestor
  with warm auth, runs BlobPrefetcher with capped thread counts
  (ProcessorCount/2), validates inputs, and properly disposes HTTP
  resources. LastBlobPrefetch.dat is passed through for noop state.

Hydration (--hydrate):
  Two-phase approach: mount downloads blobs (no hydrate), then the verb
  process hydrates files locally using Parallel.ForEach with
  ProcessorCount/2 parallelism. This avoids the mount writing to
  ProjFS-virtualized files (self-callback risk) while ensuring all
  blobs are cached before hydration starts, minimizing the ProjFS
  expansion race window.

Fallback:
  If the mount is not running, not ready, or is an older version that
  does not recognize the new IPC messages, the verb falls back to the
  existing direct-auth path transparently. Mount-side failures are
  surfaced directly (no fallback on real errors).

Benchmarks on os.2020 (144 files in tools/nmakejs+Razzle+signing):
  Mounted offload:         158s (warm auth)
  Unmounted direct:        171s (cold auth)
  Mounted offload+hydrate: 153s (two-phase)
  Auth savings: ~13s per prefetch call

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Expand blob prefetch noop cache to N entries
…tection

Use Console.IsOutputRedirected instead of P/Invoke for redirect detection
Add pack-index object existence checker strategy for prefetch
Offload prefetch to mount process for warm auth
The merge of #2002 (prefetch-offload-to-mount) on top of #2004
(expand-prefetch-cache) left three issues:

1. PrefetchVerb.cs hydration-fallback path passed the removed
   lastPrefetchArgs variable instead of prefetchCache +
   prefetchCacheSize.

2. InProcessMount.cs HandlePrefetchBlobsRequest passed the removed
   lastPrefetchArgs variable. The mount-side handler is a one-shot
   download with no persistent noop cache, so it correctly receives
   null + 0.

3. When prefetch succeeds via mount offload, the verb-side noop cache
   was never updated — SavePrefetchArgs only runs inside
   BlobPrefetcher.PrefetchWithStats, which is skipped on the offload
   path. This caused NoopPrefetch to re-download on the second run
   instead of printing 'Nothing new to prefetch.'

   Fix: add BlobPrefetcher.UpdateNoopCache() static method (extracted
   from SavePrefetchArgs logic) and call it from PrefetchVerb after
   successful mount offload.

   Update PrefetchBlobsMountedAfterRemount test to expect the noop
   message since the file was already cached by a prior test in the
   same fixture.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Fix build break: bad merge in prefetch cache parameters
Download commit pack even when commit exists as loose object
Move the named pipe server start earlier in InProcessMount so
MountVerb can connect and poll status during the parallel
auth+validation phase. Add a MountProgress field to the GetStatus
response carrying a human-readable phase description that the CLI
renders as a dynamic spinner sub-status.

Changes:
- NamedPipeMessages: add MountProgress to GetStatus.Response
- InProcessMount: volatile progress string set at each phase;
  pipe started after RepoMetadata init (before parallel tasks);
  HandleRequest guards non-GetStatus during Mounting state;
  HandleGetStatusRequest null-safe for early-pipe fields
- ConsoleHelper: new ShowStatusWhileRunning overloads accepting
  Func<string> getMessage for dynamic spinner text
- GVFSEnlistment: optional Action<string> onProgress callback on
  WaitUntilMounted (existing callers unaffected)
- MountVerb: wires dynamic spinner to progress callback

User sees: Mounting (Authenticating and validating)...
           Mounting (Starting virtualization)...
           Mounting...Succeeded

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Show mount progress phases in CLI during gvfs mount
HandleRequest now catches exceptions from individual pipe request
handlers instead of letting them propagate to OnNewConnection, which
calls Environment.Exit on any unhandled exception. A single transient
error (network timeout, disk I/O failure) in a download handler would
crash the entire mount process, breaking all pipe connections.

Both catch sites use exception filters to exclude OutOfMemoryException,
which indicates a corrupted heap state where continuing is unsafe.
StackOverflowException and AccessViolationException are already
uncatchable in .NET Core and need no explicit exclusion.

HandleDownloadObjectRequest is refactored to isolate the download
logic in DownloadObject and wrap it in a try-catch that returns a
DownloadFailed response on exception. The read-object hook then
receives a proper failure response instead of ERROR_BROKEN_PIPE (109),
and git handles the object-not-available error more gracefully.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
tyrielv and others added 26 commits June 15, 2026 13:14
When GitRepoTests uses a shared enlistment (enlistmentPerTest=false),
check IsMounted() in SetupForTest before running git commands. If the
mount process crashed during a previous test, all remaining tests in
the fixture would fail with the same unhelpful 'does not appear to be
mounted' error from the pre-command hook. The early check produces a
clear message pointing to the earlier root-cause failure.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Mount: prevent process crash on unhandled request handler exceptions
Remove Debug from the CI matrix in build, functional-test, and
upgrade-test workflows, keeping only Release. This halves CI resource
usage and wall-clock time for PR validation.

Promote the 3 remaining Debug.Assert calls to runtime checks
(InvalidOperationException / ArgumentNullException) so they fire in
Release too — strictly stronger coverage than the Debug-only asserts
they replace, with negligible overhead (all are on I/O or shutdown
paths).

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The ExtraCoverage category excluded ~110 functional test methods (19
test classes) from the default CI run. These tests cover critical
functionality -- mount edge cases, dehydrate, repair, shared cache,
disk layout upgrades, junctions -- but were never validated in CI.

Remove ExtraCoverage filtering and fix the atrophied tests so they
run in CI. Introduce SkipInCIAttribute with a required reason string
for tests that still need follow-up work.

Infrastructure:
- Remove ExtraCoverage constant, --extra-only flag, and all 19
  Category annotations
- Download FastFetch artifact in functional-tests.yaml
- Fix NUnitRunner slice grouping to include MultiEnlistmentTests
- Increase test slices from 10 to 12
- Add resilient teardown: UnmountAndDeleteAll catches stuck unmounts
  and kills the GVFS.Mount process as a fallback

Fixed tests:
- FastFetchTests: artifact now available in CI
- ConfigVerbTests: Order-dependent tests stay in same slice
- RepairTests: remove stale mount-fail assertions (GVFS now tolerates
  corrupt index)
- MountTests: capture stderr, use try/finally for metadata restore,
  check exit code only where errors go to GVFS log
- FastFetchTests git output assertion: case-insensitive match

Removed tests:
- UpgradeReminderTests: old NuGet upgrade system removed
- SharedCacheUpgradeTests: zero test methods (dead code)
- MountMergesLocalPrePostHooksConfig: mount no longer merges hooks
- ProjFS_CMDHangNoneActiveInstance: obsolete ProjFS regression test
- MountingARepositoryThatRequiresPlaceholderUpdatesWorks: placeholder
  updates moved out of mount

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1. KillMountProcess: Use Get-CimInstance Win32_Process to find
   GVFS.Mount processes whose command line contains this specific
   enlistment root, instead of killing all GVFS.Mount processes.
   Prevents collateral damage to healthy mounts owned by other
   fixtures running in parallel.

2. MountFailsWhenNoLocalCacheRootInRepoMetadata and
   MountFailsWhenNoGitObjectsRootInRepoMetadata: After verifying
   the exit code, also check the GVFS log for the expected error
   message. This ensures mount failed for the expected reason,
   not an unrelated one.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The repair job for BlobSizes.sql was unable to delete the corrupt
database file on Windows because SQLite connection pooling kept
the file handle open after the integrity check in HasIssue().

Two fixes:

1. SqliteDatabase.HasIssue: Use Pooling=False for integrity check
   connections so file handles are released immediately on dispose,
   allowing repair to delete the corrupt file.

2. BlobSizes.Initialize: Tolerate corrupt databases by catching
   SQLITE_CORRUPT and SQLITE_NOTADB errors, deleting the corrupt
   file (and WAL/SHM sidecars), and recreating a fresh database.
   This provides defense-in-depth since BlobSizes is a cache.

Also remove SkipInCI from RepairFixesCorruptBlobSizesDatabase and
add an assertion that repair actually cleans up the corrupt folder.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
LatestGVFSLogShouldContain: Search all log files in the logs
directory, not just the most recent one. Mount errors are logged by
GVFS.Mount.exe in its own log file, not the verb's log file.

FastFetchTests: Mark CanFetchAndCheckoutMultipleTimesUsingForceCheckoutFlag
and ForceCheckoutRequiresCheckout as SkipInCI - both depend on a Scripts
folder that no longer exists in the FunctionalTests/20201014 test branch.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Remove ExtraCoverage category — run all functional tests in CI
ci: drop Debug configuration from build and test matrices
The background mount process (GVFS.Mount.exe) inherits the caller's
current working directory, holding a handle that prevents the caller
from cleaning up or deleting that directory. This affects tools that
launch gvfs clone/mount and need to remove themselves afterward.

Set ProcessStartInfo.WorkingDirectory to the program's own directory
so the child process does not hold a handle on the caller's CWD.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Set WorkingDirectory for background mount process to avoid holding caller's CWD handle
EventMetadataConverter.WriteValue() was falling through to the default
case (ToString()) for EventMetadata and Dictionary<string, string> values,
producing type names like "GVFS.Common.Tracing.EventMetadata" instead of
their key-value contents in the VFS.Heartbeat payload.

Add pattern-match cases for EventMetadata (recursive) and
IDictionary<string, string> so nested objects serialize as JSON objects.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…ia GVFSTable base class

Extract shared retry and error-handling logic into GVFSTable base class,
used by both PlaceholderTable and SparseTable. This provides:

- ExecuteWrite: serialized writes with retry on BUSY/LOCKED/IOERR
- ExecuteRead: reads with retry on transient errors
- ExecuteNonCriticalRead: returns fallback on transient error (heartbeat)
- ExecuteReadThenWrite: mixed operations with retry

Transient errors handled (up to 5 retries with linear backoff):
- SQLITE_BUSY (5): connection-level lock contention
- SQLITE_LOCKED (6): table-level lock contention (fixes #59353072)
- SQLITE_IOERR (10): disk I/O errors from AV/ReFS/disk busyness

Non-critical count methods (GetCount, GetFilePlaceholdersCount,
GetFolderPlaceholdersCount) return -1 on transient failure rather than
throwing, since they are only consumed by heartbeat telemetry.

Also fixes pre-existing copy-paste bug in exception messages where
GetFilePlaceholdersCount/GetFolderPlaceholdersCount reported as GetCount.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The telemetry events we emit today record the GVFS process version but not
the architecture. With ARM64 support landing in a parallel commit, telemetry
queries can't distinguish ARM64-native installs from x64-under-Prism installs
on ARM64 hardware without this field.

This commit:
  * Adds ProcessHelper.GetCurrentProcessArchitecture() — returns the .NET
    RID-style lowercase arch ("x64", "arm64") of the running process. Caches
    the result like GetCurrentProcessVersion() does.
  * Adds an "Architecture" metadata field alongside the existing "Version"
    field in the three in-process telemetry emit sites:
      - JsonTracer.WriteStartEvent  (start-of-session log marker)
      - HeartbeatThread.EmitHeartbeat (hourly Heartbeat event)
      - GVFSService.Windows.Run (service startup event)
  * Adds an "architecture" top-level property to the PipeMessage schema in
    TelemetryDaemonEventListener — peer to "version". The constructor caches
    the value once and CreatePipeMessage attaches it to every outgoing
    message.
  * Updates TelemetryDaemonEventListenerTests for the new field (top-level
    property count 6 -> 7).

The collector side (devprod.git.telemetry) needs a matching change to pick
up the new field and pass it through to ETW / AppInsights; that lands as a
separate PR in that repo. Until then the collector silently drops the field;
old GVFS clients that don't send it cause no break either.

Assisted-by: Claude Opus 4.7
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
.NET 10 AOT means we now ship arch-specific binaries for everything; the
previous .NET Framework managed-code path that used the system-installed
framework is gone. On ARM64 Windows that means today's x64 build runs
under Prism emulation. Adding a native ARM64 build path eliminates that
overhead for users on ARM64 hosts.

Perf wins on a Snapdragon Windows host (Release / NativeAOT):
  * Per-process startup (gvfs version, gvfs --help, post-index-changed-hook):
    1.47-1.53x faster, saving 15-25 ms per invocation
  * Unit test suite: 1.21x faster
  * Full functional test suite: 1.37x faster (552/1/16 pass/fail/skip on both
    arches; same single known-flaky failure)
  * os.2020 real workload (clone + checkout + hydrate + blame): tied with
    x64-under-Prism once blob cache warmth is controlled

Architecture parameterization (foundation):
  * Directory.Build.props introduces $(VfsArch) — lowercase RID/vcpkg form
    used for RuntimeIdentifier and vcpkg triplet selection. Also introduces
    $(VfsNativePlatform) — mixed-case vcxproj-style form (x64 / ARM64).
    All hardcoded "win-x64", "x64-windows-*", "<Platform>x64</Platform>"
    references replaced with these properties.
  * Build.bat gains a 4th ARCH argument defaulting to x64 (so existing
    invocations keep their exact behaviour). The argument threads through
    vcpkg triplet, MSBuild Platform, dotnet RID. Two cross-cutting fixes
    accompany this:
      - Prepend the VS Installer dir to PATH so ilc can find vswhere when
        invoked from a non-developer cmd shell. Pre-existing trip hazard
        that was masked by how local-build invoked Build.bat.
      - Call vcvarsall.bat <arch> before the native C++ build loop so MSBuild
        can locate the matching cl.exe / link.exe and the right INCLUDE/LIB
        search paths for that arch. Without this, MSBuild finds the v145
        ARM64 toolset's targets file but cannot locate the actual ARM64
        build tool binaries.
      - Clear the Platform env var (vcvarsall.bat sets it) before the
        managed publish loop so csproj defaults to AnyCPU and doesn't add
        a spurious "\<arch>\" segment to managed output paths.
  * All 5 .vcxproj files gain Debug|ARM64 and Release|ARM64
    ProjectConfiguration entries; existing Configuration|Platform
    conditions simplified to Configuration-only (their contents were never
    platform-specific). Hardcoded UCRT lib arch path parameterized to
    \$(Platform.ToLower()). <PlatformToolset> parameterized via new
    $(VfsPlatformToolset) — v143 for x64 (preserves baseline byte-for-byte),
    v145 for arm64 (VS 2026's own toolset, the only one with ARM64
    cross-compile binaries here).
  * GVFS.sln gains Debug|ARM64 and Release|ARM64 SolutionConfigurationPlatforms
    and ProjectConfigurationPlatforms entries for every project.
  * GVFS.NativeTests.vcxproj: the x64-only GVFS.ProjFS NuGet's
    ProjectedFSLib.lib path is replaced with Windows SDK 10.0.26100's
    Lib\<sdk>\um\$(Platform.ToLower())\, which ships ProjectedFSLib.lib for
    x86, x64, and arm64 natively. The unused packages.config and its
    GVFS.ProjFS 2019.411.1 reference are removed.
  * layout.bat takes an arch arg and selects per-arch paths
    (win-<arch>, bin\<NATIVE_PLATFORM>\). For arm64 it falls back to
    %VCToolsRedistDir%\arm64\Microsoft.VC*.CRT\ for the VC runtime DLLs
    because GVFS.VCRuntime NuGet ships x64 only.
  * GVFS.Payload.csproj passes $(VfsArch) to layout.bat.
  * GVFS.Installers.csproj LayoutPath uses win-$(VfsArch); the
    InstallerArchSuffix property (empty for x64, "-arm64" for arm64) is
    passed to Inno Setup via /DArchSuffix. Setup.iss uses
    {#ArchSuffix} on OutputBaseFilename so the x64 installer keeps its
    historical name (SetupGVFS.<v>.exe) and the arm64 installer gets a
    "-arm64" suffix (SetupGVFS.<v>-arm64.exe). The two files can then
    coexist as assets on the same GitHub release.
  * GVFS.FunctionalTests.csproj NativeTests copy paths parameterized.
  * FastFetch.csproj drops its own <PlatformTarget>x64</PlatformTarget>
    override (now inherited from Directory.Build.props).
  * New vcpkg triplets: triplets/arm64-windows-static-aot.cmake and
    arm64-windows-dynamic.cmake.

Functional-test infrastructure:
  * GVFS.FunctionalTests/Settings.cs: dev-mode payload discovery uses
    RuntimeInformation.ProcessArchitecture instead of a hardcoded
    "win-x64". This way an ARM64 functional-test driver targets the ARM64
    payload it was built alongside, instead of silently falling through to
    PathToGVFS = "C:\Program Files\VFS for Git\GVFS.exe" (the system
    install) when the expected publish dir doesn't exist.
  * scripts/RunFunctionalTests-Dev.ps1 gains an -Arch parameter (x64
    default, arm64). The $payloadDir computation is fixed to match the
    Payload csproj's actual output layout
    (bin\<cfg>\win-<arch>\ — the Payload sets
    AppendTargetFrameworkToOutputPath=false), and a missing-file check
    fails loudly so the bug above can't silently recur.

CI matrix (per-arch native builds, no arch-cross):
  * .github/workflows/build.yaml gains an architecture dimension on the
    matrix (x64 + arm64). runs-on routes to windows-11-arm for arm64
    builds, windows-2025 for x64 (same pattern as the existing
    functional-tests workflow). Artifact names get an _<arch> suffix.
  * .github/workflows/functional-tests.yaml downloads arch-keyed
    GVFS_<cfg>_<arch> and FunctionalTests_<cfg>_<arch> artifacts so each
    hardware arch tests its own native build. The matrix value 'x86_64'
    is renamed to 'x64' for consistency with the artifact-name suffix and
    with everywhere else this PR uses the arch value.
  * .github/workflows/upgrade-tests.yaml is x64-only; its
    GVFS_<cfg> download is updated to GVFS_<cfg>_x64 to match the new
    build artifact name.

Assisted-by: Claude Opus 4.7
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Restructures the release pipeline to build, ESRP-sign, and publish both
x64 and ARM64 installers in parallel. Uses the same ${{ each }} pattern
as microsoft/git's release pipeline (1ES templates don't support
strategy.matrix).

Changes:
  * .azure-pipelines/release.yml: single Build job replaced with a
    build_matrix parameter that generates Build_x64 and Build_arm64
    jobs. Each job runs on its native pool (GitClientPME-1ESHostedPool-
    intel-pc for x64, GitClientPME-1ESHostedPool-arm64-pc for arm64),
    builds with the arch arg to Build.bat, signs its own payload and
    installer via ESRP, and stages arch-suffixed pipeline artifacts.
    The release stage downloads both Installer_x64 and Installer_arm64
    and publishes both SetupGVFS.<v>.exe and SetupGVFS.<v>-arm64.exe
    as assets on the same draft GitHub Release.
  * scripts/CreateBuildArtifacts.bat: gains an optional 3rd ARCH arg
    (default x64) to select the correct win-<arch> output paths.
  * .github/workflows/build.yaml: passes matrix.architecture to
    CreateBuildArtifacts.bat.

Assisted-by: Claude Opus 4.7
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
When a cache server URL is already in local git config, mount no
longer waits for /gvfs/config authentication to complete before
proceeding. Auth runs as a fire-and-forget background task so GCM
can pop up a renewal prompt for stale tokens without delaying mount.

Changes:

1. Background auth with cache server — InProcessMount only awaits
   the network task when there is NO cache server. With a cache
   server, mount proceeds immediately after local validations.

2. Credential gate — A SemaphoreSlim in GitAuthentication serializes
   all git-credential-fill calls so a background auth task and a
   foreground object download never spawn duplicate GCM prompts.

3. Process tracking in MountVerb — WaitUntilMounted now uses short-
   interval (500ms) connect retries with process liveness checks
   instead of a single 60-second blocking connect. If GVFS.Mount
   exits early (e.g., crash), MountVerb detects it within 500ms.

4. Auth/network retry progress — ConfigHttpRequestor reports retry
   failures into the mount progress message so users see live status
   instead of a frozen spinner.

5. Skip config retries with cache server — TryInitializeAndQueryGVFS
   Config uses maxRetries:0 (single attempt) when a cache server is
   configured. No point retrying when the result would be discarded.

6. Distinct exit codes for mount startup failures:
   - CredentialTimeout (10): git-credential-fill hung for 30s
   - RemoteGvfsConfigError (11): /gvfs/config query failed (non-auth)
   - AuthenticationError (9): credentials rejected by server

7. 30s credential timeout during mount — When no cache server is
   configured, git-credential-fill has a 30-second timeout so mount
   fails fast with exit code 10 instead of hanging indefinitely.
   With a cache server, no timeout (GCM can take as long as needed).

8. ICredentialStore gains timeoutMs parameter — allows callers to
   bound credential fetch duration without changing the interface
   contract for existing callers (default: infinite).

Signed-off-by: Tyrie Vella <tyrielv@gmail.com>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
…ions/checkout-7

Bump actions/checkout from 6 to 7
Mount: don't block startup on auth when cache server is configured
Emit Keywords.Telemetry events at each phase transition during mount
so that slow or hung mounts can be diagnosed from server-side telemetry.
Previously, there was a telemetry gap between VFS.EnlistmentInfo and
VFS.Mount 'Virtual repo is ready' — if any intermediate phase stalled,
no Application Insights events were recorded.

New MountPhase events with elapsed-since-mount-start timestamps:
- ParallelMountStarted: parallel auth+local validation begins
- NetworkValidationComplete: auth + /gvfs/config query done (duration)
- LocalValidationComplete: git/hooks/fs validation + config done (duration)
- ParallelMountComplete: both parallel tasks finished
- CacheServerResolved: cache server URL resolved
- LocalCacheHealthy: local object cache validated
- ContextCreated: GVFSContext initialized
- HooksUpdated: hook binaries installed (or skipped for worktrees)
- VirtualizationStarting: about to start ProjFS callbacks

Note: RetryConfig defaults are 6 retries x 30s timeout = 210s worst case
for the network task. When auth is expired/stuck, the mount can block for
up to 210s in TryInitializeAndQueryGVFSConfig with no telemetry — these
new events make that visible.

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

Fix heartbeat telemetry serialization of nested objects
…ache=Shared

Issue 1 (Keith): Microsoft.Data.Sqlite retries SQLITE_BUSY/LOCKED internally
at 150ms intervals until CommandTimeout elapses (default 30s). Our 5 outer
retries stacked on top for a worst-case ~150s. Fix: set CommandTimeout=2 on
each command, bounding per-attempt busy-wait to 2s (~10.75s total worst case).
In production (lock hold times ~10ms) this cap is never reached.

Issue 1b (root cause analysis): Cache=Shared uses table-level locking and is
the primary source of occasional SQLITE_LOCKED in production. UpdatePlaceholders
runs up to 8 threads that each check out connections before the C# writerLock,
creating a window where multiple shared-cache connections hold read locks on the
Placeholder table simultaneously. Removing Cache=Shared eliminates table-level
lock contention; WAL mode already provides the concurrency isolation needed.

Issue 2 (Keith): All four Add* methods surfaced as 'InsertPlaceholder' via
CallerMemberName, and the path/sha info from the old exception messages was lost.
Fix: propagate [CallerMemberName] through InsertPlaceholder and re-throw with
path/pathType/sha context, matching the pre-refactor Insert() exception format.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Add telemetry events to mount initialization phases
…rance

PlaceholderTable/SparseTable: add transient SQLite error resilience via GVFSTable base class
@tyrielv tyrielv enabled auto-merge June 30, 2026 18:32
@tyrielv tyrielv merged commit 5674593 into releases/shipped Jun 30, 2026
78 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