revision: add --maximal-only option

When inspecting a range of commits from some set of starting references, it is sometimes useful to learn which commits are not reachable from any other commits in the selected range. One such application is in the creation of a sequence of bundles for the bundle URI feature. Creating a stack of bundles representing different slices of time includes defining which references to include. If all references are used, then this may be overwhelming or redundant. Instead, selecting commits that are maximal to the range could help defining a smaller reference set to use in the bundle header. Add a new '--maximal-only' option to restrict the output of a revision range to be only the commits that are not reachable from any other commit in the range, based on the reachability definition of the walk. This is accomplished by adding a new 28th bit flag, CHILD_VISITED, that is set as we walk. This does extend the bit range in object.h, but using an earlier bit may collide with another feature. The tests demonstrate the behavior of the feature with a positive-only range, ranges with negative references, and walk-modifying flags like --first-parent and --exclude-first-parent-only. Since the --boundary option would not increase any results when used with the --maximal-only option, mark them as incompatible. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Jan 22, 2026 at 16:05 UTC b4e8f60a3c78477e1f28b052cd740ac4a43741d5
6 files changed +110 -5
Documentation/rev-list-options.adoc
+4
@@ -148,6 +148,10 @@ endif::git-log[]
148 from the point where it diverged from the remote branch, given
149 that arbitrary merges can be valid topic branch changes.
150
151 +`--maximal-only`::
152 + Restrict the output commits to be those that are not reachable
153 + from any other commits in the revision range.
154 +
155 `--not`::
156 Reverses the meaning of the '{caret}' prefix (or lack thereof)
157 for all following revision specifiers, up to the next `--not`.
object.h
+2 -2
@@ -64,7 +64,7 @@ void object_array_init(struct object_array *array);
64
65 /*
66 * object flag allocation:
67 - * revision.h: 0---------10 15 23------27
67 + * revision.h: 0---------10 15 23--------28
68 * fetch-pack.c: 01 67
69 * negotiator/default.c: 2--5
70 * walker.c: 0-2
@@ -86,7 +86,7 @@ void object_array_init(struct object_array *array);
86 * builtin/unpack-objects.c: 2021
87 * pack-bitmap.h: 2122
88 */
89 -#define FLAG_BITS 28
89 +#define FLAG_BITS 29
90
91 #define TYPE_BITS 3
92
revision.c
+10 -2
@@ -1150,7 +1150,8 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
1150 struct commit *p = parent->item;
1151 parent = parent->next;
1152 if (p)
1153 - p->object.flags |= UNINTERESTING;
1153 + p->object.flags |= UNINTERESTING |
1154 + CHILD_VISITED;
1155 if (repo_parse_commit_gently(revs->repo, p, 1) < 0)
1156 continue;
1157 if (p->parents)
@@ -1204,7 +1205,7 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
1205 if (!*slot)
1206 *slot = *revision_sources_at(revs->sources, commit);
1207 }
1207 - p->object.flags |= pass_flags;
1208 + p->object.flags |= pass_flags | CHILD_VISITED;
1209 if (!(p->object.flags & SEEN)) {
1210 p->object.flags |= (SEEN | NOT_USER_GIVEN);
1211 if (list)
@@ -2377,6 +2378,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
2378 } else if ((argcount = parse_long_opt("until", argv, &optarg))) {
2379 revs->min_age = approxidate(optarg);
2380 return argcount;
2381 + } else if (!strcmp(arg, "--maximal-only")) {
2382 + revs->maximal_only = 1;
2383 } else if (!strcmp(arg, "--first-parent")) {
2384 revs->first_parent_only = 1;
2385 } 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
3150 !!revs->reverse, "--reverse",
3151 !!revs->reflog_info, "--walk-reflogs");
3152
3153 + die_for_incompatible_opt2(!!revs->boundary, "--boundary",
3154 + !!revs->maximal_only, "--maximal-only");
3155 +
3156 if (revs->no_walk && revs->graph)
3157 die(_("options '%s' and '%s' cannot be used together"), "--no-walk", "--graph");
3158 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
4131 {
4132 if (commit->object.flags & SHOWN)
4133 return commit_ignore;
4134 + if (revs->maximal_only && (commit->object.flags & CHILD_VISITED))
4135 + return commit_ignore;
4136 if (revs->unpacked && has_object_pack(revs->repo, &commit->object.oid))
4137 return commit_ignore;
4138 if (revs->no_kept_objects) {
revision.h
+4 -1
@@ -52,7 +52,9 @@
52 #define NOT_USER_GIVEN (1u<<25)
53 #define TRACK_LINEAR (1u<<26)
54 #define ANCESTRY_PATH (1u<<27)
55 -#define ALL_REV_FLAGS (((1u<<11)-1) | NOT_USER_GIVEN | TRACK_LINEAR | PULL_MERGE)
55 +#define CHILD_VISITED (1u<<28)
56 +#define ALL_REV_FLAGS (((1u<<11)-1) | NOT_USER_GIVEN | TRACK_LINEAR \
57 + | PULL_MERGE | CHILD_VISITED)
58
59 #define DECORATE_SHORT_REFS 1
60 #define DECORATE_FULL_REFS 2
@@ -189,6 +191,7 @@ struct rev_info {
191 left_right:1,
192 left_only:1,
193 right_only:1,
194 + maximal_only:1,
195 rewrite_parents:1,
196 print_parents:1,
197 show_decorations:1,
t/t6000-rev-list-misc.sh
+15
@@ -248,4 +248,19 @@ test_expect_success 'rev-list -z --boundary' '
248 test_cmp expect actual
249 '
250
251 +test_expect_success 'rev-list --boundary incompatible with --maximal-only' '
252 + test_when_finished rm -rf repo &&
253 +
254 + git init repo &&
255 + test_commit -C repo 1 &&
256 + test_commit -C repo 2 &&
257 +
258 + oid1=$(git -C repo rev-parse HEAD~) &&
259 + oid2=$(git -C repo rev-parse HEAD) &&
260 +
261 + test_must_fail git -C repo rev-list --boundary --maximal-only \
262 + HEAD~1..HEAD 2>err &&
263 + test_grep "cannot be used together" err
264 +'
265 +
266 test_done
t/t6600-test-reach.sh
+75
@@ -762,4 +762,79 @@ test_expect_success 'for-each-ref is-base: --sort' '
762 --sort=refname --sort=-is-base:commit-2-3
763 '
764
765 +test_expect_success 'rev-list --maximal-only (all positive)' '
766 + # Only one maximal.
767 + cat >input <<-\EOF &&
768 + refs/heads/commit-1-1
769 + refs/heads/commit-4-2
770 + refs/heads/commit-4-4
771 + refs/heads/commit-8-4
772 + EOF
773 +
774 + cat >expect <<-EOF &&
775 + $(git rev-parse refs/heads/commit-8-4)
776 + EOF
777 + run_all_modes git rev-list --maximal-only --stdin &&
778 +
779 + # All maximal.
780 + cat >input <<-\EOF &&
781 + refs/heads/commit-5-2
782 + refs/heads/commit-4-3
783 + refs/heads/commit-3-4
784 + refs/heads/commit-2-5
785 + EOF
786 +
787 + cat >expect <<-EOF &&
788 + $(git rev-parse refs/heads/commit-5-2)
789 + $(git rev-parse refs/heads/commit-4-3)
790 + $(git rev-parse refs/heads/commit-3-4)
791 + $(git rev-parse refs/heads/commit-2-5)
792 + EOF
793 + run_all_modes git rev-list --maximal-only --stdin &&
794 +
795 + # Mix of both.
796 + cat >input <<-\EOF &&
797 + refs/heads/commit-5-2
798 + refs/heads/commit-3-2
799 + refs/heads/commit-2-5
800 + EOF
801 +
802 + cat >expect <<-EOF &&
803 + $(git rev-parse refs/heads/commit-5-2)
804 + $(git rev-parse refs/heads/commit-2-5)
805 + EOF
806 + run_all_modes git rev-list --maximal-only --stdin
807 +'
808 +
809 +test_expect_success 'rev-list --maximal-only (range)' '
810 + cat >input <<-\EOF &&
811 + refs/heads/commit-1-1
812 + refs/heads/commit-2-5
813 + refs/heads/commit-6-4
814 + ^refs/heads/commit-4-5
815 + EOF
816 +
817 + cat >expect <<-EOF &&
818 + $(git rev-parse refs/heads/commit-6-4)
819 + EOF
820 + run_all_modes git rev-list --maximal-only --stdin &&
821 +
822 + # first-parent changes reachability: the first parent
823 + # reduces the second coordinate to 1 before reducing the
824 + # first coordinate.
825 + cat >input <<-\EOF &&
826 + refs/heads/commit-1-1
827 + refs/heads/commit-2-5
828 + refs/heads/commit-6-4
829 + ^refs/heads/commit-4-5
830 + EOF
831 +
832 + cat >expect <<-EOF &&
833 + $(git rev-parse refs/heads/commit-6-4)
834 + $(git rev-parse refs/heads/commit-2-5)
835 + EOF
836 + run_all_modes git rev-list --maximal-only --stdin \
837 + --first-parent --exclude-first-parent-only
838 +'
839 +
840 test_done