diff --git a/GVFS/GVFS.Common/Enlistment.cs b/GVFS/GVFS.Common/Enlistment.cs index a9208b0ed..503a29b61 100644 --- a/GVFS/GVFS.Common/Enlistment.cs +++ b/GVFS/GVFS.Common/Enlistment.cs @@ -62,6 +62,10 @@ protected Enlistment( public string WorkingDirectoryRoot { get; } public string WorkingDirectoryBackingRoot { get; } + /// + /// Path to the shared repository metadata directory that GVFS reads or writes directly. + /// In a linked worktree this is the common git directory, not the worktree's .git file. + /// public string DotGitRoot { get; protected set; } public abstract string GitObjectsRoot { get; protected set; } public abstract string LocalObjectsRoot { get; protected set; } diff --git a/GVFS/GVFS.Common/GVFSEnlistment.cs b/GVFS/GVFS.Common/GVFSEnlistment.cs index 7cd441aad..77188b6e5 100644 --- a/GVFS/GVFS.Common/GVFSEnlistment.cs +++ b/GVFS/GVFS.Common/GVFSEnlistment.cs @@ -64,6 +64,9 @@ 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. + // + // DotGitRoot is shared git state. Per-worktree paths must come from + // Worktree.WorktreeGitDir. 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 43a40ced0..2ac36ae75 100644 --- a/GVFS/GVFS.Common/Git/GitProcess.cs +++ b/GVFS/GVFS.Common/Git/GitProcess.cs @@ -55,7 +55,7 @@ public class GitProcess : ICredentialStore private string gitBinPath; private string workingDirectoryRoot; - private string dotGitRoot; + private string gitDirPath; private Process executingProcess; private bool stopping; @@ -101,10 +101,25 @@ public GitProcess(string gitBinPath, string workingDirectoryRoot) if (this.workingDirectoryRoot != null) { - this.dotGitRoot = Path.Combine(this.workingDirectoryRoot, GVFSConstants.DotGit.Root); + // Deliberately not Enlistment.DotGitRoot. In a linked worktree, --git-dir must + // point at the worktree's .git file, not the gitdir target named inside it. Git + // follows the gitdir: pointer, resolves per-worktree HEAD/index/refs, and then + // follows commondir to shared state. + // Passing Enlistment.DotGitRoot would bind commands to the main worktree instead. + this.gitDirPath = Path.Combine(this.workingDirectoryRoot, GVFSConstants.DotGit.Root); } } + /// + /// Path passed as --git-dir to InvokeGitAgainstDotGitFolder. + /// In a linked worktree this is the worktree's .git file, not the gitdir target + /// named inside it or . + /// + internal string GitDirPath + { + get { return this.gitDirPath; } + } + public static string ExpireTimeDateString { get @@ -1152,7 +1167,8 @@ private Result InvokeGitInWorkingDirectoryRoot( } /// - /// Invokes git.exe against an enlistment's .git folder. + /// Invokes git.exe against this process's --git-dir path. In a linked worktree this is + /// the worktree's .git file, not the gitdir target named inside it. /// This method should be used only with git-commands that ignore the working directory /// private Result InvokeGitAgainstDotGitFolder(string command, bool usePreCommandHook = true) @@ -1174,7 +1190,7 @@ private Result InvokeGitAgainstDotGitFolder( return this.InvokeGitImpl( command, workingDirectory: Environment.SystemDirectory, - dotGitDirectory: this.dotGitRoot, + dotGitDirectory: this.gitDirPath, useReadObjectHook: false, writeStdIn: writeStdIn, parseStdOutLine: parseStdOutLine, diff --git a/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs b/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs index dd072b1e5..4351189d2 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,17 @@ public void DotGitRootPointsToSharedGitDir() enlistment.DotGitRoot.ShouldEqual(this.sharedGitDir); } + [TestCase] + public void GitProcessUsesWorktreeGitFileNotSharedGitDir() + { + // GitProcess passes the worktree's .git file as --git-dir. It does not pre-resolve + // the gitdir target named inside that file. This pins the string contract. + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GitProcess gitProcess = new GitProcess(enlistment); + + gitProcess.GitDirPath.ShouldEqual(Path.Combine(this.worktreePath, ".git")); + } + [TestCase] public void WorkingDirectoryRootIsWorktreePath() { @@ -155,4 +167,4 @@ public void RepoUrlIsReadFromSharedConfig() enlistment.RepoUrl.ShouldEqual("https://mock/repo"); } } -} +} diff --git a/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs b/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs index 8182f8dfb..6c9258519 100644 --- a/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs +++ b/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs @@ -102,6 +102,27 @@ public void TryKillRunningProcess_NeverRan() error.ShouldBeNull(); } + [TestCase] + public void GitDirPathMatchesDotGitRootForRegularEnlistment() + { + // Regular enlistments do not have a separate worktree .git file, + // so GitProcess and Enlistment use the same .git directory path. + MockGVFSEnlistment enlistment = new MockGVFSEnlistment(); + GitProcess process = new GitProcess(enlistment); + + process.GitDirPath.ShouldEqual(enlistment.DotGitRoot); + } + + [TestCase] + public void GitDirPathIsNullWhenWorkingDirectoryRootIsNull() + { + // Some callers only need global Git operations. Those instances have no enlistment + // root and cannot pass a --git-dir path. + GitProcess process = new GitProcess("git.exe", workingDirectoryRoot: null); + + process.GitDirPath.ShouldBeNull(); + } + [TestCase] public void ResultHasNoErrors() {