Release M277#2036
Merged
Merged
Conversation
…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>
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>
Add ARM64 native build support
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
KeithIsSleeping
approved these changes
Jun 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes:
git ls-files -sinstead ofls-treefor full-tree enumeration (Use git ls-files -s instead of ls-tree for full-tree enumeration #2013)git_odb_existsinstead ofgit_revparse_singlefor ObjectExists (Use git_odb_exists instead of git_revparse_single for ObjectExists #2006)actions/checkoutfrom 6 to 7 (Bump actions/checkout from 6 to 7 #2035)