Skip to content
Open
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
29 changes: 29 additions & 0 deletions src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -475,5 +475,34 @@ public void RefStorage_Invalid(string value)
new GitConfig(ImmutableDictionary<GitVariableName, ImmutableArray<string>>.Empty
.Add(new GitVariableName("extensions", "", "refStorage"), [value])));
}

[Theory]
[InlineData(null, ObjectNameFormat.Sha1)]
[InlineData("sha1", ObjectNameFormat.Sha1)]
[InlineData("sha256", ObjectNameFormat.Sha256)]
internal void ParseObjectFormat(string? value, ObjectNameFormat expected)
{
var variables = ImmutableDictionary<GitVariableName, ImmutableArray<string>>.Empty;
if (value != null)
{
variables = variables.Add(new GitVariableName("extensions", "", "objectFormat"), [value]);
}

Assert.Equal(expected, new GitConfig(variables).ObjectNameFormat);
}

[Theory]
[InlineData("")]
[InlineData("Sha1")]
[InlineData("sha-1")]
[InlineData("sha-256")]
[InlineData("sha384")]
[InlineData("sha512")]
internal void ParseObjectFormat_Invalid(string value)
{
Assert.Throws<InvalidDataException>(() =>
new GitConfig(ImmutableDictionary<GitVariableName, ImmutableArray<string>>.Empty
.Add(new GitVariableName("extensions", "", "objectFormat"), [value])));
}
}
}
118 changes: 104 additions & 14 deletions src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,48 @@ public void ResolveReference()
refsHeadsDir.CreateFile("br1").WriteAllText("ref: refs/heads/br2");
refsHeadsDir.CreateFile("br2").WriteAllText("ref: refs/heads/master");

var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles);
using var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);

Assert.Equal("0123456789ABCDEFabcdef000000000000000000", resolver.ResolveReference("0123456789ABCDEFabcdef000000000000000000"));

Assert.Equal("0000000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/master"));
Assert.Equal("0000000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/br1"));
Assert.Equal("0000000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/br2"));

// branch without commits (emtpy repository) will have not file in refs/heads:
// branch without commits (empty repository) will have not file in refs/heads:
Assert.Null(resolver.ResolveReference("ref: refs/heads/none"));

Assert.Null(resolver.ResolveReference("ref: refs/heads/rec1 "));
Assert.Null(resolver.ResolveReference("ref: refs/heads/none" + string.Join("/", Path.GetInvalidPathChars())));
}

[Fact]
public void ResolveReference_SHA256()
{
using var temp = new TempRoot();

var gitDir = temp.CreateDirectory();

var commonDir = temp.CreateDirectory();
var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads");

// SHA256 hash (64 characters)
refsHeadsDir.CreateFile("master").WriteAllText("0000000000000000000000000000000000000000000000000000000000000000");
refsHeadsDir.CreateFile("br1").WriteAllText("ref: refs/heads/br2");
refsHeadsDir.CreateFile("br2").WriteAllText("ref: refs/heads/master");

using var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);

// Verify SHA256 hash is accepted directly
Assert.Equal(
"0123456789ABCDEFabcdef000000000000000000000000000000000000000000",
resolver.ResolveReference("0123456789ABCDEFabcdef000000000000000000000000000000000000000000"));

Assert.Equal("0000000000000000000000000000000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/master"));
Assert.Equal("0000000000000000000000000000000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/br1"));
Assert.Equal("0000000000000000000000000000000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/br2"));
}

[Fact]
public void ResolveReference_Errors()
{
Expand All @@ -53,14 +80,24 @@ public void ResolveReference_Errors()
refsHeadsDir.CreateFile("rec1").WriteAllText("ref: refs/heads/rec2");
refsHeadsDir.CreateFile("rec2").WriteAllText("ref: refs/heads/rec1");

var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles);
using var resolver1 = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);

Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference("ref: refs/heads/rec1"));
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference("ref: xyz/heads/rec1"));
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference("ref:refs/heads/rec1"));
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference("refs/heads/rec1"));

// Invalid SHA1 hash lengths
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference(new string('0', ObjectNameFormat.Sha1.HashSize * 2 - 1)));
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference(new string('0', ObjectNameFormat.Sha1.HashSize * 2 + 1)));
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference(new string('0', ObjectNameFormat.Sha256.HashSize * 2)));

Assert.Throws<InvalidDataException>(() => resolver.ResolveReference("ref: refs/heads/rec1"));
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference("ref: xyz/heads/rec1"));
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference("ref:refs/heads/rec1"));
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference("refs/heads/rec1"));
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference(new string('0', 39)));
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference(new string('0', 41)));
using var resolver2 = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);

// Invalid SHA256 hash lengths
Assert.Throws<InvalidDataException>(() => resolver2.ResolveReference(new string('0', ObjectNameFormat.Sha256.HashSize * 2 - 1)));
Assert.Throws<InvalidDataException>(() => resolver2.ResolveReference(new string('0', ObjectNameFormat.Sha256.HashSize * 2 + 1)));
Assert.Throws<InvalidDataException>(() => resolver2.ResolveReference(new string('0', ObjectNameFormat.Sha1.HashSize * 2)));
}

[Fact]
Expand All @@ -80,13 +117,38 @@ 2222222222222222222222222222222222222222 refs/heads/br2

refsHeadsDir.CreateFile("br1").WriteAllText("ref: refs/heads/br2");

var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles);
using var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);

Assert.Equal("1111111111111111111111111111111111111111", resolver.ResolveReference("ref: refs/heads/master"));
Assert.Equal("2222222222222222222222222222222222222222", resolver.ResolveReference("ref: refs/heads/br1"));
Assert.Equal("2222222222222222222222222222222222222222", resolver.ResolveReference("ref: refs/heads/br2"));
}

[Fact]
public void ResolveReference_Packed_SHA256()
{
using var temp = new TempRoot();

var gitDir = temp.CreateDirectory();

// Packed refs with SHA256 hashes (64 characters)
gitDir.CreateFile("packed-refs").WriteAllText(
@"# pack-refs with: peeled fully-peeled sorted
1111111111111111111111111111111111111111111111111111111111111111 refs/heads/master
2222222222222222222222222222222222222222222222222222222222222222 refs/heads/br2
");
var commonDir = temp.CreateDirectory();
var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads");

refsHeadsDir.CreateFile("br1").WriteAllText("ref: refs/heads/br2");

using var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);

Assert.Equal("1111111111111111111111111111111111111111111111111111111111111111", resolver.ResolveReference("ref: refs/heads/master"));
Assert.Equal("2222222222222222222222222222222222222222222222222222222222222222", resolver.ResolveReference("ref: refs/heads/br1"));
Assert.Equal("2222222222222222222222222222222222222222222222222222222222222222", resolver.ResolveReference("ref: refs/heads/br2"));
}

[Fact]
public void ReadPackedReferences()
{
Expand All @@ -101,7 +163,8 @@ 6666666666666666666666666666666666666666 y z
7777777777777777777777777777777777777777 refs/heads/br
";

var actual = GitReferenceResolver.ReadPackedReferences(new StringReader(packedRefs), "<path>");
using var resolver = new GitReferenceResolver(TempRoot.Root, TempRoot.Root, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);
var actual = resolver.ReadPackedReferences(new StringReader(packedRefs), "<path>");

AssertEx.SetEqual(new[]
{
Expand All @@ -110,26 +173,53 @@ 7777777777777777777777777777777777777777 refs/heads/br
}, actual.Select(e => $"{e.Key}:{e.Value}"));
}

[Fact]
public void ReadPackedReferences_SHA256()
{
var packedRefs =
@"# pack-refs with:
1111111111111111111111111111111111111111111111111111111111111111 refs/heads/master
2222222222222222222222222222222222222222222222222222222222222222 refs/heads/br
^3333333333333333333333333333333333333333333333333333333333333333
4444444444444444444444444444444444444444444444444444444444444444 x
5555555555555555555555555555555555555555555555555555555555555555 y
6666666666666666666666666666666666666666666666666666666666666666 y z
7777777777777777777777777777777777777777777777777777777777777777 refs/heads/br
";

using var resolver = new GitReferenceResolver(TempRoot.Root, TempRoot.Root, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);
var actual = resolver.ReadPackedReferences(new StringReader(packedRefs), "<path>");

AssertEx.SetEqual(new[]
{
"refs/heads/br:2222222222222222222222222222222222222222222222222222222222222222",
"refs/heads/master:1111111111111111111111111111111111111111111111111111111111111111"
}, actual.Select(e => $"{e.Key}:{e.Value}"));
}

[Theory]
[InlineData("# pack-refs with:")]
[InlineData("# pack-refs with:xyz")]
[InlineData("# pack-refs with:xyz\n")]
public void ReadPackedReferences_Empty(string content)
{
Assert.Empty(GitReferenceResolver.ReadPackedReferences(new StringReader(content), "<path>"));
using var resolver = new GitReferenceResolver(TempRoot.Root, TempRoot.Root, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);
Assert.Empty(resolver.ReadPackedReferences(new StringReader(content), "<path>"));
}

[Theory]
[InlineData("")] // missing header
[InlineData("# pack-refs with")] // invalid header prefix
[InlineData("# pack-refs with:xyz\n1")] // bad object id
[InlineData("# pack-refs with:xyz\n^2222222222222222222222222222222222222222222222222222222222222222")] // bad object id: sha256
[InlineData("# pack-refs with:xyz\n1111111111111111111111111111111111111111")] // no reference name
[InlineData("# pack-refs with:xyz\n^1111111111111111111111111111111111111111")] // tag dereference without previous ref
[InlineData("# pack-refs with:xyz\n1111111111111111111111111111111111111111 x\n^1")] // bad object id
[InlineData("# pack-refs with:xyz\n^1111111111111111111111111111111111111111\n^2222222222222222222222222222222222222222")] // tag dereference without previous ref
public void ReadPackedReferences_Errors(string content)
{
Assert.Throws<InvalidDataException>(() => GitReferenceResolver.ReadPackedReferences(new StringReader(content), "<path>"));
using var resolver = new GitReferenceResolver(TempRoot.Root, TempRoot.Root, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);
Assert.Throws<InvalidDataException>(() => resolver.ReadPackedReferences(new StringReader(content), "<path>"));
}

[Fact]
Expand All @@ -150,7 +240,7 @@ public void ResolveReference_RefTable()
var ref1 = refTableDir.CreateFile("1.ref").WriteAllBytes(GitRefTableTestWriter.GetRefTableBlob([("refs/heads/a", 0x01), ("refs/heads/c", 0x02)]));
TempFile ref2;

using (var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.RefTable))
using (var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.RefTable, ObjectNameFormat.Sha1))
{
Assert.Equal("0100000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/a"));
Assert.Equal("0200000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/c"));
Expand Down
31 changes: 31 additions & 0 deletions src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ public void OpenRepository_Version1_Extensions()
preciousObjects = true
partialClone = promisor_remote
worktreeConfig = true
objectformat = sha256
");

Assert.True(GitRepository.TryFindRepository(gitDir.Path, out var location));
Expand Down Expand Up @@ -264,6 +265,36 @@ public void OpenRepository_Version1_UnknownExtension()
Assert.Throws<NotSupportedException>(() => GitRepository.OpenRepository(src.Path, new GitEnvironment(homeDir.Path)));
}

[Fact]
public void OpenRepository_Version1_ObjectFormatExtension()
{
using var temp = new TempRoot();

var homeDir = temp.CreateDirectory();

var workingDir = temp.CreateDirectory();
var gitDir = workingDir.CreateDirectory(".git");

gitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master");
gitDir.CreateDirectory("refs").CreateDirectory("heads").CreateFile("master").WriteAllText("0000000000000000000000000000000000000000");
gitDir.CreateDirectory("objects");

gitDir.CreateFile("config").WriteAllText(@"
[core]
repositoryformatversion = 1
[extensions]
objectformat = sha256");

var src = workingDir.CreateDirectory("src");

// Should not throw - objectformat extension should be supported
var repository = GitRepository.OpenRepository(src.Path, new GitEnvironment(homeDir.Path));
Assert.NotNull(repository);
Assert.Equal(gitDir.Path, repository.GitDirectory);
Assert.Equal(gitDir.Path, repository.CommonDirectory);
Assert.Equal(workingDir.Path, repository.WorkingDirectory);
}

[Fact]
public void OpenRepository_VersionNotSupported()
{
Expand Down
29 changes: 23 additions & 6 deletions src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,42 @@ internal sealed partial class GitConfig

private const int SupportedGitRepoFormatVersion = 1;

private const string ExtensionSectionName = "extensions";
private const string CoreSectionName = "core";
private const string ExtensionsSectionName = "extensions";

private const string RefStorageExtensionName = "refstorage";
private const string ObjectFormatExtensionName = "objectFormat";
private const string RepositoryFormatVersionVariableName = "repositoryformatversion";

private static readonly ImmutableArray<string> s_knownExtensions =
["noop", "preciousObjects", "partialclone", "worktreeConfig", RefStorageExtensionName];
["noop", "preciousObjects", "partialclone", "worktreeConfig", RefStorageExtensionName, ObjectFormatExtensionName];

public readonly ImmutableDictionary<GitVariableName, ImmutableArray<string>> Variables;
public readonly ReferenceStorageFormat ReferenceStorageFormat;

internal GitConfig(ImmutableDictionary<GitVariableName, ImmutableArray<string>> variables)
/// <summary>
/// The parsed value of "extensions.objectFormat" variable.
/// </summary>
public ObjectNameFormat ObjectNameFormat { get; }

/// <exception cref="InvalidDataException"/>
public GitConfig(ImmutableDictionary<GitVariableName, ImmutableArray<string>> variables)
{
Variables = variables;

ReferenceStorageFormat = GetVariableValue(ExtensionSectionName, RefStorageExtensionName) switch
ReferenceStorageFormat = GetVariableValue(ExtensionsSectionName, RefStorageExtensionName) switch
{
null => ReferenceStorageFormat.LooseFiles,
"reftable" => ReferenceStorageFormat.RefTable,
_ => throw new InvalidDataException(),
};

ObjectNameFormat = GetVariableValue(ExtensionsSectionName, ObjectFormatExtensionName) switch
{
null or "sha1" => ObjectNameFormat.Sha1,
"sha256" => ObjectNameFormat.Sha256,
_ => throw new InvalidDataException(),
};
}

/// <exception cref="IOException"/>
Expand All @@ -60,7 +77,7 @@ public static GitConfig ReadSubmoduleConfig(string gitDirectory, string commonDi
private void ValidateRepositoryConfig()
{
// See https://github.com/git/git/blob/master/Documentation/technical/repository-version.txt
var versionStr = GetVariableValue("core", "repositoryformatversion");
var versionStr = GetVariableValue(CoreSectionName, RepositoryFormatVersionVariableName);
if (TryParseInt64Value(versionStr, out var version) && version > SupportedGitRepoFormatVersion)
{
throw new NotSupportedException(string.Format(Resources.UnsupportedRepositoryVersion, versionStr, SupportedGitRepoFormatVersion));
Expand All @@ -71,7 +88,7 @@ private void ValidateRepositoryConfig()
// All variables defined under extensions section must be known, otherwise a git implementation is not allowed to proceed.
foreach (var variable in Variables)
{
if (variable.Key.SectionNameEquals(ExtensionSectionName) &&
if (variable.Key.SectionNameEquals(ExtensionsSectionName) &&
!s_knownExtensions.Contains(variable.Key.VariableName, StringComparer.OrdinalIgnoreCase))
{
throw new NotSupportedException(string.Format(
Expand Down
Loading