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 Nov 22, 2021
1 parent 4daf618 commit b05a00f
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 27 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 CanTellIfABlobIsMissing()
{
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 up for the tree that reference the blob which is now missing
var tree = repo.Lookup<Tree>("fd093bff70906175335656e6ce6ae05783708765");
var blob = (Blob) 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
42 changes: 42 additions & 0 deletions LibGit2Sharp.Tests/TreeFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public void CanCompareTwoTreeEntries()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);
TreeEntry treeEntry1 = tree["README"];
TreeEntry treeEntry2 = tree["README"];
Assert.Equal(treeEntry2, treeEntry1);
Expand All @@ -31,6 +32,7 @@ public void CanConvertEntryToBlob()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);
TreeEntry treeEntry = tree["README"];

var blob = treeEntry.Target as Blob;
Expand All @@ -45,6 +47,7 @@ public void CanConvertEntryToTree()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);
TreeEntry treeEntry = tree["1"];

var subtree = treeEntry.Target as Tree;
Expand All @@ -59,6 +62,7 @@ public void CanEnumerateBlobs()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);

IEnumerable<Blob> blobs = tree
.Where(e => e.TargetType == TreeEntryTargetType.Blob)
Expand All @@ -76,6 +80,7 @@ public void CanEnumerateSubTrees()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);

IEnumerable<Tree> subTrees = tree
.Where(e => e.TargetType == TreeEntryTargetType.Tree)
Expand All @@ -93,6 +98,7 @@ public void CanEnumerateTreeEntries()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);
Assert.Equal(tree.Count, tree.Count());

Assert.Equal(new[] { "1", "README", "branch_file.txt", "new.txt" }, tree.Select(te => te.Name).ToArray());
Expand All @@ -106,6 +112,7 @@ public void CanGetEntryByName()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);
TreeEntry treeEntry = tree["README"];
Assert.Equal("a8233120f6ad708f843d861ce2b7228ec4e3dec6", treeEntry.Target.Sha);
Assert.Equal("README", treeEntry.Name);
Expand All @@ -119,6 +126,7 @@ public void GettingAnUknownTreeEntryReturnsNull()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);
TreeEntry treeEntry = tree["I-do-not-exist"];
Assert.Null(treeEntry);
}
Expand All @@ -131,6 +139,7 @@ public void CanGetEntryCountFromTree()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);
Assert.Equal(4, tree.Count);
}
}
Expand All @@ -142,6 +151,7 @@ public void CanReadEntryAttributes()
using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>(sha);
Assert.False(tree.IsMissing);
Assert.Equal(Mode.NonExecutableFile, tree["README"].Mode);
}
}
Expand All @@ -154,6 +164,7 @@ public void CanReadTheTreeData()
{
var tree = repo.Lookup<Tree>(sha);
Assert.NotNull(tree);
Assert.False(tree.IsMissing);
}
}

Expand All @@ -165,6 +176,7 @@ public void TreeDataIsPresent()
{
GitObject tree = repo.Lookup(sha);
Assert.NotNull(tree);
Assert.False(tree.IsMissing);
}
}

Expand All @@ -175,6 +187,7 @@ public void TreeUsesPosixStylePaths()
{
/* From a commit tree */
var commitTree = repo.Lookup<Commit>("4c062a6").Tree;
Assert.False(commitTree.IsMissing);
Assert.NotNull(commitTree["1/branch_file.txt"]);
Assert.Null(commitTree["1\\branch_file.txt"]);
}
Expand All @@ -188,6 +201,7 @@ public void CanRetrieveTreeEntryPath()
{
/* From a commit tree */
var commitTree = repo.Lookup<Commit>("4c062a6").Tree;
Assert.False(commitTree.IsMissing);

TreeEntry treeTreeEntry = commitTree["1"];
Assert.Equal("1", treeTreeEntry.Path);
Expand All @@ -201,6 +215,7 @@ public void CanRetrieveTreeEntryPath()
// tree but exposes a complete path through its Path property
var subTree = treeTreeEntry.Target as Tree;
Assert.NotNull(subTree);
Assert.False(subTree.IsMissing);
TreeEntry anInstance = subTree["branch_file.txt"];

Assert.NotEqual("branch_file.txt", anInstance.Path);
Expand Down Expand Up @@ -239,6 +254,7 @@ public void CanParseSymlinkTreeEntries()
.Add("A symlink", linkContent, Mode.SymbolicLink);

Tree t = repo.ObjectDatabase.CreateTree(td);
Assert.False(t.IsMissing);

var te = t["A symlink"];

Expand All @@ -248,5 +264,31 @@ public void CanParseSymlinkTreeEntries()
Assert.Equal(linkContent, te.Target);
}
}

[Fact]
public void CanTellIfATreeIsMissing()
{
var path = SandboxBareTestRepo();

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

using (var repo = new Repository(path))
{
// Look up for the commit that reference the tree which is now missing
var commit = repo.Lookup<Commit>("4a202b346bb0fb0db7eff3cffeb3c70babbd2045");

Assert.True(commit.Tree.IsMissing);
Assert.Equal("fd093bff70906175335656e6ce6ae05783708765", commit.Tree.Sha);
Assert.Throws<NotFoundException>(() => commit.Tree.Count);
Assert.Throws<NotFoundException>(() => commit.Tree.Count());
Assert.Throws<NotFoundException>(() => commit.Tree["README"]);
Assert.Throws<NotFoundException>(() => commit.Tree.ToArray());
Assert.Throws<NotFoundException>(() =>
{
foreach (var _ in commit.Tree) { }
});
}
}
}
}
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

0 comments on commit b05a00f

Please sign in to comment.