Skip to content

Commit

Permalink
Fix discovery of working directory for worktrees (#734)
Browse files Browse the repository at this point in the history
SourceLink used gitdir to determine the directory (and incorrectly interpreting the content) but turns out git doesn't use it at all for this purpose.
  • Loading branch information
tmat committed Aug 12, 2021
1 parent 5e2c4f1 commit 48e002e
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 43 deletions.
107 changes: 106 additions & 1 deletion src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs
Expand Up @@ -95,19 +95,33 @@ public void TryFindRepository_Worktree_Realistic()
Assert.Equal(mainGitDir.Path, location.CommonDirectory);
Assert.Null(location.WorkingDirectory);

var repository = GitRepository.OpenRepository(location, GitEnvironment.Empty);
Assert.Equal(location.GitDirectory, repository.GitDirectory);
Assert.Equal(location.CommonDirectory, repository.CommonDirectory);
Assert.Null(repository.WorkingDirectory);

// start under worktree directory:
Assert.True(GitRepository.TryFindRepository(worktreeSubDir.Path, out location));

Assert.Equal(worktreeGitDir.Path, location.GitDirectory);
Assert.Equal(mainGitDir.Path, location.CommonDirectory);
Assert.Equal(worktreeDir.Path, location.WorkingDirectory);

repository = GitRepository.OpenRepository(location, GitEnvironment.Empty);
Assert.Equal(location.GitDirectory, repository.GitDirectory);
Assert.Equal(location.WorkingDirectory, repository.WorkingDirectory);
Assert.Equal(location.CommonDirectory, repository.CommonDirectory);

// start under worktree git directory (git config works from this dir, but git status requires work dir):
Assert.True(GitRepository.TryFindRepository(worktreeGitSubDir.Path, out location));

Assert.Equal(worktreeGitDir.Path, location.GitDirectory);
Assert.Equal(mainGitDir.Path, location.CommonDirectory);
Assert.Null(location.WorkingDirectory);

repository = GitRepository.OpenRepository(location, GitEnvironment.Empty);
Assert.Equal(location.GitDirectory, repository.GitDirectory);
Assert.Equal(location.CommonDirectory, repository.CommonDirectory);
Assert.Null(repository.WorkingDirectory);
}

[Fact]
Expand Down Expand Up @@ -170,6 +184,31 @@ public void OpenRepository()
Assert.Equal("0000000000000000000000000000000000000000", repository.GetHeadCommitSha());
}

[Fact]
public void OpenRepository_WorkingDirectorySpecifiedInConfig()
{
using var temp = new TempRoot();

var homeDir = temp.CreateDirectory();

var workingDir = temp.CreateDirectory();
var workingDir2 = temp.CreateDirectory();
var gitDir = workingDir.CreateDirectory(".git");

gitDir.CreateFile("HEAD");
gitDir.CreateFile("config").WriteAllText("[core]worktree = " + workingDir2.Path.Replace(@"\", @"\\"));

Assert.True(GitRepository.TryFindRepository(gitDir.Path, out var location));
Assert.Equal(gitDir.Path, location.CommonDirectory);
Assert.Equal(gitDir.Path, location.GitDirectory);
Assert.Null(location.WorkingDirectory);

var repository = GitRepository.OpenRepository(location, GitEnvironment.Empty);
Assert.Equal(gitDir.Path, repository.CommonDirectory);
Assert.Equal(gitDir.Path, repository.GitDirectory);
Assert.Equal(workingDir2.Path, repository.WorkingDirectory);
}

[Fact]
public void OpenRepository_VersionNotSupported()
{
Expand All @@ -191,6 +230,72 @@ public void OpenRepository_VersionNotSupported()
Assert.Throws<NotSupportedException>(() => GitRepository.OpenRepository(src.Path, new GitEnvironment(homeDir.Path)));
}

[Fact]
public void OpenRepository_Worktree_GitdirFileMissing()
{
using var temp = new TempRoot();

var mainWorkingDir = temp.CreateDirectory();
var mainGitDir = mainWorkingDir.CreateDirectory(".git");
mainGitDir.CreateFile("HEAD");

var worktreesDir = mainGitDir.CreateDirectory("worktrees");
var worktreeGitDir = worktreesDir.CreateDirectory("myworktree");
var worktreeDir = temp.CreateDirectory();
var worktreeGitFile = worktreeDir.CreateFile(".git").WriteAllText("gitdir: " + worktreeGitDir + " \r\n\t\v");

worktreeGitDir.CreateFile("HEAD");
worktreeGitDir.CreateFile("commondir").WriteAllText("../..\n");
// gitdir file that links back to the worktree working directory is missing from worktreeGitDir

Assert.True(GitRepository.TryFindRepository(worktreeDir.Path, out var location));
Assert.Equal(worktreeGitDir.Path, location.GitDirectory);
Assert.Equal(mainGitDir.Path, location.CommonDirectory);
Assert.Equal(worktreeDir.Path, location.WorkingDirectory);

var repository = GitRepository.OpenRepository(location, GitEnvironment.Empty);
Assert.Equal(repository.GitDirectory, location.GitDirectory);
Assert.Equal(repository.CommonDirectory, location.CommonDirectory);
Assert.Equal(repository.WorkingDirectory, location.WorkingDirectory);
}

/// <summary>
/// The directory in gitdir file is ignored for the purposes of determining repository working directory.
/// </summary>
[Fact]
public void OpenRepository_Worktree_GitdirFileDifferentPath()
{
using var temp = new TempRoot();

var mainWorkingDir = temp.CreateDirectory();
var mainGitDir = mainWorkingDir.CreateDirectory(".git");
mainGitDir.CreateFile("HEAD");

var worktreesDir = mainGitDir.CreateDirectory("worktrees");
var worktreeGitDir = worktreesDir.CreateDirectory("myworktree");
var worktreeDir = temp.CreateDirectory();
var worktreeGitFile = worktreeDir.CreateFile(".git").WriteAllText("gitdir: " + worktreeGitDir + " \r\n\t\v");

var worktreeDir2 = temp.CreateDirectory();
var worktreeGitFile2 = worktreeDir2.CreateFile(".git").WriteAllText("gitdir: " + worktreeGitDir + " \r\n\t\v");

worktreeGitDir.CreateFile("HEAD");
worktreeGitDir.CreateFile("commondir").WriteAllText("../..\n");
worktreeGitDir.CreateFile("gitdir").WriteAllText(worktreeGitFile2.Path + " \r\n\t\v");

Assert.True(GitRepository.TryFindRepository(worktreeDir.Path, out var location));
Assert.Equal(worktreeGitDir.Path, location.GitDirectory);
Assert.Equal(mainGitDir.Path, location.CommonDirectory);
Assert.Equal(worktreeDir.Path, location.WorkingDirectory);

var repository = GitRepository.OpenRepository(location, GitEnvironment.Empty);
Assert.Equal(repository.GitDirectory, location.GitDirectory);
Assert.Equal(repository.CommonDirectory, location.CommonDirectory);

// actual working dir is not affected:
Assert.Equal(worktreeDir.Path, location.WorkingDirectory);
}

[Fact]
public void Submodules()
{
Expand Down
44 changes: 3 additions & 41 deletions src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs
Expand Up @@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
Expand All @@ -19,7 +18,6 @@ internal sealed class GitRepository
private const string CommonDirFileName = "commondir";
private const string GitDirName = ".git";
private const string GitDirPrefix = "gitdir: ";
private const string GitDirFileName = "gitdir";

// See https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD
internal const string GitHeadFileName = "HEAD";
Expand Down Expand Up @@ -131,46 +129,10 @@ public static GitRepository OpenRepository(GitRepositoryLocation location, GitEn

private static string? GetWorkingDirectory(GitConfig config, GitRepositoryLocation location)
{
// Working trees cannot have the same common directory and git directory.
// 'gitdir' file in a git directory indicates a working tree.

var gitdirFilePath = Path.Combine(location.GitDirectory, GitDirFileName);

var isLinkedWorkingTree = PathUtils.ToPosixDirectoryPath(location.CommonDirectory) != PathUtils.ToPosixDirectoryPath(location.GitDirectory) &&
File.Exists(gitdirFilePath);

if (isLinkedWorkingTree)
{
// https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-worktreesltidgtgitdir

string workingDirectory;
try
{
workingDirectory = File.ReadAllText(gitdirFilePath);
}
catch (Exception e) when (!(e is IOException))
{
throw new IOException(e.Message, e);
}

workingDirectory = workingDirectory.TrimEnd(CharUtils.AsciiWhitespace);

// Path in gitdir file must be absolute.
if (!PathUtils.IsAbsolute(workingDirectory))
{
throw new InvalidDataException(string.Format(Resources.PathSpecifiedInFileIsNotAbsolute, gitdirFilePath, workingDirectory));
}

try
{
return Path.GetFullPath(workingDirectory);
}
catch
{
throw new InvalidDataException(string.Format(Resources.PathSpecifiedInFileIsInvalid, gitdirFilePath, workingDirectory));
}
}
// TODO (https://github.com/dotnet/sourcelink/issues/301):
// GIT_WORK_TREE environment variable can also override working directory.

// Working directory can be overridden by a config option.
// See https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreworktree
string? value = config.GetVariableValue("core", "worktree");
if (value != null)
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.Build.Tasks.Git/GitOperations.cs
Expand Up @@ -357,7 +357,7 @@ internal sealed class DirectoryNode
public readonly string Name;
public readonly List<DirectoryNode> OrderedChildren;

// set on nodes that represent submodule working directory:
// set on nodes that represent working directory of the repository or a submodule:
public Lazy<GitIgnore.Matcher?>? Matcher;

public DirectoryNode(string name, List<DirectoryNode> orderedChildren)
Expand Down

0 comments on commit 48e002e

Please sign in to comment.