diff --git a/Scalar.Common/Git/GitFeatureFlags.cs b/Scalar.Common/Git/GitFeatureFlags.cs new file mode 100644 index 0000000000..80201eac34 --- /dev/null +++ b/Scalar.Common/Git/GitFeatureFlags.cs @@ -0,0 +1,18 @@ +using System; + +namespace Scalar.Common.Git +{ + /// + /// Identifies a set of features that Git may support that we are interested in. + /// + [Flags] + public enum GitFeatureFlags + { + None = 0, + + /// + /// Support for the GVFS protocol. + /// + GvfsProtocol = 1 << 0, + } +} diff --git a/Scalar.Common/Git/GitVersion.cs b/Scalar.Common/Git/GitVersion.cs index f357eed676..01e9511046 100644 --- a/Scalar.Common/Git/GitVersion.cs +++ b/Scalar.Common/Git/GitVersion.cs @@ -1,14 +1,16 @@ using System; +using System.Text; namespace Scalar.Common.Git { public class GitVersion { - public GitVersion(int major, int minor, int build, string platform, int revision, int minorRevision) + public GitVersion(int major, int minor, int build, string platform = null, int revision = 0, int minorRevision = 0, int? rc = null) { this.Major = major; this.Minor = minor; this.Build = build; + this.ReleaseCandidate = rc; this.Platform = platform; this.Revision = revision; this.MinorRevision = minorRevision; @@ -16,11 +18,28 @@ public GitVersion(int major, int minor, int build, string platform, int revision public int Major { get; private set; } public int Minor { get; private set; } - public string Platform { get; private set; } public int Build { get; private set; } + public int? ReleaseCandidate { get; private set; } + public string Platform { get; private set; } public int Revision { get; private set; } public int MinorRevision { get; private set; } + /// + /// Determine the set of Git features that are supported in this version of Git. + /// + /// Set of Git features. + public GitFeatureFlags GetFeatures() + { + var flags = GitFeatureFlags.None; + + if (StringComparer.OrdinalIgnoreCase.Equals(Platform, "vfs")) + { + flags |= GitFeatureFlags.GvfsProtocol; + } + + return flags; + } + public static bool TryParseGitVersionCommandResult(string input, out GitVersion version) { // git version output is of the form @@ -59,48 +78,78 @@ public static bool TryParseInstallerName(string input, string installerExtension public static bool TryParseVersion(string input, out GitVersion version) { version = null; - int major, minor, build, revision, minorRevision; + + int major, minor, build, revision = 0, minorRevision = 0; + int? rc = null; + string platform = null; if (string.IsNullOrWhiteSpace(input)) { return false; } - string[] parsedComponents = input.Split(new char[] { '.' }); - int parsedComponentsLength = parsedComponents.Length; - if (parsedComponentsLength < 5) + string[] parsedComponents = input.Split('.'); + int numComponents = parsedComponents.Length; + + // We minimally accept the official Git version number format which + // consists of three components: "major.minor.build" or "major.minor.build-rc". + // + // The other supported formats are the Git for Windows and Microsoft Git + // formats which look like: "major.minor.build.platform.revision.minorRevision" + // or "major.minor.build-rc.platform.revision.minorRevision". + // 0 1 2 3 4 5 + // len 1 2 3 4 5 6 + // + if (numComponents < 3) { return false; } + // Major version if (!TryParseComponent(parsedComponents[0], out major)) { return false; } + // Minor version if (!TryParseComponent(parsedComponents[1], out minor)) { return false; } - if (!TryParseComponent(parsedComponents[2], out build)) + // Check if this is a release candidate version and if so split + // it from the build number. + string[] buildParts = parsedComponents[2].Split("-rc", StringSplitOptions.RemoveEmptyEntries); + if (buildParts.Length > 1 && TryParseComponent(buildParts[1], out int rcInt)) { - return false; + rc = rcInt; } - if (!TryParseComponent(parsedComponents[4], out revision)) + // Build number + if (!TryParseComponent(buildParts[0], out build)) { return false; } - if (parsedComponentsLength < 6 || !TryParseComponent(parsedComponents[5], out minorRevision)) + // Take the platform component verbatim + if (numComponents >= 4) { - minorRevision = 0; + platform = parsedComponents[3]; } - string platform = parsedComponents[3]; + // Platform revision + if (numComponents < 5 || !TryParseComponent(parsedComponents[4], out revision)) + { + revision = 0; + } - version = new GitVersion(major, minor, build, platform, revision, minorRevision); + // Minor platform revision + if (numComponents < 6 || !TryParseComponent(parsedComponents[5], out minorRevision)) + { + minorRevision = 0; + } + + version = new GitVersion(major, minor, build, platform, revision, minorRevision, rc); return true; } @@ -121,7 +170,21 @@ public bool IsLessThan(GitVersion other) public override string ToString() { - return string.Format("{0}.{1}.{2}.{3}.{4}.{5}", this.Major, this.Minor, this.Build, this.Platform, this.Revision, this.MinorRevision); + var sb = new StringBuilder(); + + sb.AppendFormat("{0}.{1}.{2}", this.Major, this.Minor, this.Build); + + if (this.ReleaseCandidate.HasValue) + { + sb.AppendFormat("-rc{0}", this.ReleaseCandidate.Value); + } + + if (!string.IsNullOrWhiteSpace(this.Platform)) + { + sb.AppendFormat(".{0}.{1}.{2}", this.Platform, this.Revision, this.MinorRevision); + } + + return sb.ToString(); } private static bool TryParseComponent(string component, out int parsedComponent) diff --git a/Scalar.UnitTests/Common/GitVersionTests.cs b/Scalar.UnitTests/Common/GitVersionTests.cs index fcd0bdd42a..f29632920e 100644 --- a/Scalar.UnitTests/Common/GitVersionTests.cs +++ b/Scalar.UnitTests/Common/GitVersionTests.cs @@ -8,6 +8,26 @@ namespace Scalar.UnitTests.Common [TestFixture] public class GitVersionTests { + [TestCase] + public void GetFeatureFlags_VfsGitVersion_ReturnsGvfsProtocolSupported() + { + var version = new GitVersion(2, 28, 0, "vfs", 1, 0); + GitFeatureFlags features = version.GetFeatures(); + features.HasFlag(GitFeatureFlags.GvfsProtocol).ShouldBeTrue(); + } + + [TestCase] + public void GetFeatureFlags_NormalGitVersion_ReturnsGvfsProtocolNotSupported() + { + var gitGitVersion = new GitVersion(2, 28, 0); + GitFeatureFlags gitGitFeatures = gitGitVersion.GetFeatures(); + gitGitFeatures.HasFlag(GitFeatureFlags.GvfsProtocol).ShouldBeFalse(); + + var winGitVersion = new GitVersion(2, 28, 0, "windows", 1, 1); + GitFeatureFlags winGitFeatures = winGitVersion.GetFeatures(); + winGitFeatures.HasFlag(GitFeatureFlags.GvfsProtocol).ShouldBeFalse(); + } + [TestCase] public void TryParseInstallerName() { @@ -36,7 +56,7 @@ public void Version_Data_Empty_Returns_False() public void Version_Data_Not_Enough_Numbers_Returns_False() { GitVersion version; - bool success = GitVersion.TryParseVersion("2.0.1.test", out version); + bool success = GitVersion.TryParseVersion("2.0", out version); success.ShouldEqual(false); } @@ -50,12 +70,36 @@ public void Version_Data_Too_Many_Numbers_Returns_True() [TestCase] public void Version_Data_Valid_Returns_True() + { + GitVersion version; + bool success = GitVersion.TryParseVersion("2.0.1", out version); + success.ShouldEqual(true); + } + + [TestCase] + public void Version_Data_Valid_With_RC_Returns_True() + { + GitVersion version; + bool success = GitVersion.TryParseVersion("2.0.1-rc3", out version); + success.ShouldEqual(true); + } + + [TestCase] + public void Version_Data_Valid_With_Platform_Returns_True() { GitVersion version; bool success = GitVersion.TryParseVersion("2.0.1.test.1.2", out version); success.ShouldEqual(true); } + [TestCase] + public void Version_Data_Valid_With_RC_And_Platform_Returns_True() + { + GitVersion version; + bool success = GitVersion.TryParseVersion("2.0.1-rc3.test.1.2", out version); + success.ShouldEqual(true); + } + [TestCase] public void Compare_Different_Platforms_Returns_False() { @@ -185,6 +229,7 @@ public void Allow_Blank_Minor_Revision() version.Major.ShouldEqual(1); version.Minor.ShouldEqual(2); version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(null); version.Platform.ShouldEqual("test"); version.Revision.ShouldEqual(4); version.MinorRevision.ShouldEqual(0); @@ -199,11 +244,72 @@ public void Allow_Invalid_Minor_Revision() version.Major.ShouldEqual(1); version.Minor.ShouldEqual(2); version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(null); version.Platform.ShouldEqual("test"); version.Revision.ShouldEqual(4); version.MinorRevision.ShouldEqual(0); } + [TestCase] + public void Allow_ReleaseCandidate() + { + GitVersion version; + GitVersion.TryParseVersion("1.2.3-rc4", out version).ShouldEqual(true); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(4); + version.Platform.ShouldBeNull(); + version.Revision.ShouldEqual(0); + version.MinorRevision.ShouldEqual(0); + } + + [TestCase] + public void Allow_ReleaseCandidate_Platform() + { + GitVersion version; + GitVersion.TryParseVersion("1.2.3-rc4.test", out version).ShouldEqual(true); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(4); + version.Platform.ShouldEqual("test"); + version.Revision.ShouldEqual(0); + version.MinorRevision.ShouldEqual(0); + } + + [TestCase] + public void Allow_LocalGitBuildVersion_ParseMajorMinorBuildOnly() + { + GitVersion version; + GitVersion.TryParseVersion("1.2.3.456.abcdefg.hijk", out version).ShouldEqual(true); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(null); + version.Platform.ShouldEqual("456"); + version.Revision.ShouldEqual(0); + version.MinorRevision.ShouldEqual(0); + } + + [TestCase] + public void Allow_GarbageBuildVersion_ParseMajorMinorBuildOnly() + { + GitVersion version; + GitVersion.TryParseVersion("1.2.3.test.4.5.6.7.g1234abcd.8.9.😀.10.11.dirty.MSVC", out version).ShouldEqual(true); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(null); + version.Platform.ShouldEqual("test"); + version.Revision.ShouldEqual(4); + version.MinorRevision.ShouldEqual(5); + } + private void ParseAndValidateInstallerVersion(string installerName) { GitVersion version; @@ -213,6 +319,7 @@ private void ParseAndValidateInstallerVersion(string installerName) version.Major.ShouldEqual(1); version.Minor.ShouldEqual(2); version.Build.ShouldEqual(3); + version.ReleaseCandidate.ShouldEqual(null); version.Platform.ShouldEqual("scalar"); version.Revision.ShouldEqual(4); version.MinorRevision.ShouldEqual(5); diff --git a/Scalar/CommandLine/CloneVerb.cs b/Scalar/CommandLine/CloneVerb.cs index c5ce7e19fc..4646a13f60 100644 --- a/Scalar/CommandLine/CloneVerb.cs +++ b/Scalar/CommandLine/CloneVerb.cs @@ -211,24 +211,53 @@ private Result DoClone(string fullEnlistmentRootPathParameter, string normalized resolvedLocalCacheRoot = Path.GetFullPath(this.LocalCacheRoot); } - string authErrorMessage = null; - GitAuthentication.Result authResult = GitAuthentication.Result.UnableToDetermine; - - // Do not try authentication on SSH URLs. - if (this.enlistment.RepoUrl.StartsWith("https://")) + // Determine what features of Git we have available to guide how we init/clone the repository + var gitFeatures = GitFeatureFlags.None; + string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); + this.tracer.RelatedInfo("Attempting to determine Git version for installation '{0}'", gitBinPath); + if (GitProcess.TryGetVersion(gitBinPath, out var gitVersion, out string gitVersionError)) + { + this.tracer.RelatedInfo("Git installation '{0}' has version '{1}", gitBinPath, gitVersion); + gitFeatures = gitVersion.GetFeatures(); + } + else { - authResult = this.TryAuthenticate(this.tracer, this.enlistment, out authErrorMessage); + this.tracer.RelatedWarning("Unable to detect Git features for installation '{0}'. Failed to get Git version: '{1}", gitBinPath, gitVersionError); + this.Output.WriteLine("Warning: unable to detect Git features: {0}", gitVersionError); } - if (authResult == GitAuthentication.Result.UnableToDetermine) + // Do not try GVFS authentication on SSH URLs or when we don't have Git support for the GVFS protocol + bool isHttpsRemote = this.enlistment.RepoUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase); + bool supportsGvfsProtocol = (gitFeatures & GitFeatureFlags.GvfsProtocol) != 0; + if (!isHttpsRemote || !supportsGvfsProtocol) { - // We can't tell, because we don't have the right endpoint! + // Perform a normal Git clone because we cannot use the GVFS protocol + this.tracer.RelatedInfo("Skipping GVFS protocol check (isHttpsRemote={0}, supportsGvfsProtocol={1})", + isHttpsRemote, supportsGvfsProtocol); + this.Output.WriteLine("Skipping GVFS protocol check..."); return this.GitClone(); } - if (authResult == GitAuthentication.Result.Failed) - { - this.ReportErrorAndExit(this.tracer, "Cannot clone because authentication failed: " + authErrorMessage); + // Check if we can authenticate with a GVFS protocol supporting endpoint (gvfs/config) + string authErrorMessage; + GitAuthentication.Result authResult = this.TryAuthenticate(this.tracer, this.enlistment, out authErrorMessage); + switch (authResult) + { + case GitAuthentication.Result.Success: + // Continue + this.tracer.RelatedInfo("Successfully authenticated to gvfs/config"); + break; + case GitAuthentication.Result.Failed: + this.tracer.RelatedInfo("Failed to authenticate to gvfs/config"); + this.ReportErrorAndExit(this.tracer, "Cannot clone because authentication failed: " + authErrorMessage); + break; + case GitAuthentication.Result.UnableToDetermine: + // We cannot determine if the GVFS protocol is supported so do a normal Git clone + this.tracer.RelatedInfo("Cannot determine authentication success to gvfs/config"); + this.Output.WriteLine("GVFS protocol is not supported."); + return this.GitClone(); + default: + throw new ArgumentOutOfRangeException(nameof(GitAuthentication.Result), authResult, "Unknown value"); } this.retryConfig = this.GetRetryConfig(this.tracer, this.enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes));