Skip to content

Release M274#1963

Merged
mjcheetham merged 41 commits into
releases/shippedfrom
milestones/m274
May 11, 2026
Merged

Release M274#1963
mjcheetham merged 41 commits into
releases/shippedfrom
milestones/m274

Conversation

@mjcheetham
Copy link
Copy Markdown
Member

Changes:*

tyrielv and others added 30 commits April 8, 2026 15:54
Enable ManagePackageVersionsCentrally and move all PackageVersion declarations
to Directory.Packages.props. Individual csproj files no longer specify Version
on PackageReference elements.

Pre-declare System.Text.Json, System.CommandLine, and
System.IO.Pipes.AccessControl for upcoming migration phases.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyvella@microsoft.com>
Centralize NuGet package versions with Directory.Packages.props
Git fires the post-index-change hook for every index write, including
temp indexes created via GIT_INDEX_FILE redirect (e.g. read-tree
--index-output, git add with a temp index). The GVFS mount process
only needs to know about changes to the real `\$GIT_DIR/index`.

When writing a 194MB temp index (2.47M entries), the hook's pipe
round-trip to the GVFS mount process adds ~5s of overhead per
read-tree/add call — a significant regression for tools that
use the temp-index flow.

Add an early-exit check in IsNonCanonicalIndex() that resolves both
GIT_INDEX_FILE and `\$GIT_DIR/index` to absolute paths via
GetFullPathNameA, then compares case-insensitively. If they differ,
the hook exits immediately without contacting the mount process.

When the environment is unexpected (GIT_DIR absent, path resolution
fails), we err on the side of correctness and proceed with the
notification rather than silently suppressing it.

Unit tests invoke the hook exe with controlled environment variables
and WorkingDirectory set to %TEMP% (outside any GVFS mount) to
verify:
- temp index paths (should skip)
- canonical index paths (should NOT skip, exits NotInGVFSEnlistment)
- missing GIT_DIR (should NOT skip)
- mixed separators and case (normalization via GetFullPathNameA)

Note: the ideal long-term fix is in Git's do_write_locked_index()
(read-cache.c) to skip firing the hook entirely for non-default
indexes, avoiding the process spawn altogether.

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

PostIndexChangedHook: skip notification for non-canonical indexes
Remove the GVFlt compatibility shim project (legacy ProjFS predecessor)
and all DiskLayout upgrade paths that depend on ManagedEsent (layouts
7→8, 8→9, 9→10, 10→11, 11→12, 12→12.1, 12→13, 13→14). These upgrades
are 6+ years old and no longer needed.

Raise minimum supported disk layout version from 7 to 14. The 14→15
upgrade (ModifiedPaths) is retained so v14 repos can still upgrade.
Users on layouts below 14 must upgrade through an intermediate GVFS
version first.

Removes ManagedEsent, Microsoft.Database.Collections.Generic, and
Microsoft.Database.Isam NuGet packages entirely.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Remove GVFS.GVFlt and ESENT legacy disk layout upgrades
The Service.UI process displayed Windows toast notifications, but only
the MountFailure notification was actively sent, and it provided no
useful value. Removing eliminates XmlSerializer and WinRT COM interop
dependencies that would block NativeAOT compilation.

Removes: project, unit tests, installer integration, service launch
logic, notification handler, diagnostics log collection, and the
Microsoft.Windows.SDK.Contracts package.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](actions/github-script@v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

build(deps): bump actions/github-script from 8 to 9
MountNewWorktree() in the post-command hook silently discarded the exit
code of 'gvfs mount', leaving worktrees unprojected when concurrent
adds caused mount contention. Also discarded the git checkout exit code.

Product fix:
- Check git checkout exit code; skip mount on failure
- Check gvfs mount exit code with retry (2 retries, 100ms/250ms)
- Extract and check --exit_code from hook args to skip post-processing
  when the git command itself failed
- Return int? from GetHookExitCode so callers decide null semantics
- Apply same TryMountWithRetry to MountMovedWorktree
- Remount old worktree when 'git worktree move' fails (pre-hook unmounts)
- Remove maxRetries parameter; retry count driven by delay array length
- Emit actionable warning to stderr on mount failure with recovery command

Test fix:
- Increase concurrent worktrees from 2 to ProcessorCount for reliable
  mount contention
- Use CountdownEvent barrier for tight synchronization of launches
- Add pipe-based mount verification as primary assertion (probes the
  worktree-specific named pipe directly instead of relying on File.Exists)
- Add diagnostic capture on failure (directory listing, .git contents)
- Use dynamic branch names with GUID suffixes to avoid collisions
- Use numeric indices instead of char arithmetic for labels/paths to
  support >26 concurrent worktrees
- Only retry at lower concurrency when ALL failures are overload-related;
  mixed failure sets (overload + real regression) are reported immediately

Bug: https://dev.azure.com/microsoft/OS/_workitems/edit/61784115

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

Improve resiliency of git worktree commands
Replace reflection-based CommandLineParser 2.6.0 with Microsoft's
System.CommandLine 2.0.5 stable release. This eliminates runtime
reflection for CLI parsing, a prerequisite for NativeAOT compilation.

All 16 GVFS verbs, FastFetch, GVFS.Mount, and LockHolder migrated.
Uses SetAction+ParseResult pattern (2.0.x stable API). Shared helpers
in GVFSVerb reduce boilerplate while preserving per-verb option binding.

Replaces System.CommandLine's built-in --version (assembly reflection,
not AOT-safe) with custom --version option routing to same code as
'gvfs version' subcommand via ProcessHelper.GetCurrentProcessVersion().
Adds 'help' subcommand for backward compatibility with old CLI.
Case-insensitive verb matching preserved via args normalization.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Migrate from CommandLineParser to System.CommandLine
Replace Newtonsoft.Json 13.0.1 with System.Text.Json 8.0.5 across the
entire codebase. This eliminates the reflection-heavy Newtonsoft
dependency, a prerequisite for NativeAOT compilation.

Add GVFSJsonOptions.cs with shared JsonSerializerOptions, centralized
Serialize/Deserialize helpers, and registered custom converters.
Add EventMetadataConverter for AOT-safe Dictionary<string,object> handling.
Add VersionConverter for System.Version (no built-in STJ support).
Use runtime type dispatch in BaseResponse<T>.ToMessage() to preserve
derived-class properties during polymorphic serialization.
Replace [JsonProperty] with [JsonPropertyName] in telemetry types.
Replace JObject.Parse with JsonDocument in functional tests.
Change GitObjectSize readonly fields to properties (STJ ignores fields).
Add parameterless constructors where STJ deserialization requires them.
Set LangVersion=latest in Directory.Build.props.
Remove Newtonsoft.Json from all projects and Directory.Packages.props.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Migrate from Newtonsoft.Json to System.Text.Json
….vfs.0.7

Update default Microsoft Git version to v2.53.0.vfs.0.7
Add GVFS.CommandLine.Tests project with unit tests for command-line
argument parsing across all three executables. Tests validate verb
dispatch, argument handling, and error output without requiring a
mounted GVFS enlistment.

Separate test project (not merged into GVFS.UnitTests) because it
references all three Exe projects (GVFS, GVFS.Mount, FastFetch).

Wire into CI via RunUnitTests.bat so CLI tests run alongside existing
unit tests on every PR build.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Add CLI argument parsing tests for GVFS, GVFS.Mount, and FastFetch
Retarget from net471 to net10.0-windows10.0.17763.0 across all managed
projects. Enable NativeAOT self-contained deployment, eliminating the
.NET runtime dependency.

Build infrastructure:
- global.json: pin SDK 10.0.203
- Directory.Build.props: centralized TFM, SelfContained, PublishAot,
  OptimizationPreference=Speed
- Directory.Build.targets: AOT build targets; opt out test projects and
  GVFS.MSBuild (netstandard2.0) from AOT
- Build.bat: 3-step build (dotnet restore, VS MSBuild for C++, dotnet
  publish for managed AOT binaries)
- publish-aot.ps1: standalone script for local AOT publish testing
  (CI uses Build.bat; this script is for dev iteration)
- Update output paths in all scripts (net471 -> net10.0-.../publish)
- Update CI to .NET 10 SDK and windows-2025 runner
- Update installer MinVersion to 10.0.17763

Package updates:
- Microsoft.Windows.ProjFS 1.1 -> 2.1.0: pure C# P/Invoke replacing
  C++/CLI interop, required for NativeAOT compatibility
- Microsoft.Data.Sqlite 2.2.4 -> 9.0.4, Microsoft.Build.* 16 -> 17.12.6
- Add System.Diagnostics.EventLog, System.IO.Pipes.AccessControl:
  previously included in .NET Framework, now separate packages
- Remove GVFS.ProjFS (ProjFS is now a Windows OS feature)

Unit test fixture updates for new ProjFS managed API surface.

Output: ~20 MB native GVFS.exe, 36.7 MB installer (vs 107 MB with
full self-contained runtime)

Co-authored-by: Michael Niksa <miniksa@microsoft.com>
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Assembly.Location returns empty string under NativeAOT since there is no
managed assembly on disk. Assembly.GetName().Version returns null.

- ProcessHelper: use Environment.ProcessPath with null guard (can be null
  in certain hosting scenarios), fall back to AppContext.BaseDirectory
- HooksInstaller: same Environment.ProcessPath pattern with null guard
- GVFSEnlistment: AppDomain.CurrentDomain.FriendlyName replaces
  Assembly.GetEntryAssembly().GetName() for process name

Co-authored-by: Michael Niksa <miniksa@microsoft.com>
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
NamedPipeServerStream (WindowsPlatform.cs):
  ACL-accepting constructor removed from .NET Core; use
  NamedPipeServerStreamAcl.Create extension method.

Directory ACL APIs (WindowsFileSystem.cs, GVFSService.Windows.cs):
  Static Directory.GetAccessControl/SetAccessControl and
  Directory.CreateDirectory(path, security) removed from .NET Core;
  replaced with DirectoryInfo instance methods and
  DirectorySecurity.CreateDirectory extension.

Uri escaping (CloneVerb.cs, GVFSVerb.cs, OrgInfoApiClient.cs):
  Uri.EscapeUriString obsoleted in .NET 10 (does not escape '#', '?');
  use Uri.EscapeDataString. HttpUtility.UrlEncode (System.Web) replaced
  with WebUtility.UrlEncode (System.Net).

UseShellExecute (WindowsPlatform.cs, InProcessMount.cs):
  .NET Framework defaults UseShellExecute=true (ShellExecuteEx, no handle
  inheritance). .NET 10 defaults to false (CreateProcess, handles inherited).
  Without this, GVFS.Mount.exe inherits the caller's stdout pipe handle,
  causing callers that read to EOF to block indefinitely.

Truncated loose object detection (GitRepo.cs):
  .NET 10 DeflateStream silently returns partial data on truncated zlib
  instead of throwing InvalidDataException. CountingStream wrapper compares
  actual bytes read to header-declared size to detect corruption.

Co-authored-by: Michael Niksa <miniksa@microsoft.com>
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
System.Management requires COM interop which is incompatible with
NativeAOT. Replace WMI queries (MSFT_Volume, MSFT_Partition, MSFT_Disk,
MSFT_PhysicalDisk) with direct kernel32 DeviceIoControl calls using
IOCTL_STORAGE_QUERY_PROPERTY and IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS
for disk telemetry collection.

Co-authored-by: Michael Niksa <miniksa@microsoft.com>
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
ProjFS managed API v2.1.0 uses Marshal.PtrToStringUni which returns null
for IntPtr.Zero (kernel operations with PID 0). The old C++/CLI wrapper
returned String.Empty. Null-coalesce to match old behavior in all three
callback sites (OnPlaceholderFileCreated, OnPlaceholderFolderCreated,
OnPlaceholderFileHydrated); ConcurrentDictionary does not accept null keys.

Co-authored-by: Michael Niksa <miniksa@microsoft.com>
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Replace HttpClientHandler with SocketsHttpHandler for explicit connection
pool lifecycle management: configurable MaxConnectionsPerServer (2x CPU
count), PooledConnectionLifetime, and PooledConnectionIdleTimeout. Remove
UseDefaultCredentials (not supported on SocketsHttpHandler) and
ServicePointManager usage (.NET Framework only).

GitSsl: X509Certificate2(byte[]) constructor obsoleted; use
X509CertificateLoader.LoadCertificate.

GitAuthentication: adapt credential flow for new HTTP handler.

Remove machine.config lock check (.NET 10 does not use machine.config).

Co-authored-by: Michael Niksa <miniksa@microsoft.com>
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
NativeAOT cannot use runtime reflection for JSON serialization.
GVFSJsonContext provides source-generated System.Text.Json serializers
for 25+ types used in named pipe messages and configuration.

GVFSJsonOptions chains source-gen (primary) with reflection fallback
for types not yet in the context, allowing incremental migration.

NamedPipeMessages: add parameterless constructors required by the
source generator's deserialization codegen.

RepoRegistration: add ServiceJsonContext source generator in
GVFS.Service for types that cannot be registered in GVFSJsonContext
(GVFS.Common) due to assembly dependency direction.

Co-authored-by: Michael Niksa <miniksa@microsoft.com>
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
.NET 10's FileInfo property setters no longer open write handles that
trigger ProjFS placeholder hydration. Adapt tests that relied on this.

BasicFileSystemTests: replace ExpandedFileAttributesAreUpdated with two
focused tests:
- PlaceholderMetadataSurvivesHydration: sets timestamps + Hidden on a
  placeholder, verifies they took effect, hydrates via read+write, and
  asserts CreationTime and Hidden survived the conversion.
- HydratedFileTimestampsAndAttributesAreUpdated: hydrates first, then
  sets all properties and verifies they stick.

GitCommandsTests: ChangeTimestampAndDiff now explicitly hydrates via
read+write before adjusting timestamps, since File.SetLastWriteTime
no longer triggers ProjFS hydration.

GVFSProcess: add 5-minute timeout per gvfs process invocation to
prevent CI hangs. Stream stdout/stderr for real-time CI output.

functional-tests.yaml: reduce mount sleep from 500ms to 100ms,
add timeout-minutes.

Co-authored-by: Michael Niksa <miniksa@microsoft.com>
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The StatusTests WriteWithoutClose and CreateFileWithoutClose tests
intentionally open file handles without closing them, then validate
git status behavior. The handles were created as local variables
inside OpenFileAndWriteWithoutClose/CreateFileWithoutClose with no
reference kept — making them eligible for GC immediately. If the
GC collected the handles before TearDown ran git reset --hard, the
reset would succeed (no lock errors) while the control repo might
still have errors, causing a flaky mismatch.

Fix: return IDisposable from the file-handle-leaking methods, hold
them in using blocks in the test methods. Handles stay alive for
the assertion scope and are deterministically disposed before
TearDown runs, eliminating the GC race. Also add StreamWriter.Flush()
to ensure written data is on disk before returning.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
tyrielv and others added 11 commits May 6, 2026 13:00
…lose

Fix flaky WriteWithoutClose/CreateFileWithoutClose functional tests
Add staged upgrade mode to the installer, activated by /KEEPMOUNTED=true
in silent installs or via dialog in interactive mode. Without this flag,
the installer behaves as before.

When staging:
- Most files go to {app}\PendingUpgrade\ instead of replacing in-place
- GVFS.Service.exe is replaced directly (brief stop/start)
- Mount processes continue running on old binaries throughout
- .ready marker written after all files staged (guards against partial)

When not staging (clean upgrade):
- CloseApplications=no prevents Restart Manager from killing processes
- Force-kill GVFS processes if unmount-all fails to clean up
- WaitForServiceProcessToExit polls sc query after sc stop/delete

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Previously the .azure-pipelines/release.yml file was not being used for
release builds, but instead a private repository containing the real
YAML (in Azure Repos) was being used.

Let's bring the actual pipeline YAML into the main repo on GitHub.
This is what we do in other projects like microsoft/git and
git-ecosystem/git-credential-manager, and allows us to keep build
changes in sync between GH and AzDO.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
release.yml: update the AzDO release pipeline YAML
Add PendingUpgradeHandler to apply staged upgrades using atomic file
moves (old files to PreviousVersion, staged files to install dir).
Skips GVFS.Service.exe (already replaced by installer, locked by
running service).

Safety mechanisms:
- .ready marker: rejects PendingUpgrade if installer was interrupted
- .phase1-complete marker: ensures crash during backup is recoverable
  (incomplete Phase 1 is restored and retried, not skipped)
- Defers if any GVFS.Mount processes are still running

Trigger upgrade in two ways:
- On service start: checks before automount
- After repo unmount: timer-based debounce (5s) so multiple unmounts
  in quick succession result in a single upgrade attempt

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Add upgrade-tests.yaml with matrix of 5 scenarios:
- staging-upgrade: LKG -> mount -> stage -> unmount -> verify completion
- clean-upgrade: LKG -> mount -> clean install -> verify
- double-staging: stage twice, verify second overwrites first
- staging-then-clean: stage then clean install removes PendingUpgrade
- mount-safety-deferral: verify upgrade defers while mount is running

Wire into build.yaml as a required check alongside functional tests.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Non-disruptive upgrade: staged install with in-process file copy
Address review feedback from #1958:

- StopService/StagingUpdateService: check sc.exe exit code separately
  from Exec launch failure. Previously non-zero sc.exe results were
  silently ignored.

- Timer race: wrap pendingUpgradeTimer create/reset in a lock. Two
  concurrent unmounts on separate pipe threads could both observe null
  and create duplicate timers, causing parallel TryApplyPendingUpgrade.

- .ready race: move service start from AfterInstall hook to ssPostInstall
  after .ready marker is written. Previously the service could start its
  5s debounce timer before .ready existed, skip the upgrade, and leave
  staged files until the next service restart.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Fix staged upgrade races and installer error handling
Upgrade to .NET 10 is a major change, and excluding bundled ProjFS driver for older systems is potentially breaking.
Update GVFS version to 2.0 in release pipeline
@mjcheetham mjcheetham requested review from dscho and tyrielv May 11, 2026 16:02
@mjcheetham mjcheetham merged commit dad0fd9 into releases/shipped May 11, 2026
109 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