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 for coding standards (StyleCop rules, error
handling, exception logging) — do not duplicate those here.
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:
# Recommended (matches Readme.md and CI):
git clone https://github.com/microsoft/VFSForGit C:\Repos\VFSForGit\srcC:\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\):
cd C:\Repos\VFSForGit # NOT C:\Repos\VFSForGit\srcThis keeps src\... and out\... paths symmetric and matches what
Build.bat does internally.
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.
For C# changes verified by unit tests only.
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.MethodSkips dotnet publish, AOT, native C++ projects, payload assembly, installer.
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 below) anddotnet publishwill not build them for you. If you have not already done aBuild.batonce in this enlistment, build the native projects via VS MSBuild and publish the managed payload projects below (or runBuild.batonce to populateout\, then iterate with the commands below). After that they are incremental and only rebuild when their own sources change.
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) xcopys 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.csprojhas noProjectReferenceitems. ItsCreatePayloadtarget runslayout.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
-Configurationand-Archby name. The script's first two positional parameters areConfigurationandArch. A bareRunFunctionalTests-Dev.ps1 Debug --test=...binds--test=...to-Archand fails itsValidateSet.
For producing an installable package (testing install/upgrade flows, or
shipping a build). This is the only correct use of Build.bat.
# 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 minimalInstaller output: out\GVFS.Installers\bin\Debug\win-x64\SetupGVFS.<version>.exe.
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.vcxprojGVFS\GVFS.NativeTests\GVFS.NativeTests.vcxprojGVFS\GVFS.PostIndexChangedHook\GVFS.PostIndexChangedHook.vcxprojGVFS\GVFS.ReadObjectHook\GVFS.ReadObjectHook.vcxprojGVFS\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.
⚠️ This codebase uses NUnitLite, which supports ONLY--testfor name filtering. The--wherefilter (from NUnit Console) is silently ignored and runs every test.
# ✅ 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.
--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.
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.
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.exeis 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_VERSIONin.github/workflows/build.yaml(used to install Git for build + functional test runs) — they are non-shipping. The only shipped Git constraint is the compiledMinimumGitVersionconstant inVersion.props, which is changed separately and rarely.
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.csunderGitConfig(e.g.ShowHydrationStatus = GVFSPrefix + "show-hydration-status"andShowHydrationStatusDefault = false). - Read it via
repo.GetConfigBoolOrDefault(name, default)(orLibGit2RepoInvoker.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.
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
stringparameter,out stringparameter, 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 asIntPtrand copy withMarshal.PtrToStringUTF8(...); do not marshal it asout 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.
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.
See 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.