VFSForGit Agent Skills & AI Rules

GitHub

Virtual File System for Git: Enable Git at Enterprise Scale

6,145 stars C# C# 9 files ยท ~13,102 tokens
RAW Rules
1 Discovered Rules โ€ข ~3,305 Estimated Tokens
Quick:
Loading skill content...

AI Agent Protocol & Instructions

AGENTS.md root-instruction ~3,305 tokens
Raw
# 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.