Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
702 changes: 356 additions & 346 deletions .azure-pipelines/release.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
shell: bash
run: |
mkdir -p artifacts/bin
mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}/gcm*.exe artifacts/
mv out/windows/Installer.Windows/bin/Release/net472/gcm*.exe artifacts/
mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }} artifacts/bin/
cp out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}.sym/* artifacts/bin/${{ matrix.runtime }}/

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.7.0.0
2.7.1.0
31 changes: 30 additions & 1 deletion docs/enterprise-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,36 @@ $ defaults read git-credential-manager configuration

## Linux

Default configuration setting stores has not been implemented.
Default settings values come from the `/etc/git-credential-manager/config.d`
directory. Each file in this directory represents a single settings dictionary.

All files in this directory are read at runtime and merged into a single
collection of settings, in the order they are read from the file system.
To provide a stable ordering, it is recommended to prefix each filename with a
number, e.g. `42-my-settings`.

The format of each file is a simple set of key/value pairs, separated by an
`=` sign, and each line separated by a line-feed (`\n`, LF) character.
Comments are identified by a `#` character at the beginning of a line.

For example:

```text
# /etc/git-credential-manager/config.d/00-example1
credential.noguiprompt=0
```

```text
# /etc/git-credential-manager/config.d/01-example2
credential.trace=true
credential.traceMsAuth=true
```

All settings names and values are the same as the [Git configuration][config]
reference.

> Note: These files are read once at startup. If changes are made to these files
they will not be reflected in an already running process.

[environment]: environment.md
[config]: configuration.md
6 changes: 3 additions & 3 deletions src/linux/Packaging.Linux/pack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ if test -z "$RUNTIME"; then
fi

TAROUT="$OUTPUT_ROOT/tar"
TARBALL="$TAROUT/gcm-$RUNTIME.$VERSION.tar.gz"
SYMTARBALL="$TAROUT/gcm-$RUNTIME.$VERSION-symbols.tar.gz"
TARBALL="$TAROUT/gcm-$RUNTIME-$VERSION.tar.gz"
SYMTARBALL="$TAROUT/gcm-$RUNTIME-$VERSION-symbols.tar.gz"

DEBOUT="$OUTPUT_ROOT/deb"
DEBROOT="$DEBOUT/root"
DEBPKG="$DEBOUT/gcm-$RUNTIME.$VERSION.deb"
DEBPKG="$DEBOUT/gcm-$RUNTIME-$VERSION.deb"
mkdir -p "$DEBROOT"

# Set full read, write, execute permissions for owner and just read and execute permissions for group and other
Expand Down
57 changes: 57 additions & 0 deletions src/shared/Core.Tests/Interop/Linux/LinuxConfigParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using GitCredentialManager.Interop.Linux;
using GitCredentialManager.Tests.Objects;
using Xunit;

namespace GitCredentialManager.Tests.Interop.Linux;

public class LinuxConfigParserTests
{
[Fact]
public void LinuxConfigParser_Parse()
{
const string contents =
"""
#
# This is a config file complete with comments
# and empty..

# lines, as well as lines with..
#
# only whitespace (like above ^), and..
invalid lines like this one, not a comment
# Here's the first real properties:
core.overrideMe=This is the first config value
baz.specialChars=I contain special chars like = in my value # this is a comment
# and let's have with a comment that also contains a = in side
#
core.overrideMe=This is the second config value
bar.scope.foo=123456
core.overrideMe=This is the correct value
###### comments that start ## with whitespace and extra ## inside
strings.one="here we have a dq string"
strings.two='here we have a sq string'
strings.three= 'here we have another sq string' # have another sq string
strings.four="this has 'nested quotes' inside"
strings.five='mixed "quotes" the other way around'
strings.six='this has an \'escaped\' set of quotes'
""";

var expected = new Dictionary<string, string>
{
["core.overrideMe"] = "This is the correct value",
["bar.scope.foo"] = "123456",
["baz.specialChars"] = "I contain special chars like = in my value",
["strings.one"] = "here we have a dq string",
["strings.two"] = "here we have a sq string",
["strings.three"] = "here we have another sq string",
["strings.four"] = "this has 'nested quotes' inside",
["strings.five"] = "mixed \"quotes\" the other way around",
["strings.six"] = "this has an \\'escaped\\' set of quotes",
};

var parser = new LinuxConfigParser(new NullTrace());

Assert.Equal(expected, parser.Parse(contents));
}
}
47 changes: 47 additions & 0 deletions src/shared/Core.Tests/Interop/Linux/LinuxSettingsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections.Generic;
using GitCredentialManager.Interop.Linux;
using GitCredentialManager.Tests.Objects;
using Xunit;

namespace GitCredentialManager.Tests.Interop.Linux;

public class LinuxSettingsTests
{
[LinuxFact]
public void LinuxSettings_TryGetExternalDefault_CombinesFiles()
{
var env = new TestEnvironment();
var git = new TestGit();
var trace = new NullTrace();
var fs = new TestFileSystem();

var utf8 = EncodingEx.UTF8NoBom;

fs.Directories = new HashSet<string>
{
"/",
"/etc",
"/etc/git-credential-manager",
"/etc/git-credential-manager/config.d"
};

const string config1 = "core.overrideMe=value1";
const string config2 = "core.overrideMe=value2";
const string config3 = "core.overrideMe=value3";

fs.Files = new Dictionary<string, byte[]>
{
["/etc/git-credential-manager/config.d/01-first"] = utf8.GetBytes(config1),
["/etc/git-credential-manager/config.d/02-second"] = utf8.GetBytes(config2),
["/etc/git-credential-manager/config.d/03-third"] = utf8.GetBytes(config3),
};

var settings = new LinuxSettings(env, git, trace, fs);

bool result = settings.TryGetExternalDefault(
"core", null, "overrideMe", out string value);

Assert.True(result);
Assert.Equal("value3", value);
}
}
2 changes: 1 addition & 1 deletion src/shared/Core/CommandContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public CommandContext()
gitPath,
FileSystem.GetCurrentDirectory()
);
Settings = new Settings(Environment, Git);
Settings = new LinuxSettings(Environment, Git, Trace, FileSystem);
}
else
{
Expand Down
1 change: 1 addition & 0 deletions src/shared/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static class Constants
public const string AuthorityIdAuto = "auto";

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

public const string MacOSBundleId = "git-credential-manager";
public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf");
Expand Down
62 changes: 62 additions & 0 deletions src/shared/Core/Interop/Linux/LinuxConfigParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace GitCredentialManager.Interop.Linux;

public class LinuxConfigParser
{
#if NETFRAMEWORK
private const string SQ = "'";
private const string DQ = "\"";
private const string Hash = "#";
#else
private const char SQ = '\'';
private const char DQ = '"';
private const char Hash = '#';
#endif

private static readonly Regex LineRegex = new(@"^\s*(?<key>[a-zA-Z0-9\.-]+)\s*=\s*(?<value>.+?)\s*(?:#.*)?$");

private readonly ITrace _trace;

public LinuxConfigParser(ITrace trace)
{
EnsureArgument.NotNull(trace, nameof(trace));

_trace = trace;
}

public IDictionary<string, string> Parse(string content)
{
var result = new Dictionary<string, string>(GitConfigurationKeyComparer.Instance);

IEnumerable<string> lines = content.Split(['\n'], StringSplitOptions.RemoveEmptyEntries);

foreach (string line in lines)
{
// Ignore empty lines or full-line comments
var trimmedLine = line.Trim();
if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith(Hash))
continue;

var match = LineRegex.Match(trimmedLine);
if (!match.Success)
{
_trace.WriteLine($"Invalid config line format: {line}");
continue;
}

string key = match.Groups["key"].Value;
string value = match.Groups["value"].Value;

// Remove enclosing quotes from the value, if any
if ((value.StartsWith(DQ) && value.EndsWith(DQ)) || (value.StartsWith(SQ) && value.EndsWith(SQ)))
value = value.Substring(1, value.Length - 2);

result[key] = value;
}

return result;
}
}
96 changes: 96 additions & 0 deletions src/shared/Core/Interop/Linux/LinuxSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Avalonia.Markup.Xaml.MarkupExtensions;

namespace GitCredentialManager.Interop.Linux;

public class LinuxSettings : Settings
{
private readonly ITrace _trace;
private readonly IFileSystem _fs;

private IDictionary<string, string> _extConfigCache;

/// <summary>
/// Reads settings from Git configuration, environment variables, and defaults from the
/// /etc/git-credential-manager.d app configuration directory.
/// </summary>
public LinuxSettings(IEnvironment environment, IGit git, ITrace trace, IFileSystem fs)
: base(environment, git)
{
EnsureArgument.NotNull(trace, nameof(trace));
EnsureArgument.NotNull(fs, nameof(fs));

_trace = trace;
_fs = fs;

PlatformUtils.EnsureLinux();
}

protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value)
{
value = null;

_extConfigCache ??= ReadExternalConfiguration();

if (_extConfigCache is null)
return false; // No external config found (or failed to read)

string name = string.IsNullOrWhiteSpace(scope)
? $"{section}.{property}"
: $"{section}.{scope}.{property}";

// Check if the setting exists in the configuration
if (!_extConfigCache.TryGetValue(name, out value))
{
// No property exists
return false;
}

_trace.WriteLine($"Default setting found in app configuration directory: {name}={value}");
return true;
}

private IDictionary<string, string> ReadExternalConfiguration()
{
try
{
// Check for system-wide config files in /etc/git-credential-manager/config.d and concatenate them together
// in alphabetical order to form a single configuration.
const string configDir = Constants.LinuxAppDefaultsDirectoryPath;
if (!_fs.DirectoryExists(configDir))
{
// No configuration directory exists
return null;
}

// Get all the files in the configuration directory
IEnumerable<string> files = _fs.EnumerateFiles(configDir, "*");

// Read the contents of each file and concatenate them together
var combinedFile = new StringBuilder();
foreach (string file in files)
{
using Stream stream = _fs.OpenFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream);
string contents = reader.ReadToEnd();
combinedFile.Append(contents);
combinedFile.Append('\n');
}

var parser = new LinuxConfigParser(_trace);

return parser.Parse(combinedFile.ToString());
}
catch (Exception ex)
{
// Reading defaults is not critical to the operation of the application
// so we can ignore any errors and just log the failure.
_trace.WriteLine("Failed to read default setting from app configuration directory.");
_trace.WriteException(ex);
return null;
}
}
}
2 changes: 1 addition & 1 deletion src/shared/Core/Interop/MacOS/MacOSSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public MacOSSettings(IEnvironment environment, IGit git, ITrace trace)
PlatformUtils.EnsureMacOS();
}

protected override bool TryGetExternalDefault(string section, string scope, string property, out string value)
protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value)
{
value = null;

Expand Down
2 changes: 1 addition & 1 deletion src/shared/Core/Interop/Windows/WindowsSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public WindowsSettings(IEnvironment environment, IGit git, ITrace trace)
PlatformUtils.EnsureWindows();
}

protected override bool TryGetExternalDefault(string section, string scope, string property, out string value)
protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value)
{
value = null;

Expand Down
2 changes: 1 addition & 1 deletion src/shared/Core/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ public IEnumerable<string> GetSettingValues(string envarName, string section, st
/// <param name="property">Configuration property name.</param>
/// <param name="value">Value of the configuration setting, or null.</param>
/// <returns>True if a default setting has been set, false otherwise.</returns>
protected virtual bool TryGetExternalDefault(string section, string scope, string property, out string value)
protected internal virtual bool TryGetExternalDefault(string section, string scope, string property, out string value)
{
value = null;
return false;
Expand Down
5 changes: 4 additions & 1 deletion src/windows/Installer.Windows/Installer.Windows.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
<TargetFramework>net472</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<EnableDefaultItems>false</EnableDefaultItems>
<PayloadPath>$(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\$(RuntimeIdentifier)</PayloadPath>
<PayloadPath>$(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\$(RuntimeIdentifier)\</PayloadPath>
<InnoSetupVersion>6.3.1</InnoSetupVersion>
<!-- We already append the RID to our intermediate PayloadPath and also to the
final installer filenames so there's no need to append to the output path. -->
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading
Loading