Skip to content

Commit 0c9779d

Browse files
Copilottmat
authored andcommitted
Add objectformat extension support for SHA256 repositories
1 parent 3b5251d commit 0c9779d

File tree

7 files changed

+205
-32
lines changed

7 files changed

+205
-32
lines changed

src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,5 +475,34 @@ public void RefStorage_Invalid(string value)
475475
new GitConfig(ImmutableDictionary<GitVariableName, ImmutableArray<string>>.Empty
476476
.Add(new GitVariableName("extensions", "", "refStorage"), [value])));
477477
}
478+
479+
[Theory]
480+
[InlineData(null, ObjectNameFormat.Sha1)]
481+
[InlineData("sha1", ObjectNameFormat.Sha1)]
482+
[InlineData("sha256", ObjectNameFormat.Sha256)]
483+
internal void ParseObjectFormat(string? value, ObjectNameFormat expected)
484+
{
485+
var variables = ImmutableDictionary<GitVariableName, ImmutableArray<string>>.Empty;
486+
if (value != null)
487+
{
488+
variables = variables.Add(new GitVariableName("extensions", "", "objectFormat"), [value]);
489+
}
490+
491+
Assert.Equal(expected, new GitConfig(variables).ObjectNameFormat);
492+
}
493+
494+
[Theory]
495+
[InlineData("")]
496+
[InlineData("Sha1")]
497+
[InlineData("sha-1")]
498+
[InlineData("sha-256")]
499+
[InlineData("sha384")]
500+
[InlineData("sha512")]
501+
internal void ParseObjectFormat_Invalid(string value)
502+
{
503+
Assert.Throws<InvalidDataException>(() =>
504+
new GitConfig(ImmutableDictionary<GitVariableName, ImmutableArray<string>>.Empty
505+
.Add(new GitVariableName("extensions", "", "objectFormat"), [value])));
506+
}
478507
}
479508
}

src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,48 @@ public void ResolveReference()
2525
refsHeadsDir.CreateFile("br1").WriteAllText("ref: refs/heads/br2");
2626
refsHeadsDir.CreateFile("br2").WriteAllText("ref: refs/heads/master");
2727

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

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

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

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

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

43+
[Fact]
44+
public void ResolveReference_SHA256()
45+
{
46+
using var temp = new TempRoot();
47+
48+
var gitDir = temp.CreateDirectory();
49+
50+
var commonDir = temp.CreateDirectory();
51+
var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads");
52+
53+
// SHA256 hash (64 characters)
54+
refsHeadsDir.CreateFile("master").WriteAllText("0000000000000000000000000000000000000000000000000000000000000000");
55+
refsHeadsDir.CreateFile("br1").WriteAllText("ref: refs/heads/br2");
56+
refsHeadsDir.CreateFile("br2").WriteAllText("ref: refs/heads/master");
57+
58+
using var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);
59+
60+
// Verify SHA256 hash is accepted directly
61+
Assert.Equal(
62+
"0123456789ABCDEFabcdef00000000000000000000000000000000000000000000",
63+
resolver.ResolveReference("0123456789ABCDEFabcdef00000000000000000000000000000000000000000000"));
64+
65+
Assert.Equal("0000000000000000000000000000000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/master"));
66+
Assert.Equal("0000000000000000000000000000000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/br1"));
67+
Assert.Equal("0000000000000000000000000000000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/br2"));
68+
}
69+
4370
[Fact]
4471
public void ResolveReference_Errors()
4572
{
@@ -53,14 +80,24 @@ public void ResolveReference_Errors()
5380
refsHeadsDir.CreateFile("rec1").WriteAllText("ref: refs/heads/rec2");
5481
refsHeadsDir.CreateFile("rec2").WriteAllText("ref: refs/heads/rec1");
5582

56-
var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles);
83+
using var resolver1 = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);
84+
85+
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference("ref: refs/heads/rec1"));
86+
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference("ref: xyz/heads/rec1"));
87+
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference("ref:refs/heads/rec1"));
88+
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference("refs/heads/rec1"));
89+
90+
// Invalid SHA1 hash lengths
91+
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference(new string('0', ObjectNameFormat.Sha1.HashSize * 2 - 1)));
92+
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference(new string('0', ObjectNameFormat.Sha1.HashSize * 2 + 1)));
93+
Assert.Throws<InvalidDataException>(() => resolver1.ResolveReference(new string('0', ObjectNameFormat.Sha256.HashSize * 2)));
5794

58-
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference("ref: refs/heads/rec1"));
59-
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference("ref: xyz/heads/rec1"));
60-
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference("ref:refs/heads/rec1"));
61-
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference("refs/heads/rec1"));
62-
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference(new string('0', 39)));
63-
Assert.Throws<InvalidDataException>(() => resolver.ResolveReference(new string('0', 41)));
95+
using var resolver2 = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);
96+
97+
// Invalid SHA256 hash lengths
98+
Assert.Throws<InvalidDataException>(() => resolver2.ResolveReference(new string('0', ObjectNameFormat.Sha256.HashSize * 2 - 1)));
99+
Assert.Throws<InvalidDataException>(() => resolver2.ResolveReference(new string('0', ObjectNameFormat.Sha256.HashSize * 2 + 1)));
100+
Assert.Throws<InvalidDataException>(() => resolver2.ResolveReference(new string('0', ObjectNameFormat.Sha1.HashSize * 2)));
64101
}
65102

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

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

83-
var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles);
120+
using var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);
84121

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

127+
[Fact]
128+
public void ResolveReference_Packed_SHA256()
129+
{
130+
using var temp = new TempRoot();
131+
132+
var gitDir = temp.CreateDirectory();
133+
134+
// Packed refs with SHA256 hashes (64 characters)
135+
gitDir.CreateFile("packed-refs").WriteAllText(
136+
@"# pack-refs with: peeled fully-peeled sorted
137+
1111111111111111111111111111111111111111111111111111111111111111 refs/heads/master
138+
2222222222222222222222222222222222222222222222222222222222222222 refs/heads/br2
139+
");
140+
var commonDir = temp.CreateDirectory();
141+
var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads");
142+
143+
refsHeadsDir.CreateFile("br1").WriteAllText("ref: refs/heads/br2");
144+
145+
using var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);
146+
147+
Assert.Equal("1111111111111111111111111111111111111111111111111111111111111111", resolver.ResolveReference("ref: refs/heads/master"));
148+
Assert.Equal("2222222222222222222222222222222222222222222222222222222222222222", resolver.ResolveReference("ref: refs/heads/br1"));
149+
Assert.Equal("2222222222222222222222222222222222222222222222222222222222222222", resolver.ResolveReference("ref: refs/heads/br2"));
150+
}
151+
90152
[Fact]
91153
public void ReadPackedReferences()
92154
{
@@ -101,7 +163,8 @@ 6666666666666666666666666666666666666666 y z
101163
7777777777777777777777777777777777777777 refs/heads/br
102164
";
103165

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

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

176+
[Fact]
177+
public void ReadPackedReferences_SHA256()
178+
{
179+
var packedRefs =
180+
@"# pack-refs with:
181+
1111111111111111111111111111111111111111111111111111111111111111 refs/heads/master
182+
2222222222222222222222222222222222222222222222222222222222222222 refs/heads/br
183+
^3333333333333333333333333333333333333333333333333333333333333333
184+
4444444444444444444444444444444444444444444444444444444444444444 x
185+
5555555555555555555555555555555555555555555555555555555555555555 y
186+
6666666666666666666666666666666666666666666666666666666666666666 y z
187+
7777777777777777777777777777777777777777777777777777777777777777 refs/heads/br
188+
";
189+
190+
using var resolver = new GitReferenceResolver(TempRoot.Root, TempRoot.Root, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);
191+
var actual = resolver.ReadPackedReferences(new StringReader(packedRefs), "<path>");
192+
193+
AssertEx.SetEqual(new[]
194+
{
195+
"refs/heads/br:2222222222222222222222222222222222222222222222222222222222222222",
196+
"refs/heads/master:1111111111111111111111111111111111111111111111111111111111111111"
197+
}, actual.Select(e => $"{e.Key}:{e.Value}"));
198+
}
199+
113200
[Theory]
114201
[InlineData("# pack-refs with:")]
115202
[InlineData("# pack-refs with:xyz")]
116203
[InlineData("# pack-refs with:xyz\n")]
117204
public void ReadPackedReferences_Empty(string content)
118205
{
119-
Assert.Empty(GitReferenceResolver.ReadPackedReferences(new StringReader(content), "<path>"));
206+
using var resolver = new GitReferenceResolver(TempRoot.Root, TempRoot.Root, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);
207+
Assert.Empty(resolver.ReadPackedReferences(new StringReader(content), "<path>"));
120208
}
121209

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

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

153-
using (var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.RefTable))
243+
using (var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.RefTable, ObjectNameFormat.Sha1))
154244
{
155245
Assert.Equal("0100000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/a"));
156246
Assert.Equal("0200000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/c"));

src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ public void OpenRepository_Version1_Extensions()
226226
preciousObjects = true
227227
partialClone = promisor_remote
228228
worktreeConfig = true
229+
objectformat = sha256
229230
");
230231

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

268+
[Fact]
269+
public void OpenRepository_Version1_ObjectFormatExtension()
270+
{
271+
using var temp = new TempRoot();
272+
273+
var homeDir = temp.CreateDirectory();
274+
275+
var workingDir = temp.CreateDirectory();
276+
var gitDir = workingDir.CreateDirectory(".git");
277+
278+
gitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master");
279+
gitDir.CreateDirectory("refs").CreateDirectory("heads").CreateFile("master").WriteAllText("0000000000000000000000000000000000000000");
280+
gitDir.CreateDirectory("objects");
281+
282+
gitDir.CreateFile("config").WriteAllText(@"
283+
[core]
284+
repositoryformatversion = 1
285+
[extensions]
286+
objectformat = sha256");
287+
288+
var src = workingDir.CreateDirectory("src");
289+
290+
// Should not throw - objectformat extension should be supported
291+
var repository = GitRepository.OpenRepository(src.Path, new GitEnvironment(homeDir.Path));
292+
Assert.NotNull(repository);
293+
Assert.Equal(gitDir.Path, repository.GitDirectory);
294+
Assert.Equal(gitDir.Path, repository.CommonDirectory);
295+
Assert.Equal(workingDir.Path, repository.WorkingDirectory);
296+
}
297+
267298
[Fact]
268299
public void OpenRepository_VersionNotSupported()
269300
{

src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,42 @@ internal sealed partial class GitConfig
1616

1717
private const int SupportedGitRepoFormatVersion = 1;
1818

19-
private const string ExtensionSectionName = "extensions";
19+
private const string CoreSectionName = "core";
20+
private const string ExtensionsSectionName = "extensions";
21+
2022
private const string RefStorageExtensionName = "refstorage";
23+
private const string ObjectFormatExtensionName = "objectFormat";
24+
private const string RepositoryFormatVersionVariableName = "repositoryformatversion";
2125

2226
private static readonly ImmutableArray<string> s_knownExtensions =
23-
["noop", "preciousObjects", "partialclone", "worktreeConfig", RefStorageExtensionName];
27+
["noop", "preciousObjects", "partialclone", "worktreeConfig", RefStorageExtensionName, ObjectFormatExtensionName];
2428

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

28-
internal GitConfig(ImmutableDictionary<GitVariableName, ImmutableArray<string>> variables)
32+
/// <summary>
33+
/// The parsed value of "extensions.objectFormat" variable.
34+
/// </summary>
35+
public ObjectNameFormat ObjectNameFormat { get; }
36+
37+
/// <exception cref="InvalidDataException"/>
38+
public GitConfig(ImmutableDictionary<GitVariableName, ImmutableArray<string>> variables)
2939
{
3040
Variables = variables;
3141

32-
ReferenceStorageFormat = GetVariableValue(ExtensionSectionName, RefStorageExtensionName) switch
42+
ReferenceStorageFormat = GetVariableValue(ExtensionsSectionName, RefStorageExtensionName) switch
3343
{
3444
null => ReferenceStorageFormat.LooseFiles,
3545
"reftable" => ReferenceStorageFormat.RefTable,
3646
_ => throw new InvalidDataException(),
3747
};
48+
49+
ObjectNameFormat = GetVariableValue(ExtensionsSectionName, ObjectFormatExtensionName) switch
50+
{
51+
null or "sha1" => ObjectNameFormat.Sha1,
52+
"sha256" => ObjectNameFormat.Sha256,
53+
_ => throw new InvalidDataException(),
54+
};
3855
}
3956

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

0 commit comments

Comments
 (0)