Skip to content

Commit b01af4f

Browse files
committed
Ensure we don't depend on an installed git
We further remove dependency on git when reading settings. Only global settings are supported now. Force fake git path on tests to ensure no dependency across all OSes.
1 parent 135722a commit b01af4f

File tree

4 files changed

+145
-68
lines changed

4 files changed

+145
-68
lines changed

src/CredentialManager/CredentialManager.cs

Lines changed: 22 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.IO;
4-
using System.Runtime.InteropServices;
5-
using DotNetConfig;
6-
using KnownEnvars = GitCredentialManager.Constants.EnvironmentVariables;
7-
using KnownGitCfg = GitCredentialManager.Constants.GitConfiguration;
3+
using GitCredentialManager.Interop.Windows;
84

95
namespace GitCredentialManager;
106

@@ -34,7 +30,10 @@ public static ICredentialStore Create(string? @namespace = default)
3430

3531
class CommandContextWrapper(CommandContext context, string? @namespace) : ICommandContext
3632
{
37-
readonly ISettings settings = new SettingsWrapper(context.Settings, @namespace);
33+
readonly ISettings settings = new SettingsWrapper(
34+
context.Settings is WindowsSettings ?
35+
new NoGitWindowsSettings(context.Environment, context.Git, context.Trace) :
36+
new NoGitSettings(context.Environment, context.Git), @namespace);
3837

3938
public ISettings Settings => settings;
4039

@@ -60,7 +59,7 @@ class CommandContextWrapper(CommandContext context, string? @namespace) : IComma
6059

6160
public IHttpClientFactory HttpClientFactory => ((ICommandContext)context).HttpClientFactory;
6261

63-
public IGit Git => ((ICommandContext)context).Git;
62+
public IGit Git => new NoGit(context.Git);
6463

6564
public IEnvironment Environment => ((ICommandContext)context).Environment;
6665

@@ -71,65 +70,27 @@ class CommandContextWrapper(CommandContext context, string? @namespace) : IComma
7170
#endregion
7271
}
7372

73+
class NoGitSettings(IEnvironment environment, IGit git) : Settings(environment, new NoGit(git)) { }
74+
75+
class NoGitWindowsSettings(IEnvironment environment, IGit git, ITrace trace) : WindowsSettings(environment, new NoGit(git), trace) { }
76+
7477
class SettingsWrapper(ISettings settings, string? @namespace) : ISettings
7578
{
76-
static readonly Config gitconfig;
77-
78-
static SettingsWrapper()
79-
{
80-
string homeDir;
81-
82-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
83-
{
84-
homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
85-
}
86-
else
87-
{
88-
// On Linux/Mac it's $HOME
89-
homeDir = Environment.GetEnvironmentVariable("HOME")
90-
?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
91-
}
92-
93-
gitconfig = Config.Build(Path.Combine(homeDir, ".gitconfig"));
94-
}
95-
96-
static bool TryGetWrappedSetting(string envarName, string section, string property, out string value)
97-
{
98-
if (envarName != null && Environment.GetEnvironmentVariable(envarName) is { Length: > 0 } envvar)
99-
{
100-
value = envvar;
101-
return true;
102-
}
103-
104-
return gitconfig.TryGetString(section, property, out value);
105-
}
106-
107-
// Overriden namespace to scope credential operations.
108-
public string CredentialNamespace => @namespace ?? (
109-
TryGetWrappedSetting(KnownEnvars.GcmCredNamespace,
110-
KnownGitCfg.Credential.SectionName, KnownGitCfg.Credential.CredNamespace,
111-
out var ns) ? ns : Constants.DefaultCredentialNamespace);
112-
113-
public string CredentialBackingStore =>
114-
TryGetWrappedSetting(
115-
KnownEnvars.GcmCredentialStore,
116-
KnownGitCfg.Credential.SectionName,
117-
KnownGitCfg.Credential.CredentialStore,
118-
out string credStore)
119-
? credStore
120-
: null!;
121-
122-
public bool UseMsAuthDefaultAccount =>
123-
TryGetWrappedSetting(
124-
KnownEnvars.MsAuthUseDefaultAccount,
125-
KnownGitCfg.Credential.SectionName,
126-
KnownGitCfg.Credential.MsAuthUseDefaultAccount,
127-
out string str)
128-
? str.IsTruthy()
129-
: PlatformUtils.IsDevBox(); // default to true in DevBox environment
79+
public string CredentialNamespace => @namespace ?? settings.CredentialNamespace;
13080

13181
#region pass-through impl.
13282

83+
public string CredentialBackingStore => settings.CredentialBackingStore;
84+
85+
public bool UseMsAuthDefaultAccount => settings.UseMsAuthDefaultAccount;
86+
87+
public IEnumerable<string> GetSettingValues(string envarName, string section, string property, bool isPath)
88+
=> settings.GetSettingValues(envarName, section, property, isPath);
89+
90+
public bool GetTracingEnabled(out string value) => settings.GetTracingEnabled(out value);
91+
public bool TryGetPathSetting(string envarName, string section, string property, out string value) => settings.TryGetPathSetting(envarName, section, property, out value);
92+
public bool TryGetSetting(string envarName, string section, string property, out string value) => settings.TryGetSetting(envarName, section, property, out value);
93+
13394
public Uri RemoteUri { get => settings.RemoteUri; set => settings.RemoteUri = value; }
13495

13596
public bool IsDebuggingEnabled => settings.IsDebuggingEnabled;
@@ -172,12 +133,7 @@ static bool TryGetWrappedSetting(string envarName, string section, string proper
172133

173134
public void Dispose() => settings.Dispose();
174135
public ProxyConfiguration GetProxyConfiguration() => settings.GetProxyConfiguration();
175-
public IEnumerable<string> GetSettingValues(string envarName, string section, string property, bool isPath) => settings.GetSettingValues(envarName, section, property, isPath);
176136
public Trace2Settings GetTrace2Settings() => settings.GetTrace2Settings();
177-
public bool GetTracingEnabled(out string value) => settings.GetTracingEnabled(out value);
178-
public bool TryGetPathSetting(string envarName, string section, string property, out string value) => settings.TryGetPathSetting(envarName, section, property, out value);
179-
public bool TryGetSetting(string envarName, string section, string property, out string value) => settings.TryGetSetting(envarName, section, property, out value);
180-
181137
#endregion
182138
}
183139
}

src/CredentialManager/NoGit.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Runtime.InteropServices;
6+
using System.Threading.Tasks;
7+
using DotNetConfig;
8+
9+
namespace GitCredentialManager;
10+
11+
/// <summary>
12+
/// Implements the <see cref="IGit"/> interface by directly reading settings
13+
/// from the global config using DotNetConfig instead of depending on a git installation.
14+
/// </summary>
15+
class NoGit(IGit git) : IGit
16+
{
17+
static readonly Config gitconfig;
18+
19+
static NoGit()
20+
{
21+
string homeDir;
22+
23+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
24+
{
25+
homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
26+
}
27+
else
28+
{
29+
// On Linux/Mac it's $HOME
30+
homeDir = Environment.GetEnvironmentVariable("HOME")
31+
?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
32+
}
33+
34+
gitconfig = Config.Build(Path.Combine(homeDir, ".gitconfig"));
35+
}
36+
37+
public IGitConfiguration GetConfiguration() => new GitConfig(gitconfig);
38+
39+
public GitVersion Version => throw new NotSupportedException();
40+
public ChildProcess CreateProcess(string args) => throw new NotSupportedException();
41+
public string GetCurrentRepository() => throw new NotSupportedException();
42+
public IEnumerable<GitRemote> GetRemotes() => throw new NotSupportedException();
43+
44+
/// <summary>Only allows git interaction for the git cache GCM store.</summary>
45+
public Task<IDictionary<string, string>> InvokeHelperAsync(string args, IDictionary<string, string> standardInput)
46+
=> Environment.GetEnvironmentVariable("GCM_CREDENTIAL_STORE") is "cache" ? git.InvokeHelperAsync(args, standardInput) :
47+
throw new NotSupportedException();
48+
49+
public bool IsInsideRepository() => throw new NotSupportedException();
50+
51+
class GitConfig(Config config) : IGitConfiguration
52+
{
53+
public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCallback cb)
54+
{
55+
foreach (var entry in config)
56+
{
57+
cb(new GitConfigurationEntry(entry.Key, entry.RawValue));
58+
}
59+
}
60+
61+
public IEnumerable<string> GetAll(GitConfigurationLevel level, GitConfigurationType type, string name)
62+
{
63+
foreach (var entry in config)
64+
{
65+
if (string.Equals(entry.Key, name, StringComparison.OrdinalIgnoreCase))
66+
yield return entry.RawValue ?? "";
67+
}
68+
}
69+
70+
public IEnumerable<string> GetRegex(GitConfigurationLevel level, GitConfigurationType type, string nameRegex, string valueRegex)
71+
{
72+
foreach (var entry in config.GetRegex(nameRegex, valueRegex))
73+
{
74+
yield return entry.RawValue ?? "";
75+
}
76+
}
77+
78+
public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, string name, out string value)
79+
{
80+
value = GetAll(level, type, name).FirstOrDefault();
81+
return value is not null;
82+
}
83+
84+
#region NotSupported (Write Operations)
85+
public void Add(GitConfigurationLevel level, string name, string value) => throw new NotSupportedException("Configuration is read-only");
86+
public void ReplaceAll(GitConfigurationLevel level, string nameRegex, string valueRegex, string value) => throw new NotSupportedException("Configuration is read-only");
87+
public void Set(GitConfigurationLevel level, string name, string value) => throw new NotSupportedException("Configuration is read-only");
88+
public void Unset(GitConfigurationLevel level, string name) => throw new NotSupportedException("Configuration is read-only");
89+
public void UnsetAll(GitConfigurationLevel level, string name, string valueRegex) => throw new NotSupportedException("Configuration is read-only");
90+
#endregion
91+
}
92+
}

src/Tests/EndToEnd.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,30 @@ namespace Tests;
1010
/// </summary>
1111
public class EndToEnd : IDisposable
1212
{
13-
public EndToEnd() => Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", null);
13+
public EndToEnd()
14+
{
15+
Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", null);
16+
17+
// Sets fake git path to ensure we're not inadvertently requiring a full git installation
18+
var file = "git";
19+
if (OperatingSystem.IsWindows())
20+
file += ".exe";
21+
22+
var dir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
23+
? Path.GetTempPath()
24+
: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
25+
26+
if (!Directory.Exists(dir))
27+
Directory.CreateDirectory(dir);
28+
29+
var path = Path.Combine(dir, file);
30+
31+
if (!File.Exists(path))
32+
File.WriteAllBytes(path, []);
33+
34+
// Sets fake git path to ensure we're not inadvertently requiring a full git installation
35+
Environment.SetEnvironmentVariable("GIT_EXEC_PATH", dir);
36+
}
1437

1538
public void Dispose() => Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", null);
1639

@@ -35,6 +58,8 @@ public void PlainTextStore()
3558
public void GitCacheStore()
3659
{
3760
Environment.SetEnvironmentVariable("GCM_CREDENTIAL_STORE", "cache");
61+
// Git cache store does depend on git being present, so we set remove the fake path.
62+
Environment.SetEnvironmentVariable("GIT_EXEC_PATH", null);
3863
Run();
3964
}
4065

@@ -84,5 +109,9 @@ void Run()
84109
store.AddOrUpdate("https://test.com", usr, pwd);
85110

86111
Assert.Equal(pwd, store.Get("https://test.com", usr).Password);
112+
113+
store.Remove("https://test.com", usr);
114+
115+
Assert.Null(store.Get("https://test.com", usr));
87116
}
88117
}

src/Tests/Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
1616
<PackageReference Include="xunit" Version="2.5.3" />
1717
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
18-
<PackageReference Include="Devlooped.CredentialManager" Version="42.42.42-main.*" Condition="!$(CI)" />
18+
<PackageReference Include="Devlooped.CredentialManager" Version="42.*" Condition="!$(CI)" />
1919
<PackageReference Include="Devlooped.CredentialManager" Version="$(Version)" Condition="$(CI) And Exists('$(PackageOutputPath)')" />
2020
</ItemGroup>
2121

0 commit comments

Comments
 (0)