Subject: Re: [PATCH RFC] git-replay: implement subcommands

From: Siddharth Asthana <[email-masked]>
Date: Sat, 14 Mar 2026 12:48:55 +0530

On 13/03/26 21:52, Toon Claes wrote:
> Justin Tobler <[email-masked]> writes:
> 
>> On 26/03/09 08:30PM, Toon Claes wrote:
>>> git-replay(1) has various operation modes. The mode depends on which of
>>> the options `--onto`, `--advance`, or `--revert` is given. These options
>>> are mutually exclusive. This usage pattern is counterintuitive and
>>> uncommon for Git commands to behave this way.
>>>
>>> Implement subcommands into git-replay(1):
>>>
>>> * `rebase`: This replaces what `--onto=` used to do.
>>
>> I'm a bit confused by this. It appears that the "rebase" subcommand
>> still requires the `--onto` option so it doesn't seem to really be
>> replacing anything. I assume we are tyring to break these operations
>> into distinct categories which seems reasonable.
> 
> Okay, maybe I should be more verbose about the problem I'm trying to
> solve.
> 
> Let's start with the existing option `--onto`. This implies a "rebase"
> operation. The argument to this option is a revision which acts as the
> base for the rebase. On this base the commits in the revision-range are
> replayed.
> 
> In the end, git-replay(1) takes the ref from the upper boundary of this
> revision-range, and updates that to the result of the rebase.
> 
> A usage example:
> 
>      $ git replay --onto=master master..my-branch
> 
> With option --ref-action=print, you'll get:
> 
>      update refs/heads/my-branch aaabbbccc 000111222
> 
> (using abbreviated OIDs for simplification in this email)
> 
> So 000111222 would be the commit my-branch is pointing to before the
> git-replay(1), and aaabbbccc the new commit.
> 
> Now looking at `--advance`, this works differently:
> 
>      $ git replay --advance=other-branch master..my-branch
> 
> With option --ref-action=print, you'll get:
> 
>      update refs/heads/other-branch 888999000 444555666
> 
> As you can see, the argument of --advance is the ref that gets updated.
> 
> So this command would be identical to:
> 
>      $ git replay --advance=other-branch master..000111222
> 
> If we try to do use the commit OID in the revision-range when using
> --onto:
> 
>      $ git replay --onto=master master..000111222
> 
> Nothing (noticable) happens. git-replay(1) does the replay, but doesn't
> know which ref to update.
> 
> This assymetry between --onto and --revert & --advance is the main issue
> I'm trying to resolve with this proposal.
> 
> 
>>> * `pick`: This replaces what `--advance=` used to do.
>>> * `revert`: This replaces what `--revert=` used to do.
>>>
>>> Option `--onto` is still accepted. It's mandatory for the `rebase`
>>> subcommand and needs to be used in the exact same way.
>>>
>>> Option `--ref` is added and required for the `pick` and `revert`
>>> subcommands. This replaces what `--advance` and `--revert` used to do,
>>> but as a single uniform option for all subcommands.
>>>
>>> The `rebase` subcommand also accepts option `--ref`, and when given this
>>> is the ref that's updated with the outcome of the git-replay(1) command.
>>> Thus following commands are identical:
>>>
>>>      $ git replay rebase --onto=master master..branch-1
>>>
>>>      $ git replay rebase --onto=master master..branch-1^{0} --ref=refs/heads/branch-1
>>>
>>> In the second example the upper boundary of the revision range is peeled
>>> down to a commit (using '^{0}'). Without option `--ref`, git-replay(1)
>>> doesn't know which ref to update, that's why `--ref` is passed
>>> explicitly.
>>>
>>> For the subcommands `pick` and `revert` it's also possible to combine
>>> `--ref` and `--onto`. Here are again two identical examples:
>>>
>>>      $ git replay pick --onto=branch-1 master..aabbccdd
>>>
>>>      $ git replay pick --onto=branch-1^{0} master..aabbccdd --ref=refs/heads/branch-1
>>>
>>> In the latter the argument for `--onto` is peeled down to a commit, so
>>> the command doesn't know which ref to update. To inform git-replay(1)
>>> which refs should be updated, it's passed explicitly as option `--ref`.
>>>
>>> Signed-off-by: Toon Claes <[email-masked]>
>>> ---
>>> In the patch series by Siddharth Asthana[1] the option `--revert` is
>>> added to git-replay(1). This is implemented as option `--revert`, next
>>> to the existing options `--advance` and `--onto`.
>>>
>>> The usage of these options is mutually exclusive, so the user can only
>>> use one of them, and depending on which one, git-replay(1) selects a
>>> "mode of operating".
>>>
>>> Various people have raised this behavior is somewhat confusing. In this
>>> series we attempt to make the usage of git-replay(1) more intuitive and
>>> user-friendly by implementing the modes as subcommands.
>>
>> Ok, subcommands for git-replay(1) seem like they could be a good fit
>> here.
> 
> <3
> 
>>
>>> This patch is submitted as an RFC to gather feedback about the design.
>>> All changes are implemented as a single patch right now, and thus
>>> reviewing the changes might be challenging. When we got people aligned
>>> on the direction, I'll work toward cleaner patches.
>>>
>>> These changes are based on 'master' at 864f55e190 (The second batch,
>>> 2026-02-09) with the patches of Siddharth[1] applied: 'sa/replay-revert'
>>> at f79189a653 (replay: add --revert mode to reverse commit changes,
>>> 2026-02-19)
>>>
>>> [1]: [email-masked]
>>> ---
>>>   Documentation/git-replay.adoc | 124 ++++++++++++++++----------
>>>   builtin/replay.c              | 150 ++++++++++++++++++++++++-------
>>>   replay.c                      |  66 +++++++-------
>>>   replay.h                      |  31 +++----
>>>   t/t3650-replay-basics.sh      | 199 +++++++++++++++++++++++-------------------
>>>   5 files changed, 349 insertions(+), 221 deletions(-)
>>>
>>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
>>> index ffdf790278..a7e8dac23f 100644
>>> --- a/Documentation/git-replay.adoc
>>> +++ b/Documentation/git-replay.adoc
>>> @@ -8,8 +8,13 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
>>>   
>>>   SYNOPSIS
>>>   --------
>>> -[verse]
>>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...
>>
>> Do we intent to remove the experimental marker?
> 
> Yeah, I did. I don't like them in this synopsis. It seems git-replay(1)
> is the only one doing this, so I'd like to get rid of it.
> 
> Doing this should end up in a separate commit.
> 
>>> +[synopsis]
>>> +git replay rebase --onto <newbase> [--ref <branch>] [--contained]
>>> +		[--ref-action[=<mode>]] <revision-range>
>>> +git replay pick --ref <branch> [--onto <newbase>]
>>> +		[--ref-action[=<mode>]] <revision-range>
>>> +git replay revert --ref <branch> [--onto <newbase>]
>>> +		[--ref-action[=<mode>]] <revision-range>
>>
>> Subcommands with required options like this feel quite bad IMO and I'm
>> not sure it makes it much more intuitive.
> 
> Okay, I did some looking up, and maybe you're right, I couldn't find any
> other command that has required options. It seems all commands have some
> kind of "default behavior" when no options are given.
> 
>> I guess subcommands do make it easier to convey which options pertain
>> to which operation. Maybe it would be better if required arguments
>> remained positional though?
> 
> I'm not sure that's better:
> 
>      [synopsis]
>      git replay rebase <newbase> [--ref <branch>] [--contained]
>      		[--ref-action[=<mode>]] <revision-range>
>      git replay pick <branch> [--onto <newbase>]
>      		[--ref-action[=<mode>]] <revision-range>
>      git replay revert <branch> [--onto <newbase>]
>      		[--ref-action[=<mode>]] <revision-range>
> 
> I don't think is better because the required argument for 'rebase' is
> used differently than 'pick' and 'revert', as explained above.
> 
> I guess my main gripe with the current options is the naming: `--onto`
> to rebase, `--advance` to cherry-pick, and `--revert` to revert. And
> while the last one does sound intuitive, the argument to that option is
> the branch you want to replay the reverted commits onto. So the argument
> isn't *what* you're reverting, it's *where* you're reverting to.
> 
> With my proposal I wanted to make that more clear.
> 
> This proposal is trying to be not too disruptive (for example, for
> rebase you only need to a add `rebase`), but that's maybe not a good
> idea. So an alternative could be: on top of this proposal, make both
> `--onto` and `--ref` required. In various cases the user will provide
> the exact same argument to both options, but since git-replay(1) is a
> plumbing command, we can consider this is acceptable?
> 
> And now, while writing this, I was thinking about yet another proposal.
> Because --advance and --revert are pretty similar. Why is --revert not
> an additional option you can add when using --advance. So instead of:
> 
>      git replay --revert=my-branch master..other-branch
>      git replay --advance=my-branch --revert master..other-branch
> 
> @Siddharth, have you considered that?

Yeah, I though about this early on but it didn't feel right. --advance 
means "cherry-pick onto this branch and move it forward", so combining 
it with --revert reads weird -- you would be "advancing" by reverting.

They also work quite differently under the hood. Revert uses 
newest-first ordering (revs.reverse = 0) while advance uses 
oldest-first, and revert sets author to the current user (author = NULL 
in create_commit) instead of preserving the original. So they really are 
different modes, kind of like how cherry-pick and revert are separate 
commands even though the merge machinery is shared.

On the subcommand RFC -- I think this is a good direction and it would 
clean up the asymmetry you pointed out between --onto and 
--advance/--revert. My v4 should be a clean base for it (which your RFC 
already builds on).

Thanks,
Siddharth

> 
>> Also, using these subcommands appears to be required now which is a
>> breaking change compared to before. The command is experimental, so this
>> may be fine, but should probably be more directly mentioned.
> 
> Sure, I can do that when I clean up the commits.
>