Skip to content

Commit 7685c32

Browse files
committed
GitRepository: Implement Lookup() for partial Git object IDs
1 parent 38d5df3 commit 7685c32

File tree

6 files changed

+142
-12
lines changed

6 files changed

+142
-12
lines changed

src/NerdBank.GitVersioning.Tests/GitContextTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public void SelectCommitByPartialId(bool fromPack)
7575
{
7676
TestUtilities.DeleteDirectory(obDirectory);
7777
}
78+
79+
// The managed git context always assumes read-only access. It won't detect a new Git pack file being
80+
// created on the fly, so we have to re-initialize.
81+
this.Context = this.CreateGitContext(this.RepoPath, null);
7882
}
7983

8084
Assert.True(this.Context.TrySelectCommit(this.Context.GitCommitId.Substring(0, 12)));

src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackIndexMappedReaderTests.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class GitPackIndexMappedReaderTests
1010
[Fact]
1111
public void ConstructorNullTest()
1212
{
13-
Assert.Throws<ArgumentNullException>(() => new GitPackIndexStreamReader(null));
13+
Assert.Throws<ArgumentNullException>(() => new GitPackIndexMappedReader(null));
1414
}
1515

1616
[Fact]
@@ -45,5 +45,45 @@ public void GetOffsetTest()
4545
}
4646

4747
}
48+
49+
[Fact]
50+
public void GetOffsetFromPartialTest()
51+
{
52+
var indexFile = Path.GetTempFileName();
53+
54+
using (Stream resourceStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx"))
55+
using (FileStream stream = File.Open(indexFile, FileMode.Open))
56+
{
57+
resourceStream.CopyTo(stream);
58+
}
59+
60+
using (FileStream stream = File.OpenRead(indexFile))
61+
using (var reader = new GitPackIndexMappedReader(stream))
62+
{
63+
// Offset of an object which is present
64+
(var offset, var objectId) = reader.GetOffset(new byte[] { 0xf5, 0xb4, 0x01, 0xf4 });
65+
Assert.Equal(12, offset);
66+
Assert.Equal(GitObjectId.Parse("f5b401f40ad83f13030e946c9ea22cb54cb853cd"), objectId);
67+
68+
(offset, objectId) = reader.GetOffset(new byte[] { 0xd6, 0x78, 0x15, 0x52 });
69+
Assert.Equal(317, offset);
70+
Assert.Equal(GitObjectId.Parse("d6781552a0a94adbf73ed77696712084754dc274"), objectId);
71+
72+
// null for an object which is not present
73+
(offset, objectId) = reader.GetOffset(new byte[] { 0x00, 0x00, 0x00, 0x00 });
74+
Assert.Null(offset);
75+
Assert.Null(objectId);
76+
}
77+
78+
try
79+
{
80+
File.Delete(indexFile);
81+
}
82+
catch (UnauthorizedAccessException)
83+
{
84+
// TBD: Figure out what's keeping a lock on the file. Seems to be unique to Windows.
85+
}
86+
87+
}
4888
}
4989
}

src/NerdBank.GitVersioning/ManagedGit/GitPack.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ public GitPack(GetObjectFromRepositoryDelegate getObjectFromRepositoryDelegate,
124124
/// </summary>
125125
public GetObjectFromRepositoryDelegate GetObjectFromRepository { get; private set; }
126126

127+
/// <summary>
128+
/// Finds a git object using a partial object ID.
129+
/// </summary>
130+
/// <param name="objectId">
131+
/// A partial object ID.
132+
/// </param>
133+
/// <returns>
134+
/// If found, a full object ID which matches the partial object ID.
135+
/// Otherwise, <see langword="false"/>.
136+
/// </returns>
137+
public GitObjectId? Lookup(Span<byte> objectId)
138+
{
139+
(var _, var actualObjectId) = this.indexReader.Value.GetOffset(objectId);
140+
return actualObjectId;
141+
}
142+
127143
/// <summary>
128144
/// Attempts to retrieve a Git object from this Git pack.
129145
/// </summary>

src/NerdBank.GitVersioning/ManagedGit/GitPackIndexMappedReader.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,11 @@ private Span<byte> Value
5151
}
5252

5353
/// <inheritdoc/>
54-
public override long? GetOffset(GitObjectId objectId)
54+
public override (long?, GitObjectId?) GetOffset(Span<byte> objectName)
5555
{
5656
this.Initialize();
5757

5858
Span<byte> buffer = stackalloc byte[4];
59-
Span<byte> objectName = stackalloc byte[20];
60-
objectId.CopyTo(objectName);
6159

6260
var packStart = this.fanoutTable[objectName[0]];
6361
var packEnd = this.fanoutTable[objectName[0] + 1];
@@ -84,7 +82,7 @@ private Span<byte> Value
8482
{
8583
i = (packStart + packEnd) / 2;
8684

87-
order = table.Slice(20 * i, 20).SequenceCompareTo(objectName);
85+
order = table.Slice(20 * i, objectName.Length).SequenceCompareTo(objectName);
8886

8987
if (order < 0)
9088
{
@@ -102,7 +100,7 @@ private Span<byte> Value
102100

103101
if (order != 0)
104102
{
105-
return null;
103+
return (null, null);
106104
}
107105

108106
// Get the offset value. It's located at:
@@ -113,7 +111,7 @@ private Span<byte> Value
113111

114112
if (offsetBuffer[0] < 128)
115113
{
116-
return offset;
114+
return (offset, GitObjectId.Parse(table.Slice(20 * i, 20)));
117115
}
118116
else
119117
{
@@ -123,7 +121,7 @@ private Span<byte> Value
123121

124122
offsetBuffer = this.Value.Slice(offsetTableStart + 4 * objectCount + 8 * (int)offset, 8);
125123
var offset64 = BinaryPrimitives.ReadInt64BigEndian(offsetBuffer);
126-
return offset64;
124+
return (offset64, GitObjectId.Parse(table.Slice(20 * i, 20)));
127125
}
128126
}
129127

src/NerdBank.GitVersioning/ManagedGit/GitPackIndexReader.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
42

53
namespace Nerdbank.GitVersioning.ManagedGit
64
{
@@ -25,7 +23,25 @@ public abstract class GitPackIndexReader : IDisposable
2523
/// If found, the offset of the Git object in the index file; otherwise,
2624
/// <see langword="null"/>.
2725
/// </returns>
28-
public abstract long? GetOffset(GitObjectId objectId);
26+
public long? GetOffset(GitObjectId objectId)
27+
{
28+
Span<byte> name = stackalloc byte[20];
29+
objectId.CopyTo(name);
30+
(var offset, var _) = this.GetOffset(name);
31+
return offset;
32+
}
33+
34+
/// <summary>
35+
/// Gets the offset of a Git object in the index file.
36+
/// </summary>
37+
/// <param name="objectId">
38+
/// A partial or full Git object id, in its binary representation.
39+
/// </param>
40+
/// <returns>
41+
/// If found, the offset of the Git object in the index file; otherwise,
42+
/// <see langword="null"/>.
43+
/// </returns>
44+
public abstract (long?, GitObjectId?) GetOffset(Span<byte> objectId);
2945

3046
/// <inheritdoc/>
3147
public abstract void Dispose();

src/NerdBank.GitVersioning/ManagedGit/GitRepository.cs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#nullable enable
22

33
using System;
4-
using System.Buffers;
54
using System.Collections.Generic;
65
using System.Diagnostics.CodeAnalysis;
6+
using System.Globalization;
77
using System.IO;
88
using System.Linq;
99
using System.Text;
@@ -374,6 +374,44 @@ public GitCommit GetCommit(GitObjectId sha, bool readAuthor = false)
374374
return GitObjectId.Parse(objectish);
375375
}
376376

377+
var possibleObjectIds = new List<GitObjectId>();
378+
if (objectish.Length > 2 && objectish.Length < 40)
379+
{
380+
// Search for _any_ object whose id starts with objectish in the object database
381+
var directory = Path.Combine(this.ObjectDirectory, objectish.Substring(0, 2));
382+
383+
if (Directory.Exists(directory))
384+
{
385+
var files = Directory.GetFiles(directory, $"{objectish.Substring(2)}*");
386+
387+
foreach (var file in files)
388+
{
389+
var objectId = $"{objectish.Substring(0, 2)}{Path.GetFileName(file)}";
390+
possibleObjectIds.Add(GitObjectId.Parse(objectId));
391+
}
392+
}
393+
394+
// Search for _any_ object whose id starts with objectish in the packfile
395+
var hex = ConvertHexStringToByteArray(objectish);
396+
397+
foreach (var pack in this.packs.Value)
398+
{
399+
var objectId = pack.Lookup(hex);
400+
401+
// It's possible for the same object to be present in both the object database and the pack files,
402+
// or in multiple pack files.
403+
if (objectId != null && !possibleObjectIds.Contains(objectId.Value))
404+
{
405+
possibleObjectIds.Add(objectId.Value);
406+
}
407+
}
408+
}
409+
410+
if (possibleObjectIds.Count == 1)
411+
{
412+
return possibleObjectIds[0];
413+
}
414+
377415
return null;
378416
}
379417

@@ -652,6 +690,24 @@ private static string TrimEndingDirectorySeparator(string path)
652690
#endif
653691
}
654692

693+
private static byte[] ConvertHexStringToByteArray(string hexString)
694+
{
695+
// https://stackoverflow.com/questions/321370/how-can-i-convert-a-hex-string-to-a-byte-array
696+
if (hexString.Length % 2 != 0)
697+
{
698+
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "The binary key cannot have an odd number of digits: {0}", hexString));
699+
}
700+
701+
byte[] data = new byte[hexString.Length / 2];
702+
for (int index = 0; index < data.Length; index++)
703+
{
704+
string byteValue = hexString.Substring(index * 2, 2);
705+
data[index] = byte.Parse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
706+
}
707+
708+
return data;
709+
}
710+
655711
/// <summary>
656712
/// Decodes a sequence of bytes from the specified byte array into a <see cref="string"/>.
657713
/// </summary>

0 commit comments

Comments
 (0)