Skip to content

Commit 6990f1e

Browse files
committed
linuxsettings: implement default settings for Linux
1 parent b62021f commit 6990f1e

File tree

10 files changed

+294
-5
lines changed

10 files changed

+294
-5
lines changed

docs/enterprise-config.md

+30-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,36 @@ $ defaults read git-credential-manager configuration
8888

8989
## Linux
9090

91-
Default configuration setting stores has not been implemented.
91+
Default settings values come from the `/etc/git-credential-manager/config.d`
92+
directory. Each file in this directory represents a single settings dictionary.
93+
94+
All files in this directory are read at runtime and merged into a single
95+
collection of settings, in the order they are read from the file system.
96+
To provide a stable ordering, it is recommended to prefix each filename with a
97+
number, e.g. `42-my-settings`.
98+
99+
The format of each file is a simple set of key/value pairs, separated by an
100+
`=` sign, and each line separated by a line-feed (`\n`, LF) character.
101+
Comments are identified by a `#` character at the beginning of a line.
102+
103+
For example:
104+
105+
```text
106+
# /etc/git-credential-manager/config.d/00-example1
107+
credential.noguiprompt=0
108+
```
109+
110+
```text
111+
# /etc/git-credential-manager/config.d/01-example2
112+
credential.trace=true
113+
credential.traceMsAuth=true
114+
```
115+
116+
All settings names and values are the same as the [Git configuration][config]
117+
reference.
118+
119+
> Note: These files are read once at startup. If changes are made to these files
120+
they will not be reflected in an already running process.
92121

93122
[environment]: environment.md
94123
[config]: configuration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Collections.Generic;
2+
using GitCredentialManager.Interop.Linux;
3+
using GitCredentialManager.Tests.Objects;
4+
using Xunit;
5+
6+
namespace GitCredentialManager.Tests.Interop.Linux;
7+
8+
public class LinuxConfigParserTests
9+
{
10+
[Fact]
11+
public void LinuxConfigParser_Parse()
12+
{
13+
const string contents =
14+
"""
15+
#
16+
# This is a config file complete with comments
17+
# and empty..
18+
19+
# lines, as well as lines with..
20+
#
21+
# only whitespace (like above ^), and..
22+
invalid lines like this one, not a comment
23+
# Here's the first real properties:
24+
core.overrideMe=This is the first config value
25+
baz.specialChars=I contain special chars like = in my value # this is a comment
26+
# and let's have with a comment that also contains a = in side
27+
#
28+
core.overrideMe=This is the second config value
29+
bar.scope.foo=123456
30+
core.overrideMe=This is the correct value
31+
###### comments that start ## with whitespace and extra ## inside
32+
strings.one="here we have a dq string"
33+
strings.two='here we have a sq string'
34+
strings.three= 'here we have another sq string' # have another sq string
35+
strings.four="this has 'nested quotes' inside"
36+
strings.five='mixed "quotes" the other way around'
37+
strings.six='this has an \'escaped\' set of quotes'
38+
""";
39+
40+
var expected = new Dictionary<string, string>
41+
{
42+
["core.overrideMe"] = "This is the correct value",
43+
["bar.scope.foo"] = "123456",
44+
["baz.specialChars"] = "I contain special chars like = in my value",
45+
["strings.one"] = "here we have a dq string",
46+
["strings.two"] = "here we have a sq string",
47+
["strings.three"] = "here we have another sq string",
48+
["strings.four"] = "this has 'nested quotes' inside",
49+
["strings.five"] = "mixed \"quotes\" the other way around",
50+
["strings.six"] = "this has an \\'escaped\\' set of quotes",
51+
};
52+
53+
var parser = new LinuxConfigParser(new NullTrace());
54+
55+
Assert.Equal(expected, parser.Parse(contents));
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Collections.Generic;
2+
using GitCredentialManager.Interop.Linux;
3+
using GitCredentialManager.Tests.Objects;
4+
using Xunit;
5+
6+
namespace GitCredentialManager.Tests.Interop.Linux;
7+
8+
public class LinuxSettingsTests
9+
{
10+
[LinuxFact]
11+
public void LinuxSettings_TryGetExternalDefault_CombinesFiles()
12+
{
13+
var env = new TestEnvironment();
14+
var git = new TestGit();
15+
var trace = new NullTrace();
16+
var fs = new TestFileSystem();
17+
18+
var utf8 = EncodingEx.UTF8NoBom;
19+
20+
fs.Directories = new HashSet<string>
21+
{
22+
"/",
23+
"/etc",
24+
"/etc/git-credential-manager",
25+
"/etc/git-credential-manager/config.d"
26+
};
27+
28+
const string config1 = "core.overrideMe=value1";
29+
const string config2 = "core.overrideMe=value2";
30+
const string config3 = "core.overrideMe=value3";
31+
32+
fs.Files = new Dictionary<string, byte[]>
33+
{
34+
["/etc/git-credential-manager/config.d/01-first"] = utf8.GetBytes(config1),
35+
["/etc/git-credential-manager/config.d/02-second"] = utf8.GetBytes(config2),
36+
["/etc/git-credential-manager/config.d/03-third"] = utf8.GetBytes(config3),
37+
};
38+
39+
var settings = new LinuxSettings(env, git, trace, fs);
40+
41+
bool result = settings.TryGetExternalDefault(
42+
"core", null, "overrideMe", out string value);
43+
44+
Assert.True(result);
45+
Assert.Equal("value3", value);
46+
}
47+
}

src/shared/Core/CommandContext.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public CommandContext()
148148
gitPath,
149149
FileSystem.GetCurrentDirectory()
150150
);
151-
Settings = new Settings(Environment, Git);
151+
Settings = new LinuxSettings(Environment, Git, Trace, FileSystem);
152152
}
153153
else
154154
{

src/shared/Core/Constants.cs

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public static class Constants
1515
public const string AuthorityIdAuto = "auto";
1616

1717
public const string GcmDataDirectoryName = ".gcm";
18+
public const string LinuxAppDefaultsDirectoryPath = "/etc/git-credential-manager/config.d";
1819

1920
public const string MacOSBundleId = "git-credential-manager";
2021
public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.RegularExpressions;
4+
5+
namespace GitCredentialManager.Interop.Linux;
6+
7+
public class LinuxConfigParser
8+
{
9+
#if NETFRAMEWORK
10+
private const string SQ = "'";
11+
private const string DQ = "\"";
12+
private const string Hash = "#";
13+
#else
14+
private const char SQ = '\'';
15+
private const char DQ = '"';
16+
private const char Hash = '#';
17+
#endif
18+
19+
private static readonly Regex LineRegex = new(@"^\s*(?<key>[a-zA-Z0-9\.-]+)\s*=\s*(?<value>.+?)\s*(?:#.*)?$");
20+
21+
private readonly ITrace _trace;
22+
23+
public LinuxConfigParser(ITrace trace)
24+
{
25+
EnsureArgument.NotNull(trace, nameof(trace));
26+
27+
_trace = trace;
28+
}
29+
30+
public IDictionary<string, string> Parse(string content)
31+
{
32+
var result = new Dictionary<string, string>(GitConfigurationKeyComparer.Instance);
33+
34+
IEnumerable<string> lines = content.Split(['\n'], StringSplitOptions.RemoveEmptyEntries);
35+
36+
foreach (string line in lines)
37+
{
38+
// Ignore empty lines or full-line comments
39+
var trimmedLine = line.Trim();
40+
if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith(Hash))
41+
continue;
42+
43+
var match = LineRegex.Match(trimmedLine);
44+
if (!match.Success)
45+
{
46+
_trace.WriteLine($"Invalid config line format: {line}");
47+
continue;
48+
}
49+
50+
string key = match.Groups["key"].Value;
51+
string value = match.Groups["value"].Value;
52+
53+
// Remove enclosing quotes from the value, if any
54+
if ((value.StartsWith(DQ) && value.EndsWith(DQ)) || (value.StartsWith(SQ) && value.EndsWith(SQ)))
55+
value = value.Substring(1, value.Length - 2);
56+
57+
result[key] = value;
58+
}
59+
60+
return result;
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text;
5+
using Avalonia.Markup.Xaml.MarkupExtensions;
6+
7+
namespace GitCredentialManager.Interop.Linux;
8+
9+
public class LinuxSettings : Settings
10+
{
11+
private readonly ITrace _trace;
12+
private readonly IFileSystem _fs;
13+
14+
private IDictionary<string, string> _extConfigCache;
15+
16+
/// <summary>
17+
/// Reads settings from Git configuration, environment variables, and defaults from the
18+
/// /etc/git-credential-manager.d app configuration directory.
19+
/// </summary>
20+
public LinuxSettings(IEnvironment environment, IGit git, ITrace trace, IFileSystem fs)
21+
: base(environment, git)
22+
{
23+
EnsureArgument.NotNull(trace, nameof(trace));
24+
EnsureArgument.NotNull(fs, nameof(fs));
25+
26+
_trace = trace;
27+
_fs = fs;
28+
29+
PlatformUtils.EnsureLinux();
30+
}
31+
32+
protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value)
33+
{
34+
value = null;
35+
36+
_extConfigCache ??= ReadExternalConfiguration();
37+
38+
string name = string.IsNullOrWhiteSpace(scope)
39+
? $"{section}.{property}"
40+
: $"{section}.{scope}.{property}";
41+
42+
// Check if the setting exists in the configuration
43+
if (!_extConfigCache?.TryGetValue(name, out value) ?? false)
44+
{
45+
// No property exists (or failed to read config)
46+
return false;
47+
}
48+
49+
_trace.WriteLine($"Default setting found in app configuration directory: {name}={value}");
50+
return true;
51+
}
52+
53+
private IDictionary<string, string> ReadExternalConfiguration()
54+
{
55+
try
56+
{
57+
// Check for system-wide config files in /etc/git-credential-manager/config.d and concatenate them together
58+
// in alphabetical order to form a single configuration.
59+
const string configDir = Constants.LinuxAppDefaultsDirectoryPath;
60+
if (!_fs.DirectoryExists(configDir))
61+
{
62+
// No configuration directory exists
63+
return null;
64+
}
65+
66+
// Get all the files in the configuration directory
67+
IEnumerable<string> files = _fs.EnumerateFiles(configDir, "*");
68+
69+
// Read the contents of each file and concatenate them together
70+
var combinedFile = new StringBuilder();
71+
foreach (string file in files)
72+
{
73+
using Stream stream = _fs.OpenFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
74+
using var reader = new StreamReader(stream);
75+
string contents = reader.ReadToEnd();
76+
combinedFile.Append(contents);
77+
combinedFile.Append('\n');
78+
}
79+
80+
var parser = new LinuxConfigParser(_trace);
81+
82+
return parser.Parse(combinedFile.ToString());
83+
}
84+
catch (Exception ex)
85+
{
86+
// Reading defaults is not critical to the operation of the application
87+
// so we can ignore any errors and just log the failure.
88+
_trace.WriteLine("Failed to read default setting from app configuration directory.");
89+
_trace.WriteException(ex);
90+
return null;
91+
}
92+
}
93+
}

src/shared/Core/Interop/MacOS/MacOSSettings.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public MacOSSettings(IEnvironment environment, IGit git, ITrace trace)
1919
PlatformUtils.EnsureMacOS();
2020
}
2121

22-
protected override bool TryGetExternalDefault(string section, string scope, string property, out string value)
22+
protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value)
2323
{
2424
value = null;
2525

src/shared/Core/Interop/Windows/WindowsSettings.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public WindowsSettings(IEnvironment environment, IGit git, ITrace trace)
1717
PlatformUtils.EnsureWindows();
1818
}
1919

20-
protected override bool TryGetExternalDefault(string section, string scope, string property, out string value)
20+
protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value)
2121
{
2222
value = null;
2323

src/shared/Core/Settings.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ public IEnumerable<string> GetSettingValues(string envarName, string section, st
489489
/// <param name="property">Configuration property name.</param>
490490
/// <param name="value">Value of the configuration setting, or null.</param>
491491
/// <returns>True if a default setting has been set, false otherwise.</returns>
492-
protected virtual bool TryGetExternalDefault(string section, string scope, string property, out string value)
492+
protected internal virtual bool TryGetExternalDefault(string section, string scope, string property, out string value)
493493
{
494494
value = null;
495495
return false;

0 commit comments

Comments
 (0)