Skip to content

Commit f60b643

Browse files
committed
commit: fall back to full read when maybe_tree is NULL
When we load a commit object from the commit graph (rather than reading the object contents), we don't fill in its "maybe_tree" entry, but rather wait to lazy-load it. This goes back to 7b8a21d (commit-graph: lazy-load trees for commits, 2018-04-06), and saves the work of instantiating tree objects that nobody cares about. But it creates a data dependency: now the commit struct depends on the graph file to do that lazy load. This is a problem if we close the graph file; now we have a commit struct that claims to be parsed but is missing some of its data. It's rare for this to be a problem in practice, because we don't tend to close the graph files at all, and if we do we don't tend to look at their commits afterward. But there is one case that is easy to trigger: git-clone's --dissociate option will close the object database before running the dissociate repack, and then afterwards still try to check out the working tree. This will yield an error like: fatal: unable to parse commit b29edc0babef41810f7b1c9ee1d74058f22e4080 warning: Clone succeeded, but checkout failed. What happens is that we expect repo_get_commit_tree() to lazy-load the tree, but commit_graph_position() returns COMMIT_NOT_FROM_GRAPH because the position slab has gone away (and even if it hadn't, we don't have the graph file itself available anymore). Let's try harder to find the tree in repo_get_commit_tree() by actually opening the commit object and parsing the tree line. This is extra work, but no more than we'd have to go to if we hadn't done the initial graph load in the first place. It does mean that a corrupt commit (e.g., one that points to a non-tree object for which we couldn't instantiate a struct) will repeatedly load the object from disk, once for each call to repo_get_commit_tree(). But such corruptions should be rare, and we don't tend to perform such calls repeatedly (usually we'd abort the operation upon seeing corruption). It also means we have to reimplement a bit of the commit parsing. We can't just use parse_commit_buffer() here, because it expects an unparsed struct and wants to load everything, including parent links. But we don't know if the parent list has been munged during traversal, so it's not safe for us to touch it. Fortunately, it's quite easy to load just the tree, as it is always the first line of the commit object. There is an alternative approach which I considered but rejected: "complete" each graph-loaded commit struct when we close the graph file by looking up and instantiating their trees at close time. This is the most elegant solution in some sense, as it resolves the data dependency at the moment it goes away. And it avoids ever opening the commit objects at all, which can be more efficient. But not always. The resolving effort scales with the number of graph-loaded commits, even though we may only later access one or a few. So the tradeoff depends on how many were loaded in total versus how many will be later accessed. And in most cases, we will not access any at all! Programs which close the object database before exiting will then do a bunch of work for no reason. This could be mitigated by requiring a separate function to resolve the graph structs before closing the file. But now each close call has to consider whether to call that resolving function. So we'd fix this case in git-clone, but we don't know what other cases (if any) are lurking. Moreover, this strategy does nothing if we lose access to the graph file unexpectedly (e.g., due to a system error). I'm not entirely sure this is possible now (we mmap it, so I'd guess any error would turn into SIGBUS anyway). But it feels like making the lazy-load more robust (which this patch does) is the best way to handle a wide variety of possible failure modes. Signed-off-by: Jeff King <peff@peff.net>
1 parent 56a4f3c commit f60b643

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

commit.c

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,27 @@ static inline void set_commit_tree(struct commit *c, struct tree *t)
434434
c->maybe_tree = t;
435435
}
436436

437+
static void load_tree_from_commit_contents(struct repository *r, struct commit *commit)
438+
{
439+
enum object_type type;
440+
unsigned long size;
441+
char *buf;
442+
const char *p;
443+
struct object_id tree_oid;
444+
445+
buf = odb_read_object(r->objects, &commit->object.oid, &type, &size);
446+
if (!buf)
447+
return;
448+
449+
if (type == OBJ_COMMIT &&
450+
skip_prefix(buf, "tree ", &p) &&
451+
!parse_oid_hex_algop(p, &tree_oid, &p, r->hash_algo) &&
452+
*p == '\n')
453+
set_commit_tree(commit, lookup_tree(r, &tree_oid));
454+
455+
free(buf);
456+
}
457+
437458
struct tree *repo_get_commit_tree(struct repository *r,
438459
const struct commit *commit)
439460
{
@@ -443,7 +464,17 @@ struct tree *repo_get_commit_tree(struct repository *r,
443464
if (commit_graph_position(commit) != COMMIT_NOT_FROM_GRAPH)
444465
return get_commit_tree_in_graph(r, commit);
445466

446-
return NULL;
467+
/*
468+
* This is either a corrupt commit, or one which we partially loaded
469+
* from a graph file but then subsequently threw away the graph data.
470+
*
471+
* Optimistically assume it's the latter and try to reload from
472+
* scratch. This gives a performance penalty if it really is a corrupt
473+
* commit, but presumably that happens rarely (and only once per
474+
* process).
475+
*/
476+
load_tree_from_commit_contents(r, (struct commit *)commit);
477+
return commit->maybe_tree;
447478
}
448479

449480
struct object_id *get_commit_tree_oid(const struct commit *commit)

t/t5604-clone-reference.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,27 @@ test_expect_success SYMLINKS 'clone repo with symlinked objects directory' '
360360
grep "is a symlink, refusing to clone with --local" err
361361
'
362362

363+
test_expect_success 'dissociate from repo with commit graph' '
364+
git init orig &&
365+
# We are trying to make sure the dissociated repo can
366+
# find the tree of the tip commit, so the test could still
367+
# serve its purpose with an empty tree. But having actual
368+
# content future-proofs us against any kind of internal
369+
# empty-tree optimizations.
370+
echo content >orig/file &&
371+
git -C orig add . &&
372+
git -C orig commit -m foo &&
373+
374+
# We will use graph.git as our "local" source to dissociate
375+
# from.
376+
git clone --bare orig graph.git &&
377+
git -C graph.git commit-graph write --reachable &&
378+
379+
# And then finally clone orig, using graph.git to get our objects. This
380+
# must be non-bare so that we perform the checkout step, which will
381+
# need to access the tree of HEAD, which we will have originally loaded
382+
# via the commit graph.
383+
git clone --no-local --reference graph.git --dissociate orig clone
384+
'
385+
363386
test_done

0 commit comments

Comments
 (0)