Skip to content

Commit 8c8af89

Browse files
committed
Simplify dealing with missing git objects
1 parent 5055fbd commit 8c8af89

File tree

7 files changed

+73
-17
lines changed

7 files changed

+73
-17
lines changed

Diff for: LibGit2Sharp.Tests/BlobFixture.cs

+34
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public void CanGetBlobAsText()
1515
using (var repo = new Repository(path))
1616
{
1717
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
18+
Assert.False(blob.IsMissing);
1819

1920
var text = blob.GetContentText();
2021

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

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

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

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

6971
var blob = (Blob)commit.Tree[bomFile].Target;
72+
Assert.False(blob.IsMissing);
7073
Assert.Equal(expectedContentBytes, blob.Size);
7174
using (var stream = blob.GetContentStream())
7275
{
@@ -92,6 +95,7 @@ public void CanGetBlobSize()
9295
using (var repo = new Repository(path))
9396
{
9497
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
98+
Assert.False(blob.IsMissing);
9599
Assert.Equal(10, blob.Size);
96100
}
97101
}
@@ -104,6 +108,7 @@ public void CanLookUpBlob()
104108
{
105109
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
106110
Assert.NotNull(blob);
111+
Assert.False(blob.IsMissing);
107112
}
108113
}
109114

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

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

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

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

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

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

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

234+
[Fact]
235+
public void CanTellIsABlobIsMissing()
236+
{
237+
string repoPath = SandboxBareTestRepo();
238+
239+
// Manually delete the objects directory to simulate a partial clone
240+
Directory.Delete(Path.Combine(repoPath, "objects", "a8"), true);
241+
242+
using (var repo = new Repository(repoPath))
243+
{
244+
// Look for the commit that reference the blob which is now missing
245+
var commit = repo.Lookup<Commit>("4a202b346bb0fb0db7eff3cffeb3c70babbd2045");
246+
var blob = (Blob) commit.Tree["README"].Target;
247+
248+
Assert.Equal("a8233120f6ad708f843d861ce2b7228ec4e3dec6", blob.Sha);
249+
Assert.NotNull(blob);
250+
Assert.True(blob.IsMissing);
251+
Assert.Throws<NotFoundException>(() => blob.Size);
252+
Assert.Throws<NotFoundException>(() => blob.IsBinary);
253+
Assert.Throws<NotFoundException>(() => blob.GetContentText());
254+
Assert.Throws<NotFoundException>(() => blob.GetContentText(new FilteringOptions("foo.txt")));
255+
}
256+
}
257+
224258
private static void SkipIfNotSupported(string autocrlf)
225259
{
226260
InconclusiveIf(() => autocrlf == "true" && Constants.IsRunningOnUnix, "Non-Windows does not support core.autocrlf = true");

Diff for: LibGit2Sharp/Blob.cs

+15-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ namespace LibGit2Sharp
88
/// <summary>
99
/// Stores the binary content of a tracked file.
1010
/// </summary>
11+
/// <remarks>
12+
/// Since the introduction of partially cloned repositories, blobs might be missing on your local repository (see https://git-scm.com/docs/partial-clone)
13+
/// </remarks>
1114
public class Blob : GitObject
1215
{
1316
private readonly ILazy<Int64> lazySize;
@@ -22,8 +25,8 @@ protected Blob()
2225
internal Blob(Repository repo, ObjectId id)
2326
: base(repo, id)
2427
{
25-
lazySize = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_blob_rawsize);
26-
lazyIsBinary = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_blob_is_binary);
28+
lazySize = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_blob_rawsize, throwIfMissing: true);
29+
lazyIsBinary = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_blob_is_binary, throwIfMissing: true);
2730
}
2831

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

3842
/// <summary>
3943
/// Determine if the blob content is most certainly binary or not.
4044
/// </summary>
41-
public virtual bool IsBinary { get { return lazyIsBinary.Value; } }
45+
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
46+
public virtual bool IsBinary => lazyIsBinary.Value;
4247

4348
/// <summary>
4449
/// Gets the blob content in a <see cref="Stream"/>.
4550
/// </summary>
51+
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
4652
public virtual Stream GetContentStream()
4753
{
4854
return Proxy.git_blob_rawcontent_stream(repo.Handle, Id, Size);
@@ -53,6 +59,7 @@ public virtual Stream GetContentStream()
5359
/// checked out to the working directory.
5460
/// <param name="filteringOptions">Parameter controlling content filtering behavior</param>
5561
/// </summary>
62+
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
5663
public virtual Stream GetContentStream(FilteringOptions filteringOptions)
5764
{
5865
Ensure.ArgumentNotNull(filteringOptions, "filteringOptions");
@@ -64,6 +71,7 @@ public virtual Stream GetContentStream(FilteringOptions filteringOptions)
6471
/// Gets the blob content, decoded with UTF8 encoding if the encoding cannot be detected from the byte order mark
6572
/// </summary>
6673
/// <returns>Blob content as text.</returns>
74+
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
6775
public virtual string GetContentText()
6876
{
6977
return ReadToEnd(GetContentStream(), null);
@@ -75,6 +83,7 @@ public virtual string GetContentText()
7583
/// </summary>
7684
/// <param name="encoding">The encoding of the text to use, if it cannot be detected</param>
7785
/// <returns>Blob content as text.</returns>
86+
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
7887
public virtual string GetContentText(Encoding encoding)
7988
{
8089
Ensure.ArgumentNotNull(encoding, "encoding");
@@ -87,6 +96,7 @@ public virtual string GetContentText(Encoding encoding)
8796
/// </summary>
8897
/// <param name="filteringOptions">Parameter controlling content filtering behavior</param>
8998
/// <returns>Blob content as text.</returns>
99+
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
90100
public virtual string GetContentText(FilteringOptions filteringOptions)
91101
{
92102
return GetContentText(filteringOptions, null);
@@ -101,6 +111,7 @@ public virtual string GetContentText(FilteringOptions filteringOptions)
101111
/// <param name="filteringOptions">Parameter controlling content filtering behavior</param>
102112
/// <param name="encoding">The encoding of the text. (default: detected or UTF8)</param>
103113
/// <returns>Blob content as text.</returns>
114+
/// <exception cref="NotFoundException">Throws if blob is missing</exception>
104115
public virtual string GetContentText(FilteringOptions filteringOptions, Encoding encoding)
105116
{
106117
Ensure.ArgumentNotNull(filteringOptions, "filteringOptions");

Diff for: LibGit2Sharp/Core/GitObjectLazyGroup.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ protected override void EvaluateInternal(Action<ObjectHandle> evaluator)
2121
}
2222
}
2323

24-
public static ILazy<TResult> Singleton<TResult>(Repository repo, ObjectId id, Func<ObjectHandle, TResult> resultSelector)
24+
public static ILazy<TResult> Singleton<TResult>(Repository repo, ObjectId id, Func<ObjectHandle, TResult> resultSelector, bool throwIfMissing = false)
2525
{
2626
return Singleton(() =>
2727
{
28-
using (var osw = new ObjectSafeWrapper(id, repo.Handle))
28+
using (var osw = new ObjectSafeWrapper(id, repo.Handle, throwIfMissing: throwIfMissing))
2929
{
3030
return resultSelector(osw.ObjectPtr);
3131
}

Diff for: LibGit2Sharp/Core/ObjectSafeWrapper.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ internal class ObjectSafeWrapper : IDisposable
77
{
88
private readonly ObjectHandle objectPtr;
99

10-
public unsafe ObjectSafeWrapper(ObjectId id, RepositoryHandle handle, bool allowNullObjectId = false)
10+
public unsafe ObjectSafeWrapper(ObjectId id, RepositoryHandle handle, bool allowNullObjectId = false, bool throwIfMissing = false)
1111
{
1212
Ensure.ArgumentNotNull(handle, "handle");
1313

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

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

30+
public ObjectHandle ObjectPtr => objectPtr;
31+
3032
public void Dispose()
3133
{
3234
Dispose(true);

Diff for: LibGit2Sharp/Core/Proxy.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static unsafe ObjectId git_blob_create_from_workdir(RepositoryHandle repo
7272
public static unsafe UnmanagedMemoryStream git_blob_filtered_content_stream(RepositoryHandle repo, ObjectId id, string path, bool check_for_binary_data)
7373
{
7474
var buf = new GitBuf();
75-
var handle = new ObjectSafeWrapper(id, repo).ObjectPtr;
75+
var handle = new ObjectSafeWrapper(id, repo, throwIfMissing: true).ObjectPtr;
7676

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

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

Diff for: LibGit2Sharp/GitObject.cs

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4-
using System.Globalization;
54
using LibGit2Sharp.Core;
6-
using LibGit2Sharp.Core.Handles;
75

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

34+
private readonly ILazy<bool> lazyIsMissing;
35+
3636
/// <summary>
3737
/// The <see cref="Repository"/> containing the object.
3838
/// </summary>
@@ -53,13 +53,22 @@ protected GitObject(Repository repo, ObjectId id)
5353
{
5454
this.repo = repo;
5555
Id = id;
56+
lazyIsMissing = GitObjectLazyGroup.Singleton(repo, id, handle => handle == null, throwIfMissing: false);
5657
}
5758

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

64+
/// <summary>
65+
/// Determine if the object is missing
66+
/// </summary>
67+
/// <remarks>
68+
/// This is common when dealing with partially cloned repositories as blobs or trees could be missing
69+
/// </remarks>
70+
public virtual bool IsMissing => lazyIsMissing.Value;
71+
6372
/// <summary>
6473
/// Gets the 40 character sha1 of this object.
6574
/// </summary>

Diff for: LibGit2Sharp/Tree.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ internal Tree(Repository repo, ObjectId id, string path)
3131
{
3232
this.path = path ?? "";
3333

34-
lazyCount = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_tree_entrycount);
34+
lazyCount = GitObjectLazyGroup.Singleton(repo, id, Proxy.git_tree_entrycount, throwIfMissing: true);
3535
}
3636

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

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

0 commit comments

Comments
 (0)