Merge branch 'ds/revision-maximal-only'

"git rev-list" and friends learn "--maximal-only" to show only the
commits that are not reachable by other commits.

* ds/revision-maximal-only:
  revision: add --maximal-only option
This commit is contained in:
Junio C Hamano
2026-02-25 11:54:17 -08:00
6 changed files with 110 additions and 5 deletions

View File

@@ -148,6 +148,10 @@ endif::git-log[]
from the point where it diverged from the remote branch, given
that arbitrary merges can be valid topic branch changes.
`--maximal-only`::
Restrict the output commits to be those that are not reachable
from any other commits in the revision range.
`--not`::
Reverses the meaning of the '{caret}' prefix (or lack thereof)
for all following revision specifiers, up to the next `--not`.

View File

@@ -64,7 +64,7 @@ void object_array_init(struct object_array *array);
/*
* object flag allocation:
* revision.h: 0---------10 15 23------27
* revision.h: 0---------10 15 23--------28
* fetch-pack.c: 01 67
* negotiator/default.c: 2--5
* walker.c: 0-2
@@ -86,7 +86,7 @@ void object_array_init(struct object_array *array);
* builtin/unpack-objects.c: 2021
* pack-bitmap.h: 2122
*/
#define FLAG_BITS 28
#define FLAG_BITS 29
#define TYPE_BITS 3

View File

@@ -1150,7 +1150,8 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
struct commit *p = parent->item;
parent = parent->next;
if (p)
p->object.flags |= UNINTERESTING;
p->object.flags |= UNINTERESTING |
CHILD_VISITED;
if (repo_parse_commit_gently(revs->repo, p, 1) < 0)
continue;
if (p->parents)
@@ -1204,7 +1205,7 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
if (!*slot)
*slot = *revision_sources_at(revs->sources, commit);
}
p->object.flags |= pass_flags;
p->object.flags |= pass_flags | CHILD_VISITED;
if (!(p->object.flags & SEEN)) {
p->object.flags |= (SEEN | NOT_USER_GIVEN);
if (list)
@@ -2377,6 +2378,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
} else if ((argcount = parse_long_opt("until", argv, &optarg))) {
revs->min_age = approxidate(optarg);
return argcount;
} else if (!strcmp(arg, "--maximal-only")) {
revs->maximal_only = 1;
} else if (!strcmp(arg, "--first-parent")) {
revs->first_parent_only = 1;
} else if (!strcmp(arg, "--exclude-first-parent-only")) {
@@ -3147,6 +3150,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
!!revs->reverse, "--reverse",
!!revs->reflog_info, "--walk-reflogs");
die_for_incompatible_opt2(!!revs->boundary, "--boundary",
!!revs->maximal_only, "--maximal-only");
if (revs->no_walk && revs->graph)
die(_("options '%s' and '%s' cannot be used together"), "--no-walk", "--graph");
if (!revs->reflog_info && revs->grep_filter.use_reflog_filter)
@@ -4125,6 +4131,8 @@ enum commit_action get_commit_action(struct rev_info *revs, struct commit *commi
{
if (commit->object.flags & SHOWN)
return commit_ignore;
if (revs->maximal_only && (commit->object.flags & CHILD_VISITED))
return commit_ignore;
if (revs->unpacked && has_object_pack(revs->repo, &commit->object.oid))
return commit_ignore;
if (revs->no_kept_objects) {

View File

@@ -52,7 +52,9 @@
#define NOT_USER_GIVEN (1u<<25)
#define TRACK_LINEAR (1u<<26)
#define ANCESTRY_PATH (1u<<27)
#define ALL_REV_FLAGS (((1u<<11)-1) | NOT_USER_GIVEN | TRACK_LINEAR | PULL_MERGE)
#define CHILD_VISITED (1u<<28)
#define ALL_REV_FLAGS (((1u<<11)-1) | NOT_USER_GIVEN | TRACK_LINEAR \
| PULL_MERGE | CHILD_VISITED)
#define DECORATE_SHORT_REFS 1
#define DECORATE_FULL_REFS 2
@@ -189,6 +191,7 @@ struct rev_info {
left_right:1,
left_only:1,
right_only:1,
maximal_only:1,
rewrite_parents:1,
print_parents:1,
show_decorations:1,

View File

@@ -248,4 +248,19 @@ test_expect_success 'rev-list -z --boundary' '
test_cmp expect actual
'
test_expect_success 'rev-list --boundary incompatible with --maximal-only' '
test_when_finished rm -rf repo &&
git init repo &&
test_commit -C repo 1 &&
test_commit -C repo 2 &&
oid1=$(git -C repo rev-parse HEAD~) &&
oid2=$(git -C repo rev-parse HEAD) &&
test_must_fail git -C repo rev-list --boundary --maximal-only \
HEAD~1..HEAD 2>err &&
test_grep "cannot be used together" err
'
test_done

View File

@@ -762,4 +762,79 @@ test_expect_success 'for-each-ref is-base: --sort' '
--sort=refname --sort=-is-base:commit-2-3
'
test_expect_success 'rev-list --maximal-only (all positive)' '
# Only one maximal.
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-4-2
refs/heads/commit-4-4
refs/heads/commit-8-4
EOF
cat >expect <<-EOF &&
$(git rev-parse refs/heads/commit-8-4)
EOF
run_all_modes git rev-list --maximal-only --stdin &&
# All maximal.
cat >input <<-\EOF &&
refs/heads/commit-5-2
refs/heads/commit-4-3
refs/heads/commit-3-4
refs/heads/commit-2-5
EOF
cat >expect <<-EOF &&
$(git rev-parse refs/heads/commit-5-2)
$(git rev-parse refs/heads/commit-4-3)
$(git rev-parse refs/heads/commit-3-4)
$(git rev-parse refs/heads/commit-2-5)
EOF
run_all_modes git rev-list --maximal-only --stdin &&
# Mix of both.
cat >input <<-\EOF &&
refs/heads/commit-5-2
refs/heads/commit-3-2
refs/heads/commit-2-5
EOF
cat >expect <<-EOF &&
$(git rev-parse refs/heads/commit-5-2)
$(git rev-parse refs/heads/commit-2-5)
EOF
run_all_modes git rev-list --maximal-only --stdin
'
test_expect_success 'rev-list --maximal-only (range)' '
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-2-5
refs/heads/commit-6-4
^refs/heads/commit-4-5
EOF
cat >expect <<-EOF &&
$(git rev-parse refs/heads/commit-6-4)
EOF
run_all_modes git rev-list --maximal-only --stdin &&
# first-parent changes reachability: the first parent
# reduces the second coordinate to 1 before reducing the
# first coordinate.
cat >input <<-\EOF &&
refs/heads/commit-1-1
refs/heads/commit-2-5
refs/heads/commit-6-4
^refs/heads/commit-4-5
EOF
cat >expect <<-EOF &&
$(git rev-parse refs/heads/commit-6-4)
$(git rev-parse refs/heads/commit-2-5)
EOF
run_all_modes git rev-list --maximal-only --stdin \
--first-parent --exclude-first-parent-only
'
test_done