Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix discovery of working directory for worktrees #734

Merged
merged 1 commit into from Aug 12, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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