Skip to content

Commit 610254f

Browse files
authored
Implement new credential helper for Git to avoid Git LFS warnings (#337)
1 parent de87177 commit 610254f

File tree

7 files changed

+264
-71
lines changed

7 files changed

+264
-71
lines changed

UET/Redpoint.Uet.Workspace/PhysicalGit/DefaultPhysicalGitCheckout.cs

Lines changed: 115 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ internal class DefaultPhysicalGitCheckout : IPhysicalGitCheckout
3535
private readonly IPackageManager _packageManager;
3636
private readonly ConcurrentDictionary<string, IReservationManager> _sharedReservationManagers;
3737
private readonly IGlobalMutexReservationManager _globalMutexReservationManager;
38+
private readonly IGitCredentialHelperProvider? _gitCredentialHelperProvider;
3839

3940
public DefaultPhysicalGitCheckout(
4041
ILogger<DefaultPhysicalGitCheckout> logger,
@@ -44,7 +45,8 @@ public DefaultPhysicalGitCheckout(
4445
ICredentialDiscovery credentialDiscovery,
4546
IReservationManagerFactory reservationManagerFactory,
4647
IWorldPermissionApplier worldPermissionApplier,
47-
IPackageManager packageManager)
48+
IPackageManager packageManager,
49+
IGitCredentialHelperProvider? gitCredentialHelperProvider = null)
4850
{
4951
_logger = logger;
5052
_pathResolver = pathResolver;
@@ -56,6 +58,7 @@ public DefaultPhysicalGitCheckout(
5658
_packageManager = packageManager;
5759
_sharedReservationManagers = new ConcurrentDictionary<string, IReservationManager>();
5860
_globalMutexReservationManager = _reservationManagerFactory.CreateGlobalMutexReservationManager();
61+
_gitCredentialHelperProvider = gitCredentialHelperProvider;
5962
}
6063

6164
/// <summary>
@@ -947,59 +950,62 @@ private async Task CheckoutTargetCommitAsync(
947950
int exitCode;
948951

949952
_logger.LogInformation($"Checking out target commit {resolvedReference.TargetCommit}...");
950-
exitCode = await FaultTolerantGitAsync(
951-
new ProcessSpecification
952-
{
953-
FilePath = gitContext.Git,
954-
Arguments =
955-
[
956-
"-C",
957-
repositoryPath,
958-
"-c",
959-
"advice.detachedHead=false",
960-
"checkout",
961-
"--progress",
962-
"-f",
963-
resolvedReference.TargetCommit,
964-
],
965-
WorkingDirectory = repositoryPath,
966-
EnvironmentVariables = gitContext.GitEnvs,
967-
},
968-
CaptureSpecification.Sanitized,
969-
cancellationToken).ConfigureAwait(false);
970-
if (exitCode != 0)
953+
using (var fetchEnvVars = gitContext.FetchEnvironmentVariablesFactory())
971954
{
972-
// Attempt to re-fetch LFS files, in case that was the error.
973-
if (await DetectGitLfsAndReconfigureIfNecessaryAsync(
974-
workspaceDescriptor,
975-
repositoryPath,
976-
repositoryUri,
977-
resolvedReference,
978-
gitContext,
979-
cancellationToken).ConfigureAwait(false) == GitLfsMode.Detected)
955+
exitCode = await FaultTolerantGitAsync(
956+
new ProcessSpecification
957+
{
958+
FilePath = gitContext.Git,
959+
Arguments =
960+
[
961+
"-C",
962+
repositoryPath,
963+
"-c",
964+
"advice.detachedHead=false",
965+
"checkout",
966+
"--progress",
967+
"-f",
968+
resolvedReference.TargetCommit,
969+
],
970+
WorkingDirectory = repositoryPath,
971+
EnvironmentVariables = fetchEnvVars.EnvironmentVariables,
972+
},
973+
CaptureSpecification.Sanitized,
974+
cancellationToken).ConfigureAwait(false);
975+
if (exitCode != 0)
980976
{
981-
// Re-attempt checkout...
982-
_logger.LogInformation($"Re-attempting check out of target commit {resolvedReference.TargetCommit}...");
983-
exitCode = await FaultTolerantGitAsync(
984-
new ProcessSpecification
985-
{
986-
FilePath = gitContext.Git,
987-
Arguments =
988-
[
989-
"-C",
990-
repositoryPath,
991-
"-c",
992-
"advice.detachedHead=false",
993-
"checkout",
994-
"--progress",
995-
"-f",
996-
resolvedReference.TargetCommit,
997-
],
998-
WorkingDirectory = repositoryPath,
999-
EnvironmentVariables = gitContext.GitEnvs,
1000-
},
1001-
CaptureSpecification.Sanitized,
1002-
cancellationToken).ConfigureAwait(false);
977+
// Attempt to re-fetch LFS files, in case that was the error.
978+
if (await DetectGitLfsAndReconfigureIfNecessaryAsync(
979+
workspaceDescriptor,
980+
repositoryPath,
981+
repositoryUri,
982+
resolvedReference,
983+
gitContext,
984+
cancellationToken).ConfigureAwait(false) == GitLfsMode.Detected)
985+
{
986+
// Re-attempt checkout...
987+
_logger.LogInformation($"Re-attempting check out of target commit {resolvedReference.TargetCommit}...");
988+
exitCode = await FaultTolerantGitAsync(
989+
new ProcessSpecification
990+
{
991+
FilePath = gitContext.Git,
992+
Arguments =
993+
[
994+
"-C",
995+
repositoryPath,
996+
"-c",
997+
"advice.detachedHead=false",
998+
"checkout",
999+
"--progress",
1000+
"-f",
1001+
resolvedReference.TargetCommit,
1002+
],
1003+
WorkingDirectory = repositoryPath,
1004+
EnvironmentVariables = fetchEnvVars.EnvironmentVariables,
1005+
},
1006+
CaptureSpecification.Sanitized,
1007+
cancellationToken).ConfigureAwait(false);
1008+
}
10031009
}
10041010
}
10051011
if (exitCode != 0)
@@ -1428,6 +1434,9 @@ private async Task CheckoutSubmoduleAsync(
14281434
_logger.LogInformation($"Submodule directory: {submoduleContentPath}");
14291435
_logger.LogInformation($"Submodule exclude on macOS: {(submodule.ExcludeOnMac ? "yes" : "no")}");
14301436

1437+
// Get the submodule URI and environment variables factory.
1438+
var (uri, fetchEnvironmentVariablesFactory) = ComputeRepositoryUriAndCredentials(submoduleUrl);
1439+
14311440
// Check if we already have the target commit in history. If we do, skip fetch.
14321441
var gitTypeStringBuilder = new StringBuilder();
14331442
_ = await _processExecutor.ExecuteAsync(
@@ -1450,7 +1459,6 @@ private async Task CheckoutSubmoduleAsync(
14501459
{
14511460
// Fetch the commit that we need.
14521461
_logger.LogInformation($"Fetching submodule {submodule.Path} from remote server...");
1453-
var (uri, fetchEnvironmentVariablesFactory) = ComputeRepositoryUriAndCredentials(submoduleUrl);
14541462
using (var fetchEnvVars = fetchEnvironmentVariablesFactory())
14551463
{
14561464
exitCode = await FaultTolerantGitAsync(
@@ -1482,24 +1490,27 @@ private async Task CheckoutSubmoduleAsync(
14821490

14831491
// Checkout the target commit.
14841492
_logger.LogInformation($"Checking out submodule {submodule.Path} target commit {submoduleCommit}...");
1485-
exitCode = await FaultTolerantGitAsync(
1486-
new ProcessSpecification
1487-
{
1488-
FilePath = git,
1489-
Arguments =
1490-
[
1491-
"-c",
1492-
"advice.detachedHead=false",
1493-
"checkout",
1494-
"--progress",
1495-
"-f",
1496-
submoduleCommit,
1497-
],
1498-
WorkingDirectory = submoduleContentPath,
1499-
EnvironmentVariables = gitEnvs,
1500-
},
1501-
CaptureSpecification.Sanitized,
1502-
cancellationToken).ConfigureAwait(false);
1493+
using (var fetchEnvVars = fetchEnvironmentVariablesFactory())
1494+
{
1495+
exitCode = await FaultTolerantGitAsync(
1496+
new ProcessSpecification
1497+
{
1498+
FilePath = git,
1499+
Arguments =
1500+
[
1501+
"-c",
1502+
"advice.detachedHead=false",
1503+
"checkout",
1504+
"--progress",
1505+
"-f",
1506+
submoduleCommit,
1507+
],
1508+
WorkingDirectory = submoduleContentPath,
1509+
EnvironmentVariables = fetchEnvVars.EnvironmentVariables,
1510+
},
1511+
CaptureSpecification.Sanitized,
1512+
cancellationToken).ConfigureAwait(false);
1513+
}
15031514
if (exitCode != 0)
15041515
{
15051516
throw new InvalidOperationException($"'git checkout' for {submodule.Path} exited with non-zero exit code {exitCode}");
@@ -1716,6 +1727,39 @@ private async Task<IReservation> GetSharedGitDependenciesPath(GitWorkspaceDescri
17161727
{
17171728
// Parse the repository URL.
17181729
var uri = new Uri(repositoryUrl);
1730+
1731+
// If this is a HTTPS URL, and we have a credential helper provider, use that
1732+
// to provide credentials.
1733+
if (_gitCredentialHelperProvider != null &&
1734+
(uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp))
1735+
{
1736+
var envVars = new Dictionary<string, string>
1737+
{
1738+
{ "GIT_ASK_YESNO", "false" },
1739+
{ "GIT_CONFIG_COUNT", "1" },
1740+
{ "GIT_CONFIG_KEY_0", "credential.helper" },
1741+
{ "GIT_CONFIG_VALUE_0", $"{_gitCredentialHelperProvider.FilePath} {_gitCredentialHelperProvider.Arguments.Select(x => x.LogicalValue)}" },
1742+
};
1743+
var uriComponents = uri.UserInfo.Split(':', 2);
1744+
var uriHost = uri.Host.Replace(".", "_", StringComparison.Ordinal);
1745+
if (uriComponents.Length == 1)
1746+
{
1747+
envVars[$"REDPOINT_CREDENTIAL_DISCOVERY_USERNAME_{uriHost}"] = uriComponents[0];
1748+
}
1749+
else if (uriComponents.Length == 2)
1750+
{
1751+
envVars[$"REDPOINT_CREDENTIAL_DISCOVERY_USERNAME_{uriHost}"] = uriComponents[0];
1752+
envVars[$"REDPOINT_CREDENTIAL_DISCOVERY_PASSWORD_{uriHost}"] = uriComponents[1];
1753+
}
1754+
1755+
var builder = new UriBuilder(uri);
1756+
builder.UserName = null;
1757+
builder.Password = null;
1758+
uri = builder.Uri;
1759+
return (uri, () => new GitTemporaryEnvVarsForFetch(envVars));
1760+
}
1761+
1762+
// Fallback to direct credential lookup.
17191763
try
17201764
{
17211765
var uriCredential = _credentialDiscovery.GetGitCredential(repositoryUrl);

UET/Redpoint.Uet.Workspace/PhysicalGit/GitTemporaryEnvVarsForFetch.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ public GitTemporaryEnvVarsForFetch()
1717
};
1818
}
1919

20+
public GitTemporaryEnvVarsForFetch(Dictionary<string, string> envVars)
21+
{
22+
_path = null;
23+
_envVars = envVars;
24+
}
25+
2026
public GitTemporaryEnvVarsForFetch(string privateKey)
2127
{
2228
_path = Path.GetTempFileName();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Redpoint.Uet.Workspace.PhysicalGit
2+
{
3+
using Redpoint.ProcessExecution;
4+
using System.Collections.Generic;
5+
6+
public interface IGitCredentialHelperProvider
7+
{
8+
string FilePath { get; }
9+
10+
IEnumerable<LogicalProcessArgument> Arguments { get; }
11+
}
12+
}

UET/uet/Commands/CommandExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
using Redpoint.GrpcPipes.Transport.Tcp;
3737
using Redpoint.PackageManagement;
3838
using Redpoint.Uet.Core.BugReport;
39+
using Redpoint.Uet.Workspace.PhysicalGit;
3940

4041
internal static class CommandExtensions
4142
{
@@ -97,6 +98,10 @@ private static void AddGeneralServices(IServiceCollection services, LogLevel min
9798
services.AddCredentialDiscovery();
9899
services.AddSingleton<ISelfLocation, DefaultSelfLocation>();
99100
services.AddSingleton<IReleaseVersioning, DefaultReleaseVersioning>();
101+
if (Environment.GetEnvironmentVariable("UET_NEW_GIT_CREDENTIAL_HELPER_OPTIN") == "1")
102+
{
103+
services.AddSingleton<IGitCredentialHelperProvider, DefaultGitCredentialHelperProvider>();
104+
}
100105
services.AddUba();
101106
}
102107

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
namespace UET.Commands.Internal.GitCredentialHelper
2+
{
3+
using Redpoint.CredentialDiscovery;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.CommandLine;
7+
using System.CommandLine.Invocation;
8+
using System.Linq;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
12+
internal sealed class GitCredentialHelperCommand
13+
{
14+
internal sealed class Options
15+
{
16+
public Argument<string> Operation;
17+
18+
public Options()
19+
{
20+
Operation = new Argument<string>("operation");
21+
Operation.AddCompletions("get", "store", "erase");
22+
}
23+
}
24+
25+
public static Command CreateGitCredentialHelperCommand()
26+
{
27+
var options = new Options();
28+
var command = new Command(
29+
"git-credential-helper",
30+
"Used as the credential helper for Git when running on a build server. This allows us to provide credentials from environment variables without setting the username and password in remote URLs.");
31+
command.AddAllOptions(options);
32+
command.AddCommonHandler<GitCredentialHelperCommandInstance>(options);
33+
return command;
34+
}
35+
36+
private sealed class GitCredentialHelperCommandInstance : ICommandInstance
37+
{
38+
private readonly ICredentialDiscovery _credentialDiscovery;
39+
private readonly Options _options;
40+
41+
public GitCredentialHelperCommandInstance(
42+
ICredentialDiscovery credentialDiscovery,
43+
Options options)
44+
{
45+
_credentialDiscovery = credentialDiscovery;
46+
_options = options;
47+
}
48+
49+
public Task<int> ExecuteAsync(InvocationContext context)
50+
{
51+
var operation = context.ParseResult.GetValueForArgument(_options.Operation);
52+
if (operation != "get")
53+
{
54+
// We don't handle anything other than "get".
55+
return Task.FromResult(0);
56+
}
57+
58+
// Read input request.
59+
var input = new Dictionary<string, string>();
60+
string? line;
61+
do
62+
{
63+
line = Console.ReadLine();
64+
if (!string.IsNullOrWhiteSpace(line))
65+
{
66+
var components = line.Split('=', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
67+
input[components[0]] = components[1];
68+
}
69+
} while (!string.IsNullOrWhiteSpace(line));
70+
71+
// Attempt to perform credential discovery.
72+
try
73+
{
74+
var credential = _credentialDiscovery.GetGitCredential($"{input["protocol"]}://{input["host"]}/{input["path"]}");
75+
if (!string.IsNullOrWhiteSpace(credential.Username) &&
76+
!string.IsNullOrWhiteSpace(credential.Password))
77+
{
78+
Console.WriteLine($"protocol={input["protocol"]}");
79+
Console.WriteLine($"host={input["host"]}");
80+
Console.WriteLine($"username={credential.Username}");
81+
Console.WriteLine($"password={credential.Password}");
82+
83+
// Found credential for HTTP/HTTPS.
84+
return Task.FromResult(0);
85+
}
86+
87+
// The git-credential flow can't handle SSH.
88+
return Task.FromResult(0);
89+
}
90+
catch (UnableToDiscoverCredentialException)
91+
{
92+
// We don't have a useful credential.
93+
return Task.FromResult(0);
94+
}
95+
}
96+
}
97+
98+
}
99+
}

0 commit comments

Comments
 (0)