Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/Microsoft.Build.Tasks.Git.UnitTests/GitReferenceResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,136 @@ public void ReadPackedReferences_Errors(string content)
{
Assert.Throws<InvalidDataException>(() => GitReferenceResolver.ReadPackedReferences(new StringReader(content), "<path>"));
}

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

var gitDir = temp.CreateDirectory();
var reftableDir = gitDir.CreateDirectory("reftable");

// Create a minimal reftable file with a reference
// This is a simplified test - in reality, we'd need to create a proper binary reftable file
// For now, we'll test that the resolver falls back correctly when reftable is empty

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

refsHeadsDir.CreateFile("master").WriteAllText("1111111111111111111111111111111111111111");

var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path);

// Should still resolve refs from files even when reftable directory exists but is empty
Assert.Equal("1111111111111111111111111111111111111111", resolver.ResolveReference("ref: refs/heads/master"));
}

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

var gitDir = temp.CreateDirectory();
var reftableDir = gitDir.CreateDirectory("reftable");

// Create a minimal valid reftable file
var reftableFile = reftableDir.CreateFile("test.ref");
using (var stream = new FileStream(reftableFile.Path, FileMode.Create))
using (var writer = new BinaryWriter(stream))
{
// Write reftable header (24 bytes)
WriteUInt32BE(writer, 0x52454654); // Magic: 'REFT'
WriteUInt32BE(writer, 1); // Version: 1
WriteUInt64BE(writer, 1); // Min update index
WriteUInt64BE(writer, 1); // Max update index

// Write a ref block
long blockStart = stream.Position;
writer.Write((byte)0x72); // Block type: 'r' (ref)

// Placeholder for block size (will be updated later)
long sizePos = stream.Position;
writer.Write((byte)0);
writer.Write((byte)0);
writer.Write((byte)0);

// Write a ref record for refs/heads/test
byte[] refName = System.Text.Encoding.UTF8.GetBytes("refs/heads/test");
WriteVarint(writer, 0); // Prefix length
WriteVarint(writer, refName.Length); // Suffix length
writer.Write(refName);

// Value type: 0x1 (has object ID)
writer.Write((byte)0x01);

// Object ID (20 bytes SHA-1) - all 0x22 for testing
writer.Write(new byte[] { 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22 });

// Restart points (2 bytes)
writer.Write((byte)0);
writer.Write((byte)0);

// Calculate and write block size
long blockEnd = stream.Position;
int blockSize = (int)(blockEnd - blockStart);

// Pad to 256 bytes
int padding = 256 - (blockSize % 256);
if (padding < 256)
{
writer.Write(new byte[padding]);
blockSize += padding;
}

// Update block size in header
long currentPos = stream.Position;
stream.Seek(sizePos, SeekOrigin.Begin);
writer.Write((byte)((blockSize >> 16) & 0xFF));
writer.Write((byte)((blockSize >> 8) & 0xFF));
writer.Write((byte)(blockSize & 0xFF));
stream.Seek(currentPos, SeekOrigin.Begin);

// Write footer (68 bytes of zeros for simplicity)
writer.Write(new byte[68]);
}

var commonDir = temp.CreateDirectory();
var resolver = new GitReferenceResolver(gitDir.Path, commonDir.Path);

// Should be able to resolve the ref from reftable
var result = resolver.ResolveReference("ref: refs/heads/test");
Assert.Equal("2222222222222222222222222222222222222222", result);
}

private static void WriteUInt32BE(BinaryWriter writer, uint value)
{
writer.Write((byte)((value >> 24) & 0xFF));
writer.Write((byte)((value >> 16) & 0xFF));
writer.Write((byte)((value >> 8) & 0xFF));
writer.Write((byte)(value & 0xFF));
}

private static void WriteUInt64BE(BinaryWriter writer, ulong value)
{
writer.Write((byte)((value >> 56) & 0xFF));
writer.Write((byte)((value >> 48) & 0xFF));
writer.Write((byte)((value >> 40) & 0xFF));
writer.Write((byte)((value >> 32) & 0xFF));
writer.Write((byte)((value >> 24) & 0xFF));
writer.Write((byte)((value >> 16) & 0xFF));
writer.Write((byte)((value >> 8) & 0xFF));
writer.Write((byte)(value & 0xFF));
}

private static void WriteVarint(BinaryWriter writer, int value)
{
while (value > 0x7F)
{
writer.Write((byte)(0x80 | (value & 0x7F)));
value >>= 7;
}
writer.Write((byte)(value & 0x7F));
}
}
}
33 changes: 33 additions & 0 deletions src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,39 @@ public void OpenRepository_Version1_Extensions()
Assert.Null(repository.WorkingDirectory);
}

[Fact]
public void OpenRepository_Version1_ReftableExtension()
{
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/main");
gitDir.CreateDirectory("reftable");
gitDir.CreateDirectory("objects");

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

Assert.True(GitRepository.TryFindRepository(gitDir.Path, out var location));
Assert.Equal(gitDir.Path, location.CommonDirectory);
Assert.Equal(gitDir.Path, location.GitDirectory);
Assert.Null(location.WorkingDirectory);

// Should not throw - refstorage is a known extension
var repository = GitRepository.OpenRepository(location, GitEnvironment.Empty);
Assert.Equal(gitDir.Path, repository.CommonDirectory);
Assert.Equal(gitDir.Path, repository.GitDirectory);
Assert.Null(repository.WorkingDirectory);
}

[Fact]
public void OpenRepository_Version1_UnknownExtension()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal sealed class GitReferenceResolver
// See https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD

private const string PackedRefsFileName = "packed-refs";
private const string ReftableDirectoryName = "reftable";
private const string RefsPrefix = "refs/";

private readonly string _commonDirectory;
Expand All @@ -39,6 +40,25 @@ private static ImmutableDictionary<string, string> ReadPackedReferences(string g
{
// https://git-scm.com/docs/git-pack-refs

// First try to read from reftable
var reftableDirectory = Path.Combine(gitDirectory, ReftableDirectoryName);
if (Directory.Exists(reftableDirectory))
{
try
{
var reftableRefs = GitReftableReader.ReadReftableReferences(reftableDirectory);
if (reftableRefs.Count > 0)
{
return reftableRefs;
}
}
catch
{
// If reftable reading fails, fall through to try packed-refs
}
}

// Fall back to packed-refs
var packedRefsPath = Path.Combine(gitDirectory, PackedRefsFileName);
if (!File.Exists(packedRefsPath))
{
Expand Down
Loading