diff --git a/eng/Version.Details.props b/eng/Version.Details.props
index e2c7d5f8..285a257c 100644
--- a/eng/Version.Details.props
+++ b/eng/Version.Details.props
@@ -8,6 +8,7 @@ This file should be imported by eng/Versions.props
10.0.0-beta.25506.103
2.0.0
+ 10.0.0-rc.2.25502.107
17.11.31
17.11.31
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 25ffcf7c..9649d2e7 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -21,5 +21,9 @@
https://github.com/dotnet/msbuild
933b72e36e86c22ba73e8b8148488f8298bb73c7
+
+ https://github.com/dotnet/dotnet
+ be28ec777bf12db631725399c442448d52093087
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index a1ab4835..07e0d6d2 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -22,6 +22,7 @@
+
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs
index 0b506b1a..19bee0d7 100644
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the License.txt file in the project root for more information.
+
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
@@ -448,5 +450,59 @@ public void TryParseBooleanValue_Error(string? str)
{
Assert.False(GitConfig.TryParseBooleanValue(str, out _));
}
+
+ [Theory]
+ [InlineData(null, ReferenceStorageFormat.LooseFiles)]
+ [InlineData("reftable", ReferenceStorageFormat.RefTable)]
+ internal void RefStorage(string? value, ReferenceStorageFormat expected)
+ {
+ var variables = ImmutableDictionary>.Empty;
+ if (value != null)
+ {
+ variables = variables.Add(new GitVariableName("extensions", "", "refStorage"), [value]);
+ }
+
+ Assert.Equal(expected, new GitConfig(variables).ReferenceStorageFormat);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("x")]
+ [InlineData("refTable")]
+ public void RefStorage_Invalid(string value)
+ {
+ Assert.Throws(() =>
+ new GitConfig(ImmutableDictionary>.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>.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(() =>
+ new GitConfig(ImmutableDictionary>.Empty
+ .Add(new GitVariableName("extensions", "", "objectFormat"), [value])));
+ }
}
}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRefTableReaderTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRefTableReaderTests.cs
new file mode 100644
index 00000000..1335d097
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRefTableReaderTests.cs
@@ -0,0 +1,1188 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Xunit;
+
+namespace Microsoft.Build.Tasks.Git.UnitTests;
+
+public class GitRefTableReaderTests
+{
+ private static GitRefTableReader Create(params byte[] bytes)
+ => new(new MemoryStream(bytes));
+
+ private static GitRefTableReader Create(GitRefTableTestWriter writer)
+ {
+ writer.Position = 0;
+ return new(writer.Stream);
+ }
+
+ private static byte[] GetObjectName(byte b0, ObjectNameFormat format = ObjectNameFormat.Sha1)
+ => GitRefTableTestWriter.GetObjectName(b0, format);
+
+ [Fact]
+ public void ReadByte_Success()
+ {
+ using var reader = Create(0x12, 0x34);
+ Assert.Equal((byte)0x12, reader.ReadByte());
+ Assert.Equal((byte)0x34, reader.ReadByte());
+ }
+
+ [Fact]
+ public void ReadByte_EndOfStream()
+ {
+ using var reader = Create();
+ Assert.Throws(() => reader.ReadByte());
+ }
+
+ [Theory]
+ [InlineData(new byte[] { 0x00 }, 0)]
+ [InlineData(new byte[] { 0x01 }, 1)]
+ [InlineData(new byte[] { 0x7F }, 127)]
+ [InlineData(new byte[] { 0x80, 0x00 }, 128)]
+ [InlineData(new byte[] { 0x80, 0x01 }, 129)]
+ [InlineData(new byte[] { 0x80, 0x02 }, 130)]
+ [InlineData(new byte[] { 0xFF, 0x7F }, 16511)]
+ [InlineData(new byte[] { 0x80, 0x80, 0x00 }, 16512)]
+ [InlineData(new byte[] { 0x80, 0x80, 0x01 }, 16513)]
+ [InlineData(new byte[] { 0xFF, 0xFF, 0x7F }, 2113663)]
+ [InlineData(new byte[] { 0x80, 0x80, 0x80, 0x00 }, 2113664)]
+ [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0x7F }, 270549119)]
+ [InlineData(new byte[] { 0x80, 0x80, 0x80, 0x80, 0x00 }, 270549120)]
+ [InlineData(new byte[] { 0x86, 0xFE, 0xFE, 0xFE, 0x7F }, int.MaxValue)]
+ public void ReadVarInt_Valid(byte[] encoding, int expected)
+ {
+ using var reader = Create(encoding);
+ Assert.Equal(expected, reader.ReadVarInt());
+
+ var writer = new GitRefTableTestWriter();
+ writer.WriteVarInt(expected);
+ Assert.Equal(encoding, writer.ToArray());
+ }
+
+ [Fact]
+ public void ReadVarInt_Invalid_TooManyContinuationBytes()
+ {
+ using var reader = Create(0x86, 0xFE, 0xFE, 0xFF, 0x7F);
+ Assert.Throws(() => reader.ReadVarInt());
+ }
+
+ [Fact]
+ public void ReadUInt16BE()
+ {
+ using var reader = Create(0x12, 0x34);
+ Assert.Equal((ushort)0x1234, reader.ReadUInt16BE());
+ }
+
+ [Fact]
+ public void ReadUInt24BE()
+ {
+ using var reader = Create(0x01, 0x02, 0x03);
+ Assert.Equal(0x10203, reader.ReadUInt24BE());
+ }
+
+ [Fact]
+ public void ReadUInt32BE()
+ {
+ using var reader = Create(0x01, 0x02, 0x03, 0x04);
+ Assert.Equal(0x01020304u, reader.ReadUInt32BE());
+ }
+
+ [Fact]
+ public void ReadUInt64BE()
+ {
+ using var reader = Create(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08);
+ Assert.Equal(0x0102030405060708ul, reader.ReadUInt64BE());
+ }
+
+ [Fact]
+ public void ReadBytes_Success()
+ {
+ using var reader = Create(0x10, 0x20, 0x30, 0x40);
+ var data = reader.ReadBytes(4);
+ Assert.Equal([0x10, 0x20, 0x30, 0x40], data);
+ }
+
+ [Fact]
+ public void ReadBytes_EndOfStream()
+ {
+ using var reader = Create(0x10, 0x20);
+ Assert.Throws(() => reader.ReadBytes(3));
+ }
+
+ [Fact]
+ public void ReadBytes_OutOfMemory()
+ {
+ using var reader = Create();
+ Assert.Throws(() => reader.ReadBytes(int.MaxValue));
+ }
+
+ [Fact]
+ public void ReadExactly_FillsBuffer()
+ {
+ using var reader = Create(0xAA, 0xBB, 0xCC);
+ var buffer = new byte[3];
+ reader.ReadExactly(buffer);
+ Assert.Equal([0xAA, 0xBB, 0xCC], buffer);
+ }
+
+ [Fact]
+ public void ReadExactly_WithCount_FillsPartialBuffer()
+ {
+ using var reader = Create(0xAA, 0xBB, 0xCC);
+ var buffer = new byte[5];
+ reader.ReadExactly(buffer, 3);
+ Assert.Equal([0xAA, 0xBB, 0xCC, 0x00, 0x00], buffer);
+ }
+
+ [Fact]
+ public void ReadExactly_EndOfStream()
+ {
+ using var reader = Create(0xAA);
+ var buffer = new byte[2];
+ Assert.Throws(() => reader.ReadExactly(buffer));
+ }
+
+ [Theory]
+ [InlineData(new byte[] { 0x00 }, "00")]
+ [InlineData(new byte[] { 0x01, 0x02, 0x0A }, "01020a")]
+ public void ReadObjectName(byte[] bytes, string expected)
+ {
+ using var reader = Create(bytes);
+ var actual = reader.ReadObjectName(bytes.Length);
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ReadExactly_MultipleUnderlyingReads()
+ {
+ var data = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
+ using var stream = new ChunkedStream(new MemoryStream(data), maxChunk: 3);
+ using var reader = new GitRefTableReader(stream);
+ var buffer = new byte[data.Length];
+ reader.ReadExactly(buffer);
+ Assert.Equal(data, buffer);
+ Assert.True(stream.ReadCallCount > 1, "Expected multiple Read calls");
+ }
+
+ private sealed class ChunkedStream(Stream inner, int maxChunk) : Stream
+ {
+ public int ReadCallCount { get; private set; }
+
+ public override bool CanRead => true;
+ public override bool CanSeek => inner.CanSeek;
+ public override bool CanWrite => false;
+ public override long Length => inner.Length;
+ public override long Position { get => inner.Position; set => inner.Position = value; }
+ public override void Flush() => inner.Flush();
+
+ public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin);
+ public override void SetLength(long value) => inner.SetLength(value);
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ var toRead = Math.Min(maxChunk, count);
+ var n = inner.Read(buffer, offset, toRead);
+ if (n > 0) ReadCallCount++;
+ return n;
+ }
+ }
+
+ [Fact]
+ public void ReadHeader_InvalidMagic()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteHeader(
+ magic: 0xDEADBEEF,
+ version: 1,
+ blockSize: 0x100,
+ minUpdate: 0,
+ maxUpdate: 0);
+
+ using var reader = Create(writer.ToArray());
+ Assert.Throws(() => reader.ReadHeader());
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(3)]
+ public void ReadHeader_Version_Unsupported(byte version)
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteHeader(
+ magic: 0x52454654, // 'RFTB'
+ version: version,
+ blockSize: 0x010203,
+ minUpdate: 0,
+ maxUpdate: 0);
+
+ using var reader = Create(writer.ToArray());
+ Assert.Throws(() => reader.ReadHeader());
+ }
+
+ [Fact]
+ public void ReadHeader_Version1()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteHeader(
+ magic: 0x52454654, // 'RFTB'
+ version: 1,
+ blockSize: 0x010203,
+ minUpdate: 0,
+ maxUpdate: 0);
+
+ using var reader = Create(writer.ToArray());
+ var header = reader.ReadHeader();
+
+ Assert.Equal(24, header.Size);
+ Assert.Equal(0x010203, header.BlockSize);
+ Assert.Equal(ObjectNameFormat.Sha1, header.ObjectNameFormat);
+ }
+
+ [Theory]
+ [InlineData("sha1", ObjectNameFormat.Sha1)]
+ [InlineData("s256", ObjectNameFormat.Sha256)]
+ internal void ReadHeader_Version2(string hashId, ObjectNameFormat format)
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteHeader(
+ magic: 0x52454654, // 'RFTB'
+ version: 2,
+ blockSize: 0x000102,
+ minUpdate: 1,
+ maxUpdate: 2,
+ hashId: hashId);
+
+ using var reader = Create(writer.ToArray());
+ var header = reader.ReadHeader();
+ Assert.Equal(28, header.Size);
+ Assert.Equal(0x102, header.BlockSize);
+ Assert.Equal(format, header.ObjectNameFormat);
+ }
+
+ [Theory]
+ [InlineData("SHA1")]
+ [InlineData("sha2")]
+ public void ReadHeader_Version2_InvalidHashId(string hashId)
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteHeader(
+ magic: 0x52454654, // 'RFTB'
+ version: 2,
+ blockSize: 0x000102,
+ minUpdate: 1,
+ maxUpdate: 2,
+ hashId: hashId);
+
+ using var reader = Create(writer.ToArray());
+ Assert.Throws(() => reader.ReadHeader());
+ }
+
+ [Fact]
+ public void ReadFooter_Success()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteFooter(
+ writer => writer.WriteHeader(
+ magic: 0x52454654, // 'RFTB'
+ version: 2,
+ blockSize: 0x500,
+ minUpdate: 10,
+ maxUpdate: 20,
+ hashId: "sha1"),
+ refIndexPosition: 0,
+ objPosition: 1,
+ objIndexPosition: 2,
+ logPosition: 3,
+ logIndexPosition: 4);
+
+ using var reader = Create(writer.ToArray());
+ var footer = reader.ReadFooter();
+ Assert.Equal(28, footer.Header.Size);
+ Assert.Equal(0x500, footer.Header.BlockSize);
+ Assert.Equal(ObjectNameFormat.Sha1, footer.Header.ObjectNameFormat);
+ Assert.Equal(0, footer.RefIndexPosition);
+ }
+
+ [Fact]
+ public void ReadFooter_InvalidChecksum()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteFooter(
+ writer => writer.WriteHeader(
+ magic: 0x52454654,
+ version: 1,
+ blockSize: 0x000100,
+ minUpdate: 0,
+ maxUpdate: 0,
+ hashId: "s256"),
+ refIndexPosition: 0,
+ objPosition: 0,
+ objIndexPosition: 0,
+ logPosition: 0,
+ logIndexPosition: 0);
+
+ var footerBytes = writer.ToArray();
+
+ // Corrupt checksum (last4 bytes)
+ footerBytes[^1] ^= 0xFF;
+
+ using var reader = Create(footerBytes);
+ Assert.Throws(() => reader.ReadFooter());
+ }
+
+ [Fact]
+ public void ReadFooter_InvalidRefIndexPosition()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteFooter(
+ writer => writer.WriteHeader(
+ magic: 0x52454654,
+ version: 2,
+ blockSize: 0x000200,
+ minUpdate: 0,
+ maxUpdate: 0,
+ hashId: "sha1"),
+ refIndexPosition: 10_000_000,
+ objPosition: 0,
+ objIndexPosition: 0,
+ logPosition: 0,
+ logIndexPosition: 0);
+
+ using var reader = Create(writer.ToArray());
+ Assert.Throws(() => reader.ReadFooter());
+ }
+
+ [Fact]
+ public void ReadRefRecord_Deletion()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 0, // deletion
+ suffix: Encoding.ASCII.GetBytes("refs/heads/main"),
+ updateIndexDelta: 0);
+
+ using var reader = Create(writer);
+
+ var header = new GitRefTableReader.Header()
+ {
+ BlockSize = 0x100,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var record = reader.ReadRefRecord(header, priorName: "");
+ Assert.Equal("refs/heads/main", record.RefName);
+ Assert.Null(record.ObjectName);
+ Assert.Null(record.SymbolicRef);
+ }
+
+ [Fact]
+ public void ReadRefRecord_ObjectName()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 1, // object name
+ suffix: Encoding.ASCII.GetBytes("refs/heads/main"),
+ updateIndexDelta: 0,
+ objectName: GetObjectName(0x01));
+
+ using var reader = Create(writer);
+
+ var header = new GitRefTableReader.Header()
+ {
+ BlockSize = 0x100,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var record = reader.ReadRefRecord(header, priorName: "");
+ Assert.Equal("refs/heads/main", record.RefName);
+ Assert.Equal("0100000000000000000000000000000000000000", record.ObjectName);
+ Assert.Null(record.SymbolicRef);
+ }
+
+ [Fact]
+ public void ReadRefRecord_ObjectNamePeeled()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 2, // object name and peeled target
+ suffix: Encoding.ASCII.GetBytes("refs/heads/main"),
+ updateIndexDelta: 0,
+ objectName: GetObjectName(0x12),
+ peeledObjectName: GetObjectName(0x34));
+
+ using var reader = Create(writer);
+
+ var header = new GitRefTableReader.Header()
+ {
+ BlockSize = 0x100,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var record = reader.ReadRefRecord(header, priorName: "");
+ Assert.Equal("refs/heads/main", record.RefName);
+ Assert.Equal("1200000000000000000000000000000000000000", record.ObjectName);
+ Assert.Null(record.SymbolicRef);
+ }
+
+ [Fact]
+ public void ReadRefRecord_SymbolicRef()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 3, // symbolic ref
+ suffix: Encoding.ASCII.GetBytes("refs/heads/main"),
+ updateIndexDelta: 0,
+ symbolicRef: Encoding.ASCII.GetBytes("refs/heads/foo"));
+
+ using var reader = Create(writer);
+
+ var header = new GitRefTableReader.Header()
+ {
+ BlockSize = 0x100,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var record = reader.ReadRefRecord(header, priorName: "");
+ Assert.Equal("refs/heads/main", record.RefName);
+ Assert.Null(record.ObjectName);
+ Assert.Equal("refs/heads/foo", record.SymbolicRef);
+ }
+
+ [Fact]
+ public void ReadRefRecord_SymbolicRef_InvalidEncoding()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 3, // symbolic ref
+ suffix: Encoding.ASCII.GetBytes("refs/heads/main"),
+ updateIndexDelta: 0,
+ symbolicRef: [0x00, 0xD8]); // U+D800: unpaired surrogate
+
+ using var reader = Create(writer);
+
+ var header = new GitRefTableReader.Header()
+ {
+ BlockSize = 0x100,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ Assert.Throws(() => reader.ReadRefRecord(header, priorName: ""));
+ }
+
+ [Fact]
+ public void ReadRefRecord_Prefix()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var prefix = "refs/heads/";
+
+ writer.WriteRefRecord(
+ prefixLength: prefix.Length,
+ valueType: 0, // deletion
+ suffix: Encoding.ASCII.GetBytes("main"),
+ updateIndexDelta: 0);
+
+ using var reader = Create(writer);
+
+ var header = new GitRefTableReader.Header()
+ {
+ BlockSize = 0x100,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var record = reader.ReadRefRecord(header, priorName: prefix + "foo");
+ Assert.Equal("refs/heads/main", record.RefName);
+ }
+
+ [Fact]
+ public void ReadRefRecord_InvalidPrefixLength()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteRefRecord(
+ prefixLength: 100,
+ valueType: 0, // deletion
+ suffix: Encoding.ASCII.GetBytes("main"),
+ updateIndexDelta: 0);
+
+ using var reader = Create(writer);
+
+ var header = new GitRefTableReader.Header()
+ {
+ BlockSize = 0x100,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ Assert.Throws(() => reader.ReadRefRecord(header, priorName: "foo"));
+ }
+
+ [Fact]
+ public void ReadRefRecord_InvalidEncoding()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 0, // deletion
+ suffix: [0x00, 0xD8], // U+D800: unpaired surrogate
+ updateIndexDelta: 0);
+
+ using var reader = Create(writer);
+
+ var header = new GitRefTableReader.Header()
+ {
+ BlockSize = 0x100,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ Assert.Throws(() => reader.ReadRefRecord(header, priorName: ""));
+ }
+
+ [Fact]
+ public void ReadRefIndexRecord_Prefix()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var prefix = "refs/heads/";
+
+ writer.WriteRefIndexRecord(
+ prefixLength: prefix.Length,
+ valueType: 0, // deletion
+ suffix: Encoding.ASCII.GetBytes("main"),
+ blockPosition: 12345);
+
+ using var reader = Create(writer);
+
+ var record = reader.ReadRefIndexRecord(priorName: prefix + "foo");
+ Assert.Equal("refs/heads/main", record.LastRefName);
+ Assert.Equal(12345, record.BlockPosition);
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ public void ReadRefIndexRecord_InvalidType(byte type)
+ {
+ var writer = new GitRefTableTestWriter();
+
+ writer.WriteRefIndexRecord(
+ prefixLength: 0,
+ valueType: type,
+ suffix: Encoding.ASCII.GetBytes("main"),
+ blockPosition: 1234);
+
+ using var reader = Create(writer);
+ Assert.Throws(() => reader.ReadRefIndexRecord(priorName: ""));
+ }
+
+ [Theory]
+ [CombinatorialData]
+ public void ReadRestartOffsets(bool isFirst)
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 10,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var offset1 = 0;
+ var offset2 = 0;
+
+ writer.WriteBlock(isFirst ? header : null, 'r', (writer, blockStart) =>
+ {
+ // ref_record+
+ offset1 = (int)(writer.Position - blockStart);
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 1, // object name
+ suffix: Encoding.ASCII.GetBytes("refs/heads/main"),
+ updateIndexDelta: 0,
+ objectName: GetObjectName(0x01));
+
+ writer.WriteRefRecord(
+ prefixLength: "refs/heads/".Length,
+ valueType: 1, // object name
+ suffix: Encoding.ASCII.GetBytes("foo"),
+ updateIndexDelta: 0,
+ objectName: GetObjectName(0x02));
+
+ writer.WriteRefRecord(
+ prefixLength: "refs/heads/".Length,
+ valueType: 1, // object name
+ suffix: Encoding.ASCII.GetBytes("bar"),
+ updateIndexDelta: 0,
+ objectName: GetObjectName(0x03));
+
+ offset2 = (int)writer.Position;
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 1, // object name
+ suffix: Encoding.ASCII.GetBytes("baz"),
+ updateIndexDelta: 0,
+ objectName: GetObjectName(0x04));
+
+ // uint24(restart_offset)+
+ writer.WriteRestartOffsets([offset1, offset2]);
+ });
+
+ using var reader = Create(writer);
+
+ if (isFirst)
+ {
+ _ = reader.ReadHeader();
+ }
+
+ Assert.Equal((byte)'r', reader.ReadByte());
+
+ var offsets = reader.ReadRestartOffsets(header, blockLengthLimited: false, out var blockStart, out var blockEnd);
+
+ Assert.Equal([offset1, offset2, (byte)(blockEnd - sizeof(ushort) - 2 * 3)], offsets);
+ Assert.Equal(0, blockStart);
+ Assert.Equal(writer.Stream.Length, blockEnd);
+ }
+
+ [Fact]
+ public void ReadRestartOffsets_InvalidBlockLength_LargerThanHeaderBlockSize()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 0x100,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ writer.WriteBlock(null, 'r', (writer, _) =>
+ {
+ // dummy content:
+ writer.Stream.WriteByte(0);
+ });
+
+ using var reader = Create(writer);
+
+ Assert.Equal((byte)'r', reader.ReadByte());
+
+ Assert.Throws(() => reader.ReadRestartOffsets(header, blockLengthLimited: true, out _, out _));
+ }
+
+ [Fact]
+ public void ReadRestartOffsets_InvalidBlockLength_BlockSizeTooSmall()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 1,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ writer.WriteBlock(null, 'r', (writer, _) =>
+ {
+ // dummy content:
+ writer.Stream.WriteByte(0);
+ });
+
+ using var reader = Create(writer);
+
+ Assert.Equal((byte)'r', reader.ReadByte());
+
+ Assert.Throws(() => reader.ReadRestartOffsets(header, blockLengthLimited: false, out _, out _));
+ }
+
+ public enum OffsetError
+ {
+ Empty,
+ Unordered,
+ OutOfBlock,
+ }
+
+ [Theory]
+ [CombinatorialData]
+ public void ReadRestartOffsets_Unordered(OffsetError error)
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 10,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var offset1 = 0;
+ var offset2 = 0;
+
+ writer.WriteBlock(header: null, 'r', (writer, blockStart) =>
+ {
+ // ref_record+
+ offset1 = (int)(writer.Position - blockStart);
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 1, // object name
+ suffix: Encoding.ASCII.GetBytes("refs/heads/main1"),
+ updateIndexDelta: 0,
+ objectName: GetObjectName(0x01));
+
+ writer.WriteRefRecord(
+ prefixLength: "refs/heads/".Length,
+ valueType: 1, // object name
+ suffix: Encoding.ASCII.GetBytes("foo"),
+ updateIndexDelta: 0,
+ objectName: GetObjectName(0x02));
+
+ offset2 = (int)(writer.Position - blockStart);
+
+ writer.WriteRefRecord(
+ prefixLength: 0,
+ valueType: 1, // object name
+ suffix: Encoding.ASCII.GetBytes("refs/heads/main2"),
+ updateIndexDelta: 0,
+ objectName: GetObjectName(0x03));
+
+ // uint24(restart_offset)+
+ writer.WriteRestartOffsets(
+ error switch
+ {
+ OffsetError.Empty => [],
+ OffsetError.Unordered => [offset2, offset1],
+ OffsetError.OutOfBlock => [offset1, 10000],
+ _ => throw new InvalidOperationException()
+ });
+ });
+
+ using var reader = Create(writer);
+
+ Assert.Equal((byte)'r', reader.ReadByte());
+
+ Assert.Throws(() => reader.ReadRestartOffsets(header, blockLengthLimited: false, out _, out _));
+ }
+
+ [Fact]
+ public void SearchBlock_RefRecord()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 1000,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var refBlock1 = writer.WriteRefBlock(header: null,
+ [
+ ("", "refs/heads/a", 0x01),
+ ("refs/heads/", "b", 0x02),
+ ("refs/heads/", "c", 0x03),
+ ("", "refs/heads/d", 0x04),
+ ]);
+
+ using var reader = Create(writer);
+
+ writer.Position = refBlock1;
+ var record = reader.SearchBlock(header, "refs/heads/c");
+ Assert.NotNull(record);
+ Assert.Equal("refs/heads/c", record.Value.RefName);
+ Assert.Equal("0300000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = 0;
+ record = reader.SearchBlock(header, "refs/heads/a");
+ Assert.NotNull(record);
+ Assert.Equal("refs/heads/a", record.Value.RefName);
+ Assert.Equal("0100000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = 0;
+ record = reader.SearchBlock(header, "refs/heads/b");
+ Assert.NotNull(record);
+ Assert.Equal("refs/heads/b", record.Value.RefName);
+ Assert.Equal("0200000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = 0;
+ record = reader.SearchBlock(header, "refs/heads/d");
+ Assert.NotNull(record);
+ Assert.Equal("refs/heads/d", record.Value.RefName);
+ Assert.Equal("0400000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = 0;
+ record = reader.SearchBlock(header, "refs/heads");
+ Assert.Null(record);
+
+ writer.Position = 0;
+ record = reader.SearchBlock(header, "refs/heads/aa");
+ Assert.Null(record);
+
+ writer.Position = 0;
+ record = reader.SearchBlock(header, "refs/heads/bb");
+ Assert.Null(record);
+
+ writer.Position = 0;
+ record = reader.SearchBlock(header, "refs/heads/cc");
+ Assert.Null(record);
+
+ writer.Position = 0;
+ record = reader.SearchBlock(header, "refs/heads/dd");
+ Assert.Null(record);
+ }
+
+ [Fact]
+ public void SearchBlock_RefIndexRecord()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 1000,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ // I3 -> I1 -> R1
+ // R2
+ // I2 -> R3
+ // R4
+ // R5
+
+ var refBlock1 = writer.WriteRefBlock(header,
+ [
+ ("", "refs/heads/a", 0x01),
+ ("refs/heads/", "b", 0x02),
+ ("refs/heads/", "c", 0x03),
+ ]);
+
+ var refBlock2 = writer.WriteRefBlock(header: null,
+ [
+ ("", "refs/heads/d", 0x04),
+ ("refs/heads/", "e", 0x05),
+ ]);
+
+ var refBlock3 = writer.WriteRefBlock(header: null,
+ [
+ ("", "refs/heads/f", 0x06),
+ ]);
+
+ var refBlock4 = writer.WriteRefBlock(header: null,
+ [
+ ("", "refs/heads/g", 0x07),
+ ]);
+
+ var refBlock5 = writer.WriteRefBlock(header: null,
+ [
+ ("", "refs/heads/h", 0x08),
+ ]);
+
+ var refIndexBlock1 = writer.WriteRefIndexBlock(
+ [
+ ("", "refs/heads/c", refBlock1),
+ ("refs/heads/", "e", refBlock2),
+ ]);
+
+ var refIndexBlock2 = writer.WriteRefIndexBlock(
+ [
+ ("", "refs/heads/f", refBlock3),
+ ("refs/heads/", "g", refBlock4),
+ ]);
+
+ var refIndexBlock3 = writer.WriteRefIndexBlock(
+ [
+ ("", "refs/heads/e", refIndexBlock1),
+ ("refs/heads/", "g", refIndexBlock2),
+ ("", "refs/heads/h", refBlock5),
+ ]);
+
+ using var reader = Create(writer);
+
+ writer.Position = refIndexBlock3;
+ var record = reader.SearchBlock(header, "refs/heads/a");
+ Assert.NotNull(record);
+ Assert.Equal("0100000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/b");
+ Assert.NotNull(record);
+ Assert.Equal("0200000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/c");
+ Assert.NotNull(record);
+ Assert.Equal("0300000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/d");
+ Assert.NotNull(record);
+ Assert.Equal("0400000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/e");
+ Assert.NotNull(record);
+ Assert.Equal("0500000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/f");
+ Assert.NotNull(record);
+ Assert.Equal("0600000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/g");
+ Assert.NotNull(record);
+ Assert.Equal("0700000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/h");
+ Assert.NotNull(record);
+ Assert.Equal("0800000000000000000000000000000000000000", record.Value.ObjectName);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads");
+ Assert.Null(record);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/aa");
+ Assert.Null(record);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/bb");
+ Assert.Null(record);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/cc");
+ Assert.Null(record);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/dd");
+ Assert.Null(record);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/ee");
+ Assert.Null(record);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/ff");
+ Assert.Null(record);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/gg");
+ Assert.Null(record);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/hh");
+ Assert.Null(record);
+
+ writer.Position = refIndexBlock3;
+ record = reader.SearchBlock(header, "refs/heads/z");
+ Assert.Null(record);
+ }
+
+ [Fact]
+ public void TryFindReference_RefIndex()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 1000,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var refBlock1 = writer.WriteRefBlock(header,
+ [
+ ("", "refs/heads/a", 0x01),
+ ("refs/heads/", "b", "sym-ref"),
+ ]);
+
+ var refIndexBlock1 = writer.WriteRefIndexBlock(
+ [
+ ("", "refs/heads/b", refBlock1),
+ ]);
+
+ writer.WriteFooter(header, refIndexBlock1);
+
+ using var reader = Create(writer);
+
+ Assert.True(reader.TryFindReference("refs/heads/a", out var objectName, out var symRef));
+ Assert.Equal("0100000000000000000000000000000000000000", objectName);
+ Assert.Null(symRef);
+
+ Assert.True(reader.TryFindReference("refs/heads/b", out objectName, out symRef));
+ Assert.Equal("sym-ref", symRef);
+ Assert.Null(objectName);
+
+ Assert.False(reader.TryFindReference("refs/heads/c", out objectName, out symRef));
+ Assert.Null(symRef);
+ Assert.Null(objectName);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(100)]
+ public void TryFindReference_NoRefIndex(int blockSize)
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = blockSize,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ writer.WriteRefBlock(header,
+ [
+ ("", "refs/heads/a", 0x01),
+ ("refs/heads/", "b", "sym-ref"),
+ ]);
+
+ writer.WritePadding(blockSize);
+
+ writer.WriteRefBlock(header: null,
+ [
+ ("", "refs/heads/c", 0x02),
+ ]);
+
+ writer.WritePadding(blockSize);
+
+ writer.WriteFooter(header, refIndexPosition: 0);
+
+ using var reader = Create(writer);
+
+ Assert.True(reader.TryFindReference("refs/heads/a", out var objectName, out var symRef));
+ Assert.Equal("0100000000000000000000000000000000000000", objectName);
+ Assert.Null(symRef);
+
+ Assert.True(reader.TryFindReference("refs/heads/b", out objectName, out symRef));
+ Assert.Equal("sym-ref", symRef);
+ Assert.Null(objectName);
+
+ Assert.True(reader.TryFindReference("refs/heads/c", out objectName, out symRef));
+ Assert.Equal("0200000000000000000000000000000000000000", objectName);
+ Assert.Null(symRef);
+
+ Assert.False(reader.TryFindReference("refs/heads/d", out objectName, out symRef));
+ Assert.Null(symRef);
+ Assert.Null(objectName);
+ }
+
+ [Fact]
+ public void TryFindReference_RefBlockFollowedByObjBlock()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 0,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ writer.WriteRefBlock(header,
+ [
+ ("", "refs/heads/a", 0x01)
+ ]);
+
+ writer.WriteBlock(header, 'o', (writer, _) =>
+ {
+ writer.Stream.WriteByte(0);
+ });
+
+ writer.WriteFooter(header, refIndexPosition: 0);
+
+ using var reader = Create(writer);
+
+ Assert.True(reader.TryFindReference("refs/heads/a", out var objectName, out var symRef));
+ Assert.Equal("0100000000000000000000000000000000000000", objectName);
+ Assert.Null(symRef);
+
+ Assert.False(reader.TryFindReference("refs/heads/b", out objectName, out symRef));
+ Assert.Null(objectName);
+ Assert.Null(symRef);
+ }
+
+ [Fact]
+ public void TryFindReference_InvalidBlockType()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 0,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ var refBlock1 = writer.WriteRefBlock(header,
+ [
+ ("", "refs/heads/a", 0x01)
+ ]);
+
+ writer.WriteRefIndexBlock(
+ [
+ ("", "refs/heads/b", refBlock1),
+ ]);
+
+ writer.WriteFooter(header, refIndexPosition: 0);
+
+ using var reader = Create(writer);
+
+ Assert.True(reader.TryFindReference("refs/heads/a", out var objectName, out var symRef));
+ Assert.Equal("0100000000000000000000000000000000000000", objectName);
+ Assert.Null(symRef);
+
+ Assert.Throws(() => reader.TryFindReference("refs/heads/b", out _, out _));
+ }
+
+ [Fact]
+ public void TryFindReference_NoRefBlock()
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 0,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ writer.WriteBlock(header, 'o', (writer, _) =>
+ {
+ writer.Stream.WriteByte(0);
+ });
+
+ writer.WriteFooter(header, refIndexPosition: 0);
+
+ using var reader = Create(writer);
+ Assert.Throws(() => reader.TryFindReference("refs/heads/a", out _, out _));
+ }
+}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRefTableTestWriter.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRefTableTestWriter.cs
new file mode 100644
index 00000000..d29e1d0a
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRefTableTestWriter.cs
@@ -0,0 +1,359 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Hashing;
+using System.Linq;
+using System.Text;
+
+namespace Microsoft.Build.Tasks.Git.UnitTests;
+
+internal class GitRefTableTestWriter
+{
+ public MemoryStream Stream { get; } = new();
+
+ public long Position
+ {
+ get => Stream.Position;
+ set => Stream.Position = value;
+ }
+
+ public byte[] ToArray()
+ => Stream.ToArray();
+
+ public void WriteBytes(byte[] data)
+ => Stream.Write(data, 0, data.Length);
+
+ public void WriteUInt16BE(int value)
+ {
+ Stream.WriteByte((byte)((value >> 8) & 0xFF));
+ Stream.WriteByte((byte)(value & 0xFF));
+ }
+
+ public void WriteUInt24BE(int value)
+ {
+ Stream.WriteByte((byte)((value >> 16) & 0xFF));
+ Stream.WriteByte((byte)((value >> 8) & 0xFF));
+ Stream.WriteByte((byte)(value & 0xFF));
+ }
+
+ public void WriteUInt32BE(uint value)
+ {
+ Stream.WriteByte((byte)((value >> 24) & 0xFF));
+ Stream.WriteByte((byte)((value >> 16) & 0xFF));
+ Stream.WriteByte((byte)((value >> 8) & 0xFF));
+ Stream.WriteByte((byte)(value & 0xFF));
+ }
+
+ public void WriteUInt64BE(ulong value)
+ {
+ Stream.WriteByte((byte)((value >> 56) & 0xFF));
+ Stream.WriteByte((byte)((value >> 48) & 0xFF));
+ Stream.WriteByte((byte)((value >> 40) & 0xFF));
+ Stream.WriteByte((byte)((value >> 32) & 0xFF));
+ Stream.WriteByte((byte)((value >> 24) & 0xFF));
+ Stream.WriteByte((byte)((value >> 16) & 0xFF));
+ Stream.WriteByte((byte)((value >> 8) & 0xFF));
+ Stream.WriteByte((byte)(value & 0xFF));
+ }
+
+ public void WriteVarInt(int value)
+ {
+ const int V2 = 1 << 7;
+ const int V3 = V2 + (1 << 14);
+ const int V4 = V3 + (1 << 21);
+ const int V5 = V4 + (1 << 28);
+
+ int shift;
+ if (value >= V5)
+ {
+ value -= V5;
+ shift = 28;
+ }
+ else if (value >= V4)
+ {
+ value -= V4;
+ shift = 21;
+ }
+ else if (value >= V3)
+ {
+ value -= V3;
+ shift = 14;
+ }
+ else if (value >= V2)
+ {
+ value -= V2;
+ shift = 7;
+ }
+ else
+ {
+ shift = 0;
+ }
+
+ for (var s = shift; s >= 0; s -= 7)
+ {
+ Stream.WriteByte((byte)((value >> s) & 0x7f | (s > 0 ? 0x80 : 0)));
+ }
+ }
+
+ public void WriteFooter(
+ Action writeHeader,
+ ulong refIndexPosition,
+ ulong objPosition,
+ ulong objIndexPosition,
+ ulong logPosition,
+ ulong logIndexPosition)
+ {
+ var startPosition = (int)Stream.Position;
+ writeHeader(this);
+ WriteUInt64BE(refIndexPosition);
+ WriteUInt64BE(objPosition);
+ WriteUInt64BE(objIndexPosition);
+ WriteUInt64BE(logPosition);
+ WriteUInt64BE(logIndexPosition);
+ var endPosition = (int)Stream.Position;
+
+ WriteUInt32BE(Crc32.HashToUInt32(Stream.ToArray().AsSpan()[startPosition..endPosition]));
+ }
+
+ public void WriteFooter(GitRefTableReader.Header header, long refIndexPosition)
+ {
+ WriteFooter(
+ writer => writer.WriteHeader(header),
+ (ulong)refIndexPosition,
+ objPosition: 0,
+ objIndexPosition: 0,
+ logPosition: 0,
+ logIndexPosition: 0);
+ }
+
+ public void WriteHeader(
+ uint magic,
+ byte version,
+ int blockSize,
+ ulong minUpdate,
+ ulong maxUpdate,
+ string? hashId = null)
+ {
+ WriteUInt32BE(magic);
+ Stream.WriteByte(version);
+ WriteUInt24BE(blockSize);
+ WriteUInt64BE(minUpdate);
+ WriteUInt64BE(maxUpdate);
+ if (hashId != null)
+ {
+ WriteBytes(Encoding.ASCII.GetBytes(hashId));
+ }
+ }
+
+ public void WriteHeader(GitRefTableReader.Header header)
+ {
+ WriteHeader(
+ magic: 0x52454654, // 'RFTB'
+ version: 1,
+ blockSize: header.BlockSize,
+ minUpdate: 0,
+ maxUpdate: 0);
+ }
+
+ public void WriteNameAndValueType(
+ int prefixLength,
+ int suffixLength,
+ byte valueType,
+ byte[] suffix)
+ {
+ WriteVarInt(prefixLength);
+ WriteVarInt((suffixLength << 3) | valueType);
+ WriteBytes(suffix);
+ }
+
+ public void WriteRefRecord(
+ int prefixLength,
+ byte valueType,
+ byte[] suffix,
+ int updateIndexDelta,
+ byte[]? objectName = null,
+ byte[]? peeledObjectName = null,
+ byte[]? symbolicRef = null)
+ {
+ WriteNameAndValueType(
+ prefixLength,
+ suffix.Length,
+ valueType,
+ suffix);
+
+ WriteVarInt(updateIndexDelta);
+
+ if (objectName != null)
+ {
+ WriteBytes(objectName);
+ }
+
+ if (peeledObjectName != null)
+ {
+ WriteBytes(peeledObjectName);
+ }
+
+ if (symbolicRef != null)
+ {
+ WriteVarInt(symbolicRef.Length);
+ WriteBytes(symbolicRef);
+ }
+ }
+
+ public void WriteRefIndexRecord(
+ int prefixLength,
+ byte valueType,
+ byte[] suffix,
+ long blockPosition)
+ {
+ WriteNameAndValueType(
+ prefixLength,
+ suffix.Length,
+ valueType,
+ suffix);
+
+ WriteVarInt((int)blockPosition);
+ }
+
+ public void WriteRestartOffsets(params int[] offsets)
+ {
+ foreach (var offset in offsets)
+ {
+ WriteUInt24BE(offset);
+ }
+
+ WriteUInt16BE(offsets.Length);
+ }
+
+ public long WriteBlock(GitRefTableReader.Header? header, char kind, Action writeContent)
+ {
+ var blockStart = Stream.Position;
+
+ if (header != null)
+ {
+ WriteHeader(
+ magic: 0x52454654, // 'RFTB'
+ version: 1,
+ blockSize: header.Value.BlockSize,
+ minUpdate: 0,
+ maxUpdate: 0);
+ }
+
+ var blockTypePosition = Stream.Position;
+
+ Stream.WriteByte((byte)kind);
+
+ // uint24(block_len)
+ var lengthPosition = Stream.Position;
+ WriteUInt24BE(0);
+
+ writeContent(this, blockStart);
+
+ var blockEnd = Stream.Position;
+
+ // patch length:
+ Stream.Position = lengthPosition;
+ WriteUInt24BE((int)(blockEnd - blockStart));
+
+ Stream.Position = blockEnd;
+
+ return blockTypePosition;
+ }
+
+ public static byte[] GetObjectName(byte b0, ObjectNameFormat format = ObjectNameFormat.Sha1)
+ {
+ var name = new byte[format.HashSize];
+ name[0] = b0;
+ return name;
+ }
+
+ public readonly record struct NameOrSymRef(byte[]? ObjectName, byte[]? SymbolicReference)
+ {
+ public static implicit operator NameOrSymRef(string symbolicReference) => new(null, Encoding.UTF8.GetBytes(symbolicReference));
+ public static implicit operator NameOrSymRef(byte objectName) => new(GetObjectName(objectName), null);
+ }
+
+ public long WriteRefBlock(
+ GitRefTableReader.Header? header,
+ params (string prefix, string suffix, NameOrSymRef name)[] records)
+ {
+ return WriteBlock(header, 'r', (writer, blockStart) =>
+ {
+ var offsets = new List();
+
+ foreach (var (prefix, suffix, name) in records)
+ {
+ if (prefix == "")
+ {
+ offsets.Add((int)(writer.Stream.Position - blockStart));
+ }
+
+ writer.WriteRefRecord(
+ prefixLength: prefix.Length,
+ valueType: (byte)(name.ObjectName != null ? 1 : 3),
+ suffix: Encoding.ASCII.GetBytes(suffix),
+ updateIndexDelta: 0,
+ objectName: name.ObjectName,
+ symbolicRef: name.SymbolicReference);
+ }
+
+ WriteRestartOffsets([.. offsets]);
+ });
+ }
+
+ public long WriteRefIndexBlock(params (string prefix, string suffix, long blockPosition)[] records)
+ {
+ return WriteBlock(header: null, 'i', (writer, blockStart) =>
+ {
+ var offsets = new List();
+
+ foreach (var (prefix, suffix, blockPosition) in records)
+ {
+ if (prefix == "")
+ {
+ offsets.Add((int)(Stream.Position - blockStart));
+ }
+
+ writer.WriteRefIndexRecord(
+ prefixLength: prefix.Length,
+ valueType: 0,
+ suffix: Encoding.ASCII.GetBytes(suffix),
+ blockPosition: blockPosition);
+ }
+
+ WriteRestartOffsets([.. offsets]);
+ });
+ }
+
+ public void WritePadding(int alignment)
+ {
+ if (alignment == 0)
+ return;
+
+ while (Stream.Position % alignment != 0)
+ {
+ Stream.WriteByte(0);
+ }
+ }
+
+ public static byte[] GetRefTableBlob((string reference, NameOrSymRef name)[] references)
+ {
+ var writer = new GitRefTableTestWriter();
+
+ var header = new GitRefTableReader.Header()
+ {
+ Size = 24,
+ BlockSize = 0,
+ ObjectNameFormat = ObjectNameFormat.Sha1
+ };
+
+ writer.WriteRefBlock(header, [.. references.Select(r => ("", r.reference, r.name))]);
+ writer.WriteFooter(header, refIndexPosition: 0);
+
+ return writer.ToArray();
+ }
+}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs
index c61fb555..1015de5a 100644
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the License.txt file in the project root for more information.
-using System;
+
using System.IO;
using System.Linq;
using TestUtilities;
@@ -25,7 +25,7 @@ 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);
+ using var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);
Assert.Equal("0123456789ABCDEFabcdef000000000000000000", resolver.ResolveReference("0123456789ABCDEFabcdef000000000000000000"));
@@ -33,13 +33,40 @@ public void ResolveReference()
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(
+ "0123456789ABCDEFabcdef00000000000000000000000000000000000000000000",
+ resolver.ResolveReference("0123456789ABCDEFabcdef00000000000000000000000000000000000000000000"));
+
+ 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()
{
@@ -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);
+ using var resolver1 = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);
- Assert.Throws(() => resolver.ResolveReference("ref: refs/heads/rec1"));
- Assert.Throws(() => resolver.ResolveReference("ref: xyz/heads/rec1"));
- Assert.Throws(() => resolver.ResolveReference("ref:refs/heads/rec1"));
- Assert.Throws(() => resolver.ResolveReference("refs/heads/rec1"));
- Assert.Throws(() => resolver.ResolveReference(new string('0', 39)));
- Assert.Throws(() => resolver.ResolveReference(new string('0', 41)));
+ Assert.Throws(() => resolver1.ResolveReference("ref: refs/heads/rec1"));
+ Assert.Throws(() => resolver1.ResolveReference("ref: xyz/heads/rec1"));
+ Assert.Throws(() => resolver1.ResolveReference("ref:refs/heads/rec1"));
+ Assert.Throws(() => resolver1.ResolveReference("refs/heads/rec1"));
+
+ // Invalid SHA1 hash lengths
+ Assert.Throws(() => resolver1.ResolveReference(new string('0', ObjectNameFormat.Sha1.HashSize * 2 - 1)));
+ Assert.Throws(() => resolver1.ResolveReference(new string('0', ObjectNameFormat.Sha1.HashSize * 2 + 1)));
+ Assert.Throws(() => resolver1.ResolveReference(new string('0', ObjectNameFormat.Sha256.HashSize * 2)));
+
+ using var resolver2 = new GitReferenceResolver(gitDir.Path, commonDir.Path, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);
+
+ // Invalid SHA256 hash lengths
+ Assert.Throws(() => resolver2.ResolveReference(new string('0', ObjectNameFormat.Sha256.HashSize * 2 - 1)));
+ Assert.Throws(() => resolver2.ResolveReference(new string('0', ObjectNameFormat.Sha256.HashSize * 2 + 1)));
+ Assert.Throws(() => resolver2.ResolveReference(new string('0', ObjectNameFormat.Sha1.HashSize * 2)));
}
[Fact]
@@ -80,13 +117,38 @@ 2222222222222222222222222222222222222222 refs/heads/br2
refsHeadsDir.CreateFile("br1").WriteAllText("ref: refs/heads/br2");
- var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path);
+ 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()
{
@@ -101,7 +163,8 @@ 6666666666666666666666666666666666666666 y z
7777777777777777777777777777777777777777 refs/heads/br
";
- var actual = GitReferenceResolver.ReadPackedReferences(new StringReader(packedRefs), "");
+ using var resolver = new GitReferenceResolver(TempRoot.Root, TempRoot.Root, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);
+ var actual = resolver.ReadPackedReferences(new StringReader(packedRefs), "");
AssertEx.SetEqual(new[]
{
@@ -110,26 +173,88 @@ 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), "");
+
+ 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), ""));
+ using var resolver = new GitReferenceResolver(TempRoot.Root, TempRoot.Root, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha256);
+ Assert.Empty(resolver.ReadPackedReferences(new StringReader(content), ""));
}
[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(() => GitReferenceResolver.ReadPackedReferences(new StringReader(content), ""));
+ using var resolver = new GitReferenceResolver(TempRoot.Root, TempRoot.Root, ReferenceStorageFormat.LooseFiles, ObjectNameFormat.Sha1);
+ Assert.Throws(() => resolver.ReadPackedReferences(new StringReader(content), ""));
+ }
+
+ [Fact]
+ public void ResolveReference_RefTable()
+ {
+ using var temp = new TempRoot();
+
+ var gitDir = temp.CreateDirectory();
+ var commonDir = temp.CreateDirectory();
+
+ var refTableDir = gitDir.CreateDirectory("reftable");
+
+ refTableDir.CreateFile("tables.list").WriteAllText("""
+ 2.ref
+ 1.ref
+ """);
+
+ 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, ObjectNameFormat.Sha1))
+ {
+ Assert.Equal("0100000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/a"));
+ Assert.Equal("0200000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/c"));
+
+ // 2.ref shouldn't be opened until needed:
+ ref2 = refTableDir.CreateFile("2.ref").WriteAllBytes(GitRefTableTestWriter.GetRefTableBlob([("refs/heads/b", 0x03), ("refs/heads/c", 0x04)]));
+
+ Assert.Equal("0300000000000000000000000000000000000000", resolver.ResolveReference("ref: refs/heads/b"));
+ Assert.Null(resolver.ResolveReference("ref: refs/heads/d"));
+ }
+
+ // files should have been closed:
+ File.WriteAllBytes(ref1.Path, [0]);
+ File.WriteAllBytes(ref2.Path, [0]);
}
}
}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs
index 7ba9993d..e32f3af2 100644
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs
@@ -65,7 +65,9 @@ public void TryFindRepository_Worktree_Realistic()
{
using var temp = new TempRoot();
- var mainWorkingDir = temp.CreateDirectory();
+ var repoDir = temp.CreateDirectory();
+
+ var mainWorkingDir = repoDir.CreateDirectory("main");
var mainWorkingSubDir = mainWorkingDir.CreateDirectory("A");
var mainGitDir = mainWorkingDir.CreateDirectory(".git");
mainGitDir.CreateFile("HEAD");
@@ -73,13 +75,17 @@ public void TryFindRepository_Worktree_Realistic()
var worktreesDir = mainGitDir.CreateDirectory("worktrees");
var worktreeGitDir = worktreesDir.CreateDirectory("myworktree");
var worktreeGitSubDir = worktreeGitDir.CreateDirectory("B");
- var worktreeDir = temp.CreateDirectory();
+ var worktreeDir = repoDir.CreateDirectory("worktree");
var worktreeSubDir = worktreeDir.CreateDirectory("C");
- var worktreeGitFile = worktreeDir.CreateFile(".git").WriteAllText("gitdir: " + worktreeGitDir + " \r\n\t\v");
+
+ // test relative path to work tree dir:
+ var worktreeGitFile = worktreeDir.CreateFile(".git").WriteAllText("gitdir: ../main/.git/worktrees/myworktree \r\n\t\v");
worktreeGitDir.CreateFile("HEAD");
worktreeGitDir.CreateFile("commondir").WriteAllText("../..\n");
- worktreeGitDir.CreateFile("gitdir").WriteAllText(worktreeGitFile.Path + " \r\n\t\v");
+
+ // test relative path to work tree .git file:
+ worktreeGitDir.CreateFile("gitdir").WriteAllText("../../../../worktree/.git \r\n\t\v");
// start under main repository directory:
Assert.True(GitRepository.TryFindRepository(mainWorkingSubDir.Path, out var location));
@@ -226,6 +232,8 @@ public void OpenRepository_Version1_Extensions()
preciousObjects = true
partialClone = promisor_remote
worktreeConfig = true
+ relativeWorktrees = true
+ objectformat = sha256
");
Assert.True(GitRepository.TryFindRepository(gitDir.Path, out var location));
@@ -264,6 +272,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()
{
@@ -307,7 +345,7 @@ public void OpenRepository_Worktree_GitdirFileMissing()
Assert.Equal(worktreeGitDir.Path, location.GitDirectory);
Assert.Equal(mainGitDir.Path, location.CommonDirectory);
Assert.Equal(worktreeDir.Path, location.WorkingDirectory);
-
+
var repository = GitRepository.OpenRepository(location, GitEnvironment.Empty);
Assert.Equal(repository.GitDirectory, location.GitDirectory);
Assert.Equal(repository.CommonDirectory, location.CommonDirectory);
@@ -346,7 +384,7 @@ public void OpenRepository_Worktree_GitdirFileDifferentPath()
var repository = GitRepository.OpenRepository(location, GitEnvironment.Empty);
Assert.Equal(repository.GitDirectory, location.GitDirectory);
Assert.Equal(repository.CommonDirectory, location.CommonDirectory);
-
+
// actual working dir is not affected:
Assert.Equal(worktreeDir.Path, location.WorkingDirectory);
}
@@ -503,7 +541,7 @@ public void GetSubmoduleHeadCommitSha()
submoduleGitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master");
Assert.Equal("0000000000000000000000000000000000000000",
- GitRepository.GetSubmoduleReferenceResolver(submoduleWorkingDir.Path)?.ResolveHeadReference());
+ GitRepository.GetSubmoduleReferenceResolver(submoduleWorkingDir.Path, GitEnvironment.Empty)?.ResolveHeadReference());
}
[Fact]
@@ -523,7 +561,7 @@ public void GetOldStyleSubmoduleHeadCommitSha()
oldStyleSubmoduleGitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/branch1");
Assert.Equal("1111111111111111111111111111111111111111",
- GitRepository.GetSubmoduleReferenceResolver(oldStyleSubmoduleWorkingDir.Path)?.ResolveHeadReference());
+ GitRepository.GetSubmoduleReferenceResolver(oldStyleSubmoduleWorkingDir.Path, GitEnvironment.Empty)?.ResolveHeadReference());
}
[Fact]
@@ -537,7 +575,38 @@ public void GetSubmoduleHeadCommitSha_NoGitFile()
var submoduleGitDir = temp.CreateDirectory();
var submoduleWorkingDir = workingDir.CreateDirectory("sub").CreateDirectory("abc");
- Assert.Null(GitRepository.GetSubmoduleReferenceResolver(submoduleWorkingDir.Path)?.ResolveHeadReference());
+ Assert.Null(GitRepository.GetSubmoduleReferenceResolver(submoduleWorkingDir.Path, GitEnvironment.Empty)?.ResolveHeadReference());
+ }
+
+ [Fact]
+ public void GetSubmoduleHeadCommitSha_RefTable()
+ {
+ using var temp = new TempRoot();
+
+ var gitDir = temp.CreateDirectory();
+ var workingDir = temp.CreateDirectory();
+
+ var submoduleGitDir = temp.CreateDirectory();
+
+ var submoduleWorkingDir = workingDir.CreateDirectory("sub").CreateDirectory("abc");
+ submoduleWorkingDir.CreateFile(".git").WriteAllText("gitdir: " + submoduleGitDir.Path + "\t \v\f\r\n\n\r");
+ submoduleGitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/.invalid");
+
+ submoduleGitDir.CreateFile("config").WriteAllText("""
+ [core]
+ repositoryformatversion = 1
+ [extensions]
+ refStorage = reftable
+ """);
+
+ var refTableDir = submoduleGitDir.CreateDirectory("reftable");
+ refTableDir.CreateFile("tables.list").WriteAllText("1.ref");
+ var ref1 = refTableDir.CreateFile("1.ref").WriteAllBytes(GitRefTableTestWriter.GetRefTableBlob([("HEAD", "refs/heads/main"), ("refs/heads/main", 0x01)]));
+
+ var resolver = GitRepository.GetSubmoduleReferenceResolver(submoduleWorkingDir.Path, GitEnvironment.Empty);
+ Assert.NotNull(resolver);
+ Assert.Equal("0100000000000000000000000000000000000000", resolver.ResolveHeadReference());
+ Assert.Equal("refs/heads/main", resolver.GetBranchForHead());
}
[Fact]
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/TestUtilities.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/TestUtilities.cs
index abfd0721..a49590f4 100644
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/TestUtilities.cs
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/TestUtilities.cs
@@ -3,6 +3,8 @@
// See the License.txt file in the project root for more information.
using System;
+using System.Collections;
+using System.Collections.Generic;
using Microsoft.Build.Framework;
namespace Microsoft.Build.Tasks.Git.UnitTests
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/CharUtils.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/CharUtils.cs
index 730910dd..71d265fb 100644
--- a/src/Microsoft.Build.Tasks.Git/GitDataReader/CharUtils.cs
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/CharUtils.cs
@@ -13,5 +13,8 @@ internal static class CharUtils
public static bool IsHexadecimalDigit(char c)
=> c is >= '0' and <= '9' or >= 'A' and <= 'F' or >= 'a' and <= 'f';
+
+ public static char ToHexDigit(byte nibble)
+ => nibble < 10 ? (char)('0' + nibble) : (char)('a' + (nibble - 10));
}
}
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/CollectionExtensions.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/CollectionExtensions.cs
new file mode 100644
index 00000000..76f4bb14
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/CollectionExtensions.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Build.Tasks.Git;
+
+internal static class CollectionExtensions
+{
+ internal static (TValue? exactMatch, int firstGreater) BinarySearch(
+ this IReadOnlyList array,
+ int index,
+ int length,
+ Func selector,
+ Func compareItemToSearchValue)
+ {
+ var lo = index;
+ var hi = index + length - 1;
+ while (lo <= hi)
+ {
+ var i = lo + ((hi - lo) >> 1);
+ var item = selector(array[i]);
+
+ var comparison = compareItemToSearchValue(item);
+ if (comparison == 0)
+ {
+ return (exactMatch: item, firstGreater: -1);
+ }
+
+ if (comparison < 0)
+ {
+ lo = i + 1;
+ }
+ else
+ {
+ hi = i - 1;
+ }
+ }
+
+ return (exactMatch: default, firstGreater: lo);
+ }
+}
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs
index 854e6785..f0bdc503 100644
--- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs
@@ -60,7 +60,7 @@ internal sealed class Reader
private readonly Func _fileOpener;
private readonly GitEnvironment _environment;
- public Reader(string gitDirectory, string commonDirectory, GitEnvironment environment, Func? fileOpener = null)
+ internal Reader(string gitDirectory, string commonDirectory, GitEnvironment environment, Func? fileOpener = null)
{
NullableDebug.Assert(environment != null);
@@ -102,7 +102,7 @@ internal GitConfig LoadFrom(string path)
{
return Path.Combine(xdgConfigHome, "git");
}
-
+
if (_environment.HomeDirectory != null)
{
return Path.Combine(_environment.HomeDirectory, ".config", "git");
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs
index 4462d0fc..40e13fc6 100644
--- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs
@@ -5,24 +5,98 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
-using System.Text;
namespace Microsoft.Build.Tasks.Git
{
internal sealed partial class GitConfig
{
- public static readonly GitConfig Empty = new GitConfig(ImmutableDictionary>.Empty);
+ public static readonly GitConfig Empty = new(ImmutableDictionary>.Empty);
+
+ private const int SupportedGitRepoFormatVersion = 1;
+
+ private const string CoreSectionName = "core";
+ private const string ExtensionsSectionName = "extensions";
+
+ private const string RefStorageExtensionName = "refstorage";
+ private const string ObjectFormatExtensionName = "objectFormat";
+ private const string RelativeWorktreesExtensionName = "relativeWorktrees";
+ private const string RepositoryFormatVersionVariableName = "repositoryformatversion";
+
+ private static readonly ImmutableArray s_knownExtensions =
+ ["noop", "preciousObjects", "partialclone", "worktreeConfig", RefStorageExtensionName, ObjectFormatExtensionName, RelativeWorktreesExtensionName];
public readonly ImmutableDictionary> Variables;
+ public readonly ReferenceStorageFormat ReferenceStorageFormat;
+
+ ///
+ /// The parsed value of "extensions.objectFormat" variable.
+ ///
+ public ObjectNameFormat ObjectNameFormat { get; }
+ ///
public GitConfig(ImmutableDictionary> variables)
{
- NullableDebug.Assert(variables != null);
Variables = variables;
+
+ 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(),
+ };
+ }
+
+ ///
+ ///
+ ///
+ public static GitConfig ReadRepositoryConfig(string gitDirectory, string commonDirectory, GitEnvironment environment)
+ {
+ var reader = new Reader(gitDirectory, commonDirectory, environment);
+ var config = reader.Load();
+ config.ValidateRepositoryConfig();
+ return config;
+ }
+
+ ///
+ ///
+ ///
+ public static GitConfig ReadSubmoduleConfig(string gitDirectory, string commonDirectory, GitEnvironment environment, string submodulesFile)
+ {
+ var reader = new Reader(gitDirectory, commonDirectory, environment);
+ return reader.LoadFrom(submodulesFile);
+ }
+
+ private void ValidateRepositoryConfig()
+ {
+ // See https://github.com/git/git/blob/master/Documentation/technical/repository-version.txt
+ var versionStr = GetVariableValue(CoreSectionName, RepositoryFormatVersionVariableName);
+ if (TryParseInt64Value(versionStr, out var version) && version > SupportedGitRepoFormatVersion)
+ {
+ throw new NotSupportedException(string.Format(Resources.UnsupportedRepositoryVersion, versionStr, SupportedGitRepoFormatVersion));
+ }
+
+ if (version == 1)
+ {
+ // 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(ExtensionsSectionName) &&
+ !s_knownExtensions.Contains(variable.Key.VariableName, StringComparer.OrdinalIgnoreCase))
+ {
+ throw new NotSupportedException(string.Format(
+ Resources.UnsupportedRepositoryExtension, variable.Key.VariableName, string.Join(", ", s_knownExtensions)));
+ }
+ }
+ }
}
// for testing:
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRefTableReader.Primitives.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRefTableReader.Primitives.cs
new file mode 100644
index 00000000..dbd6b532
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRefTableReader.Primitives.cs
@@ -0,0 +1,147 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Text;
+
+namespace Microsoft.Build.Tasks.Git;
+
+internal sealed partial class GitRefTableReader
+{
+ private const int SizeOfUInt24 = 3;
+
+ private readonly byte[] _buffer = new byte[64];
+
+ private long Position
+ {
+ get => stream.Position;
+ set => stream.Position = value;
+ }
+
+ private long Length
+ => stream.Length;
+
+ internal byte ReadByte()
+ {
+ var result = stream.ReadByte();
+ if (result == -1)
+ {
+ throw new EndOfStreamException();
+ }
+
+ return (byte)result;
+ }
+
+ ///
+ /// See https://git-scm.com/docs/reftable#_varint_encoding
+ /// and "offset encoding" in https://git-scm.com/docs/pack-format#_original_version_1_pack_idx_files_have_the_following_format
+ ///
+ internal int ReadVarInt()
+ {
+ long result = -1;
+
+ while (true)
+ {
+ var b = ReadByte();
+
+ result = (result + 1) << 7 | (long)(b & 0x7f);
+ if (result > int.MaxValue)
+ {
+ throw new InvalidDataException();
+ }
+
+ if ((b & 0x80) == 0)
+ {
+ return (int)result;
+ }
+ }
+ }
+
+ internal ushort ReadUInt16BE()
+ {
+ ReadExactly(_buffer, sizeof(ushort));
+ return (ushort)(_buffer[0] << 8 | _buffer[1]);
+ }
+
+ internal int ReadUInt24BE()
+ {
+ ReadExactly(_buffer, 3);
+ return _buffer[0] << 16 | _buffer[1] << 8 | _buffer[2];
+ }
+
+ internal uint ReadUInt32BE()
+ {
+ ReadExactly(_buffer, sizeof(uint));
+ return
+ (uint)_buffer[0] << 24 |
+ (uint)_buffer[1] << 16 |
+ (uint)_buffer[2] << 8 |
+ _buffer[3];
+ }
+
+ internal ulong ReadUInt64BE()
+ {
+ ReadExactly(_buffer, sizeof(ulong));
+ return
+ (ulong)_buffer[0] << 56 |
+ (ulong)_buffer[1] << 48 |
+ (ulong)_buffer[2] << 40 |
+ (ulong)_buffer[3] << 32 |
+ (ulong)_buffer[4] << 24 |
+ (ulong)_buffer[5] << 16 |
+ (ulong)_buffer[6] << 8 |
+ _buffer[7];
+ }
+
+ internal string ReadObjectName(int objectIdSize)
+ {
+ ReadExactly(_buffer, objectIdSize);
+
+ var builder = new StringBuilder(objectIdSize * 2);
+
+ for (var i = 0; i < objectIdSize; i++)
+ {
+ var b = _buffer[i];
+ builder.Append(CharUtils.ToHexDigit((byte)(b >> 4)));
+ builder.Append(CharUtils.ToHexDigit((byte)(b & 0xf)));
+ }
+
+ return builder.ToString();
+ }
+
+ internal byte[] ReadBytes(int count)
+ {
+ byte[] bytes;
+ try
+ {
+ bytes = new byte[count];
+ }
+ catch (OutOfMemoryException)
+ {
+ throw new InvalidDataException();
+ }
+
+ ReadExactly(bytes);
+ return bytes;
+ }
+
+ internal void ReadExactly(byte[] buffer)
+ => ReadExactly(buffer, buffer.Length);
+
+ internal void ReadExactly(byte[] buffer, int count)
+ {
+ var totalRead = 0;
+ while (totalRead < count)
+ {
+ var read = stream.Read(buffer, totalRead, count - totalRead);
+ if (read == 0)
+ {
+ throw new EndOfStreamException();
+ }
+
+ totalRead += read;
+ }
+ }
+}
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRefTableReader.Structs.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRefTableReader.Structs.cs
new file mode 100644
index 00000000..629eb4d4
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRefTableReader.Structs.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Build.Tasks.Git;
+
+internal sealed partial class GitRefTableReader
+{
+ internal readonly struct Header
+ {
+ ///
+ /// Size of the header (differs between versions).
+ ///
+ public int Size { get; init; }
+
+ ///
+ /// Block size or 0 if blocks are unaligned.
+ ///
+ public int BlockSize { get; init; }
+
+ public ObjectNameFormat ObjectNameFormat { get; init; }
+ }
+
+ internal readonly struct Footer
+ {
+ public const int SizeExcludingHeader = 44;
+
+ public Header Header { get; init; }
+ public long RefIndexPosition { get; init; }
+ }
+
+ internal readonly struct RefRecord
+ {
+ public string RefName { get; init; }
+ public string? ObjectName { get; init; }
+ public string? SymbolicRef { get; init; }
+ }
+
+ internal readonly struct RefIndexRecord
+ {
+ ///
+ /// Last reference in the target block.
+ ///
+ public string LastRefName { get; init; }
+
+ ///
+ /// Position of leaf RefBlock or next level RefIndexBlock from the start of the file.
+ ///
+ public long BlockPosition { get; init; }
+ }
+
+}
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRefTableReader.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRefTableReader.cs
new file mode 100644
index 00000000..e84a4a8c
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRefTableReader.cs
@@ -0,0 +1,545 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Hashing;
+using System.Text;
+
+namespace Microsoft.Build.Tasks.Git;
+
+///
+/// Implements reftable data structure used by Git to store references.
+/// See https://git-scm.com/docs/reftable
+///
+internal sealed partial class GitRefTableReader(Stream stream) : IDisposable
+{
+ private static readonly Encoding s_utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
+
+ private const uint Magic = 'R' << 24 | 'E' << 16 | 'F' << 8 | 'T';
+ private const uint HashId_SHA1 = 's' << 24 | 'h' << 16 | 'a' << 8 | '1';
+ private const uint HashId_SHA256 = 's' << 24 | '2' << 16 | '5' << 8 | '6';
+
+ private const byte BlockTypeRef = (byte)'r';
+ private const byte BlockTypeIndex = (byte)'i';
+
+ private const int MinRefBlockSize =
+ 1 + // block_type
+ 3 + // uint24(block_len)
+ 3 + // uint24(restart_offset)+
+ 2; // uint16(restart_count)
+
+ public void Dispose()
+ {
+ stream.Dispose();
+ }
+
+ ///
+ ///
+ ///
+ public bool TryFindReference(string referenceName, out string? objectName, out string? symbolicReference)
+ {
+ var record = FindRefRecord(referenceName);
+ if (record == null)
+ {
+ objectName = null;
+ symbolicReference = null;
+ return false;
+ }
+
+ objectName = record.Value.ObjectName;
+ symbolicReference = record.Value.SymbolicRef;
+ return true;
+ }
+
+ private RefRecord? FindRefRecord(string referenceName)
+ {
+ Position = 0;
+
+ // Readers are required to read header and validate version it before reading the footer.
+ // https://git-scm.com/docs/reftable#_footer
+ var header = ReadHeader();
+
+ var footerPosition = Length - header.Size - Footer.SizeExcludingHeader;
+
+ // Skip ahead to read the footer.
+ // It contains a copy of header and position of the RefIndex.
+ Position = footerPosition;
+ var footer = ReadFooter();
+
+ if (footer.RefIndexPosition > 0)
+ {
+ Position = footer.RefIndexPosition;
+ if (ReadByte() != BlockTypeIndex)
+ {
+ throw new InvalidDataException();
+ }
+
+ return SearchRefIndex(footer.Header, referenceName);
+ }
+
+ // No RefIndex, read RefBlocks sequentially.
+
+ var blockStartPosition = 0L;
+
+ while (true)
+ {
+ var isFirstBlock = blockStartPosition == 0;
+
+ // The first block starts with header.
+ Position = blockStartPosition + (isFirstBlock ? header.Size : 0);
+ switch (ReadByte())
+ {
+ case BlockTypeRef:
+ break;
+
+ case BlockTypeIndex:
+ throw new InvalidDataException();
+
+ default:
+ return isFirstBlock ? throw new InvalidDataException() : null;
+ }
+
+ var result = SearchRefBlock(footer.Header, referenceName, out var blockEndPosition);
+ if (result != null)
+ {
+ return result;
+ }
+
+ // If the file is unaligned the next block starts at the end of the current block,
+ // otherwise the current block its padded to block size and the next block starts after the padding.
+ blockStartPosition = footer.Header.BlockSize > 0 ? blockStartPosition + footer.Header.BlockSize : blockEndPosition;
+ if (blockStartPosition >= footerPosition)
+ {
+ return null;
+ }
+ }
+ }
+
+ internal Header ReadHeader()
+ {
+ // https://git-scm.com/docs/reftable#_header_version_1
+
+ var headerStartPosition = Position;
+
+ var magic = ReadUInt32BE();
+ if (magic != Magic)
+ {
+ throw new InvalidDataException();
+ }
+
+ var version = ReadByte();
+ if (version is not (1 or 2))
+ {
+ throw new InvalidDataException();
+ }
+
+ // block_size
+ var blockSize = ReadUInt24BE();
+
+ // min_update_index
+ _ = ReadUInt64BE();
+
+ // max_update_index
+ _ = ReadUInt64BE();
+
+ ObjectNameFormat objectNameFormat;
+ if (version == 1)
+ {
+ objectNameFormat = ObjectNameFormat.Sha1;
+ }
+ else
+ {
+ objectNameFormat = ReadUInt32BE() switch
+ {
+ HashId_SHA1 => ObjectNameFormat.Sha1,
+ HashId_SHA256 => ObjectNameFormat.Sha256,
+ _ => throw new InvalidDataException(),
+ };
+ }
+
+ return new Header
+ {
+ Size = (int)(Position - headerStartPosition),
+ BlockSize = blockSize,
+ ObjectNameFormat = objectNameFormat
+ };
+ }
+
+ internal Footer ReadFooter()
+ {
+ var footerStartPosition = Position;
+
+ // header
+ var header = ReadHeader();
+
+ // uint64(ref_index_position)
+ var refIndexPosition = ReadUInt64BE();
+ if (refIndexPosition > (ulong)Length)
+ {
+ throw new InvalidDataException();
+ }
+
+ // obj_position (not need for reference lookup):
+ _ = ReadUInt64BE();
+
+ // obj_index_position (not need for reference lookup):
+ _ = ReadUInt64BE();
+
+ // log_position (not need for reference lookup):
+ _ = ReadUInt64BE();
+
+ // log_index_position (not need for reference lookup):
+ _ = ReadUInt64BE();
+
+ var checksumedSectionEndPosition = Position;
+
+ // uint32(CRC - 32 of above)
+ var checksum = ReadUInt32BE();
+
+ // Validate checksum:
+ Position = footerStartPosition;
+ var buffer = ReadBytes((int)(checksumedSectionEndPosition - footerStartPosition));
+ var computedChecksum = Crc32.HashToUInt32(buffer);
+ if (computedChecksum != checksum)
+ {
+ throw new InvalidDataException();
+ }
+
+ return new Footer
+ {
+ Header = header,
+ RefIndexPosition = (long)refIndexPosition
+ };
+ }
+
+ ///
+ /// RefIndex stores the last reference name of each RefBlock.
+ ///
+ private RefRecord? SearchRefIndex(Header header, string referenceName)
+ {
+ // 'i' (already read)
+ // uint24(block_len)
+ // index_record+
+ // uint24(restart_offset)+
+ // uint16(restart_count)
+ // padding?
+
+ // The index block may exceed the block size specified in the header.
+ var restartOffsets = ReadRestartOffsets(header, blockLengthLimited: false, out var blockStartPosition, out _);
+
+ var (record, firstGreater) = restartOffsets.BinarySearch(
+ index: 0,
+ length: restartOffsets.Length - 1,
+ selector: restartOffset =>
+ {
+ Position = blockStartPosition + restartOffset;
+ return ReadRefIndexRecord(priorName: "");
+ },
+ compareItemToSearchValue: item => StringComparer.Ordinal.Compare(item.LastRefName, referenceName));
+
+ if (firstGreater < 0)
+ {
+ // the last reference of the block is the one we are looking for:
+ Position = record.BlockPosition;
+ return SearchBlock(header, referenceName);
+ }
+
+ // firstGreater points to the first record at a restart offset that contains references with last name larger than the searched value.
+ // The reference is either in the record at firstGreater, or in the previous run.
+
+ if (firstGreater < restartOffsets.Length - 1)
+ {
+ Position = blockStartPosition + restartOffsets[firstGreater];
+ record = ReadRefIndexRecord(priorName: "");
+
+ Debug.Assert(StringComparer.Ordinal.Compare(referenceName, record.LastRefName) < 0);
+
+ Position = record.BlockPosition;
+ var result = SearchBlock(header, referenceName);
+ if (result != null)
+ {
+ return result;
+ }
+ }
+
+ firstGreater--;
+
+ if (firstGreater == -1)
+ {
+ // reference is not found - it would be ordered before the first record
+ return null;
+ }
+
+ Position = blockStartPosition + restartOffsets[firstGreater];
+ var endPosition = blockStartPosition + restartOffsets[firstGreater + 1];
+
+ var priorName = "";
+ while (Position < endPosition)
+ {
+ record = ReadRefIndexRecord(priorName);
+
+ if (StringComparer.Ordinal.Compare(referenceName, record.LastRefName) <= 0)
+ {
+ // the last reference of the block is the one we are looking for:
+ Position = record.BlockPosition;
+ return SearchBlock(header, referenceName);
+ }
+
+ priorName = record.LastRefName;
+ }
+
+ return null;
+ }
+
+ public RefRecord? SearchBlock(Header header, string referenceName)
+ {
+ return ReadByte() switch
+ {
+ BlockTypeRef => SearchRefBlock(header, referenceName, out _),
+ BlockTypeIndex => SearchRefIndex(header, referenceName),
+ _ => throw new InvalidDataException(),
+ };
+ }
+
+ private RefRecord? SearchRefBlock(Header header, string referenceName, out long blockEndPosition)
+ {
+ // 'r' (already read)
+ // uint24(block_len)
+ // ref_record+
+ // uint24(restart_offset)+
+ // uint16(restart_count)
+ // padding?
+
+ var restartOffsets = ReadRestartOffsets(header, blockLengthLimited: true, out var blockStartPosition, out blockEndPosition);
+
+ var (record, firstGreater) = restartOffsets.BinarySearch(
+ index: 0,
+ length: restartOffsets.Length - 1,
+ selector: restartOffset =>
+ {
+ Position = blockStartPosition + restartOffset;
+ return ReadRefRecord(header, priorName: "");
+ },
+ compareItemToSearchValue: item => StringComparer.Ordinal.Compare(item.RefName, referenceName));
+
+ if (firstGreater < 0)
+ {
+ // The record at the index has the reference we are looking for.
+ return record;
+ }
+
+ // firstGreater points to the first record at a restart offset that has reference name greater than the searched value.
+ // Record runs are sorted by *first* reference name of the run (run starts at a restart offset and ends at the next restart offset).
+ // Hence, firstGreater - 1 points to the run that contains the reference we are looking for.
+ if (firstGreater == 0)
+ {
+ // reference is not found - it would be ordered before the first record
+ return null;
+ }
+
+ Position = blockStartPosition + restartOffsets[firstGreater - 1];
+ var endPosition = blockStartPosition + restartOffsets[firstGreater];
+
+ var priorName = "";
+ while (Position < endPosition)
+ {
+ record = ReadRefRecord(header, priorName);
+
+ if (record.RefName == referenceName)
+ {
+ return record;
+ }
+
+ priorName = record.RefName;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns offsets (relative to the start of the block).
+ /// Each offset points at a record with no prefix optionally followed by records that use prefix compression.
+ /// The last offset points at the end of records in the block.
+ /// Offset values are increasing.
+ ///
+ internal int[] ReadRestartOffsets(Header header, bool blockLengthLimited, out long blockStartPosition, out long blockEndPosition)
+ {
+ const int SizeOfBlockType = sizeof(byte);
+ const int SizeOfRestartCount = sizeof(ushort);
+ const int SizeOfRestartOffset = SizeOfUInt24;
+ const int SizeOfBlockLength = SizeOfUInt24;
+
+ // The first block includes the header.
+ // block_type is already read.
+ var isFirstBlock = Position == header.Size + SizeOfBlockType;
+
+ // Block starts with a block_type:
+ blockStartPosition = isFirstBlock ? 0 : Position - SizeOfBlockType;
+
+ // block_len of the first block includes the file header.
+ // Must be less than or equal to block size, unless the file is unaligned.
+ var blockLength = ReadUInt24BE();
+ if (blockLengthLimited && header.BlockSize > 0 && blockLength > header.BlockSize ||
+ blockLength < MinRefBlockSize)
+ {
+ throw new InvalidDataException();
+ }
+
+ // block_len excludes padding:
+ blockEndPosition = blockStartPosition + blockLength;
+
+ // uint16(restart_count)
+ Position = blockStartPosition + blockLength - SizeOfRestartCount;
+ var restartCount = ReadUInt16BE();
+ if (restartCount == 0)
+ {
+ throw new InvalidDataException();
+ }
+
+ // uint24(restart_offset)+
+ var endOffset = blockLength - SizeOfRestartCount - SizeOfRestartOffset * restartCount;
+ Position = blockStartPosition + endOffset;
+
+ int[] offsets;
+ try
+ {
+ offsets = new int[restartCount + 1];
+ }
+ catch (OutOfMemoryException)
+ {
+ throw new InvalidDataException();
+ }
+
+ for (var i = 0; i < restartCount; i++)
+ {
+ // Offset relative to the start of the block:
+ var offset = ReadUInt24BE();
+
+ // first offset points to the first record:
+ if (i == 0 && offset != (isFirstBlock ? header.Size : 0) + SizeOfBlockType + SizeOfBlockLength)
+ {
+ throw new InvalidDataException();
+ }
+
+ // offsets must be increasing:
+ if (i > 0 && offset <= offsets[i - 1])
+ {
+ throw new InvalidDataException();
+ }
+
+ if (offset >= endOffset)
+ {
+ throw new InvalidDataException();
+ }
+
+ offsets[i] = offset;
+ }
+
+ offsets[^1] = endOffset;
+
+ return offsets;
+ }
+
+ private (string name, byte valueType) ReadNameAndValueType(string priorName)
+ {
+ // varint(prefix_length)
+ var prefixLength = ReadVarInt();
+
+ // varint((suffix_length << 3) | value_type)
+ var suffixLengthAndValueType = ReadVarInt();
+ var suffixLength = suffixLengthAndValueType >> 3;
+ var valueType = (byte)(suffixLengthAndValueType & 0x07);
+
+ // suffix
+ var suffixBytes = ReadBytes(suffixLength);
+ try
+ {
+ var name = priorName[0..prefixLength] + s_utf8.GetString(suffixBytes);
+ return (name, valueType);
+ }
+ catch
+ {
+ throw new InvalidDataException();
+ }
+ }
+
+ internal RefIndexRecord ReadRefIndexRecord(string priorName)
+ {
+ var (name, valueType) = ReadNameAndValueType(priorName);
+ if (valueType != 0)
+ {
+ throw new InvalidDataException();
+ }
+
+ // position of RefBlock from the start of the file:
+ var blockPosition = ReadVarInt();
+
+ return new RefIndexRecord
+ {
+ LastRefName = name,
+ BlockPosition = blockPosition
+ };
+ }
+
+ internal RefRecord ReadRefRecord(Header header, string priorName)
+ {
+ var (name, valueType) = ReadNameAndValueType(priorName);
+
+ // varint(update_index_delta)
+ _ = ReadVarInt();
+
+ string? symbolicRef = null;
+ string? objectName = null;
+
+ var objectIdSize = header.ObjectNameFormat.HashSize;
+
+ switch (valueType)
+ {
+ case 0: // deletion, no value -- stops lookup from opening previous reftable file
+ break;
+
+ case 1: // object name
+ objectName = ReadObjectName(objectIdSize);
+ break;
+
+ case 2: // value, peeled target
+ objectName = ReadObjectName(objectIdSize);
+
+ // peeled target object name
+ _ = ReadObjectName(objectIdSize);
+ break;
+
+ case 3: // symbolic ref
+ symbolicRef = ReadSymbolicRef();
+ break;
+
+ default:
+ throw new InvalidDataException();
+ }
+
+ return new RefRecord
+ {
+ RefName = name,
+ ObjectName = objectName,
+ SymbolicRef = symbolicRef
+ };
+ }
+
+ private string ReadSymbolicRef()
+ {
+ var length = ReadVarInt();
+ var bytes = ReadBytes(length);
+
+ try
+ {
+ return s_utf8.GetString(bytes);
+ }
+ catch
+ {
+ throw new InvalidDataException();
+ }
+ }
+}
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitReferenceResolver.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitReferenceResolver.cs
index 4f3687e3..8f06c0f4 100644
--- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitReferenceResolver.cs
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitReferenceResolver.cs
@@ -12,30 +12,54 @@
namespace Microsoft.Build.Tasks.Git
{
- internal sealed class GitReferenceResolver
+ internal sealed class GitReferenceResolver : IDisposable
{
// See https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD
private const string PackedRefsFileName = "packed-refs";
+ private const string TablesListFileName = "tables.list";
private const string RefsPrefix = "refs/";
+ private const string RefTableDirectoryName = "reftable";
private readonly string _commonDirectory;
private readonly string _gitDirectory;
+ private readonly ReferenceStorageFormat _storageFormat;
+ private readonly ObjectNameFormat _objectNameFormat;
// maps refs/heads references to the correspondign object ids:
private readonly Lazy> _lazyPackedReferences;
+ private readonly Lazy> _lazyRefTableReferenceReaders;
- public GitReferenceResolver(string gitDirectory, string commonDirectory)
+ // lock on access:
+ private readonly List _openedRefTableReaders = [];
+
+ public GitReferenceResolver(string gitDirectory, string commonDirectory, ReferenceStorageFormat storageFormat, ObjectNameFormat objectNameFormat)
{
Debug.Assert(PathUtils.IsNormalized(gitDirectory));
Debug.Assert(PathUtils.IsNormalized(commonDirectory));
_gitDirectory = gitDirectory;
_commonDirectory = commonDirectory;
- _lazyPackedReferences = new Lazy>(() => ReadPackedReferences(_gitDirectory));
+ _storageFormat = storageFormat;
+ _objectNameFormat = objectNameFormat;
+ _lazyPackedReferences = new(() => ReadPackedReferences(_gitDirectory));
+ _lazyRefTableReferenceReaders = new(() => CreateRefTableReaders(_gitDirectory, _openedRefTableReaders));
}
- private static ImmutableDictionary ReadPackedReferences(string gitDirectory)
+ public void Dispose()
+ {
+ lock (_openedRefTableReaders)
+ {
+ foreach (var reader in _openedRefTableReaders)
+ {
+ reader.Dispose();
+ }
+
+ _openedRefTableReaders.Clear();
+ }
+ }
+
+ private ImmutableDictionary ReadPackedReferences(string gitDirectory)
{
// https://git-scm.com/docs/git-pack-refs
@@ -62,7 +86,7 @@ private static ImmutableDictionary ReadPackedReferences(string g
}
// internal for testing
- internal static ImmutableDictionary ReadPackedReferences(TextReader reader, string path)
+ internal ImmutableDictionary ReadPackedReferences(TextReader reader, string path)
{
var builder = ImmutableDictionary.CreateBuilder();
@@ -150,20 +174,67 @@ internal static ImmutableDictionary ReadPackedReferences(TextRea
///
///
- public string? ResolveHeadReference()
- => ResolveReference(ReadReferenceFromFile(Path.Combine(_gitDirectory, GitRepository.GitHeadFileName)));
+ private static IEnumerable CreateRefTableReaders(string gitDirectory, List openReaders)
+ {
+ var refTableDirectory = Path.Combine(gitDirectory, RefTableDirectoryName);
+ var tablesFilePath = Path.Combine(refTableDirectory, TablesListFileName);
+
+ // Create lazily-evaluated sequence of readers for each entry in the tables.list file (in reverse order).
+ // Only evaluate the first one that exists.
+ // Reference resolution will open the subsequent files as needed.
+
+ var readers = File.ReadAllLines(tablesFilePath)
+ .Where(fileName => fileName.EndsWith(".ref"))
+ .Reverse()
+ .Select(fileName =>
+ {
+ var path = Path.Combine(refTableDirectory, fileName);
+
+ Stream stream;
+ try
+ {
+ stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
+ }
+ catch (FileNotFoundException)
+ {
+ return null;
+ }
+
+ var reader = new GitRefTableReader(stream);
+ lock (openReaders)
+ {
+ openReaders.Add(reader);
+ }
+
+ return reader;
+ })
+ .Where(s => s != null);
+
+ if (!readers.Any())
+ {
+ throw new InvalidDataException();
+ }
+
+ return readers!;
+ }
public string? GetBranchForHead()
- {
- var reference = ReadReferenceFromFile(Path.Combine(_gitDirectory, GitRepository.GitHeadFileName));
+ => FindHeadReference().referenceName;
- return TryGetReferenceName(reference, out var name) ? name : null;
+ ///
+ ///
+ public string? ResolveHeadReference()
+ {
+ var (objectName, referenceName) = FindHeadReference();
+ return objectName ?? ResolveReferenceName(referenceName!);
}
+ ///
+ ///
public string? ResolveReference(string reference)
{
- HashSet? lazyVisitedReferences = null;
- return ResolveReference(reference, ref lazyVisitedReferences);
+ var (objectName, referenceName) = ParseObjectNameOrReference(reference);
+ return objectName ?? ResolveReferenceName(referenceName!);
}
private static bool TryGetReferenceName(string reference, [NotNullWhen(true)] out string? name)
@@ -179,24 +250,101 @@ private static bool TryGetReferenceName(string reference, [NotNullWhen(true)] ou
return false;
}
+ private (string? objectName, string? referenceName) ParseObjectNameOrReference(string value)
+ {
+ if (TryGetReferenceName(value, out var referenceName))
+ {
+ return (objectName: null, referenceName);
+ }
+
+ if (IsObjectId(value))
+ {
+ return (value, referenceName: null);
+ }
+
+ throw new InvalidDataException(string.Format(Resources.InvalidReference, value));
+ }
+
///
///
- private string? ResolveReference(string reference, ref HashSet? lazyVisitedReferences)
+ private string? ResolveReferenceName(string referenceName)
{
- // See https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD
+ HashSet? lazyVisitedReferences = null;
+ return Recurse(referenceName);
- if (TryGetReferenceName(reference, out var symRef))
+ string? Recurse(string currentReferenceName)
{
- if (lazyVisitedReferences != null && !lazyVisitedReferences.Add(symRef))
+ // See https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD
+
+ if (lazyVisitedReferences != null && !lazyVisitedReferences.Add(currentReferenceName))
{
// infinite recursion
- throw new InvalidDataException(string.Format(Resources.RecursionDetectedWhileResolvingReference, reference));
+ throw new InvalidDataException(string.Format(Resources.RecursionDetectedWhileResolvingReference, referenceName));
+ }
+
+ var (objectName, foundReferenceName) = FindReference(currentReferenceName);
+ if (objectName != null)
+ {
+ return objectName;
+ }
+
+ if (foundReferenceName == null)
+ {
+ return null;
+ }
+
+ lazyVisitedReferences ??= [];
+ return Recurse(foundReferenceName);
+ }
+ }
+
+ private (string? objectName, string? referenceName) FindHeadReference()
+ => _storageFormat switch
+ {
+ ReferenceStorageFormat.LooseFiles => ParseObjectNameOrReference(ReadReferenceFromFile(Path.Combine(_gitDirectory, GitRepository.GitHeadFileName))),
+ ReferenceStorageFormat.RefTable => FindReferenceInRefTable(GitRepository.GitHeadFileName),
+ _ => throw new InvalidOperationException(),
+ };
+
+ private (string? objectName, string? referenceName) FindReference(string referenceName)
+ => _storageFormat switch
+ {
+ ReferenceStorageFormat.LooseFiles => FindReferenceInLooseFile(referenceName),
+ ReferenceStorageFormat.RefTable => FindReferenceInRefTable(referenceName),
+ _ => throw new InvalidOperationException()
+ };
+
+ private (string? objectName, string? referenceName) FindReferenceInRefTable(string referenceName)
+ {
+ foreach (var reader in _lazyRefTableReferenceReaders.Value)
+ {
+ if (!reader.TryFindReference(referenceName, out var objectName, out var symbolicReference))
+ {
+ continue;
}
+ return (objectName, symbolicReference);
+ }
+
+ return default;
+ }
+
+ private (string? objectName, string? referenceName) FindReferenceInLooseFile(string referenceName)
+ {
+ var content = Find() ?? FindPackedReference(referenceName);
+ if (content == null)
+ {
+ return default;
+ }
+
+ return ParseObjectNameOrReference(content);
+
+ string? Find()
+ {
string path;
try
{
- path = Path.Combine(_commonDirectory, symRef);
+ path = Path.Combine(_commonDirectory, referenceName);
}
catch
{
@@ -205,36 +353,19 @@ private static bool TryGetReferenceName(string reference, [NotNullWhen(true)] ou
if (!File.Exists(path))
{
- return ResolvePackedReference(symRef);
+ return null;
}
- string content;
try
{
- content = ReadReferenceFromFile(path);
+ return ReadReferenceFromFile(path);
}
catch (Exception e) when (e is ArgumentException or FileNotFoundException or DirectoryNotFoundException)
{
// invalid path or file doesn't exist:
- return ResolvePackedReference(symRef);
- }
-
- if (IsObjectId(content))
- {
- return content;
+ return null;
}
-
- lazyVisitedReferences ??= new HashSet();
-
- return ResolveReference(content, ref lazyVisitedReferences);
}
-
- if (IsObjectId(reference))
- {
- return reference;
- }
-
- throw new InvalidDataException(string.Format(Resources.InvalidReference, reference));
}
///
@@ -251,10 +382,10 @@ internal static string ReadReferenceFromFile(string path)
}
}
- private string? ResolvePackedReference(string reference)
+ private string? FindPackedReference(string reference)
=> _lazyPackedReferences.Value.TryGetValue(reference, out var objectId) ? objectId : null;
- private static bool IsObjectId(string reference)
- => reference.Length == 40 && reference.All(CharUtils.IsHexadecimalDigit);
+ private bool IsObjectId(string reference)
+ => reference.Length == _objectNameFormat.HashSize * 2 && 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 a0979fc2..1308a16e 100644
--- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs
@@ -11,10 +11,8 @@
namespace Microsoft.Build.Tasks.Git
{
- internal sealed class GitRepository
+ internal sealed class GitRepository : IDisposable
{
- private const int SupportedGitRepoFormatVersion = 1;
-
private const string CommonDirFileName = "commondir";
private const string GitDirName = ".git";
private const string GitDirPrefix = "gitdir: ";
@@ -24,9 +22,6 @@ internal sealed class GitRepository
private const string GitModulesFileName = ".gitmodules";
- private static readonly ImmutableArray s_knownExtensions =
- ImmutableArray.Create("noop", "preciousObjects", "partialclone", "worktreeConfig");
-
public GitConfig Config { get; }
public GitIgnore Ignore => _lazyIgnore.Value;
@@ -56,8 +51,6 @@ internal sealed class GitRepository
internal GitRepository(GitEnvironment environment, GitConfig config, string gitDirectory, string commonDirectory, string? workingDirectory)
{
- NullableDebug.Assert(environment != null);
- NullableDebug.Assert(config != null);
NullableDebug.Assert(PathUtils.IsNormalized(gitDirectory));
NullableDebug.Assert(PathUtils.IsNormalized(commonDirectory));
NullableDebug.Assert(workingDirectory == null || PathUtils.IsNormalized(workingDirectory));
@@ -68,11 +61,11 @@ internal GitRepository(GitEnvironment environment, GitConfig config, string gitD
WorkingDirectory = workingDirectory;
Environment = environment;
- _referenceResolver = new GitReferenceResolver(gitDirectory, commonDirectory);
- _lazySubmodules = new Lazy<(ImmutableArray, ImmutableArray)>(ReadSubmodules);
- _lazyIgnore = new Lazy(LoadIgnore);
- _lazyHeadCommitSha = new Lazy(ReadHeadCommitSha);
- _lazyBranchName = new Lazy(ReadBranchName);
+ _referenceResolver = new GitReferenceResolver(gitDirectory, commonDirectory, config.ReferenceStorageFormat, config.ObjectNameFormat);
+ _lazySubmodules = new(ReadSubmodules);
+ _lazyIgnore = new(LoadIgnore);
+ _lazyHeadCommitSha = new(ReadHeadCommitSha);
+ _lazyBranchName = new(ReadBranchName);
}
// test only
@@ -89,11 +82,16 @@ internal GitRepository(
string? branchName)
: this(environment, config, gitDirectory, commonDirectory, workingDirectory)
{
- _lazySubmodules = new Lazy<(ImmutableArray, ImmutableArray)>(() => (submodules, submoduleDiagnostics));
- _lazyIgnore = new Lazy(() => ignore);
- _lazyHeadCommitSha = new Lazy(() => headCommitSha);
- _lazyBranchName = new Lazy(() => branchName);
- }
+ _lazySubmodules = new(() => (submodules, submoduleDiagnostics));
+ _lazyIgnore = new(() => ignore);
+ _lazyHeadCommitSha = new(() => headCommitSha);
+ _lazyBranchName = new(() => branchName);
+ }
+
+ public void Dispose()
+ {
+ _referenceResolver.Dispose();
+ }
///
/// Opens a repository at the specified location.
@@ -119,31 +117,9 @@ public static GitRepository OpenRepository(GitRepositoryLocation location, GitEn
// See https://git-scm.com/docs/gitrepository-layout
- var reader = new GitConfig.Reader(location.GitDirectory, location.CommonDirectory, environment);
- var config = reader.Load();
-
+ var config = GitConfig.ReadRepositoryConfig(location.GitDirectory, location.CommonDirectory, environment);
var workingDirectory = GetWorkingDirectory(config, location);
- // See https://github.com/git/git/blob/master/Documentation/technical/repository-version.txt
- var versionStr = config.GetVariableValue("core", "repositoryformatversion");
- if (GitConfig.TryParseInt64Value(versionStr, out var version) && version > SupportedGitRepoFormatVersion)
- {
- throw new NotSupportedException(string.Format(Resources.UnsupportedRepositoryVersion, versionStr, SupportedGitRepoFormatVersion));
- }
-
- if (version == 1)
- {
- // All variables defined under extensions section must be known, otherwise a git implementation is not allowed to proced.
- foreach (var variable in config.Variables)
- {
- if (variable.Key.SectionNameEquals("extensions") && !s_knownExtensions.Contains(variable.Key.VariableName, StringComparer.OrdinalIgnoreCase))
- {
- throw new NotSupportedException(string.Format(
- Resources.UnsupportedRepositoryExtension, variable.Key.VariableName, string.Join(", ", s_knownExtensions)));
- }
- }
- }
-
return new GitRepository(environment, config, location.GitDirectory, location.CommonDirectory, workingDirectory);
}
@@ -197,7 +173,7 @@ public static GitRepository OpenRepository(GitRepositoryLocation location, GitEn
///
///
/// Null if the submodule can't be located.
- public static GitReferenceResolver? GetSubmoduleReferenceResolver(string submoduleWorkingDirectoryFullPath)
+ public static GitReferenceResolver? GetSubmoduleReferenceResolver(string submoduleWorkingDirectoryFullPath, GitEnvironment environment)
{
// Submodules don't usually have their own .git directories but this is still legal.
// This can occur with older versions of Git or other tools, or when a user clones one
@@ -214,7 +190,8 @@ public static GitRepository OpenRepository(GitRepositoryLocation location, GitEn
return null;
}
- return new GitReferenceResolver(gitDirectory, commonDirectory);
+ var config = GitConfig.ReadRepositoryConfig(gitDirectory, commonDirectory, environment);
+ return new GitReferenceResolver(gitDirectory, commonDirectory, config.ReferenceStorageFormat, config.ObjectNameFormat);
}
private string GetWorkingDirectory()
@@ -275,7 +252,7 @@ void reportDiagnostic(string diagnostic)
string? headCommitSha;
try
{
- var resolver = GetSubmoduleReferenceResolver(fullPath);
+ using var resolver = GetSubmoduleReferenceResolver(fullPath, Environment);
if (resolver == null)
{
// If we can't locate the submodule directory then it won't have any source files
@@ -307,8 +284,7 @@ void reportDiagnostic(string diagnostic)
return null;
}
- var reader = new GitConfig.Reader(GitDirectory, CommonDirectory, Environment);
- return reader.LoadFrom(submodulesConfigFile);
+ return GitConfig.ReadSubmoduleConfig(GitDirectory, CommonDirectory, Environment, submodulesConfigFile);
}
// internal for testing
@@ -478,7 +454,7 @@ private static string ReadDotGitFile(string path)
}
}
- private static bool IsGitDirectory(string directory, [NotNullWhen(true)]out string? commonDirectory)
+ private static bool IsGitDirectory(string directory, [NotNullWhen(true)] out string? commonDirectory)
{
// HEAD file is required
if (!File.Exists(Path.Combine(directory, GitHeadFileName)))
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitVariableName.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitVariableName.cs
index ddddbc35..ca039083 100644
--- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitVariableName.cs
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitVariableName.cs
@@ -3,54 +3,43 @@
// See the License.txt file in the project root for more information.
using System;
-using System.Diagnostics;
-namespace Microsoft.Build.Tasks.Git
+namespace Microsoft.Build.Tasks.Git;
+
+internal readonly struct GitVariableName(string sectionName, string subsectionName, string variableName) : IEquatable
{
- internal readonly struct GitVariableName : IEquatable
- {
- public static readonly StringComparer SectionNameComparer = StringComparer.OrdinalIgnoreCase;
- public static readonly StringComparer SubsectionNameComparer = StringComparer.Ordinal;
- public static readonly StringComparer VariableNameComparer = StringComparer.OrdinalIgnoreCase;
-
- public readonly string SectionName;
- public readonly string SubsectionName;
- public readonly string VariableName;
-
- public GitVariableName(string sectionName, string subsectionName, string variableName)
- {
- NullableDebug.Assert(sectionName != null);
- NullableDebug.Assert(subsectionName != null);
- NullableDebug.Assert(variableName != null);
-
- SectionName = sectionName;
- SubsectionName = subsectionName;
- VariableName = variableName;
- }
-
- public bool SectionNameEquals(string name)
- => SectionNameComparer.Equals(SectionName, name);
-
- public bool SubsectionNameEquals(string name)
- => SubsectionNameComparer.Equals(SubsectionName, name);
-
- public bool VariableNameEquals(string name)
- => VariableNameComparer.Equals(VariableName, name);
-
- public bool Equals(GitVariableName other)
- => SectionNameEquals(other.SectionName) &&
- SubsectionNameEquals(other.SubsectionName) &&
- VariableNameEquals(other.VariableName);
-
- public override bool Equals(object? obj)
- => obj is GitVariableName other && Equals(other);
-
- public override int GetHashCode()
- => SectionName.GetHashCode() ^ SubsectionName.GetHashCode() ^ VariableName.GetHashCode();
-
- public override string ToString()
- => (SubsectionName.Length == 0) ?
- SectionName + "." + VariableName :
- SectionName + "." + SubsectionName + "." + VariableName;
- }
+ public static readonly StringComparer SectionNameComparer = StringComparer.OrdinalIgnoreCase;
+ public static readonly StringComparer SubsectionNameComparer = StringComparer.Ordinal;
+ public static readonly StringComparer VariableNameComparer = StringComparer.OrdinalIgnoreCase;
+
+ public readonly string SectionName = sectionName;
+ public readonly string SubsectionName = subsectionName;
+ public readonly string VariableName = variableName;
+
+ public bool SectionNameEquals(string name)
+ => SectionNameComparer.Equals(SectionName, name);
+
+ public bool SubsectionNameEquals(string name)
+ => SubsectionNameComparer.Equals(SubsectionName, name);
+
+ public bool VariableNameEquals(string name)
+ => VariableNameComparer.Equals(VariableName, name);
+
+ public bool Equals(GitVariableName other)
+ => SectionNameEquals(other.SectionName) &&
+ SubsectionNameEquals(other.SubsectionName) &&
+ VariableNameEquals(other.VariableName);
+
+ public override bool Equals(object? obj)
+ => obj is GitVariableName other && Equals(other);
+
+ public override int GetHashCode()
+ => SectionNameComparer.GetHashCode(SectionName) ^
+ SubsectionNameComparer.GetHashCode(SubsectionName) ^
+ VariableNameComparer.GetHashCode(VariableName);
+
+ public override string ToString()
+ => (SubsectionName.Length == 0) ?
+ SectionName + "." + VariableName :
+ SectionName + "." + SubsectionName + "." + VariableName;
}
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/ObjectNameFormat.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/ObjectNameFormat.cs
new file mode 100644
index 00000000..2e338216
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/ObjectNameFormat.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Build.Tasks.Git;
+
+///
+/// Format of object names.
+/// https://git-scm.com/docs/hash-function-transition.html#_object_format
+///
+internal enum ObjectNameFormat
+{
+ Sha1,
+ Sha256
+}
+
+internal static class ObjectNameFormatExtensions
+{
+ extension(ObjectNameFormat format)
+ {
+ public int HashSize
+ => format switch
+ {
+ ObjectNameFormat.Sha1 => 20,
+ ObjectNameFormat.Sha256 => 32,
+ _ => throw new InvalidOperationException()
+ };
+ }
+}
+
+
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/ReferenceStorageFormat.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/ReferenceStorageFormat.cs
new file mode 100644
index 00000000..74ab09be
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/ReferenceStorageFormat.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Build.Tasks.Git;
+
+///
+/// The format of storage for references.
+///
+internal enum ReferenceStorageFormat
+{
+ ///
+ /// References stored as files in refs directory or packed-refs.
+ ///
+ LooseFiles = 0,
+
+ ///
+ /// References stored in RefTable ().
+ ///
+ RefTable = 1,
+}
diff --git a/src/Microsoft.Build.Tasks.Git/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs
index 78fcb2e6..fc411c6b 100644
--- a/src/Microsoft.Build.Tasks.Git/GitOperations.cs
+++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs
@@ -339,8 +339,9 @@ public static ITaskItem[] GetUntrackedFiles(GitRepository repository, ITaskItem[
=> GetUntrackedFiles(repository, files, projectDirectory, CreateSubmoduleRepository);
private static GitRepository? CreateSubmoduleRepository(GitEnvironment environment, string directoryFullPath)
- => GitRepository.TryGetRepositoryLocation(directoryFullPath, out var location) ?
- GitRepository.OpenRepository(location, environment) : null;
+ => GitRepository.TryGetRepositoryLocation(directoryFullPath, out var location)
+ ? GitRepository.OpenRepository(location, environment)
+ : null;
// internal for testing
internal static ITaskItem[] GetUntrackedFiles(GitRepository repository, ITaskItem[] files, string projectDirectory, Func repositoryFactory)
@@ -388,8 +389,14 @@ internal static DirectoryNode BuildDirectoryTree(GitRepository repository, Func<
{
var submoduleWorkingDirectory = submodule.WorkingDirectoryFullPath;
- AddTreeNode(treeRoot, submoduleWorkingDirectory,
- new Lazy(() => repositoryFactory(repository.Environment, submoduleWorkingDirectory)?.Ignore.CreateMatcher()));
+ AddTreeNode(
+ treeRoot,
+ submoduleWorkingDirectory,
+ matcher: new Lazy(() =>
+ {
+ using var submoduleRepository = repositoryFactory(repository.Environment, submoduleWorkingDirectory);
+ return submoduleRepository?.Ignore.CreateMatcher();
+ }));
}
return treeRoot;
diff --git a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj
index c6176c5f..3ea8e7ee 100644
--- a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj
+++ b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs b/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs
index eb19f467..efb060c5 100644
--- a/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs
+++ b/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs
@@ -5,7 +5,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Runtime.CompilerServices;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
@@ -13,6 +12,15 @@ namespace Microsoft.Build.Tasks.Git
{
public abstract class RepositoryTask : Task
{
+ private sealed class RepositoryContainer(GitRepository? repository) : IDisposable
+ {
+ public GitRepository? Repository
+ => repository;
+
+ public void Dispose()
+ => repository?.Dispose();
+ }
+
///
/// Sets the scope of git repository configuration. By default (no scope specified) configuration is read from environment variables
/// and system and global user git/ssh configuration files.
@@ -144,11 +152,11 @@ private Tuple GetCacheKey(string repositoryId)
private bool TryGetCachedRepositoryInstance(Tuple cacheKey, bool requireCached, [NotNullWhen(true)]out GitRepository? repository)
{
- var entry = (StrongBox?)BuildEngine4.GetRegisteredTaskObject(cacheKey, RegisteredTaskObjectLifetime.Build);
+ var entry = (RepositoryContainer?)BuildEngine4.GetRegisteredTaskObject(cacheKey, RegisteredTaskObjectLifetime.Build);
if (entry != null)
{
Log.LogMessage(MessageImportance.Low, $"SourceLink: Reusing cached git repository information.");
- repository = entry.Value;
+ repository = entry.Repository;
return repository != null;
}
@@ -170,7 +178,7 @@ private void CacheRepositoryInstance(Tuple cacheKey, GitRepository
{
BuildEngine4.RegisterTaskObject(
cacheKey,
- new StrongBox(repository),
+ new RepositoryContainer(repository),
RegisteredTaskObjectLifetime.Build,
allowEarlyCollection: true);
}
diff --git a/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs b/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs
index 56cac303..2fa94ba4 100644
--- a/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs
+++ b/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs
@@ -255,7 +255,7 @@ public void CustomTranslation()
",
customTargets: @"
-
@@ -275,7 +275,7 @@ public void CustomTranslation()
-
+
<_SourceLinkFileWrites Include=""@(FileWrites)"" Condition=""$([MSBuild]::ValueOrDefault('%(Identity)', '').EndsWith('sourcelink.json'))""/>
diff --git a/src/SourceLink.Git.IntegrationTests/TargetTests.cs b/src/SourceLink.Git.IntegrationTests/TargetTests.cs
index 4264ee3b..59280ed2 100644
--- a/src/SourceLink.Git.IntegrationTests/TargetTests.cs
+++ b/src/SourceLink.Git.IntegrationTests/TargetTests.cs
@@ -35,7 +35,7 @@ public void GenerateSourceLinkFileTarget_EnableSourceLinkCondition()
",
customTargets: @"
-
+
false