]> git.ipfire.org Git - thirdparty/git.git/commitdiff
revision: add --maximal-only option
authorDerrick Stolee <stolee@gmail.com>
Thu, 22 Jan 2026 16:05:58 +0000 (16:05 +0000)
committerJunio C Hamano <gitster@pobox.com>
Thu, 22 Jan 2026 18:58:14 +0000 (10:58 -0800)
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>
Documentation/rev-list-options.adoc
object.h
revision.c
revision.h
t/t6000-rev-list-misc.sh
t/t6600-test-reach.sh

index 453ec590571ffced37db55f27c4eb473c77633c6..a39cf88bbcfaaa0e28d7a1a7592adccd9e5c3951 100644 (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`.
index 4bca957b8dcbd698e2757b7940f8c5b2298f84cf..dfe7a1f0ea29da6062ce3b8d370e9c90eeca0342 100644 (file)
--- a/object.h
+++ b/object.h
@@ -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
 
index 9b131670f79b96b72272ef554fefb511facab4b8..6ca4b9dfcd54b93870ac05ba390c53c69fed404d 100644 (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) {
index b36acfc2d9f61dfa19144a93a9f0dfd246e3be4d..69242ecb189a527db53cb1f3f5a7616f98ff9227 100644 (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,
index fec16448cfddb873c1dde45975dd119bee29b989..d0a2a866100d56a613acf4066621f531c25e454b 100755 (executable)
@@ -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
index 6638d1aa1dcebe6f68cf7168bafafe06b54d5fb2..2613075894282d0fc884e5f5e4e71e0573672dcb 100755 (executable)
@@ -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