diff mbox series

[v3,13/15] replay: add --advance or 'cherry-pick' mode

Message ID 20230602102533.876905-14-christian.couder@gmail.com (mailing list archive)
State New, archived
Headers show
Series Introduce new `git replay` command | expand

Commit Message

Christian Couder June 2, 2023, 10:25 a.m. UTC
From: Elijah Newren <newren@gmail.com>

There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
'cherry-pick' mode with `--advance`. This new mode will make the target
branch advance as we replay commits onto it.

Co-authored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-replay.txt |  33 ++++++-
 builtin/replay.c             | 185 +++++++++++++++++++++++++++++++++--
 t/t3650-replay-basics.sh     |  26 +++++
 3 files changed, 230 insertions(+), 14 deletions(-)

Comments

Toon claes June 22, 2023, 10:05 a.m. UTC | #1
Christian Couder <christian.couder@gmail.com> writes:

> +	/*
> +	 * When the user specifies e.g.
> +	 *   git replay origin/main..mybranch
> +	 *   git replay ^origin/next mybranch1 mybranch2

When I'm trying these, I'm getting the error:
    error: option --onto or --advance is mandatory

In what situation can I omit both --onto and --advance?

> +static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
> +				  const char *onto_name,
> +				  const char **advance_name,
> +				  struct commit **onto,

Would it make sense to call this target?

> +				  struct strset **update_refs)
> +{
> +	struct ref_info rinfo;
> +
> +	get_ref_information(cmd_info, &rinfo);
> +	if (!rinfo.positive_refexprs)
> +		die(_("need some commits to replay"));
> +	if (onto_name && *advance_name)
> +		die(_("--onto and --advance are incompatible"));

Do we actually need to disallow this? I mean from git-replay's point of
view, there's no technical limitation why can cannot support both modes
at once. The update-ref commands in the output will update both target
and source branches, but it's not up to us whether that's desired.

> +	else if (onto_name) {

No need to 'else' here IMHO.

> +		*onto = peel_committish(onto_name);
> +		if (rinfo.positive_refexprs <
> +		    strset_get_size(&rinfo.positive_refs))
> +			die(_("all positive revisions given must be references"));

I tested this locally with the following command:

$ git replay --onto main OID..OID

This command didn't give any errors, neither did it return any
update-ref lines. I would have expected to hit this die().

> +	} else if (*advance_name) {
> +		struct object_id oid;
> +		char *fullname = NULL;
> +
> +		*onto = peel_committish(*advance_name);
> +		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
> +			     &oid, &fullname, 0) == 1) {
> +			*advance_name = fullname;
> +		} else {
> +			die(_("argument to --advance must be a reference"));
> +		}
> +		if (rinfo.positive_refexprs > 1)
> +			die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));

The sources aren't always branches, so I suggest something like:

+			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));

> +	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
> +			      &onto, &update_refs);
> +
> +	if (!onto) /* FIXME: Should handle replaying down to root commit */
> +		die("Replaying down to root commit is not supported yet!");

When I was testing locally I tried the following:

$ git replay --onto main feature

I was expecting this command to find the common ancestor automatically,
but instead I got this error. I'm fine if for now the command does not
determine the common ancestor yet, but I think we should provide a
better error for this scenario.

> +test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
> +	git -C bare replay --advance main topic1..topic2 >result-bare &&
> +	test_cmp expect result-bare
> +'
> +
>  test_done

Shall we add a test case when providing both --onto and --advance? And
one that omits both?
Junio C Hamano July 25, 2023, 9:41 p.m. UTC | #2
Christian Couder <christian.couder@gmail.com> writes:

> There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
> 'cherry-pick' mode with `--advance`. This new mode will make the target
> branch advance as we replay commits onto it.

If I say

	$ git replay --(advance|onto) xyzzy frotz..nitfol yomin

where the topology of the cherry-picked range look like this

                      x---x---Y yomin
                     /
            E---F---x----x----N nitfol
           /  frotz
          /
         X xyzzy     

after transplanting the commits, we would get something like

                      x---x---Y yomin
                     /
            E---F---x----x----N nitfol
           /  frotz
          /
         X---x----x----N'
              \
               x---x---Y'

Now, if this was done with --onto, nitfol and yomin would point at
N' and Y', but with --advance, where would xyzzy go?

Yes, my point is that without --advance, there always is a
reasonable set of branch tips that will be moved, but with
"--advance", you cannot guarantee that you have any reasonable
answer to that question.

The answer could be "when there is no single 'tip of the new
history', the command with '--advance' errors out", but whatever
behaviour we choose, it should be documented.

> @@ -29,9 +29,17 @@ OPTIONS
>  	Starting point at which to create the new commits.  May be any
>  	valid commit, and not just an existing branch name.
>  +
> -The update-ref commands in the output will update the branch(es)
> -in the revision range to point at the new commits (in other
> -words, this mimics a rebase operation).
> +When `--onto` is specified, the update-ref command(s) in the output will
> +update the branch(es) in the revision range to point at the new
> +commits (in other words, this mimics a rebase operation).
> +
> +--advance <branch>::
> +	Starting point at which to create the new commits; must be a
> +	branch name.
> ++
> +When `--advance` is specified, the update-ref command(s) in the output
> +will update the branch passed as an argument to `--advance` to point at
> +the new commits (in other words, this mimics a cherry-pick operation).

This part is not giving much useful information to determine the
answer (which might be fine, as long as the answer can easily be
found in some other parts of this document, but I would have
expected everything necessary would be found here or one item before
this one about --onto).

> @@ -47,7 +55,10 @@ input to `git update-ref --stdin`.  It is basically of the form:
>  	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>  	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
>  
> -where the number of refs updated depend on the arguments passed.
> +where the number of refs updated depend on the arguments passed.  When
> +using `--advance`, the number of refs updated is always one, but for
> +`--onto`, it can be one or more (rebasing multiple branches
> +simultaneously is supported).

"dependS on the arguments passed" is not incorrect per-se, in the
sense that if you "replay --onto X E..N" (in the above picture), I
think you'll move F and N (two), while "F..N" will only move N
(one).  But the important thing to convey is how many branches had
their tips in the replayed range, no?  "depends on the shape of the
history being replayed" would be a more correct thing to say for the
"--onto" mode.  For "--advance", presumably you would require to
have a single positive endpoint [*], so "depends on the arguments"
is still not wrong per-se, because "when --advance is part of the
arguments, the number becomes one".

	Side note: even then, I suspect that

		git replay --advance X E..F N

        should be allowed, simply because there is only one sensible
        interpretation.  You'll end up with a single strand of
        pearls F--x--x--N transplanted on top of X, and the range
        happens to contain F and N but it is obvious the end result
        should advance xyzzy to N because F fast-forwards to N.

I'd say "where the number of ..." and everything after these sample
"update" lines should be removed, and instead we should add a few
words to the main description of the options, e.g.  for "--onto"

> +When `--onto` is specified, the update-ref command(s) in the output will
> +update the branch(es) in the revision range to point at the new
> +commits (in other words, this mimics a rebase operation).

we could update the above to something like

    ... will update the branches in the revision range to point at the
    new commits, similar to the way how "rebase --update-refs" updates
    multiple branches in the affected range.
Christian Couder Sept. 7, 2023, 8:35 a.m. UTC | #3
On Thu, Jun 22, 2023 at 12:09 PM Toon Claes <toon@iotcl.com> wrote:
>
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > +     /*
> > +      * When the user specifies e.g.
> > +      *   git replay origin/main..mybranch
> > +      *   git replay ^origin/next mybranch1 mybranch2
>
> When I'm trying these, I'm getting the error:
>     error: option --onto or --advance is mandatory
>
> In what situation can I omit both --onto and --advance?

It was possible with version 1 of this series as one of the patches
allowed the command to guess the base:

https://lore.kernel.org/git/20230407072415.1360068-13-christian.couder@gmail.com/

so --onto wasn't needed to specify it.

Comments on that patch said that it might be better to focus on a
plumbing command first and for that the patch wasn't needed, so I
removed it in version 2.

> > +static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
> > +                               const char *onto_name,
> > +                               const char **advance_name,
> > +                               struct commit **onto,
>
> Would it make sense to call this target?

This is more the new base we are rebasing target branches onto. So if
we want to change the name, `--base` or `--newbase` would make more
sense. `git rebase` already has `--onto` though, so, if we want to be
consistent with it, we should keep `--onto` for this.

> > +                               struct strset **update_refs)
> > +{
> > +     struct ref_info rinfo;
> > +
> > +     get_ref_information(cmd_info, &rinfo);
> > +     if (!rinfo.positive_refexprs)
> > +             die(_("need some commits to replay"));
> > +     if (onto_name && *advance_name)
> > +             die(_("--onto and --advance are incompatible"));
>
> Do we actually need to disallow this? I mean from git-replay's point of
> view, there's no technical limitation why can cannot support both modes
> at once. The update-ref commands in the output will update both target
> and source branches, but it's not up to us whether that's desired.

I am not sure what you call "target" and "source" branches. Anyway
here is in simple terms the way the command works:

  1) it takes either `--onto <newbase>` or `--advance <branch>` and
then one or more <revision-range>,
  2) it replays all the commits in the <revision-range> onto either
<newbase> or <branch>,
  3) in case of `--advance`, it outputs a single command for `git
update-ref --stdin` to advance <branch> to the last commit that was
just replayed,
  4) in case of `--onto`, it outputs a number of commands for `git
update-ref --stdin` to update the branches in <revision-range> to
where the tip commits of these branches have been replayed.

So `--advance` is like a cherry-pick, and `--onto` is like a rebase.

It would be possible to do both a rebase onto a branch and a
cherry-pick of the rebased commits onto that branch, but this is not
very common and you can achieve the same result by just rebasing and
then using `git reset` or `git update-ref` to make the branch point to
the result of the rebase. So I don't see the point of complicating the
command at this point.

> > +     else if (onto_name) {
>
> No need to 'else' here IMHO.
>
> > +             *onto = peel_committish(onto_name);
> > +             if (rinfo.positive_refexprs <
> > +                 strset_get_size(&rinfo.positive_refs))
> > +                     die(_("all positive revisions given must be references"));
>
> I tested this locally with the following command:
>
> $ git replay --onto main OID..OID
>
> This command didn't give any errors, neither did it return any
> update-ref lines. I would have expected to hit this die().

Yeah, this might be unexpected.

I tested it too and 'rinfo.positive_refexprs' is 1 while
'strset_get_size(&rinfo.positive_refs)' is 0 with such a command.

The result does not look wrong though. Above that code there is:

    if (!rinfo.positive_refexprs)
        die(_("need some commits to replay"));

so it looks like there is at least a check that the revision range
passed to the command contains positive commits.

It might be possible that users prefer a command that outputs nothing
when there is nothing to replay instead of erroring out.

> > +     } else if (*advance_name) {
> > +             struct object_id oid;
> > +             char *fullname = NULL;
> > +
> > +             *onto = peel_committish(*advance_name);
> > +             if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
> > +                          &oid, &fullname, 0) == 1) {
> > +                     *advance_name = fullname;
> > +             } else {
> > +                     die(_("argument to --advance must be a reference"));
> > +             }
> > +             if (rinfo.positive_refexprs > 1)
> > +                     die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
>
> The sources aren't always branches, so I suggest something like:
>
> +                       die(_("cannot advance target with multiple sources because ordering would be ill-defined"));

Yeah, that looks reasonable. I have made this change in version 4 I
will send very soon.

> > +     determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
> > +                           &onto, &update_refs);
> > +
> > +     if (!onto) /* FIXME: Should handle replaying down to root commit */
> > +             die("Replaying down to root commit is not supported yet!");
>
> When I was testing locally I tried the following:
>
> $ git replay --onto main feature
>
> I was expecting this command to find the common ancestor automatically,
> but instead I got this error. I'm fine if for now the command does not
> determine the common ancestor yet, but I think we should provide a
> better error for this scenario.

I agree that it isn't very user friendly. We could indeed try to find
if there is a common ancestor, and, if that's the case, suggest
another way to call the command. This is a plumbing command in its
early stage though for now. So I guess it's Ok to postpone working on
nicer error messages.

> > +test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
> > +     git -C bare replay --advance main topic1..topic2 >result-bare &&
> > +     test_cmp expect result-bare
> > +'
> > +
> >  test_done
>
> Shall we add a test case when providing both --onto and --advance? And
> one that omits both?

Ok, I have made this change in version 4.
Christian Couder Sept. 7, 2023, 8:35 a.m. UTC | #4
On Tue, Jul 25, 2023 at 11:41 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > There is already a 'rebase' mode with `--onto`. Let's add an 'advance' or
> > 'cherry-pick' mode with `--advance`. This new mode will make the target
> > branch advance as we replay commits onto it.
>
> If I say
>
>         $ git replay --(advance|onto) xyzzy frotz..nitfol yomin
>
> where the topology of the cherry-picked range look like this
>
>                       x---x---Y yomin
>                      /
>             E---F---x----x----N nitfol
>            /  frotz
>           /
>          X xyzzy
>
> after transplanting the commits, we would get something like
>
>                       x---x---Y yomin
>                      /
>             E---F---x----x----N nitfol
>            /  frotz
>           /
>          X---x----x----N'
>               \
>                x---x---Y'
>
> Now, if this was done with --onto, nitfol and yomin would point at
> N' and Y', but with --advance, where would xyzzy go?
>
> Yes, my point is that without --advance, there always is a
> reasonable set of branch tips that will be moved, but with
> "--advance", you cannot guarantee that you have any reasonable
> answer to that question.
>
> The answer could be "when there is no single 'tip of the new
> history', the command with '--advance' errors out", but whatever
> behaviour we choose, it should be documented.

Ok, I have improved the commit message by adding the following:

"The replayed commits should have a single tip, so that it's clear where
the target branch should be advanced. If they have more than one tip,
this new mode will error out."

I have also updated the doc for <revision-range> like this:

"<revision-range>::
    Range of commits to replay. More than one <revision-range> can
    be passed, but in `--advance <branch>` mode, they should have
    a single tip, so that it's clear where <branch> should point
    to. See "Specifying Ranges" in linkgit:git-rev-parse."

> > +--advance <branch>::
> > +     Starting point at which to create the new commits; must be a
> > +     branch name.
> > ++
> > +When `--advance` is specified, the update-ref command(s) in the output
> > +will update the branch passed as an argument to `--advance` to point at
> > +the new commits (in other words, this mimics a cherry-pick operation).
>
> This part is not giving much useful information to determine the
> answer (which might be fine, as long as the answer can easily be
> found in some other parts of this document, but I would have
> expected everything necessary would be found here or one item before
> this one about --onto).

The doc about <revision-range> is just after the above, so I think the
above change in the <revision-range> doc is Ok and enough here.

> > @@ -47,7 +55,10 @@ input to `git update-ref --stdin`.  It is basically of the form:
> >       update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> >       update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
> >
> > -where the number of refs updated depend on the arguments passed.
> > +where the number of refs updated depend on the arguments passed.  When
> > +using `--advance`, the number of refs updated is always one, but for
> > +`--onto`, it can be one or more (rebasing multiple branches
> > +simultaneously is supported).
>
> "dependS on the arguments passed" is not incorrect per-se, in the
> sense that if you "replay --onto X E..N" (in the above picture), I
> think you'll move F and N (two), while "F..N" will only move N
> (one).  But the important thing to convey is how many branches had
> their tips in the replayed range, no?  "depends on the shape of the
> history being replayed" would be a more correct thing to say for the
> "--onto" mode.  For "--advance", presumably you would require to
> have a single positive endpoint [*], so "depends on the arguments"
> is still not wrong per-se, because "when --advance is part of the
> arguments, the number becomes one".

Yeah, I agree.

>         Side note: even then, I suspect that
>
>                 git replay --advance X E..F N
>
>         should be allowed, simply because there is only one sensible
>         interpretation.  You'll end up with a single strand of
>         pearls F--x--x--N transplanted on top of X, and the range
>         happens to contain F and N but it is obvious the end result
>         should advance xyzzy to N because F fast-forwards to N.
>
> I'd say "where the number of ..." and everything after these sample
> "update" lines should be removed,

I am not sure it's a good thing to remove that, as I think repeating
how things work in the context of an example output can help people
understand. I have updated these sentences to the following:

"where the number of refs updated depends on the arguments passed and
the shape of the history being replayed.  When using `--advance`, the
number of refs updated is always one, but for `--onto`, it can be one
or more (rebasing multiple branches simultaneously is supported)."

> and instead we should add a few
> words to the main description of the options, e.g.  for "--onto"
>
> > +When `--onto` is specified, the update-ref command(s) in the output will
> > +update the branch(es) in the revision range to point at the new
> > +commits (in other words, this mimics a rebase operation).
>
> we could update the above to something like
>
>     ... will update the branches in the revision range to point at the
>     new commits, similar to the way how "rebase --update-refs" updates
>     multiple branches in the affected range.

Yeah, I agree. In the version 4 I will send soon, have updated the above to:

"When `--onto` is specified, the update-ref command(s) in the output will
update the branch(es) in the revision range to point at the new
commits, similar to the way how `git rebase --update-refs` updates

multiple branches in the affected range."
diff mbox series

Patch

diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt
index 394d7b0050..439b2f92e7 100644
--- a/Documentation/git-replay.txt
+++ b/Documentation/git-replay.txt
@@ -9,7 +9,7 @@  git-replay - Replay commits on a different base, without touching working tree
 SYNOPSIS
 --------
 [verse]
-'git replay' --onto <newbase> <revision-range>...
+'git replay' (--onto <newbase> | --advance <branch>) <revision-range>...
 
 DESCRIPTION
 -----------
@@ -29,9 +29,17 @@  OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-The update-ref commands in the output will update the branch(es)
-in the revision range to point at the new commits (in other
-words, this mimics a rebase operation).
+When `--onto` is specified, the update-ref command(s) in the output will
+update the branch(es) in the revision range to point at the new
+commits (in other words, this mimics a rebase operation).
+
+--advance <branch>::
+	Starting point at which to create the new commits; must be a
+	branch name.
++
+When `--advance` is specified, the update-ref command(s) in the output
+will update the branch passed as an argument to `--advance` to point at
+the new commits (in other words, this mimics a cherry-pick operation).
 
 <revision-range>::
 	Range of commits to replay; see "Specifying Ranges" in
@@ -47,7 +55,10 @@  input to `git update-ref --stdin`.  It is basically of the form:
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
-where the number of refs updated depend on the arguments passed.
+where the number of refs updated depend on the arguments passed.  When
+using `--advance`, the number of refs updated is always one, but for
+`--onto`, it can be one or more (rebasing multiple branches
+simultaneously is supported).
 
 EXIT STATUS
 -----------
@@ -67,6 +78,18 @@  $ git replay --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
+To cherry-pick the commits from mybranch onto target:
+
+------------
+$ git replay --advance target origin/main..mybranch
+update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
+------------
+
+Note that the first two examples replay the exact same commits and on
+top of the exact same new base, they only differ in that the first
+provides instructions to make mybranch point at the new commits and
+the second provides instructions to make target point at them.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
diff --git a/builtin/replay.c b/builtin/replay.c
index cffbf34290..4b64d15159 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -14,6 +14,7 @@ 
 #include "parse-options.h"
 #include "refs.h"
 #include "revision.h"
+#include "strmap.h"
 #include <oidset.h>
 #include <tree.h>
 
@@ -82,6 +83,146 @@  static struct commit *create_commit(struct tree *tree,
 	return (struct commit *)obj;
 }
 
+struct ref_info {
+	struct commit *onto;
+	struct strset positive_refs;
+	struct strset negative_refs;
+	int positive_refexprs;
+	int negative_refexprs;
+};
+
+static void get_ref_information(struct rev_cmdline_info *cmd_info,
+				struct ref_info *ref_info)
+{
+	int i;
+
+	ref_info->onto = NULL;
+	strset_init(&ref_info->positive_refs);
+	strset_init(&ref_info->negative_refs);
+	ref_info->positive_refexprs = 0;
+	ref_info->negative_refexprs = 0;
+
+	/*
+	 * When the user specifies e.g.
+	 *   git replay origin/main..mybranch
+	 *   git replay ^origin/next mybranch1 mybranch2
+	 * we want to be able to determine where to replay the commits.  In
+	 * these examples, the branches are probably based on an old version
+	 * of either origin/main or origin/next, so we want to replay on the
+	 * newest version of that branch.  In contrast we would want to error
+	 * out if they ran
+	 *   git replay ^origin/master ^origin/next mybranch
+	 *   git replay mybranch~2..mybranch
+	 * the first of those because there's no unique base to choose, and
+	 * the second because they'd likely just be replaying commits on top
+	 * of the same commit and not making any difference.
+	 */
+	for (i = 0; i < cmd_info->nr; i++) {
+		struct rev_cmdline_entry *e = cmd_info->rev + i;
+		struct object_id oid;
+		const char *refexpr = e->name;
+		char *fullname = NULL;
+		int can_uniquely_dwim = 1;
+
+		if (*refexpr == '^')
+			refexpr++;
+		if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+			can_uniquely_dwim = 0;
+
+		if (e->flags & BOTTOM) {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->negative_refs, fullname);
+			if (!ref_info->negative_refexprs)
+				ref_info->onto = lookup_commit_reference_gently(the_repository,
+										&e->item->oid, 1);
+			ref_info->negative_refexprs++;
+		} else {
+			if (can_uniquely_dwim)
+				strset_add(&ref_info->positive_refs, fullname);
+			ref_info->positive_refexprs++;
+		}
+
+		free(fullname);
+	}
+}
+
+static void determine_replay_mode(struct rev_cmdline_info *cmd_info,
+				  const char *onto_name,
+				  const char **advance_name,
+				  struct commit **onto,
+				  struct strset **update_refs)
+{
+	struct ref_info rinfo;
+
+	get_ref_information(cmd_info, &rinfo);
+	if (!rinfo.positive_refexprs)
+		die(_("need some commits to replay"));
+	if (onto_name && *advance_name)
+		die(_("--onto and --advance are incompatible"));
+	else if (onto_name) {
+		*onto = peel_committish(onto_name);
+		if (rinfo.positive_refexprs <
+		    strset_get_size(&rinfo.positive_refs))
+			die(_("all positive revisions given must be references"));
+	} else if (*advance_name) {
+		struct object_id oid;
+		char *fullname = NULL;
+
+		*onto = peel_committish(*advance_name);
+		if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name),
+			     &oid, &fullname, 0) == 1) {
+			*advance_name = fullname;
+		} else {
+			die(_("argument to --advance must be a reference"));
+		}
+		if (rinfo.positive_refexprs > 1)
+			die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+	} else {
+		int positive_refs_complete = (
+			rinfo.positive_refexprs ==
+			strset_get_size(&rinfo.positive_refs));
+		int negative_refs_complete = (
+			rinfo.negative_refexprs ==
+			strset_get_size(&rinfo.negative_refs));
+		/*
+		 * We need either positive_refs_complete or
+		 * negative_refs_complete, but not both.
+		 */
+		if (rinfo.negative_refexprs > 0 &&
+		    positive_refs_complete == negative_refs_complete)
+			die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+		if (negative_refs_complete) {
+			struct hashmap_iter iter;
+			struct strmap_entry *entry;
+
+			if (rinfo.negative_refexprs == 0)
+				die(_("all positive revisions given must be references"));
+			else if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
+			else if (rinfo.positive_refexprs > 1)
+				die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
+
+			/* Only one entry, but we have to loop to get it */
+			strset_for_each_entry(&rinfo.negative_refs,
+					      &iter, entry) {
+				*advance_name = entry->key;
+			}
+		} else { /* positive_refs_complete */
+			if (rinfo.negative_refexprs > 1)
+				die(_("cannot implicitly determine correct base for --onto"));
+			if (rinfo.negative_refexprs == 1)
+				*onto = rinfo.onto;
+		}
+	}
+	if (!*advance_name) {
+		*update_refs = xcalloc(1, sizeof(**update_refs));
+		**update_refs = rinfo.positive_refs;
+		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	}
+	strset_clear(&rinfo.negative_refs);
+	strset_clear(&rinfo.positive_refs);
+}
+
 static struct commit *pick_regular_commit(struct commit *pickme,
 					  struct commit *last_commit,
 					  struct merge_options *merge_opt,
@@ -114,20 +255,26 @@  static struct commit *pick_regular_commit(struct commit *pickme,
 
 int cmd_replay(int argc, const char **argv, const char *prefix)
 {
-	struct commit *onto;
+	const char *advance_name = NULL;
+	struct commit *onto = NULL;
 	const char *onto_name = NULL;
-	struct commit *last_commit = NULL;
+
 	struct rev_info revs;
+	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct merge_options merge_opt;
 	struct merge_result result;
+	struct strset *update_refs = NULL;
 	int ret = 0, i;
 
 	const char * const replay_usage[] = {
-		N_("git replay --onto <newbase> <revision-range>..."),
+		N_("git replay (--onto <newbase> | --advance <branch>) <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
+		OPT_STRING(0, "advance", &advance_name,
+			   N_("branch"),
+			   N_("make replay advance given branch")),
 		OPT_STRING(0, "onto", &onto_name,
 			   N_("revision"),
 			   N_("replay onto given commit")),
@@ -151,13 +298,11 @@  int cmd_replay(int argc, const char **argv, const char *prefix)
 			usage_with_options(replay_usage, replay_options);
 		}
 
-	if (!onto_name) {
-		error(_("option --onto is mandatory"));
+	if (!onto_name && !advance_name) {
+		error(_("option --onto or --advance is mandatory"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	onto = peel_committish(onto_name);
-
 	repo_init_revisions(the_repository, &revs, prefix);
 
 	argc = setup_revisions(argc, argv, &revs, NULL);
@@ -182,6 +327,12 @@  int cmd_replay(int argc, const char **argv, const char *prefix)
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
 
+	determine_replay_mode(&revs.cmdline, onto_name, &advance_name,
+			      &onto, &update_refs);
+
+	if (!onto) /* FIXME: Should handle replaying down to root commit */
+		die("Replaying down to root commit is not supported yet!");
+
 	if (prepare_revision_walk(&revs) < 0) {
 		ret = error(_("error preparing revisions"));
 		goto cleanup;
@@ -190,6 +341,7 @@  int cmd_replay(int argc, const char **argv, const char *prefix)
 	init_merge_options(&merge_opt, the_repository);
 	memset(&result, 0, sizeof(result));
 	merge_opt.show_rename_progress = 0;
+
 	result.tree = repo_get_commit_tree(the_repository, onto);
 	last_commit = onto;
 	while ((commit = get_revision(&revs))) {
@@ -204,12 +356,15 @@  int cmd_replay(int argc, const char **argv, const char *prefix)
 		if (!last_commit)
 			break;
 
+		/* Update any necessary branches */
+		if (advance_name)
+			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
 			continue;
-
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL) {
+			if (decoration->type == DECORATION_REF_LOCAL &&
+			    strset_contains(update_refs, decoration->name)) {
 				printf("update %s %s %s\n",
 				       decoration->name,
 				       oid_to_hex(&last_commit->object.oid),
@@ -219,11 +374,23 @@  int cmd_replay(int argc, const char **argv, const char *prefix)
 		}
 	}
 
+	/* In --advance mode, advance the target ref */
+	if (result.clean == 1 && advance_name) {
+		printf("update %s %s %s\n",
+		       advance_name,
+		       oid_to_hex(&last_commit->object.oid),
+		       oid_to_hex(&onto->object.oid));
+	}
+
 	/* Cleanup */
 	merge_finalize(&merge_opt, &result);
 	ret = result.clean;
 
 cleanup:
+	if (update_refs) {
+		strset_clear(update_refs);
+		free(update_refs);
+	}
 	release_revisions(&revs);
 
 	/* Return */
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index de6e40950e..bca405c431 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -96,4 +96,30 @@  test_expect_success 'using replay on bare repo with disallowed pathspec' '
 	test_must_fail git -C bare replay --onto main topic1..topic2 -- A.t
 '
 
+test_expect_success 'using replay to perform basic cherry-pick' '
+	# The differences between this test and previous ones are:
+	#   --advance vs --onto
+	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
+	# 4th field of result is hash for main instead of hash for topic2
+
+	git replay --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result) >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result
+'
+
+test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
+	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	test_cmp expect result-bare
+'
+
 test_done