Skip to content

Latest commit

 

History

History
275 lines (214 loc) · 12.6 KB

File metadata and controls

275 lines (214 loc) · 12.6 KB

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 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:

# 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\):

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.

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 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.

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.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.

# 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.

# ✅ 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 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.