Merge branch 'ds/rev-list-maximal-only-optim' into next

"git rev-list --maximal-only" has been optimized by borrowing the
logic used by "git show-branch --independent", which computes the
same kind of information much more efficiently.

* ds/rev-list-maximal-only-optim:
  rev-list: use reduce_heads() for --maximal-only
  p6011: add perf test for rev-list --maximal-only
  t6600: test --maximal-only and --independent
next
Junio C Hamano 2026-04-07 11:10:41 -07:00
commit 7a70817efa
4 changed files with 164 additions and 0 deletions

View File

@ -25,6 +25,7 @@
#include "oidset.h"
#include "oidmap.h"
#include "packfile.h"
#include "commit-reach.h"
#include "quote.h"
#include "strbuf.h"

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

/*
* If revs->maximal_only is set and no other walk modifiers are provided,
* run a faster computation to filter the independent commits and prepare
* them for output. Set revs->no_walk to prevent later walking.
*
* If this algorithm doesn't apply, then no changes are made to revs.
*/
static void prepare_maximal_independent(struct rev_info *revs)
{
struct commit_list *c;

if (!revs->maximal_only)
return;

for (c = revs->commits; c; c = c->next) {
if (c->item->object.flags & UNINTERESTING)
return;
}

if (revs->limited ||
revs->topo_order ||
revs->first_parent_only ||
revs->reverse ||
revs->max_count >= 0 ||
revs->skip_count >= 0 ||
revs->min_age != (timestamp_t)-1 ||
revs->max_age != (timestamp_t)-1 ||
revs->min_parents > 0 ||
revs->max_parents >= 0 ||
revs->prune_data.nr ||
revs->count ||
revs->left_right ||
revs->boundary ||
revs->tag_objects ||
revs->tree_objects ||
revs->blob_objects ||
revs->filter.choice ||
revs->reflog_info ||
revs->diff ||
revs->grep_filter.pattern_list ||
revs->grep_filter.header_list ||
revs->verbose_header ||
revs->print_parents ||
revs->edge_hint ||
revs->unpacked ||
revs->no_kept_objects ||
revs->line_level_traverse)
return;

reduce_heads_replace(&revs->commits);

/* Modify 'revs' to only output this commit list. */
revs->no_walk = 1;
}

int cmd_rev_list(int argc,
const char **argv,
const char *prefix,
@ -875,6 +931,9 @@ int cmd_rev_list(int argc,

if (prepare_revision_walk(&revs))
die("revision walk setup failed");

prepare_maximal_independent(&revs);

if (revs.tree_objects)
mark_edges_uninteresting(&revs, show_edge, 0);


View File

@ -0,0 +1,29 @@
#!/bin/sh

test_description='Test --maximal-only and --independent options'

. ./perf-lib.sh

test_perf_default_repo

test_expect_success 'setup' '
git for-each-ref --format="%(*objecttype) %(objecttype) %(objectname)" \
"refs/heads/*" "refs/tags/*" |
sed -n -e "s/^commit commit //p" -e "s/^ commit //p" |
head -n 50 >commits &&
git commit-graph write --reachable
'

test_perf 'merge-base --independent' '
git merge-base --independent $(cat commits) >/dev/null
'

test_perf 'rev-list --maximal-only' '
git rev-list --maximal-only $(cat commits) >/dev/null
'

test_perf 'rev-list --maximal-only --since' '
git rev-list --maximal-only --since=2000-01-01 $(cat commits) >/dev/null
'

test_done

View File

@ -263,4 +263,35 @@ test_expect_success 'rev-list --boundary incompatible with --maximal-only' '
test_grep "cannot be used together" err
'

test_expect_success 'rev-list --maximal-only and --pretty' '
test_when_finished rm -rf repo &&

git init repo &&
test_commit -C repo 1 &&
oid1=$(git -C repo rev-parse HEAD) &&
test_commit -C repo 2 &&
oid2=$(git -C repo rev-parse HEAD) &&
git -C repo checkout --detach HEAD~1 &&
test_commit -C repo 3 &&
oid3=$(git -C repo rev-parse HEAD) &&

cat >expect <<-EOF &&
commit $oid3
$oid3
commit $oid2
$oid2
EOF

git -C repo rev-list --pretty="%H" --maximal-only $oid1 $oid2 $oid3 >out &&
test_cmp expect out &&

cat >expect <<-EOF &&
$oid3
$oid2
EOF

git -C repo log --pretty="%H" --maximal-only $oid1 $oid2 $oid3 >out &&
test_cmp expect out
'

test_done

View File

@ -837,4 +837,49 @@ test_expect_success 'rev-list --maximal-only (range)' '
--first-parent --exclude-first-parent-only
'

test_expect_success 'rev-list --maximal-only matches merge-base --independent' '
# Mix of independent and dependent
git merge-base --independent \
refs/heads/commit-5-2 \
refs/heads/commit-3-2 \
refs/heads/commit-2-5 >expect &&
sort expect >expect.sorted &&
git rev-list --maximal-only \
refs/heads/commit-5-2 \
refs/heads/commit-3-2 \
refs/heads/commit-2-5 >actual &&
sort actual >actual.sorted &&
test_cmp expect.sorted actual.sorted &&

# All independent commits.
git merge-base --independent \
refs/heads/commit-5-2 \
refs/heads/commit-4-3 \
refs/heads/commit-3-4 \
refs/heads/commit-2-5 >expect &&
sort expect >expect.sorted &&
git rev-list --maximal-only \
refs/heads/commit-5-2 \
refs/heads/commit-4-3 \
refs/heads/commit-3-4 \
refs/heads/commit-2-5 >actual &&
sort actual >actual.sorted &&
test_cmp expect.sorted actual.sorted &&

# Only one independent.
git merge-base --independent \
refs/heads/commit-1-1 \
refs/heads/commit-4-2 \
refs/heads/commit-4-4 \
refs/heads/commit-8-4 >expect &&
sort expect >expect.sorted &&
git rev-list --maximal-only \
refs/heads/commit-1-1 \
refs/heads/commit-4-2 \
refs/heads/commit-4-4 \
refs/heads/commit-8-4 >actual &&
sort actual >actual.sorted &&
test_cmp expect.sorted actual.sorted
'

test_done