Skip to content
5 changes: 5 additions & 0 deletions Scalar.Common/Git/GitFeatureFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,10 @@ public enum GitFeatureFlags
/// * Subcommands: run, register, unregister, start, stop
/// </summary>
MaintenanceBuiltin = 1 << 1,

/// <summary>
/// Supports the builtin FS Monitor
/// </summary>
BuiltinFSMonitor = 1 << 2,
}
}
24 changes: 24 additions & 0 deletions Scalar.Common/Git/GitProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,34 @@ public static bool TryGetVersion(string gitBinPath, out GitVersion gitVersion, o
return false;
}

gitVersion.Features.UnionWith(GetAdvertisedFeatures(gitBinPath));

error = null;
return true;
}

public static IEnumerable<string> GetAdvertisedFeatures(string gitBinPath)
{
GitProcess gitProcess = new GitProcess(gitBinPath, null);
Result result = gitProcess.InvokeGitOutsideEnlistment("version --build-options");
string version = result.Output;

if (result.ExitCodeIsFailure)
{
yield break;
}
const string prefix = "feature: ";
foreach (string line in result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
string trimmed = line.Trim();

if (trimmed.StartsWith(prefix))
{
yield return line.Substring(prefix.Length);
}
}
}

/// <summary>
/// Tries to kill the run git process. Make sure you only use this on git processes that can safely be killed!
/// </summary>
Expand Down
53 changes: 51 additions & 2 deletions Scalar.Common/Git/GitVersion.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
using Scalar.Common.Tracing;

namespace Scalar.Common.Git
{
public class GitVersion
{
public GitVersion(int major, int minor, int build, string platform = null, int revision = 0, int minorRevision = 0, int? rc = null)
public GitVersion(int major, int minor, int build, string platform = null, int revision = 0, int minorRevision = 0, int? rc = null, string extra = null)
{
this.Major = major;
this.Minor = minor;
Expand All @@ -14,6 +16,8 @@ public GitVersion(int major, int minor, int build, string platform = null, int r
this.Platform = platform;
this.Revision = revision;
this.MinorRevision = minorRevision;
this.Extra = extra;
this.Features = new HashSet<string>();
}

public int Major { get; private set; }
Expand All @@ -23,6 +27,8 @@ public GitVersion(int major, int minor, int build, string platform = null, int r
public string Platform { get; private set; }
public int Revision { get; private set; }
public int MinorRevision { get; private set; }
public string Extra { get; private set; }
public HashSet<string> Features { get; private set; }

/// <summary>
/// Determine the set of Git features that are supported in this version of Git.
Expand All @@ -43,6 +49,11 @@ public GitFeatureFlags GetFeatures()
flags |= GitFeatureFlags.MaintenanceBuiltin;
}

if (this.Features.Contains("fsmonitor--daemon"))
{
flags |= GitFeatureFlags.BuiltinFSMonitor;
}

return flags;
}

Expand Down Expand Up @@ -88,6 +99,7 @@ public static bool TryParseVersion(string input, out GitVersion version)
int major, minor, build, revision = 0, minorRevision = 0;
int? rc = null;
string platform = null;
string extra = null;

if (string.IsNullOrWhiteSpace(input))
{
Expand Down Expand Up @@ -155,10 +167,33 @@ public static bool TryParseVersion(string input, out GitVersion version)
minorRevision = 0;
}

version = new GitVersion(major, minor, build, platform, revision, minorRevision, rc);
if (numComponents > 6) {
extra = parsedComponents[6].Trim();
Comment thread
derrickstolee marked this conversation as resolved.
}

version = new GitVersion(major, minor, build, platform, revision, minorRevision, rc, extra);
return true;
}

public static GitFeatureFlags GetAvailableGitFeatures(ITracer tracer)
{
// 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();
tracer?.RelatedInfo("Attempting to determine Git version for installation '{0}'", gitBinPath);
if (GitProcess.TryGetVersion(gitBinPath, out var gitVersion, out string gitVersionError))
{
tracer?.RelatedInfo("Git installation '{0}' has version '{1}", gitBinPath, gitVersion);
gitFeatures = gitVersion.GetFeatures();
}
else
{
tracer?.RelatedWarning("Unable to detect Git features for installation '{0}'. Failed to get Git version: '{1}", gitBinPath, gitVersionError);
}

return gitFeatures;
}

public bool IsEqualTo(GitVersion other)
{
if (this.Platform != other.Platform)
Expand Down Expand Up @@ -190,6 +225,20 @@ public override string ToString()
sb.AppendFormat(".{0}.{1}.{2}", this.Platform, this.Revision, this.MinorRevision);
}

if (this.Extra != null) {
sb.Append($".{this.Extra}");
}

if (this.Features.Count > 0)
{
sb.Append(" (");
foreach (string feature in this.Features)
{
sb.Append($" {feature} ");
}
sb.Append(")");
}

return sb.ToString();
}

Expand Down
57 changes: 52 additions & 5 deletions Scalar.Common/Maintenance/ConfigStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ private enum GitCoreGVFSFlags
}

private bool? UseGvfsProtocol = true;
private Dictionary<string, GitConfigSetting> existingConfigSettings = null;
Comment thread
derrickstolee marked this conversation as resolved.

public ConfigStep(ScalarContext context, bool? useGvfsProtocol = null, GitFeatureFlags gitFeatures = GitFeatureFlags.None)
: base(context, requireObjectCacheLock: false, gitFeatures: gitFeatures)
Expand Down Expand Up @@ -208,25 +209,71 @@ public bool TrySetConfig(out string error)
return false;
}

return this.ConfigureWatchmanIntegration(out error);
GitProcess.ConfigResult config = null;
GitProcess.Result getResult = this.RunGitCommand(
process => {
config = process.GetFromLocalConfig("feature.scalar");
return null;
},
nameof(GitProcess.GetFromLocalConfig)
);
GitFeatureFlags flags = GitVersion.GetAvailableGitFeatures(this.Context.Tracer);
config.TryParseAsString(out string scalar, out error, defaultValue: "true");

if (StringComparer.OrdinalIgnoreCase.Equals(scalar, "false"))
{
GitProcess.Result deleteResult = this.RunGitCommand(
process => process.DeleteFromLocalConfig("core.fsmonitor"),
nameof(GitProcess.DeleteFromLocalConfig)
);

return deleteResult.ExitCodeIsSuccess;
}
else if (StringComparer.OrdinalIgnoreCase.Equals(scalar, "experimental")
// Make sure Git supports builtin FS Monitor
&& flags.HasFlag(GitFeatureFlags.BuiltinFSMonitor))
{
// ":internal:" is a custom value to specify the builtin
// FS Monitor feature.
GitProcess.Result setResult = this.RunGitCommand(
process => process.SetInLocalConfig("core.fsmonitor", ":internal:"),
nameof(GitProcess.SetInLocalConfig)
);

return setResult.ExitCodeIsSuccess;
}
else
{
return this.ConfigureWatchmanIntegration(out error);
}
}

protected override void PerformMaintenance()
{
this.TrySetConfig(out _);
}

private bool TrySetConfig(Dictionary<string, string> configSettings, bool isRequired, out string error, bool add = false)
private Dictionary<string, GitConfigSetting> GetConfigSettings()
{
Dictionary<string, GitConfigSetting> existingConfigSettings = null;
if (this.existingConfigSettings != null)
{
return this.existingConfigSettings;
}

GitProcess.Result getConfigResult = this.RunGitCommand(
process => process.TryGetAllConfig(localOnly: isRequired, configSettings: out existingConfigSettings),
process => process.TryGetAllConfig(localOnly: true, configSettings: out this.existingConfigSettings),
Comment thread
derrickstolee marked this conversation as resolved.
nameof(GitProcess.TryGetAllConfig));

return this.existingConfigSettings;
}

private bool TrySetConfig(Dictionary<string, string> configSettings, bool isRequired, out string error, bool add = false)
{
Dictionary<string, GitConfigSetting> existingConfigSettings = this.GetConfigSettings();

// If the settings are required, then only check local config settings, because we don't want to depend on
// global settings that can then change independent of this repo.
if (getConfigResult.ExitCodeIsFailure)
if (existingConfigSettings == null)
{
error = "Failed to get all config entries";
return false;
Expand Down
3 changes: 2 additions & 1 deletion Scalar.FunctionalTests/Tools/GitHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ private static IEnumerable<string> NonEmptyLines(string data)
{
return data
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Where(s => !string.IsNullOrWhiteSpace(s));
.Where(s => !string.IsNullOrWhiteSpace(s))
.Where(s => !s.Contains("gvfs-helper error: '(curl"));
}

private static bool LinesAreEqual(string actualLine, string expectedLine)
Expand Down
16 changes: 16 additions & 0 deletions Scalar.UnitTests/Common/GitVersionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ public void GetFeatureFlags_MaintenanceBuiltin()
}
}

[TestCase]
public void GetFeatureFlags_BuiltinFSMonitor()
{
GitVersion version = new GitVersion(2, 30, 0, "vfs", 0, 0);
GitFeatureFlags gitFeatures = version.GetFeatures();
gitFeatures.HasFlag(GitFeatureFlags.BuiltinFSMonitor).ShouldBeFalse($"Incorrect for version {version}");

version.Features.Add("bogus");
gitFeatures = version.GetFeatures();
gitFeatures.HasFlag(GitFeatureFlags.BuiltinFSMonitor).ShouldBeFalse($"Incorrect for version {version}");

version.Features.Add("fsmonitor--daemon");
gitFeatures = version.GetFeatures();
gitFeatures.HasFlag(GitFeatureFlags.BuiltinFSMonitor).ShouldBeTrue($"Incorrect for version {version}");
}

[TestCase]
public void TryParseInstallerName()
{
Expand Down
2 changes: 1 addition & 1 deletion Scalar/CommandLine/CloneVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ private Result DoClone(string fullEnlistmentRootPathParameter, string normalized
resolvedLocalCacheRoot = Path.GetFullPath(this.LocalCacheRoot);
}

GitFeatureFlags gitFeatures = this.GetAvailableGitFeatures(this.tracer);
GitFeatureFlags gitFeatures = GitVersion.GetAvailableGitFeatures(this.tracer);

// 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);
Expand Down
2 changes: 1 addition & 1 deletion Scalar/CommandLine/RunVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ protected override void Execute(ScalarEnlistment enlistment)
GitObjectsHttpRequestor objectRequestor = null;
CacheServerInfo cacheServer;
GitObjects gitObjects;
GitFeatureFlags gitFeatures = this.GetAvailableGitFeatures(tracer);
GitFeatureFlags gitFeatures = GitVersion.GetAvailableGitFeatures(tracer);

switch (this.MaintenanceTask)
{
Expand Down
20 changes: 0 additions & 20 deletions Scalar/CommandLine/ScalarVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -492,26 +492,6 @@ protected void UnregisterRepo(string enlistmentRoot)
}
}

protected GitFeatureFlags GetAvailableGitFeatures(ITracer tracer)
{
// 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();
tracer.RelatedInfo("Attempting to determine Git version for installation '{0}'", gitBinPath);
if (GitProcess.TryGetVersion(gitBinPath, out var gitVersion, out string gitVersionError))
{
tracer.RelatedInfo("Git installation '{0}' has version '{1}", gitBinPath, gitVersion);
gitFeatures = gitVersion.GetFeatures();
}
else
{
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);
}

return gitFeatures;
}

private string GetAlternatesPath(ScalarEnlistment enlistment)
{
return Path.Combine(enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.Objects.Info.Alternates);
Expand Down