Skip to content

Commit b0ba57d

Browse files
derrickstoleegitster
authored andcommitted
rev-list: use reduce_heads() for --maximal-only
The 'git rev-list --maximal-only' option filters the output to only independent commits. A commit is independent if it is not reachable from other listed commits. Currently this is implemented by doing a full revision walk and marking parents with CHILD_VISITED to skip non-maximal commits. The 'git merge-base --independent' command computes the same result using reduce_heads(), which uses the more efficient remove_redundant() algorithm. This is significantly faster because it avoids walking the entire commit graph. Add a fast path in rev-list that detects when --maximal-only is the only interesting option and all input commits are positive (no revision ranges). In this case, use reduce_heads() directly instead of doing a full revision walk. In order to preserve the rest of the output filtering, this computation is done opportunistically in a new prepare_maximal_independent() method when possible. If successful, it populates revs->commits with the list of independent commits and set revs->no_walk to prevent any other walk from occurring. This allows us to have any custom output be handled using the existing output code hidden inside traverse_commit_list_filtered(). A new test is added to demonstrate that this output is preserved. The fast path is only used when no other flags complicate the walk or output format: no UNINTERESTING commits, no limiting options (max-count, age filters, path filters, grep filters), no output formatting beyond plain OIDs, and no object listing flags. Running the p6011 performance test for my copy of git.git, I see the following improvement with this change: Test HEAD~1 HEAD ------------------------------------------------------------ 6011.2: merge-base --independent 0.03 0.03 +0.0% 6011.3: rev-list --maximal-only 0.06 0.03 -50.0% 6011.4: rev-list --maximal-only --since 0.06 0.06 +0.0% And for a fresh clone of the Linux kernel repository, I see: Test HEAD~1 HEAD ------------------------------------------------------------ 6011.2: merge-base --independent 0.00 0.00 = 6011.3: rev-list --maximal-only 0.70 0.00 -100.0% 6011.4: rev-list --maximal-only --since 0.70 0.70 +0.0% In both cases, the performance is indeed matching the behavior of 'git merge-base --independent', as expected. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent e8e5453 commit b0ba57d

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

builtin/rev-list.c

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "oidset.h"
2626
#include "oidmap.h"
2727
#include "packfile.h"
28+
#include "commit-reach.h"
2829
#include "quote.h"
2930
#include "strbuf.h"
3031

@@ -633,6 +634,61 @@ static int try_bitmap_disk_usage(struct rev_info *revs,
633634
return 0;
634635
}
635636

637+
/*
638+
* If revs->maximal_only is set and no other walk modifiers are provided,
639+
* run a faster computation to filter the independent commits and prepare
640+
* them for output. Set revs->no_walk to prevent later walking.
641+
*
642+
* If this algorithm doesn't apply, then no changes are made to revs.
643+
*/
644+
static void prepare_maximal_independent(struct rev_info *revs)
645+
{
646+
struct commit_list *c;
647+
648+
if (!revs->maximal_only)
649+
return;
650+
651+
for (c = revs->commits; c; c = c->next) {
652+
if (c->item->object.flags & UNINTERESTING)
653+
return;
654+
}
655+
656+
if (revs->limited ||
657+
revs->topo_order ||
658+
revs->first_parent_only ||
659+
revs->reverse ||
660+
revs->max_count >= 0 ||
661+
revs->skip_count >= 0 ||
662+
revs->min_age != (timestamp_t)-1 ||
663+
revs->max_age != (timestamp_t)-1 ||
664+
revs->min_parents > 0 ||
665+
revs->max_parents >= 0 ||
666+
revs->prune_data.nr ||
667+
revs->count ||
668+
revs->left_right ||
669+
revs->boundary ||
670+
revs->tag_objects ||
671+
revs->tree_objects ||
672+
revs->blob_objects ||
673+
revs->filter.choice ||
674+
revs->reflog_info ||
675+
revs->diff ||
676+
revs->grep_filter.pattern_list ||
677+
revs->grep_filter.header_list ||
678+
revs->verbose_header ||
679+
revs->print_parents ||
680+
revs->edge_hint ||
681+
revs->unpacked ||
682+
revs->no_kept_objects ||
683+
revs->line_level_traverse)
684+
return;
685+
686+
reduce_heads_replace(&revs->commits);
687+
688+
/* Modify 'revs' to only output this commit list. */
689+
revs->no_walk = 1;
690+
}
691+
636692
int cmd_rev_list(int argc,
637693
const char **argv,
638694
const char *prefix,
@@ -875,6 +931,9 @@ int cmd_rev_list(int argc,
875931

876932
if (prepare_revision_walk(&revs))
877933
die("revision walk setup failed");
934+
935+
prepare_maximal_independent(&revs);
936+
878937
if (revs.tree_objects)
879938
mark_edges_uninteresting(&revs, show_edge, 0);
880939

t/t6000-rev-list-misc.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,35 @@ test_expect_success 'rev-list --boundary incompatible with --maximal-only' '
263263
test_grep "cannot be used together" err
264264
'
265265

266+
test_expect_success 'rev-list --maximal-only and --pretty' '
267+
test_when_finished rm -rf repo &&
268+
269+
git init repo &&
270+
test_commit -C repo 1 &&
271+
oid1=$(git -C repo rev-parse HEAD) &&
272+
test_commit -C repo 2 &&
273+
oid2=$(git -C repo rev-parse HEAD) &&
274+
git -C repo checkout --detach HEAD~1 &&
275+
test_commit -C repo 3 &&
276+
oid3=$(git -C repo rev-parse HEAD) &&
277+
278+
cat >expect <<-EOF &&
279+
commit $oid3
280+
$oid3
281+
commit $oid2
282+
$oid2
283+
EOF
284+
285+
git -C repo rev-list --pretty="%H" --maximal-only $oid1 $oid2 $oid3 >out &&
286+
test_cmp expect out &&
287+
288+
cat >expect <<-EOF &&
289+
$oid3
290+
$oid2
291+
EOF
292+
293+
git -C repo log --pretty="%H" --maximal-only $oid1 $oid2 $oid3 >out &&
294+
test_cmp expect out
295+
'
296+
266297
test_done

0 commit comments

Comments
 (0)