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));