{"thread":{"id":"65183","subject":"[PATCH RFC] git-replay: implement subcommands","startedAt":"2026-03-09T19:31:13Z","lastAt":"2026-03-14T07:18:59Z","messageCount":4,"participants":["Toon Claes","Justin Tobler","Siddharth Asthana"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"538315","messageId":"20260309-toon-replay-subcommands-v1-1-864ec82ef68a@iotcl.com","threadId":"65183","inReplyTo":null,"subject":"[PATCH RFC] git-replay: implement subcommands","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-09T19:30:57Z","receivedAt":"2026-03-09T19:31:13Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"git-replay(1) has various operation modes. The mode depends on which of\nthe options `--onto`, `--advance`, or `--revert` is given. These options\nare mutually exclusive. This usage pattern is counterintuitive and\nuncommon for Git commands to behave this way.\n\nImplement subcommands into git-replay(1):\n\n* `rebase`: This replaces what `--onto=` used to do.\n* `pick`: This replaces what `--advance=` used to do.\n* `revert`: This replaces what `--revert=` used to do.\n\nOption `--onto` is still accepted. It's mandatory for the `rebase`\nsubcommand and needs to be used in the exact same way.\n\nOption `--ref` is added and required for the `pick` and `revert`\nsubcommands. This replaces what `--advance` and `--revert` used to do,\nbut as a single uniform option for all subcommands.\n\nThe `rebase` subcommand also accepts option `--ref`, and when given this\nis the ref that's updated with the outcome of the git-replay(1) command.\nThus following commands are identical:\n\n    $ git replay rebase --onto=master master..branch-1\n\n    $ git replay rebase --onto=master master..branch-1^{0} --ref=refs/heads/branch-1\n\nIn the second example the upper boundary of the revision range is peeled\ndown to a commit (using '^{0}'). Without option `--ref`, git-replay(1)\ndoesn't know which ref to update, that's why `--ref` is passed\nexplicitly.\n\nFor the subcommands `pick` and `revert` it's also possible to combine\n`--ref` and `--onto`. Here are again two identical examples:\n\n    $ git replay pick --onto=branch-1 master..aabbccdd\n\n    $ git replay pick --onto=branch-1^{0} master..aabbccdd --ref=refs/heads/branch-1\n\nIn the latter the argument for `--onto` is peeled down to a commit, so\nthe command doesn't know which ref to update. To inform git-replay(1)\nwhich refs should be updated, it's passed explicitly as option `--ref`.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\nIn the patch series by Siddharth Asthana[1] the option `--revert` is\nadded to git-replay(1). This is implemented as option `--revert`, next\nto the existing options `--advance` and `--onto`.\n\nThe usage of these options is mutually exclusive, so the user can only\nuse one of them, and depending on which one, git-replay(1) selects a\n\"mode of operating\".\n\nVarious people have raised this behavior is somewhat confusing. In this\nseries we attempt to make the usage of git-replay(1) more intuitive and\nuser-friendly by implementing the modes as subcommands.\n\nThis patch is submitted as an RFC to gather feedback about the design.\nAll changes are implemented as a single patch right now, and thus\nreviewing the changes might be challenging. When we got people aligned\non the direction, I'll work toward cleaner patches.\n\nThese changes are based on 'master' at 864f55e190 (The second batch,\n2026-02-09) with the patches of Siddharth[1] applied: 'sa/replay-revert'\nat f79189a653 (replay: add --revert mode to reverse commit changes,\n2026-02-19)\n\n[1]: 20260218234215.89326-3-siddharthasthana31@gmail.com\n---\n Documentation/git-replay.adoc | 124 ++++++++++++++++----------\n builtin/replay.c              | 150 ++++++++++++++++++++++++-------\n replay.c                      |  66 +++++++-------\n replay.h                      |  31 +++----\n t/t3650-replay-basics.sh      | 199 +++++++++++++++++++++++-------------------\n 5 files changed, 349 insertions(+), 221 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex ffdf790278..a7e8dac23f 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -8,8 +8,13 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n \n SYNOPSIS\n --------\n-[verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n+[synopsis]\n+git replay rebase --onto <newbase> [--ref <branch>] [--contained]\n+\t\t[--ref-action[=<mode>]] <revision-range>\n+git replay pick --ref <branch> [--onto <newbase>]\n+\t\t[--ref-action[=<mode>]] <revision-range>\n+git replay revert --ref <branch> [--onto <newbase>]\n+\t\t[--ref-action[=<mode>]] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -23,49 +28,62 @@ instead get update commands that can be piped to `git update-ref --stdin`\n \n THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n \n-OPTIONS\n--------\n-\n---onto <newbase>::\n-\tStarting point at which to create the new commits.  May be any\n-\tvalid commit, and not just an existing branch name.\n-+\n-When `--onto` is specified, the branch(es) in the revision range will be\n-updated to point at the new commits, similar to the way `git rebase --update-refs`\n-updates multiple branches in the affected range.\n-\n---advance <branch>::\n-\tStarting point at which to create the new commits; must be a\n-\tbranch name.\n-+\n-The history is replayed on top of the <branch> and <branch> is updated to\n-point at the tip of the resulting history. This is different from `--onto`,\n-which uses the target only as a starting point without updating it.\n+SUBCOMMANDS\n+-----------\n \n---revert <branch>::\n-\tStarting point at which to create the reverted commits; must be a\n-\tbranch name.\n-+\n-When `--revert` is specified, the commits in the revision range are reverted\n-(their changes are undone) and the reverted commits are created on top of\n-<branch>. The <branch> is then updated to point at the new commits. This is\n-the same as running `git revert <revision-range>` but does not update the\n-working tree.\n+`rebase`::\n+\tReplay commits onto a new base, similar to `git rebase`. The\n+\t`--onto` option is required to specify the new base. When `--ref`\n+\tis not given, the branch(es) in the revision range will be updated\n+\tto point at the new commits, similar to the way\n+\t`git rebase --update-refs` updates multiple branches in the\n+\taffected range. When `--ref` is given, only that reference is\n+\tupdated.\n+\n+`pick`::\n+\tCherry-pick commits onto a branch. The `--ref` option is required\n+\tto specify which branch to update. The history is replayed on top\n+\tof the commit pointed to by `--ref` (or `--onto` if given) and\n+\t`--ref` is updated to point at the tip of the resulting history.\n+\n+`revert`::\n+\tRevert commits onto a branch. The `--ref` option is required to\n+\tspecify which branch to update. The commits in the revision range\n+\tare reverted (their changes are undone) and the reverted commits\n+\tare created on top of the commit pointed to by `--ref` (or `--onto`\n+\tif given). The `--ref` is then updated to point at the new commits.\n +\n The commit messages follow `git revert` conventions: they are prefixed with\n \"Revert\" and include \"This reverts commit <hash>.\" When reverting a commit\n whose message starts with \"Revert\", the new message uses \"Reapply\" instead.\n Unlike cherry-pick which preserves the original author, revert commits use\n the current user as the author, matching the behavior of `git revert`.\n+\n+OPTIONS\n+-------\n+\n+`--onto=<newbase>`::\n+\tStarting point at which to create the new commits. May be any\n+\tvalid commit, and not just an existing branch name.\n++\n+For the `rebase` subcommand, this option is required.\n+For the `pick` and `revert` subcommands, this option is optional; when\n+omitted, commits are replayed on top of the commit pointed to by `--ref`.\n+\n+`--ref=<branch>`::\n+\tReference to update with the result of the replay. Must be a\n+\tvalid reference name.\n +\n-This option is mutually exclusive with `--onto` and `--advance`. It is also\n-incompatible with `--contained` (which is a modifier for `--onto` only).\n+For the `pick` and `revert` subcommands, this option is required.\n+For the `rebase` subcommand, this option is optional; when omitted, refs\n+are inferred from the revision range.\n \n---contained::\n+`--contained`::\n \tUpdate all branches that point at commits in\n-\t<revision-range>. Requires `--onto`.\n+\t<revision-range>. Only valid for the `rebase` subcommand when\n+\t`--ref` is not set.\n \n---ref-action[=<mode>]::\n+`--ref-action[=<mode>]`::\n \tControl how references are updated. The mode can be:\n +\n --\n@@ -77,11 +95,11 @@ incompatible with `--contained` (which is a modifier for `--onto` only).\n +\n The default mode can be configured via the `replay.refAction` configuration variable.\n \n-<revision-range>::\n+`<revision-range>`::\n \tRange of commits to replay; see \"Specifying Ranges\" in\n-\tlinkgit:git-rev-parse[1]. In `--advance <branch>` mode, the\n+\tlinkgit:git-rev-parse[1]. In `pick` and `revert` modes, the\n \trange should have a single tip, so that it's clear to which tip the\n-\tadvanced <branch> should point. Any commits in the range whose\n+\tupdated `--ref` should point. Any commits in the range whose\n \tchanges are already present in the branch the commits are being\n \treplayed onto will be dropped.\n \n@@ -103,10 +121,10 @@ When using `--ref-action=print`, the output is usable as input to\n \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n \n where the number of refs updated depends on the arguments passed and\n-the shape of the history being replayed.  When using `--advance` or\n-`--revert`, the number of refs updated is always one, but for `--onto`,\n-it can be one or more (rebasing multiple branches simultaneously is\n-supported).\n+the shape of the history being replayed.  When using `pick` or\n+`revert`, the number of refs updated is always one, but for `rebase`\n+without `--ref`, it can be one or more (rebasing multiple branches\n+simultaneously is supported).\n \n There is no stderr output on conflicts; see the <<exit-status,EXIT\n STATUS>> section below.\n@@ -126,7 +144,7 @@ EXAMPLES\n To simply rebase `mybranch` onto `target`:\n \n ------------\n-$ git replay --onto target origin/main..mybranch\n+$ git replay rebase --onto target origin/main..mybranch\n ------------\n \n The refs are updated atomically and no output is produced on success.\n@@ -134,14 +152,14 @@ The refs are updated atomically and no output is produced on success.\n To see what would be updated without actually updating:\n \n ------------\n-$ git replay --ref-action=print --onto target origin/main..mybranch\n+$ git replay rebase --ref-action=print --onto target origin/main..mybranch\n update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n ------------\n \n To cherry-pick the commits from mybranch onto target:\n \n ------------\n-$ git replay --advance target origin/main..mybranch\n+$ git replay pick --ref target origin/main..mybranch\n ------------\n \n Note that the first two examples replay the exact same commits and on\n@@ -153,18 +171,18 @@ What if you have a stack of branches, one depending upon another, and\n you'd really like to rebase the whole set?\n \n ------------\n-$ git replay --contained --onto origin/main origin/main..tipbranch\n+$ git replay rebase --contained --onto origin/main origin/main..tipbranch\n ------------\n \n All three branches (`branch1`, `branch2`, and `tipbranch`) are updated\n atomically.\n \n-When calling `git replay`, one does not need to specify a range of\n+When calling `git replay rebase`, one does not need to specify a range of\n commits to replay using the syntax `A..B`; any range expression will\n do:\n \n ------------\n-$ git replay --onto origin/main ^base branch1 branch2 branch3\n+$ git replay rebase --onto origin/main ^base branch1 branch2 branch3\n ------------\n \n This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\n@@ -175,12 +193,22 @@ that they have in common, but that does not need to be the case.\n To revert commits on a branch:\n \n ------------\n-$ git replay --revert main main~2..main\n+$ git replay revert --ref main main~2..main\n ------------\n \n This reverts the last two commits on `main`, creating two revert commits\n on top of `main`, and updates `main` to point at the result.\n \n+To rebase onto a specific commit while updating a named ref:\n+\n+------------\n+$ git replay rebase --ref refs/heads/mybranch --onto 112233 aabbcc..ddeeff\n+------------\n+\n+This replays the range `aabbcc..ddeeff` onto commit `112233` and updates\n+`refs/heads/mybranch` to point at the result. This is useful when you want\n+to use bare commit IDs instead of branch names.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 28ce5196db..b7bf64821e 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -13,6 +13,24 @@\n #include \"replay.h\"\n #include \"revision.h\"\n \n+#define REBASE_USAGE \\\n+\tN_(\"git replay rebase --onto <newbase> [--ref <branch>] [--contained]\\n\" \\\n+\t\t\"           [--ref-action[=<mode>]] <revision-range>\")\n+\n+#define PICK_USAGE \\\n+\tN_(\"git replay pick --ref <branch> [--onto <newbase>]\\n\" \\\n+\t\t\"           [--ref-action[=<mode>]] <revision-range>\")\n+\n+#define REVERT_USAGE \\\n+\tN_(\"git replay revert --ref <branch> [--onto <newbase>]\\n\" \\\n+\t\t\"           [--ref-action[=<mode>]] <revision-range>\")\n+\n+enum replay_subcommand {\n+\tREPLAY_SUBCMD_REBASE,\n+\tREPLAY_SUBCMD_PICK,\n+\tREPLAY_SUBCMD_REVERT,\n+};\n+\n enum ref_action_mode {\n \tREF_ACTION_UPDATE,\n \tREF_ACTION_PRINT,\n@@ -66,10 +84,9 @@ static int handle_ref_update(enum ref_action_mode mode,\n \t}\n }\n \n-int cmd_replay(int argc,\n-\t       const char **argv,\n-\t       const char *prefix,\n-\t       struct repository *repo)\n+static int run_replay(int argc, const char **argv, const char *prefix,\n+\t\t      struct repository *repo,\n+\t\t      enum replay_subcommand subcommand)\n {\n \tstruct replay_revisions_options opts = { 0 };\n \tstruct replay_result result = { 0 };\n@@ -81,45 +98,71 @@ int cmd_replay(int argc,\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n \tint ret = 0;\n \n-\tconst char *const replay_usage[] = {\n-\t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n-\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n+\tconst char *const rebase_usage[] = {\n+\t\tREBASE_USAGE,\n \t\tNULL\n \t};\n+\tconst char *const pick_usage[] = {\n+\t\tPICK_USAGE,\n+\t\tNULL\n+\t};\n+\tconst char *const revert_usage[] = {\n+\t\tREVERT_USAGE,\n+\t\tNULL\n+\t};\n+\n+\tconst char *const *usage;\n+\n \tstruct option replay_options[] = {\n-\t\tOPT_STRING(0, \"advance\", &opts.advance,\n-\t\t\t   N_(\"branch\"),\n-\t\t\t   N_(\"make replay advance given branch\")),\n \t\tOPT_STRING(0, \"onto\", &opts.onto,\n \t\t\t   N_(\"revision\"),\n-\t\t\t   N_(\"replay onto given commit\")),\n-\t\tOPT_BOOL(0, \"contained\", &opts.contained,\n-\t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n-\t\tOPT_STRING(0, \"revert\", &opts.revert,\n+\t\t\t   N_(\"starting point for new commits\")),\n+\t\tOPT_STRING(0, \"ref\", &opts.ref,\n \t\t\t   N_(\"branch\"),\n-\t\t\t   N_(\"revert commits onto given branch\")),\n+\t\t\t   N_(\"reference to update with result\")),\n+\t\tOPT_BOOL(0, \"contained\", &opts.contained,\n+\t\t\t N_(\"update all branches contained in <revision-range>\")),\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n \t\t\t   N_(\"control ref update behavior (update|print)\")),\n \t\tOPT_END()\n \t};\n \n-\targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n+\tswitch (subcommand) {\n+\tcase REPLAY_SUBCMD_REBASE:\n+\t\tusage = rebase_usage;\n+\t\tbreak;\n+\tcase REPLAY_SUBCMD_PICK:\n+\t\tusage = pick_usage;\n+\t\tbreak;\n+\tcase REPLAY_SUBCMD_REVERT:\n+\t\tusage = revert_usage;\n+\t\topts.revert = 1;\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"unknown subcommand %d\", subcommand);\n+\t}\n+\n+\targc = parse_options(argc, argv, prefix, replay_options, usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n-\t/* Exactly one mode must be specified */\n-\tif (!opts.onto && !opts.advance && !opts.revert) {\n-\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n-\t\tusage_with_options(replay_usage, replay_options);\n+\tif (subcommand == REPLAY_SUBCMD_REBASE) {\n+\t\tif (!opts.onto) {\n+\t\t\terror(_(\"--onto is required for 'rebase' subcommand\"));\n+\t\t\tusage_with_options(usage, replay_options);\n+\t\t}\n+\t\tif (opts.contained && opts.ref)\n+\t\t\tdie(_(\"--contained and --ref cannot be used together\"));\n+\t} else {\n+\t\tif (!opts.ref) {\n+\t\t\terror(_(\"--ref is required for '%s' subcommand\"),\n+\t\t\t      subcommand == REPLAY_SUBCMD_PICK ? \"pick\" : \"revert\");\n+\t\t\tusage_with_options(usage, replay_options);\n+\t\t}\n+\t\tif (opts.contained)\n+\t\t\tdie(_(\"--contained can only be used with the 'rebase' subcommand\"));\n \t}\n \n-\tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n-\t\t\t\t  !!opts.advance, \"--advance\",\n-\t\t\t\t  !!opts.revert, \"--revert\");\n-\tif (opts.contained && !opts.onto)\n-\t\tdie(_(\"--contained requires --onto\"));\n-\n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n \n@@ -179,15 +222,17 @@ int cmd_replay(int argc,\n \t\tgoto cleanup;\n \n \t/* Build reflog message */\n-\tif (opts.revert) {\n-\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", opts.revert);\n-\t} else if (opts.advance) {\n-\t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n+\tif (opts.ref) {\n+\t\tstrbuf_addf(&reflog_msg, \"replay %s %s\",\n+\t\t\t    subcommand == REPLAY_SUBCMD_REVERT ? \"revert\" :\n+\t\t\t    subcommand == REPLAY_SUBCMD_PICK   ? \"pick\" :\n+\t\t\t\t\t\t\t         \"rebase\",\n+\t\t\t    opts.ref);\n \t} else {\n \t\tstruct object_id oid;\n \t\tif (repo_get_oid_committish(repo, opts.onto, &oid))\n \t\t\tBUG(\"--onto commit should have been resolved beforehand already\");\n-\t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\", oid_to_hex(&oid));\n+\t\tstrbuf_addf(&reflog_msg, \"replay rebase --onto %s\", oid_to_hex(&oid));\n \t}\n \n \t/* Initialize ref transaction if using update mode */\n@@ -236,3 +281,44 @@ int cmd_replay(int argc,\n \t\texit(128);\n \treturn ret;\n }\n+\n+static int cmd_replay_rebase(int argc, const char **argv,\n+\t\t\t     const char *prefix, struct repository *repo)\n+{\n+\treturn run_replay(argc, argv, prefix, repo, REPLAY_SUBCMD_REBASE);\n+}\n+\n+static int cmd_replay_pick(int argc, const char **argv,\n+\t\t\t   const char *prefix, struct repository *repo)\n+{\n+\treturn run_replay(argc, argv, prefix, repo, REPLAY_SUBCMD_PICK);\n+}\n+\n+static int cmd_replay_revert(int argc, const char **argv,\n+\t\t\t     const char *prefix, struct repository *repo)\n+{\n+\treturn run_replay(argc, argv, prefix, repo, REPLAY_SUBCMD_REVERT);\n+}\n+\n+int cmd_replay(int argc,\n+\t       const char **argv,\n+\t       const char *prefix,\n+\t       struct repository *repo)\n+{\n+\tconst char *const usage[] = {\n+\t\tREBASE_USAGE,\n+\t\tPICK_USAGE,\n+\t\tREVERT_USAGE,\n+\t\tNULL\n+\t};\n+\tparse_opt_subcommand_fn *fn = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"rebase\", &fn, cmd_replay_rebase),\n+\t\tOPT_SUBCOMMAND(\"pick\", &fn, cmd_replay_pick),\n+\t\tOPT_SUBCOMMAND(\"revert\", &fn, cmd_replay_revert),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\treturn fn(argc, argv, prefix, repo);\n+}\ndiff --git a/replay.c b/replay.c\nindex 6f8b5720b3..9d02f2bcc9 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -138,15 +138,15 @@ static void get_ref_information(struct repository *repo,\n \n \t/*\n \t * When the user specifies e.g.\n-\t *   git replay origin/main..mybranch\n-\t *   git replay ^origin/next mybranch1 mybranch2\n+\t *   git replay rebase --onto origin/main origin/main..mybranch\n+\t *   git replay rebase --onto origin/next ^origin/next mybranch1 mybranch2\n \t * we want to be able to determine where to replay the commits.  In\n \t * these examples, the branches are probably based on an old version\n \t * of either origin/main or origin/next, so we want to replay on the\n \t * newest version of that branch.  In contrast we would want to error\n \t * out if they ran\n-\t *   git replay ^origin/master ^origin/next mybranch\n-\t *   git replay mybranch~2..mybranch\n+\t *   git replay rebase --onto origin/main ^origin/master ^origin/next mybranch\n+\t *   git replay rebase --onto target mybranch~2..mybranch\n \t * the first of those because there's no unique base to choose, and\n \t * the second because they'd likely just be replaying commits on top\n \t * of the same commit and not making any difference.\n@@ -182,7 +182,6 @@ static void get_ref_information(struct repository *repo,\n \n static void set_up_branch_mode(struct repository *repo,\n \t\t\t       char **branch_name,\n-\t\t\t       const char *option_name,\n \t\t\t       struct ref_info *rinfo,\n \t\t\t       struct commit **onto)\n {\n@@ -194,21 +193,20 @@ static void set_up_branch_mode(struct repository *repo,\n \t\tfree(*branch_name);\n \t\t*branch_name = fullname;\n \t} else {\n-\t\tdie(_(\"argument to %s must be a reference\"), option_name);\n+\t\tdie(_(\"argument to --ref must be a reference\"));\n \t}\n-\t*onto = peel_committish(repo, *branch_name, option_name);\n+\t*onto = peel_committish(repo, *branch_name, \"--ref\");\n \tif (rinfo->positive_refexprs > 1)\n-\t\tdie(_(\"cannot %s target with multiple sources because ordering would be ill-defined\"),\n-\t\t    option_name + 2); /* skip \"--\" prefix */\n+\t\tdie(_(\"cannot replay target with multiple sources because ordering would be ill-defined\"));\n }\n \n static void set_up_replay_mode(struct repository *repo,\n \t\t\t       struct rev_cmdline_info *cmd_info,\n-\t\t\t       const char *onto_name,\n+\t\t\t       struct replay_revisions_options *opts,\n \t\t\t       bool *detached_head,\n-\t\t\t       char **advance_name,\n-\t\t\t       char **revert_name,\n+\t\t\t       char **ref_name,\n \t\t\t       struct commit **onto,\n+\t\t\t       struct object_id *ref_old_oid,\n \t\t\t       struct strset **update_refs)\n {\n \tstruct ref_info rinfo;\n@@ -222,20 +220,19 @@ static void set_up_replay_mode(struct repository *repo,\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n \n-\tif (onto_name) {\n-\t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n+\tif (!opts->ref) {\n+\t\t*onto = peel_committish(repo, opts->onto, \"--onto\");\n \t\tif (rinfo.positive_refexprs <\n \t\t    strset_get_size(&rinfo.positive_refs))\n \t\t\tdie(_(\"all positive revisions given must be references\"));\n \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n \t\t**update_refs = rinfo.positive_refs;\n \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n-\t} else if (*advance_name) {\n-\t\tset_up_branch_mode(repo, advance_name, \"--advance\", &rinfo, onto);\n-\t} else if (*revert_name) {\n-\t\tset_up_branch_mode(repo, revert_name, \"--revert\", &rinfo, onto);\n \t} else {\n-\t\tBUG(\"expected one of onto_name, *advance_name, or *revert_name\");\n+\t\tset_up_branch_mode(repo, ref_name, &rinfo, onto);\n+\t\toidcpy(ref_old_oid, &(*onto)->object.oid);\n+\t\tif (opts->onto)\n+\t\t\t*onto = peel_committish(repo, opts->onto, \"--onto\");\n \t}\n \tstrset_clear(&rinfo.negative_refs);\n \tstrset_clear(&rinfo.positive_refs);\n@@ -344,19 +341,18 @@ int replay_revisions(struct rev_info *revs,\n \t\t.clean = 1,\n \t};\n \tbool detached_head;\n-\tchar *advance;\n-\tchar *revert;\n+\tchar *ref;\n+\tstruct object_id ref_old_oid;\n \tenum replay_mode mode = REPLAY_MODE_PICK;\n \tint ret;\n \n-\tadvance = xstrdup_or_null(opts->advance);\n-\trevert = xstrdup_or_null(opts->revert);\n-\tif (revert)\n+\tref = xstrdup_or_null(opts->ref);\n+\toidclr(&ref_old_oid, the_repository->hash_algo);\n+\tif (opts->revert)\n \t\tmode = REPLAY_MODE_REVERT;\n-\tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n-\t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n-\n-\t/* FIXME: Should allow replaying commits with the first as a root commit */\n+\tset_up_replay_mode(revs->repo, &revs->cmdline, opts,\n+\t\t\t   &detached_head, &ref, &onto, &ref_old_oid,\n+\t\t\t   &update_refs);\n \n \tif (prepare_revision_walk(revs) < 0) {\n \t\tret = error(_(\"error preparing revisions\"));\n@@ -372,6 +368,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkhint_t pos;\n \t\tint hr;\n \n+\t\t/* FIXME: Should allow replaying commits with the first as a root commit */\n \t\tif (!commit->parents)\n \t\t\tdie(_(\"replaying down from root commit is not supported yet!\"));\n \t\tif (commit->parents->next)\n@@ -390,7 +387,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance || revert)\n+\t\tif (ref)\n \t\t\tcontinue;\n \n \t\tfor (decoration = get_name_decoration(&commit->object);\n@@ -424,13 +421,11 @@ int replay_revisions(struct rev_info *revs,\n \t\tgoto out;\n \t}\n \n-\t/* In --advance or --revert mode, update the target ref */\n-\tif (advance || revert) {\n-\t\tconst char *ref = advance ? advance : revert;\n+\t/* In --ref mode, update the target ref */\n+\tif (ref)\n \t\treplay_result_queue_update(out, ref,\n-\t\t\t\t\t   &onto->object.oid,\n+\t\t\t\t\t   &ref_old_oid,\n \t\t\t\t\t   &last_commit->object.oid);\n-\t}\n \n \tret = 0;\n \n@@ -441,7 +436,6 @@ int replay_revisions(struct rev_info *revs,\n \t}\n \tkh_destroy_oid_map(replayed_commits);\n \tmerge_finalize(&merge_opt, &result);\n-\tfree(advance);\n-\tfree(revert);\n+\tfree(ref);\n \treturn ret;\n }\ndiff --git a/replay.h b/replay.h\nindex e916a5f975..89ed236215 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -11,31 +11,32 @@ struct rev_info;\n  */\n struct replay_revisions_options {\n \t/*\n-\t * Starting point at which to create the new commits; must be a branch\n-\t * name. The branch will be updated to point to the rewritten commits.\n-\t * This option is mutually exclusive with `onto` and `revert`.\n-\t */\n-\tconst char *advance;\n-\n-\t/*\n-\t * Starting point at which to create the new commits; must be a\n-\t * committish. References pointing at decendants of `onto` will be\n-\t * updated to point to the new commits.\n+\t * Starting point at which to create the new commits. For\n+\t * 'rebase' this is required. For 'pick' and 'revert' it is\n+\t * optional; when omitted, defaults to the commit pointed to\n+\t * by `ref`. May be any valid committish.\n \t */\n \tconst char *onto;\n \n \t/*\n-\t * Starting point at which to create revert commits; must be a branch\n-\t * name. The branch will be updated to point to the revert commits.\n-\t * This option is mutually exclusive with `onto` and `advance`.\n+\t * The ref to update with the result. For 'pick' and 'revert'\n+\t * subcommands this is required. For 'rebase' it is optional;\n+\t * when omitted, refs are inferred from the revision range.\n+\t * Must be a valid reference name.\n \t */\n-\tconst char *revert;\n+\tconst char *ref;\n \n \t/*\n \t * Update branches that point at commits in the given revision range.\n-\t * Requires `onto` to be set.\n+\t * Only valid for the 'rebase' subcommand when `ref` is not set.\n \t */\n \tint contained;\n+\n+\t/*\n+\t * If set, reverse the effect of the commits being replayed\n+\t * rather than cherry-picking them.\n+\t */\n+\tint revert;\n };\n \n /* This struct is used as an out-parameter by `replay_revisions()`. */\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex ca517cf607..4fe8f3d736 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -60,54 +60,72 @@ test_expect_success 'setup bare' '\n \tgit clone --bare . bare\n '\n \n-test_expect_success 'argument to --advance must be a reference' '\n-\techo \"fatal: argument to --advance must be a reference\" >expect &&\n+test_expect_success 'a subcommand is required' '\n+\ttest_must_fail git replay 2>actual &&\n+\ttest_grep \"need a subcommand\" actual\n+'\n+\n+test_expect_success 'argument to --ref must be a reference' '\n+\techo \"fatal: argument to --ref must be a reference\" >expect &&\n \toid=$(git rev-parse main) &&\n-\ttest_must_fail git replay --advance=$oid topic1..topic2 2>actual &&\n+\ttest_must_fail git replay pick --ref=$oid topic1..topic2 2>actual &&\n \ttest_cmp expect actual\n '\n \n-test_expect_success '--onto with invalid commit-ish' '\n+test_expect_success 'rebase --onto with invalid commit-ish' '\n \tprintf \"fatal: ${SQ}refs/not-valid${SQ} is not \" >expect &&\n \tprintf \"a valid commit-ish for --onto\\n\" >>expect &&\n-\ttest_must_fail git replay --onto=refs/not-valid topic1..topic2 2>actual &&\n+\ttest_must_fail git replay rebase --onto=refs/not-valid topic1..topic2 2>actual &&\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'exactly one of --onto, --advance, or --revert is required' '\n-\techo \"error: exactly one of --onto, --advance, or --revert is required\" >expect &&\n-\ttest_might_fail git replay -h >>expect &&\n-\ttest_must_fail git replay topic1..topic2 2>actual &&\n-\ttest_cmp expect actual\n+test_expect_success '--onto is required for rebase subcommand' '\n+\ttest_must_fail git replay rebase topic1..topic2 2>actual &&\n+\ttest_grep \"is required\" actual\n+'\n+\n+test_expect_success '--ref is required for pick subcommand' '\n+\ttest_must_fail git replay pick topic1..topic2 2>actual &&\n+\ttest_grep \"is required\" actual\n+'\n+\n+test_expect_success '--ref is required for revert subcommand' '\n+\ttest_must_fail git replay revert topic1..topic2 2>actual &&\n+\ttest_grep \"is required\" actual\n '\n \n test_expect_success 'no base or negative ref gives no-replaying down to root error' '\n \techo \"fatal: replaying down from root commit is not supported yet!\" >expect &&\n-\ttest_must_fail git replay --onto=topic1 topic2 2>actual &&\n+\ttest_must_fail git replay rebase --onto=topic1 topic2 2>actual &&\n \ttest_cmp expect actual\n '\n \n-test_expect_success '--contained requires --onto' '\n-\techo \"fatal: --contained requires --onto\" >expect &&\n-\ttest_must_fail git replay --advance=main --contained \\\n+test_expect_success '--contained can only be used with rebase' '\n+\ttest_must_fail git replay pick --ref main --contained \\\n \t\ttopic1..topic2 2>actual &&\n-\ttest_cmp expect actual\n+\ttest_grep \"only be used with\" actual\n+'\n+\n+test_expect_success '--contained and --ref cannot be used together' '\n+\ttest_must_fail git replay rebase --onto main --ref main --contained \\\n+\t\ttopic1..topic2 2>actual &&\n+\ttest_grep \"cannot be used together\" actual\n '\n \n-test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n-\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n-\ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n+test_expect_success 'cannot replay target with multiple sources' '\n+\techo \"fatal: cannot replay target with multiple sources because ordering would be ill-defined\" >expect &&\n+\ttest_must_fail git replay pick --ref main main topic1 topic2 2>actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'replaying merge commits is not supported yet' '\n \techo \"fatal: replaying merge commits is not supported yet!\" >expect &&\n-\ttest_must_fail git replay --advance=main main..topic-with-merge 2>actual &&\n+\ttest_must_fail git replay pick --ref main main..topic-with-merge 2>actual &&\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'using replay to rebase two branches, one on top of other' '\n-\tgit replay --ref-action=print --onto main topic1..topic2 >result &&\n+test_expect_success 'using replay rebase to rebase two branches, one on top of other' '\n+\tgit replay rebase --ref-action=print --onto main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -122,26 +140,25 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n \ttest_cmp expect result\n '\n \n-test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n-\tgit -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&\n+test_expect_success 'using replay rebase on bare repo to rebase two branches, one on top of other' '\n+\tgit -C bare replay rebase --ref-action=print --onto main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n-test_expect_success 'using replay to rebase with a conflict' '\n-\ttest_expect_code 1 git replay --onto topic1 B..conflict\n+test_expect_success 'using replay rebase with a conflict' '\n+\ttest_expect_code 1 git replay rebase --onto topic1 B..conflict\n '\n \n-test_expect_success 'using replay on bare repo to rebase with a conflict' '\n-\ttest_expect_code 1 git -C bare replay --onto topic1 B..conflict\n+test_expect_success 'using replay rebase on bare repo with a conflict' '\n+\ttest_expect_code 1 git -C bare replay rebase --onto topic1 B..conflict\n '\n \n-test_expect_success 'using replay to perform basic cherry-pick' '\n+test_expect_success 'using replay pick to perform basic cherry-pick' '\n \t# The differences between this test and previous ones are:\n-\t#   --advance vs --onto\n+\t#   pick --ref vs rebase --onto\n \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n \t# 4th field of result is hash for main instead of hash for topic2\n-\n-\tgit replay --ref-action=print --advance main topic1..topic2 >result &&\n+\tgit replay pick --ref-action=print --ref main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -156,8 +173,8 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n \ttest_cmp expect result\n '\n \n-test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n-\tgit -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&\n+test_expect_success 'using replay pick on bare repo to perform basic cherry-pick' '\n+\tgit -C bare replay pick --ref-action=print --ref main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -168,11 +185,11 @@ test_expect_success 'commits that become empty are dropped' '\n \ttest_when_finished \"git update-ref --stdin <original-branches &&\n \t\trm original-branches\" &&\n \t# Cherry-pick tip of topic1 (\"F\"), from the middle of A..empty, to main\n-\tgit replay --advance main topic1^! &&\n+\tgit replay pick --ref main topic1^! &&\n \n \t# Replay all of A..empty onto main (which includes topic1 & thus F\n \t# in the middle)\n-\tgit replay --onto main --branches --ancestry-path=empty ^A \\\n+\tgit replay rebase --onto main --branches --ancestry-path=empty ^A \\\n \t\t>result &&\n \tgit log --format=\"%s%d\" L..empty >actual &&\n \ttest_write_lines >expect \\\n@@ -180,16 +197,16 @@ test_expect_success 'commits that become empty are dropped' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'replay on bare repo fails with both --advance and --onto' '\n-\ttest_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare\n+test_expect_success 'replay rebase on bare repo fails with both --ref and --contained' '\n+\ttest_must_fail git -C bare replay rebase --ref main --contained --onto main topic1..topic2\n '\n \n-test_expect_success 'replay fails when both --advance and --onto are omitted' '\n-\ttest_must_fail git replay topic1..topic2 >result\n+test_expect_success 'replay rebase fails without --onto' '\n+\ttest_must_fail git replay rebase topic1..topic2\n '\n \n-test_expect_success 'using replay to also rebase a contained branch' '\n-\tgit replay --ref-action=print --contained --onto main main..topic3 >result &&\n+test_expect_success 'using replay rebase to also rebase a contained branch' '\n+\tgit replay rebase --ref-action=print --contained --onto main main..topic3 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -212,13 +229,13 @@ test_expect_success 'using replay to also rebase a contained branch' '\n \ttest_cmp expect result\n '\n \n-test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n-\tgit -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&\n+test_expect_success 'using replay rebase on bare repo to also rebase a contained branch' '\n+\tgit -C bare replay rebase --ref-action=print --contained --onto main main..topic3 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n-test_expect_success 'using replay to rebase multiple divergent branches' '\n-\tgit replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n+test_expect_success 'using replay rebase to rebase multiple divergent branches' '\n+\tgit replay rebase --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -241,8 +258,8 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n \ttest_cmp expect result\n '\n \n-test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n-\tgit -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n+test_expect_success 'using replay rebase on bare repo to rebase multiple divergent branches, including contained ones' '\n+\tgit -C bare replay rebase --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n \n \ttest_line_count = 4 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -269,12 +286,12 @@ test_expect_success 'using replay on bare repo to rebase multiple divergent bran\n \tdone\n '\n \n-test_expect_success 'using replay to update detached HEAD' '\n+test_expect_success 'using replay rebase to update detached HEAD' '\n \tcurrent_head=$(git branch --show-current) &&\n \ttest_when_finished git switch \"$current_head\" &&\n \tgit switch --detach &&\n \ttest_commit something &&\n-\tgit replay --ref-action=print --onto HEAD~2 --ref-action=print HEAD~..HEAD >updates &&\n+\tgit replay rebase --ref-action=print --onto HEAD~2 --ref HEAD HEAD~..HEAD >updates &&\n \ttest_grep \"update HEAD \" updates\n '\n \n@@ -297,7 +314,7 @@ test_expect_success 'merge.directoryRenames=false' '\n \tgit commit -m modified to-rename/add-a-file.t &&\n \n \tgit -c merge.directoryRenames=false replay \\\n-\t\t--onto rename-onto rename-onto..rename-from\n+\t\trebase --onto rename-onto rename-onto..rename-from\n '\n \n test_expect_success 'default atomic behavior updates refs directly' '\n@@ -306,7 +323,7 @@ test_expect_success 'default atomic behavior updates refs directly' '\n \ttest_when_finished \"git branch -D test-atomic\" &&\n \n \t# Test default atomic behavior (no output, refs updated)\n-\tgit replay --onto main topic1..test-atomic >output &&\n+\tgit replay rebase --onto main topic1..test-atomic >output &&\n \ttest_must_be_empty output &&\n \n \t# Verify ref was updated\n@@ -317,7 +334,7 @@ test_expect_success 'default atomic behavior updates refs directly' '\n \t# Verify reflog message includes SHA of onto commit\n \tgit reflog test-atomic -1 --format=%gs >reflog-msg &&\n \tONTO_SHA=$(git rev-parse main) &&\n-\techo \"replay --onto $ONTO_SHA\" >expect-reflog &&\n+\techo \"replay rebase --onto $ONTO_SHA\" >expect-reflog &&\n \ttest_cmp expect-reflog reflog-msg\n '\n \n@@ -327,7 +344,7 @@ test_expect_success 'atomic behavior in bare repository' '\n \ttest_when_finished \"git -C bare update-ref refs/heads/topic2 $START\" &&\n \n \t# Test atomic updates work in bare repo\n-\tgit -C bare replay --onto main topic1..topic2 >output &&\n+\tgit -C bare replay rebase --onto main topic1..topic2 >output &&\n \ttest_must_be_empty output &&\n \n \t# Verify ref was updated in bare repo\n@@ -336,18 +353,18 @@ test_expect_success 'atomic behavior in bare repository' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'reflog message for --advance mode' '\n+test_expect_success 'reflog message for pick subcommand' '\n \t# Store original state\n \tSTART=$(git rev-parse main) &&\n \ttest_when_finished \"git update-ref refs/heads/main $START\" &&\n \n-\t# Test --advance mode reflog message\n-\tgit replay --advance main topic1..topic2 >output &&\n+\t# Test pick mode reflog message\n+\tgit replay pick --ref main topic1..topic2 >output &&\n \ttest_must_be_empty output &&\n \n-\t# Verify reflog message includes --advance and branch name\n+\t# Verify reflog message includes subcommand and branch name\n \tgit reflog main -1 --format=%gs >reflog-msg &&\n-\techo \"replay --advance main\" >expect-reflog &&\n+\techo \"replay pick main\" >expect-reflog &&\n \ttest_cmp expect-reflog reflog-msg\n '\n \n@@ -358,7 +375,7 @@ test_expect_success 'replay.refAction=print config option' '\n \n \t# Test with config set to print\n \ttest_config replay.refAction print &&\n-\tgit replay --onto main topic1..topic2 >output &&\n+\tgit replay rebase --onto main topic1..topic2 >output &&\n \ttest_line_count = 1 output &&\n \ttest_grep \"^update refs/heads/topic2 \" output\n '\n@@ -370,7 +387,7 @@ test_expect_success 'replay.refAction=update config option' '\n \n \t# Test with config set to update\n \ttest_config replay.refAction update &&\n-\tgit replay --onto main topic1..topic2 >output &&\n+\tgit replay rebase --onto main topic1..topic2 >output &&\n \ttest_must_be_empty output &&\n \n \t# Verify ref was updated\n@@ -386,37 +403,30 @@ test_expect_success 'command-line --ref-action overrides config' '\n \n \t# Set config to update but use --ref-action=print\n \ttest_config replay.refAction update &&\n-\tgit replay --ref-action=print --onto main topic1..topic2 >output &&\n+\tgit replay rebase --ref-action=print --onto main topic1..topic2 >output &&\n \ttest_line_count = 1 output &&\n \ttest_grep \"^update refs/heads/topic2 \" output\n '\n \n test_expect_success 'invalid replay.refAction value' '\n \ttest_config replay.refAction invalid &&\n-\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n+\ttest_must_fail git replay rebase --onto main topic1..topic2 2>error &&\n \ttest_grep \"invalid.*replay.refAction.*value\" error\n '\n \n-test_expect_success 'argument to --revert must be a reference' '\n-\techo \"fatal: argument to --revert must be a reference\" >expect &&\n-\toid=$(git rev-parse main) &&\n-\ttest_must_fail git replay --revert=$oid topic1..topic2 2>actual &&\n-\ttest_cmp expect actual\n-'\n-\n test_expect_success 'cannot revert with multiple sources' '\n-\techo \"fatal: cannot revert target with multiple sources because ordering would be ill-defined\" >expect &&\n-\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n+\techo \"fatal: cannot replay target with multiple sources because ordering would be ill-defined\" >expect &&\n+\ttest_must_fail git replay revert --ref main main topic1 topic2 2>actual &&\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'using replay --revert to revert commits' '\n+test_expect_success 'using replay revert to revert commits' '\n \t# Reuse existing topic4 branch (has commits I and J on top of main)\n \tSTART=$(git rev-parse topic4) &&\n \ttest_when_finished \"git branch -f topic4 $START\" &&\n \n \t# Revert commits I and J\n-\tgit replay --revert topic4 topic4~2..topic4 &&\n+\tgit replay revert --ref topic4 topic4~2..topic4 &&\n \n \t# Verify the revert commits were created\n \tgit log --format=%s -4 topic4 >actual &&\n@@ -437,17 +447,17 @@ test_expect_success 'using replay --revert to revert commits' '\n \n \t# Verify reflog message\n \tgit reflog topic4 -1 --format=%gs >reflog-msg &&\n-\techo \"replay --revert topic4\" >expect-reflog &&\n+\techo \"replay revert topic4\" >expect-reflog &&\n \ttest_cmp expect-reflog reflog-msg\n '\n \n-test_expect_success 'using replay --revert in bare repo' '\n+test_expect_success 'using replay revert in bare repo' '\n \t# Reuse existing topic4 in bare repo\n \tSTART=$(git -C bare rev-parse topic4) &&\n \ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n \n \t# Revert commit J in bare repo\n-\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n+\tgit -C bare replay revert --ref topic4 topic4~1..topic4 &&\n \n \t# Verify revert was created\n \tgit -C bare log -1 --format=%s topic4 >actual &&\n@@ -461,11 +471,11 @@ test_expect_success 'revert of revert uses Reapply' '\n \ttest_when_finished \"git branch -f topic4 $START\" &&\n \n \t# First revert J\n-\tgit replay --revert topic4 topic4~1..topic4 &&\n+\tgit replay revert --ref topic4 topic4~1..topic4 &&\n \tREVERT_J=$(git rev-parse topic4) &&\n \n \t# Now revert the revert - should become Reapply\n-\tgit replay --revert topic4 topic4~1..topic4 &&\n+\tgit replay revert --ref topic4 topic4~1..topic4 &&\n \n \t# Verify Reapply prefix and message format\n \ttest_commit_message topic4 <<-EOF\n@@ -475,24 +485,33 @@ test_expect_success 'revert of revert uses Reapply' '\n \tEOF\n '\n \n-test_expect_success 'git replay --revert with conflict' '\n+test_expect_success 'git replay revert with conflict' '\n \t# conflict branch has C.conflict which conflicts with topic1s C\n-\ttest_expect_code 1 git replay --revert conflict B..topic1\n+\ttest_expect_code 1 git replay revert --ref conflict B..topic1\n '\n \n-test_expect_success 'git replay --revert incompatible with --contained' '\n-\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n-\ttest_grep \"requires --onto\" error\n-'\n+test_expect_success 'using replay rebase with --ref and --onto' '\n+\tgit branch test-ref-onto topic2 &&\n+\ttest_when_finished \"git branch -D test-ref-onto\" &&\n \n-test_expect_success 'git replay --revert incompatible with --onto' '\n-\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n-\ttest_grep \"cannot be used together\" error\n+\tgit replay rebase --ref test-ref-onto --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\tgit log --format=%s test-ref-onto >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n '\n \n-test_expect_success 'git replay --revert incompatible with --advance' '\n-\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n-\ttest_grep \"cannot be used together\" error\n+test_expect_success 'using replay pick with --onto' '\n+\tSTART=$(git rev-parse main) &&\n+\ttest_when_finished \"git update-ref refs/heads/main $START\" &&\n+\n+\tgit replay pick --ref main --onto topic1 topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\tgit log --format=%s -4 main >actual &&\n+\ttest_write_lines E D F C >expect &&\n+\ttest_cmp expect actual\n '\n \n test_done\n\n---\nbase-commit: 4bf257a78a32b23e8f95d801e334a8b9f80a88d0\nchange-id: 20260309-toon-replay-subcommands-844b3ce72626\n\n"},{"id":"538654","messageId":"abGutGnWo1gN0Dii@denethor","threadId":"65183","inReplyTo":"20260309-toon-replay-subcommands-v1-1-864ec82ef68a@iotcl.com","subject":"Re: [PATCH RFC] git-replay: implement subcommands","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-11T18:33:34Z","receivedAt":"2026-03-11T18:33:40Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/09 08:30PM, Toon Claes wrote:\n> git-replay(1) has various operation modes. The mode depends on which of\n> the options `--onto`, `--advance`, or `--revert` is given. These options\n> are mutually exclusive. This usage pattern is counterintuitive and\n> uncommon for Git commands to behave this way.\n> \n> Implement subcommands into git-replay(1):\n> \n> * `rebase`: This replaces what `--onto=` used to do.\n\nI'm a bit confused by this. It appears that the \"rebase\" subcommand\nstill requires the `--onto` option so it doesn't seem to really be\nreplacing anything. I assume we are tyring to break these operations\ninto distinct categories which seems reasonable.\n\n> * `pick`: This replaces what `--advance=` used to do.\n> * `revert`: This replaces what `--revert=` used to do.\n> \n> Option `--onto` is still accepted. It's mandatory for the `rebase`\n> subcommand and needs to be used in the exact same way.\n> \n> Option `--ref` is added and required for the `pick` and `revert`\n> subcommands. This replaces what `--advance` and `--revert` used to do,\n> but as a single uniform option for all subcommands.\n> \n> The `rebase` subcommand also accepts option `--ref`, and when given this\n> is the ref that's updated with the outcome of the git-replay(1) command.\n> Thus following commands are identical:\n> \n>     $ git replay rebase --onto=master master..branch-1\n> \n>     $ git replay rebase --onto=master master..branch-1^{0} --ref=refs/heads/branch-1\n> \n> In the second example the upper boundary of the revision range is peeled\n> down to a commit (using '^{0}'). Without option `--ref`, git-replay(1)\n> doesn't know which ref to update, that's why `--ref` is passed\n> explicitly.\n> \n> For the subcommands `pick` and `revert` it's also possible to combine\n> `--ref` and `--onto`. Here are again two identical examples:\n> \n>     $ git replay pick --onto=branch-1 master..aabbccdd\n> \n>     $ git replay pick --onto=branch-1^{0} master..aabbccdd --ref=refs/heads/branch-1\n> \n> In the latter the argument for `--onto` is peeled down to a commit, so\n> the command doesn't know which ref to update. To inform git-replay(1)\n> which refs should be updated, it's passed explicitly as option `--ref`.\n> \n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n> In the patch series by Siddharth Asthana[1] the option `--revert` is\n> added to git-replay(1). This is implemented as option `--revert`, next\n> to the existing options `--advance` and `--onto`.\n> \n> The usage of these options is mutually exclusive, so the user can only\n> use one of them, and depending on which one, git-replay(1) selects a\n> \"mode of operating\".\n> \n> Various people have raised this behavior is somewhat confusing. In this\n> series we attempt to make the usage of git-replay(1) more intuitive and\n> user-friendly by implementing the modes as subcommands.\n\nOk, subcommands for git-replay(1) seem like they could be a good fit\nhere.\n\n> This patch is submitted as an RFC to gather feedback about the design.\n> All changes are implemented as a single patch right now, and thus\n> reviewing the changes might be challenging. When we got people aligned\n> on the direction, I'll work toward cleaner patches.\n> \n> These changes are based on 'master' at 864f55e190 (The second batch,\n> 2026-02-09) with the patches of Siddharth[1] applied: 'sa/replay-revert'\n> at f79189a653 (replay: add --revert mode to reverse commit changes,\n> 2026-02-19)\n> \n> [1]: 20260218234215.89326-3-siddharthasthana31@gmail.com\n> ---\n>  Documentation/git-replay.adoc | 124 ++++++++++++++++----------\n>  builtin/replay.c              | 150 ++++++++++++++++++++++++-------\n>  replay.c                      |  66 +++++++-------\n>  replay.h                      |  31 +++----\n>  t/t3650-replay-basics.sh      | 199 +++++++++++++++++++++++-------------------\n>  5 files changed, 349 insertions(+), 221 deletions(-)\n> \n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index ffdf790278..a7e8dac23f 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -8,8 +8,13 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>  \n>  SYNOPSIS\n>  --------\n> -[verse]\n> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n\nDo we intent to remove the experimental marker?\n\n> +[synopsis]\n> +git replay rebase --onto <newbase> [--ref <branch>] [--contained]\n> +\t\t[--ref-action[=<mode>]] <revision-range>\n> +git replay pick --ref <branch> [--onto <newbase>]\n> +\t\t[--ref-action[=<mode>]] <revision-range>\n> +git replay revert --ref <branch> [--onto <newbase>]\n> +\t\t[--ref-action[=<mode>]] <revision-range>\n\nSubcommands with required options like this feel quite bad IMO and I'm\nnot sure it makes it much more intuitive. I guess subcommands do make it\neasier to convey which options pertain to which operation. Maybe it\nwould be better if required arguments remained positional though?\n\nAlso, using these subcommands appears to be required now which is a\nbreaking change compared to before. The command is experimental, so this\nmay be fine, but should probably be more directly mentioned.\n\n-Justin\n"},{"id":"538912","messageId":"87v7ezsnqf.fsf@iotcl.com","threadId":"65183","inReplyTo":"abGutGnWo1gN0Dii@denethor","subject":"Re: [PATCH RFC] git-replay: implement subcommands","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-13T16:22:48Z","receivedAt":"2026-03-13T16:23:34Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> On 26/03/09 08:30PM, Toon Claes wrote:\n>> git-replay(1) has various operation modes. The mode depends on which of\n>> the options `--onto`, `--advance`, or `--revert` is given. These options\n>> are mutually exclusive. This usage pattern is counterintuitive and\n>> uncommon for Git commands to behave this way.\n>> \n>> Implement subcommands into git-replay(1):\n>> \n>> * `rebase`: This replaces what `--onto=` used to do.\n>\n> I'm a bit confused by this. It appears that the \"rebase\" subcommand\n> still requires the `--onto` option so it doesn't seem to really be\n> replacing anything. I assume we are tyring to break these operations\n> into distinct categories which seems reasonable.\n\nOkay, maybe I should be more verbose about the problem I'm trying to\nsolve.\n\nLet's start with the existing option `--onto`. This implies a \"rebase\"\noperation. The argument to this option is a revision which acts as the\nbase for the rebase. On this base the commits in the revision-range are\nreplayed.\n\nIn the end, git-replay(1) takes the ref from the upper boundary of this\nrevision-range, and updates that to the result of the rebase.\n\nA usage example:\n\n    $ git replay --onto=master master..my-branch\n\nWith option --ref-action=print, you'll get:\n\n    update refs/heads/my-branch aaabbbccc 000111222\n\n(using abbreviated OIDs for simplification in this email)\n\nSo 000111222 would be the commit my-branch is pointing to before the\ngit-replay(1), and aaabbbccc the new commit.\n\nNow looking at `--advance`, this works differently:\n\n    $ git replay --advance=other-branch master..my-branch\n\nWith option --ref-action=print, you'll get:\n\n    update refs/heads/other-branch 888999000 444555666\n\nAs you can see, the argument of --advance is the ref that gets updated.\n\nSo this command would be identical to:\n\n    $ git replay --advance=other-branch master..000111222\n\nIf we try to do use the commit OID in the revision-range when using\n--onto: \n\n    $ git replay --onto=master master..000111222\n\nNothing (noticable) happens. git-replay(1) does the replay, but doesn't\nknow which ref to update.\n\nThis assymetry between --onto and --revert & --advance is the main issue\nI'm trying to resolve with this proposal.\n\n\n>> * `pick`: This replaces what `--advance=` used to do.\n>> * `revert`: This replaces what `--revert=` used to do.\n>> \n>> Option `--onto` is still accepted. It's mandatory for the `rebase`\n>> subcommand and needs to be used in the exact same way.\n>> \n>> Option `--ref` is added and required for the `pick` and `revert`\n>> subcommands. This replaces what `--advance` and `--revert` used to do,\n>> but as a single uniform option for all subcommands.\n>> \n>> The `rebase` subcommand also accepts option `--ref`, and when given this\n>> is the ref that's updated with the outcome of the git-replay(1) command.\n>> Thus following commands are identical:\n>> \n>>     $ git replay rebase --onto=master master..branch-1\n>> \n>>     $ git replay rebase --onto=master master..branch-1^{0} --ref=refs/heads/branch-1\n>> \n>> In the second example the upper boundary of the revision range is peeled\n>> down to a commit (using '^{0}'). Without option `--ref`, git-replay(1)\n>> doesn't know which ref to update, that's why `--ref` is passed\n>> explicitly.\n>> \n>> For the subcommands `pick` and `revert` it's also possible to combine\n>> `--ref` and `--onto`. Here are again two identical examples:\n>> \n>>     $ git replay pick --onto=branch-1 master..aabbccdd\n>> \n>>     $ git replay pick --onto=branch-1^{0} master..aabbccdd --ref=refs/heads/branch-1\n>> \n>> In the latter the argument for `--onto` is peeled down to a commit, so\n>> the command doesn't know which ref to update. To inform git-replay(1)\n>> which refs should be updated, it's passed explicitly as option `--ref`.\n>> \n>> Signed-off-by: Toon Claes <toon@iotcl.com>\n>> ---\n>> In the patch series by Siddharth Asthana[1] the option `--revert` is\n>> added to git-replay(1). This is implemented as option `--revert`, next\n>> to the existing options `--advance` and `--onto`.\n>> \n>> The usage of these options is mutually exclusive, so the user can only\n>> use one of them, and depending on which one, git-replay(1) selects a\n>> \"mode of operating\".\n>> \n>> Various people have raised this behavior is somewhat confusing. In this\n>> series we attempt to make the usage of git-replay(1) more intuitive and\n>> user-friendly by implementing the modes as subcommands.\n>\n> Ok, subcommands for git-replay(1) seem like they could be a good fit\n> here.\n\n<3\n\n>\n>> This patch is submitted as an RFC to gather feedback about the design.\n>> All changes are implemented as a single patch right now, and thus\n>> reviewing the changes might be challenging. When we got people aligned\n>> on the direction, I'll work toward cleaner patches.\n>> \n>> These changes are based on 'master' at 864f55e190 (The second batch,\n>> 2026-02-09) with the patches of Siddharth[1] applied: 'sa/replay-revert'\n>> at f79189a653 (replay: add --revert mode to reverse commit changes,\n>> 2026-02-19)\n>> \n>> [1]: 20260218234215.89326-3-siddharthasthana31@gmail.com\n>> ---\n>>  Documentation/git-replay.adoc | 124 ++++++++++++++++----------\n>>  builtin/replay.c              | 150 ++++++++++++++++++++++++-------\n>>  replay.c                      |  66 +++++++-------\n>>  replay.h                      |  31 +++----\n>>  t/t3650-replay-basics.sh      | 199 +++++++++++++++++++++++-------------------\n>>  5 files changed, 349 insertions(+), 221 deletions(-)\n>> \n>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n>> index ffdf790278..a7e8dac23f 100644\n>> --- a/Documentation/git-replay.adoc\n>> +++ b/Documentation/git-replay.adoc\n>> @@ -8,8 +8,13 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>>  \n>>  SYNOPSIS\n>>  --------\n>> -[verse]\n>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n>\n> Do we intent to remove the experimental marker?\n\nYeah, I did. I don't like them in this synopsis. It seems git-replay(1)\nis the only one doing this, so I'd like to get rid of it.\n\nDoing this should end up in a separate commit.\n\n>> +[synopsis]\n>> +git replay rebase --onto <newbase> [--ref <branch>] [--contained]\n>> +\t\t[--ref-action[=<mode>]] <revision-range>\n>> +git replay pick --ref <branch> [--onto <newbase>]\n>> +\t\t[--ref-action[=<mode>]] <revision-range>\n>> +git replay revert --ref <branch> [--onto <newbase>]\n>> +\t\t[--ref-action[=<mode>]] <revision-range>\n>\n> Subcommands with required options like this feel quite bad IMO and I'm\n> not sure it makes it much more intuitive.\n\nOkay, I did some looking up, and maybe you're right, I couldn't find any\nother command that has required options. It seems all commands have some\nkind of \"default behavior\" when no options are given.\n\n> I guess subcommands do make it easier to convey which options pertain\n> to which operation. Maybe it would be better if required arguments\n> remained positional though?\n\nI'm not sure that's better:\n\n    [synopsis]\n    git replay rebase <newbase> [--ref <branch>] [--contained]\n    \t\t[--ref-action[=<mode>]] <revision-range>\n    git replay pick <branch> [--onto <newbase>]\n    \t\t[--ref-action[=<mode>]] <revision-range>\n    git replay revert <branch> [--onto <newbase>]\n    \t\t[--ref-action[=<mode>]] <revision-range>\n\nI don't think is better because the required argument for 'rebase' is\nused differently than 'pick' and 'revert', as explained above.\n\nI guess my main gripe with the current options is the naming: `--onto`\nto rebase, `--advance` to cherry-pick, and `--revert` to revert. And\nwhile the last one does sound intuitive, the argument to that option is\nthe branch you want to replay the reverted commits onto. So the argument\nisn't *what* you're reverting, it's *where* you're reverting to.\n\nWith my proposal I wanted to make that more clear.\n\nThis proposal is trying to be not too disruptive (for example, for\nrebase you only need to a add `rebase`), but that's maybe not a good\nidea. So an alternative could be: on top of this proposal, make both\n`--onto` and `--ref` required. In various cases the user will provide\nthe exact same argument to both options, but since git-replay(1) is a\nplumbing command, we can consider this is acceptable?\n\nAnd now, while writing this, I was thinking about yet another proposal.\nBecause --advance and --revert are pretty similar. Why is --revert not\nan additional option you can add when using --advance. So instead of:\n\n    git replay --revert=my-branch master..other-branch\n    git replay --advance=my-branch --revert master..other-branch\n\n@Siddharth, have you considered that?\n\n> Also, using these subcommands appears to be required now which is a\n> breaking change compared to before. The command is experimental, so this\n> may be fine, but should probably be more directly mentioned.\n\nSure, I can do that when I clean up the commits.\n\n-- \nCheers,\nToon\n"},{"id":"538957","messageId":"5370f3b2-2c4a-4d9d-904d-2a8f6094b6e1@gmail.com","threadId":"65183","inReplyTo":"87v7ezsnqf.fsf@iotcl.com","subject":"Re: [PATCH RFC] git-replay: implement subcommands","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-14T07:18:55Z","receivedAt":"2026-03-14T07:18:59Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\n\nOn 13/03/26 21:52, Toon Claes wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n>> On 26/03/09 08:30PM, Toon Claes wrote:\n>>> git-replay(1) has various operation modes. The mode depends on which of\n>>> the options `--onto`, `--advance`, or `--revert` is given. These options\n>>> are mutually exclusive. This usage pattern is counterintuitive and\n>>> uncommon for Git commands to behave this way.\n>>>\n>>> Implement subcommands into git-replay(1):\n>>>\n>>> * `rebase`: This replaces what `--onto=` used to do.\n>>\n>> I'm a bit confused by this. It appears that the \"rebase\" subcommand\n>> still requires the `--onto` option so it doesn't seem to really be\n>> replacing anything. I assume we are tyring to break these operations\n>> into distinct categories which seems reasonable.\n> \n> Okay, maybe I should be more verbose about the problem I'm trying to\n> solve.\n> \n> Let's start with the existing option `--onto`. This implies a \"rebase\"\n> operation. The argument to this option is a revision which acts as the\n> base for the rebase. On this base the commits in the revision-range are\n> replayed.\n> \n> In the end, git-replay(1) takes the ref from the upper boundary of this\n> revision-range, and updates that to the result of the rebase.\n> \n> A usage example:\n> \n>      $ git replay --onto=master master..my-branch\n> \n> With option --ref-action=print, you'll get:\n> \n>      update refs/heads/my-branch aaabbbccc 000111222\n> \n> (using abbreviated OIDs for simplification in this email)\n> \n> So 000111222 would be the commit my-branch is pointing to before the\n> git-replay(1), and aaabbbccc the new commit.\n> \n> Now looking at `--advance`, this works differently:\n> \n>      $ git replay --advance=other-branch master..my-branch\n> \n> With option --ref-action=print, you'll get:\n> \n>      update refs/heads/other-branch 888999000 444555666\n> \n> As you can see, the argument of --advance is the ref that gets updated.\n> \n> So this command would be identical to:\n> \n>      $ git replay --advance=other-branch master..000111222\n> \n> If we try to do use the commit OID in the revision-range when using\n> --onto:\n> \n>      $ git replay --onto=master master..000111222\n> \n> Nothing (noticable) happens. git-replay(1) does the replay, but doesn't\n> know which ref to update.\n> \n> This assymetry between --onto and --revert & --advance is the main issue\n> I'm trying to resolve with this proposal.\n> \n> \n>>> * `pick`: This replaces what `--advance=` used to do.\n>>> * `revert`: This replaces what `--revert=` used to do.\n>>>\n>>> Option `--onto` is still accepted. It's mandatory for the `rebase`\n>>> subcommand and needs to be used in the exact same way.\n>>>\n>>> Option `--ref` is added and required for the `pick` and `revert`\n>>> subcommands. This replaces what `--advance` and `--revert` used to do,\n>>> but as a single uniform option for all subcommands.\n>>>\n>>> The `rebase` subcommand also accepts option `--ref`, and when given this\n>>> is the ref that's updated with the outcome of the git-replay(1) command.\n>>> Thus following commands are identical:\n>>>\n>>>      $ git replay rebase --onto=master master..branch-1\n>>>\n>>>      $ git replay rebase --onto=master master..branch-1^{0} --ref=refs/heads/branch-1\n>>>\n>>> In the second example the upper boundary of the revision range is peeled\n>>> down to a commit (using '^{0}'). Without option `--ref`, git-replay(1)\n>>> doesn't know which ref to update, that's why `--ref` is passed\n>>> explicitly.\n>>>\n>>> For the subcommands `pick` and `revert` it's also possible to combine\n>>> `--ref` and `--onto`. Here are again two identical examples:\n>>>\n>>>      $ git replay pick --onto=branch-1 master..aabbccdd\n>>>\n>>>      $ git replay pick --onto=branch-1^{0} master..aabbccdd --ref=refs/heads/branch-1\n>>>\n>>> In the latter the argument for `--onto` is peeled down to a commit, so\n>>> the command doesn't know which ref to update. To inform git-replay(1)\n>>> which refs should be updated, it's passed explicitly as option `--ref`.\n>>>\n>>> Signed-off-by: Toon Claes <toon@iotcl.com>\n>>> ---\n>>> In the patch series by Siddharth Asthana[1] the option `--revert` is\n>>> added to git-replay(1). This is implemented as option `--revert`, next\n>>> to the existing options `--advance` and `--onto`.\n>>>\n>>> The usage of these options is mutually exclusive, so the user can only\n>>> use one of them, and depending on which one, git-replay(1) selects a\n>>> \"mode of operating\".\n>>>\n>>> Various people have raised this behavior is somewhat confusing. In this\n>>> series we attempt to make the usage of git-replay(1) more intuitive and\n>>> user-friendly by implementing the modes as subcommands.\n>>\n>> Ok, subcommands for git-replay(1) seem like they could be a good fit\n>> here.\n> \n> <3\n> \n>>\n>>> This patch is submitted as an RFC to gather feedback about the design.\n>>> All changes are implemented as a single patch right now, and thus\n>>> reviewing the changes might be challenging. When we got people aligned\n>>> on the direction, I'll work toward cleaner patches.\n>>>\n>>> These changes are based on 'master' at 864f55e190 (The second batch,\n>>> 2026-02-09) with the patches of Siddharth[1] applied: 'sa/replay-revert'\n>>> at f79189a653 (replay: add --revert mode to reverse commit changes,\n>>> 2026-02-19)\n>>>\n>>> [1]: 20260218234215.89326-3-siddharthasthana31@gmail.com\n>>> ---\n>>>   Documentation/git-replay.adoc | 124 ++++++++++++++++----------\n>>>   builtin/replay.c              | 150 ++++++++++++++++++++++++-------\n>>>   replay.c                      |  66 +++++++-------\n>>>   replay.h                      |  31 +++----\n>>>   t/t3650-replay-basics.sh      | 199 +++++++++++++++++++++++-------------------\n>>>   5 files changed, 349 insertions(+), 221 deletions(-)\n>>>\n>>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n>>> index ffdf790278..a7e8dac23f 100644\n>>> --- a/Documentation/git-replay.adoc\n>>> +++ b/Documentation/git-replay.adoc\n>>> @@ -8,8 +8,13 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>>>   \n>>>   SYNOPSIS\n>>>   --------\n>>> -[verse]\n>>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n>>\n>> Do we intent to remove the experimental marker?\n> \n> Yeah, I did. I don't like them in this synopsis. It seems git-replay(1)\n> is the only one doing this, so I'd like to get rid of it.\n> \n> Doing this should end up in a separate commit.\n> \n>>> +[synopsis]\n>>> +git replay rebase --onto <newbase> [--ref <branch>] [--contained]\n>>> +\t\t[--ref-action[=<mode>]] <revision-range>\n>>> +git replay pick --ref <branch> [--onto <newbase>]\n>>> +\t\t[--ref-action[=<mode>]] <revision-range>\n>>> +git replay revert --ref <branch> [--onto <newbase>]\n>>> +\t\t[--ref-action[=<mode>]] <revision-range>\n>>\n>> Subcommands with required options like this feel quite bad IMO and I'm\n>> not sure it makes it much more intuitive.\n> \n> Okay, I did some looking up, and maybe you're right, I couldn't find any\n> other command that has required options. It seems all commands have some\n> kind of \"default behavior\" when no options are given.\n> \n>> I guess subcommands do make it easier to convey which options pertain\n>> to which operation. Maybe it would be better if required arguments\n>> remained positional though?\n> \n> I'm not sure that's better:\n> \n>      [synopsis]\n>      git replay rebase <newbase> [--ref <branch>] [--contained]\n>      \t\t[--ref-action[=<mode>]] <revision-range>\n>      git replay pick <branch> [--onto <newbase>]\n>      \t\t[--ref-action[=<mode>]] <revision-range>\n>      git replay revert <branch> [--onto <newbase>]\n>      \t\t[--ref-action[=<mode>]] <revision-range>\n> \n> I don't think is better because the required argument for 'rebase' is\n> used differently than 'pick' and 'revert', as explained above.\n> \n> I guess my main gripe with the current options is the naming: `--onto`\n> to rebase, `--advance` to cherry-pick, and `--revert` to revert. And\n> while the last one does sound intuitive, the argument to that option is\n> the branch you want to replay the reverted commits onto. So the argument\n> isn't *what* you're reverting, it's *where* you're reverting to.\n> \n> With my proposal I wanted to make that more clear.\n> \n> This proposal is trying to be not too disruptive (for example, for\n> rebase you only need to a add `rebase`), but that's maybe not a good\n> idea. So an alternative could be: on top of this proposal, make both\n> `--onto` and `--ref` required. In various cases the user will provide\n> the exact same argument to both options, but since git-replay(1) is a\n> plumbing command, we can consider this is acceptable?\n> \n> And now, while writing this, I was thinking about yet another proposal.\n> Because --advance and --revert are pretty similar. Why is --revert not\n> an additional option you can add when using --advance. So instead of:\n> \n>      git replay --revert=my-branch master..other-branch\n>      git replay --advance=my-branch --revert master..other-branch\n> \n> @Siddharth, have you considered that?\n\n\nYeah, I though about this early on but it didn't feel right. --advance \nmeans \"cherry-pick onto this branch and move it forward\", so combining \nit with --revert reads weird -- you would be \"advancing\" by reverting.\n\nThey also work quite differently under the hood. Revert uses \nnewest-first ordering (revs.reverse = 0) while advance uses \noldest-first, and revert sets author to the current user (author = NULL \nin create_commit) instead of preserving the original. So they really are \ndifferent modes, kind of like how cherry-pick and revert are separate \ncommands even though the merge machinery is shared.\n\nOn the subcommand RFC -- I think this is a good direction and it would \nclean up the asymmetry you pointed out between --onto and \n--advance/--revert. My v4 should be a clean base for it (which your RFC \nalready builds on).\n\nThanks,\nSiddharth\n\n\n> \n>> Also, using these subcommands appears to be required now which is a\n>> breaking change compared to before. The command is experimental, so this\n>> may be fine, but should probably be more directly mentioned.\n> \n> Sure, I can do that when I clean up the commits.\n> \n\n"}]}