Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions GVFS/GVFS.Common/Enlistment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ protected Enlistment(
public string WorkingDirectoryRoot { get; }
public string WorkingDirectoryBackingRoot { get; }

/// <summary>
/// Path to the git directory that holds the repository metadata GVFS reads or writes
/// directly. The default is "&lt;WorkingDirectoryBackingRoot&gt;\.git", which is a file in a
Comment thread
tyrielv marked this conversation as resolved.
Resolved
/// linked worktree. Enlistments that support linked worktrees must set it to the
/// shared (common) git directory during construction.
/// </summary>
Comment thread
tyrielv marked this conversation as resolved.
Resolved
public string DotGitRoot { get; protected set; }
public abstract string GitObjectsRoot { get; protected set; }
public abstract string LocalObjectsRoot { get; protected set; }
Expand Down
20 changes: 16 additions & 4 deletions GVFS/GVFS.Common/Git/GitProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -101,7 +101,18 @@ 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 be
// the path of the worktree's .git file, not the gitdir: target stored inside it.
// Given that file, git reads the gitdir: pointer itself. It resolves files in the
// per-worktree git directory by default (for example HEAD, index, ORIG_HEAD,
// logs/HEAD, MERGE_HEAD, CHERRY_PICK_HEAD, BISECT_START, FETCH_HEAD, rebase-merge/,
// refs/bisect, refs/worktree, refs/rewritten). Only an explicit shared set goes
// through commondir to the shared git directory (config, objects, packed-refs,
// refs/heads, hooks).
// Passing Enlistment.DotGitRoot would bind commands to the main worktree instead.
Comment thread
tyrielv marked this conversation as resolved.
Resolved
// Built from an Enlistment, workingDirectoryRoot is WorkingDirectoryBackingRoot,
// the same root that the base Enlistment uses to derive DotGitRoot.
this.gitDirPath = Path.Combine(this.workingDirectoryRoot, GVFSConstants.DotGit.Root);
Comment thread
tyrielv marked this conversation as resolved.
Resolved
}
}

Expand Down Expand Up @@ -1204,7 +1215,8 @@ private Result InvokeGitInWorkingDirectoryRoot(
}

/// <summary>
/// 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
/// </summary>
private Result InvokeGitAgainstDotGitFolder(string command, bool usePreCommandHook = true)
Expand All @@ -1228,7 +1240,7 @@ private Result InvokeGitAgainstDotGitFolder(
return this.InvokeGitImpl(
command,
workingDirectory: Environment.SystemDirectory,
dotGitDirectory: this.dotGitRoot,
dotGitDirectory: this.gitDirPath,
useReadObjectHook: false,
writeStdIn: writeStdIn,
parseStdOutLine: parseStdOutLine,
Expand Down
22 changes: 22 additions & 0 deletions GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System.IO;

Expand Down Expand Up @@ -91,6 +93,26 @@ public void DotGitRootPointsToSharedGitDir()
enlistment.DotGitRoot.ShouldEqual(this.sharedGitDir);
}

[TestCase]
public void GitProcessPassesWorktreeGitFileAsGitDir()
{
// The --git-dir that reaches git must be the worktree's .git file, not the gitdir:
// target inside it and not DotGitRoot. Git resolves per-worktree state through the file.
GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
MockGitProcess gitProcess = new MockGitProcess(enlistment);
gitProcess.SetExpectedCommandResult(
"rev-parse HEAD",
() => new GitProcess.Result("abc123", string.Empty, GitProcess.Result.SuccessCode));

gitProcess.RevParse("HEAD");

gitProcess.InvocationsRun.Count.ShouldEqual(1);
string gitDir = gitProcess.InvocationsRun[0].DotGitDirectory;
gitDir.ShouldEqual(Path.Combine(this.worktreePath, ".git"));
gitDir.ShouldNotEqual(enlistment.DotGitRoot);
gitDir.ShouldNotEqual(this.worktreeGitDir);
}
Comment thread
tyrielv marked this conversation as resolved.
Resolved

[TestCase]
public void WorkingDirectoryRootIsWorktreePath()
{
Expand Down
34 changes: 34 additions & 0 deletions GVFS/GVFS.UnitTests/Git/GitProcessTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using GVFS.Common.Git;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System.Diagnostics;
using System.IO;

namespace GVFS.UnitTests.Git
{
Expand Down Expand Up @@ -102,6 +104,38 @@ public void TryKillRunningProcess_NeverRan()
error.ShouldBeNull();
}

[TestCase]
public void GitDirectoryMatchesDotGitRootForRegularEnlistment()
{
// Regular enlistments have no separate worktree .git file,
// so the --git-dir that reaches git matches Enlistment.DotGitRoot.
MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
MockGitProcess process = new MockGitProcess(enlistment);
process.SetExpectedCommandResult(
"rev-parse HEAD",
() => new GitProcess.Result("abc123", string.Empty, GitProcess.Result.SuccessCode));

process.RevParse("HEAD");

process.InvocationsRun.Count.ShouldEqual(1);
process.InvocationsRun[0].DotGitDirectory.ShouldEqual(enlistment.DotGitRoot);
}

[TestCase]
public void NoGitDirectoryIsPassedWhenWorkingDirectoryRootIsNull()
{
// Instances without an enlistment root cannot supply a --git-dir.
MockGitProcess process = new MockGitProcess(Path.Combine("mock:", "git"), workingDirectoryRoot: null);
process.SetExpectedCommandResult(
"rev-parse HEAD",
() => new GitProcess.Result("abc123", string.Empty, GitProcess.Result.SuccessCode));

process.RevParse("HEAD");

process.InvocationsRun.Count.ShouldEqual(1);
process.InvocationsRun[0].DotGitDirectory.ShouldBeNull();
}

[TestCase]
public void ResultHasNoErrors()
{
Expand Down
42 changes: 26 additions & 16 deletions GVFS/GVFS.UnitTests/Mock/Git/MockGitProcess.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using GVFS.Common.Git;
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
Expand All @@ -15,28 +16,33 @@ public class MockGitProcess : GitProcess
private List<CommandInfo> expectedCommandInfos = new List<CommandInfo>();

public MockGitProcess()
: base(new MockGVFSEnlistment())
: this(new MockGVFSEnlistment())
{
this.CommandsRun = new List<string>();
this.InvocationsRun = new List<GitInvocation>();
this.StoredCredentials = new Dictionary<string, Credential>(StringComparer.OrdinalIgnoreCase);
this.CredentialApprovals = new Dictionary<string, List<Credential>>();
this.CredentialRejections = new Dictionary<string, List<Credential>>();
}

public List<string> CommandsRun { get; }
public MockGitProcess(Enlistment enlistment)
: base(enlistment)
{
}

public MockGitProcess(string gitBinPath, string workingDirectoryRoot)
: base(gitBinPath, workingDirectoryRoot)
{
}

public List<string> CommandsRun { get; } = new List<string>();

/// <summary>
/// Records every call into <see cref="InvokeGitImpl"/> in order, with the mayRequireAuth and
/// hasVisibleConsoleWindow values passed to it. Recording each invocation (rather than keeping only
/// Records every call into <see cref="InvokeGitImpl"/> in order, with the mayRequireAuth,
/// hasVisibleConsoleWindow and --git-dir values passed to it. Recording each invocation (rather than keeping only
/// the last value per command) keeps the wiring assertions honest if a command is ever run more than
/// once with different flags.
/// </summary>
public List<GitInvocation> InvocationsRun { get; }
public List<GitInvocation> InvocationsRun { get; } = new List<GitInvocation>();
public bool ShouldFail { get; set; }
public Dictionary<string, Credential> StoredCredentials { get; }
public Dictionary<string, List<Credential>> CredentialApprovals { get; }
public Dictionary<string, List<Credential>> CredentialRejections { get; }
public Dictionary<string, Credential> StoredCredentials { get; } = new Dictionary<string, Credential>(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, List<Credential>> CredentialApprovals { get; } = new Dictionary<string, List<Credential>>();
public Dictionary<string, List<Credential>> CredentialRejections { get; } = new Dictionary<string, List<Credential>>();

/// <summary>
/// Returns the mayRequireAuth value the given command was invoked with, asserting the command ran and
Expand Down Expand Up @@ -115,7 +121,7 @@ protected override Result InvokeGitImpl(
bool? hasVisibleConsoleWindow = null)
{
this.CommandsRun.Add(command);
this.InvocationsRun.Add(new GitInvocation(command, mayRequireAuth, hasVisibleConsoleWindow));
this.InvocationsRun.Add(new GitInvocation(command, dotGitDirectory, mayRequireAuth, hasVisibleConsoleWindow));

if (this.ShouldFail)
{
Expand Down Expand Up @@ -156,14 +162,18 @@ protected override Result InvokeGitImpl(

public class GitInvocation
{
public GitInvocation(string command, bool mayRequireAuth, bool? hasVisibleConsoleWindow)
public GitInvocation(string command, string dotGitDirectory, bool mayRequireAuth, bool? hasVisibleConsoleWindow)
{
this.Command = command;
this.DotGitDirectory = dotGitDirectory;
this.MayRequireAuth = mayRequireAuth;
this.HasVisibleConsoleWindow = hasVisibleConsoleWindow;
}

public string Command { get; }

/// <summary>The --git-dir value passed to git, or null when none would be passed.</summary>
public string DotGitDirectory { get; }
public bool MayRequireAuth { get; }
public bool? HasVisibleConsoleWindow { get; }
}
Expand Down
Loading