Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify dealing with missing git objects #1909

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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