Skip to content

Commit b05a00f

Browse files
committed
Simplify dealing with missing git objects
1 parent 4daf618 commit b05a00f

File tree

8 files changed

+126
-27
lines changed

8 files changed

+126
-27
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 CanTellIfABlobIsMissing()
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 up for the tree that reference the blob which is now missing
245+
var tree = repo.Lookup<Tree>("fd093bff70906175335656e6ce6ae05783708765");
246+
var blob = (Blob) 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.Tests/TreeFixture.cs

+42
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public void CanCompareTwoTreeEntries()
1717
using (var repo = new Repository(path))
1818
{
1919
var tree = repo.Lookup<Tree>(sha);
20+
Assert.False(tree.IsMissing);
2021
TreeEntry treeEntry1 = tree["README"];
2122
TreeEntry treeEntry2 = tree["README"];
2223
Assert.Equal(treeEntry2, treeEntry1);
@@ -31,6 +32,7 @@ public void CanConvertEntryToBlob()
3132
using (var repo = new Repository(path))
3233
{
3334
var tree = repo.Lookup<Tree>(sha);
35+
Assert.False(tree.IsMissing);
3436
TreeEntry treeEntry = tree["README"];
3537

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

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

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

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

98104
Assert.Equal(new[] { "1", "README", "branch_file.txt", "new.txt" }, tree.Select(te => te.Name).ToArray());
@@ -106,6 +112,7 @@ public void CanGetEntryByName()
106112
using (var repo = new Repository(path))
107113
{
108114
var tree = repo.Lookup<Tree>(sha);
115+
Assert.False(tree.IsMissing);
109116
TreeEntry treeEntry = tree["README"];
110117
Assert.Equal("a8233120f6ad708f843d861ce2b7228ec4e3dec6", treeEntry.Target.Sha);
111118
Assert.Equal("README", treeEntry.Name);
@@ -119,6 +126,7 @@ public void GettingAnUknownTreeEntryReturnsNull()
119126
using (var repo = new Repository(path))
120127
{
121128
var tree = repo.Lookup<Tree>(sha);
129+
Assert.False(tree.IsMissing);
122130
TreeEntry treeEntry = tree["I-do-not-exist"];
123131
Assert.Null(treeEntry);
124132
}
@@ -131,6 +139,7 @@ public void CanGetEntryCountFromTree()
131139
using (var repo = new Repository(path))
132140
{
133141
var tree = repo.Lookup<Tree>(sha);
142+
Assert.False(tree.IsMissing);
134143
Assert.Equal(4, tree.Count);
135144
}
136145
}
@@ -142,6 +151,7 @@ public void CanReadEntryAttributes()
142151
using (var repo = new Repository(path))
143152
{
144153
var tree = repo.Lookup<Tree>(sha);
154+
Assert.False(tree.IsMissing);
145155
Assert.Equal(Mode.NonExecutableFile, tree["README"].Mode);
146156
}
147157
}
@@ -154,6 +164,7 @@ public void CanReadTheTreeData()
154164
{
155165
var tree = repo.Lookup<Tree>(sha);
156166
Assert.NotNull(tree);
167+
Assert.False(tree.IsMissing);
157168
}
158169
}
159170

@@ -165,6 +176,7 @@ public void TreeDataIsPresent()
165176
{
166177
GitObject tree = repo.Lookup(sha);
167178
Assert.NotNull(tree);
179+
Assert.False(tree.IsMissing);
168180
}
169181
}
170182

@@ -175,6 +187,7 @@ public void TreeUsesPosixStylePaths()
175187
{
176188
/* From a commit tree */
177189
var commitTree = repo.Lookup<Commit>("4c062a6").Tree;
190+
Assert.False(commitTree.IsMissing);
178191
Assert.NotNull(commitTree["1/branch_file.txt"]);
179192
Assert.Null(commitTree["1\\branch_file.txt"]);
180193
}
@@ -188,6 +201,7 @@ public void CanRetrieveTreeEntryPath()
188201
{
189202
/* From a commit tree */
190203
var commitTree = repo.Lookup<Commit>("4c062a6").Tree;
204+
Assert.False(commitTree.IsMissing);
191205

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

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

241256
Tree t = repo.ObjectDatabase.CreateTree(td);
257+
Assert.False(t.IsMissing);
242258

243259
var te = t["A symlink"];
244260

@@ -248,5 +264,31 @@ public void CanParseSymlinkTreeEntries()
248264
Assert.Equal(linkContent, te.Target);
249265
}
250266
}
267+
268+
[Fact]
269+
public void CanTellIfATreeIsMissing()
270+
{
271+
var path = SandboxBareTestRepo();
272+
273+
// Manually delete the objects directory to simulate a partial clone
274+
Directory.Delete(Path.Combine(path, "objects", "fd"), true);
275+
276+
using (var repo = new Repository(path))
277+
{
278+
// Look up for the commit that reference the tree which is now missing
279+
var commit = repo.Lookup<Commit>("4a202b346bb0fb0db7eff3cffeb3c70babbd2045");
280+
281+
Assert.True(commit.Tree.IsMissing);
282+
Assert.Equal("fd093bff70906175335656e6ce6ae05783708765", commit.Tree.Sha);
283+
Assert.Throws<NotFoundException>(() => commit.Tree.Count);
284+
Assert.Throws<NotFoundException>(() => commit.Tree.Count());
285+
Assert.Throws<NotFoundException>(() => commit.Tree["README"]);
286+
Assert.Throws<NotFoundException>(() => commit.Tree.ToArray());
287+
Assert.Throws<NotFoundException>(() =>
288+
{
289+
foreach (var _ in commit.Tree) { }
290+
});
291+
}
292+
}
251293
}
252294
}

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);

0 commit comments

Comments
 (0)