diff --git a/GVFS/GVFS.Common/GVFSEnlistment.cs b/GVFS/GVFS.Common/GVFSEnlistment.cs index 7cd441aad..bee8d0dc1 100644 --- a/GVFS/GVFS.Common/GVFSEnlistment.cs +++ b/GVFS/GVFS.Common/GVFSEnlistment.cs @@ -64,6 +64,13 @@ private GVFSEnlistment(string enlistmentRoot, string gitBinPath, GitAuthenticati // Override DotGitRoot to point to the shared .git directory. // The base constructor sets it to WorkingDirectoryBackingRoot/.git // which is a file (not directory) in worktrees. + // + // NOTE: DotGitRoot is the SHARED git directory, and is used for shared state that GVFS + // reads or writes directly on disk (objects, objects/info/alternates, hooks). It is + // deliberately NOT what GitProcess passes as --git-dir; GitProcess uses the worktree's own + // ".git" file so git resolves per-worktree HEAD/index/refs as well as the shared state. + // See GitProcess's constructor. Per-worktree paths that GVFS touches directly must come + // from Worktree.WorktreeGitDir (see GitIndexPath), never from DotGitRoot. this.DotGitRoot = worktreeInfo.SharedGitDir; this.DotGVFSRoot = Path.Combine(worktreeInfo.WorktreeGitDir, GVFSPlatform.Instance.Constants.DotGVFSRoot); diff --git a/GVFS/GVFS.Common/Git/GitProcess.cs b/GVFS/GVFS.Common/Git/GitProcess.cs index 03cc27b41..8e440843b 100644 --- a/GVFS/GVFS.Common/Git/GitProcess.cs +++ b/GVFS/GVFS.Common/Git/GitProcess.cs @@ -101,10 +101,36 @@ public GitProcess(string gitBinPath, string workingDirectoryRoot) if (this.workingDirectoryRoot != null) { + // This is deliberately NOT Enlistment.DotGitRoot. In a linked worktree those two + // values differ, and each is correct for its own purpose: + // + // Enlistment.DotGitRoot -> the SHARED .git directory of the main repo. GVFS uses it + // for shared state it manages directly on disk (objects, + // objects/info/alternates, hooks). + // this.dotGitRoot -> "\.git", which in a linked worktree + // is a FILE containing "gitdir: /.git/worktrees/". + // + // We pass this.dotGitRoot as --git-dir. Git follows the gitdir: pointer, so it resolves + // BOTH the per-worktree state (HEAD, index, per-worktree refs, reflog) and, via the + // commondir file, the shared state (config, objects, packed-refs). Passing + // Enlistment.DotGitRoot instead would silently bind every command to the MAIN worktree's + // HEAD and index -- e.g. "rev-parse HEAD" and "name-rev --name-only HEAD" would report the + // main worktree's branch and commit while running inside a linked worktree. this.dotGitRoot = Path.Combine(this.workingDirectoryRoot, GVFSConstants.DotGit.Root); } } + /// + /// The path passed as --git-dir to every InvokeGitAgainstDotGitFolder call. + /// In a linked worktree this is the worktree's own .git file, NOT + /// (the shared .git directory). See the constructor + /// for why the two intentionally differ. + /// + public string GitDirectoryPath + { + get { return this.dotGitRoot; } + } + public static string ExpireTimeDateString { get diff --git a/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs b/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs index dd072b1e5..6504dee1c 100644 --- a/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs +++ b/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs @@ -1,4 +1,5 @@ using GVFS.Common; +using GVFS.Common.Git; using GVFS.Tests.Should; using NUnit.Framework; using System.IO; @@ -91,6 +92,31 @@ public void DotGitRootPointsToSharedGitDir() enlistment.DotGitRoot.ShouldEqual(this.sharedGitDir); } + [TestCase] + public void GitProcessUsesWorktreeGitFileNotSharedGitDir() + { + // GitProcess passes its own git dir as --git-dir. For a worktree that must be the + // worktree's ".git" file, which git resolves to both the per-worktree state + // (HEAD, index, refs) and, through commondir, the shared state (config, objects). + // Using DotGitRoot (the shared .git directory) would bind every command to the + // MAIN worktree's HEAD and index instead. + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GitProcess gitProcess = new GitProcess(enlistment); + + gitProcess.GitDirectoryPath.ShouldEqual(Path.Combine(this.worktreePath, ".git")); + gitProcess.GitDirectoryPath.ShouldNotEqual(enlistment.DotGitRoot); + } + + [TestCase] + public void WorkingDirectoryBackingRootIsWorktreePath() + { + // GitProcess derives its --git-dir from WorkingDirectoryBackingRoot. If the worktree + // constructor ever redirected this to the primary enlistment, GitProcess would silently + // start operating on the main worktree. + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + enlistment.WorkingDirectoryBackingRoot.ShouldEqual(this.worktreePath); + } + [TestCase] public void WorkingDirectoryRootIsWorktreePath() {