diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..4fafb53e9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,174 @@ +# 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` cascades to its dependencies (GVFS, GVFS.Mount, GVFS.Hooks, +GVFS.Service) via `ProjectReference`. + +> **Prerequisite: the native C++ projects must already be built.** They 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 first (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.Payload\GVFS.Payload.csproj ` + -c Debug /p:PublishAot=false /p:SkipCreateInstaller=true + +src\scripts\RunFunctionalTests-Dev.ps1 Debug --test=GVFS.FunctionalTests.Tests... +``` + +`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. + +`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 data dir, so concurrent runs from different worktrees don't collide. + +### 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..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 Debug --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 Debug --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. + +## 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. diff --git a/GVFS/FastFetch/CheckoutPrefetcher.cs b/GVFS/FastFetch/CheckoutPrefetcher.cs index e8a48a941..4064aa7bc 100644 --- a/GVFS/FastFetch/CheckoutPrefetcher.cs +++ b/GVFS/FastFetch/CheckoutPrefetcher.cs @@ -72,7 +72,7 @@ public override void Prefetch(string branchOrCommit, bool isBranch) commitToFetch = branchOrCommit; } - using (new IndexLock(this.Enlistment.EnlistmentRoot, this.Tracer)) + using (new IndexLock(this.Enlistment.PrimaryEnlistmentRoot, this.Tracer)) { this.DownloadMissingCommit(commitToFetch, this.GitObjects); @@ -124,7 +124,7 @@ public override void Prefetch(string branchOrCommit, bool isBranch) if (!indexGen.HasFailures) { Index newIndex = new Index( - this.Enlistment.EnlistmentRoot, + this.Enlistment.PrimaryEnlistmentRoot, this.Tracer, indexGen.TemporaryIndexFilePath, readOnly: false); @@ -200,7 +200,7 @@ private Index GetSourceIndex() if (File.Exists(indexPath)) { - Index output = new Index(this.Enlistment.EnlistmentRoot, this.Tracer, indexPath, readOnly: true); + Index output = new Index(this.Enlistment.PrimaryEnlistmentRoot, this.Tracer, indexPath, readOnly: true); output.Parse(); return output; } diff --git a/GVFS/FastFetch/FastFetchVerb.cs b/GVFS/FastFetch/FastFetchVerb.cs index 27c5b1c4f..737b31ffe 100644 --- a/GVFS/FastFetch/FastFetchVerb.cs +++ b/GVFS/FastFetch/FastFetchVerb.cs @@ -240,7 +240,7 @@ private int ExecuteWithExitCode() CacheServerInfo cacheServer = new CacheServerInfo(this.GetRemoteUrl(enlistment), null); tracer.WriteStartEvent( - enlistment.EnlistmentRoot, + enlistment.PrimaryEnlistmentRoot, enlistment.RepoUrl, cacheServer.Url, new EventMetadata diff --git a/GVFS/FastFetch/GitEnlistment.cs b/GVFS/FastFetch/GitEnlistment.cs index fd08cdda0..2fec7cbbc 100644 --- a/GVFS/FastFetch/GitEnlistment.cs +++ b/GVFS/FastFetch/GitEnlistment.cs @@ -29,7 +29,7 @@ private GitEnlistment(string repoRoot, string gitBinPath) public string FastFetchLogRoot { - get { return Path.Combine(this.EnlistmentRoot, GVFSConstants.DotGit.Root, ".fastfetch"); } + get { return Path.Combine(this.PrimaryEnlistmentRoot, GVFSConstants.DotGit.Root, ".fastfetch"); } } public static GitEnlistment CreateFromCurrentDirectory(string gitBinPath) diff --git a/GVFS/GVFS.Common/Database/GVFSDatabase.cs b/GVFS/GVFS.Common/Database/GVFSDatabase.cs index 4e191a7ff..5104c504a 100644 --- a/GVFS/GVFS.Common/Database/GVFSDatabase.cs +++ b/GVFS/GVFS.Common/Database/GVFSDatabase.cs @@ -21,10 +21,10 @@ public class GVFSDatabase : IGVFSConnectionPool, IDisposable private IDbConnectionFactory connectionFactory; private BlockingCollection connectionPool; - public GVFSDatabase(PhysicalFileSystem fileSystem, string enlistmentRoot, IDbConnectionFactory connectionFactory, int initialPooledConnections = InitialPooledConnections) + public GVFSDatabase(PhysicalFileSystem fileSystem, string dotGVFSRoot, IDbConnectionFactory connectionFactory, int initialPooledConnections = InitialPooledConnections) { this.connectionPool = new BlockingCollection(); - this.databasePath = Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.VFSForGit); + this.databasePath = Path.Combine(dotGVFSRoot, GVFSConstants.DotGVFS.Databases.VFSForGit); this.connectionFactory = connectionFactory; string folderPath = Path.GetDirectoryName(this.databasePath); diff --git a/GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade_SqlitePlaceholders.cs b/GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade_SqlitePlaceholders.cs index eeffd14e0..e5a295480 100644 --- a/GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade_SqlitePlaceholders.cs +++ b/GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade_SqlitePlaceholders.cs @@ -30,7 +30,7 @@ public override bool TryUpgrade(ITracer tracer, string enlistmentRoot) } using (placeholderList) - using (GVFSDatabase database = new GVFSDatabase(fileSystem, enlistmentRoot, new SqliteDatabase())) + using (GVFSDatabase database = new GVFSDatabase(fileSystem, Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot), new SqliteDatabase())) { PlaceholderTable placeholders = new PlaceholderTable(database); List oldPlaceholderEntries = placeholderList.GetAllEntries(); diff --git a/GVFS/GVFS.Common/Enlistment.cs b/GVFS/GVFS.Common/Enlistment.cs index 5dcfd9e54..a9208b0ed 100644 --- a/GVFS/GVFS.Common/Enlistment.cs +++ b/GVFS/GVFS.Common/Enlistment.cs @@ -21,7 +21,7 @@ protected Enlistment( throw new ArgumentException("Path to git.exe must be set"); } - this.EnlistmentRoot = enlistmentRoot; + this.PrimaryEnlistmentRoot = enlistmentRoot; this.WorkingDirectoryRoot = workingDirectoryRoot; this.WorkingDirectoryBackingRoot = workingDirectoryBackingRoot; this.DotGitRoot = Path.Combine(this.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Root); @@ -52,7 +52,7 @@ protected Enlistment( this.Authentication = authentication ?? new GitAuthentication(gitProcess, this.RepoUrl); } - public string EnlistmentRoot { get; } + public string PrimaryEnlistmentRoot { get; } // Path to the root of the working (i.e. "src") directory. // On platforms where the contents of the working directory are stored diff --git a/GVFS/GVFS.Common/FileSystem/HooksInstaller.cs b/GVFS/GVFS.Common/FileSystem/HooksInstaller.cs index f7dfae0fc..bdf6a03cb 100644 --- a/GVFS/GVFS.Common/FileSystem/HooksInstaller.cs +++ b/GVFS/GVFS.Common/FileSystem/HooksInstaller.cs @@ -229,7 +229,7 @@ private static bool TryUpdateHook( metadata.Add(nameof(installedHookPath), installedHookPath); metadata.Add("Exception", e.ToString()); context.Tracer.RelatedError(metadata, "Failed to compare " + hookName + " version"); - errorMessage = "Error comparing " + hookName + " versions. " + ConsoleHelper.GetGVFSLogMessage(context.Enlistment.EnlistmentRoot); + errorMessage = "Error comparing " + hookName + " versions. " + ConsoleHelper.GetGVFSLogMessage(context.Enlistment.WorkingDirectoryRoot); return false; } } @@ -248,7 +248,7 @@ private static bool TryUpdateHook( metadata.Add(nameof(installedHookPath), installedHookPath); metadata.Add("Exception", e.ToString()); context.Tracer.RelatedError(metadata, "Failed to copy " + hookName + " to enlistment"); - errorMessage = "Error copying " + hookName + " to enlistment. " + ConsoleHelper.GetGVFSLogMessage(context.Enlistment.EnlistmentRoot); + errorMessage = "Error copying " + hookName + " to enlistment. " + ConsoleHelper.GetGVFSLogMessage(context.Enlistment.WorkingDirectoryRoot); return false; } } diff --git a/GVFS/GVFS.Common/GVFSEnlistment.cs b/GVFS/GVFS.Common/GVFSEnlistment.cs index e0b18b28b..286b32037 100644 --- a/GVFS/GVFS.Common/GVFSEnlistment.cs +++ b/GVFS/GVFS.Common/GVFSEnlistment.cs @@ -30,11 +30,11 @@ public GVFSEnlistment(string enlistmentRoot, string repoUrl, string gitBinPath, flushFileBuffersForPacks: true, authentication: authentication) { - this.NamedPipeName = GVFSPlatform.Instance.GetNamedPipeName(this.EnlistmentRoot); - this.DotGVFSRoot = Path.Combine(this.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); + this.NamedPipeName = GVFSPlatform.Instance.GetNamedPipeName(this.PrimaryEnlistmentRoot); + this.DotGVFSRoot = Path.Combine(this.PrimaryEnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); this.GitStatusCacheFolder = Path.Combine(this.DotGVFSRoot, GVFSConstants.DotGVFS.GitStatusCache.Name); this.GitStatusCachePath = Path.Combine(this.DotGVFSRoot, GVFSConstants.DotGVFS.GitStatusCache.CachePath); - this.GVFSLogsRoot = Path.Combine(this.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot, GVFSConstants.DotGVFS.LogName); + this.GVFSLogsRoot = Path.Combine(this.DotGVFSRoot, GVFSConstants.DotGVFS.LogName); this.LocalObjectsRoot = Path.Combine(this.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Objects.Root); } diff --git a/GVFS/GVFS.Common/Git/GitRepo.cs b/GVFS/GVFS.Common/Git/GitRepo.cs index e5aefa579..d88ebbd89 100644 --- a/GVFS/GVFS.Common/Git/GitRepo.cs +++ b/GVFS/GVFS.Common/Git/GitRepo.cs @@ -16,6 +16,7 @@ public class GitRepo : IDisposable private PhysicalFileSystem fileSystem; private LibGit2RepoInvoker libgit2RepoInvoker; private Enlistment enlistment; + private string dotGVFSRoot; public GitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSystem, Func repoFactory = null) { @@ -23,6 +24,11 @@ public GitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSys this.enlistment = enlistment; this.fileSystem = fileSystem; + // Resolve the per-worktree .gvfs root if available; otherwise + // derive from EnlistmentRoot (primary enlistments). + this.dotGVFSRoot = (enlistment as GVFSEnlistment)?.DotGVFSRoot + ?? Path.Combine(enlistment.PrimaryEnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); + this.GVFSLock = new GVFSLock(tracer); this.libgit2RepoInvoker = new LibGit2RepoInvoker( @@ -240,7 +246,7 @@ private LooseBlobState GetLooseBlobStateAtPath(string blobPath, Action - public static Dictionary GetRequiredSettings(Enlistment enlistment) + public static Dictionary GetRequiredSettings(GVFSEnlistment enlistment) { string expectedHooksPath = Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.Hooks.RootName); expectedHooksPath = Paths.ConvertPathToGitFormat(expectedHooksPath); @@ -31,8 +31,7 @@ public static Dictionary GetRequiredSettings(Enlistment enlistme if (!GVFSEnlistment.IsUnattended(tracer: null) && GVFSPlatform.Instance.IsGitStatusCacheSupported()) { gitStatusCachePath = Path.Combine( - enlistment.EnlistmentRoot, - GVFSPlatform.Instance.Constants.DotGVFSRoot, + enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.GitStatusCache.CachePath); gitStatusCachePath = Paths.ConvertPathToGitFormat(gitStatusCachePath); diff --git a/GVFS/GVFS.Common/HealthCalculator/EnlistmentPathData.cs b/GVFS/GVFS.Common/HealthCalculator/EnlistmentPathData.cs index 8a6df6818..d56cfc8bd 100644 --- a/GVFS/GVFS.Common/HealthCalculator/EnlistmentPathData.cs +++ b/GVFS/GVFS.Common/HealthCalculator/EnlistmentPathData.cs @@ -54,7 +54,7 @@ public void LoadPlaceholdersFromDatabase(GVFSEnlistment enlistment) List filePlaceholders = new List(); List folderPlaceholders = new List(); - using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistment.EnlistmentRoot, new SqliteDatabase())) + using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistment.DotGVFSRoot, new SqliteDatabase())) { PlaceholderTable placeholderTable = new PlaceholderTable(database); placeholderTable.GetAllEntries(out filePlaceholders, out folderPlaceholders); diff --git a/GVFS/GVFS.Common/LocalCacheResolver.cs b/GVFS/GVFS.Common/LocalCacheResolver.cs index b0c31dc86..dbdb261e0 100644 --- a/GVFS/GVFS.Common/LocalCacheResolver.cs +++ b/GVFS/GVFS.Common/LocalCacheResolver.cs @@ -33,7 +33,7 @@ public static bool TryGetDefaultLocalCacheRoot(GVFSEnlistment enlistment, out st return true; } - return GVFSPlatform.Instance.TryGetDefaultLocalCacheRoot(enlistment.EnlistmentRoot, out localCacheRoot, out localCacheRootError); + return GVFSPlatform.Instance.TryGetDefaultLocalCacheRoot(enlistment.PrimaryEnlistmentRoot, out localCacheRoot, out localCacheRootError); } public bool TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( diff --git a/GVFS/GVFS.Common/Maintenance/GitMaintenanceStep.cs b/GVFS/GVFS.Common/Maintenance/GitMaintenanceStep.cs index ae14ab4ce..1df648fc4 100644 --- a/GVFS/GVFS.Common/Maintenance/GitMaintenanceStep.cs +++ b/GVFS/GVFS.Common/Maintenance/GitMaintenanceStep.cs @@ -36,7 +36,7 @@ public static bool EnlistmentRootReady(GVFSContext context) // a "Device is not ready" error. try { - return context.FileSystem.DirectoryExists(context.Enlistment.EnlistmentRoot) + return context.FileSystem.DirectoryExists(context.Enlistment.WorkingDirectoryRoot) && context.FileSystem.DirectoryExists(context.Enlistment.GitObjectsRoot); } catch (IOException) diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorktreeTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorktreeTests.cs index 1f8501043..7de8bb680 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorktreeTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorktreeTests.cs @@ -268,6 +268,101 @@ public void WorktreeOutsideEnlistmentTree() } } + [TestCase] + public void WorktreeUsesPerWorktreePlaceholderDatabase() + { + // Two commits where test4.txt has different content + const string CommitA = "5d7a7d4db1734fb468a4094469ec58d26301b59d"; + const string CommitB = "fec239ea12de1eda6ae5329d4f345784d5b61ff9"; + string testFileRelativePath = Path.Combine( + "Test_EPF_UpdatePlaceholderTests", + "LockToPreventUpdateAndDelete", + "test4.txt"); + const string ContentAtCommitA = "TestFileLockToPreventUpdateAndDelete4 \r\n"; + const string ContentAtCommitB = "Commit2LockToPreventUpdateAndDelete4 \r\n"; + + string suffix = Guid.NewGuid().ToString("N").Substring(0, 8); + string tempDir = Path.Combine(Path.GetTempPath(), $"gvfs-db-test-{suffix}"); + string worktreePath = Path.Combine(tempDir, "wt"); + string branchName = $"db-test-branch-{suffix}"; + + try + { + Directory.CreateDirectory(tempDir); + + // 1. Checkout commitB in the primary and read the file to + // create a placeholder with version B content. + GitHelpers.InvokeGitAgainstGVFSRepo( + this.Enlistment.RepoRoot, $"checkout {CommitB}") + .ExitCode.ShouldEqual(0, "checkout commitB in primary failed"); + + string primaryFilePath = Path.Combine(this.Enlistment.RepoRoot, testFileRelativePath); + File.ReadAllText(primaryFilePath).ShouldEqual(ContentAtCommitB, + "Primary should have version B content"); + + // 2. Create a worktree at commitA and read the same file. + // Without the fix, the worktree mount writes the placeholder + // entry (SHA_A) to the primary's DB, overwriting SHA_B. + ProcessResult addResult = GitHelpers.InvokeGitAgainstGVFSRepo( + this.Enlistment.RepoRoot, + $"worktree add -b {branchName} \"{worktreePath}\" {CommitA}"); + addResult.ExitCode.ShouldEqual(0, + $"worktree add failed: {addResult.Errors}"); + + this.AssertWorktreeMounted(worktreePath, "db-test worktree"); + + string worktreeFilePath = Path.Combine(worktreePath, testFileRelativePath); + File.ReadAllText(worktreeFilePath).ShouldEqual(ContentAtCommitA, + "Worktree should have version A content"); + + // 3. Checkout commitA in the primary. UpdatePlaceholders reads + // the placeholder DB to find files that need updating. + // With a contaminated DB (SHA_A from worktree), the primary's + // placeholder (on-disk SHA_B) looks up-to-date (DB says SHA_A, + // index wants SHA_A — match) and the update is skipped. + // The file retains stale commitB content. + GitHelpers.InvokeGitAgainstGVFSRepo( + this.Enlistment.RepoRoot, $"checkout {CommitA}") + .ExitCode.ShouldEqual(0, "checkout commitA in primary failed"); + + string contentAfterCheckout = File.ReadAllText(primaryFilePath); + contentAfterCheckout.ShouldEqual(ContentAtCommitA, + $"After checkout to commitA, primary should have version A content.\n" + + $"Got version B content instead — the worktree mount contaminated\n" + + $"the primary's placeholder DB, causing UpdatePlaceholders to skip\n" + + $"the update because the DB SHA matched the index SHA."); + + // 4. Verify the per-worktree placeholder DB exists (isolation check) + GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(worktreePath); + Assert.IsNotNull(wtInfo, "Should be able to resolve worktree info"); + string worktreeDbPath = Path.Combine( + wtInfo.WorktreeGitDir, ".gvfs", "databases", "VFSForGit.sqlite"); + + bool dbExists = false; + for (int i = 0; i < 20; i++) + { + if (File.Exists(worktreeDbPath)) + { + dbExists = true; + break; + } + + System.Threading.Thread.Sleep(500); + } + + Assert.IsTrue(dbExists, + $"Per-worktree VFSForGit.sqlite should exist at {worktreeDbPath}"); + } + finally + { + this.ForceCleanupWorktree(worktreePath, branchName); + if (Directory.Exists(tempDir)) + { + try { Directory.Delete(tempDir, recursive: true); } catch { } + } + } + } + private void InitWorktreeArrays(int count, out string[] paths, out string[] branches) { paths = new string[count]; diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index 824940a0f..1272eb876 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -211,7 +211,7 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords) this.FailMountAndExit("The .git folder is missing or has invalid contents"); } - if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.EnlistmentRoot, out string fsError)) + if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.WorkingDirectoryRoot, out string fsError)) { this.FailMountAndExit("FileSystem unsupported: " + fsError); } @@ -277,7 +277,7 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords) try { - Console.Title = "GVFS " + ProcessHelper.GetCurrentProcessVersion() + " - " + this.enlistment.EnlistmentRoot; + Console.Title = "GVFS " + ProcessHelper.GetCurrentProcessVersion() + " - " + this.enlistment.WorkingDirectoryRoot; } catch (IOException) { @@ -365,7 +365,7 @@ private void InitializeWorktreeMetadata() // Use try/finally to guarantee Shutdown() even if an unexpected // exception occurs — the singleton must not be left pointing at // the primary's metadata directory. - string primaryDotGVFS = Path.Combine(this.enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); + string primaryDotGVFS = Path.Combine(this.enlistment.PrimaryEnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); string error; string gitObjectsRoot; string localCacheRoot; @@ -996,7 +996,7 @@ private void HandlePostFetchJobRequest(NamedPipeMessages.Message message, NamedP private void HandleGetStatusRequest(NamedPipeServer.Connection connection) { NamedPipeMessages.GetStatus.Response response = new NamedPipeMessages.GetStatus.Response(); - response.EnlistmentRoot = this.enlistment.EnlistmentRoot; + response.EnlistmentRoot = this.enlistment.WorkingDirectoryRoot; response.LocalCacheRoot = !string.IsNullOrWhiteSpace(this.enlistment.LocalCacheRoot) ? this.enlistment.LocalCacheRoot : this.enlistment.GitObjectsRoot; response.RepoUrl = this.enlistment.RepoUrl; response.CacheServer = this.cacheServer.ToString(); @@ -1082,7 +1082,7 @@ private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache, bool this.tracer.RelatedInfo("Git status cache is not enabled"); } - this.gvfsDatabase = this.CreateOrReportAndExit(() => new GVFSDatabase(this.context.FileSystem, this.context.Enlistment.EnlistmentRoot, new SqliteDatabase()), "Failed to create database connection"); + this.gvfsDatabase = this.CreateOrReportAndExit(() => new GVFSDatabase(this.context.FileSystem, this.context.Enlistment.DotGVFSRoot, new SqliteDatabase()), "Failed to create database connection"); this.fileSystemCallbacks = this.CreateOrReportAndExit( () => { @@ -1193,7 +1193,7 @@ private void ValidateFileSystemSupportsRequiredFeatures() { string warning; string error; - if (!GVFSPlatform.Instance.KernelDriver.IsSupported(this.enlistment.EnlistmentRoot, out warning, out error)) + if (!GVFSPlatform.Instance.KernelDriver.IsSupported(this.enlistment.WorkingDirectoryRoot, out warning, out error)) { this.FailMountAndExit("Error: {0}", error); } diff --git a/GVFS/GVFS.Mount/InProcessMountVerb.cs b/GVFS/GVFS.Mount/InProcessMountVerb.cs index 309d56fbd..16328f60d 100644 --- a/GVFS/GVFS.Mount/InProcessMountVerb.cs +++ b/GVFS/GVFS.Mount/InProcessMountVerb.cs @@ -115,7 +115,7 @@ public void Execute() CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); tracer.WriteStartEvent( - enlistment.EnlistmentRoot, + enlistment.WorkingDirectoryRoot, enlistment.RepoUrl, cacheServer.Url, new EventMetadata diff --git a/GVFS/GVFS.PerfProfiling/ProfilingEnvironment.cs b/GVFS/GVFS.PerfProfiling/ProfilingEnvironment.cs index 4798c85c3..65145acba 100644 --- a/GVFS/GVFS.PerfProfiling/ProfilingEnvironment.cs +++ b/GVFS/GVFS.PerfProfiling/ProfilingEnvironment.cs @@ -76,7 +76,7 @@ private FileSystemCallbacks CreateFileSystemCallbacks() cacheServer, new RetryConfig()); - this.gvfsDatabase = new GVFSDatabase(this.Context.FileSystem, this.Context.Enlistment.EnlistmentRoot, new SqliteDatabase()); + this.gvfsDatabase = new GVFSDatabase(this.Context.FileSystem, this.Context.Enlistment.DotGVFSRoot, new SqliteDatabase()); GVFSGitObjects gitObjects = new GVFSGitObjects(this.Context, objectRequestor); return new FileSystemCallbacks( this.Context, diff --git a/GVFS/GVFS.Platform.Windows/CurrentUser.cs b/GVFS/GVFS.Platform.Windows/CurrentUser.cs index 3fe329ae1..1d1ff151f 100644 --- a/GVFS/GVFS.Platform.Windows/CurrentUser.cs +++ b/GVFS/GVFS.Platform.Windows/CurrentUser.cs @@ -134,7 +134,7 @@ public bool RunAs(string processName, string args) if (CreateEnvironmentBlock(ref environment, duplicate, false)) { STARTUP_INFO info = new STARTUP_INFO(); - info.Length = Marshal.SizeOf(typeof(STARTUP_INFO)); + info.Length = Marshal.SizeOf(); PROCESS_INFORMATION procInfo = new PROCESS_INFORMATION(); if (CreateProcessAsUser( @@ -193,6 +193,55 @@ public bool RunAs(string processName, string args) return false; } + /// + /// Returns session IDs for sessions that have a logged-in user + /// whose token can be queried via WTSQueryUserToken. + /// + /// + /// + /// WTS session states relevant to telemetry pipe attachment: + /// + /// + /// StateMeaning + /// Active + /// User logged in, session connected (console or RDP). + /// Has a valid user token. This is the primary case. + /// + /// Connected + /// Client attached but no user logged in (e.g. the console + /// session showing the Windows login screen on a Cloud PC). + /// No user token — WTSQueryUserToken will fail. + /// + /// Disconnected + /// User logged in but client disconnected (e.g. closed RDP + /// window without logging off). The user's profile, processes, + /// and token are still available. Included so the service can + /// attach telemetry even when no client is actively connected. + /// + /// + /// + /// Session 0 is the services session and never has a user token, + /// even when its state is Disconnected. Excluded by the ID > 0 + /// guard. + /// + /// + public static List GetInteractiveSessionIds(ITracer tracer) + { + List sessionIds = new List(); + List sessions = ListSessions(tracer); + foreach (WTS_SESSION_INFO session in sessions) + { + if (session.SessionID > 0 && + (session.State == ConnectionState.Active || + session.State == ConnectionState.Disconnected)) + { + sessionIds.Add(session.SessionID); + } + } + + return sessionIds; + } + public void Dispose() { if (this.token != IntPtr.Zero) @@ -216,7 +265,10 @@ private static IntPtr GetCurrentUserToken(ITracer tracer, int sessionId) } else { - TraceWin32Error(tracer, string.Format("Unable to query user token from session '{0}'.", sessionId)); + // Warning, not error: sessions without a logged-in user + // (e.g. the console session on a Cloud PC) are expected. + Win32Exception e = new Win32Exception(Marshal.GetLastWin32Error()); + tracer.RelatedWarning("Unable to query user token from session '{0}'. Exception: {1}", sessionId, e.Message); } return IntPtr.Zero; @@ -234,12 +286,12 @@ private static List ListSessions(ITracer tracer) int retval = WTSEnumerateSessions(IntPtr.Zero, 0, 1, ref sessionInfo, ref count); if (retval != 0) { - int dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO)); + int dataSize = Marshal.SizeOf(); long current = sessionInfo.ToInt64(); for (int i = 0; i < count; i++) { - WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO)); + WTS_SESSION_INFO si = Marshal.PtrToStructure((IntPtr)current); current += dataSize; output.Add(si); diff --git a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs index 8977bfa88..f39128e7a 100644 --- a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs +++ b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs @@ -981,6 +981,9 @@ private void GetFileStreamHandlerAsyncHandler( } catch (GetFileStreamException e) { + requestMetadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.GetFileStreamHandlerAsyncHandler)}: GetFileStreamException HResult 0x{e.HResult:X8}"); + this.Context.Tracer.RelatedWarning(requestMetadata, nameof(this.GetFileStreamHandlerAsyncHandler) + "_GetFileStreamException"); + this.TryCompleteCommand(commandId, (HResult)e.HResult); return; } diff --git a/GVFS/GVFS.Service/Configuration.cs b/GVFS/GVFS.Service/Configuration.cs index 0e4ebe18e..e573d5262 100644 --- a/GVFS/GVFS.Service/Configuration.cs +++ b/GVFS/GVFS.Service/Configuration.cs @@ -1,16 +1,25 @@ using GVFS.Common; +using System; using System.IO; namespace GVFS.Service { public class Configuration { + /// + /// Subdirectory name for the junction that points to the active version. + /// When a versioned install layout is present, {app}\Current is a + /// junction pointing to {app}\Versions\{version}. + /// + public const string CurrentVersionDirName = "Current"; + + private static readonly Lazy assemblyPath = + new Lazy(ProcessHelper.GetCurrentProcessLocation); + private static Configuration instance = new Configuration(); - private static string assemblyPath = null; private Configuration() { - this.GVFSLocation = Path.Combine(AssemblyPath, GVFSPlatform.Instance.Constants.GVFSExecutableName); } public static Configuration Instance @@ -21,19 +30,39 @@ public static Configuration Instance } } - public static string AssemblyPath + /// + /// The directory containing GVFS.Service.exe (the install root). + /// + public static string AssemblyPath => assemblyPath.Value; + + /// + /// The directory containing the current version's binaries. + /// With versioned layout this resolves through the Current junction + /// (e.g. {app}\Current{app}\Versions\1.0.X). + /// Falls back to when no Current + /// junction exists (flat/legacy layout). + /// Evaluated on each access so that junction re-targeting is picked up + /// without restarting the service. + /// + public static string CurrentVersionPath { get { - if (assemblyPath == null) + string currentDir = Path.Combine(AssemblyPath, CurrentVersionDirName); + if (Directory.Exists(currentDir)) { - assemblyPath = ProcessHelper.GetCurrentProcessLocation(); + return currentDir; } - return assemblyPath; + // Legacy flat layout — binaries are siblings of the service. + return AssemblyPath; } } - public string GVFSLocation { get; private set; } + /// + /// Full path to the current version's GVFS.exe. Resolved + /// dynamically so that junction re-targeting takes effect immediately. + /// + public string GVFSLocation => Path.Combine(CurrentVersionPath, GVFSPlatform.Instance.Constants.GVFSExecutableName); } } diff --git a/GVFS/GVFS.Service/GVFSService.Windows.cs b/GVFS/GVFS.Service/GVFSService.Windows.cs index c7f125b83..3b60ba104 100644 --- a/GVFS/GVFS.Service/GVFSService.Windows.cs +++ b/GVFS/GVFS.Service/GVFSService.Windows.cs @@ -5,6 +5,7 @@ using GVFS.Platform.Windows; using GVFS.Service.Handlers; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.AccessControl; @@ -46,6 +47,46 @@ public void Run() metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(GVFSService)}_{nameof(this.Run)}", metadata); + // Set up deferred telemetry pipe attachment FIRST, before any + // telemetry-emitting work (particularly PendingUpgradeHandler, + // whose Deferred/Complete events we want to capture). + // + // The service runs as SYSTEM and can't read the user's global + // git config (where gvfs.telemetry-pipe is configured) at + // startup. The DeferredTelemetryAttacher adds a + // BufferingTelemetryListener that captures events in memory, + // then replays them once the real pipe listener attaches. + // + // Three attach paths exist: + // 1. TryAttachTelemetryPipeForAnySessions() below — tries + // all Active/Disconnected sessions immediately. + // 2. OnSessionChange (SessionLogon) — fires when a new user + // logs in after the service is already running. + // 3. StartRetryTimer — periodic retry (10s, 30s, 1m, 5m) + // as a fallback, reads system config only (no user + // config available without a session to impersonate). + string gitBinRoot = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); + if (!string.IsNullOrEmpty(gitBinRoot)) + { + this.telemetryAttacher = new DeferredTelemetryAttacher( + this.tracer, + GVFSConstants.Service.ServiceName, + enlistmentId: null, + mountId: null); + this.telemetryAttacher.StartRetryTimer(gitBinRoot); + + // If a user is already logged in (e.g. service restart + // during active session), try attaching immediately by + // enumerating all interactive sessions. This is needed + // because WTSGetActiveConsoleSessionId only returns the + // physical console session, which on Cloud PCs / DevBoxes + // (RDP-only) has no logged-in user. The actual user is + // in an RDP session that the console-only check misses. + // SessionLogon events also won't fire for sessions that + // were already established before the service started. + this.TryAttachTelemetryPipeForAnySessions(); + } + // Check for a staged upgrade before doing anything else. // If no GVFS.Mount processes are running (typical at boot or after // unmount-all), copy staged files in-place and proceed normally. @@ -77,29 +118,6 @@ public void Run() { this.CheckEnableGitStatusCacheTokenFile(); - // Set up deferred telemetry pipe attachment. The service - // runs as SYSTEM and can't read the user's global git - // config at startup, so the daemon listener can't be - // created in the JsonTracer constructor. - string gitBinRoot = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); - if (!string.IsNullOrEmpty(gitBinRoot)) - { - this.telemetryAttacher = new DeferredTelemetryAttacher( - this.tracer, - GVFSConstants.Service.ServiceName, - enlistmentId: null, - mountId: null); - this.telemetryAttacher.StartRetryTimer(gitBinRoot); - - // If a user is already logged in (e.g. service restart - // during active session), try attaching immediately. - int activeSession = NativeMethods.GetActiveConsoleSessionId(); - if (activeSession > 0) - { - this.TryAttachTelemetryPipeForSession(activeSession); - } - } - using (ITracer activity = this.tracer.StartActivity("EnsurePrjFltHealthy", EventLevel.Informational)) { // Make a best-effort to enable PrjFlt. Continue even if it fails. @@ -475,6 +493,50 @@ private void TryAttachTelemetryPipeForSession(int sessionId) } } + /// + /// Enumerates all interactive sessions (Active and Disconnected) + /// and tries to attach the telemetry pipe using each session's + /// user profile. Stops on the first successful attach. + /// + /// + /// This method exists because the console-only check + /// (WTSGetActiveConsoleSessionId) fails on Cloud PCs and + /// RDP-only machines where the console session is in the Connected + /// state (login screen, no user). Disconnected sessions are also + /// checked because an RDP user who disconnected without logging + /// off still has a valid token and git config. + /// + private void TryAttachTelemetryPipeForAnySessions() + { + if (this.telemetryAttacher == null || this.telemetryAttacher.IsAttached) + { + return; + } + + List sessionIds = CurrentUser.GetInteractiveSessionIds(this.tracer); + if (sessionIds.Count == 0) + { + this.tracer.RelatedInfo("TryAttachTelemetryPipeForAnySessions: No interactive sessions found"); + return; + } + + foreach (int sessionId in sessionIds) + { + this.TryAttachTelemetryPipeForSession(sessionId); + if (this.telemetryAttacher.IsAttached) + { + break; + } + } + + if (!this.telemetryAttacher.IsAttached) + { + this.tracer.RelatedWarning( + "TryAttachTelemetryPipeForAnySessions: Could not attach from any of {0} interactive session(s)", + sessionIds.Count); + } + } + private DirectorySecurity GetServiceDirectorySecurity(string serviceDataRootPath) { DirectorySecurity serviceDataRootSecurity; diff --git a/GVFS/GVFS.Service/PendingUpgradeHandler.cs b/GVFS/GVFS.Service/PendingUpgradeHandler.cs index f891402b1..2b3a56098 100644 --- a/GVFS/GVFS.Service/PendingUpgradeHandler.cs +++ b/GVFS/GVFS.Service/PendingUpgradeHandler.cs @@ -82,16 +82,15 @@ public static bool IsPending() /// /// Returns GVFS.Mount processes whose executable is in the install - /// directory. Processes from dev builds or other installs are excluded - /// so they don't block upgrades of the system install. If a process's - /// path cannot be read (access denied, 32/64-bit mismatch), it is - /// included conservatively. + /// directory (or any versioned subdirectory). Processes from dev builds + /// or other installs are excluded so they don't block upgrades of the + /// system install. If a process's path cannot be read (access denied, + /// 32/64-bit mismatch), it is included conservatively. /// Caller must dispose the returned Process objects. /// public static List GetInstalledMountProcesses(ITracer tracer) { string installDir = Configuration.AssemblyPath; - string expectedPath = Path.Combine(installDir, MountExeName); Process[] allMountProcesses = Process.GetProcessesByName(MountProcessName); List installed = new List(); @@ -101,8 +100,7 @@ public static List GetInstalledMountProcesses(ITracer tracer) try { string processPath = process.MainModule?.FileName; - if (processPath != null && - !PathComparer.Equals(processPath, expectedPath)) + if (processPath != null && !IsInstalledMountPath(installDir, processPath)) { include = false; tracer.RelatedInfo( @@ -326,6 +324,26 @@ private static bool IsMarkerFile(string relativePath) string.Equals(relativePath, Phase1CompleteMarkerFileName, StringComparison.OrdinalIgnoreCase); } + /// + /// Returns true if the given GVFS.Mount.exe path is under the install + /// directory — either in the flat layout ({app}\GVFS.Mount.exe) + /// or in a versioned subdirectory ({app}\Versions\*\GVFS.Mount.exe + /// or {app}\Current\GVFS.Mount.exe). + /// + private static bool IsInstalledMountPath(string installDir, string processPath) + { + // Verify filename is actually GVFS.Mount.exe (not some other exe + // under the install dir). + if (!PathComparer.Equals(Path.GetFileName(processPath), MountExeName)) + { + return false; + } + + // Verify the exe lives under the install directory. + string normalizedInstallDir = installDir.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + return processPath.StartsWith(normalizedInstallDir, StringComparison.OrdinalIgnoreCase); + } + /// /// Moves a file, retrying once after killing any process that has the /// source file locked (e.g. a GVFS process that started mid-upgrade). diff --git a/GVFS/GVFS.UnitTests/Common/Database/GVFSDatabaseTests.cs b/GVFS/GVFS.UnitTests/Common/Database/GVFSDatabaseTests.cs index 4ec88b34c..6617b2329 100644 --- a/GVFS/GVFS.UnitTests/Common/Database/GVFSDatabaseTests.cs +++ b/GVFS/GVFS.UnitTests/Common/Database/GVFSDatabaseTests.cs @@ -113,7 +113,7 @@ private void TestGVFSDatabase(Action testCode, bool throwException Mock mockConnectionFactory = new Mock(MockBehavior.Strict); bool firstConnection = true; - string databasePath = Path.Combine("mock:root", ".mockvfsforgit", "databases", "VFSForGit.sqlite"); + string databasePath = Path.Combine("mock:root", "databases", "VFSForGit.sqlite"); mockConnectionFactory.Setup(x => x.OpenNewConnection(databasePath)).Returns(() => { if (firstConnection) diff --git a/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs b/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs index 2541e56ac..dd072b1e5 100644 --- a/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs +++ b/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs @@ -145,7 +145,7 @@ public void DotGVFSRootIsInWorktreeGitDir() public void EnlistmentRootIsPrimaryRoot() { GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); - enlistment.EnlistmentRoot.ShouldEqual(this.primaryRoot); + enlistment.PrimaryEnlistmentRoot.ShouldEqual(this.primaryRoot); } [TestCase] diff --git a/GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceQueueTests.cs b/GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceQueueTests.cs index c284e3cfc..4897e9976 100644 --- a/GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceQueueTests.cs +++ b/GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceQueueTests.cs @@ -28,13 +28,13 @@ public void GitMaintenanceQueueEnlistmentRootReady() GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context); queue.EnlistmentRootReady().ShouldBeTrue(); - this.fileSystem.Paths.Remove(this.enlistment.EnlistmentRoot); + this.fileSystem.Paths.Remove(this.enlistment.WorkingDirectoryRoot); queue.EnlistmentRootReady().ShouldBeFalse(); this.fileSystem.Paths.Remove(this.enlistment.GitObjectsRoot); queue.EnlistmentRootReady().ShouldBeFalse(); - this.fileSystem.Paths.Add(this.enlistment.EnlistmentRoot); + this.fileSystem.Paths.Add(this.enlistment.WorkingDirectoryRoot); queue.EnlistmentRootReady().ShouldBeFalse(); this.fileSystem.Paths.Add(this.enlistment.GitObjectsRoot); @@ -108,10 +108,10 @@ private void TestSetup() ITracer tracer = new MockTracer(); this.enlistment = new MockGVFSEnlistment(); - // We need to have the EnlistmentRoot and GitObjectsRoot available for jobs to run + // We need to have the WorkingDirectoryRoot and GitObjectsRoot available for jobs to run this.fileSystem = new ReadyFileSystem(new string[] { - this.enlistment.EnlistmentRoot, + this.enlistment.WorkingDirectoryRoot, this.enlistment.GitObjectsRoot }); diff --git a/GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceStepTests.cs b/GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceStepTests.cs index e1f6b134a..d37236fbc 100644 --- a/GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceStepTests.cs +++ b/GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceStepTests.cs @@ -80,7 +80,7 @@ private void TestSetup() { ITracer tracer = new MockTracer(); GVFSEnlistment enlistment = new MockGVFSEnlistment(); - PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, null, null)); + PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.PrimaryEnlistmentRoot, null, null)); this.context = new GVFSContext(tracer, fileSystem, null, enlistment); } diff --git a/GVFS/GVFS.UnitTests/Maintenance/LooseObjectStepTests.cs b/GVFS/GVFS.UnitTests/Maintenance/LooseObjectStepTests.cs index d1d8d4b94..47f699889 100644 --- a/GVFS/GVFS.UnitTests/Maintenance/LooseObjectStepTests.cs +++ b/GVFS/GVFS.UnitTests/Maintenance/LooseObjectStepTests.cs @@ -248,7 +248,7 @@ private void TestSetup(DateTime lastRun) // Add object directory to file System List directories = new List() { gitObjectsRoot }; - PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, directories, null)); + PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.PrimaryEnlistmentRoot, directories, null)); // Create and return Context this.tracer = new MockTracer(); diff --git a/GVFS/GVFS.UnitTests/Maintenance/PackfileMaintenanceStepTests.cs b/GVFS/GVFS.UnitTests/Maintenance/PackfileMaintenanceStepTests.cs index c9e0f8939..811b55e15 100644 --- a/GVFS/GVFS.UnitTests/Maintenance/PackfileMaintenanceStepTests.cs +++ b/GVFS/GVFS.UnitTests/Maintenance/PackfileMaintenanceStepTests.cs @@ -219,7 +219,7 @@ private void TestSetup(DateTime lastRun, bool failOnVerify = false) // Add object directory to file System List directories = new List() { gitObjectsRoot }; - PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, directories, null)); + PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.PrimaryEnlistmentRoot, directories, null)); MockGitRepo repository = new MockGitRepo(this.tracer, enlistment, fileSystem); diff --git a/GVFS/GVFS.UnitTests/Maintenance/PostFetchStepTests.cs b/GVFS/GVFS.UnitTests/Maintenance/PostFetchStepTests.cs index 8f1b35f6b..a35f11a59 100644 --- a/GVFS/GVFS.UnitTests/Maintenance/PostFetchStepTests.cs +++ b/GVFS/GVFS.UnitTests/Maintenance/PostFetchStepTests.cs @@ -91,7 +91,7 @@ private void TestSetup() // Create enlistment using git process GVFSEnlistment enlistment = new MockGVFSEnlistment(this.gitProcess); - PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, null, null)); + PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.PrimaryEnlistmentRoot, null, null)); // Create and return Context this.tracer = new MockTracer(); diff --git a/GVFS/GVFS/CommandLine/CacheVerb.cs b/GVFS/GVFS/CommandLine/CacheVerb.cs index b576d4275..8b6538348 100644 --- a/GVFS/GVFS/CommandLine/CacheVerb.cs +++ b/GVFS/GVFS/CommandLine/CacheVerb.cs @@ -216,7 +216,7 @@ private void GetLocalCachePaths(ITracer tracer, GVFSEnlistment enlistment, out s try { string error; - if (RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot), out error)) + if (RepoMetadata.TryInitialize(tracer, enlistment.DotGVFSRoot, out error)) { if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error)) { diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index e64749d8c..977c5b082 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -162,9 +162,9 @@ public override void Execute() // Create the enlistment root explicitly with CreateDirectoryAccessibleByAuthUsers before calling // AddLogFileEventListener to ensure that elevated and non-elevated users have access to the root. string createDirectoryError; - if (!GVFSPlatform.Instance.FileSystem.TryCreateDirectoryAccessibleByAuthUsers(enlistment.EnlistmentRoot, out createDirectoryError)) + if (!GVFSPlatform.Instance.FileSystem.TryCreateDirectoryAccessibleByAuthUsers(enlistment.PrimaryEnlistmentRoot, out createDirectoryError)) { - this.ReportErrorAndExit($"Failed to create '{enlistment.EnlistmentRoot}': {createDirectoryError}"); + this.ReportErrorAndExit($"Failed to create '{enlistment.PrimaryEnlistmentRoot}': {createDirectoryError}"); } tracer.AddLogFileEventListener( @@ -172,7 +172,7 @@ public override void Execute() EventLevel.Informational, Keywords.Any); tracer.WriteStartEvent( - enlistment.EnlistmentRoot, + enlistment.PrimaryEnlistmentRoot, enlistment.RepoUrl, this.CacheServerUrl, new EventMetadata @@ -214,7 +214,7 @@ public override void Execute() this.Output.WriteLine(" Branch: " + (string.IsNullOrWhiteSpace(this.Branch) ? "Default" : this.Branch)); this.Output.WriteLine(" Cache Server: " + cacheServer); this.Output.WriteLine(" Local Cache: " + resolvedLocalCacheRoot); - this.Output.WriteLine(" Destination: " + enlistment.EnlistmentRoot); + this.Output.WriteLine(" Destination: " + enlistment.PrimaryEnlistmentRoot); RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); @@ -291,7 +291,7 @@ public override void Execute() { UseShellExecute = true, WindowStyle = ProcessWindowStyle.Minimized, - WorkingDirectory = enlistment.EnlistmentRoot + WorkingDirectory = enlistment.PrimaryEnlistmentRoot }); this.Output.WriteLine("\r\nPrefetch of commit graph has been started as a background process. Git operations involving history may be slower until prefetch has completed.\r\n"); } @@ -457,7 +457,7 @@ private Result TryClone( return new Result(error); } - if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(enlistment.EnlistmentRoot, out string fsError)) + if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(enlistment.PrimaryEnlistmentRoot, out string fsError)) { string error = $"FileSystem unsupported: {fsError}"; tracer.RelatedError(error); @@ -754,7 +754,7 @@ private Result CreateClone( // TODO(#1364): Don't call this method on POSIX platforms (or have it no-op on them) private void CreateGitScript(GVFSEnlistment enlistment) { - FileInfo gitCmd = new FileInfo(Path.Combine(enlistment.EnlistmentRoot, "git.cmd")); + FileInfo gitCmd = new FileInfo(Path.Combine(enlistment.PrimaryEnlistmentRoot, "git.cmd")); using (FileStream fs = gitCmd.Create()) using (StreamWriter writer = new StreamWriter(fs)) { diff --git a/GVFS/GVFS/CommandLine/DehydrateVerb.cs b/GVFS/GVFS/CommandLine/DehydrateVerb.cs index 2d7b7f958..28ce30357 100644 --- a/GVFS/GVFS/CommandLine/DehydrateVerb.cs +++ b/GVFS/GVFS/CommandLine/DehydrateVerb.cs @@ -91,7 +91,7 @@ protected override void Execute(GVFSEnlistment enlistment) EventLevel.Informational, Keywords.Any); tracer.WriteStartEvent( - enlistment.EnlistmentRoot, + enlistment.PrimaryEnlistmentRoot, enlistment.RepoUrl, CacheServerResolver.GetUrlFromConfig(enlistment), new EventMetadata @@ -160,7 +160,7 @@ any of your uncommitted changes. in the backup folder, but it will be harder to find them because 'git status' will not work in the backup. -To actually execute the dehydrate, run 'gvfs dehydrate --confirm --full' from {enlistment.EnlistmentRoot}. +To actually execute the dehydrate, run 'gvfs dehydrate --confirm --full' from {enlistment.PrimaryEnlistmentRoot}. "); return; @@ -196,13 +196,13 @@ from a parent of the folders list. if (fullDehydrate && Environment.CurrentDirectory.StartsWith(enlistment.WorkingDirectoryBackingRoot)) { /* If running from /src, the dehydrate would fail because of the handle we are holding on it. */ - this.Output.WriteLine($"Dehydrate --full must be run from {enlistment.EnlistmentRoot}"); + this.Output.WriteLine($"Dehydrate --full must be run from {enlistment.PrimaryEnlistmentRoot}"); return; } bool cleanStatus = this.StatusChecked || this.CheckGitStatus(tracer, enlistment, fullDehydrate); - string backupRoot = Path.GetFullPath(Path.Combine(enlistment.EnlistmentRoot, "dehydrate_backup", DateTime.Now.ToString("yyyyMMdd_HHmmss"))); + string backupRoot = Path.GetFullPath(Path.Combine(enlistment.PrimaryEnlistmentRoot, "dehydrate_backup", DateTime.Now.ToString("yyyyMMdd_HHmmss"))); this.Output.WriteLine(); if (fullDehydrate) @@ -223,7 +223,7 @@ from a parent of the folders list. this.Unmount(tracer); string error; - if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.EnlistmentRoot, out error)) + if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.PrimaryEnlistmentRoot, out error)) { this.ReportErrorAndExit(tracer, error); } diff --git a/GVFS/GVFS/CommandLine/DiagnoseVerb.cs b/GVFS/GVFS/CommandLine/DiagnoseVerb.cs index d1a624db1..928b104df 100644 --- a/GVFS/GVFS/CommandLine/DiagnoseVerb.cs +++ b/GVFS/GVFS/CommandLine/DiagnoseVerb.cs @@ -77,7 +77,7 @@ protected override void Execute(GVFSEnlistment enlistment) this.WriteMessage(enlistment.GitBinPath); this.WriteMessage(string.Empty); - this.WriteMessage("Enlistment root: " + enlistment.EnlistmentRoot); + this.WriteMessage("Enlistment root: " + enlistment.PrimaryEnlistmentRoot); this.WriteMessage("Cache Server: " + CacheServerResolver.GetCacheServerFromConfig(enlistment)); string localCacheRoot; @@ -101,8 +101,8 @@ protected override void Execute(GVFSEnlistment enlistment) this.ShowStatusWhileRunning( () => { - // .gvfs - this.CopyAllFiles(enlistment.EnlistmentRoot, archiveFolderPath, GVFSPlatform.Instance.Constants.DotGVFSRoot, copySubFolders: false); + // .gvfs (top-level files like mount.lock) + this.CopyAllFiles(Path.GetDirectoryName(enlistment.DotGVFSRoot), archiveFolderPath, Path.GetFileName(enlistment.DotGVFSRoot), copySubFolders: false); // driver if (this.FlushKernelDriverLogs()) @@ -274,7 +274,7 @@ private void GetLocalCachePaths(GVFSEnlistment enlistment, out string localCache using (ITracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "DiagnoseVerb")) { string error; - if (RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot), out error)) + if (RepoMetadata.TryInitialize(tracer, enlistment.DotGVFSRoot, out error)) { RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error); RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error); diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index c44608daf..069ac1661 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -96,7 +96,7 @@ public string ServicePipeName protected abstract string VerbName { get; } - public static bool TrySetRequiredGitConfigSettings(Enlistment enlistment) + public static bool TrySetRequiredGitConfigSettings(GVFSEnlistment enlistment) { Dictionary requiredSettings = RequiredGitConfig.GetRequiredSettings(enlistment); @@ -164,7 +164,7 @@ protected ReturnCode Execute( where TVerb : GVFSVerb.ForExistingEnlistment, new() { TVerb verb = new TVerb(); - verb.EnlistmentRootPathParameter = enlistment.EnlistmentRoot; + verb.EnlistmentRootPathParameter = enlistment.WorkingDirectoryRoot; verb.ServiceName = this.ServiceName; verb.Unattended = this.Unattended; @@ -246,7 +246,7 @@ protected bool TryAuthenticateAndQueryGVFSConfig( out error, out _), "Authenticating", - enlistment.EnlistmentRoot); + enlistment.WorkingDirectoryRoot); if (!result && fallbackCacheServer != null && !string.IsNullOrWhiteSpace(fallbackCacheServer.Url)) { @@ -677,7 +677,7 @@ private void CheckFileSystemSupportsRequiredFeatures(ITracer tracer, Enlistment { string warning; string error; - if (!GVFSPlatform.Instance.KernelDriver.IsSupported(enlistment.EnlistmentRoot, out warning, out error)) + if (!GVFSPlatform.Instance.KernelDriver.IsSupported(enlistment.WorkingDirectoryRoot, out warning, out error)) { this.ReportErrorAndExit(tracer, $"Error: {error}"); } @@ -935,7 +935,7 @@ protected void InitializeLocalCacheAndObjectsPaths( CacheServerInfo cacheServer) { string error; - if (!RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot), out error)) + if (!RepoMetadata.TryInitialize(tracer, enlistment.DotGVFSRoot, out error)) { this.ReportErrorAndExit(tracer, "Failed to initialize repo metadata: " + error); } diff --git a/GVFS/GVFS/CommandLine/MountVerb.cs b/GVFS/GVFS/CommandLine/MountVerb.cs index edaf0b85c..37e5f1041 100644 --- a/GVFS/GVFS/CommandLine/MountVerb.cs +++ b/GVFS/GVFS/CommandLine/MountVerb.cs @@ -158,7 +158,7 @@ protected override void Execute(GVFSEnlistment enlistment) EventLevel.Verbose, Keywords.Any); tracer.WriteStartEvent( - enlistment.EnlistmentRoot, + enlistment.PrimaryEnlistmentRoot, enlistment.RepoUrl, cacheServerFromConfig.Url, new EventMetadata @@ -169,7 +169,7 @@ protected override void Execute(GVFSEnlistment enlistment) { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, }); - if (!GVFSPlatform.Instance.KernelDriver.IsReady(tracer, enlistment.EnlistmentRoot, this.Output, out errorMessage)) + if (!GVFSPlatform.Instance.KernelDriver.IsReady(tracer, enlistment.WorkingDirectoryRoot, this.Output, out errorMessage)) { tracer.RelatedEvent( EventLevel.Informational, @@ -181,7 +181,7 @@ protected override void Execute(GVFSEnlistment enlistment) }); if (!this.ShowStatusWhileRunning( - () => { return this.TryEnableAndAttachPrjFltThroughService(enlistment.EnlistmentRoot, out errorMessage); }, + () => { return this.TryEnableAndAttachPrjFltThroughService(enlistment.WorkingDirectoryRoot, out errorMessage); }, $"Attaching ProjFS to volume")) { this.ReportErrorAndExit(tracer, ReturnCode.FilterError, errorMessage); @@ -255,7 +255,7 @@ private bool TryMount(ITracer tracer, GVFSEnlistment enlistment, string mountExe // For worktrees, pass the worktree path so GVFS.Mount.exe creates the right enlistment string mountPath = enlistment.IsWorktree ? enlistment.WorkingDirectoryRoot - : enlistment.EnlistmentRoot; + : enlistment.PrimaryEnlistmentRoot; tracer.RelatedInfo($"{nameof(this.TryMount)}: Launching background process('{mountExecutableLocation}') for {mountPath}"); @@ -277,7 +277,7 @@ private bool TryMount(ITracer tracer, GVFSEnlistment enlistment, string mountExe tracer.RelatedInfo($"{nameof(this.TryMount)}: Waiting for repo to be mounted"); - return GVFSEnlistment.WaitUntilMounted(tracer, enlistment.NamedPipeName, enlistment.EnlistmentRoot, this.Unattended, out errorMessage); + return GVFSEnlistment.WaitUntilMounted(tracer, enlistment.NamedPipeName, enlistment.WorkingDirectoryRoot, this.Unattended, out errorMessage); } private bool RegisterMount(GVFSEnlistment enlistment, out string errorMessage) @@ -290,7 +290,7 @@ private bool RegisterMount(GVFSEnlistment enlistment, out string errorMessage) // listed and unregistered independently of the primary enlistment. request.EnlistmentRoot = enlistment.IsWorktree ? enlistment.WorkingDirectoryRoot - : enlistment.EnlistmentRoot; + : enlistment.PrimaryEnlistmentRoot; request.OwnerSID = GVFSPlatform.Instance.GetCurrentUser(); diff --git a/GVFS/GVFS/CommandLine/PrefetchVerb.cs b/GVFS/GVFS/CommandLine/PrefetchVerb.cs index 1d3d555b4..17483c34c 100644 --- a/GVFS/GVFS/CommandLine/PrefetchVerb.cs +++ b/GVFS/GVFS/CommandLine/PrefetchVerb.cs @@ -138,7 +138,7 @@ protected override void Execute(GVFSEnlistment enlistment) EventLevel.Informational, Keywords.Any); tracer.WriteStartEvent( - enlistment.EnlistmentRoot, + enlistment.PrimaryEnlistmentRoot, enlistment.RepoUrl, cacheServerFromConfig.Url); @@ -216,8 +216,8 @@ protected override void Execute(GVFSEnlistment enlistment) catch (AggregateException aggregateException) { this.Output.WriteLine( - "Cannot prefetch {0}. " + ConsoleHelper.GetGVFSLogMessage(enlistment.EnlistmentRoot), - enlistment.EnlistmentRoot); + "Cannot prefetch {0}. " + ConsoleHelper.GetGVFSLogMessage(enlistment.WorkingDirectoryRoot), + enlistment.WorkingDirectoryRoot); foreach (Exception innerException in aggregateException.Flatten().InnerExceptions) { tracer.RelatedError( @@ -234,8 +234,8 @@ protected override void Execute(GVFSEnlistment enlistment) catch (Exception e) { this.Output.WriteLine( - "Cannot prefetch {0}. " + ConsoleHelper.GetGVFSLogMessage(enlistment.EnlistmentRoot), - enlistment.EnlistmentRoot); + "Cannot prefetch {0}. " + ConsoleHelper.GetGVFSLogMessage(enlistment.WorkingDirectoryRoot), + enlistment.WorkingDirectoryRoot); tracer.RelatedError( new EventMetadata { diff --git a/GVFS/GVFS/CommandLine/RepairVerb.cs b/GVFS/GVFS/CommandLine/RepairVerb.cs index 731af334f..dc0034adc 100644 --- a/GVFS/GVFS/CommandLine/RepairVerb.cs +++ b/GVFS/GVFS/CommandLine/RepairVerb.cs @@ -92,7 +92,7 @@ To actually execute any necessary repair(s), run 'gvfs repair --confirm' } string error; - if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistment.EnlistmentRoot, error: out error)) + if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistment.PrimaryEnlistmentRoot, error: out error)) { this.ReportErrorAndExit(error); } @@ -129,7 +129,7 @@ To actually execute any necessary repair(s), run 'gvfs repair --confirm' EventLevel.Verbose, Keywords.Any); tracer.WriteStartEvent( - enlistment.EnlistmentRoot, + enlistment.PrimaryEnlistmentRoot, enlistment.RepoUrl, "N/A", new EventMetadata @@ -214,7 +214,7 @@ To actually execute any necessary repair(s), run 'gvfs repair --confirm' this.WriteMessage(tracer, "Repair succeeded, but requires some manual steps before remounting."); break; case RepairJob.FixResult.Failure: - this.WriteMessage(tracer, "Repair failed. " + ConsoleHelper.GetGVFSLogMessage(enlistment.EnlistmentRoot)); + this.WriteMessage(tracer, "Repair failed. " + ConsoleHelper.GetGVFSLogMessage(enlistment.PrimaryEnlistmentRoot)); break; } diff --git a/GVFS/GVFS/CommandLine/SparseVerb.cs b/GVFS/GVFS/CommandLine/SparseVerb.cs index bf5150c27..1ea6e5aed 100644 --- a/GVFS/GVFS/CommandLine/SparseVerb.cs +++ b/GVFS/GVFS/CommandLine/SparseVerb.cs @@ -159,7 +159,7 @@ protected override void Execute(GVFSEnlistment enlistment) string.IsNullOrEmpty(this.Set) && string.IsNullOrEmpty(this.File))) { - this.ListSparseFolders(enlistment.EnlistmentRoot); + this.ListSparseFolders(enlistment.DotGVFSRoot); return; } @@ -183,7 +183,7 @@ protected override void Execute(GVFSEnlistment enlistment) HashSet directories; bool needToChangeProjection = false; - using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistment.EnlistmentRoot, new SqliteDatabase())) + using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistment.DotGVFSRoot, new SqliteDatabase())) { SparseTable sparseTable = new SparseTable(database); directories = sparseTable.GetAll(); @@ -555,9 +555,9 @@ private void CheckOptions() } } - private void ListSparseFolders(string enlistmentRoot) + private void ListSparseFolders(string dotGVFSRoot) { - using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistmentRoot, new SqliteDatabase())) + using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), dotGVFSRoot, new SqliteDatabase())) { SparseTable sparseTable = new SparseTable(database); HashSet directories = sparseTable.GetAll(); diff --git a/GVFS/GVFS/RepairJobs/GitConfigRepairJob.cs b/GVFS/GVFS/RepairJobs/GitConfigRepairJob.cs index c1f8c5ab9..2903bcf72 100644 --- a/GVFS/GVFS/RepairJobs/GitConfigRepairJob.cs +++ b/GVFS/GVFS/RepairJobs/GitConfigRepairJob.cs @@ -50,7 +50,7 @@ public override IssueType HasIssue(List messages) try { GVFSEnlistment enlistment = GVFSEnlistment.CreateFromDirectory( - this.Enlistment.EnlistmentRoot, + this.Enlistment.PrimaryEnlistmentRoot, this.Enlistment.GitBinPath, authentication: null); diff --git a/Readme.md b/Readme.md index 6267980ba..c72ae98d5 100644 --- a/Readme.md +++ b/Readme.md @@ -1,12 +1,5 @@ # VFS for Git -**Notice:** With the release of VFS for Git 2.32, VFS for Git is in maintenance mode. Only required updates as a reaction to critical security vulnerabilities will prompt a release. - -|Branch|Unit Tests|Functional Tests|Large Repo Perf|Large Repo Build| -|:--:|:--:|:--:|:--:|:--:| -|**master**|[![Build status](https://dev.azure.com/gvfs/ci/_apis/build/status/CI%20-%20Windows?branchName=master)](https://dev.azure.com/gvfs/ci/_build/latest?definitionId=7&branchName=master)|[![Build status](https://dev.azure.com/gvfs/ci/_apis/build/status/CI%20-%20Windows%20-%20Full%20Functional%20Tests?branchName=master)](https://dev.azure.com/gvfs/ci/_build/latest?definitionId=6&branchName=master)|[![Build status](https://dev.azure.com/mseng/AzureDevOps/_apis/build/status/GVFS/GitHub%20VFSForGit%20Large%20Repo%20Perf%20Tests?branchName=master)](https://dev.azure.com/mseng/AzureDevOps/_build/latest?definitionId=7179&branchName=master)|[![Build status](https://dev.azure.com/mseng/AzureDevOps/_apis/build/status/GVFS/GitHub%20VFSForGit%20Large%20Repo%20Build?branchName=master)](https://dev.azure.com/mseng/AzureDevOps/_build/latest?definitionId=7180&branchName=master)| -|**shipped**|[![Build status](https://dev.azure.com/gvfs/ci/_apis/build/status/CI%20-%20Windows?branchName=releases%2Fshipped)](https://dev.azure.com/gvfs/ci/_build/latest?definitionId=7&branchName=releases%2Fshipped)|[![Build status](https://dev.azure.com/gvfs/ci/_apis/build/status/CI%20-%20Windows%20-%20Full%20Functional%20Tests?branchName=releases%2Fshipped)](https://dev.azure.com/gvfs/ci/_build/latest?definitionId=6&branchName=releases%2Fshipped)|[![Build status](https://dev.azure.com/mseng/AzureDevOps/_apis/build/status/GVFS/GitHub%20VFSForGit%20Large%20Repo%20Perf%20Tests?branchName=releases%2Fshipped)](https://dev.azure.com/mseng/AzureDevOps/_build/latest?definitionId=7179&branchName=releases%2Fshipped)|[![Build status](https://dev.azure.com/mseng/AzureDevOps/_apis/build/status/GVFS/GitHub%20VFSForGit%20Large%20Repo%20Build?branchName=releases%2Fshipped)](https://dev.azure.com/mseng/AzureDevOps/_build/latest?definitionId=7180&branchName=releases%2Fshipped)| - ## What is VFS for Git? VFS stands for Virtual File System. VFS for Git virtualizes the file system @@ -46,22 +39,22 @@ If you'd like to build your own VFS for Git Windows installer: * Include the following workloads: * .NET desktop development * Desktop development with C++ - * .NET Core cross-platform development * Include the following additional components: - * .NET Core runtime * Windows 10 or 11 SDK (10.0+) -* Install the .NET Core 8 SDK (https://www.microsoft.com/net/download/dotnet-core/8) -* Install [`nuget.exe`](https://www.nuget.org/downloads) +* Install the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +* Install [`vcpkg`](https://learn.microsoft.com/vcpkg/get-started/get-started) (or use the copy bundled with Visual Studio's C++ workload) * Create a folder to clone into, e.g. `C:\Repos\VFSForGit` * Clone this repo into the `src` subfolder, e.g. `C:\Repos\VFSForGit\src` -* Run `\src\Scripts\BuildGVFSForWindows.bat` +* Run `src\scripts\Build.bat` (defaults to a Debug build) * You can also build in Visual Studio by opening `src\GVFS.sln` (do not upgrade any projects) and building. However, the very first build will fail, and the second and subsequent builds will succeed. This is because the build requires a prebuild code generation step. For details, see the build script in the previous step. Visual Studio 2022 will [automatically prompt you to install these dependencies](https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/) when you open the solution. The .vsconfig file that is present in the root of the repository specifies all required components. -The installer can now be found at `C:\Repos\VFSForGit\BuildOutput\GVFS.Installer.Windows\bin\x64\[Debug|Release]\SetupGVFS..exe` +The installer can now be found at `C:\Repos\VFSForGit\out\GVFS.Installers\bin\[Debug|Release]\win-x64\SetupGVFS..exe` + +AI coding assistants working in this repo: see [AGENTS.md](AGENTS.md) for project-specific build/test guidance. ## Trying out VFS for Git