diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs index c61fb555..bfdc6edb 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs @@ -40,6 +40,32 @@ public void ResolveReference() 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"); + + var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path); + + // 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() { @@ -59,8 +85,12 @@ public void ResolveReference_Errors() Assert.Throws(() => resolver.ResolveReference("ref: xyz/heads/rec1")); Assert.Throws(() => resolver.ResolveReference("ref:refs/heads/rec1")); Assert.Throws(() => resolver.ResolveReference("refs/heads/rec1")); + // Invalid SHA1 hash lengths Assert.Throws(() => resolver.ResolveReference(new string('0', 39))); Assert.Throws(() => resolver.ResolveReference(new string('0', 41))); + // Invalid SHA256 hash lengths + Assert.Throws(() => resolver.ResolveReference(new string('0', 63))); + Assert.Throws(() => resolver.ResolveReference(new string('0', 65))); } [Fact] @@ -87,6 +117,31 @@ 2222222222222222222222222222222222222222 refs/heads/br2 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"); + + var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path); + + 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() { @@ -110,6 +165,29 @@ 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 +"; + + var actual = GitReferenceResolver.ReadPackedReferences(new StringReader(packedRefs), ""); + + 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")] diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs index 7ba9993d..7cc4eb83 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs @@ -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)); @@ -264,6 +265,36 @@ public void OpenRepository_Version1_UnknownExtension() Assert.Throws(() => 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() { diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitReferenceResolver.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitReferenceResolver.cs index 7d970c09..54d6fb1c 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitReferenceResolver.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitReferenceResolver.cs @@ -254,7 +254,8 @@ internal static string ReadReferenceFromFile(string path) private string? ResolvePackedReference(string reference) => _lazyPackedReferences.Value.TryGetValue(reference, out var objectId) ? objectId : null; + // SHA1 hashes are 40 hex characters, SHA256 hashes are 64 hex characters private static bool IsObjectId(string reference) - => reference.Length == 40 && reference.All(CharUtils.IsHexadecimalDigit); + => (reference.Length == 40 || reference.Length == 64) && reference.All(CharUtils.IsHexadecimalDigit); } } diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs index fc7446ea..7186b258 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs @@ -25,7 +25,7 @@ internal sealed class GitRepository private const string GitModulesFileName = ".gitmodules"; private static readonly ImmutableArray s_knownExtensions = - ImmutableArray.Create("noop", "preciousObjects", "partialclone", "worktreeConfig"); + ImmutableArray.Create("noop", "preciousObjects", "partialclone", "worktreeConfig", "objectformat"); public GitConfig Config { get; }