Skip to content

Commit 2930a21

Browse files
committed
Merge branch 'td/ref-filter-memoize-contains' into seen
* td/ref-filter-memoize-contains: : 'git branch --contains' and 'git for-each-ref --contains' have been : optimized to use the memoized commit traversal previously used only by : 'git tag --contains', significantly speeding up connectivity checks : across many candidate refs with shared history. ref-filter: memoize --contains with generations commit-reach: handle cycles in contains walk
2 parents 592adfc + 8d8ff42 commit 2930a21

5 files changed

Lines changed: 137 additions & 8 deletions

File tree

commit-reach.c

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,8 @@ static int in_commit_list(const struct commit_list *want, struct commit *c)
757757

758758
/*
759759
* Test whether the candidate is contained in the list.
760-
* Do not recurse to find out, though, but return -1 if inconclusive.
760+
* Do not recurse to find out, though, but return CONTAINS_UNKNOWN if
761+
* inconclusive.
761762
*/
762763
static enum contains_result contains_test(struct commit *candidate,
763764
const struct commit_list *want,
@@ -793,7 +794,7 @@ static void push_to_contains_stack(struct commit *candidate, struct contains_sta
793794
}
794795

795796
static enum contains_result contains_tag_algo(struct commit *candidate,
796-
const struct commit_list *want,
797+
struct commit_list *want,
797798
struct contains_cache *cache)
798799
{
799800
struct contains_stack contains_stack = { 0, 0, NULL };
@@ -814,6 +815,7 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
814815
if (result != CONTAINS_UNKNOWN)
815816
return result;
816817

818+
*contains_cache_at(cache, candidate) = CONTAINS_IN_PROGRESS;
817819
push_to_contains_stack(candidate, &contains_stack);
818820
while (contains_stack.nr) {
819821
struct contains_stack_entry *entry = &contains_stack.contains_stack[contains_stack.nr - 1];
@@ -825,8 +827,8 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
825827
contains_stack.nr--;
826828
}
827829
/*
828-
* If we just popped the stack, parents->item has been marked,
829-
* therefore contains_test will return a meaningful yes/no.
830+
* A parent may have just been popped and marked, or may still
831+
* be active when replacement refs create a cycle.
830832
*/
831833
else switch (contains_test(parents->item, want, cache, cutoff)) {
832834
case CONTAINS_YES:
@@ -836,21 +838,50 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
836838
case CONTAINS_NO:
837839
entry->parents = parents->next;
838840
break;
841+
case CONTAINS_IN_PROGRESS:
842+
/*
843+
* Partial negative answers are not safe across a cycle.
844+
* Discard them and use the cycle-safe reachability walk.
845+
*/
846+
goto cycle;
839847
case CONTAINS_UNKNOWN:
848+
*contains_cache_at(cache, parents->item) =
849+
CONTAINS_IN_PROGRESS;
840850
push_to_contains_stack(parents->item, &contains_stack);
841851
break;
842852
}
843853
}
844854
free(contains_stack.contains_stack);
845855
return contains_test(candidate, want, cache, cutoff);
856+
857+
cycle:
858+
free(contains_stack.contains_stack);
859+
clear_contains_cache(cache);
860+
init_contains_cache(cache);
861+
862+
result = repo_is_descendant_of(the_repository, candidate, want);
863+
if (result < 0)
864+
exit(128);
865+
*contains_cache_at(cache, candidate) =
866+
result ? CONTAINS_YES : CONTAINS_NO;
867+
return result ? CONTAINS_YES : CONTAINS_NO;
846868
}
847869

848870
int commit_contains(struct ref_filter *filter, struct commit *commit,
849871
struct commit_list *list, struct contains_cache *cache)
850872
{
851-
if (filter->with_commit_tag_algo)
873+
int result;
874+
875+
if (!list)
876+
return 1;
877+
if (filter->with_commit_tag_algo ||
878+
generation_numbers_enabled(the_repository))
852879
return contains_tag_algo(commit, list, cache) == CONTAINS_YES;
853-
return repo_is_descendant_of(the_repository, commit, list);
880+
881+
result = repo_is_descendant_of(the_repository, commit, list);
882+
if (result < 0)
883+
exit(128);
884+
return result;
854885
}
855886

856887
int can_all_from_reach_with_flag(struct object_array *from,

commit-reach.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,19 @@ int ref_newer(const struct object_id *new_oid, const struct object_id *old_oid);
7373
enum contains_result {
7474
CONTAINS_UNKNOWN = 0,
7575
CONTAINS_NO,
76-
CONTAINS_YES
76+
CONTAINS_YES,
77+
CONTAINS_IN_PROGRESS
7778
};
7879

7980
define_commit_slab(contains_cache, enum contains_result);
8081

82+
/*
83+
* Return whether "commit" is a descendant of any commit in "list". An empty
84+
* list matches.
85+
*
86+
* The memoized traversal records answers in "cache" for one fixed "list".
87+
* Clear it before changing the list.
88+
*/
8189
int commit_contains(struct ref_filter *filter, struct commit *commit,
8290
struct commit_list *list, struct contains_cache *cache);
8391

t/perf/p1500-graph-walks.sh

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,47 @@ test_expect_success 'setup' '
3232
echo "X:$line" >>test-tool-tags || return 1
3333
done &&
3434
35-
commit=$(git commit-tree $(git rev-parse HEAD^{tree})) &&
35+
git rev-list --first-parent --max-count=8192 HEAD >contains-commits &&
36+
test_file_not_empty contains-commits &&
37+
git update-ref refs/contains-perf-base "$(tail -n 1 contains-commits)" &&
38+
awk "{
39+
printf \"update refs/contains-perf/%04d %s\\n\", NR, \$1
40+
}" contains-commits |
41+
git update-ref --stdin &&
42+
git pack-refs --include "refs/contains-perf/*" &&
43+
44+
tree=$(git rev-parse HEAD^{tree}) &&
45+
base=$(git rev-parse HEAD) &&
46+
target=$(echo target | git commit-tree "$tree" -p "$base") &&
47+
git update-ref refs/contains-diverged/target "$target" &&
48+
for i in $(test_seq 1 4)
49+
do
50+
commit=$(echo candidate-$i |
51+
git commit-tree "$tree" -p "$base") &&
52+
git update-ref refs/contains-diverged/candidate-$i "$commit" ||
53+
return 1
54+
done &&
55+
56+
commit=$(git commit-tree "$tree") &&
3657
git update-ref refs/heads/disjoint-base $commit &&
3758
3859
git commit-graph write --reachable
3960
'
4061

62+
test_expect_success 'verify contains results' '
63+
git for-each-ref --contains=refs/contains-perf-base \
64+
refs/contains-perf/ >actual &&
65+
test_line_count = $(wc -l <contains-commits) actual &&
66+
67+
echo refs/contains-diverged/target >expect &&
68+
GIT_TEST_COMMIT_GRAPH=0 \
69+
git -c core.commitGraph=false for-each-ref \
70+
--format="%(refname)" \
71+
--contains=refs/contains-diverged/target \
72+
refs/contains-diverged/ >actual &&
73+
test_cmp expect actual
74+
'
75+
4176
test_perf 'ahead-behind counts: git for-each-ref' '
4277
git for-each-ref --format="%(ahead-behind:HEAD)" --stdin <refs
4378
'
@@ -62,6 +97,18 @@ test_perf 'contains: git tag --merged' '
6297
xargs git tag --merged=HEAD <tags
6398
'
6499

100+
test_perf 'contains: git for-each-ref --contains' '
101+
git for-each-ref --contains=refs/contains-perf-base \
102+
refs/contains-perf/ >/dev/null
103+
'
104+
105+
test_perf 'contains without generations: divergent refs' '
106+
GIT_TEST_COMMIT_GRAPH=0 \
107+
git -c core.commitGraph=false for-each-ref \
108+
--contains=refs/contains-diverged/target \
109+
refs/contains-diverged/ >/dev/null
110+
'
111+
65112
test_perf 'is-base check: test-tool reach (refs)' '
66113
test-tool reach get_branch_base_for_tip <test-tool-refs
67114
'

t/t6301-for-each-ref-errors.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,28 @@ test_expect_success 'Missing objects are reported correctly' '
5252
test_must_be_empty brief-err
5353
'
5454

55+
test_expect_success 'missing ancestors are reported by contains filters' '
56+
test_when_finished "git update-ref -d refs/heads/missing-parent" &&
57+
{
58+
echo "tree $(git rev-parse HEAD^{tree})" &&
59+
echo "parent $MISSING" &&
60+
git cat-file commit HEAD |
61+
sed -n -e "/^author /p" -e "/^committer /p" &&
62+
echo &&
63+
echo "missing parent"
64+
} >commit &&
65+
broken=$(git hash-object -t commit -w commit) &&
66+
git update-ref refs/heads/missing-parent "$broken" &&
67+
for option in --contains --no-contains
68+
do
69+
test_must_fail git for-each-ref "$option=HEAD" \
70+
refs/heads/missing-parent >out 2>err &&
71+
test_must_be_empty out &&
72+
test_grep "parse commit $MISSING" err ||
73+
return 1
74+
done
75+
'
76+
5577
test_expect_success 'ahead-behind requires an argument' '
5678
test_must_fail git for-each-ref \
5779
--format="%(ahead-behind)" 2>err &&

t/t7004-tag.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,6 +1611,27 @@ test_expect_success 'checking that first commit is in all tags (hash)' '
16111611
test_cmp expected actual
16121612
'
16131613

1614+
test_expect_success 'tag --contains handles cyclic replacement histories' '
1615+
first=$(git rev-parse HEAD~2) &&
1616+
second=$(git rev-parse HEAD~) &&
1617+
third=$(git rev-parse HEAD) &&
1618+
test_when_finished "
1619+
git replace -d $first
1620+
git replace -d $third
1621+
git tag -d cycle-a cycle-b
1622+
" &&
1623+
git tag cycle-a "$first" &&
1624+
git tag cycle-b "$third" &&
1625+
git replace --graft "$first" "$third" "$second" &&
1626+
git replace --graft "$third" "$first" &&
1627+
cat >expected <<-\EOF &&
1628+
cycle-a
1629+
cycle-b
1630+
EOF
1631+
git tag --contains="$second" --list "cycle-*" >actual &&
1632+
test_cmp expected actual
1633+
'
1634+
16141635
# other ways of specifying the commit
16151636
test_expect_success 'checking that first commit is in all tags (tag)' '
16161637
cat >expected <<-\EOF &&

0 commit comments

Comments
 (0)