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

Detect Git features (the GVFS Protocol) based on the version #407

Merged
merged 7 commits into from
Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Scalar.Common/Git/GitFeatureFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace Scalar.Common.Git
{
/// <summary>
/// Identifies a set of features that Git may support that we are interested in.
/// </summary>
[Flags]
public enum GitFeatureFlags
{
None = 0,

/// <summary>
/// Support for the GVFS protocol.
/// </summary>
GvfsProtocol = 1 << 0,
}
}
91 changes: 77 additions & 14 deletions Scalar.Common/Git/GitVersion.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
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;
}

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

/// <summary>
/// Determine the set of Git features that are supported in this version of Git.
/// </summary>
/// <returns>Set of Git features.</returns>
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
Expand Down Expand Up @@ -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<N>".
//
// 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<N>.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;
}

Expand All @@ -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)
Expand Down
94 changes: 93 additions & 1 deletion Scalar.UnitTests/Common/GitVersionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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);
}

Expand All @@ -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()
{
Expand Down Expand Up @@ -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);
Expand All @@ -199,11 +244,57 @@ 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);
mjcheetham marked this conversation as resolved.
Show resolved Hide resolved

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

private void ParseAndValidateInstallerVersion(string installerName)
{
GitVersion version;
Expand All @@ -213,6 +304,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);
Expand Down
51 changes: 40 additions & 11 deletions Scalar/CommandLine/CloneVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down