Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Match Git config in a mixed-sensitively way #234

Merged
merged 2 commits into from
Nov 25, 2020
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using Xunit;

namespace Microsoft.Git.CredentialManager.Tests
{
public class GitConfigurationKeyComparerTests
{
[Theory]
[InlineData("", "", true)]
[InlineData(null, null, true)]
[InlineData(" ", " ", true)]
[InlineData("foo", "foo", true)]
[InlineData("foo", "FOO", true)]
[InlineData("foo", "bar", false)]
[InlineData("foo.bar", "foo.bar", true)]
[InlineData("foo.bar", "foo.fish", false)]
[InlineData("fish.bar", "foo.bar", false)]
[InlineData("foo.bar", "FOO.BAR", true)]
[InlineData("foo.bar", "foo.BAR", true)]
[InlineData("foo.bar", "FOO.bar", true)]
[InlineData("foo.example.com.bar", "foo.example.com.bar", true)]
[InlineData("foo.example.com.bar", "foo.example.com.BAR", true)]
[InlineData("foo.example.com.bar", "FOO.example.com.BAR", true)]
[InlineData("foo.example.com.bar", "FOO.example.com.bar", true)]
[InlineData("foo.example.com.bar", "foo.EXAMPLE.COM.bar", false)]
public void GitConfigurationKeyComparer_Equals(string x, string y, bool expected)
{
bool actual = GitConfigurationKeyComparer.Instance.Equals(x, y);
Assert.Equal(expected, actual);
}

[Theory]
[InlineData("", "", 0)]
[InlineData(null, null, 0)]
[InlineData(" ", " ", 0)]
[InlineData("foo", "foo", 0)]
[InlineData("foo", "FOO", 0)]
[InlineData("foo", "bar", 4)]
[InlineData("foo.bar", "foo.bar", 0)]
[InlineData("foo.bar", "foo.fish", -4)]
[InlineData("fish.bar", "foo.bar", -6)]
[InlineData("foo.bar", "FOO.BAR", 0)]
[InlineData("foo.bar", "foo.BAR", 0)]
[InlineData("foo.bar", "FOO.bar", 0)]
[InlineData("foo.example.com.bar", "foo.example.com.bar", 0)]
[InlineData("foo.example.com.bar", "foo.example.com.BAR", 0)]
[InlineData("foo.example.com.bar", "FOO.example.com.BAR", 0)]
[InlineData("foo.example.com.bar", "FOO.example.com.bar", 0)]
[InlineData("foo.example.com.bar", "foo.EXAMPLE.COM.bar", 32)]
public void GitConfigurationKeyComparer_Compare(string x, string y, int expected)
{
int actual = GitConfigurationKeyComparer.Instance.Compare(x, y);
Assert.Equal(expected, actual);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ public void GitConfiguration_TryGet_SectionScopeProperty_DoesNotExists_ReturnsFa
}

[Fact]
public void GitConfiguration_GetString_Name_Exists_ReturnsString()
public void GitConfiguration_Get_Name_Exists_ReturnsString()
{
string repoPath = CreateRepository(out string workDirPath);
Git(repoPath, workDirPath, "config --local user.name john.doe").AssertSuccess();
Expand All @@ -265,7 +265,7 @@ public void GitConfiguration_GetString_Name_Exists_ReturnsString()
}

[Fact]
public void GitConfiguration_GetString_Name_DoesNotExists_ThrowsException()
public void GitConfiguration_Get_Name_DoesNotExists_ThrowsException()
{
string repoPath = CreateRepository();

Expand All @@ -279,7 +279,7 @@ public void GitConfiguration_GetString_Name_DoesNotExists_ThrowsException()
}

[Fact]
public void GitConfiguration_GetString_SectionProperty_Exists_ReturnsString()
public void GitConfiguration_Get_SectionProperty_Exists_ReturnsString()
{
string repoPath = CreateRepository(out string workDirPath);
Git(repoPath, workDirPath, "config --local user.name john.doe").AssertSuccess();
Expand All @@ -295,7 +295,7 @@ public void GitConfiguration_GetString_SectionProperty_Exists_ReturnsString()
}

[Fact]
public void GitConfiguration_GetString_SectionProperty_DoesNotExists_ThrowsException()
public void GitConfiguration_Get_SectionProperty_DoesNotExists_ThrowsException()
{
string repoPath = CreateRepository();

Expand All @@ -310,7 +310,7 @@ public void GitConfiguration_GetString_SectionProperty_DoesNotExists_ThrowsExcep
}

[Fact]
public void GitConfiguration_GetString_SectionScopeProperty_Exists_ReturnsString()
public void GitConfiguration_Get_SectionScopeProperty_Exists_ReturnsString()
{
string repoPath = CreateRepository(out string workDirPath);
Git(repoPath, workDirPath, "config --local user.example.com.name john.doe").AssertSuccess();
Expand All @@ -326,7 +326,7 @@ public void GitConfiguration_GetString_SectionScopeProperty_Exists_ReturnsString
}

[Fact]
public void GitConfiguration_GetString_SectionScopeProperty_NullScope_ReturnsUnscopedString()
public void GitConfiguration_Get_SectionScopeProperty_NullScope_ReturnsUnscopedString()
{
string repoPath = CreateRepository(out string workDirPath);
Git(repoPath, workDirPath, "config --local user.name john.doe").AssertSuccess();
Expand All @@ -342,7 +342,7 @@ public void GitConfiguration_GetString_SectionScopeProperty_NullScope_ReturnsUns
}

[Fact]
public void GitConfiguration_GetString_SectionScopeProperty_DoesNotExists_ThrowsException()
public void GitConfiguration_Get_SectionScopeProperty_DoesNotExists_ThrowsException()
{
string repoPath = CreateRepository();

Expand Down
89 changes: 89 additions & 0 deletions src/shared/Microsoft.Git.CredentialManager.Tests/SettingsTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Git.CredentialManager.Tests.Objects;
using Xunit;
Expand Down Expand Up @@ -1038,5 +1039,93 @@ public void Settings_GetSettingValues_EnvarAndMultipleConfig_ReturnsAllWithCorre

Assert.Equal(expectedValues, actualValues);
}

[Fact]
public void Settings_GetSettingValues_ReturnsAllMatchingValues()
{
const string remoteUrl = "http://example.com/foo/bar/bazz.git";
const string broadScope = "example.com";
const string tightScope = "example.com/foo/bar";
const string otherScope1 = "test.com";
const string otherScope2 = "sub.test.com";
const string envarName = "GCM_TESTVAR";
const string envarValue = "envar-value";
const string section = "gcmtest";
const string property = "bar";
var remoteUri = new Uri(remoteUrl);

const string tightScopeValue = "value-scope1";
const string broadScopeValue = "value-scope2";
const string noScopeValue = "value-no-scope";
const string otherValue1 = "other-scope1";
const string otherValue2 = "other-scope2";

string[] expectedValues = {envarValue, tightScopeValue, broadScopeValue, noScopeValue};

var envars = new TestEnvironment
{
Variables = {[envarName] = envarValue}
};

var git = new TestGit();
git.LocalConfiguration[$"{section}.{property}"] = noScopeValue;
git.LocalConfiguration[$"{section}.{broadScope}.{property}"] = broadScopeValue;
git.LocalConfiguration[$"{section}.{tightScope}.{property}"] = tightScopeValue;
git.LocalConfiguration[$"{section}.{otherScope1}.{property}"] = otherValue1;
git.LocalConfiguration[$"{section}.{otherScope2}.{property}"] = otherValue2;

var settings = new Settings(envars, git)
{
RemoteUri = remoteUri
};

string[] actualValues = settings.GetSettingValues(envarName, section, property).ToArray();

Assert.NotNull(actualValues);
Assert.Equal(expectedValues, actualValues);
}

[Fact]
public void Settings_GetSettingValues_IgnoresSectionAndPropertyCase_ScopeIsCaseSensitive()
{
const string remoteUrl = "http://example.com/foo/bar/bazz.git";
const string scopeLo = "example.com";
const string scopeHi = "EXAMPLE.COM";
const string envarName = "GCM_TESTVAR";
const string envarValue = "envar-value";
const string sectionLo = "gcmtest";
const string sectionHi = "GCMTEST";
const string sectionMix = "GcMtEsT";
const string propertyLo = "bar";
const string propertyHi = "BAR";
const string propertyMix = "bAr";
var remoteUri = new Uri(remoteUrl);

const string noScopeValue = "the-value";
const string lowScopeValue = "value-scope-lo";
const string highScopeValue = "value-scope-hi";

string[] expectedValues = {envarValue, lowScopeValue, noScopeValue};

var envars = new TestEnvironment
{
Variables = {[envarName] = envarValue}
};

var git = new TestGit();
git.LocalConfiguration[$"{sectionLo}.{propertyHi}"] = noScopeValue;
git.LocalConfiguration[$"{sectionHi}.{scopeLo}.{propertyHi}"] = lowScopeValue;
git.LocalConfiguration[$"{sectionLo}.{scopeHi}.{propertyLo}"] = highScopeValue;

var settings = new Settings(envars, git)
{
RemoteUri = remoteUri
};

string[] actualValues = settings.GetSettingValues(envarName, sectionMix, propertyMix).ToArray();

Assert.NotNull(actualValues);
Assert.Equal(expectedValues, actualValues);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;

namespace Microsoft.Git.CredentialManager
{
/// <summary>
/// Represents string comparison of Git configuration entry key names.
/// </summary>
/// <remarks>
/// Git configuration entries have the form "section[.scope].property", where the
/// scope part is optional.
/// <para/>
/// The section and property components are NOT case sensitive.
/// The scope component if present IS case sensitive.
/// </remarks>
public class GitConfigurationKeyComparer : StringComparer
{
public static readonly GitConfigurationKeyComparer Instance = new GitConfigurationKeyComparer();

private GitConfigurationKeyComparer() { }

public override int Compare(string x, string y)
{
Split(x, out string xSection, out string xScope, out string xProperty);
Split(y, out string ySection, out string yScope, out string yProperty);

int cmpSection = OrdinalIgnoreCase.Compare(xSection, ySection);
if (cmpSection != 0) return cmpSection;

int cmpProperty = OrdinalIgnoreCase.Compare(xProperty, yProperty);
if (cmpProperty != 0) return cmpProperty;

return Ordinal.Compare(xScope, yScope);
}

public override bool Equals(string x, string y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;

Split(x, out string xSection, out string xScope, out string xProperty);
Split(y, out string ySection, out string yScope, out string yProperty);

// Section and property names are not case sensitive, but the inner 'scope' IS case sensitive!
return OrdinalIgnoreCase.Equals(xSection, ySection) &&
OrdinalIgnoreCase.Equals(xProperty, yProperty) &&
Ordinal.Equals(xScope, yScope);
}

public override int GetHashCode(string obj)
{
Split(obj, out string section, out string scope, out string property);

int code = OrdinalIgnoreCase.GetHashCode(section) ^
OrdinalIgnoreCase.GetHashCode(property);

return scope is null
? code
: code ^ Ordinal.GetHashCode(scope);
}

private static void Split(string str, out string section, out string scope, out string property)
{
section = null;
scope = null;
property = null;

if (string.IsNullOrWhiteSpace(str))
{
return;
}

mjcheetham marked this conversation as resolved.
Show resolved Hide resolved
section = str.TruncateFromIndexOf('.');
property = str.TrimUntilLastIndexOf('.');
int scopeLength = str.Length - (section.Length + property.Length + 2);
scope = scopeLength > 0 ? str.Substring(section.Length + 1, scopeLength) : null;
}
}
}
6 changes: 5 additions & 1 deletion src/shared/Microsoft.Git.CredentialManager/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,16 @@ public IEnumerable<string> GetSettingValues(string envarName, string section, st
* 4b. [section "example.com"]
* property = value
*
* It is also important to note that although the section and property names are NOT case
* sensitive, the "scope" part IS case sensitive! We must be careful when searching to ensure
* we follow Git's rules.
*
*/

// Enumerate all configuration entries with the correct section and property name
// and make a local copy of them here to avoid needing to call `TryGetValue` on the
// IGitConfiguration object multiple times in a loop below.
var configEntries = new Dictionary<string, string>();
var configEntries = new Dictionary<string, string>(GitConfigurationKeyComparer.Instance);
config.Enumerate((entryName, entryValue) =>
{
string entrySection = entryName.TruncateFromIndexOf('.');
Expand Down
2 changes: 1 addition & 1 deletion src/shared/TestInfrastructure/Objects/TestGit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ IGitConfiguration IGit.GetConfiguration(GitConfigurationLevel level)

private static IDictionary<string, IList<string>> MergeDictionaries(params IDictionary<string, IList<string>>[] dictionaries)
{
var result = new Dictionary<string, IList<string>>();
var result = new Dictionary<string, IList<string>>(GitConfigurationKeyComparer.Instance);

foreach (IDictionary<string, IList<string>> dict in dictionaries)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class TestGitConfiguration : IGitConfiguration
{
public TestGitConfiguration(IDictionary<string, IList<string>> config = null)
{
Dictionary = config ?? new Dictionary<string, IList<string>>();
Dictionary = config ?? new Dictionary<string, IList<string>>(GitConfigurationKeyComparer.Instance);
}

/// <summary>
Expand Down