Skip to content

Commit 2de5486

Browse files
committed
graph: Create git_graph_reachable_from_any()
This change introduces a new API function `git_graph_reachable_from_any()`, that answers the question whether a commit is reachable from any of the provided commits through following parent edges. This function can take advantage of optimizations provided by the existence of a `commit-graph` file, since it makes it faster to know whether, given two commits X and Y, X cannot possibly be an reachable from Y. Part of: libgit2#5757
1 parent 29d2d95 commit 2de5486

File tree

5 files changed

+179
-20
lines changed

5 files changed

+179
-20
lines changed

include/git2/graph.h

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ GIT_EXTERN(int) git_graph_ahead_behind(size_t *ahead, size_t *behind, git_reposi
4343
* Note that a commit is not considered a descendant of itself, in contrast
4444
* to `git merge-base --is-ancestor`.
4545
*
46-
* @param commit a previously loaded commit.
47-
* @param ancestor a potential ancestor commit.
46+
* @param repo the repository where the commits exist
47+
* @param commit a previously loaded commit
48+
* @param ancestor a potential ancestor commit
4849
* @return 1 if the given commit is a descendant of the potential ancestor,
4950
* 0 if not, error code otherwise.
5051
*/
@@ -53,6 +54,23 @@ GIT_EXTERN(int) git_graph_descendant_of(
5354
const git_oid *commit,
5455
const git_oid *ancestor);
5556

57+
/**
58+
* Determine if a commit is reachable from any of a list of commits by
59+
* following parent edges.
60+
*
61+
* @param repo the repository where the commits exist
62+
* @param commit a previously loaded commit
63+
* @param length the number of commits in the provided `descendant_array`
64+
* @param descendant_array oids of the commits
65+
* @return 1 if the given commit is an ancestor of any of the given potential
66+
* descendants, 0 if not, error code otherwise.
67+
*/
68+
GIT_EXTERN(int) git_graph_reachable_from_any(
69+
git_repository *repo,
70+
const git_oid *commit,
71+
size_t length,
72+
const git_oid descendant_array[]);
73+
5674
/** @} */
5775
GIT_END_DECL
5876
#endif

src/graph.c

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,19 +176,74 @@ int git_graph_ahead_behind(size_t *ahead, size_t *behind, git_repository *repo,
176176

177177
int git_graph_descendant_of(git_repository *repo, const git_oid *commit, const git_oid *ancestor)
178178
{
179-
git_oid merge_base;
180-
int error;
181-
182179
if (git_oid_equal(commit, ancestor))
183180
return 0;
184181

185-
error = git_merge_base(&merge_base, repo, commit, ancestor);
186-
/* No merge-base found, it's not a descendant */
187-
if (error == GIT_ENOTFOUND)
182+
return git_graph_reachable_from_any(repo, ancestor, 1, commit);
183+
}
184+
185+
int git_graph_reachable_from_any(
186+
git_repository *repo,
187+
const git_oid *commit_id,
188+
size_t length,
189+
const git_oid descendant_array[])
190+
{
191+
git_revwalk *walk = NULL;
192+
git_vector list;
193+
git_commit_list *result = NULL;
194+
git_commit_list_node *commit;
195+
size_t i;
196+
uint32_t minimum_generation = 0xffffffff;
197+
int error = 0;
198+
199+
if (!length)
188200
return 0;
189201

190-
if (error < 0)
202+
for (i = 0; i < length; ++i) {
203+
if (git_oid_equal(commit_id, &descendant_array[i]))
204+
return 1;
205+
}
206+
207+
if ((error = git_vector_init(&list, length + 1, NULL)) < 0)
191208
return error;
192209

193-
return git_oid_equal(&merge_base, ancestor);
210+
if ((error = git_revwalk_new(&walk, repo)) < 0)
211+
goto done;
212+
213+
for (i = 0; i < length; i++) {
214+
commit = git_revwalk__commit_lookup(walk, &descendant_array[i]);
215+
if (commit == NULL) {
216+
error = -1;
217+
goto done;
218+
}
219+
220+
git_vector_insert(&list, commit);
221+
if (minimum_generation > commit->generation)
222+
minimum_generation = commit->generation;
223+
}
224+
225+
commit = git_revwalk__commit_lookup(walk, commit_id);
226+
if (commit == NULL) {
227+
error = -1;
228+
goto done;
229+
}
230+
231+
if (minimum_generation > commit->generation)
232+
minimum_generation = commit->generation;
233+
234+
if ((error = git_merge__bases_many(&result, walk, commit, &list, minimum_generation)) < 0)
235+
goto done;
236+
237+
if (result) {
238+
error = git_oid_equal(commit_id, &result->item->oid);
239+
} else {
240+
/* No merge-base found, it's not a descendant */
241+
error = 0;
242+
}
243+
244+
done:
245+
git_commit_list_free(&result);
246+
git_vector_free(&list);
247+
git_revwalk_free(walk);
248+
return error;
194249
}

src/merge.c

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ static int merge_bases_many(git_commit_list **out, git_revwalk **walk_out, git_r
112112
if (commit == NULL)
113113
goto on_error;
114114

115-
if (git_merge__bases_many(&result, walk, commit, &list) < 0)
115+
if (git_merge__bases_many(&result, walk, commit, &list, 0) < 0)
116116
goto on_error;
117117

118118
if (!result) {
@@ -243,7 +243,7 @@ static int merge_bases(git_commit_list **out, git_revwalk **walk_out, git_reposi
243243
if (commit == NULL)
244244
goto on_error;
245245

246-
if (git_merge__bases_many(&result, walk, commit, &list) < 0)
246+
if (git_merge__bases_many(&result, walk, commit, &list, 0) < 0)
247247
goto on_error;
248248

249249
if (!result) {
@@ -378,7 +378,11 @@ static int clear_commit_marks(git_commit_list_node *commit, unsigned int mark)
378378
}
379379

380380
static int paint_down_to_common(
381-
git_commit_list **out, git_revwalk *walk, git_commit_list_node *one, git_vector *twos)
381+
git_commit_list **out,
382+
git_revwalk *walk,
383+
git_commit_list_node *one,
384+
git_vector *twos,
385+
uint32_t minimum_generation)
382386
{
383387
git_pqueue list;
384388
git_commit_list *result = NULL;
@@ -399,7 +403,6 @@ static int paint_down_to_common(
399403
return -1;
400404

401405
two->flags |= PARENT2;
402-
403406
if (git_pqueue_insert(&list, two) < 0)
404407
return -1;
405408
}
@@ -427,6 +430,8 @@ static int paint_down_to_common(
427430
git_commit_list_node *p = commit->parents[i];
428431
if ((p->flags & flags) == flags)
429432
continue;
433+
if (p->generation < minimum_generation)
434+
continue;
430435

431436
if ((error = git_commit_list_parse(walk, p)) < 0)
432437
return error;
@@ -442,7 +447,7 @@ static int paint_down_to_common(
442447
return 0;
443448
}
444449

445-
static int remove_redundant(git_revwalk *walk, git_vector *commits)
450+
static int remove_redundant(git_revwalk *walk, git_vector *commits, uint32_t minimum_generation)
446451
{
447452
git_vector work = GIT_VECTOR_INIT;
448453
unsigned char *redundant;
@@ -478,7 +483,7 @@ static int remove_redundant(git_revwalk *walk, git_vector *commits)
478483
goto done;
479484
}
480485

481-
error = paint_down_to_common(&common, walk, commit, &work);
486+
error = paint_down_to_common(&common, walk, commit, &work, minimum_generation);
482487
if (error < 0)
483488
goto done;
484489

@@ -510,7 +515,12 @@ static int remove_redundant(git_revwalk *walk, git_vector *commits)
510515
return error;
511516
}
512517

513-
int git_merge__bases_many(git_commit_list **out, git_revwalk *walk, git_commit_list_node *one, git_vector *twos)
518+
int git_merge__bases_many(
519+
git_commit_list **out,
520+
git_revwalk *walk,
521+
git_commit_list_node *one,
522+
git_vector *twos,
523+
uint32_t minimum_generation)
514524
{
515525
int error;
516526
unsigned int i;
@@ -532,7 +542,7 @@ int git_merge__bases_many(git_commit_list **out, git_revwalk *walk, git_commit_l
532542
if (git_commit_list_parse(walk, one) < 0)
533543
return -1;
534544

535-
error = paint_down_to_common(&result, walk, one, twos);
545+
error = paint_down_to_common(&result, walk, one, twos, minimum_generation);
536546
if (error < 0)
537547
return error;
538548

@@ -559,7 +569,7 @@ int git_merge__bases_many(git_commit_list **out, git_revwalk *walk, git_commit_l
559569

560570
if ((error = clear_commit_marks(one, ALL_FLAGS)) < 0 ||
561571
(error = clear_commit_marks_many(twos, ALL_FLAGS)) < 0 ||
562-
(error = remove_redundant(walk, &redundant)) < 0) {
572+
(error = remove_redundant(walk, &redundant, minimum_generation)) < 0) {
563573
git_vector_free(&redundant);
564574
return error;
565575
}

src/merge.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ int git_merge__bases_many(
129129
git_commit_list **out,
130130
git_revwalk *walk,
131131
git_commit_list_node *one,
132-
git_vector *twos);
132+
git_vector *twos,
133+
uint32_t minimum_generation);
133134

134135
/*
135136
* Three-way tree differencing

tests/graph/reachable_from_any.c

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#include "clar_libgit2.h"
2+
3+
#include <git2.h>
4+
5+
#include "commit_graph.h"
6+
7+
static git_repository *repo;
8+
9+
#define TEST_REPO_PATH "merge-recursive"
10+
11+
void test_graph_reachable_from_any__initialize(void)
12+
{
13+
git_oid oid;
14+
git_commit *commit;
15+
16+
repo = cl_git_sandbox_init(TEST_REPO_PATH);
17+
18+
git_oid_fromstr(&oid, "539bd011c4822c560c1d17cab095006b7a10f707");
19+
cl_git_pass(git_commit_lookup(&commit, repo, &oid));
20+
cl_git_pass(git_reset(repo, (git_object *)commit, GIT_RESET_HARD, NULL));
21+
git_commit_free(commit);
22+
}
23+
24+
void test_graph_reachable_from_any__cleanup(void)
25+
{
26+
cl_git_sandbox_cleanup();
27+
}
28+
29+
void test_graph_reachable_from_any__returns_correct_result(void)
30+
{
31+
git_object *branchA1, *branchA2, *branchB1, *branchB2, *branchC1, *branchC2, *branchH1,
32+
*branchH2;
33+
git_oid descendants[7];
34+
35+
cl_git_pass(git_revparse_single(&branchA1, repo, "branchA-1"));
36+
cl_git_pass(git_revparse_single(&branchA2, repo, "branchA-2"));
37+
cl_git_pass(git_revparse_single(&branchB1, repo, "branchB-1"));
38+
cl_git_pass(git_revparse_single(&branchB2, repo, "branchB-2"));
39+
cl_git_pass(git_revparse_single(&branchC1, repo, "branchC-1"));
40+
cl_git_pass(git_revparse_single(&branchC2, repo, "branchC-2"));
41+
cl_git_pass(git_revparse_single(&branchH1, repo, "branchH-1"));
42+
cl_git_pass(git_revparse_single(&branchH2, repo, "branchH-2"));
43+
44+
cl_assert_equal_i(
45+
git_graph_reachable_from_any(
46+
repo, git_object_id(branchH1), 1, git_object_id(branchA1)),
47+
0);
48+
cl_assert_equal_i(
49+
git_graph_reachable_from_any(
50+
repo, git_object_id(branchH1), 1, git_object_id(branchA2)),
51+
0);
52+
53+
cl_git_pass(git_oid_cpy(&descendants[0], git_object_id(branchA1)));
54+
cl_git_pass(git_oid_cpy(&descendants[1], git_object_id(branchA2)));
55+
cl_git_pass(git_oid_cpy(&descendants[2], git_object_id(branchB1)));
56+
cl_git_pass(git_oid_cpy(&descendants[3], git_object_id(branchB2)));
57+
cl_git_pass(git_oid_cpy(&descendants[4], git_object_id(branchC1)));
58+
cl_git_pass(git_oid_cpy(&descendants[5], git_object_id(branchC2)));
59+
cl_git_pass(git_oid_cpy(&descendants[6], git_object_id(branchH2)));
60+
cl_assert_equal_i(
61+
git_graph_reachable_from_any(repo, git_object_id(branchH2), 6, descendants),
62+
0);
63+
cl_assert_equal_i(
64+
git_graph_reachable_from_any(repo, git_object_id(branchH2), 7, descendants),
65+
1);
66+
67+
git_object_free(branchA1);
68+
git_object_free(branchA2);
69+
git_object_free(branchB1);
70+
git_object_free(branchB2);
71+
git_object_free(branchC1);
72+
git_object_free(branchC2);
73+
git_object_free(branchH1);
74+
git_object_free(branchH2);
75+
}

0 commit comments

Comments
 (0)