Skip to content

Commit 47057c1

Browse files
committed
commit-graph: Support lookups of entries in a commit-graph
This change introduces `git_commit_graph_entry_find()` and `git_commit_graph_entry_parent()`. These two functions allow a much faster lookup of commits by ID, since the ODB does not need to be consulted, the commit object does not need to be inflated, and the contents of the commit object do not need to be parsed. Part of: libgit2#5757
1 parent 155a84c commit 47057c1

File tree

5 files changed

+251
-0
lines changed

5 files changed

+251
-0
lines changed

fuzzers/commit_graph_fuzzer.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ int LLVMFuzzerInitialize(int *argc, char ***argv)
3232
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
3333
{
3434
git_commit_graph_file cgraph = {{0}};
35+
git_commit_graph_entry e;
3536
git_buf commit_graph_buf = GIT_BUF_INIT;
3637
git_oid oid = {{0}};
3738
bool append_hash = false;
@@ -68,6 +69,10 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
6869
< 0)
6970
goto cleanup;
7071

72+
/* Search for any oid, just to exercise that codepath. */
73+
if (git_commit_graph_entry_find(&e, &cgraph, &oid, GIT_OID_HEXSZ) < 0)
74+
goto cleanup;
75+
7176
cleanup:
7277
git_commit_graph_close(&cgraph);
7378
git_buf_dispose(&commit_graph_buf);

src/commit_graph.c

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include "futils.h"
1111
#include "hash.h"
12+
#include "pack.h"
1213

1314
#define GIT_COMMIT_GRAPH_MISSING_PARENT 0x70000000
1415

@@ -278,6 +279,142 @@ int git_commit_graph_open(git_commit_graph_file **cgraph_out, const char *path)
278279
return 0;
279280
}
280281

282+
static int git_commit_graph_entry_get_byindex(
283+
git_commit_graph_entry *e,
284+
const git_commit_graph_file *cgraph,
285+
size_t pos)
286+
{
287+
const unsigned char *commit_data;
288+
289+
GIT_ASSERT_ARG(e);
290+
GIT_ASSERT_ARG(cgraph);
291+
292+
if (pos >= cgraph->num_commits) {
293+
git_error_set(GIT_ERROR_INVALID, "commit index %zu does not exist", pos);
294+
return GIT_ENOTFOUND;
295+
}
296+
297+
commit_data = cgraph->commit_data + pos * (GIT_OID_RAWSZ + 4 * sizeof(uint32_t));
298+
git_oid_cpy(&e->tree_oid, (const git_oid *)commit_data);
299+
e->parent_indices[0] = ntohl(*((uint32_t *)(commit_data + GIT_OID_RAWSZ)));
300+
e->parent_indices[1]
301+
= ntohl(*((uint32_t *)(commit_data + GIT_OID_RAWSZ + sizeof(uint32_t))));
302+
e->parent_count = (e->parent_indices[0] != GIT_COMMIT_GRAPH_MISSING_PARENT)
303+
+ (e->parent_indices[1] != GIT_COMMIT_GRAPH_MISSING_PARENT);
304+
e->generation = ntohl(*((uint32_t *)(commit_data + GIT_OID_RAWSZ + 2 * sizeof(uint32_t))));
305+
e->commit_time = ntohl(*((uint32_t *)(commit_data + GIT_OID_RAWSZ + 3 * sizeof(uint32_t))));
306+
307+
e->commit_time |= (e->generation & 0x3ull) << 32ull;
308+
e->generation >>= 2u;
309+
if (e->parent_indices[1] & 0x80000000u) {
310+
uint32_t extra_edge_list_pos = e->parent_indices[1] & 0x7fffffff;
311+
312+
/* Make sure we're not being sent out of bounds */
313+
if (extra_edge_list_pos >= cgraph->num_extra_edge_list) {
314+
git_error_set(GIT_ERROR_INVALID,
315+
"commit %u does not exist",
316+
extra_edge_list_pos);
317+
return GIT_ENOTFOUND;
318+
}
319+
320+
e->extra_parents_index = extra_edge_list_pos;
321+
while (extra_edge_list_pos < cgraph->num_extra_edge_list
322+
&& (ntohl(*(
323+
(uint32_t *)(cgraph->extra_edge_list
324+
+ extra_edge_list_pos * sizeof(uint32_t))))
325+
& 0x80000000u)
326+
== 0) {
327+
extra_edge_list_pos++;
328+
e->parent_count++;
329+
}
330+
331+
}
332+
git_oid_cpy(&e->sha1, &cgraph->oid_lookup[pos]);
333+
return 0;
334+
}
335+
336+
int git_commit_graph_entry_find(
337+
git_commit_graph_entry *e,
338+
const git_commit_graph_file *cgraph,
339+
const git_oid *short_oid,
340+
size_t len)
341+
{
342+
int pos, found = 0;
343+
uint32_t hi, lo;
344+
const git_oid *current = NULL;
345+
346+
GIT_ASSERT_ARG(e);
347+
GIT_ASSERT_ARG(cgraph);
348+
GIT_ASSERT_ARG(short_oid);
349+
350+
hi = ntohl(cgraph->oid_fanout[(int)short_oid->id[0]]);
351+
lo = ((short_oid->id[0] == 0x0) ? 0 : ntohl(cgraph->oid_fanout[(int)short_oid->id[0] - 1]));
352+
353+
pos = git_pack__lookup_sha1(cgraph->oid_lookup, GIT_OID_RAWSZ, lo, hi, short_oid->id);
354+
355+
if (pos >= 0) {
356+
/* An object matching exactly the oid was found */
357+
found = 1;
358+
current = cgraph->oid_lookup + pos;
359+
} else {
360+
/* No object was found */
361+
/* pos refers to the object with the "closest" oid to short_oid */
362+
pos = -1 - pos;
363+
if (pos < (int)cgraph->num_commits) {
364+
current = cgraph->oid_lookup + pos;
365+
366+
if (!git_oid_ncmp(short_oid, current, len))
367+
found = 1;
368+
}
369+
}
370+
371+
if (found && len != GIT_OID_HEXSZ && pos + 1 < (int)cgraph->num_commits) {
372+
/* Check for ambiguousity */
373+
const git_oid *next = current + 1;
374+
375+
if (!git_oid_ncmp(short_oid, next, len)) {
376+
found = 2;
377+
}
378+
}
379+
380+
if (!found)
381+
return git_odb__error_notfound(
382+
"failed to find offset for multi-pack index entry", short_oid, len);
383+
if (found > 1)
384+
return git_odb__error_ambiguous(
385+
"found multiple offsets for multi-pack index entry");
386+
387+
return git_commit_graph_entry_get_byindex(e, cgraph, pos);
388+
}
389+
390+
int git_commit_graph_entry_parent(
391+
git_commit_graph_entry *parent,
392+
const git_commit_graph_file *cgraph,
393+
const git_commit_graph_entry *entry,
394+
size_t n)
395+
{
396+
GIT_ASSERT_ARG(parent);
397+
GIT_ASSERT_ARG(cgraph);
398+
399+
if (n >= entry->parent_count) {
400+
git_error_set(GIT_ERROR_INVALID, "parent index %zu does not exist", n);
401+
return GIT_ENOTFOUND;
402+
}
403+
404+
if (n == 0 || (n == 1 && entry->parent_count == 2))
405+
return git_commit_graph_entry_get_byindex(parent, cgraph, entry->parent_indices[n]);
406+
407+
return git_commit_graph_entry_get_byindex(
408+
parent,
409+
cgraph,
410+
ntohl(
411+
*(uint32_t *)(cgraph->extra_edge_list
412+
+ (entry->extra_parents_index + n - 1)
413+
* sizeof(uint32_t)))
414+
& 0x7fffffff);
415+
}
416+
417+
281418
int git_commit_graph_close(git_commit_graph_file *cgraph)
282419
{
283420
GIT_ASSERT_ARG(cgraph);

src/commit_graph.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,47 @@ typedef struct git_commit_graph_file {
5757
git_buf filename;
5858
} git_commit_graph_file;
5959

60+
/**
61+
* An entry in the multi-pack-index file. Similar in purpose to git_pack_entry.
62+
*/
63+
typedef struct git_commit_graph_entry {
64+
/* The generation number of the commit within the graph */
65+
size_t generation;
66+
67+
/* Time in seconds from UNIX epoch. */
68+
git_time_t commit_time;
69+
70+
/* The number of parents of the commit. */
71+
size_t parent_count;
72+
73+
/*
74+
* The indices of the parent commits within the Commit Data table. The value
75+
* of `GIT_COMMIT_GRAPH_MISSING_PARENT` indicates that no parent is in that
76+
* position.
77+
*/
78+
size_t parent_indices[2];
79+
80+
/* The index within the Extra Edge List of any parent after the first two. */
81+
size_t extra_parents_index;
82+
83+
/* The SHA-1 hash of the root tree of the commit. */
84+
git_oid tree_oid;
85+
86+
/* The SHA-1 hash of the requested commit. */
87+
git_oid sha1;
88+
} git_commit_graph_entry;
89+
6090
int git_commit_graph_open(git_commit_graph_file **cgraph_out, const char *path);
91+
int git_commit_graph_entry_find(
92+
git_commit_graph_entry *e,
93+
const git_commit_graph_file *cgraph,
94+
const git_oid *short_oid,
95+
size_t len);
96+
int git_commit_graph_entry_parent(
97+
git_commit_graph_entry *parent,
98+
const git_commit_graph_file *cgraph,
99+
const git_commit_graph_entry *entry,
100+
size_t n);
61101
int git_commit_graph_close(git_commit_graph_file *cgraph);
62102
void git_commit_graph_free(git_commit_graph_file *cgraph);
63103

tests/graph/commit_graph.c

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,81 @@ void test_graph_commit_graph__parse(void)
88
{
99
git_repository *repo;
1010
struct git_commit_graph_file *cgraph;
11+
struct git_commit_graph_entry e, parent;
12+
git_oid id;
1113
git_buf commit_graph_path = GIT_BUF_INIT;
1214

1315
cl_git_pass(git_repository_open(&repo, cl_fixture("testrepo.git")));
1416
cl_git_pass(git_buf_joinpath(&commit_graph_path, git_repository_path(repo), "objects/info/commit-graph"));
1517
cl_git_pass(git_commit_graph_open(&cgraph, git_buf_cstr(&commit_graph_path)));
1618

19+
cl_git_pass(git_oid_fromstr(&id, "5001298e0c09ad9c34e4249bc5801c75e9754fa5"));
20+
cl_git_pass(git_commit_graph_entry_find(&e, cgraph, &id, GIT_OID_HEXSZ));
21+
cl_assert_equal_oid(&e.sha1, &id);
22+
cl_git_pass(git_oid_fromstr(&id, "418382dff1ffb8bdfba833f4d8bbcde58b1e7f47"));
23+
cl_assert_equal_oid(&e.tree_oid, &id);
24+
cl_assert_equal_i(e.generation, 1);
25+
cl_assert_equal_i(e.commit_time, 1273610423ull);
26+
cl_assert_equal_i(e.parent_count, 0);
27+
28+
cl_git_pass(git_oid_fromstr(&id, "be3563ae3f795b2b4353bcce3a527ad0a4f7f644"));
29+
cl_git_pass(git_commit_graph_entry_find(&e, cgraph, &id, GIT_OID_HEXSZ));
30+
cl_assert_equal_oid(&e.sha1, &id);
31+
cl_assert_equal_i(e.generation, 5);
32+
cl_assert_equal_i(e.commit_time, 1274813907ull);
33+
cl_assert_equal_i(e.parent_count, 2);
34+
35+
cl_git_pass(git_oid_fromstr(&id, "9fd738e8f7967c078dceed8190330fc8648ee56a"));
36+
cl_git_pass(git_commit_graph_entry_parent(&parent, cgraph, &e, 0));
37+
cl_assert_equal_oid(&parent.sha1, &id);
38+
cl_assert_equal_i(parent.generation, 4);
39+
40+
cl_git_pass(git_oid_fromstr(&id, "c47800c7266a2be04c571c04d5a6614691ea99bd"));
41+
cl_git_pass(git_commit_graph_entry_parent(&parent, cgraph, &e, 1));
42+
cl_assert_equal_oid(&parent.sha1, &id);
43+
cl_assert_equal_i(parent.generation, 3);
44+
45+
git_commit_graph_free(cgraph);
46+
git_repository_free(repo);
47+
git_buf_dispose(&commit_graph_path);
48+
}
49+
50+
void test_graph_commit_graph__parse_octopus_merge(void)
51+
{
52+
git_repository *repo;
53+
struct git_commit_graph_file *cgraph;
54+
struct git_commit_graph_entry e, parent;
55+
git_oid id;
56+
git_buf commit_graph_path = GIT_BUF_INIT;
57+
58+
cl_git_pass(git_repository_open(&repo, cl_fixture("merge-recursive/.gitted")));
59+
cl_git_pass(git_buf_joinpath(&commit_graph_path, git_repository_path(repo), "objects/info/commit-graph"));
60+
cl_git_pass(git_commit_graph_open(&cgraph, git_buf_cstr(&commit_graph_path)));
61+
62+
cl_git_pass(git_oid_fromstr(&id, "d71c24b3b113fd1d1909998c5bfe33b86a65ee03"));
63+
cl_git_pass(git_commit_graph_entry_find(&e, cgraph, &id, GIT_OID_HEXSZ));
64+
cl_assert_equal_oid(&e.sha1, &id);
65+
cl_git_pass(git_oid_fromstr(&id, "348f16ffaeb73f319a75cec5b16a0a47d2d5e27c"));
66+
cl_assert_equal_oid(&e.tree_oid, &id);
67+
cl_assert_equal_i(e.generation, 7);
68+
cl_assert_equal_i(e.commit_time, 1447083009ull);
69+
cl_assert_equal_i(e.parent_count, 3);
70+
71+
cl_git_pass(git_oid_fromstr(&id, "ad2ace9e15f66b3d1138922e6ffdc3ea3f967fa6"));
72+
cl_git_pass(git_commit_graph_entry_parent(&parent, cgraph, &e, 0));
73+
cl_assert_equal_oid(&parent.sha1, &id);
74+
cl_assert_equal_i(parent.generation, 6);
75+
76+
cl_git_pass(git_oid_fromstr(&id, "483065df53c0f4a02cdc6b2910b05d388fc17ffb"));
77+
cl_git_pass(git_commit_graph_entry_parent(&parent, cgraph, &e, 1));
78+
cl_assert_equal_oid(&parent.sha1, &id);
79+
cl_assert_equal_i(parent.generation, 2);
80+
81+
cl_git_pass(git_oid_fromstr(&id, "815b5a1c80ca749d705c7aa0cb294a00cbedd340"));
82+
cl_git_pass(git_commit_graph_entry_parent(&parent, cgraph, &e, 2));
83+
cl_assert_equal_oid(&parent.sha1, &id);
84+
cl_assert_equal_i(parent.generation, 6);
85+
1786
git_commit_graph_free(cgraph);
1887
git_repository_free(repo);
1988
git_buf_dispose(&commit_graph_path);
Binary file not shown.

0 commit comments

Comments
 (0)