{"thread":{"id":"64828","subject":"[PATCH] revision: add --maximal option","startedAt":"2026-01-18T02:34:08Z","lastAt":"2026-01-29T14:57:58Z","messageCount":19,"participants":["Derrick Stolee via GitGitGadget","Johannes Sixt","Derrick Stolee","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"534132","messageId":"pull.2032.git.1768703645125.gitgitgadget@gmail.com","threadId":"64828","inReplyTo":null,"subject":"[PATCH] revision: add --maximal option","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-18T02:34:05Z","receivedAt":"2026-01-18T02:34:08Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWhen inspecting a range of commits from some set of starting references, it\nis sometimes useful to learn which commits are not reachable from any other\ncommits in the selected range.\n\nOne such application is in the creation of a sequence of bundles for the\nbundle URI feature. Creating a stack of bundles representing different\nslices of time includes defining which references to include. If all\nreferences are used, then this may be overwhelming or redundant. Instead,\nselecting commits that are maximal to the range could help defining a\nsmaller reference set to use in the bundle header.\n\nAdd a new '--maximal' option to restrict the output of a revision range to\nbe only the commits that are not reachable from any other commit in the\nrange, based on the reachability definition of the walk.\n\nThis is accomplished by adding a new 28th bit flag, CHILD_VISITED, that is\nset as we walk. This does extend the bit range in object.h, but using an\nearlier bit may collide with another feature.\n\nThe tests demonstrate the behavior of the feature with a positive-only\nrange, ranges with negative references, and walk-modifying flags like\n--first-parent and --exclude-first-parent-only.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n    revision: add --maximal option\n    \n    My motivation for this feature is very similar to the bundle URI\n    application. I can get around it by creating a tool that uses git\n    rev-list --parents and then uses a hashset to collect the parent list\n    and filter out any commits that ever appear as parents. It would be more\n    efficient to use Git's native revision-walking feature.\n    \n    This does bring the object struct up to a 32-bit boundary with 28 flag\n    bits, 3 type bits, and a parsed bit. That's the biggest concern I have\n    about this update adding a new flag bit. I would understand if this\n    feature is not worth running out of room for extensions there.\n    \n    I considered looking through the earlier bit positions to see the impact\n    of an overlap, but they certainly looked potentially risky to reuse.\n    \n    I wonder if anyone else has thought about this as a useful technique.\n    For instance, it could be part of a strategy for choosing commits for\n    reachability bitmaps.\n    \n    Thanks, -Stolee\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2032%2Fderrickstolee%2Fmaximal-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2032/derrickstolee/maximal-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2032\n\n Documentation/rev-list-options.adoc |  4 ++\n object.h                            |  4 +-\n revision.c                          |  9 +++-\n revision.h                          |  5 +-\n t/t6600-test-reach.sh               | 75 +++++++++++++++++++++++++++++\n 5 files changed, 92 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex 453ec59057..f0d2ab32a9 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -444,6 +444,10 @@ The following options affect the way the simplification is performed:\n \ttimes; if so, a commit is included if it is any of the commits\n \tgiven or if it is an ancestor or descendant of one of them.\n \n+`--maximal`::\n+\tRestrict the output commits to be those that are not reachable\n+\tfrom any other commits in the revision range.\n+\n A more detailed explanation follows.\n \n Suppose you specified `foo` as the _<paths>_.  We shall call commits\ndiff --git a/object.h b/object.h\nindex 4bca957b8d..dfe7a1f0ea 100644\n--- a/object.h\n+++ b/object.h\n@@ -64,7 +64,7 @@ void object_array_init(struct object_array *array);\n \n /*\n  * object flag allocation:\n- * revision.h:               0---------10         15               23------27\n+ * revision.h:               0---------10         15               23--------28\n  * fetch-pack.c:             01    67\n  * negotiator/default.c:       2--5\n  * walker.c:                 0-2\n@@ -86,7 +86,7 @@ void object_array_init(struct object_array *array);\n  * builtin/unpack-objects.c:                                 2021\n  * pack-bitmap.h:                                              2122\n  */\n-#define FLAG_BITS  28\n+#define FLAG_BITS  29\n \n #define TYPE_BITS 3\n \ndiff --git a/revision.c b/revision.c\nindex 1858e093ee..29864426d6 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -1150,7 +1150,8 @@ static int process_parents(struct rev_info *revs, struct commit *commit,\n \t\t\tstruct commit *p = parent->item;\n \t\t\tparent = parent->next;\n \t\t\tif (p)\n-\t\t\t\tp->object.flags |= UNINTERESTING;\n+\t\t\t\tp->object.flags |= UNINTERESTING |\n+\t\t\t\t\t\t   CHILD_VISITED;\n \t\t\tif (repo_parse_commit_gently(revs->repo, p, 1) < 0)\n \t\t\t\tcontinue;\n \t\t\tif (p->parents)\n@@ -1204,7 +1205,7 @@ static int process_parents(struct rev_info *revs, struct commit *commit,\n \t\t\tif (!*slot)\n \t\t\t\t*slot = *revision_sources_at(revs->sources, commit);\n \t\t}\n-\t\tp->object.flags |= pass_flags;\n+\t\tp->object.flags |= pass_flags | CHILD_VISITED;\n \t\tif (!(p->object.flags & SEEN)) {\n \t\t\tp->object.flags |= (SEEN | NOT_USER_GIVEN);\n \t\t\tif (list)\n@@ -2381,6 +2382,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t\trevs->first_parent_only = 1;\n \t} else if (!strcmp(arg, \"--exclude-first-parent-only\")) {\n \t\trevs->exclude_first_parent_only = 1;\n+\t} else if (!strcmp(arg, \"--maximal\")) {\n+\t\trevs->maximal = 1;\n \t} else if (!strcmp(arg, \"--ancestry-path\")) {\n \t\trevs->ancestry_path = 1;\n \t\trevs->simplify_history = 0;\n@@ -4125,6 +4128,8 @@ enum commit_action get_commit_action(struct rev_info *revs, struct commit *commi\n {\n \tif (commit->object.flags & SHOWN)\n \t\treturn commit_ignore;\n+\tif (revs->maximal && (commit->object.flags & CHILD_VISITED))\n+\t\treturn commit_ignore;\n \tif (revs->unpacked && has_object_pack(revs->repo, &commit->object.oid))\n \t\treturn commit_ignore;\n \tif (revs->no_kept_objects) {\ndiff --git a/revision.h b/revision.h\nindex b36acfc2d9..e5c2c82145 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -52,7 +52,9 @@\n #define NOT_USER_GIVEN\t(1u<<25)\n #define TRACK_LINEAR\t(1u<<26)\n #define ANCESTRY_PATH\t(1u<<27)\n-#define ALL_REV_FLAGS\t(((1u<<11)-1) | NOT_USER_GIVEN | TRACK_LINEAR | PULL_MERGE)\n+#define CHILD_VISITED\t(1u<<28)\n+#define ALL_REV_FLAGS\t(((1u<<11)-1) | NOT_USER_GIVEN | TRACK_LINEAR \\\n+\t\t\t\t      | PULL_MERGE | CHILD_VISITED)\n \n #define DECORATE_SHORT_REFS\t1\n #define DECORATE_FULL_REFS\t2\n@@ -198,6 +200,7 @@ struct rev_info {\n \t\t\tcherry_mark:1,\n \t\t\tbisect:1,\n \t\t\tancestry_path:1,\n+\t\t\tmaximal:1,\n \n \t\t\t/* True if --ancestry-path was specified without an\n \t\t\t * argument. The bottom revisions are implicitly\ndiff --git a/t/t6600-test-reach.sh b/t/t6600-test-reach.sh\nindex 6638d1aa1d..a759409756 100755\n--- a/t/t6600-test-reach.sh\n+++ b/t/t6600-test-reach.sh\n@@ -762,4 +762,79 @@ test_expect_success 'for-each-ref is-base: --sort' '\n \t\t--sort=refname --sort=-is-base:commit-2-3\n '\n \n+test_expect_success 'rev-list --maximal (all positive)' '\n+\t# Only one maximal.\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-1-1\n+\trefs/heads/commit-4-2\n+\trefs/heads/commit-4-4\n+\trefs/heads/commit-8-4\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-8-4)\n+\tEOF\n+\trun_all_modes git rev-list --maximal --stdin &&\n+\n+\t# All maximal.\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-5-2\n+\trefs/heads/commit-4-3\n+\trefs/heads/commit-3-4\n+\trefs/heads/commit-2-5\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-5-2)\n+\t$(git rev-parse refs/heads/commit-4-3)\n+\t$(git rev-parse refs/heads/commit-3-4)\n+\t$(git rev-parse refs/heads/commit-2-5)\n+\tEOF\n+\trun_all_modes git rev-list --maximal --stdin &&\n+\n+\t# Mix of both.\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-5-2\n+\trefs/heads/commit-3-2\n+\trefs/heads/commit-2-5\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-5-2)\n+\t$(git rev-parse refs/heads/commit-2-5)\n+\tEOF\n+\trun_all_modes git rev-list --maximal --stdin\n+'\n+\n+test_expect_success 'rev-list --maximal (range)' '\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-1-1\n+\trefs/heads/commit-2-5\n+\trefs/heads/commit-6-4\n+\t^refs/heads/commit-4-5\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-6-4)\n+\tEOF\n+\trun_all_modes git rev-list --maximal --stdin &&\n+\n+\t# first-parent changes reachability: the first parent\n+\t# reduces the second coordinate to 1 before reducing the\n+\t# first coordinate.\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-1-1\n+\trefs/heads/commit-2-5\n+\trefs/heads/commit-6-4\n+\t^refs/heads/commit-4-5\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-6-4)\n+\t$(git rev-parse refs/heads/commit-2-5)\n+\tEOF\n+\trun_all_modes git rev-list --maximal --stdin \\\n+\t\t--first-parent --exclude-first-parent-only\n+'\n+\n test_done\n\nbase-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c\n-- \ngitgitgadget\n"},{"id":"534138","messageId":"1da38e88-3f61-43df-9c75-5716d715bf80@kdbg.org","threadId":"64828","inReplyTo":"pull.2032.git.1768703645125.gitgitgadget@gmail.com","subject":"Re: [PATCH] revision: add --maximal option","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-01-18T09:05:30Z","receivedAt":"2026-01-18T09:05:39Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 18.01.26 um 03:34 schrieb Derrick Stolee via GitGitGadget:\n> diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\n> index 453ec59057..f0d2ab32a9 100644\n> --- a/Documentation/rev-list-options.adoc\n> +++ b/Documentation/rev-list-options.adoc\n> @@ -444,6 +444,10 @@ The following options affect the way the simplification is performed:\n>  \ttimes; if so, a commit is included if it is any of the commits\n>  \tgiven or if it is an ancestor or descendant of one of them.\n>  \n> +`--maximal`::\n> +\tRestrict the output commits to be those that are not reachable\n> +\tfrom any other commits in the revision range.\n\nI had to read this sentence three times to understand what it wants to\nsay, and that even though I had a rough idea what it was supposed to\nmean. I tried to come up with a better wording, but found it to be\nreally hard.\n\n\tRestrict output to the commits at the tips of the\n\trevision range.\n\nis all I could do, but this isn't a lot better, I am afraid.\n\nThe option name is too generic IMHO. How about \"--starting-point\",\n\"--topmost-only\"?  It's function is somewhat parallel to --boundary, but\nat the positive end of the revision range. Perhaps we can use that as\ninspiration.\n\nThe option is listed among options that affect the way the\nsimplification is performed. But is this true? Isn't it just an option\nthat changes what output is produced?\n\n-- Hannes\n\n"},{"id":"534150","messageId":"b46885b1-5781-43d8-8751-d85048c45e5e@gmail.com","threadId":"64828","inReplyTo":"1da38e88-3f61-43df-9c75-5716d715bf80@kdbg.org","subject":"Re: [PATCH] revision: add --maximal option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-01-18T18:27:11Z","receivedAt":"2026-01-18T18:27:13Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 1/18/26 4:05 AM, Johannes Sixt wrote:\n> Am 18.01.26 um 03:34 schrieb Derrick Stolee via GitGitGadget:\n>> diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\n>> index 453ec59057..f0d2ab32a9 100644\n>> --- a/Documentation/rev-list-options.adoc\n>> +++ b/Documentation/rev-list-options.adoc\n>> @@ -444,6 +444,10 @@ The following options affect the way the simplification is performed:\n>>   \ttimes; if so, a commit is included if it is any of the commits\n>>   \tgiven or if it is an ancestor or descendant of one of them.\n>>   \n>> +`--maximal`::\n>> +\tRestrict the output commits to be those that are not reachable\n>> +\tfrom any other commits in the revision range.\n> \n> I had to read this sentence three times to understand what it wants to\n> say, and that even though I had a rough idea what it was supposed to\n> mean. I tried to come up with a better wording, but found it to be\n> really hard.\n> \n> \tRestrict output to the commits at the tips of the\n> \trevision range.\n> \n> is all I could do, but this isn't a lot better, I am afraid.\n > > The option name is too generic IMHO. How about \"--starting-point\",\n> \"--topmost-only\"?  It's function is somewhat parallel to --boundary, but\n> at the positive end of the revision range. Perhaps we can use that as\n> inspiration.\n\nMy perspective is skewed, because \"maximal\" is a concrete term in the\nworld of partially-ordered sets (such as commit history ordered by\nreachability across child-to-parent relationships). It's important to\ndistinguish from \"starting points\" because the inputs to the command\nare a list of starting points, not all of which are maximal within the\nset. In fact, if some positive starting points are reachable from the\nnegative starting points, then they are already excluded.\n\nMy familiarity with this term is skewed by my experience working with\nsuch terms, so I'm very open to new names for this option.\n\nYour comparison to --boundary is interesting, because --boundary _adds_\ncommits to the range by selecting the commits from the negative range\nthat are reachable from the output commits. --maximal as defined here\n_restricts_ to the output of commits in the range. It's interaction with\n--boundary is trivial because no boundary commits would be included as\nthey are necessarily reachable from a maximal commit.\n\n> The option is listed among options that affect the way the\n> simplification is performed. But is this true? Isn't it just an option\n> that changes what output is produced?\n\nYou're right that this is poorly placed. I'll put it in a better location\nin v2.\n\nThanks,\n-Stolee\n\n"},{"id":"534179","messageId":"1ce18cac-f988-4741-b9dd-6c1cf2d4e6af@kdbg.org","threadId":"64828","inReplyTo":"b46885b1-5781-43d8-8751-d85048c45e5e@gmail.com","subject":"Re: [PATCH] revision: add --maximal option","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-01-19T11:15:45Z","receivedAt":"2026-01-19T11:16:03Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 18.01.26 um 19:27 schrieb Derrick Stolee:\n> On 1/18/26 4:05 AM, Johannes Sixt wrote:\n>> Am 18.01.26 um 03:34 schrieb Derrick Stolee via GitGitGadget:\n>> > The option name is too generic IMHO. How about \"--starting-point\",\n>> \"--topmost-only\"?  It's function is somewhat parallel to --boundary, but\n>> at the positive end of the revision range. Perhaps we can use that as\n>> inspiration.\n> \n> My perspective is skewed, because \"maximal\" is a concrete term in the\n> world of partially-ordered sets (such as commit history ordered by\n> reachability across child-to-parent relationships). It's important to\n> distinguish from \"starting points\" because the inputs to the command\n> are a list of starting points, not all of which are maximal within the\n> set. In fact, if some positive starting points are reachable from the\n> negative starting points, then they are already excluded.\n\nAFAICS, we don't have options named after graph- or set-theoretical\nterms, but tend to stick to terms established in the Git ecosystem. I\nassume that \"maximal\" isn't a meaning that an average Git user would\nassociate with the operation that is performed here.\n\nBut even if we decide to use \"maximal\", the option must be named\nsomething other than *just* \"--maximal\"; this is simply too generic.\nPerhaps \"--only-maximal\" or \"--maximal-only\".\n\nOther ideas:\n- --hide-reachable\n- --range-head\n- --range-head-only\n- --most-recent\n- --most-recent-only\n\n> [--maximal]'s interaction with\n> --boundary is trivial because no boundary commits would be included as\n> they are necessarily reachable from a maximal commit.\n\nSo, --boundary --maximal shows only the maximal commits? That sounds\nunexpected. Boundary commits are shown with additional mark-up; they\ndon't need to be suppressed. But in a first iteration it's probably\nbetter to just make the two options incompatible.\n\n-- Hannes\n\n"},{"id":"534184","messageId":"3fab80c8-f602-44d5-8e7d-436982a5e3a8@gmail.com","threadId":"64828","inReplyTo":"1ce18cac-f988-4741-b9dd-6c1cf2d4e6af@kdbg.org","subject":"Re: [PATCH] revision: add --maximal option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-01-19T16:44:35Z","receivedAt":"2026-01-19T16:44:37Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 1/19/2026 6:15 AM, Johannes Sixt wrote:\n> Am 18.01.26 um 19:27 schrieb Derrick Stolee:\n>> On 1/18/26 4:05 AM, Johannes Sixt wrote:\n>>> Am 18.01.26 um 03:34 schrieb Derrick Stolee via GitGitGadget:\n>>>> The option name is too generic IMHO. How about \"--starting-point\",\n>>> \"--topmost-only\"?  It's function is somewhat parallel to --boundary, but\n>>> at the positive end of the revision range. Perhaps we can use that as\n>>> inspiration.\n>>\n>> My perspective is skewed, because \"maximal\" is a concrete term in the\n>> world of partially-ordered sets (such as commit history ordered by\n>> reachability across child-to-parent relationships). It's important to\n>> distinguish from \"starting points\" because the inputs to the command\n>> are a list of starting points, not all of which are maximal within the\n>> set. In fact, if some positive starting points are reachable from the\n>> negative starting points, then they are already excluded.\n> \n> AFAICS, we don't have options named after graph- or set-theoretical\n> terms, but tend to stick to terms established in the Git ecosystem. I\n> assume that \"maximal\" isn't a meaning that an average Git user would\n> associate with the operation that is performed here.\n\nMy mindset is usually \"all words are made up by somebody\" and since\nthere isn't an established term for this in the existing Git ecosystem,\nit is up to us to create a term. Borrowing one that exists elsewhere is\na valuable way to build upon any context that term brings with it.\n\nIt is also helpful that the term has an explicit technical definition\nthat means exactly what we're using it for here. It explicitly\ndifferentiates from any \"maximum\" or confusion with a collapse to a\ntotal order (such as Git's --date-order or --topo-order apply).\n\n> But even if we decide to use \"maximal\", the option must be named\n> something other than *just* \"--maximal\"; this is simply too generic.\n> Perhaps \"--only-maximal\" or \"--maximal-only\".\n\nWhen the argument is moved in the documentation into the set of\nfilters, then the fact that --maximal restricts the set of commits\nmakes any modifier such as \"only\" redundant.\n \n> Other ideas:\n> - --hide-reachable\n> - --range-head\n> - --range-head-only\n> - --most-recent\n> - --most-recent-only\n\nThese all have issues, such as being technically wrong (maximal commits\nare reachable) or imply total orders, date orders, or generally only a\nsingle result.\n\n>> [--maximal]'s interaction with\n>> --boundary is trivial because no boundary commits would be included as\n>> they are necessarily reachable from a maximal commit.\n> \n> So, --boundary --maximal shows only the maximal commits? That sounds\n> unexpected. Boundary commits are shown with additional mark-up; they\n> don't need to be suppressed. But in a first iteration it's probably\n> better to just make the two options incompatible.\n\nSure. But I'd like to counter that filters like --author also restrict\nthe set, including not showing boundary commits that don't fit the\n--author pattern. It just happens that no boundary commits are also\nmaximal by definition.\n\nI do sense that a lot of this is a matter of taste, and that you and I\ndiffer greatly in our tastes on this topic. I look forward to more\nopinions that can lead us towards one side or another (or in a new\ndirection).\n\nThanks,\n-Stolee\n\n"},{"id":"534190","messageId":"156396e7-7efe-4ed0-a217-c8c2539d9dcb@kdbg.org","threadId":"64828","inReplyTo":"3fab80c8-f602-44d5-8e7d-436982a5e3a8@gmail.com","subject":"Re: [PATCH] revision: add --maximal option","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-01-19T19:05:29Z","receivedAt":"2026-01-19T19:05:39Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 19.01.26 um 17:44 schrieb Derrick Stolee:\n> When the argument is moved in the documentation into the set of\n> filters, then the fact that --maximal restricts the set of commits\n> makes any modifier such as \"only\" redundant.\n\nKeep in mind that rev-list arguments are also understood by git-log.\nThen one could expect --maximal to be somehow the opposite of --minimal.\n(Which is badly named for the same reason, but that ship has sailed.)\n\n-- Hannes\n\n"},{"id":"534199","messageId":"xmqqo6mp3zft.fsf@gitster.g","threadId":"64828","inReplyTo":"1ce18cac-f988-4741-b9dd-6c1cf2d4e6af@kdbg.org","subject":"Re: [PATCH] revision: add --maximal option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-20T00:22:46Z","receivedAt":"2026-01-20T00:22:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Johannes Sixt <j6t@kdbg.org> writes:\n\nJohannes Sixt <j6t@kdbg.org> writes:\n\n> But even if we decide to use \"maximal\", the option must be named\n> something other than *just* \"--maximal\"; this is simply too generic.\n> Perhaps \"--only-maximal\" or \"--maximal-only\".\n>\n> Other ideas:\n> - --hide-reachable\n> - --range-head\n> - --range-head-only\n> - --most-recent\n> - --most-recent-only\n>\n>> [--maximal]'s interaction with\n>> --boundary is trivial because no boundary commits would be included as\n>> they are necessarily reachable from a maximal commit.\n>\n> So, --boundary --maximal shows only the maximal commits? That sounds\n> unexpected. Boundary commits are shown with additional mark-up; they\n> don't need to be suppressed. But in a first iteration it's probably\n> better to just make the two options incompatible.\n\nIf I am reading the answer to \"what is minimal/maximal elements in\npartially ordered set?\" correctly, our \"--boundary\" essentially is\nto show direct parents of those commits that would be shown with the\n(nonexistent) \"--minimal-only\" option.  So I agree with you that it\nmakes perfect sense to make \"--boundary\" and \"--maximal-only\"\nincompatible (it is like asking for both \"--minimal-only\" and\n\"--maximal-only\" at the same time).\n\n\n"},{"id":"534457","messageId":"85c6fdce-d48a-4af2-ba19-432885a034ab@gmail.com","threadId":"64828","inReplyTo":"xmqqo6mp3zft.fsf@gitster.g","subject":"Re: [PATCH] revision: add --maximal option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-01-22T15:08:53Z","receivedAt":"2026-01-22T15:08:56Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 1/19/26 7:22 PM, Junio C Hamano wrote:\n> Johannes Sixt <j6t@kdbg.org> writes:\n>> But even if we decide to use \"maximal\", the option must be named\n>> something other than *just* \"--maximal\"; this is simply too generic.\n>> Perhaps \"--only-maximal\" or \"--maximal-only\".\n>>\n>> Other ideas:\n>> - --hide-reachable\n>> - --range-head\n>> - --range-head-only\n>> - --most-recent\n>> - --most-recent-only\n>>\n>>> [--maximal]'s interaction with\n>>> --boundary is trivial because no boundary commits would be included as\n>>> they are necessarily reachable from a maximal commit.\n>>\n>> So, --boundary --maximal shows only the maximal commits? That sounds\n>> unexpected. Boundary commits are shown with additional mark-up; they\n>> don't need to be suppressed. But in a first iteration it's probably\n>> better to just make the two options incompatible.\n> \n> If I am reading the answer to \"what is minimal/maximal elements in\n> partially ordered set?\" correctly, our \"--boundary\" essentially is\n> to show direct parents of those commits that would be shown with the\n> (nonexistent) \"--minimal-only\" option.  So I agree with you that it\n> makes perfect sense to make \"--boundary\" and \"--maximal-only\"\n> incompatible (it is like asking for both \"--minimal-only\" and\n> \"--maximal-only\" at the same time).\n\nThe existence of a --minimal option that doesn't match the mirror\nof my suggested --maximal option convinces me to move to\n--maximal-only. I will send a v2 shortly that updates this and\nmoves the documentation next to other filtering options.\n\nI'll also mark --boundary and --maximal-only as incompatible to\navoid confusion.\n\nThanks,\n-Stolee\n\n"},{"id":"534468","messageId":"pull.2032.v2.git.1769097958549.gitgitgadget@gmail.com","threadId":"64828","inReplyTo":"pull.2032.git.1768703645125.gitgitgadget@gmail.com","subject":"[PATCH v2] revision: add --maximal-only option","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-22T16:05:58Z","receivedAt":"2026-01-22T16:06:05Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWhen inspecting a range of commits from some set of starting references, it\nis sometimes useful to learn which commits are not reachable from any other\ncommits in the selected range.\n\nOne such application is in the creation of a sequence of bundles for the\nbundle URI feature. Creating a stack of bundles representing different\nslices of time includes defining which references to include. If all\nreferences are used, then this may be overwhelming or redundant. Instead,\nselecting commits that are maximal to the range could help defining a\nsmaller reference set to use in the bundle header.\n\nAdd a new '--maximal-only' option to restrict the output of a revision range\nto be only the commits that are not reachable from any other commit in the\nrange, based on the reachability definition of the walk.\n\nThis is accomplished by adding a new 28th bit flag, CHILD_VISITED, that is\nset as we walk. This does extend the bit range in object.h, but using an\nearlier bit may collide with another feature.\n\nThe tests demonstrate the behavior of the feature with a positive-only\nrange, ranges with negative references, and walk-modifying flags like\n--first-parent and --exclude-first-parent-only.\n\nSince the --boundary option would not increase any results when used with\nthe --maximal-only option, mark them as incompatible.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n    revision: add --maximal-only option\n    \n    My motivation for this feature is very similar to the bundle URI\n    application. I can get around it by creating a tool that uses git\n    rev-list --parents and then uses a hashset to collect the parent list\n    and filter out any commits that ever appear as parents. It would be more\n    efficient to use Git's native revision-walking feature.\n    \n    This does bring the object struct up to a 32-bit boundary with 28 flag\n    bits, 3 type bits, and a parsed bit. That's the biggest concern I have\n    about this update adding a new flag bit. I would understand if this\n    feature is not worth running out of room for extensions there.\n    \n    I considered looking through the earlier bit positions to see the impact\n    of an overlap, but they certainly looked potentially risky to reuse.\n    \n    I wonder if anyone else has thought about this as a useful technique.\n    For instance, it could be part of a strategy for choosing commits for\n    reachability bitmaps.\n    \n    \n    Updates in v2\n    =============\n    \n     * option is now called --maximal-only.\n     * Documentation is moved within the commit-filtering options not the\n       walk-altering options.\n     * --boundary and --maximal-only are marked as incompatible.\n    \n    Thanks, -Stolee\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2032%2Fderrickstolee%2Fmaximal-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2032/derrickstolee/maximal-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2032\n\nRange-diff vs v1:\n\n 1:  889a2737bc ! 1:  54fbf36a1f revision: add --maximal option\n     @@ Metadata\n      Author: Derrick Stolee <stolee@gmail.com>\n      \n       ## Commit message ##\n     -    revision: add --maximal option\n     +    revision: add --maximal-only option\n      \n          When inspecting a range of commits from some set of starting references, it\n          is sometimes useful to learn which commits are not reachable from any other\n     @@ Commit message\n          selecting commits that are maximal to the range could help defining a\n          smaller reference set to use in the bundle header.\n      \n     -    Add a new '--maximal' option to restrict the output of a revision range to\n     -    be only the commits that are not reachable from any other commit in the\n     +    Add a new '--maximal-only' option to restrict the output of a revision range\n     +    to be only the commits that are not reachable from any other commit in the\n          range, based on the reachability definition of the walk.\n      \n          This is accomplished by adding a new 28th bit flag, CHILD_VISITED, that is\n     @@ Commit message\n          range, ranges with negative references, and walk-modifying flags like\n          --first-parent and --exclude-first-parent-only.\n      \n     +    Since the --boundary option would not increase any results when used with\n     +    the --maximal-only option, mark them as incompatible.\n     +\n          Signed-off-by: Derrick Stolee <stolee@gmail.com>\n      \n       ## Documentation/rev-list-options.adoc ##\n     -@@ Documentation/rev-list-options.adoc: The following options affect the way the simplification is performed:\n     - \ttimes; if so, a commit is included if it is any of the commits\n     - \tgiven or if it is an ancestor or descendant of one of them.\n     +@@ Documentation/rev-list-options.adoc: endif::git-log[]\n     + \tfrom the point where it diverged from the remote branch, given\n     + \tthat arbitrary merges can be valid topic branch changes.\n       \n     -+`--maximal`::\n     ++`--maximal-only`::\n      +\tRestrict the output commits to be those that are not reachable\n      +\tfrom any other commits in the revision range.\n      +\n     - A more detailed explanation follows.\n     - \n     - Suppose you specified `foo` as the _<paths>_.  We shall call commits\n     + `--not`::\n     + \tReverses the meaning of the '{caret}' prefix (or lack thereof)\n     + \tfor all following revision specifiers, up to the next `--not`.\n      \n       ## object.h ##\n      @@ object.h: void object_array_init(struct object_array *array);\n     @@ revision.c: static int process_parents(struct rev_info *revs, struct commit *com\n       \t\t\tp->object.flags |= (SEEN | NOT_USER_GIVEN);\n       \t\t\tif (list)\n      @@ revision.c: static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n     + \t} else if ((argcount = parse_long_opt(\"until\", argv, &optarg))) {\n     + \t\trevs->min_age = approxidate(optarg);\n     + \t\treturn argcount;\n     ++\t} else if (!strcmp(arg, \"--maximal-only\")) {\n     ++\t\trevs->maximal_only = 1;\n     + \t} else if (!strcmp(arg, \"--first-parent\")) {\n       \t\trevs->first_parent_only = 1;\n       \t} else if (!strcmp(arg, \"--exclude-first-parent-only\")) {\n     - \t\trevs->exclude_first_parent_only = 1;\n     -+\t} else if (!strcmp(arg, \"--maximal\")) {\n     -+\t\trevs->maximal = 1;\n     - \t} else if (!strcmp(arg, \"--ancestry-path\")) {\n     - \t\trevs->ancestry_path = 1;\n     - \t\trevs->simplify_history = 0;\n     +@@ revision.c: int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n     + \t\t\t\t  !!revs->reverse, \"--reverse\",\n     + \t\t\t\t  !!revs->reflog_info, \"--walk-reflogs\");\n     + \n     ++\tdie_for_incompatible_opt2(!!revs->boundary, \"--boundary\",\n     ++\t\t\t\t  !!revs->maximal_only, \"--maximal-only\");\n     ++\n     + \tif (revs->no_walk && revs->graph)\n     + \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n     + \tif (!revs->reflog_info && revs->grep_filter.use_reflog_filter)\n      @@ revision.c: enum commit_action get_commit_action(struct rev_info *revs, struct commit *commi\n       {\n       \tif (commit->object.flags & SHOWN)\n       \t\treturn commit_ignore;\n     -+\tif (revs->maximal && (commit->object.flags & CHILD_VISITED))\n     ++\tif (revs->maximal_only && (commit->object.flags & CHILD_VISITED))\n      +\t\treturn commit_ignore;\n       \tif (revs->unpacked && has_object_pack(revs->repo, &commit->object.oid))\n       \t\treturn commit_ignore;\n     @@ revision.h\n       #define DECORATE_SHORT_REFS\t1\n       #define DECORATE_FULL_REFS\t2\n      @@ revision.h: struct rev_info {\n     - \t\t\tcherry_mark:1,\n     - \t\t\tbisect:1,\n     - \t\t\tancestry_path:1,\n     -+\t\t\tmaximal:1,\n     + \t\t\tleft_right:1,\n     + \t\t\tleft_only:1,\n     + \t\t\tright_only:1,\n     ++\t\t\tmaximal_only:1,\n     + \t\t\trewrite_parents:1,\n     + \t\t\tprint_parents:1,\n     + \t\t\tshow_decorations:1,\n     +\n     + ## t/t6000-rev-list-misc.sh ##\n     +@@ t/t6000-rev-list-misc.sh: test_expect_success 'rev-list -z --boundary' '\n     + \ttest_cmp expect actual\n     + '\n       \n     - \t\t\t/* True if --ancestry-path was specified without an\n     - \t\t\t * argument. The bottom revisions are implicitly\n     ++test_expect_success 'rev-list --boundary incompatible with --maximal-only' '\n     ++\ttest_when_finished rm -rf repo &&\n     ++\n     ++\tgit init repo &&\n     ++\ttest_commit -C repo 1 &&\n     ++\ttest_commit -C repo 2 &&\n     ++\n     ++\toid1=$(git -C repo rev-parse HEAD~) &&\n     ++\toid2=$(git -C repo rev-parse HEAD) &&\n     ++\n     ++\ttest_must_fail git -C repo rev-list --boundary --maximal-only \\\n     ++\t\tHEAD~1..HEAD 2>err &&\n     ++\ttest_grep \"cannot be used together\" err\n     ++'\n     ++\n     + test_done\n      \n       ## t/t6600-test-reach.sh ##\n      @@ t/t6600-test-reach.sh: test_expect_success 'for-each-ref is-base: --sort' '\n       \t\t--sort=refname --sort=-is-base:commit-2-3\n       '\n       \n     -+test_expect_success 'rev-list --maximal (all positive)' '\n     ++test_expect_success 'rev-list --maximal-only (all positive)' '\n      +\t# Only one maximal.\n      +\tcat >input <<-\\EOF &&\n      +\trefs/heads/commit-1-1\n     @@ t/t6600-test-reach.sh: test_expect_success 'for-each-ref is-base: --sort' '\n      +\tcat >expect <<-EOF &&\n      +\t$(git rev-parse refs/heads/commit-8-4)\n      +\tEOF\n     -+\trun_all_modes git rev-list --maximal --stdin &&\n     ++\trun_all_modes git rev-list --maximal-only --stdin &&\n      +\n      +\t# All maximal.\n      +\tcat >input <<-\\EOF &&\n     @@ t/t6600-test-reach.sh: test_expect_success 'for-each-ref is-base: --sort' '\n      +\t$(git rev-parse refs/heads/commit-3-4)\n      +\t$(git rev-parse refs/heads/commit-2-5)\n      +\tEOF\n     -+\trun_all_modes git rev-list --maximal --stdin &&\n     ++\trun_all_modes git rev-list --maximal-only --stdin &&\n      +\n      +\t# Mix of both.\n      +\tcat >input <<-\\EOF &&\n     @@ t/t6600-test-reach.sh: test_expect_success 'for-each-ref is-base: --sort' '\n      +\t$(git rev-parse refs/heads/commit-5-2)\n      +\t$(git rev-parse refs/heads/commit-2-5)\n      +\tEOF\n     -+\trun_all_modes git rev-list --maximal --stdin\n     ++\trun_all_modes git rev-list --maximal-only --stdin\n      +'\n      +\n     -+test_expect_success 'rev-list --maximal (range)' '\n     ++test_expect_success 'rev-list --maximal-only (range)' '\n      +\tcat >input <<-\\EOF &&\n      +\trefs/heads/commit-1-1\n      +\trefs/heads/commit-2-5\n     @@ t/t6600-test-reach.sh: test_expect_success 'for-each-ref is-base: --sort' '\n      +\tcat >expect <<-EOF &&\n      +\t$(git rev-parse refs/heads/commit-6-4)\n      +\tEOF\n     -+\trun_all_modes git rev-list --maximal --stdin &&\n     ++\trun_all_modes git rev-list --maximal-only --stdin &&\n      +\n      +\t# first-parent changes reachability: the first parent\n      +\t# reduces the second coordinate to 1 before reducing the\n     @@ t/t6600-test-reach.sh: test_expect_success 'for-each-ref is-base: --sort' '\n      +\t$(git rev-parse refs/heads/commit-6-4)\n      +\t$(git rev-parse refs/heads/commit-2-5)\n      +\tEOF\n     -+\trun_all_modes git rev-list --maximal --stdin \\\n     ++\trun_all_modes git rev-list --maximal-only --stdin \\\n      +\t\t--first-parent --exclude-first-parent-only\n      +'\n      +\n\n\n Documentation/rev-list-options.adoc |  4 ++\n object.h                            |  4 +-\n revision.c                          | 12 ++++-\n revision.h                          |  5 +-\n t/t6000-rev-list-misc.sh            | 15 ++++++\n t/t6600-test-reach.sh               | 75 +++++++++++++++++++++++++++++\n 6 files changed, 110 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex 453ec59057..a39cf88bbc 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -148,6 +148,10 @@ endif::git-log[]\n \tfrom the point where it diverged from the remote branch, given\n \tthat arbitrary merges can be valid topic branch changes.\n \n+`--maximal-only`::\n+\tRestrict the output commits to be those that are not reachable\n+\tfrom any other commits in the revision range.\n+\n `--not`::\n \tReverses the meaning of the '{caret}' prefix (or lack thereof)\n \tfor all following revision specifiers, up to the next `--not`.\ndiff --git a/object.h b/object.h\nindex 4bca957b8d..dfe7a1f0ea 100644\n--- a/object.h\n+++ b/object.h\n@@ -64,7 +64,7 @@ void object_array_init(struct object_array *array);\n \n /*\n  * object flag allocation:\n- * revision.h:               0---------10         15               23------27\n+ * revision.h:               0---------10         15               23--------28\n  * fetch-pack.c:             01    67\n  * negotiator/default.c:       2--5\n  * walker.c:                 0-2\n@@ -86,7 +86,7 @@ void object_array_init(struct object_array *array);\n  * builtin/unpack-objects.c:                                 2021\n  * pack-bitmap.h:                                              2122\n  */\n-#define FLAG_BITS  28\n+#define FLAG_BITS  29\n \n #define TYPE_BITS 3\n \ndiff --git a/revision.c b/revision.c\nindex 1858e093ee..2dee78b838 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -1150,7 +1150,8 @@ static int process_parents(struct rev_info *revs, struct commit *commit,\n \t\t\tstruct commit *p = parent->item;\n \t\t\tparent = parent->next;\n \t\t\tif (p)\n-\t\t\t\tp->object.flags |= UNINTERESTING;\n+\t\t\t\tp->object.flags |= UNINTERESTING |\n+\t\t\t\t\t\t   CHILD_VISITED;\n \t\t\tif (repo_parse_commit_gently(revs->repo, p, 1) < 0)\n \t\t\t\tcontinue;\n \t\t\tif (p->parents)\n@@ -1204,7 +1205,7 @@ static int process_parents(struct rev_info *revs, struct commit *commit,\n \t\t\tif (!*slot)\n \t\t\t\t*slot = *revision_sources_at(revs->sources, commit);\n \t\t}\n-\t\tp->object.flags |= pass_flags;\n+\t\tp->object.flags |= pass_flags | CHILD_VISITED;\n \t\tif (!(p->object.flags & SEEN)) {\n \t\t\tp->object.flags |= (SEEN | NOT_USER_GIVEN);\n \t\t\tif (list)\n@@ -2377,6 +2378,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t} else if ((argcount = parse_long_opt(\"until\", argv, &optarg))) {\n \t\trevs->min_age = approxidate(optarg);\n \t\treturn argcount;\n+\t} else if (!strcmp(arg, \"--maximal-only\")) {\n+\t\trevs->maximal_only = 1;\n \t} else if (!strcmp(arg, \"--first-parent\")) {\n \t\trevs->first_parent_only = 1;\n \t} else if (!strcmp(arg, \"--exclude-first-parent-only\")) {\n@@ -3147,6 +3150,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \t\t\t\t  !!revs->reverse, \"--reverse\",\n \t\t\t\t  !!revs->reflog_info, \"--walk-reflogs\");\n \n+\tdie_for_incompatible_opt2(!!revs->boundary, \"--boundary\",\n+\t\t\t\t  !!revs->maximal_only, \"--maximal-only\");\n+\n \tif (revs->no_walk && revs->graph)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n \tif (!revs->reflog_info && revs->grep_filter.use_reflog_filter)\n@@ -4125,6 +4131,8 @@ enum commit_action get_commit_action(struct rev_info *revs, struct commit *commi\n {\n \tif (commit->object.flags & SHOWN)\n \t\treturn commit_ignore;\n+\tif (revs->maximal_only && (commit->object.flags & CHILD_VISITED))\n+\t\treturn commit_ignore;\n \tif (revs->unpacked && has_object_pack(revs->repo, &commit->object.oid))\n \t\treturn commit_ignore;\n \tif (revs->no_kept_objects) {\ndiff --git a/revision.h b/revision.h\nindex b36acfc2d9..69242ecb18 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -52,7 +52,9 @@\n #define NOT_USER_GIVEN\t(1u<<25)\n #define TRACK_LINEAR\t(1u<<26)\n #define ANCESTRY_PATH\t(1u<<27)\n-#define ALL_REV_FLAGS\t(((1u<<11)-1) | NOT_USER_GIVEN | TRACK_LINEAR | PULL_MERGE)\n+#define CHILD_VISITED\t(1u<<28)\n+#define ALL_REV_FLAGS\t(((1u<<11)-1) | NOT_USER_GIVEN | TRACK_LINEAR \\\n+\t\t\t\t      | PULL_MERGE | CHILD_VISITED)\n \n #define DECORATE_SHORT_REFS\t1\n #define DECORATE_FULL_REFS\t2\n@@ -189,6 +191,7 @@ struct rev_info {\n \t\t\tleft_right:1,\n \t\t\tleft_only:1,\n \t\t\tright_only:1,\n+\t\t\tmaximal_only:1,\n \t\t\trewrite_parents:1,\n \t\t\tprint_parents:1,\n \t\t\tshow_decorations:1,\ndiff --git a/t/t6000-rev-list-misc.sh b/t/t6000-rev-list-misc.sh\nindex fec16448cf..d0a2a86610 100755\n--- a/t/t6000-rev-list-misc.sh\n+++ b/t/t6000-rev-list-misc.sh\n@@ -248,4 +248,19 @@ test_expect_success 'rev-list -z --boundary' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'rev-list --boundary incompatible with --maximal-only' '\n+\ttest_when_finished rm -rf repo &&\n+\n+\tgit init repo &&\n+\ttest_commit -C repo 1 &&\n+\ttest_commit -C repo 2 &&\n+\n+\toid1=$(git -C repo rev-parse HEAD~) &&\n+\toid2=$(git -C repo rev-parse HEAD) &&\n+\n+\ttest_must_fail git -C repo rev-list --boundary --maximal-only \\\n+\t\tHEAD~1..HEAD 2>err &&\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n test_done\ndiff --git a/t/t6600-test-reach.sh b/t/t6600-test-reach.sh\nindex 6638d1aa1d..2613075894 100755\n--- a/t/t6600-test-reach.sh\n+++ b/t/t6600-test-reach.sh\n@@ -762,4 +762,79 @@ test_expect_success 'for-each-ref is-base: --sort' '\n \t\t--sort=refname --sort=-is-base:commit-2-3\n '\n \n+test_expect_success 'rev-list --maximal-only (all positive)' '\n+\t# Only one maximal.\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-1-1\n+\trefs/heads/commit-4-2\n+\trefs/heads/commit-4-4\n+\trefs/heads/commit-8-4\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-8-4)\n+\tEOF\n+\trun_all_modes git rev-list --maximal-only --stdin &&\n+\n+\t# All maximal.\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-5-2\n+\trefs/heads/commit-4-3\n+\trefs/heads/commit-3-4\n+\trefs/heads/commit-2-5\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-5-2)\n+\t$(git rev-parse refs/heads/commit-4-3)\n+\t$(git rev-parse refs/heads/commit-3-4)\n+\t$(git rev-parse refs/heads/commit-2-5)\n+\tEOF\n+\trun_all_modes git rev-list --maximal-only --stdin &&\n+\n+\t# Mix of both.\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-5-2\n+\trefs/heads/commit-3-2\n+\trefs/heads/commit-2-5\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-5-2)\n+\t$(git rev-parse refs/heads/commit-2-5)\n+\tEOF\n+\trun_all_modes git rev-list --maximal-only --stdin\n+'\n+\n+test_expect_success 'rev-list --maximal-only (range)' '\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-1-1\n+\trefs/heads/commit-2-5\n+\trefs/heads/commit-6-4\n+\t^refs/heads/commit-4-5\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-6-4)\n+\tEOF\n+\trun_all_modes git rev-list --maximal-only --stdin &&\n+\n+\t# first-parent changes reachability: the first parent\n+\t# reduces the second coordinate to 1 before reducing the\n+\t# first coordinate.\n+\tcat >input <<-\\EOF &&\n+\trefs/heads/commit-1-1\n+\trefs/heads/commit-2-5\n+\trefs/heads/commit-6-4\n+\t^refs/heads/commit-4-5\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\t$(git rev-parse refs/heads/commit-6-4)\n+\t$(git rev-parse refs/heads/commit-2-5)\n+\tEOF\n+\trun_all_modes git rev-list --maximal-only --stdin \\\n+\t\t--first-parent --exclude-first-parent-only\n+'\n+\n test_done\n\nbase-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c\n-- \ngitgitgadget\n"},{"id":"534504","messageId":"xmqqikctl3vj.fsf@gitster.g","threadId":"64828","inReplyTo":"pull.2032.v2.git.1769097958549.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T21:44:00Z","receivedAt":"2026-01-22T21:44:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>     My motivation for this feature is very similar to the bundle URI\n>     application. I can get around it by creating a tool that uses git\n>     rev-list --parents and then uses a hashset to collect the parent list\n>     and filter out any commits that ever appear as parents. It would be more\n>     efficient to use Git's native revision-walking feature.\n\nHow does this relate to \"git merge-base --independent\", or do they\ncompute completely different things?\n\n\n"},{"id":"534508","messageId":"7daff220-f93a-463a-b586-dd876b51edae@gmail.com","threadId":"64828","inReplyTo":"xmqqikctl3vj.fsf@gitster.g","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-01-22T22:15:55Z","receivedAt":"2026-01-22T22:15:59Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 1/22/2026 4:44 PM, Junio C Hamano wrote:\n> \"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>>     My motivation for this feature is very similar to the bundle URI\n>>     application. I can get around it by creating a tool that uses git\n>>     rev-list --parents and then uses a hashset to collect the parent list\n>>     and filter out any commits that ever appear as parents. It would be more\n>>     efficient to use Git's native revision-walking feature.\n> \n> How does this relate to \"git merge-base --independent\", or do they\n> compute completely different things?\n\nThis is the same idea, where among the merge bases, the ones that are\n\"maximal\" to that set are the --independent ones. \n \nThe documentation for --independent even uses similar language here:\n\n  In other words, among the commits given, list those which cannot\n  be reached from any other.\n\nUnfortunately, it also says \"print a minimal subset\" which in some\nsense is correct by \"it cannot be made smaller without losing\ninformation\" but we actually choose the maximal set there, not a\nminimal set.\n\nThe merge-base --independent calculation is basically asking for\nthe maximal set among commits in the intersection of two (or more)\ncommit histories. One trick the merge-base calculation does is\nthat it first looks for the --boundary commits, and then reduces\nfrom within that set. This avoid walking further into the history\nthan necessary.\n\nYou are presenting interesting overlaps of terminology and needs.\nOne thing that is different about 'git rev-list --maximal-only' with\na list of starting commits is that it wants the maximal set from\nthe _union_ of the histories, instead of the _intersection_ like\n'git merge-base --independent' does.\n\nThere is potential for a performance improvement if we converted\nthe search to be like a merge-base algorithm and checked the\npriority queue to see if all elements have the CHILD_VISITED flag.\nI think the cost here is that we would need more new logic and\nwould lose some expressiveness of 'git rev-list'.\n\nFor example, one of the applications I mentioned will require a\nrange request including negative refs, such as\n\n  git rev-list --maximal-only --stdin <<-\\EOF\n  refs/heads/branch1\n  refs/heads/branch2\n  ^refs/heads/main\n  ^refs/heads/release\n  EOF\n\nAnd this would likely return the tips of the two branches, but\nalso will detect if one already reaches the other or if one of\n'main' or 'release' reaches one or both of them, excluding it\nfrom the maximal set.\n\nThanks,\n-Stolee\n"},{"id":"534512","messageId":"xmqqwm19jl8v.fsf@gitster.g","threadId":"64828","inReplyTo":"7daff220-f93a-463a-b586-dd876b51edae@gmail.com","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T23:11:44Z","receivedAt":"2026-01-22T23:11:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Derrick Stolee <stolee@gmail.com> writes:\n\n> The merge-base --independent calculation is basically asking for\n> the maximal set among commits in the intersection of two (or more)\n> commit histories. One trick the merge-base calculation does is\n> that it first looks for the --boundary commits, and then reduces\n> from within that set. This avoid walking further into the history\n> than necessary.\n> ...\n> And this would likely return the tips of the two branches, but\n> also will detect if one already reaches the other or if one of\n> 'main' or 'release' reaches one or both of them, excluding it\n> from the maximal set.\n\nThanks for your thoughts.\n"},{"id":"534524","messageId":"13ff1d94-401e-4fa7-b247-fe8396ca9970@kdbg.org","threadId":"64828","inReplyTo":"7daff220-f93a-463a-b586-dd876b51edae@gmail.com","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-01-23T06:38:08Z","receivedAt":"2026-01-23T06:38:19Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 22.01.26 um 23:15 schrieb Derrick Stolee:\n> Unfortunately, it also says \"print a minimal subset\" which in some\n> sense is correct by \"it cannot be made smaller without losing\n> information\" but we actually choose the maximal set there, not a\n> minimal set.\n> ...\n> You are presenting interesting overlaps of terminology and needs.\n> One thing that is different about 'git rev-list --maximal-only' with\n> a list of starting commits is that it wants the maximal set from\n> the _union_ of the histories, instead of the _intersection_ like\n> 'git merge-base --independent' does.\n\nI don't quite understand how a union or intersection come into play\nhere. The difference between the two is that `git rev-list\n--maximal-only` permits negative revisions as input, but `git merge-base\n--independent` does not. In the case where the input is only positive\nrevisions, the result of --maximal-only should always be exactly\nidentical to --independent, right? Even if the revisions are on\ndisconnected histories?\n\n-- Hannes\n\n"},{"id":"534554","messageId":"xmqqecngjp87.fsf@gitster.g","threadId":"64828","inReplyTo":"13ff1d94-401e-4fa7-b247-fe8396ca9970@kdbg.org","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-23T15:58:00Z","receivedAt":"2026-01-23T15:58:03Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Johannes Sixt <j6t@kdbg.org> writes:\n\n> Am 22.01.26 um 23:15 schrieb Derrick Stolee:\n>> Unfortunately, it also says \"print a minimal subset\" which in some\n>> sense is correct by \"it cannot be made smaller without losing\n>> information\" but we actually choose the maximal set there, not a\n>> minimal set.\n>> ...\n>> You are presenting interesting overlaps of terminology and needs.\n>> One thing that is different about 'git rev-list --maximal-only' with\n>> a list of starting commits is that it wants the maximal set from\n>> the _union_ of the histories, instead of the _intersection_ like\n>> 'git merge-base --independent' does.\n>\n> I don't quite understand how a union or intersection come into play\n> here. The difference between the two is that `git rev-list\n> --maximal-only` permits negative revisions as input, but `git merge-base\n> --independent` does not. In the case where the input is only positive\n> revisions, the result of --maximal-only should always be exactly\n> identical to --independent, right? Even if the revisions are on\n> disconnected histories?\n\nAhh, it is an ancient history that I forgot how the command worked.\n\"merge-base --independent A B C\" does not do any \"merge-base\"\ncomputation over the commits A B C and shows the ones that cannot be\nreached from any other.  If it were to compute merge bases across\nthese commits and then find commits, among the computed merge bases,\nthat cannot be reached from any other merge bases, \"intersection\"\nmight come into play, but I do not think that is what the command\ndoes.\n"},{"id":"534562","messageId":"f363c16c-1c36-4485-b1e9-22abe32b3a25@gmail.com","threadId":"64828","inReplyTo":"xmqqecngjp87.fsf@gitster.g","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-01-23T16:55:49Z","receivedAt":"2026-01-23T16:55:51Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 1/23/2026 10:58 AM, Junio C Hamano wrote:\n> Johannes Sixt <j6t@kdbg.org> writes:\n> \n>> Am 22.01.26 um 23:15 schrieb Derrick Stolee:\n>>> Unfortunately, it also says \"print a minimal subset\" which in some\n>>> sense is correct by \"it cannot be made smaller without losing\n>>> information\" but we actually choose the maximal set there, not a\n>>> minimal set.\n>>> ...\n>>> You are presenting interesting overlaps of terminology and needs.\n>>> One thing that is different about 'git rev-list --maximal-only' with\n>>> a list of starting commits is that it wants the maximal set from\n>>> the _union_ of the histories, instead of the _intersection_ like\n>>> 'git merge-base --independent' does.\n>>\n>> I don't quite understand how a union or intersection come into play\n>> here. The difference between the two is that `git rev-list\n>> --maximal-only` permits negative revisions as input, but `git merge-base\n>> --independent` does not. In the case where the input is only positive\n>> revisions, the result of --maximal-only should always be exactly\n>> identical to --independent, right? Even if the revisions are on\n>> disconnected histories?\n> \n> Ahh, it is an ancient history that I forgot how the command worked.\n> \"merge-base --independent A B C\" does not do any \"merge-base\"\n> computation over the commits A B C and shows the ones that cannot be\n> reached from any other.  If it were to compute merge bases across\n> these commits and then find commits, among the computed merge bases,\n> that cannot be reached from any other merge bases, \"intersection\"\n> might come into play, but I do not think that is what the command\n> does.\n\nInteresting. Thanks for the correction. So we _do_ have a way to\nget this information for a range that doesn't have negative refs\nor other custom walk modifiers (and this implementation would be\nfaster for this case).\n\nMy patch includes test cases that are not covered by the\nmerge-base command. I don't think it would be valuable to extend\nthe merge-base command with even more cases that don't actually\noutput merge-bases / intersections.\n\nThanks,\n-Stolee\n\n"},{"id":"534575","messageId":"xmqqfr7wgq1p.fsf@gitster.g","threadId":"64828","inReplyTo":"f363c16c-1c36-4485-b1e9-22abe32b3a25@gmail.com","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-23T18:08:34Z","receivedAt":"2026-01-23T18:08:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Derrick Stolee <stolee@gmail.com> writes:\n\n> Interesting. Thanks for the correction. So we _do_ have a way to\n> get this information for a range that doesn't have negative refs\n> or other custom walk modifiers (and this implementation would be\n> faster for this case).\n\nPerhaps.  If so, perhaps we can improve --maximal-only (and possibly\nrename it to --independent?  I dunno about this part) by special\ncasing the logic, and then steer people to use the new implementation\nthat can use negative ends, deprecating \"merge-base --independent\"\n(which was written to be a better \"show-branch --independent\")?\n\n> My patch includes test cases that are not covered by the\n> merge-base command. I don't think it would be valuable to extend\n> the merge-base command with even more cases that don't actually\n> output merge-bases / intersections.\n\nYup, I do not think show-branch nor merge-base were good home for\nthe feature.  We only needed to make reduce_heads_replace()\navailable somewhere, and \"git show --maximal-only A B C\" might be a\nmuch better way to express \"show only the independent ones\", as it\nwould allow using all kinds of output options the \"log\" family of\ncommands support.\n\n"},{"id":"534763","messageId":"c506f9aa-31c9-4c37-98eb-d60076e2e8f5@gmail.com","threadId":"64828","inReplyTo":"xmqqfr7wgq1p.fsf@gitster.g","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-01-28T14:28:49Z","receivedAt":"2026-01-28T14:28:51Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 1/23/26 1:08 PM, Junio C Hamano wrote:\n> Derrick Stolee <stolee@gmail.com> writes:\n> \n>> Interesting. Thanks for the correction. So we _do_ have a way to\n>> get this information for a range that doesn't have negative refs\n>> or other custom walk modifiers (and this implementation would be\n>> faster for this case).\n> \n> Perhaps.  If so, perhaps we can improve --maximal-only (and possibly\n> rename it to --independent?  I dunno about this part) by special\n> casing the logic, and then steer people to use the new implementation\n> that can use negative ends, deprecating \"merge-base --independent\"\n> (which was written to be a better \"show-branch --independent\")?\n> \n>> My patch includes test cases that are not covered by the\n>> merge-base command. I don't think it would be valuable to extend\n>> the merge-base command with even more cases that don't actually\n>> output merge-bases / intersections.\n> \n> Yup, I do not think show-branch nor merge-base were good home for\n> the feature.  We only needed to make reduce_heads_replace()\n> available somewhere, and \"git show --maximal-only A B C\" might be a\n> much better way to express \"show only the independent ones\", as it\n> would allow using all kinds of output options the \"log\" family of\n> commands support.\n\nI explored some of these directions, and I see the value of allowing\na --maximal-only option to them in the future. I have some concerns\nabout them not solving the needs I have that this 'git rev-list'\nimplementation provides. I believe that you're suggesting that these\nare other places where a user could benefit from such an option, and\nI agree.\n\nCan we delay such extensions to another series?\n\nThanks,\n-Stolee\n\n"},{"id":"534795","messageId":"xmqqqzr9cm28.fsf@gitster.g","threadId":"64828","inReplyTo":"c506f9aa-31c9-4c37-98eb-d60076e2e8f5@gmail.com","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-29T00:14:07Z","receivedAt":"2026-01-29T00:14:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Derrick Stolee <stolee@gmail.com> writes:\n\n>> Yup, I do not think show-branch nor merge-base were good home for\n>> the feature.  We only needed to make reduce_heads_replace()\n>> available somewhere, and \"git show --maximal-only A B C\" might be a\n>> much better way to express \"show only the independent ones\", as it\n>> would allow using all kinds of output options the \"log\" family of\n>> commands support.\n>\n> I explored some of these directions, and I see the value of allowing\n> a --maximal-only option to them in the future. I have some concerns\n> about them not solving the needs I have that this 'git rev-list'\n> implementation provides. I believe that you're suggesting that these\n> are other places where a user could benefit from such an option, and\n> I agree.\n>\n> Can we delay such extensions to another series?\n\nAbsolutely, as long as we all agree on what the longer term\ndirection is, which includes educating existing users of\n\"show-branch --independent\" and \"merge-base --independent\" that\n\"rev-list --maximal-only\" is the future even for their \"positive end\nonly\" use cases and it also can work on a history bounded by both\npositive and negative ends.\n\nThe only small thing we need to decide here in the above is that\n\"--maximal-only\" is understandable as an appropriate name for a\nsuperset of \"--independent\" by those who are used to what the\nlatter has been doing for the past 15 years or so.\n\nAs long as with such understanding, it can be left to the future to\neven advertise this option as a better alternative for existing\n\"--independent\" option in the manual pages of these other two\ncommands.\n\nTHanks.\n\n"},{"id":"534813","messageId":"9193fab6-f7e9-41ab-bf76-c868feb86db1@gmail.com","threadId":"64828","inReplyTo":"xmqqqzr9cm28.fsf@gitster.g","subject":"Re: [PATCH v2] revision: add --maximal-only option","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-01-29T14:57:56Z","receivedAt":"2026-01-29T14:57:58Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 1/28/2026 7:14 PM, Junio C Hamano wrote:\n> Derrick Stolee <stolee@gmail.com> writes:\n> \n>>> Yup, I do not think show-branch nor merge-base were good home for\n>>> the feature.  We only needed to make reduce_heads_replace()\n>>> available somewhere, and \"git show --maximal-only A B C\" might be a\n>>> much better way to express \"show only the independent ones\", as it\n>>> would allow using all kinds of output options the \"log\" family of\n>>> commands support.\n>>\n>> I explored some of these directions, and I see the value of allowing\n>> a --maximal-only option to them in the future. I have some concerns\n>> about them not solving the needs I have that this 'git rev-list'\n>> implementation provides. I believe that you're suggesting that these\n>> are other places where a user could benefit from such an option, and\n>> I agree.\n>>\n>> Can we delay such extensions to another series?\n> \n> Absolutely, as long as we all agree on what the longer term\n> direction is, which includes educating existing users of\n> \"show-branch --independent\" and \"merge-base --independent\" that\n> \"rev-list --maximal-only\" is the future even for their \"positive end\n> only\" use cases and it also can work on a history bounded by both\n> positive and negative ends.\n\nI agree to this direction and have some drafts in this direction.\n\n> The only small thing we need to decide here in the above is that\n> \"--maximal-only\" is understandable as an appropriate name for a\n> superset of \"--independent\" by those who are used to what the\n> latter has been doing for the past 15 years or so.\n\nMy strong preference for using the word \"maximal\" somewhere over\ncontinued reliance on \"independent\" is that a set of commits can\nbe \"mutually independent\" without any of the commits actually\nbeing maximal within the range.\n\nHere's an example:\n\n    A       B\n    |\\     /|\n    D E   F G\n     \\ \\ / /\n      H I J\n       \\|/\n        K\n\nIn this commit history graph, each row is an \"independent\" set of\ncommits:\n\n\t{ A, B }\n\t{ D, E, F, G }\n\t{ H, I, J }\n\t{ K }\n\nand some sets like { A, F, J } are also independent.\n\nOnly A and B are \"maximal\" commits within the history.\n\nI describe this through an example mostly because I don't feel that\nI've adequately described this distinction in this thread and\nwould not feel satisfied in my arguments without it.\n\nWith my reasoning more completely described, I am more ready for\nsomeone to overrule my opinion with the argument that \"independent\"\nhas enough historical context to mean \"a maximal independent set\".\n\n> As long as with such understanding, it can be left to the future to\n> even advertise this option as a better alternative for existing\n> \"--independent\" option in the manual pages of these other two\n> commands.\n\nThe other, more complicated, task is to have the rev-list command\nuse the algorithm that backs 'git merge-base --independent' when\nthe input range and options is appropriate for that purpose. This\nperformance-only feature will require more careful construction\nand review.\n\nThanks,\n-Stolee\n\n"}]}