git/t/t6600-test-reach.sh
Derrick Stolee 49abcd21da for-each-ref: add ahead-behind format atom
The previous change implemented the ahead_behind() method, including an
algorithm to compute the ahead/behind values for a number of commit tips
relative to a number of commit bases. Now, integrate that algorithm as
part of 'git for-each-ref' hidden behind a new format atom,
ahead-behind. This naturally extends to 'git branch' and 'git tag'
builtins, as well.

This format allows specifying multiple bases, if so desired, and all
matching references are compared against all of those bases. For this
reason, failing to read a reference provided from these atoms results in
an error.

In order to translate the ahead_behind() method information to the
format output code in ref-filter.c, we must populate arrays of
ahead_behind_count structs. In struct ref_array, we store the full array
that will be passed to ahead_behind(). In struct ref_array_item, we
store an array of pointers that point to the relvant items within the
full array. In this way, we can pull all relevant ahead/behind values
directly when formatting output for a specific item. It also ensures the
lifetime of the ahead_behind_count structs matches the time that the
array is being used.

Add specific tests of the ahead/behind counts in t6600-test-reach.sh, as
it has an interesting repository shape. In particular, its merging
strategy and its use of different commit-graphs would demonstrate over-
counting if the ahead_behind() method did not already account for that
possibility.

Also add tests for the specific for-each-ref, branch, and tag builtins.
In the case of 'git tag', there are intersting cases that happen when
some of the selected tips are not commits. This requires careful logic
around commits_nr in the second loop of filter_ahead_behind(). Also, the
test in t7004 is carefully located to avoid being dependent on the GPG
prereq. It also avoids using the test_commit helper, as that will add
ticks to the time and disrupt the expected timestamps in later tag
tests.

Also add performance tests in a new p1300-graph-walks.sh script. This
will be useful for more uses in the future, but for now compare the
ahead-behind counting algorithm in 'git for-each-ref' to the naive
implementation by running 'git rev-list --count' processes for each
input.

For the Git source code repository, the improvement is already obvious:

Test                                            this tree
---------------------------------------------------------------
1500.2: ahead-behind counts: git for-each-ref   0.07(0.07+0.00)
1500.3: ahead-behind counts: git branch         0.07(0.06+0.00)
1500.4: ahead-behind counts: git tag            0.07(0.06+0.00)
1500.5: ahead-behind counts: git rev-list       1.32(1.04+0.27)

But the standard performance benchmark is the Linux kernel repository,
which demosntrates a significant improvement:

Test                                            this tree
---------------------------------------------------------------
1500.2: ahead-behind counts: git for-each-ref   0.27(0.24+0.02)
1500.3: ahead-behind counts: git branch         0.27(0.24+0.03)
1500.4: ahead-behind counts: git tag            0.28(0.27+0.01)
1500.5: ahead-behind counts: git rev-list       4.57(4.03+0.54)

The 'git rev-list' test exists in this change as a demonstration, but it
will be removed in the next change to avoid wasting time on this
comparison.

Signed-off-by: Derrick Stolee <derrickstolee@github.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2023-03-20 12:17:33 -07:00

533 lines
12 KiB
Bash
Executable file

#!/bin/sh
test_description='basic commit reachability tests'
. ./test-lib.sh
# Construct a grid-like commit graph with points (x,y)
# with 1 <= x <= 10, 1 <= y <= 10, where (x,y) has
# parents (x-1, y) and (x, y-1), keeping in mind that
# we drop a parent if a coordinate is nonpositive.
#
# (10,10)
# / \
# (10,9) (9,10)
# / \ / \
# (10,8) (9,9) (8,10)
# / \ / \ / \
# ( continued...)
# \ / \ / \ /
# (3,1) (2,2) (1,3)
# \ / \ /
# (2,1) (2,1)
# \ /
# (1,1)
#
# We use branch 'commit-x-y' to refer to (x,y).
# This grid allows interesting reachability and
# non-reachability queries: (x,y) can reach (x',y')
# if and only if x' <= x and y' <= y.
test_expect_success 'setup' '
for i in $(test_seq 1 10)
do
test_commit "1-$i" &&
git branch -f commit-1-$i &&
git tag -a -m "1-$i" tag-1-$i commit-1-$i || return 1
done &&
for j in $(test_seq 1 9)
do
git reset --hard commit-$j-1 &&
x=$(($j + 1)) &&
test_commit "$x-1" &&
git branch -f commit-$x-1 &&
git tag -a -m "$x-1" tag-$x-1 commit-$x-1 &&
for i in $(test_seq 2 10)
do
git merge commit-$j-$i -m "$x-$i" &&
git branch -f commit-$x-$i &&
git tag -a -m "$x-$i" tag-$x-$i commit-$x-$i || return 1
done
done &&
git commit-graph write --reachable &&
mv .git/objects/info/commit-graph commit-graph-full &&
chmod u+w commit-graph-full &&
git show-ref -s commit-5-5 | git commit-graph write --stdin-commits &&
mv .git/objects/info/commit-graph commit-graph-half &&
chmod u+w commit-graph-half &&
git -c commitGraph.generationVersion=1 commit-graph write --reachable &&
mv .git/objects/info/commit-graph commit-graph-no-gdat &&
chmod u+w commit-graph-no-gdat &&
git config core.commitGraph true
'
run_all_modes () {
test_when_finished rm -rf .git/objects/info/commit-graph &&
"$@" <input >actual &&
test_cmp expect actual &&
cp commit-graph-full .git/objects/info/commit-graph &&
"$@" <input >actual &&
test_cmp expect actual &&
cp commit-graph-half .git/objects/info/commit-graph &&
"$@" <input >actual &&
test_cmp expect actual &&
cp commit-graph-no-gdat .git/objects/info/commit-graph &&
"$@" <input >actual &&
test_cmp expect actual
}
test_all_modes () {
run_all_modes test-tool reach "$@"
}
test_expect_success 'ref_newer:miss' '
cat >input <<-\EOF &&
A:commit-5-7
B:commit-4-9
EOF
echo "ref_newer(A,B):0" >expect &&
test_all_modes ref_newer
'
test_expect_success 'ref_newer:hit' '
cat >input <<-\EOF &&
A:commit-5-7
B:commit-2-3
EOF
echo "ref_newer(A,B):1" >expect &&
test_all_modes ref_newer
'
test_expect_success 'in_merge_bases:hit' '
cat >input <<-\EOF &&
A:commit-5-7
B:commit-8-8
EOF
echo "in_merge_bases(A,B):1" >expect &&
test_all_modes in_merge_bases
'
test_expect_success 'in_merge_bases:miss' '
cat >input <<-\EOF &&
A:commit-6-8
B:commit-5-9
EOF
echo "in_merge_bases(A,B):0" >expect &&
test_all_modes in_merge_bases
'
test_expect_success 'in_merge_bases_many:hit' '
cat >input <<-\EOF &&
A:commit-6-8
X:commit-6-9
X:commit-5-7
EOF
echo "in_merge_bases_many(A,X):1" >expect &&
test_all_modes in_merge_bases_many
'
test_expect_success 'in_merge_bases_many:miss' '
cat >input <<-\EOF &&
A:commit-6-8
X:commit-7-7
X:commit-8-6
EOF
echo "in_merge_bases_many(A,X):0" >expect &&
test_all_modes in_merge_bases_many
'
test_expect_success 'in_merge_bases_many:miss-heuristic' '
cat >input <<-\EOF &&
A:commit-6-8
X:commit-7-5
X:commit-6-6
EOF
echo "in_merge_bases_many(A,X):0" >expect &&
test_all_modes in_merge_bases_many
'
test_expect_success 'is_descendant_of:hit' '
cat >input <<-\EOF &&
A:commit-5-7
X:commit-4-8
X:commit-6-6
X:commit-1-1
EOF
echo "is_descendant_of(A,X):1" >expect &&
test_all_modes is_descendant_of
'
test_expect_success 'is_descendant_of:miss' '
cat >input <<-\EOF &&
A:commit-6-8
X:commit-5-9
X:commit-4-10
X:commit-7-6
EOF
echo "is_descendant_of(A,X):0" >expect &&
test_all_modes is_descendant_of
'
test_expect_success 'get_merge_bases_many' '
cat >input <<-\EOF &&
A:commit-5-7
X:commit-4-8
X:commit-6-6
X:commit-8-3
EOF
{
echo "get_merge_bases_many(A,X):" &&
git rev-parse commit-5-6 \
commit-4-7 | sort
} >expect &&
test_all_modes get_merge_bases_many
'
test_expect_success 'reduce_heads' '
cat >input <<-\EOF &&
X:commit-1-10
X:commit-2-8
X:commit-3-6
X:commit-4-4
X:commit-1-7
X:commit-2-5
X:commit-3-3
X:commit-5-1
EOF
{
echo "reduce_heads(X):" &&
git rev-parse commit-5-1 \
commit-4-4 \
commit-3-6 \
commit-2-8 \
commit-1-10 | sort
} >expect &&
test_all_modes reduce_heads
'
test_expect_success 'can_all_from_reach:hit' '
cat >input <<-\EOF &&
X:commit-2-10
X:commit-3-9
X:commit-4-8
X:commit-5-7
X:commit-6-6
X:commit-7-5
X:commit-8-4
X:commit-9-3
Y:commit-1-9
Y:commit-2-8
Y:commit-3-7
Y:commit-4-6
Y:commit-5-5
Y:commit-6-4
Y:commit-7-3
Y:commit-8-1
EOF
echo "can_all_from_reach(X,Y):1" >expect &&
test_all_modes can_all_from_reach
'
test_expect_success 'can_all_from_reach:miss' '
cat >input <<-\EOF &&
X:commit-2-10
X:commit-3-9
X:commit-4-8
X:commit-5-7
X:commit-6-6
X:commit-7-5
X:commit-8-4
X:commit-9-3
Y:commit-1-9
Y:commit-2-8
Y:commit-3-7
Y:commit-4-6
Y:commit-5-5
Y:commit-6-4
Y:commit-8-5
EOF
echo "can_all_from_reach(X,Y):0" >expect &&
test_all_modes can_all_from_reach
'
test_expect_success 'can_all_from_reach_with_flag: tags case' '
cat >input <<-\EOF &&
X:tag-2-10
X:tag-3-9
X:tag-4-8
X:commit-5-7
X:commit-6-6
X:commit-7-5
X:commit-8-4
X:commit-9-3
Y:tag-1-9
Y:tag-2-8
Y:tag-3-7
Y:commit-4-6
Y:commit-5-5
Y:commit-6-4
Y:commit-7-3
Y:commit-8-1
EOF
echo "can_all_from_reach_with_flag(X,_,_,0,0):1" >expect &&
test_all_modes can_all_from_reach_with_flag
'
test_expect_success 'commit_contains:hit' '
cat >input <<-\EOF &&
A:commit-7-7
X:commit-2-10
X:commit-3-9
X:commit-4-8
X:commit-5-7
X:commit-6-6
X:commit-7-5
X:commit-8-4
X:commit-9-3
EOF
echo "commit_contains(_,A,X,_):1" >expect &&
test_all_modes commit_contains &&
test_all_modes commit_contains --tag
'
test_expect_success 'commit_contains:miss' '
cat >input <<-\EOF &&
A:commit-6-5
X:commit-2-10
X:commit-3-9
X:commit-4-8
X:commit-5-7
X:commit-6-6
X:commit-7-5
X:commit-8-4
X:commit-9-3
EOF
echo "commit_contains(_,A,X,_):0" >expect &&
test_all_modes commit_contains &&
test_all_modes commit_contains --tag
'
test_expect_success 'rev-list: basic topo-order' '
git rev-parse \
commit-6-6 commit-5-6 commit-4-6 commit-3-6 commit-2-6 commit-1-6 \
commit-6-5 commit-5-5 commit-4-5 commit-3-5 commit-2-5 commit-1-5 \
commit-6-4 commit-5-4 commit-4-4 commit-3-4 commit-2-4 commit-1-4 \
commit-6-3 commit-5-3 commit-4-3 commit-3-3 commit-2-3 commit-1-3 \
commit-6-2 commit-5-2 commit-4-2 commit-3-2 commit-2-2 commit-1-2 \
commit-6-1 commit-5-1 commit-4-1 commit-3-1 commit-2-1 commit-1-1 \
>expect &&
run_all_modes git rev-list --topo-order commit-6-6
'
test_expect_success 'rev-list: first-parent topo-order' '
git rev-parse \
commit-6-6 \
commit-6-5 \
commit-6-4 \
commit-6-3 \
commit-6-2 \
commit-6-1 commit-5-1 commit-4-1 commit-3-1 commit-2-1 commit-1-1 \
>expect &&
run_all_modes git rev-list --first-parent --topo-order commit-6-6
'
test_expect_success 'rev-list: range topo-order' '
git rev-parse \
commit-6-6 commit-5-6 commit-4-6 commit-3-6 commit-2-6 commit-1-6 \
commit-6-5 commit-5-5 commit-4-5 commit-3-5 commit-2-5 commit-1-5 \
commit-6-4 commit-5-4 commit-4-4 commit-3-4 commit-2-4 commit-1-4 \
commit-6-3 commit-5-3 commit-4-3 \
commit-6-2 commit-5-2 commit-4-2 \
commit-6-1 commit-5-1 commit-4-1 \
>expect &&
run_all_modes git rev-list --topo-order commit-3-3..commit-6-6
'
test_expect_success 'rev-list: range topo-order' '
git rev-parse \
commit-6-6 commit-5-6 commit-4-6 \
commit-6-5 commit-5-5 commit-4-5 \
commit-6-4 commit-5-4 commit-4-4 \
commit-6-3 commit-5-3 commit-4-3 \
commit-6-2 commit-5-2 commit-4-2 \
commit-6-1 commit-5-1 commit-4-1 \
>expect &&
run_all_modes git rev-list --topo-order commit-3-8..commit-6-6
'
test_expect_success 'rev-list: first-parent range topo-order' '
git rev-parse \
commit-6-6 \
commit-6-5 \
commit-6-4 \
commit-6-3 \
commit-6-2 \
commit-6-1 commit-5-1 commit-4-1 \
>expect &&
run_all_modes git rev-list --first-parent --topo-order commit-3-8..commit-6-6
'
test_expect_success 'rev-list: ancestry-path topo-order' '
git rev-parse \
commit-6-6 commit-5-6 commit-4-6 commit-3-6 \
commit-6-5 commit-5-5 commit-4-5 commit-3-5 \
commit-6-4 commit-5-4 commit-4-4 commit-3-4 \
commit-6-3 commit-5-3 commit-4-3 \
>expect &&
run_all_modes git rev-list --topo-order --ancestry-path commit-3-3..commit-6-6
'
test_expect_success 'rev-list: symmetric difference topo-order' '
git rev-parse \
commit-6-6 commit-5-6 commit-4-6 \
commit-6-5 commit-5-5 commit-4-5 \
commit-6-4 commit-5-4 commit-4-4 \
commit-6-3 commit-5-3 commit-4-3 \
commit-6-2 commit-5-2 commit-4-2 \
commit-6-1 commit-5-1 commit-4-1 \
commit-3-8 commit-2-8 commit-1-8 \
commit-3-7 commit-2-7 commit-1-7 \
>expect &&
run_all_modes git rev-list --topo-order commit-3-8...commit-6-6
'
test_expect_success 'get_reachable_subset:all' '
cat >input <<-\EOF &&
X:commit-9-1
X:commit-8-3
X:commit-7-5
X:commit-6-6
X:commit-1-7
Y:commit-3-3
Y:commit-1-7
Y:commit-5-6
EOF
(
echo "get_reachable_subset(X,Y)" &&
git rev-parse commit-3-3 \
commit-1-7 \
commit-5-6 | sort
) >expect &&
test_all_modes get_reachable_subset
'
test_expect_success 'get_reachable_subset:some' '
cat >input <<-\EOF &&
X:commit-9-1
X:commit-8-3
X:commit-7-5
X:commit-1-7
Y:commit-3-3
Y:commit-1-7
Y:commit-5-6
EOF
(
echo "get_reachable_subset(X,Y)" &&
git rev-parse commit-3-3 \
commit-1-7 | sort
) >expect &&
test_all_modes get_reachable_subset
'
test_expect_success 'get_reachable_subset:none' '
cat >input <<-\EOF &&
X:commit-9-1
X:commit-8-3
X:commit-7-5
X:commit-1-7
Y:commit-9-3
Y:commit-7-6
Y:commit-2-8
EOF
echo "get_reachable_subset(X,Y)" >expect &&
test_all_modes get_reachable_subset
'
test_expect_success 'for-each-ref ahead-behind:linear' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-1-3
refs/heads/commit-1-5
refs/heads/commit-1-8
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1 0 8
refs/heads/commit-1-3 0 6
refs/heads/commit-1-5 0 4
refs/heads/commit-1-8 0 1
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-1-9)" --stdin
'
test_expect_success 'for-each-ref ahead-behind:all' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-2-4
refs/heads/commit-4-2
refs/heads/commit-4-4
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1 0 24
refs/heads/commit-2-4 0 17
refs/heads/commit-4-2 0 17
refs/heads/commit-4-4 0 9
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-5-5)" --stdin
'
test_expect_success 'for-each-ref ahead-behind:some' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-5-3
refs/heads/commit-4-8
refs/heads/commit-9-9
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1 0 53
refs/heads/commit-4-8 8 30
refs/heads/commit-5-3 0 39
refs/heads/commit-9-9 27 0
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-9-6)" --stdin
'
test_expect_success 'for-each-ref ahead-behind:some, multibase' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-5-3
refs/heads/commit-7-8
refs/heads/commit-4-8
refs/heads/commit-9-9
EOF
cat >expect <<-\EOF &&
refs/heads/commit-1-1 0 53 0 53
refs/heads/commit-4-8 8 30 0 22
refs/heads/commit-5-3 0 39 0 39
refs/heads/commit-7-8 14 12 8 6
refs/heads/commit-9-9 27 0 27 0
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-9-6) %(ahead-behind:commit-6-9)" \
--stdin
'
test_expect_success 'for-each-ref ahead-behind:none' '
cat >input <<-\EOF &&
refs/heads/commit-7-5
refs/heads/commit-4-8
refs/heads/commit-9-9
EOF
cat >expect <<-\EOF &&
refs/heads/commit-4-8 16 16
refs/heads/commit-7-5 7 4
refs/heads/commit-9-9 49 0
EOF
run_all_modes git for-each-ref \
--format="%(refname) %(ahead-behind:commit-8-4)" --stdin
'
test_done