Skip to content

Commit

Permalink
Simplify dealing with missing git objects
Browse files Browse the repository at this point in the history
  • Loading branch information
jairbubbles committed Oct 3, 2021
1 parent 5055fbd commit 8c8af89
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 17 deletions.
34 changes: 34 additions & 0 deletions LibGit2Sharp.Tests/BlobFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public void CanGetBlobAsText()
using (var repo = new Repository(path))
{
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
Assert.False(blob.IsMissing);

var text = blob.GetContentText();

Expand All @@ -36,6 +37,7 @@ public void CanGetBlobAsFilteredText(string autocrlf, string expectedText)
repo.Config.Set("core.autocrlf", autocrlf);

var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
Assert.False(blob.IsMissing);

var text = blob.GetContentText(new FilteringOptions("foo.txt"));

Expand Down Expand Up @@ -67,6 +69,7 @@ public void CanGetBlobAsTextWithVariousEncodings(string encodingName, int expect
var commit = repo.Commit("bom", Constants.Signature, Constants.Signature);

var blob = (Blob)commit.Tree[bomFile].Target;
Assert.False(blob.IsMissing);
Assert.Equal(expectedContentBytes, blob.Size);
using (var stream = blob.GetContentStream())
{
Expand All @@ -92,6 +95,7 @@ public void CanGetBlobSize()
using (var repo = new Repository(path))
{
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
Assert.False(blob.IsMissing);
Assert.Equal(10, blob.Size);
}
}
Expand All @@ -104,6 +108,7 @@ public void CanLookUpBlob()
{
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
Assert.NotNull(blob);
Assert.False(blob.IsMissing);
}
}

Expand All @@ -114,6 +119,7 @@ public void CanReadBlobStream()
using (var repo = new Repository(path))
{
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
Assert.False(blob.IsMissing);

var contentStream = blob.GetContentStream();
Assert.Equal(blob.Size, contentStream.Length);
Expand All @@ -140,6 +146,7 @@ public void CanReadBlobFilteredStream(string autocrlf, string expectedContent)
repo.Config.Set("core.autocrlf", autocrlf);

var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
Assert.False(blob.IsMissing);

var contentStream = blob.GetContentStream(new FilteringOptions("foo.txt"));
Assert.Equal(expectedContent.Length, contentStream.Length);
Expand All @@ -164,6 +171,7 @@ public void CanReadBlobFilteredStreamOfUnmodifiedBinary()
using (var stream = new MemoryStream(binaryContent))
{
Blob blob = repo.ObjectDatabase.CreateBlob(stream);
Assert.False(blob.IsMissing);

using (var filtered = blob.GetContentStream(new FilteringOptions("foo.txt")))
{
Expand Down Expand Up @@ -196,6 +204,7 @@ public void CanStageAFileGeneratedFromABlobContentStream()
Assert.Equal("baae1fb3760a73481ced1fa03dc15614142c19ef", entry.Id.Sha);

var blob = repo.Lookup<Blob>(entry.Id.Sha);
Assert.False(blob.IsMissing);

using (Stream stream = blob.GetContentStream())
using (Stream file = File.OpenWrite(Path.Combine(repo.Info.WorkingDirectory, "small.fromblob.txt")))
Expand All @@ -217,10 +226,35 @@ public void CanTellIfTheBlobContentLooksLikeBinary()
using (var repo = new Repository(path))
{
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
Assert.False(blob.IsMissing);
Assert.False(blob.IsBinary);
}
}

[Fact]
public void CanTellIsABlobIsMissing()
{
string repoPath = SandboxBareTestRepo();

// Manually delete the objects directory to simulate a partial clone
Directory.Delete(Path.Combine(repoPath, "objects", "a8"), true);

using (var repo = new Repository(repoPath))
{
// Look for the commit that reference the blob which is now missing
var commit = repo.Lookup<Commit>("4a202b346bb0fb0db7eff3cffeb3c70babbd2045");
var blob = (Blob) commit.Tree["README"].Target;

Assert.Equal("a8233120f6ad708f843d861ce2b7228ec4e3dec6", blob.Sha);
Assert.NotNull(blob);
Assert.True(blob.IsMissing);
Assert.Throws<NotFoundException>(() => blob.Size);
Assert.Throws<NotFoundException>(() => blob.IsBinary);
Assert.Throws<NotFoundException>(() => blob.GetContentText());
Assert.Throws<NotFoundException>(() => blob.GetContentText(new FilteringOptions("foo.txt")));
}
}

private static void SkipIfNotSupported(string autocrlf)
{
InconclusiveIf(() => autocrlf == "true" && Constants.IsRunningOnUnix, "Non-Windows does not support core.autocrlf = true");
Expand Down
19 changes: 15 additions & 4 deletions LibGit2Sharp/Blob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ namespace LibGit2Sharp
/// <summary>
/// Stores the binary content of a tracked file.
/// </summary>
/// <remarks>
/// Since the introduction of partially cloned repositories, blobs might be missing on your local repository (see https://git-scm.com/docs/partial-clone)
/// </remarks>
public class Blob : GitObject
{
private readonly ILazy<Int64> lazySize;
Expand All @@ -22,8 +25,8 @@ protected Blob()
internal Blob(Repository repo, ObjectId id)
: base(repo, id)
{
lazySize = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_blob_rawsize);
lazyIsBinary = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_blob_is_binary);
lazySize = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_blob_rawsize, throwIfMissing: true);
lazyIsBinary = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_blob_is_binary, throwIfMissing: true);
}

/// <summary>
Expand All @@ -33,16 +36,19 @@ internal Blob(Repository repo, ObjectId id)
/// can be used.
/// </para>
/// </summary>
public virtual long Size { get { return lazySize.Value; } }
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
public virtual long Size => lazySize.Value;

/// <summary>
/// Determine if the blob content is most certainly binary or not.
/// </summary>
public virtual bool IsBinary { get { return lazyIsBinary.Value; } }
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
public virtual bool IsBinary => lazyIsBinary.Value;

/// <summary>
/// Gets the blob content in a <see cref="Stream"/>.
/// </summary>
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
public virtual Stream GetContentStream()
{
return Proxy.git_blob_rawcontent_stream(repo.Handle, Id, Size);
Expand All @@ -53,6 +59,7 @@ public virtual Stream GetContentStream()
/// checked out to the working directory.
/// <param name="filteringOptions">Parameter controlling content filtering behavior</param>
/// </summary>
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
public virtual Stream GetContentStream(FilteringOptions filteringOptions)
{
Ensure.ArgumentNotNull(filteringOptions, "filteringOptions");
Expand All @@ -64,6 +71,7 @@ public virtual Stream GetContentStream(FilteringOptions filteringOptions)
/// Gets the blob content, decoded with UTF8 encoding if the encoding cannot be detected from the byte order mark
/// </summary>
/// <returns>Blob content as text.</returns>
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
public virtual string GetContentText()
{
return ReadToEnd(GetContentStream(), null);
Expand All @@ -75,6 +83,7 @@ public virtual string GetContentText()
/// </summary>
/// <param name="encoding">The encoding of the text to use, if it cannot be detected</param>
/// <returns>Blob content as text.</returns>
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
public virtual string GetContentText(Encoding encoding)
{
Ensure.ArgumentNotNull(encoding, "encoding");
Expand All @@ -87,6 +96,7 @@ public virtual string GetContentText(Encoding encoding)
/// </summary>
/// <param name="filteringOptions">Parameter controlling content filtering behavior</param>
/// <returns>Blob content as text.</returns>
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
public virtual string GetContentText(FilteringOptions filteringOptions)
{
return GetContentText(filteringOptions, null);
Expand All @@ -101,6 +111,7 @@ public virtual string GetContentText(FilteringOptions filteringOptions)
/// <param name="filteringOptions">Parameter controlling content filtering behavior</param>
/// <param name="encoding">The encoding of the text. (default: detected or UTF8)</param>
/// <returns>Blob content as text.</returns>
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
public virtual string GetContentText(FilteringOptions filteringOptions, Encoding encoding)
{
Ensure.ArgumentNotNull(filteringOptions, "filteringOptions");
Expand Down
4 changes: 2 additions & 2 deletions LibGit2Sharp/Core/GitObjectLazyGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ protected override void EvaluateInternal(Action<ObjectHandle> evaluator)
}
}

public static ILazy<TResult> Singleton<TResult>(Repository repo, ObjectId id, Func<ObjectHandle, TResult> resultSelector)
public static ILazy<TResult> Singleton<TResult>(Repository repo, ObjectId id, Func<ObjectHandle, TResult> resultSelector, bool throwIfMissing = false)
{
return Singleton(() =>
{
using (var osw = new ObjectSafeWrapper(id, repo.Handle))
using (var osw = new ObjectSafeWrapper(id, repo.Handle, throwIfMissing: throwIfMissing))
{
return resultSelector(osw.ObjectPtr);
}
Expand Down
12 changes: 7 additions & 5 deletions LibGit2Sharp/Core/ObjectSafeWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ internal class ObjectSafeWrapper : IDisposable
{
private readonly ObjectHandle objectPtr;

public unsafe ObjectSafeWrapper(ObjectId id, RepositoryHandle handle, bool allowNullObjectId = false)
public unsafe ObjectSafeWrapper(ObjectId id, RepositoryHandle handle, bool allowNullObjectId = false, bool throwIfMissing = false)
{
Ensure.ArgumentNotNull(handle, "handle");

Expand All @@ -20,13 +20,15 @@ public unsafe ObjectSafeWrapper(ObjectId id, RepositoryHandle handle, bool allow
Ensure.ArgumentNotNull(id, "id");
objectPtr = Proxy.git_object_lookup(handle, id, GitObjectType.Any);
}
}

public ObjectHandle ObjectPtr
{
get { return objectPtr; }
if (objectPtr == null && throwIfMissing)
{
throw new NotFoundException($"No valid git object identified by '{id}' exists in the repository.");
}
}

public ObjectHandle ObjectPtr => objectPtr;

public void Dispose()
{
Dispose(true);
Expand Down
4 changes: 2 additions & 2 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static unsafe ObjectId git_blob_create_from_workdir(RepositoryHandle repo
public static unsafe UnmanagedMemoryStream git_blob_filtered_content_stream(RepositoryHandle repo, ObjectId id, string path, bool check_for_binary_data)
{
var buf = new GitBuf();
var handle = new ObjectSafeWrapper(id, repo).ObjectPtr;
var handle = new ObjectSafeWrapper(id, repo, throwIfMissing: true).ObjectPtr;

return new RawContentStream(handle, h =>
{
Expand All @@ -85,7 +85,7 @@ public static unsafe UnmanagedMemoryStream git_blob_filtered_content_stream(Repo

public static unsafe UnmanagedMemoryStream git_blob_rawcontent_stream(RepositoryHandle repo, ObjectId id, Int64 size)
{
var handle = new ObjectSafeWrapper(id, repo).ObjectPtr;
var handle = new ObjectSafeWrapper(id, repo, throwIfMissing: true).ObjectPtr;
return new RawContentStream(handle, h => NativeMethods.git_blob_rawcontent(h), h => size);
}

Expand Down
13 changes: 11 additions & 2 deletions LibGit2Sharp/GitObject.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Handles;

namespace LibGit2Sharp
{
Expand Down Expand Up @@ -33,6 +31,8 @@ public abstract class GitObject : IEquatable<GitObject>, IBelongToARepository
private static readonly LambdaEqualityHelper<GitObject> equalityHelper =
new LambdaEqualityHelper<GitObject>(x => x.Id);

private readonly ILazy<bool> lazyIsMissing;

/// <summary>
/// The <see cref="Repository"/> containing the object.
/// </summary>
Expand All @@ -53,13 +53,22 @@ protected GitObject(Repository repo, ObjectId id)
{
this.repo = repo;
Id = id;
lazyIsMissing = GitObjectLazyGroup.Singleton(repo, id, handle => handle == null, throwIfMissing: false);
}

/// <summary>
/// Gets the id of this object
/// </summary>
public virtual ObjectId Id { get; private set; }

/// <summary>
/// Determine if the object is missing
/// </summary>
/// <remarks>
/// This is common when dealing with partially cloned repositories as blobs or trees could be missing
/// </remarks>
public virtual bool IsMissing => lazyIsMissing.Value;

/// <summary>
/// Gets the 40 character sha1 of this object.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions LibGit2Sharp/Tree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ internal Tree(Repository repo, ObjectId id, string path)
{
this.path = path ?? "";

lazyCount = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_tree_entrycount);
lazyCount = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_tree_entrycount, throwIfMissing: true);
}

/// <summary>
/// Gets the number of <see cref="TreeEntry"/> immediately under this <see cref="Tree"/>.
/// </summary>
public virtual int Count { get { return lazyCount.Value; } }
public virtual int Count => lazyCount.Value;

/// <summary>
/// Gets the <see cref="TreeEntry"/> pointed at by the <paramref name="relativePath"/> in this <see cref="Tree"/> instance.
Expand Down

0 comments on commit 8c8af89

Please sign in to comment.