diff --git a/GVFS/GVFS.UnitTests/Common/Git/AuthenticationConfigurationTests.cs b/GVFS/GVFS.UnitTests/Common/Git/AuthenticationConfigurationTests.cs index 2a1b94835..aeb47628d 100644 --- a/GVFS/GVFS.UnitTests/Common/Git/AuthenticationConfigurationTests.cs +++ b/GVFS/GVFS.UnitTests/Common/Git/AuthenticationConfigurationTests.cs @@ -24,7 +24,7 @@ public class AuthenticationConfigurationTests [SetUp] public void Setup() { - this.gitPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Git", "cmd", "git.exe"); + this.gitPath = GitTestHelper.FindGitOnPath(); this.root = Path.Combine(Path.GetTempPath(), nameof(AuthenticationConfigurationTests) + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(Path.Combine(this.root, ".git", "objects")); Directory.CreateDirectory(Path.Combine(this.root, ".git", "refs", "heads")); @@ -521,7 +521,7 @@ public void ProductionFillCarriesItsOwnEnvironmentIntoTheCollector() { File.AppendAllText(Path.Combine(this.root, ".git", "config"), "[credential]\nhelper =\ninteractive = never\n"); - PlumbingGitProcess git = new PlumbingGitProcess(this.gitPath, this.root); + PlumbingGitProcess git = new PlumbingGitProcess(this.GetGitPath(), this.root); MockTracer tracer = new MockTracer(); Assert.That(git.TryGetCredential(tracer, "https://dev.azure.com/org/repo", out _, out _, out _, timeoutMs: 5000), Is.False); Assert.That(git.CollectedEnvironment, Is.SameAs(git.ResultEnvironment)); @@ -551,7 +551,7 @@ public void FailedCredentialFillBeforeCloneCreatesTheWorkingDirectoryCollectsCon { "GIT_TERMINAL_PROMPT", "0" }, { "GCM_VALIDATE", "0" }, }; - PreCloneGitProcess git = new PreCloneGitProcess(this.gitPath, workingDirectory, environment); + PreCloneGitProcess git = new PreCloneGitProcess(this.GetGitPath(), workingDirectory, environment); MockTracer tracer = new MockTracer(); Assert.That(git.TryGetCredential(tracer, "https://dev.azure.com/org/repo", out _, out _, out _), Is.False); @@ -571,18 +571,23 @@ public void FailedCredentialFillBeforeCloneCreatesTheWorkingDirectoryCollectsCon this.AssertNoSecret(snapshot); } + private string GetGitPath() + { + if (!File.Exists(this.gitPath)) + { + Assert.Fail("The authentication configuration integration tests require Git on PATH."); + } + + return this.gitPath; + } + private EventMetadata Collect( string url, Action customize = null, Dictionary credentialFillEnvironment = null, ITracer tracer = null) { - if (!File.Exists(this.gitPath)) - { - Assert.Fail("The authentication configuration integration tests require Git for Windows."); - } - - GitProcess git = new GitProcess(this.gitPath, this.root); + GitProcess git = new GitProcess(this.GetGitPath(), this.root); return AuthenticationConfiguration.Collect(url, command => { Process process = git.GetGitProcess(command, this.root, Path.Combine(this.root, ".git"), false, null, false); diff --git a/GVFS/GVFS.UnitTests/Common/Git/GitProcessCredentialTests.cs b/GVFS/GVFS.UnitTests/Common/Git/GitProcessCredentialTests.cs index 39899ec0c..434af25e8 100644 --- a/GVFS/GVFS.UnitTests/Common/Git/GitProcessCredentialTests.cs +++ b/GVFS/GVFS.UnitTests/Common/Git/GitProcessCredentialTests.cs @@ -156,10 +156,10 @@ public void CredentialFillDoesNotRetryWhenTheTimeoutCannotCoverTheDelay() [TestCase(false, Timeout.Infinite)] public void CredentialFillHandlesARealLockedGitConfig(bool releaseAfterFirstAttempt, int timeoutMs) { - string gitPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Git", "cmd", "git.exe"); - if (!File.Exists(gitPath)) + string gitPath = GitTestHelper.FindGitOnPath(); + if (gitPath == null) { - Assert.Ignore("Git is not installed at the expected path."); + Assert.Ignore("Git was not found on PATH."); } string repoPath = Path.Combine(Path.GetTempPath(), nameof(GitProcessCredentialTests) + Guid.NewGuid().ToString("N")); diff --git a/GVFS/GVFS.UnitTests/Common/RetryConfigTests.cs b/GVFS/GVFS.UnitTests/Common/RetryConfigTests.cs index 64ab99190..e2d3c87c0 100644 --- a/GVFS/GVFS.UnitTests/Common/RetryConfigTests.cs +++ b/GVFS/GVFS.UnitTests/Common/RetryConfigTests.cs @@ -595,10 +595,10 @@ public void LibGit2ReadsConfigWithIncludeConditionsItEvaluates(string condition) private static string GetGitPathOrIgnore() { - string gitPath = "C:\\Program Files\\Git\\cmd\\git.exe"; - if (!File.Exists(gitPath)) + string gitPath = GitTestHelper.FindGitOnPath(); + if (gitPath == null) { - Assert.Ignore("Git not found at expected path — skipping integration test"); + Assert.Ignore("Git was not found on PATH."); } return gitPath; diff --git a/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs b/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs index 04fc9038d..2f587b698 100644 --- a/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs +++ b/GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs @@ -77,10 +77,8 @@ public void TearDown() } } - private GVFSEnlistment CreateWorktreeEnlistment() + private GVFSEnlistment CreateWorktreeEnlistment(string gitBinPath = @"mock:\git") { - string gitBinPath = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath() - ?? @"C:\Program Files\Git\cmd\git.exe"; return GVFSEnlistment.CreateForWorktree( this.primaryRoot, gitBinPath, authentication: null, GVFSEnlistment.TryGetWorktreeInfo(this.worktreePath), @@ -198,7 +196,7 @@ public void RepoUrlIsReadFromSharedConfig() public void WorktreeEnlistmentIdIsEmptyWhenNothingIsPersisted() { this.AppendToSharedConfig("[gvfs]\n\tenlistment-id = primaryenlistmentid\n"); - GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(GetGitBinPath()); enlistment.GetEnlistmentId().ShouldEqual(string.Empty); File.Exists(GetRepoMetadataPath(enlistment.DotGVFSRoot)).ShouldBeFalse("Reading the id must not create worktree repo metadata"); @@ -210,10 +208,10 @@ public void WorktreeEnlistmentIdIsDerivedFromPrimaryRepoMetadata() { this.AppendToSharedConfig("[gvfs]\n\tenlistment-id = sharedconfigenlistmentid\n"); EditRepoMetadata(this.PrimaryDotGVFSRoot, metadata => metadata.SetEntry(RepoMetadata.Keys.EnlistmentId, "primaryenlistmentid")); - GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(GetGitBinPath()); enlistment.GetEnlistmentId().ShouldEqual(DerivedEnlistmentIdOfAgentWorktree); - this.CreateWorktreeEnlistment().GetEnlistmentId().ShouldEqual(DerivedEnlistmentIdOfAgentWorktree); + this.CreateWorktreeEnlistment(GetGitBinPath()).GetEnlistmentId().ShouldEqual(DerivedEnlistmentIdOfAgentWorktree); RepoMetadata.Instance.ShouldBeNull("Reading the id must not initialize RepoMetadata.Instance"); } @@ -300,7 +298,7 @@ public void WorktreeMetadataCreatesEnlistmentIdWhenNoneIsGivenOrStored(string st [TestCase] public void RequiredSettingsEnableWorktreeConfigAndWritePerWorktreeValues() { - GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(GetGitBinPath()); string error; RequiredGitConfig.TrySetRequiredSettings( @@ -323,7 +321,7 @@ public void RequiredSettingsRepairCopiedWorktreeValueAndSharedValueOfPrimary() { this.AppendToSharedConfig("[status]\n\tdeserializePath = C:/sibling/status.dat\n"); File.WriteAllText(this.WorktreeConfigPath, "[status]\n\tdeserializePath = C:/sibling/status.dat\n"); - GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(GetGitBinPath()); string error; RequiredGitConfig.TrySetRequiredSettings( @@ -340,7 +338,7 @@ public void RequiredSettingsDisableNullPerWorktreeValueInWorktreeConfig() { this.AppendToSharedConfig("[extensions]\n\tworktreeConfig = true\n[status]\n\tdeserializePath = C:/primary/status.dat\n"); File.WriteAllText(this.WorktreeConfigPath, "[status]\n\tdeserializePath = C:/worktree/status.dat\n"); - GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(GetGitBinPath()); string error; RequiredGitConfig.TrySetRequiredSettings( @@ -357,7 +355,7 @@ public void RequiredSettingsDisableNullPerWorktreeValueInWorktreeConfig() public void RequiredSettingsRemoveRequiredOverridesFromWorktreeConfig() { File.WriteAllText(this.WorktreeConfigPath, "[core]\n\tautocrlf = true\n[gvfs]\n\tmount-id = worktreemountid\n"); - GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(GetGitBinPath()); string error; RequiredGitConfig.TrySetRequiredSettings( @@ -374,7 +372,7 @@ public void RequiredSettingsRemoveRequiredOverridesFromWorktreeConfig() public void RequiredSettingsReplaceDuplicateWorktreeConfigExtensionValues() { this.AppendToSharedConfig("[extensions]\n\tworktreeConfig = true\n\tworktreeConfig = false\n"); - GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(GetGitBinPath()); string error; RequiredGitConfig.TrySetRequiredSettings(enlistment, new Dictionary(), out error).ShouldBeTrue(error); @@ -421,7 +419,7 @@ public void PrimaryRequiredSettingsDoNotEnableWorktreeConfigWithoutLinkedWorktre public void RequiredSettingsWriteNothingWhenAppliedAgain() { GVFSEnlistment primary = new GVFSEnlistment(this.primaryRoot, "https://mock/repo", GetGitBinPath(), authentication: null); - GVFSEnlistment worktree = this.CreateWorktreeEnlistment(); + GVFSEnlistment worktree = this.CreateWorktreeEnlistment(GetGitBinPath()); string error; RequiredGitConfig.TrySetRequiredSettings(primary, out error).ShouldBeTrue(error); @@ -441,7 +439,7 @@ public void RequiredSettingsWriteNothingWhenAppliedAgain() public void RequiredSettingsFailWhenWorktreeConfigIsCorrupt() { File.WriteAllText(this.WorktreeConfigPath, "[status\n"); - GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(); + GVFSEnlistment enlistment = this.CreateWorktreeEnlistment(GetGitBinPath()); string error; RequiredGitConfig.TrySetRequiredSettings( @@ -462,7 +460,9 @@ public void RequiredSettingsFailWhenWorktreeConfigIsCorrupt() private static string GetGitBinPath() { - return GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath() ?? @"C:\Program Files\Git\cmd\git.exe"; + string gitPath = GitTestHelper.FindGitOnPath(); + Assert.That(gitPath, Is.Not.Null, "The required-settings tests require Git on PATH."); + return gitPath; } private static string GetRepoMetadataPath(string dotGVFSRoot) diff --git a/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs b/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs index 4dd4b0fec..b401efd5b 100644 --- a/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs +++ b/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs @@ -555,10 +555,10 @@ public void QuoteGitPath(string input, string expected) [TestCase(Timeout.Infinite)] public void GitCommandWaitsForOutputCallbacks(int timeoutMs) { - string gitPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Git", "cmd", "git.exe"); - if (!File.Exists(gitPath)) + string gitPath = GitTestHelper.FindGitOnPath(); + if (gitPath == null) { - Assert.Ignore("Git is not installed at the expected path."); + Assert.Ignore("Git was not found on PATH."); } OutputCallbackGitProcess gitProcess = new OutputCallbackGitProcess(gitPath); @@ -605,10 +605,10 @@ public void QuoteGitPath_RoundTripThroughProcess() "path\\with\\backslashes\\file.txt", }; - string gitPath = "C:\\Program Files\\Git\\cmd\\git.exe"; - if (!System.IO.File.Exists(gitPath)) + string gitPath = GitTestHelper.FindGitOnPath(); + if (gitPath == null) { - Assert.Ignore("Git not found at expected path — skipping integration test"); + Assert.Ignore("Git was not found on PATH."); } foreach (string testPath in testPaths) diff --git a/GVFS/GVFS.UnitTests/Git/GitTestHelperTests.cs b/GVFS/GVFS.UnitTests/Git/GitTestHelperTests.cs new file mode 100644 index 000000000..607680b93 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Git/GitTestHelperTests.cs @@ -0,0 +1,79 @@ +using NUnit.Framework; +using System; +using System.IO; + +namespace GVFS.UnitTests.Git +{ + [TestFixture] + [NonParallelizable] + public class GitTestHelperTests + { + private string root; + + [SetUp] + public void SetUp() + { + this.root = Path.Combine(Path.GetTempPath(), nameof(GitTestHelperTests) + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(this.root); + } + + [TearDown] + public void TearDown() + { + Directory.Delete(this.root, recursive: true); + } + + [TestCase("First Git")] + [TestCase("First; Git")] + public void FindsTheFirstGitInQuotedPathEntriesWithSpaces(string firstDirectoryName) + { + string firstDirectory = Path.Combine(this.root, firstDirectoryName); + string secondDirectory = Path.Combine(this.root, "Second Git"); + Directory.CreateDirectory(firstDirectory); + Directory.CreateDirectory(secondDirectory); + string expectedGit = Path.Combine(firstDirectory, "git.exe"); + File.WriteAllText(expectedGit, string.Empty); + File.WriteAllText(Path.Combine(secondDirectory, "git.exe"), string.Empty); + string searchPath = string.Join(Path.PathSeparator, + Path.Combine(this.root, "missing"), "\"" + firstDirectory + "\"", secondDirectory); + + Assert.That(GitTestHelper.FindGitOnPath(searchPath), Is.EqualTo(expectedGit)); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(";;")] + [TestCase("\"\"; ;")] + public void EmptyPathEntriesDoNotSearchTheWorkingDirectory(string searchPath) + { + File.WriteAllText(Path.Combine(this.root, "git.exe"), string.Empty); + string originalDirectory = Environment.CurrentDirectory; + try + { + Environment.CurrentDirectory = this.root; + + Assert.That(GitTestHelper.FindGitOnPath(searchPath), Is.Null); + } + finally + { + Environment.CurrentDirectory = originalDirectory; + } + } + + [Test] + public void RelativePathEntriesReturnAnAbsolutePath() + { + string expectedGit = Path.Combine(this.root, "git.exe"); + File.WriteAllText(expectedGit, string.Empty); + string searchPath = Path.GetRelativePath(Environment.CurrentDirectory, this.root); + + Assert.That(GitTestHelper.FindGitOnPath(searchPath), Is.EqualTo(expectedGit).IgnoreCase); + } + + [Test] + public void MissingGitReturnsNull() + { + Assert.That(GitTestHelper.FindGitOnPath(this.root), Is.Null); + } + } +} diff --git a/GVFS/GVFS.UnitTests/GitTestHelper.cs b/GVFS/GVFS.UnitTests/GitTestHelper.cs new file mode 100644 index 000000000..6a947558b --- /dev/null +++ b/GVFS/GVFS.UnitTests/GitTestHelper.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; + +namespace GVFS.UnitTests +{ + internal static class GitTestHelper + { + internal static string FindGitOnPath() + { + return FindGitOnPath(Environment.GetEnvironmentVariable("PATH")); + } + + internal static string FindGitOnPath(string searchPath) + { + if (string.IsNullOrEmpty(searchPath)) + { + return null; + } + + int entryStart = 0; + bool quoted = false; + for (int index = 0; index <= searchPath.Length; ++index) + { + if (index < searchPath.Length) + { + if (searchPath[index] == '"') + { + quoted = !quoted; + } + + if (searchPath[index] != Path.PathSeparator || quoted) + { + continue; + } + } + + string directory = searchPath.Substring(entryStart, index - entryStart).Trim('"'); + entryStart = index + 1; + if (string.IsNullOrWhiteSpace(directory)) + { + continue; + } + + string gitPath = Path.Combine(directory, "git.exe"); + if (File.Exists(gitPath)) + { + return Path.GetFullPath(gitPath); + } + } + + return null; + } + } +}