From: Phillip Wood <phillip.wood123@gmail.com>
To: Siddharth Asthana <siddharthasthana31@gmail.com>, git@vger.kernel.org
Cc: christian.couder@gmail.com, ps@pks.im, newren@gmail.com,
gitster@pobox.com, phillip.wood@dunelm.org.uk,
karthik.188@gmail.com, johannes.schindelin@gmx.de,
toon@iotcl.com
Subject: Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes
Date: Tue, 16 Dec 2025 16:23:39 +0000 [thread overview]
Message-ID: <c49b2375-c975-4591-b3e9-aa87771a8015@gmail.com> (raw)
In-Reply-To: <20251202201611.22137-3-siddharthasthana31@gmail.com>
Hi Siddarth
I agree with Patrick's comments, I've added a few more of my own below
On 02/12/2025 20:16, Siddharth Asthana wrote:
> The `git replay` command performs server-side history rewriting without
> requiring a working tree. While it currently supports cherry-picking
> commits (--advance) and rebasing (--onto), it lacks the ability to
> revert them.
>
> At GitLab, we use replay in Gitaly for efficient server-side operations
> on bare repositories. Adding revert functionality enables us to reverse
> problematic commits directly on the server, eliminating client-side
> roundtrips and reducing network overhead.
>
> Add a `--revert <branch>` mode that reverses the changes introduced by
> the specified commits. Following the architecture of --onto and --advance,
> --revert is a standalone mode that takes a branch argument and updates
> that branch with the revert commits.
s/revert/reverted/?
> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> index dcb26e8a8e..eb297c7530 100644
> --- a/Documentation/git-replay.adoc
> +++ b/Documentation/git-replay.adoc
> @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
> SYNOPSIS
> --------
> [verse]
> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...
> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...
>
> DESCRIPTION
> -----------
> @@ -42,6 +42,24 @@ The history is replayed on top of the <branch> and <branch> is updated to
> point at the tip of the resulting history. This is different from `--onto`,
> which uses the target only as a starting point without updating it.
>
> +--revert <branch>::
> + Starting point at which to create the new revert commits; must be a
> + branch name.
> ++
> +When `--revert` is specified, the commits in the revision range are reverted
> +(their changes are undone) and the revert commits
s/revert/reverted/
> are applied on top of <branch>.
> +The <branch> is then updated to point at the new commits. This is similar to
> +running `git revert` for each commit in the range, but works without a working tree.
"git revert" takes a revision range so it is the same as running "git
revert <revision-range>" but does not update the working tree.
> ++
> +The commit messages follow `git revert` conventions: prefixed with "Revert" and
s/conventions: prefixed/conventions: they are prefixed/
> +including the original commit SHA.
s/including/include/
s/SHA/hash/
> When reverting a commit whose message starts
> +with "Revert", the new message uses "Reapply" instead. The author of the revert
> +commits is the current user, not the original commit author.
> ++
> +This option is mutually exclusive with `--onto` and `--advance`. It is also
> +incompatible with `--contained` (which is a modifier for `--onto` only).
> +
> +
> --ref-action[=<mode>]::
> Control how references are updated. The mode can be:
> +
> @@ -141,6 +159,22 @@ all commits they have since `base`, playing them on top of
> `origin/main`. These three branches may have commits on top of `base`
> that they have in common, but that does not need to be the case.
>
> +To revert commits from a branch:
> +
> +------------
> +$ git replay --revert main feature~2..feature
> +------------
> +
> +This reverts the last two commits from 'feature', creating revert commits on
> +top of 'main', and updates 'main' to point at the result. This is useful when
> +commits from 'feature' were previously merged or cherry-picked into 'main' and
> +need to be undone.
> +
> +NOTE: For reverting an entire merge request as a single commit (rather than
> +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
> +which can avoid unnecessary merge conflicts.
That's a good suggestion
> +
> GIT
> ---
> Part of the linkgit:git[1] suite
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 6606a2c94b..7660f7412f 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -17,6 +17,7 @@
> #include "parse-options.h"
> #include "refs.h"
> #include "revision.h"
> +#include "sequencer.h"
> #include "strmap.h"
> #include <oidset.h>
> #include <tree.h>
> @@ -26,6 +27,11 @@ enum ref_action_mode {
> REF_ACTION_PRINT,
> };
>
> +enum replay_action {
> + REPLAY_PICK,
> + REPLAY_REVERT,
> +};
sequencer.h already defines enum replay_action with an extra member so
this is a bit confusing, maybe we should use a different name?
> +static void generate_revert_message(struct strbuf *msg,
> + struct commit *commit,
> + struct repository *repo)
> +{
> + const char *out_enc = get_commit_output_encoding();
> + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);
> + const char *subject_start;
> + int subject_len;
> + char *subject;
> +
> + subject_len = find_commit_subject(message, &subject_start);
> + subject = xmemdupz(subject_start, subject_len);
> +
> + sequencer_format_revert_header(msg, subject);
> + strbuf_addstr(msg, oid_to_hex(&commit->object.oid));
> + strbuf_addstr(msg, ".\n");
It's a bit odd that sequencer_format_revert_header() actually adds the
beginning of the body but we have to add the commit oid ourselves. It
would be nicer if we could pass the oid to that function and have it
format the message for us. It's a bit tricky because the sequencer needs
to handle merges as well but it shouldn't be too difficult.
The function name is also a bit strange as header normally refers to the
commit metadata not the subject line.
> @@ -77,9 +105,14 @@ static struct commit *create_commit(struct repository *repo,
>
> commit_list_insert(parent, &parents);
> extra = read_commit_extra_headers(based_on, exclude_gpgsig);
> - find_commit_subject(message, &orig_message);
> - strbuf_addstr(&msg, orig_message);
> - author = get_author(message);
> + if (action == REPLAY_REVERT) {
> + generate_revert_message(&msg, based_on, repo);
> + author = xstrdup(git_author_info(IDENT_STRICT));
write_commit_tree() will look up the author for us if we just pass NULL
so I would just set author = NULL here or delete this line and
initialize author to NULL at the beginning of this function.
> + } else {
> + find_commit_subject(message, &orig_message);
> + strbuf_addstr(&msg, orig_message);
> + author = get_author(message);
> + }
This matches the deleted lines - good
> @@ -196,6 +230,20 @@ static void determine_replay_mode(struct repository *repo,
> }
> if (rinfo.positive_refexprs > 1)
> die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
> + } else if (*revert_name) {
> + struct object_id oid;
> + char *fullname = NULL;
> +
> + *onto = peel_committish(repo, *revert_name);
> + if (repo_dwim_ref(repo, *revert_name, strlen(*revert_name),
> + &oid, &fullname, 0) == 1) {
> + free(*revert_name);
> + *revert_name = fullname;
> + } else {
> + die(_("argument to --revert must be a reference"));
> + }
> + if (rinfo.positive_refexprs > 1)
> + die(_("cannot revert with multiple sources because ordering would be ill-defined"));
This is a copy of what we do with --advance but with a different option
name - can be factor this out into a common function that's called for
both options?
> @@ -452,10 +535,13 @@ int cmd_replay(int argc,
> }
>
> determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
> + &revert_name,
> &onto, &update_refs);
Let's not fold the line after "revert_name"
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index cf3aacf355..1c4e1cb666 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -314,4 +314,115 @@ test_expect_success 'invalid replay.refAction value' '
> test_grep "invalid.*replay.refAction.*value" error
> '
>
> +test_expect_success 'setup for revert tests' '
> + git switch -c revert-test main &&
> + test_commit R1 &&
> + test_commit R2 &&
> + test_commit R3 &&
> + git switch main
> +'
Why do we need a new branch for this? We already have topic3 built on
top of topic1 so we can test reverting commits with
git replay --revert=topic3 main..topic1
which should revert C and F
> +test_expect_success 'git replay --revert reverts commits' '
> + # Store original state
> + START=$(git rev-parse revert-test) &&
> + test_when_finished "git branch -f revert-test $START" &&
> +
> + git replay --revert revert-test revert-test~2..revert-test >output &&
> + test_must_be_empty output &&
> +
> + # Verify revert-test was updated with revert commits
> + git log --format=%s -n 5 revert-test >actual &&
> + cat >expect <<-\EOF &&
> + Revert "R3"
> + Revert "R2"
> + R3
> + R2
> + R1
> + EOF
> + test_cmp expect actual &&
> +
> + # Verify commit message format
> + test_commit_message revert-test -m "Revert \"R3\"
> +
> +This reverts commit $(git rev-parse R3)."
test_commit_message accepts the expected message on stdin so you can write
test_commit_message revert-test <<-EOF
Revert "R3"
This reverts commit $(git rev-parse R3)
EOF
which is cleaner as we don't have to escape the double quotes and the
message is nicely indented.
> +'
> +
> +test_expect_success 'git replay --revert with --ref-action=print' '
Given the ref updating code is independent of --advance, --revert etc
I'm not sure what extra coverage this test adds. If the previous test
passes what is a plausible scenario where this one fails?
> + # Store original state
> + START=$(git rev-parse revert-test) &&
> + test_when_finished "git branch -f revert-test $START" &&
> +
> + git replay --ref-action=print --revert revert-test revert-test~2..revert-test >result &&
> + test_line_count = 1 result &&
> +
> + # Verify output format: update refs/heads/revert-test <new> <old>
> + cut -f 3 -d " " result >new-tip &&
> + printf "update refs/heads/revert-test " >expect &&
> + printf "%s " $(cat new-tip) >>expect &&
> + printf "%s\n" $START >>expect &&
> + test_cmp expect result
> +'
> +
> +test_expect_success 'git replay --revert reapply behavior' '
Good idea
> + # Store original state
> + START=$(git rev-parse revert-test) &&
> + test_when_finished "git branch -f revert-test $START" &&
> +
> + # First revert R3
> + git replay --revert revert-test revert-test~1..revert-test &&
> + REVERT_R3=$(git rev-parse revert-test) &&
> +
> + # Now revert the revert (should create "Reapply" message)
> + git replay --revert revert-test revert-test~1..revert-test >output &&
> + test_must_be_empty output &&
> +
> + # Verify Reapply message
> + test_commit_message revert-test -m "Reapply \"R3\"
> +
> +This reverts commit $(git rev-parse $REVERT_R3)."
> +'
> +
> +test_expect_success 'git replay --revert with conflict' '
> + # Create a conflicting scenario
> + git switch -c revert-conflict main &&
> + test_commit C1 &&
> + echo conflict >C1.t &&
> + test_commit C2 C1.t &&
> + git switch main &&
> + echo different >C1.t &&
> + test_commit C3 C1.t &&
> +
> + # Try to revert C2 onto main (which has conflicting C3)
> + test_expect_code 1 git replay --revert main revert-conflict~1..revert-conflict
> +'
> +
> +test_expect_success 'git replay --revert reflog message' '
I think we should just check the reflog message in one of the earlier tests.
> + # Store original state
> + START=$(git rev-parse revert-test) &&
> + test_when_finished "git branch -f revert-test $START" &&
> +
> + git replay --revert revert-test revert-test~1..revert-test >output &&
> + test_must_be_empty output &&
> +
> + # Verify reflog message includes --revert and branch name
> + git reflog revert-test -1 --format=%gs >reflog-msg &&
> + echo "replay --revert revert-test" >expect-reflog &&
> + test_cmp expect-reflog reflog-msg
> +'
> +
> +test_expect_success 'git replay --revert incompatible with --contained' '
> + test_must_fail git replay --revert revert-test --contained revert-test~1..revert-test 2>error &&
> + test_grep "requires --onto" error
> +'
> +
> +test_expect_success 'git replay --revert incompatible with --onto' '
> + test_must_fail git replay --revert revert-test --onto main revert-test~1..revert-test 2>error &&
> + test_grep "cannot be used together" error
> +'
> +
> +test_expect_success 'git replay --revert incompatible with --advance' '
> + test_must_fail git replay --revert revert-test --advance main revert-test~1..revert-test 2>error &&
> + test_grep "cannot be used together" error
> +'
These last three look good.
Thanks
Phillip
next prev parent reply other threads:[~2025-12-16 16:23 UTC|newest]
Thread overview: 96+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-11-25 17:00 [PATCH 0/1] replay: add --revert option to reverse commit changes Siddharth Asthana
2025-11-25 17:00 ` [PATCH 1/1] " Siddharth Asthana
2025-11-25 19:22 ` Junio C Hamano
2025-11-25 19:30 ` Junio C Hamano
2025-11-25 19:39 ` Junio C Hamano
2025-11-25 20:06 ` Junio C Hamano
2025-11-26 19:31 ` Siddharth Asthana
2025-11-26 19:28 ` Siddharth Asthana
2025-11-26 19:26 ` Siddharth Asthana
2025-11-26 21:13 ` Junio C Hamano
2025-11-27 19:23 ` Siddharth Asthana
2025-11-26 11:10 ` Phillip Wood
2025-11-26 17:35 ` Elijah Newren
2025-11-26 18:41 ` Junio C Hamano
2025-11-26 21:17 ` Junio C Hamano
2025-11-26 23:06 ` Elijah Newren
2025-11-26 23:14 ` Junio C Hamano
2025-11-26 23:57 ` Elijah Newren
2025-11-26 19:50 ` Siddharth Asthana
2025-11-26 19:39 ` Siddharth Asthana
2025-11-27 16:21 ` Phillip Wood
2025-11-27 19:24 ` Siddharth Asthana
2025-11-25 17:25 ` [PATCH 0/1] " Johannes Schindelin
2025-11-25 18:02 ` Junio C Hamano
2025-11-26 19:18 ` Siddharth Asthana
2025-11-26 21:04 ` Junio C Hamano
2025-11-27 19:21 ` Siddharth Asthana
2025-11-27 20:17 ` Junio C Hamano
2025-11-28 8:07 ` Elijah Newren
2025-11-28 8:24 ` Siddharth Asthana
2025-11-28 16:35 ` Junio C Hamano
2025-11-28 17:07 ` Elijah Newren
2025-11-28 20:50 ` Junio C Hamano
2025-11-28 22:03 ` Elijah Newren
2025-11-29 5:59 ` Junio C Hamano
2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana
2025-12-02 20:16 ` [PATCH v2 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
2025-12-05 11:33 ` Patrick Steinhardt
2025-12-07 23:00 ` Siddharth Asthana
2025-12-08 7:07 ` Patrick Steinhardt
2026-02-11 13:03 ` Toon Claes
2026-02-11 13:40 ` Patrick Steinhardt
2026-02-11 15:23 ` Kristoffer Haugsbakk
2026-02-11 17:41 ` Junio C Hamano
2026-02-18 22:53 ` Siddharth Asthana
2025-12-02 20:16 ` [PATCH v2 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
2025-12-05 11:33 ` Patrick Steinhardt
2025-12-07 23:03 ` Siddharth Asthana
2025-12-16 16:23 ` Phillip Wood [this message]
2026-02-18 23:42 ` [PATCH v3 0/2] " Siddharth Asthana
2026-02-18 23:42 ` [PATCH v3 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
2026-02-20 17:01 ` Toon Claes
2026-02-25 21:53 ` Junio C Hamano
2026-03-06 4:55 ` Siddharth Asthana
2026-03-06 4:31 ` Siddharth Asthana
2026-02-26 14:27 ` Phillip Wood
2026-03-06 5:00 ` Siddharth Asthana
2026-02-18 23:42 ` [PATCH v3 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
2026-02-20 17:35 ` Toon Claes
2026-02-20 20:23 ` Junio C Hamano
2026-02-23 9:13 ` Christian Couder
2026-02-23 11:23 ` Toon Claes
2026-03-06 5:05 ` Siddharth Asthana
2026-02-26 14:45 ` Phillip Wood
2026-03-06 5:28 ` Siddharth Asthana
2026-03-06 15:52 ` Phillip Wood
2026-03-06 16:20 ` Siddharth Asthana
2026-03-13 5:40 ` [PATCH v4 0/2] " Siddharth Asthana
2026-03-13 5:40 ` [PATCH v4 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
2026-03-13 15:53 ` Junio C Hamano
2026-03-16 19:12 ` Toon Claes
2026-03-16 16:57 ` Phillip Wood
2026-03-13 5:40 ` [PATCH v4 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
2026-03-16 16:57 ` Phillip Wood
2026-03-16 19:52 ` Toon Claes
2026-03-17 10:11 ` Phillip Wood
2026-03-16 16:59 ` [PATCH v4 0/2] " Phillip Wood
2026-03-16 19:53 ` Toon Claes
2026-03-24 22:03 ` [PATCH v5 " Siddharth Asthana
2026-03-24 22:04 ` [PATCH v5 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
2026-03-24 22:04 ` [PATCH v5 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
2026-03-25 6:29 ` Junio C Hamano
2026-03-25 15:10 ` Toon Claes
2026-03-25 15:38 ` Siddharth Asthana
2026-03-25 16:44 ` Phillip Wood
2026-03-25 15:36 ` Siddharth Asthana
2026-03-25 20:23 ` [PATCH v6 0/2] " Siddharth Asthana
2026-03-25 20:23 ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
2026-03-25 20:23 ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
2026-03-28 4:33 ` Tian Yuchen
2026-03-29 16:17 ` Siddharth Asthana
2026-03-30 17:23 ` Tian Yuchen
2026-03-31 8:08 ` Toon Claes
2026-03-31 8:11 ` Toon Claes
2026-03-25 20:23 ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
2026-03-25 20:23 ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=c49b2375-c975-4591-b3e9-aa87771a8015@gmail.com \
--to=phillip.wood123@gmail.com \
--cc=christian.couder@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=johannes.schindelin@gmx.de \
--cc=karthik.188@gmail.com \
--cc=newren@gmail.com \
--cc=phillip.wood@dunelm.org.uk \
--cc=ps@pks.im \
--cc=siddharthasthana31@gmail.com \
--cc=toon@iotcl.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.