From 7c3575a24cda3635404883a23bcb677b028e5c43 Mon Sep 17 00:00:00 2001 From: tmat Date: Thu, 12 Aug 2021 15:02:58 -0700 Subject: [PATCH] Fix discovery of working directory for worktrees 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. --- .../GitRepositoryTests.cs | 107 +++++++++++++++++- .../GitDataReader/GitRepository.cs | 44 +------ .../GitOperations.cs | 2 +- 3 files changed, 110 insertions(+), 43 deletions(-) diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs index c0e502af..d86a3e84 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs @@ -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] @@ -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() { @@ -191,6 +230,72 @@ public void OpenRepository_VersionNotSupported() Assert.Throws(() => 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); + } + + /// + /// The directory in gitdir file is ignored for the purposes of determining repository working directory. + /// + [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() { diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs index 30ee72bc..73be8c8f 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs @@ -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; @@ -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"; @@ -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) diff --git a/src/Microsoft.Build.Tasks.Git/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs index ed9d36a7..4cf550f4 100644 --- a/src/Microsoft.Build.Tasks.Git/GitOperations.cs +++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs @@ -357,7 +357,7 @@ internal sealed class DirectoryNode public readonly string Name; public readonly List OrderedChildren; - // set on nodes that represent submodule working directory: + // set on nodes that represent working directory of the repository or a submodule: public Lazy? Matcher; public DirectoryNode(string name, List orderedChildren)