Virtual File System for Git: Enable Git at Enterprise Scale
# Agent instructions for VFS for Git
This file is for AI coding assistants (GitHub Copilot, Claude Code, Cursor, OpenAI
Codex, Gemini CLI, Aider, etc.). It captures project-specific knowledge that isn't
obvious from a fresh `git clone` and which routinely trips up agents. See
[CONTRIBUTING.md](CONTRIBUTING.md) for coding standards (StyleCop rules, error
handling, exception logging) โ do not duplicate those here.
## Repository layout
The build scripts (`scripts\Build.bat` and friends) put build outputs **one
level up** from the git working tree. The Readme documents the recommended
clone-into-`src\` convention, which matches the layout that `gvfs clone`
creates for end users:
```powershell
# Recommended (matches Readme.md and CI):
git clone https://github.com/microsoft/VFSForGit C:\Repos\VFSForGit\src
```
```
C:\Repos\VFSForGit\
โโโ src\ โ git working tree (.git, GVFS.sln, all source)
โโโ out\ โ build output (gitignored โ it's outside the working tree)
โโโ packages\ โ NuGet cache
```
Cloning without the `src\` suffix also works โ outputs just land one level
above wherever you cloned. The rest of this document and the project's own
scripts (CI, Readme) use the `src\` form, so commands assume it. **The
parent directory must be writable** (cloning to `C:\` itself won't work
because the build can't create `C:\out`).
All commands below run from the **enlistment root** (the parent of `src\`):
```powershell
cd C:\Repos\VFSForGit # NOT C:\Repos\VFSForGit\src
```
This keeps `src\...` and `out\...` paths symmetric and matches what
`Build.bat` does internally.
## Build paths
`scripts\Build.bat` does a full installer build with NativeAOT publish plus
Inno Setup. That's ~5 minutes minimum, and AOT has no incremental support
โ ilc relinks every executable on every `dotnet publish`. **Do not use
`Build.bat` for dev-loop iteration.** Pick the right path for what you're
doing.
### Path A โ Unit-test inner loop (~10โ15 s incremental)
For C# changes verified by unit tests only.
```powershell
dotnet build src\GVFS\GVFS.UnitTests\GVFS.UnitTests.csproj -c Debug
& "out\GVFS.UnitTests\bin\Debug\net10.0-windows10.0.17763.0\win-x64\GVFS.UnitTests.exe" --test Fully.Qualified.Class.Or.Method
```
Skips `dotnet publish`, AOT, native C++ projects, payload assembly, installer.
### Path B โ Functional-test inner loop (~30โ60 s incremental)
For changes that need the GVFS payload (`gvfs.exe`, hooks, service) but not
an installer. `PublishAot=false` skips ilc (~3โ4 min saved);
`SkipCreateInstaller=true` skips Inno Setup (~95 s saved).
`GVFS.Payload` only assembles the payload directory. It does not build or
publish the projects that it copies from.
> **Prerequisite: the native C++ projects and all managed payload projects must
> already have built outputs.** The native projects are `.vcxproj` (see
> [Native C++ projects](#native-c-projects-need-msbuild-not-dotnet-build)
> below) and `dotnet publish` will not build them for you. If you have not
> already done a `Build.bat` once in this enlistment, build the native
> projects via VS MSBuild and publish the managed payload projects below
> (or run `Build.bat` once to populate `out\`, then iterate with the commands
> below). After that they are incremental and only rebuild when their own
> sources change.
```powershell
dotnet publish src\GVFS\GVFS.FunctionalTests\GVFS.FunctionalTests.csproj `
-c Debug /p:PublishAot=false
dotnet publish src\GVFS\GVFS\GVFS.csproj `
-c Debug /p:PublishAot=false
dotnet publish src\GVFS\GVFS.Hooks\GVFS.Hooks.csproj `
-c Debug /p:PublishAot=false
dotnet publish src\GVFS\GVFS.Mount\GVFS.Mount.csproj `
-c Debug /p:PublishAot=false
dotnet publish src\GVFS\GVFS.Service\GVFS.Service.csproj `
-c Debug /p:PublishAot=false
dotnet publish src\GVFS\GVFS.Payload\GVFS.Payload.csproj `
-c Debug /p:PublishAot=false /p:SkipCreateInstaller=true
src\scripts\RunFunctionalTests-Dev.ps1 -Configuration Debug -Arch x64 `
--test=GVFS.FunctionalTests.Tests.<Namespace>.<Class>.<Method>
```
`layout.bat` (invoked by GVFS.Payload) `xcopy`s from each project's `publish\`
or native-output directory โ the C# projects do not require AOT, so
`PublishAot=false` produces a fully functional test payload. The native
hook binaries are copied straight from the vcxproj output.
> **Publish each changed project explicitly.** `GVFS.Payload.csproj` has no
> `ProjectReference` items. Its `CreatePayload` target runs `layout.bat`, which
> copies existing project output. A project that you do not publish can
> contribute stale output from an earlier build. Publish all four managed
> payload projects at least once in a new enlistment, and publish a project
> again when you change it.
`RunFunctionalTests-Dev.ps1` runs functional tests against the build output
without requiring admin or a system-wide install. It launches the test
service as a console process. Each invocation gets a unique service name and
service-data directory. Some fixtures still use shared machine paths or drive
mappings, so do not assume that separate test processes can run concurrently.
The script sets dev-mode variables. `Settings.Default.Initialize` then resolves
`gvfs.exe` and `GVFS.Service.exe` from `out\GVFS.Payload\...`, not
`C:\Program Files\VFS for Git\`. This path does not require a GVFS
installation.
> **Pass `-Configuration` and `-Arch` by name.** The script's first two
> positional parameters are `Configuration` and `Arch`. A bare
> `RunFunctionalTests-Dev.ps1 Debug --test=...` binds `--test=...` to `-Arch`
> and fails its `ValidateSet`.
### Path C โ Installer build (~5 min โ only when you need an installer)
For producing an installable package (testing install/upgrade flows, or
shipping a build). This is the only correct use of `Build.bat`.
```powershell
# Build.bat takes (configuration, version, verbosity). The 0. prefix on the
# version tells GVFS to treat this as a development build and skip the
# server-side version check.
$v = & { $n = [DateTime]::Now; "0.2.$($n.ToString('yy'))$($n.DayOfYear.ToString('D3')).$([int]($n.TimeOfDay.TotalSeconds / 2))" }
src\scripts\Build.bat Debug $v minimal
```
Installer output: `out\GVFS.Installers\bin\Debug\win-x64\SetupGVFS.<version>.exe`.
## Native C++ projects (need MSBuild, not `dotnet build`)
These five projects are `.vcxproj` and require Visual Studio MSBuild with
the C++ workload. `Build.bat` invokes MSBuild for them automatically; if
you need to rebuild them outside `Build.bat`, use `msbuild`, not
`dotnet build`.
- `GVFS\GitHooksLoader\GitHooksLoader.vcxproj`
- `GVFS\GVFS.NativeTests\GVFS.NativeTests.vcxproj`
- `GVFS\GVFS.PostIndexChangedHook\GVFS.PostIndexChangedHook.vcxproj`
- `GVFS\GVFS.ReadObjectHook\GVFS.ReadObjectHook.vcxproj`
- `GVFS\GVFS.VirtualFileSystemHook\GVFS.VirtualFileSystemHook.vcxproj`
These only need to be (re)built when their own sources change; they don't
participate in the C# inner-loop paths above.
## Running tests
### NUnit filter syntax โ `--test`, NEVER `--where`
> **โ ๏ธ This codebase uses NUnitLite, which supports ONLY `--test` for name
> filtering. The `--where` filter (from NUnit Console) is silently ignored
> and runs every test.**
```powershell
# โ
Correct
& "out\GVFS.UnitTests\bin\...\GVFS.UnitTests.exe" --test "GVFS.UnitTests.Common.WorktreeInfoTests"
src\scripts\RunFunctionalTests-Dev.ps1 -Configuration Debug -Arch x64 `
--test=GVFS.FunctionalTests.Tests.GVFSVerbTests.UnknownVerb
# โ Wrong โ silently runs the entire suite
& "out\GVFS.UnitTests\bin\...\GVFS.UnitTests.exe" --where "class =~ Worktree"
src\scripts\RunFunctionalTests-Dev.ps1 -Configuration Debug -Arch x64 `
--where "cat == Smoke"
```
For unit tests, `--where` is merely annoying (the whole suite runs in
~10 seconds). For functional tests, it's a disaster: each test provisions
its own fresh enlistment, so accidentally running the full suite eats
hours and masks whichever failure you were actually investigating.
### Fully qualified names required
`--test` matches against the fully qualified name (`Namespace.Class.Method`
or `Namespace.Class`). Short names like `--test=ReproCherryPickRestoreCorruption`
silently match nothing and the runner reports "0 tests selected" without
making the typo obvious.
## vcpkg caching
`Build.bat` checks for `out\vcpkg_installed\dynamic\x64-windows-dynamic\bin\
git2.dll` as a "vcpkg already installed" marker and skips the vcpkg step
if present. The vcpkg step is the slowest part of a from-scratch build
(several minutes of native compilation), so this caching matters.
Do not manually delete or re-run vcpkg unless you've changed an overlay
port. If you've copied `out\` from another enlistment as a build-time
shortcut, vcpkg results come along with it.
## What ships in a public release
The signed release pipeline builds several artifacts, but the **public GitHub
release only carries the installer and symbols**: `SetupGVFS.<version>.exe`
(per arch) and `Symbols.zip`.
- **FastFetch is NOT shipped publicly.** `FastFetch.exe` is built and
ESRP-signed as a pipeline artifact, but it is not attached to the GitHub
release. Treat FastFetch-only changes as **non-shipping** when scoping a
release or writing changelog notes โ they don't reach the installer end
users get.
- **Git is NOT bundled.** VFS for Git does not ship Microsoft Git. PRs titled
"update default Microsoft Git version" change only the CI `GIT_VERSION` in
`.github/workflows/build.yaml` (used to install Git for build + functional
test runs) โ they are **non-shipping**. The only shipped Git constraint is
the compiled `MinimumGitVersion` constant in `Version.props`, which is
changed separately and rarely.
## Feature flags
Product feature flags are **git config** keys under the `gvfs.` prefix (not a
separate config system). To add one, mirror `gvfs.show-hydration-status`:
- Declare the key name and default in `GVFS.Common/GVFSConstants.cs` under
`GitConfig` (e.g. `ShowHydrationStatus = GVFSPrefix + "show-hydration-status"`
and `ShowHydrationStatusDefault = false`).
- Read it via `repo.GetConfigBoolOrDefault(name, default)` (or
`LibGit2RepoInvoker.GetConfigBoolOrDefault`) at the point of use.
Default new gates to `false` and gate the **runtime entry point** into a
feature, not its build, so the code still compiles and ships (and keeps
getting exercised) while its behavior stays off by default.
## libgit2 P/Invoke string marshalling (UTF-8, not ANSI)
The libgit2 bindings live in `GVFS.Common/Git/LibGit2Repo.cs` under the
`Native` class. **libgit2 treats every string it receives and returns as
UTF-8** โ paths, revspecs, config keys and values, and error messages.
The .NET default for a bare `[DllImport]` string parameter is
`CharSet.Ansi`, which encodes through the Windows ANSI code page and
**silently corrupts non-ASCII input** (a repo path under a non-English user
name, a non-ASCII branch name, etc.) โ an unmappable character becomes `?`,
so `git_repository_open` and friends resolve the wrong path or fail. There is
no `[module: DefaultCharSet]` override in `GVFS.Common`, so the ANSI default
applies unless each declaration opts out.
When you add a libgit2 P/Invoke:
- Annotate **every** `string` parameter, `out string` parameter, string return
value, and string struct field with
`[MarshalAs(UnmanagedType.LPUTF8Str)]`.
- Do **not** use `CharSet.Unicode` โ that marshals UTF-16, which libgit2 does
not accept (it is the same bug in the other direction).
- For a function that returns a **borrowed** pointer owned by libgit2 (e.g.
`git_config_get_string`), marshal it as `IntPtr` and copy with
`Marshal.PtrToStringUTF8(...)`; do not marshal it as `out string`, or the
interop marshaller frees libgit2's heap memory with the wrong allocator.
Because the mismatch only manifests through the native call, cover new
string-carrying paths with a real-libgit2 test that uses a non-ASCII input
(see `GVFS.FunctionalTests/Tests/LibGit2NonAsciiPathTests.cs`), not a
mock-based unit test.
## Public repository hygiene
Do not include Microsoft-internal work item IDs, ADO URLs, incident IDs, or
internal service names in repository content or pull requests. Describe the
public problem and fix without requiring internal access.
## Coding standards
See [CONTRIBUTING.md](CONTRIBUTING.md) for StyleCop rules, error-handling
patterns (`TryXxx` over exceptions, "fail fast" on data-loss risks),
tracing/logging conventions (Error level reserved for unrecoverable
failures), and the `mock:\\` / `mock://` URL convention for unit tests.