diff --git a/LibGit2Sharp.Tests/ObjectDatabaseFixture.cs b/LibGit2Sharp.Tests/ObjectDatabaseFixture.cs index 649cb26ec..091876d8a 100644 --- a/LibGit2Sharp.Tests/ObjectDatabaseFixture.cs +++ b/LibGit2Sharp.Tests/ObjectDatabaseFixture.cs @@ -556,5 +556,47 @@ public void CalculatingHistoryDivergenceWithBadParamsThrows() () => repo.ObjectDatabase.CalculateHistoryDivergence(null, repo.Head.Tip)); } } + + [Fact] + public void CanShortenObjectIdentifier() + { + /* + * $ echo "aabqhq" | git hash-object -t blob --stdin + * dea509d0b3cb8ee0650f6ca210bc83f4678851ba + * + * $ echo "aaazvc" | git hash-object -t blob --stdin + * dea509d097ce692e167dfc6a48a7a280cc5e877e + */ + + string path = CloneBareTestRepo(); + using (var repo = new Repository(path)) + { + repo.Config.Set("core.abbrev", 4); + + Blob blob1 = CreateBlob(repo, "aabqhq\n"); + Assert.Equal("dea509d0b3cb8ee0650f6ca210bc83f4678851ba", blob1.Sha); + + Assert.Equal("dea5", repo.ObjectDatabase.ShortenObjectId(blob1)); + Assert.Equal("dea509d0b3cb", repo.ObjectDatabase.ShortenObjectId(blob1, 12)); + Assert.Equal("dea509d0b3cb8ee0650f6ca210bc83f4678851b", repo.ObjectDatabase.ShortenObjectId(blob1, 39)); + + Blob blob2 = CreateBlob(repo, "aaazvc\n"); + Assert.Equal("dea509d09", repo.ObjectDatabase.ShortenObjectId(blob2)); + Assert.Equal("dea509d09", repo.ObjectDatabase.ShortenObjectId(blob2, 4)); + Assert.Equal("dea509d0b", repo.ObjectDatabase.ShortenObjectId(blob1)); + Assert.Equal("dea509d0b", repo.ObjectDatabase.ShortenObjectId(blob1, 7)); + + Assert.Equal("dea509d0b3cb", repo.ObjectDatabase.ShortenObjectId(blob1, 12)); + Assert.Equal("dea509d097ce", repo.ObjectDatabase.ShortenObjectId(blob2, 12)); + } + } + + private static Blob CreateBlob(Repository repo, string content) + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(content))) + { + return repo.ObjectDatabase.CreateBlob(stream); + } + } } } diff --git a/LibGit2Sharp.Tests/OdbBackendFixture.cs b/LibGit2Sharp.Tests/OdbBackendFixture.cs index 1304d1097..cd8429ffc 100644 --- a/LibGit2Sharp.Tests/OdbBackendFixture.cs +++ b/LibGit2Sharp.Tests/OdbBackendFixture.cs @@ -177,6 +177,50 @@ public void CanPushWithACustomBackend() } } + [Fact] + public void CanShortenObjectIdentifier() + { + /* + * $ echo "aabqhq" | git hash-object -t blob --stdin + * dea509d0b3cb8ee0650f6ca210bc83f4678851ba + * + * $ echo "aaazvc" | git hash-object -t blob --stdin + * dea509d097ce692e167dfc6a48a7a280cc5e877e + */ + + string path = CloneBareTestRepo(); + using (var repo = new Repository(path)) + { + repo.ObjectDatabase.AddBackend(new MockOdbBackend(), 5); + + repo.Config.Set("core.abbrev", 4); + + Blob blob1 = CreateBlob(repo, "aabqhq\n"); + Assert.Equal("dea509d0b3cb8ee0650f6ca210bc83f4678851ba", blob1.Sha); + + Assert.Equal("dea5", repo.ObjectDatabase.ShortenObjectId(blob1)); + Assert.Equal("dea509d0b3cb", repo.ObjectDatabase.ShortenObjectId(blob1, 12)); + Assert.Equal("dea509d0b3cb8ee0650f6ca210bc83f4678851b", repo.ObjectDatabase.ShortenObjectId(blob1, 39)); + + Blob blob2 = CreateBlob(repo, "aaazvc\n"); + Assert.Equal("dea509d09", repo.ObjectDatabase.ShortenObjectId(blob2)); + Assert.Equal("dea509d09", repo.ObjectDatabase.ShortenObjectId(blob2, 4)); + Assert.Equal("dea509d0b", repo.ObjectDatabase.ShortenObjectId(blob1)); + Assert.Equal("dea509d0b", repo.ObjectDatabase.ShortenObjectId(blob1, 7)); + + Assert.Equal("dea509d0b3cb", repo.ObjectDatabase.ShortenObjectId(blob1, 12)); + Assert.Equal("dea509d097ce", repo.ObjectDatabase.ShortenObjectId(blob2, 12)); + } + } + + private static Blob CreateBlob(Repository repo, string content) + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(content))) + { + return repo.ObjectDatabase.CreateBlob(stream); + } + } + #region MockOdbBackend private class MockOdbBackend : OdbBackend @@ -190,6 +234,7 @@ protected override OdbBackendOperations SupportedOperations OdbBackendOperations.Write | OdbBackendOperations.WriteStream | OdbBackendOperations.Exists | + OdbBackendOperations.ExistsPrefix | OdbBackendOperations.ForEach | OdbBackendOperations.ReadHeader; } @@ -301,6 +346,37 @@ public override bool Exists(ObjectId oid) return m_objectIdToContent.ContainsKey(oid); } + public override int ExistsPrefix(string shortSha, out ObjectId found) + { + found = null; + int numFound = 0; + + foreach (ObjectId id in m_objectIdToContent.Keys) + { + if (!id.Sha.StartsWith(shortSha)) + { + continue; + } + + found = id; + numFound++; + + if (numFound > 1) + { + found = null; + return (int) ReturnCode.GIT_EAMBIGUOUS; + } + } + + if (numFound == 0) + { + found = null; + return (int)ReturnCode.GIT_ENOTFOUND; + } + + return (int)ReturnCode.GIT_OK; + } + public override int ReadHeader(ObjectId oid, out int length, out ObjectType objectType) { objectType = default(ObjectType); diff --git a/LibGit2Sharp/Core/GitOdbBackend.cs b/LibGit2Sharp/Core/GitOdbBackend.cs index 4f69ac70f..a7b0acf41 100644 --- a/LibGit2Sharp/Core/GitOdbBackend.cs +++ b/LibGit2Sharp/Core/GitOdbBackend.cs @@ -30,7 +30,7 @@ static GitOdbBackend() public writestream_callback WriteStream; public readstream_callback ReadStream; public exists_callback Exists; - public IntPtr ExistsPrefix; + public exists_prefix_callback ExistsPrefix; public IntPtr Refresh; public foreach_callback Foreach; public IntPtr Writepack; @@ -157,6 +157,23 @@ public delegate bool exists_callback( IntPtr backend, ref GitOid oid); + /// + /// The backend is passed a short OID and the number of characters in that short OID. + /// The backend is asked to return a value that indicates whether or not + /// the object exists in the backing store. The short OID might not be long enough to resolve + /// to just one object. In that case the backend should return GIT_EAMBIGUOUS. + /// + /// [out] If the call is successful, the backend will write the full OID if the object here. + /// [in] A pointer to the backend which is being asked to perform the task. + /// [in] The short-form OID which the backend is being asked to look up. + /// [in] The length of the short-form OID (short_oid). + /// 1 if the object exists, 0 if the object doesn't; an error code otherwise. + public delegate int exists_prefix_callback( + ref GitOid found_oid, + IntPtr backend, + ref GitOid short_oid, + UIntPtr len); + /// /// The backend is passed a callback function and a void* to pass through to the callback. The backend is /// asked to iterate through all objects in the backing store, invoking the callback for each item. diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index ea5bff09b..e25a9a353 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -753,6 +753,11 @@ internal static extern int git_object_peel( GitObjectSafeHandle obj, GitObjectType type); + [DllImport(libgit2)] + internal static extern int git_object_short_id( + GitBuf buf, + GitObjectSafeHandle obj); + [DllImport(libgit2)] internal static extern GitObjectType git_object_type(GitObjectSafeHandle obj); diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index 00d24d6c9..1d05208ac 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -1251,6 +1251,19 @@ public static GitObjectSafeHandle git_object_peel(RepositorySafeHandle repo, Obj } } + public static string git_object_short_id(RepositorySafeHandle repo, ObjectId id) + { + using (ThreadAffinity()) + using (var obj = new ObjectSafeWrapper(id, repo)) + using (var buf = new GitBuf()) + { + int res = NativeMethods.git_object_short_id(buf, obj.ObjectPtr); + Ensure.Int32Result(res); + + return LaxUtf8Marshaler.FromNative(buf.ptr); + } + } + public static GitObjectType git_object_type(GitObjectSafeHandle obj) { return NativeMethods.git_object_type(obj); diff --git a/LibGit2Sharp/ObjectDatabase.cs b/LibGit2Sharp/ObjectDatabase.cs index 3d36b9c26..2a51fad5c 100644 --- a/LibGit2Sharp/ObjectDatabase.cs +++ b/LibGit2Sharp/ObjectDatabase.cs @@ -368,5 +368,30 @@ public virtual HistoryDivergence CalculateHistoryDivergence(Commit one, Commit a return new HistoryDivergence(repo, one, another); } + + /// + /// Calculates the current shortest abbreviated + /// string representation for a . + /// + /// The which identifier should be shortened. + /// Minimum length of the shortened representation. + /// A short string representation of the . + public virtual string ShortenObjectId(GitObject gitObject, int? minLength = null) + { + if (minLength.HasValue && (minLength <= 0 || minLength > ObjectId.HexSize)) + { + throw new ArgumentOutOfRangeException("minLength", minLength, + string.Format("Expected value should be greater than zero and less than or equal to {0}.", ObjectId.HexSize)); + } + + string shortSha = Proxy.git_object_short_id(repo.Handle, gitObject.Id); + + if (minLength == null || (minLength <= shortSha.Length)) + { + return shortSha; + } + + return gitObject.Sha.Substring(0, minLength.Value); + } } } diff --git a/LibGit2Sharp/ObjectId.cs b/LibGit2Sharp/ObjectId.cs index 0d7b845dd..7b247e8a1 100644 --- a/LibGit2Sharp/ObjectId.cs +++ b/LibGit2Sharp/ObjectId.cs @@ -11,13 +11,13 @@ namespace LibGit2Sharp public sealed class ObjectId : IEquatable { private readonly GitOid oid; - private const int rawSize = 20; + private const int rawSize = GitOid.Size; private readonly string sha; /// /// Size of the string-based representation of a SHA-1. /// - private const int HexSize = rawSize * 2; + internal const int HexSize = rawSize * 2; private const string hexDigits = "0123456789abcdef"; private static readonly byte[] reverseHexDigits = BuildReverseHexDigits(); diff --git a/LibGit2Sharp/OdbBackend.cs b/LibGit2Sharp/OdbBackend.cs index 549942537..04b43a72e 100644 --- a/LibGit2Sharp/OdbBackend.cs +++ b/LibGit2Sharp/OdbBackend.cs @@ -106,6 +106,11 @@ public abstract int WriteStream( /// public abstract bool Exists(ObjectId id); + /// + /// Requests that this backend check if an object ID exists. The object ID may not be complete (may be a prefix). + /// + public abstract int ExistsPrefix(string shortSha, out ObjectId found); + /// /// Requests that this backend enumerate all items in the backing store. /// @@ -169,6 +174,11 @@ internal IntPtr GitOdbBackendPointer nativeBackend.Exists = BackendEntryPoints.ExistsCallback; } + if ((supportedOperations & OdbBackendOperations.ExistsPrefix) != 0) + { + nativeBackend.ExistsPrefix = BackendEntryPoints.ExistsPrefixCallback; + } + if ((supportedOperations & OdbBackendOperations.ForEach) != 0) { nativeBackend.Foreach = BackendEntryPoints.ForEachCallback; @@ -196,6 +206,7 @@ private static class BackendEntryPoints public static readonly GitOdbBackend.write_callback WriteCallback = Write; public static readonly GitOdbBackend.writestream_callback WriteStreamCallback = WriteStream; public static readonly GitOdbBackend.exists_callback ExistsCallback = Exists; + public static readonly GitOdbBackend.exists_prefix_callback ExistsPrefixCallback = ExistsPrefix; public static readonly GitOdbBackend.foreach_callback ForEachCallback = Foreach; public static readonly GitOdbBackend.free_callback FreeCallback = Free; @@ -498,6 +509,40 @@ private static bool Exists( } } + private static int ExistsPrefix( + ref GitOid found_oid, + IntPtr backend, + ref GitOid short_oid, + UIntPtr len) + { + OdbBackend odbBackend = MarshalOdbBackend(backend); + if (odbBackend == null) + { + return (int)GitErrorCode.Error; + } + + try + { + ObjectId found; + var shortSha = ObjectId.ToString(short_oid.Id, (int)len); + + found_oid.Id = ObjectId.Zero.RawId; + int result = odbBackend.ExistsPrefix(shortSha, out found); + + if (result == (int) GitErrorCode.Ok) + { + found_oid.Id = found.RawId; + } + + return result; + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Odb, ex); + return (int)GitErrorCode.Error; + } + } + private static int Foreach( IntPtr backend, GitOdbBackend.foreach_callback_callback cb, @@ -620,10 +665,15 @@ protected enum OdbBackendOperations /// Exists = 64, + /// + /// This OdbBackend declares that it supports the ExistsPrefix method. + /// + ExistsPrefix = 128, + /// /// This OdbBackend declares that it supports the Foreach method. /// - ForEach = 128, + ForEach = 256, } ///