public inbox for git@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/1] replay: add --revert option to reverse commit changes
@ 2025-11-25 17:00 Siddharth Asthana
  2025-11-25 17:00 ` [PATCH 1/1] " Siddharth Asthana
                   ` (2 more replies)
  0 siblings, 3 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-25 17:00 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon,
	johncai86, johannes.schindelin, Siddharth Asthana

The `git replay` command currently supports cherry-picking commits for
server-side history rewriting, but lacks the ability to revert them.
This patch adds a `--revert` option to enable reversing commits directly
on bare repositories.

At GitLab, we use replay in Gitaly for efficient server-side operations.
Adding revert functionality enables us to reverse problematic commits
without client-side roundtrips, reducing network overhead.

The implementation leverages the insight that cherry-pick and revert are
essentially the same merge operation with swapped arguments. By swapping
the base and pickme trees when calling `merge_incore_nonrecursive()`, we
effectively reverse the diff direction. The existing conflict handling,
ref updates, and atomic transaction support work unchanged.

The revert message generation logic is extracted into a new shared
`sequencer_format_revert_header()` function in `sequencer.c`, allowing
code reuse between `sequencer.c` and `builtin/replay.c`. The commit
messages follow `git revert` conventions, including "Revert"/"Reapply"
prefixes and the original commit SHA.

This patch includes comprehensive tests covering various scenarios:
bare repositories, --advance mode, conflicts, reapply behavior, and
multiple commits.

Siddharth Asthana (1):
  replay: add --revert option to reverse commit changes

 Documentation/git-replay.adoc |  35 +++++++-
 builtin/replay.c              |  86 ++++++++++++++----
 sequencer.c                   |  23 +++++
 sequencer.h                   |   8 ++
 t/t3650-replay-basics.sh      | 160 ++++++++++++++++++++++++++++++++++
 5 files changed, 295 insertions(+), 17 deletions(-)

-- 
2.51.0


^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-25 17:00 [PATCH 0/1] replay: add --revert option to reverse commit changes Siddharth Asthana
@ 2025-11-25 17:00 ` Siddharth Asthana
  2025-11-25 19:22   ` Junio C Hamano
  2025-11-26 11:10   ` Phillip Wood
  2025-11-25 17:25 ` [PATCH 0/1] " Johannes Schindelin
  2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana
  2 siblings, 2 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-25 17:00 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon,
	johncai86, johannes.schindelin, Siddharth Asthana

The `git replay` command performs server-side history rewriting without
requiring a working tree. While it currently supports cherry-picking
commits, 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` option that reverses the changes introduced by the
specified commits. The implementation follows the same approach as
`sequencer.c` (around lines 2358-2390), where cherry-pick and revert
are essentially the same merge operation but with swapped arguments:

  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
  - Revert: merge(ancestor=commit, ours=current, theirs=parent)

We swap the base and pickme trees when calling
`merge_incore_nonrecursive()`, effectively reversing the diff direction.
The existing conflict handling, ref updates, and atomic transaction
support work unchanged.

The revert message generation logic (handling "Revert" and "Reapply"
cases) is extracted into a new `sequencer_format_revert_header()`
function in `sequencer.c`, which can be shared between `sequencer.c`
and `builtin/replay.c`. The `builtin/replay.c` code calls this shared
function and then appends the commit OID using `oid_to_hex()` directly,
since git replay is designed for simpler server-side operations without
the interactive features and `replay_opts` framework used by
`sequencer.c`.

The commit messages follow `git revert` conventions: prefixed with
"Revert" and including the original commit SHA. When reverting a commit
that itself starts with "Revert", the message uses "Reapply" instead.
Unlike cherry-pick which preserves the original author, revert commits
use the current user as the author, matching the behavior of `git
revert`.

Mark the option as incompatible with `--contained` since reverting
changes across multiple branches simultaneously could lead to
inconsistent repository states.

Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  35 +++++++-
 builtin/replay.c              |  86 ++++++++++++++----
 sequencer.c                   |  23 +++++
 sequencer.h                   |   8 ++
 t/t3650-replay-basics.sh      | 160 ++++++++++++++++++++++++++++++++++
 5 files changed, 295 insertions(+), 17 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index dcb26e8a8e..ad7dc08622 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>) [--ref-action[=<mode>]] [--revert] <revision-range>...
 
 DESCRIPTION
 -----------
@@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it.
 +
 The default mode can be configured via the `replay.refAction` configuration variable.
 
+--revert::
+	Revert the changes introduced by the commits in the revision range
+	instead of applying them. This reverses the diff direction and creates
+	new commits that undo the changes, similar to `git revert`.
++
+The commit messages are prefixed with "Revert" and include the original
+commit SHA. If reverting a commit whose message starts with "Revert", the new
+message will start with "Reapply" instead. The author of the new commits
+will be the current user, not the original commit author.
++
+This option is incompatible with `--contained`.
+
 <revision-range>::
 	Range of commits to replay. More than one <revision-range> can
 	be passed, but in `--advance <branch>` mode, they should have
@@ -141,6 +153,27 @@ 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 a range of commits:
+
+------------
+$ git replay --revert --onto main feature~3..feature
+------------
+
+This creates new commits on top of 'main' that reverse the changes introduced
+by the last three commits on 'feature'. The 'feature' branch is updated to
+point at the last of these revert commits. The 'main' branch is not updated
+in this case.
+
+To revert commits and advance a branch:
+
+------------
+$ git replay --revert --advance main feature~2..feature
+------------
+
+This reverts the last two commits from 'feature', applies those reverts
+on top of 'main', and updates 'main' to point at the result. The 'feature'
+branch is not updated in this case.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 6606a2c94b..7258d0bbc5 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>
@@ -57,10 +58,25 @@ static char *get_author(const char *message)
 	return NULL;
 }
 
+/*
+ * Generates a revert commit message using the shared sequencer function.
+ * We use oid_to_hex() directly instead of refer_to_commit() since git replay
+ * is designed for simpler server-side operations without interactive features.
+ */
+static void generate_revert_message(struct strbuf *msg,
+				    const char *orig_message,
+				    const struct object_id *oid)
+{
+	sequencer_format_revert_header(msg, orig_message);
+	strbuf_addstr(msg, oid_to_hex(oid));
+	strbuf_addstr(msg, ".\n");
+}
+
 static struct commit *create_commit(struct repository *repo,
 				    struct tree *tree,
 				    struct commit *based_on,
-				    struct commit *parent)
+				    struct commit *parent,
+				    int is_revert)
 {
 	struct object_id ret;
 	struct object *obj = NULL;
@@ -78,8 +94,17 @@ 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 (is_revert) {
+		generate_revert_message(&msg, orig_message, &based_on->object.oid);
+		/* For revert, use current user as author */
+		author = NULL;
+	} else {
+		/* Cherry-pick mode: use original commit message and author */
+		strbuf_addstr(&msg, orig_message);
+		author = get_author(message);
+	}
+
 	reset_ident_date();
 	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
 				 &ret, author, NULL, sign_commit, extra)) {
@@ -261,7 +286,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  kh_oid_map_t *replayed_commits,
 					  struct commit *onto,
 					  struct merge_options *merge_opt,
-					  struct merge_result *result)
+					  struct merge_result *result,
+					  int is_revert)
 {
 	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
@@ -273,21 +299,41 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	pickme_tree = repo_get_commit_tree(repo, pickme);
 	base_tree = repo_get_commit_tree(repo, base);
 
-	merge_opt->branch1 = short_commit_name(repo, replayed_base);
-	merge_opt->branch2 = short_commit_name(repo, pickme);
-	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+	if (is_revert) {
+		/* For revert: swap base and pickme to reverse the diff */
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme));
+		merge_opt->ancestor = short_commit_name(repo, pickme);
 
-	merge_incore_nonrecursive(merge_opt,
-				  base_tree,
-				  result->tree,
-				  pickme_tree,
-				  result);
+		merge_incore_nonrecursive(merge_opt,
+					  pickme_tree,
+					  result->tree,
+					  base_tree,
+					  result);
+
+		/* branch2 was allocated with xstrfmt, needs freeing */
+		free((char *)merge_opt->branch2);
+	} else {
+		/* For cherry-pick: normal order */
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = short_commit_name(repo, pickme);
+		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+		merge_incore_nonrecursive(merge_opt,
+					  base_tree,
+					  result->tree,
+					  pickme_tree,
+					  result);
+
+		/* ancestor was allocated with xstrfmt, needs freeing */
+		free((char *)merge_opt->ancestor);
+	}
 
-	free((char*)merge_opt->ancestor);
 	merge_opt->ancestor = NULL;
+	merge_opt->branch2 = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(repo, result->tree, pickme, replayed_base);
+	return create_commit(repo, result->tree, pickme, replayed_base, is_revert);
 }
 
 static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
@@ -350,6 +396,7 @@ int cmd_replay(int argc,
 	int contained = 0;
 	const char *ref_action = NULL;
 	enum ref_action_mode ref_mode;
+	int is_revert = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -366,7 +413,7 @@ int cmd_replay(int argc,
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "[--ref-action[=<mode>]] <revision-range>..."),
+		   "[--ref-action[=<mode>]] [--revert] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -381,6 +428,8 @@ int cmd_replay(int argc,
 		OPT_STRING(0, "ref-action", &ref_action,
 			   N_("mode"),
 			   N_("control ref update behavior (update|print)")),
+		OPT_BOOL(0, "revert", &is_revert,
+			 N_("revert commits instead of cherry-picking them")),
 		OPT_END()
 	};
 
@@ -395,6 +444,10 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
+	/* --revert is incompatible with --contained */
+	die_for_incompatible_opt2(is_revert, "--revert",
+				  contained, "--contained");
+
 	/* Parse ref action mode from command line or config */
 	ref_mode = get_ref_action_mode(repo, ref_action);
 
@@ -496,7 +549,8 @@ int cmd_replay(int argc,
 			die(_("replaying merge commits is not supported yet!"));
 
 		last_commit = pick_regular_commit(repo, commit, replayed_commits,
-						  onto, &merge_opt, &result);
+						  onto, &merge_opt, &result,
+						  is_revert);
 		if (!last_commit)
 			break;
 
diff --git a/sequencer.c b/sequencer.c
index 5476d39ba9..e6d82c8368 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r,
 	return res;
 }
 
+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)
+{
+	const char *revert_subject;
+
+	if (skip_prefix(orig_subject, "Revert \"", &revert_subject) &&
+	    /*
+	     * We don't touch pre-existing repeated reverts, because
+	     * theoretically these can be nested arbitrarily deeply,
+	     * thus requiring excessive complexity to deal with.
+	     */
+	    !starts_with(revert_subject, "Revert \"")) {
+		strbuf_addstr(out, "Reapply \"");
+		strbuf_addstr(out, revert_subject);
+		strbuf_addch(out, '\n');
+	} else {
+		strbuf_addstr(out, "Revert \"");
+		strbuf_addstr(out, orig_subject);
+		strbuf_addstr(out, "\"\n");
+	}
+
+	strbuf_addstr(out, "\nThis reverts commit ");
+}
+
 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
 {
 	unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
diff --git a/sequencer.h b/sequencer.h
index 719684c8a9..2d4a2d3fac 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -205,6 +205,14 @@ int todo_list_rearrange_squash(struct todo_list *todo_list);
  */
 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag);
 
+/*
+ * Formats a revert commit message header following standard Git conventions.
+ * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert
+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit " at the end.
+ * The caller should append the commit OID after calling this function.
+ */
+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject);
+
 void append_conflicts_hint(struct index_state *istate,
 		struct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode);
 enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg,
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index cf3aacf355..5fcd730b54 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -314,4 +314,164 @@ test_expect_success 'invalid replay.refAction value' '
 	test_grep "invalid.*replay.refAction.*value" error
 '
 
+test_expect_success 'using replay with --revert to revert a commit' '
+	# Revert commits D and E from topic2
+	git replay --revert --onto topic1 topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+	NEW_TOPIC2=$(cut -f 3 -d " " result) &&
+
+	# Verify the result updates the topic2 branch
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $NEW_TOPIC2 >>expect &&
+	git rev-parse topic2 >>expect &&
+
+	test_cmp expect result &&
+
+	# Verify the commit messages contain "Revert"
+	# topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A)
+	git log --format=%s $NEW_TOPIC2 >actual &&
+	test_line_count = 6 actual &&
+	head -n 1 actual >first-line &&
+	test_grep "^Revert" first-line
+'
+
+test_expect_success 'using replay with --revert on bare repo' '
+	git -C bare replay --revert --onto topic1 topic1..topic2 >result-bare &&
+
+	test_line_count = 1 result-bare &&
+	NEW_COMMIT=$(cut -f 3 -d " " result-bare) &&
+
+	# Verify the commit message contains "Revert"
+	git -C bare log --format=%s $NEW_COMMIT >actual-bare &&
+	test_line_count = 6 actual-bare &&
+	head -n 1 actual-bare >first-line-bare &&
+	test_grep "^Revert" first-line-bare
+'
+
+test_expect_success 'using replay with --revert and --advance' '
+	# Revert commits from topic2 and advance main
+	git replay --revert --advance main topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+	NEW_MAIN=$(cut -f 3 -d " " result) &&
+
+	# Verify the result updates the main branch
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $NEW_MAIN >>expect &&
+	git rev-parse main >>expect &&
+
+	test_cmp expect result &&
+
+	# Verify the commit message contains "Revert"
+	git log --format=%s $NEW_MAIN >actual &&
+	head -n 1 actual >first-line &&
+	test_grep "^Revert" first-line
+'
+
+test_expect_success 'replay with --revert fails with --contained' '
+	test_must_fail git replay --revert --contained --onto main main..topic3 2>error &&
+	test_grep "revert.*contained.*cannot be used together" error
+'
+
+test_expect_success 'verify revert actually reverses changes' '
+	# Create a branch with a simple change
+	git switch -c revert-test main &&
+	echo "new content" >test-file.txt &&
+	git add test-file.txt &&
+	git commit -m "Add test file" &&
+
+	# Revert the commit
+	git replay --revert --advance revert-test HEAD^..HEAD >result &&
+	REVERTED=$(cut -f 3 -d " " result) &&
+
+	# The file should no longer exist (reverted)
+	test_must_fail git show $REVERTED:test-file.txt
+'
+
+test_expect_success 'revert of a revert creates reapply message' '
+	# Create a commit
+	git switch -c revert-revert main &&
+	echo "content" >revert-test-2.txt &&
+	git add revert-test-2.txt &&
+	git commit -m "Add revert test file" &&
+
+	ORIGINAL=$(git rev-parse HEAD) &&
+
+	# First revert
+	git replay --revert --advance revert-revert HEAD^..HEAD >result1 &&
+	FIRST_REVERT=$(cut -f 3 -d " " result1) &&
+
+	# Check first revert message starts with "Revert"
+	git log --format=%s -1 $FIRST_REVERT >msg1 &&
+	test_grep "^Revert" msg1 &&
+
+	# Now revert the revert
+	git replay --revert --advance revert-revert $ORIGINAL..$FIRST_REVERT >result2 &&
+	REAPPLY=$(cut -f 3 -d " " result2) &&
+
+	# Check second revert message starts with "Reapply"
+	git log --format=%s -1 $REAPPLY >msg2 &&
+	test_grep "^Reapply" msg2 &&
+
+	# The file should exist again (reapplied)
+	git show $REAPPLY:revert-test-2.txt >actual &&
+	echo "content" >expected &&
+	test_cmp expected actual
+'
+
+test_expect_success 'replay --revert includes commit SHA in message' '
+	git switch -c revert-sha-test main &&
+	echo "test" >sha-test.txt &&
+	git add sha-test.txt &&
+	git commit -m "Test commit for SHA" &&
+
+	COMMIT_SHA=$(git rev-parse HEAD) &&
+	git replay --revert --advance revert-sha-test HEAD^..HEAD >result &&
+	REVERT_COMMIT=$(cut -f 3 -d " " result) &&
+
+	# Check that the commit message includes the original SHA
+	git log --format=%B -1 $REVERT_COMMIT >msg &&
+	test_grep "$COMMIT_SHA" msg
+'
+
+test_expect_success 'replay --revert with conflict' '
+	# Create a conflicting situation
+	git switch -c revert-conflict main &&
+	echo "line1" >conflict-file.txt &&
+	git add conflict-file.txt &&
+	git commit -m "Add conflict file" &&
+
+	git switch -c revert-conflict-branch HEAD^ &&
+	echo "different" >conflict-file.txt &&
+	git add conflict-file.txt &&
+	git commit -m "Different content" &&
+
+	# Try to revert the first commit onto the conflicting branch
+	test_expect_code 1 git replay --revert --onto revert-conflict-branch revert-conflict^..revert-conflict
+'
+
+test_expect_success 'replay --revert handles multiple commits' '
+	# Verify that reverting multiple commits works correctly
+	# The output should show both revert commits in the history
+	git log --format=%s topic2 >topic2-log &&
+	test_write_lines E D C B A >expected-topic2 &&
+	test_cmp expected-topic2 topic2-log &&
+
+	# Revert D and E from topic2, applying the reverts onto topic1
+	git replay --revert --onto topic1 topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+	FINAL=$(cut -f 3 -d " " result) &&
+
+	# Verify both revert commits appear in the log
+	git log --format=%s $FINAL >log &&
+	head -n 2 log >first-two &&
+	test_grep "^Revert" first-two &&
+
+	# Verify we have both "Revert D" and "Revert E"
+	test_grep "Revert.*E" log &&
+	test_grep "Revert.*D" log
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  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 17:25 ` Johannes Schindelin
  2025-11-25 18:02   ` Junio C Hamano
  2025-11-26 19:18   ` Siddharth Asthana
  2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana
  2 siblings, 2 replies; 92+ messages in thread
From: Johannes Schindelin @ 2025-11-25 17:25 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon,
	johncai86

Hi Siddharth,

On Tue, 25 Nov 2025, Siddharth Asthana wrote:

> The `git replay` command currently supports cherry-picking commits for
> server-side history rewriting, but lacks the ability to revert them.
> This patch adds a `--revert` option to enable reversing commits directly
> on bare repositories.
> 
> At GitLab, we use replay in Gitaly for efficient server-side operations.
> Adding revert functionality enables us to reverse problematic commits
> without client-side roundtrips, reducing network overhead.
> 
> The implementation leverages the insight that cherry-pick and revert are
> essentially the same merge operation with swapped arguments. By swapping
> the base and pickme trees when calling `merge_incore_nonrecursive()`, we
> effectively reverse the diff direction. The existing conflict handling,
> ref updates, and atomic transaction support work unchanged.

Are you reverting rebased Merge Requests commit by commit? If not, I would
suggest the shortcut to use `merge-tree` directly for the entire Merge
Request. That is, if `$BASE` corresponds to the base branch onto which the
Merge Request was rebased, and `$TIP` corresponds to the Merge Request's
rebased tip commit, then the following will revert that Merge Request:

	git merge-tree --merge-base $TIP HEAD $BASE

The upside is that this can potentially avoid a lot of unnecessary merge
conflicts. The downside is that it does not revert the rebased Merge
Request commit by commit.

The patch itself looks fine to me, if a bit too extensive on the side of
adding tests: Remember, a nimble test suite that catches a bug once is
better than a long-running test suite that would catch a bug several times
_iff_ it didn't tax the developer's patience so much that it is
interrupted and aborted. You probably agree that Git's CI runtimes are
already counter-productively long.

Ciao,
Johannes

> The revert message generation logic is extracted into a new shared
> `sequencer_format_revert_header()` function in `sequencer.c`, allowing
> code reuse between `sequencer.c` and `builtin/replay.c`. The commit
> messages follow `git revert` conventions, including "Revert"/"Reapply"
> prefixes and the original commit SHA.
> 
> This patch includes comprehensive tests covering various scenarios:
> bare repositories, --advance mode, conflicts, reapply behavior, and
> multiple commits.
> 
> Siddharth Asthana (1):
>   replay: add --revert option to reverse commit changes
> 
>  Documentation/git-replay.adoc |  35 +++++++-
>  builtin/replay.c              |  86 ++++++++++++++----
>  sequencer.c                   |  23 +++++
>  sequencer.h                   |   8 ++
>  t/t3650-replay-basics.sh      | 160 ++++++++++++++++++++++++++++++++++
>  5 files changed, 295 insertions(+), 17 deletions(-)
> 
> -- 
> 2.51.0
> 
> 

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  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
  1 sibling, 0 replies; 92+ messages in thread
From: Junio C Hamano @ 2025-11-25 18:02 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: Siddharth Asthana, git, christian.couder, ps, newren,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> The patch itself looks fine to me, if a bit too extensive on the side of
> adding tests: Remember, a nimble test suite that catches a bug once is
> better than a long-running test suite that would catch a bug several times
> _iff_ it didn't tax the developer's patience so much that it is
> interrupted and aborted. You probably agree that Git's CI runtimes are
> already counter-productively long.

I am not sure about some of the negations in the above, but it is
very good to point out that tests want to cover widely but without
overlap.  Two tests that try to see the tool works well under
identical scenarios can be better done as a single test.  We do not
need to catch the same bug in multiple tests, as people tend to see
test breakages, update the code to fix the first one, and continue,
wanting to fix more and different kind of breakages.

Thanks.



^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  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-26 19:26     ` Siddharth Asthana
  2025-11-26 11:10   ` Phillip Wood
  1 sibling, 2 replies; 92+ messages in thread
From: Junio C Hamano @ 2025-11-25 19:22 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> The revert message generation logic (handling "Revert" and "Reapply"
> cases) is extracted into a new `sequencer_format_revert_header()`
> function in `sequencer.c`, which can be shared between `sequencer.c`
> and `builtin/replay.c`. The `builtin/replay.c` code calls this shared
> function and then appends the commit OID using `oid_to_hex()` directly,
> since git replay is designed for simpler server-side operations without
> the interactive features and `replay_opts` framework used by
> `sequencer.c`.

When I review a patch that claims to refactor existing logic into a
separate helper function to reuse it in more places, I look at the
diffstat to see how many lines are removed.  The logic for
generating the message does not seem to be "extracted into", but
rather "duplicated to", the new helper function.  It gives the two
message sources opportunity to drift apart over time, which is not
what you want.

In do_pick_commit() where TODO_REVERT command is handled, we find a
code block that is almost identical to what this patch adds to the
new helper function; it should be rewritten to call the new helper
function or perhaps a shared helper function is introduced and
called from there and also from the sequencer_format_revert_header()
function, if there is still some impedance mismatch.  If such a
refactoring is done as a separate preliminary patch in a N-patch
series, the resulting patch series may be easier to follow (and
there may be other opportunities to reuse existing code more).

> Mark the option as incompatible with `--contained` since reverting
> changes across multiple branches simultaneously could lead to
> inconsistent repository states.

This, and the documentation part, does not seem to tell what
"inconsistent state" we are worried about.  Is it just a buggy
design of --revert can be implemented that produces wrong result
when used with --contened, or are these two options inherently try
to achieve contradicting goals?  I am guessing that it is the
latter, but if so, can we make it clear why?

> +--revert::
> +	Revert the changes introduced by the commits in the revision range
> +	instead of applying them. This reverses the diff direction and creates
> +	new commits that undo the changes, similar to `git revert`.
> ++
> +The commit messages are prefixed with "Revert" and include the original
> +commit SHA. If reverting a commit whose message starts with "Revert", the new
> +message will start with "Reapply" instead. The author of the new commits
> +will be the current user, not the original commit author.
> ++
> +This option is incompatible with `--contained`.

I have never used the `--contained` option, but is it so obvious to
those who have why these two have to be made incompatible that the
above statement does not have to be followed by "because ..."?

> @@ -141,6 +153,27 @@ 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 a range of commits:
> +
> +------------
> +$ git replay --revert --onto main feature~3..feature
> +------------
> +
> +This creates new commits on top of 'main' that reverse the changes introduced
> +by the last three commits on 'feature'. The 'feature' branch is updated to
> +point at the last of these revert commits. The 'main' branch is not updated
> +in this case.

Is there any topological requirement between 'main' and 'feature'
branches?  Naïvely, I would expect that it would be perfect if
'feature' branch has been merged to 'main' (then you'd be reverting
the top 3 commits of that branch), but that would be something you
would do to correct 'main', and not 'feature', but the description
explains this is a way to update 'feature' to lose the three topmost
commits, so I am not sure what this example really does and when it
would be useful.

> +To revert commits and advance a branch:
> +
> +------------
> +$ git replay --revert --advance main feature~2..feature
> +------------
> +
> +This reverts the last two commits from 'feature', applies those reverts
> +on top of 'main', and updates 'main' to point at the result. The 'feature'
> +branch is not updated in this case.

The same question.  If I assume that 'main' has merged 'feature'
before, this I can understand and match what I often do quite well
while working on integrating topic branches.  I may merge a topic
that is not yet well cooked enough into 'next', regret that the two
commits at the tip of the topic were premature, and revert these two
commits out of 'next', or something.  This example can be explained
well if there is topological requirement that 'main' has at least
these two commits from 'feature'.

> @@ -261,7 +286,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  					  kh_oid_map_t *replayed_commits,
>  					  struct commit *onto,
>  					  struct merge_options *merge_opt,
> -					  struct merge_result *result)
> +					  struct merge_result *result,
> +					  int is_revert)

Are there other ways to pick commit imaginable (if not planned to be
implemented), other than "revert"?  I am wondering if this is better
done as "enum { CHERRY_PICK, REVERT, } pick_variant" for readability
and maintainability.

> @@ -273,21 +299,41 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  	pickme_tree = repo_get_commit_tree(repo, pickme);
>  	base_tree = repo_get_commit_tree(repo, base);
>  
> -	merge_opt->branch1 = short_commit_name(repo, replayed_base);
> -	merge_opt->branch2 = short_commit_name(repo, pickme);
> -	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
> +	if (is_revert) {

It may be just me, but it would have been easier to follow if
!revert case is given first, as that is the common variant the
pick_regular_commit() function.

> +		/* For revert: swap base and pickme to reverse the diff */
> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
> +		merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme));

That is an overly long line (sorry, I notice these things when a
line does not even fit in 92-col terminal).

> +		merge_opt->ancestor = short_commit_name(repo, pickme);

> -	merge_incore_nonrecursive(merge_opt,
> -				  base_tree,
> -				  result->tree,
> -				  pickme_tree,
> -				  result);
> +		merge_incore_nonrecursive(merge_opt,
> +					  pickme_tree,
> +					  result->tree,
> +					  base_tree,
> +					  result);

OK.  These are applications of the standard 3-way merge trick to
(ab)use ancestor to implement cherry-pick and revert.  Looking good.

> +
> +		/* branch2 was allocated with xstrfmt, needs freeing */
> +		free((char *)merge_opt->branch2);
> +	} else {
> +		/* For cherry-pick: normal order */
> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
> +		merge_opt->branch2 = short_commit_name(repo, pickme);
> +		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
> +
> +		merge_incore_nonrecursive(merge_opt,
> +					  base_tree,
> +					  result->tree,
> +					  pickme_tree,
> +					  result);
> +
> +		/* ancestor was allocated with xstrfmt, needs freeing */
> +		free((char *)merge_opt->ancestor);

And the "else" block has the original sequence of statements.

> +	}
>  
> -	free((char*)merge_opt->ancestor);
>  	merge_opt->ancestor = NULL;
> +	merge_opt->branch2 = NULL;

Not a new problem, but what is the point of setting these two (but
not branch1) to NULL?  If a later caller misuses ->ancestor left
behind without setting its own, it would result in an access after
free, but if such a caller misuses ->branch1 left behind without
setting its own, because it is not allocated, it won't be an access
after free, *but* it is nevertheless wrong as the string in ->branch1
is *not* computed suitably for that caller, isn't it?

>  	if (!result->clean)
>  		return NULL;
> -	return create_commit(repo, result->tree, pickme, replayed_base);
> +	return create_commit(repo, result->tree, pickme, replayed_base, is_revert);
>  }


> @@ -350,6 +396,7 @@ int cmd_replay(int argc,
>  	int contained = 0;
>  	const char *ref_action = NULL;
>  	enum ref_action_mode ref_mode;
> +	int is_revert = 0;

Ditto on "revert,cherry-pick".

> diff --git a/sequencer.c b/sequencer.c
> index 5476d39ba9..e6d82c8368 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r,
>  	return res;
>  }
>  
> +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)
> +{
> +	const char *revert_subject;
> +
> +	if (skip_prefix(orig_subject, "Revert \"", &revert_subject) &&
> +	    /*
> +	     * We don't touch pre-existing repeated reverts, because
> +	     * theoretically these can be nested arbitrarily deeply,
> +	     * thus requiring excessive complexity to deal with.
> +	     */
> +	    !starts_with(revert_subject, "Revert \"")) {
> +		strbuf_addstr(out, "Reapply \"");
> +		strbuf_addstr(out, revert_subject);
> +		strbuf_addch(out, '\n');
> +	} else {
> +		strbuf_addstr(out, "Revert \"");
> +		strbuf_addstr(out, orig_subject);
> +		strbuf_addstr(out, "\"\n");
> +	}
> +
> +	strbuf_addstr(out, "\nThis reverts commit ");
> +}
> +

Dedup with do_pick_commit() where this was taken from.  Possibly in
a separte patch before the main one.


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  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-26 19:26     ` Siddharth Asthana
  1 sibling, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2025-11-25 19:30 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Junio C Hamano <gitster@pobox.com> writes:

> In do_pick_commit() where TODO_REVERT command is handled, we find a
> code block that is almost identical to what this patch adds to the
> new helper function; it should be rewritten to call the new helper
> function or perhaps a shared helper function is introduced and
> called from there and also from the sequencer_format_revert_header()
> function, if there is still some impedance mismatch.  If such a
> refactoring is done as a separate preliminary patch in a N-patch
> series, the resulting patch series may be easier to follow (and
> there may be other opportunities to reuse existing code more).
> ...
>> diff --git a/sequencer.c b/sequencer.c
>> index 5476d39ba9..e6d82c8368 100644
>> --- a/sequencer.c
>> +++ b/sequencer.c
>> @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r,
>>  	return res;
>>  }
>>  
>> +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)
>> +{
>> +	const char *revert_subject;
>> +
>> +	if (skip_prefix(orig_subject, "Revert \"", &revert_subject) &&
>> +	    /*
>> +	     * We don't touch pre-existing repeated reverts, because
>> +	     * theoretically these can be nested arbitrarily deeply,
>> +	     * thus requiring excessive complexity to deal with.
>> +	     */
>> +	    !starts_with(revert_subject, "Revert \"")) {
>> +		strbuf_addstr(out, "Reapply \"");
>> +		strbuf_addstr(out, revert_subject);
>> +		strbuf_addch(out, '\n');
>> +	} else {
>> +		strbuf_addstr(out, "Revert \"");
>> +		strbuf_addstr(out, orig_subject);
>> +		strbuf_addstr(out, "\"\n");
>> +	}
>> +
>> +	strbuf_addstr(out, "\nThis reverts commit ");
>> +}
>> +
>
> Dedup with do_pick_commit() where this was taken from.  Possibly in
> a separte patch before the main one.

Forgot to attach this at the end.  What I meant was that something
along this line may be a good starting point.

 sequencer.c | 14 +-------------
 1 file changed, 1 insertion(+), 13 deletions(-)

diff --git c/sequencer.c w/sequencer.c
index e6d82c8368..29909952d4 100644
--- c/sequencer.c
+++ w/sequencer.c
@@ -2365,20 +2365,8 @@ static int do_pick_commit(struct repository *r,
 		if (opts->commit_use_reference) {
 			strbuf_commented_addf(&ctx->message, comment_line_str,
 				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
-		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
-			   /*
-			    * We don't touch pre-existing repeated reverts, because
-			    * theoretically these can be nested arbitrarily deeply,
-			    * thus requiring excessive complexity to deal with.
-			    */
-			   !starts_with(orig_subject, "Revert \"")) {
-			strbuf_addstr(&ctx->message, "Reapply \"");
-			strbuf_addstr(&ctx->message, orig_subject);
-			strbuf_addstr(&ctx->message, "\n");
 		} else {
-			strbuf_addstr(&ctx->message, "Revert \"");
-			strbuf_addstr(&ctx->message, msg.subject);
-			strbuf_addstr(&ctx->message, "\"\n");
+			sequencer_format_revert_header(&ctx->message, msg.subject);
 		}
 		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
 		refer_to_commit(opts, &ctx->message, commit);

^ permalink raw reply related	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  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:28         ` Siddharth Asthana
  0 siblings, 2 replies; 92+ messages in thread
From: Junio C Hamano @ 2025-11-25 19:39 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Junio C Hamano <gitster@pobox.com> writes:

>> Dedup with do_pick_commit() where this was taken from.  Possibly in
>> a separte patch before the main one.
>
> Forgot to attach this at the end.  What I meant was that something
> along this line may be a good starting point.
>
>  sequencer.c | 14 +-------------
>  1 file changed, 1 insertion(+), 13 deletions(-)
>
> diff --git c/sequencer.c w/sequencer.c
> index e6d82c8368..29909952d4 100644
> --- c/sequencer.c
> +++ w/sequencer.c
> @@ -2365,20 +2365,8 @@ static int do_pick_commit(struct repository *r,
>  		if (opts->commit_use_reference) {
>  			strbuf_commented_addf(&ctx->message, comment_line_str,
>  				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
> -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
> -			   /*
> -			    * We don't touch pre-existing repeated reverts, because
> -			    * theoretically these can be nested arbitrarily deeply,
> -			    * thus requiring excessive complexity to deal with.
> -			    */
> -			   !starts_with(orig_subject, "Revert \"")) {
> -			strbuf_addstr(&ctx->message, "Reapply \"");
> -			strbuf_addstr(&ctx->message, orig_subject);
> -			strbuf_addstr(&ctx->message, "\n");
>  		} else {
> -			strbuf_addstr(&ctx->message, "Revert \"");
> -			strbuf_addstr(&ctx->message, msg.subject);
> -			strbuf_addstr(&ctx->message, "\"\n");
> +			sequencer_format_revert_header(&ctx->message, msg.subject);
>  		}
>  		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>  		refer_to_commit(opts, &ctx->message, commit);

By the way, I probably would not be queuing this version today, as
this has obvious conflict with a large code movement made by
Patrick's "history" series, which itself is expecting a reroll.

Perhaps collect review comments on this iteration a bit more and
wait for that other topic to be rerolled, and if it turns out to be
solid enough, base a v2 of this patch on top of it?

Thanks.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  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
  1 sibling, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2025-11-25 20:06 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Junio C Hamano <gitster@pobox.com> writes:

> By the way, I probably would not be queuing this version today, as
> this has obvious conflict with a large code movement made by
> Patrick's "history" series, which itself is expecting a reroll.
>
> Perhaps collect review comments on this iteration a bit more and
> wait for that other topic to be rerolled, and if it turns out to be
> solid enough, base a v2 of this patch on top of it?

While I cannot test it with other topics, I had a chance to run
tests after applying the patch directly on top of 'master':

    $ make CC=clang SANITIZE=address,leak test
    ...
    Test Summary Report
    -------------------
    t3650-replay-basics.sh                           (Wstat: 256 (exited 1) Tests: 31 Failed: 5)
      Failed tests:  23-25, 27, 31
      Non-zero exit status: 1

The first failure was this one

    expecting success of 3650.23 'using replay with --revert to revert a commit': 
            # Revert commits D and E from topic2
            git replay --revert --onto topic1 topic1..topic2 >result &&

            test_line_count = 1 result &&
            NEW_TOPIC2=$(cut -f 3 -d " " result) &&

            # Verify the result updates the topic2 branch
            printf "update refs/heads/topic2 " >expect &&
            printf "%s " $NEW_TOPIC2 >>expect &&
            git rev-parse topic2 >>expect &&

            test_cmp expect result &&

            # Verify the commit messages contain "Revert"
            # topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A)
            git log --format=%s $NEW_TOPIC2 >actual &&
            test_line_count = 6 actual &&
            head -n 1 actual >first-line &&
            test_grep "^Revert" first-line

    test_line_count: line count for result != 1

The "result" file has 0 bytes (hence 0 lines).

Actually, address or leak sanitizing build is not needed to
reproduce this problem, it seems.

    $ make CC=clang test

Was sufficient to see the same first failure.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-25 17:00 ` [PATCH 1/1] " Siddharth Asthana
  2025-11-25 19:22   ` Junio C Hamano
@ 2025-11-26 11:10   ` Phillip Wood
  2025-11-26 17:35     ` Elijah Newren
  2025-11-26 19:39     ` Siddharth Asthana
  1 sibling, 2 replies; 92+ messages in thread
From: Phillip Wood @ 2025-11-26 11:10 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood, karthik.188,
	code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin

Hi Siddharth

On 25/11/2025 17:00, Siddharth Asthana wrote:
> 
> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> index dcb26e8a8e..ad7dc08622 100644
> --- a/Documentation/git-replay.adoc
> +++ b/Documentation/git-replay.adoc
> @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it.
> [...]
> +To revert a range of commits:
> +
> +------------
> +$ git replay --revert --onto main feature~3..feature
> +------------
> +
> +This creates new commits on top of 'main' that reverse the changes introduced
> +by the last three commits on 'feature'. The 'feature' branch is updated to
> +point at the last of these revert commits. The 'main' branch is not updated
> +in this case.

I'm struggling to understand when I'd want to do this. Why would I want 
to update 'feature' to point to the reverted version of its last tree 
commits rebased onto 'main'? In order to understand I ran the first 
tests case which does

	git replay --onto topic1 --revert topic1..topic2

after fixing it by adding --ref-action=print the resulting commit log 
looks like

commit d337fab78e90008835f74e890039b464a0308cbe
Author: author@name <bogus@email@address>
Date:   Thu Apr 7 15:30:13 2005 -0700

     Revert "E
     "

     This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276.

commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a
Author: author@name <bogus@email@address>
Date:   Thu Apr 7 15:30:13 2005 -0700

     Revert "D
     "

     This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7.


The commits are empty because the original they are reverting each 
create a new file which is then present in the base revision but not in 
either of the merge heads when we revert. This suggests to me that it is 
not a very realistic test and I'm still scratching my head to see where 
"git replay --onto <commit> --revert" is useful.

If '--revert' does not make sense with '--onto' then perhaps it should 
be a new mode that takes a ref and acts like '--advance' but reverts the 
commits rather than cherry-picking them. When reverting a range of 
commits it would reduce the likelihood of conflicts to revert then in 
reverse order so we should either recommend passing '--reverse' or make 
that the default when '--revert' is given.

As you can see in the log output above the new function to format the 
revert subject lines is buggy. If you had used test_commit_message() to 
check the commit message, rather than just grepping for ^Revert the 
tests would have picked that up.

Thanks

Phillip


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  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 19:50       ` Siddharth Asthana
  2025-11-26 19:39     ` Siddharth Asthana
  1 sibling, 2 replies; 92+ messages in thread
From: Elijah Newren @ 2025-11-26 17:35 UTC (permalink / raw)
  To: phillip.wood
  Cc: Siddharth Asthana, git, christian.couder, ps, gitster,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Wed, Nov 26, 2025 at 3:10 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Siddharth
>
> On 25/11/2025 17:00, Siddharth Asthana wrote:
> >
> > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> > index dcb26e8a8e..ad7dc08622 100644
> > --- a/Documentation/git-replay.adoc
> > +++ b/Documentation/git-replay.adoc
> > @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it.
> > [...]
> > +To revert a range of commits:
> > +
> > +------------
> > +$ git replay --revert --onto main feature~3..feature
> > +------------
> > +
> > +This creates new commits on top of 'main' that reverse the changes introduced
> > +by the last three commits on 'feature'. The 'feature' branch is updated to
> > +point at the last of these revert commits. The 'main' branch is not updated
> > +in this case.
>
> I'm struggling to understand when I'd want to do this. Why would I want
> to update 'feature' to point to the reverted version of its last tree
> commits rebased onto 'main'? In order to understand I ran the first
> tests case which does
>
>         git replay --onto topic1 --revert topic1..topic2
>
> after fixing it by adding --ref-action=print the resulting commit log
> looks like
>
> commit d337fab78e90008835f74e890039b464a0308cbe
> Author: author@name <bogus@email@address>
> Date:   Thu Apr 7 15:30:13 2005 -0700
>
>      Revert "E
>      "
>
>      This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276.
>
> commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a
> Author: author@name <bogus@email@address>
> Date:   Thu Apr 7 15:30:13 2005 -0700
>
>      Revert "D
>      "
>
>      This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7.
>
>
> The commits are empty because the original they are reverting each
> create a new file which is then present in the base revision but not in
> either of the merge heads when we revert. This suggests to me that it is
> not a very realistic test and I'm still scratching my head to see where
> "git replay --onto <commit> --revert" is useful.
>
> If '--revert' does not make sense with '--onto' then perhaps it should
> be a new mode that takes a ref and acts like '--advance' but reverts the
> commits rather than cherry-picking them. When reverting a range of
> commits it would reduce the likelihood of conflicts to revert then in
> reverse order so we should either recommend passing '--reverse' or make
> that the default when '--revert' is given.
>
> As you can see in the log output above the new function to format the
> revert subject lines is buggy. If you had used test_commit_message() to
> check the commit message, rather than just grepping for ^Revert the
> tests would have picked that up.
>
> Thanks
>
> Phillip

I was going to say the same thing, but from a different angle.

The sequencer in git is used for three different types of operations:
rebasing, cherry-picking, and reverting a range (with a sequence of
reverts rather than one big revert).  In replay, these correspond to
--onto, --advance, and the new thing you are trying to add.  As such,
it should be its own new mode.

(I do tend to see ranges reverted by a single big revert, the way
Johannes suggested, rather than as a range of individual reverts, so
to me the utility of the new mode looks low, but perhaps others find
more utility in it.  Or maybe the intent is to only use it with a
revision range that is only one commit long?)

Phillip also went into more detail about why "--onto $COMMIT --revert"
specifically doesn't make sense.  I'd also say "--advance $BRANCH
--revert" doesn't read well because to users, "revert" means going
back while "advance" means going forward, so it's a rather confusing
command line to make them wrap their head around.

And yes, Siddharth, you were right that the new mode should be
incompatible with --contained, but that's because --contained is a
special modifier of --onto.  --onto, --advance, and --revert are three
different modes that are incompatible with each other.  Once you've
checked for that incompatibility between the three modes, then you can
either check that whenever --contained is specified, either --onto is
as well, or neither --advance nor --revert are.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  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 19:50       ` Siddharth Asthana
  1 sibling, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2025-11-26 18:41 UTC (permalink / raw)
  To: Elijah Newren
  Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Elijah Newren <newren@gmail.com> writes:

>> I'm struggling to understand when I'd want to do this. Why would I want
>> to update 'feature' to point to the reverted version of its last tree
>> commits rebased onto 'main'?
>> ...
> I was going to say the same thing, but from a different angle.
>
> The sequencer in git is used for three different types of operations:
> rebasing, cherry-picking, and reverting a range (with a sequence of
> reverts rather than one big revert).  In replay, these correspond to
> --onto, --advance, and the new thing you are trying to add.  As such,
> it should be its own new mode.

This is a great comment that clarifies what the problem is with this.

> And yes, Siddharth, you were right that the new mode should be
> incompatible with --contained, but that's because --contained is a
> special modifier of --onto.  --onto, --advance, and --revert are three
> different modes that are incompatible with each other.

This answers the question I had on the patch perfectly.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  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
  1 sibling, 1 reply; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-26 19:18 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: git, christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon,
	johncai86


On 25/11/25 22:55, Johannes Schindelin wrote:
> Hi Siddharth,
>
> On Tue, 25 Nov 2025, Siddharth Asthana wrote:
>
>> The `git replay` command currently supports cherry-picking commits for
>> server-side history rewriting, but lacks the ability to revert them.
>> This patch adds a `--revert` option to enable reversing commits directly
>> on bare repositories.
>>
>> At GitLab, we use replay in Gitaly for efficient server-side operations.
>> Adding revert functionality enables us to reverse problematic commits
>> without client-side roundtrips, reducing network overhead.
>>
>> The implementation leverages the insight that cherry-pick and revert are
>> essentially the same merge operation with swapped arguments. By swapping
>> the base and pickme trees when calling `merge_incore_nonrecursive()`, we
>> effectively reverse the diff direction. The existing conflict handling,
>> ref updates, and atomic transaction support work unchanged.


Hi Johannes,
Thanks for the review!


> Are you reverting rebased Merge Requests commit by commit? If not, I would
> suggest the shortcut to use `merge-tree` directly for the entire Merge
> Request.


That's a great point. At GitLab, we have use cases for both approaches:

1.  For quick undoing an entire MR, the `merge-tree` approach you 
suggest is indeed more efficient and avoids unnecessary intermediate 
conflicts.

2. For commit-by-commit reverts, we need individual revert commits with 
proper attribution (which commit is being reverted) for auditability and 
history clarity. This is particularly useful when only specific commits 
from a merged branch need to be reverted.


I will add a note in the documentation mentioning the `merge-tree` 
alternative for whole-MR reverts.


> That is, if `$BASE` corresponds to the base branch onto which the
> Merge Request was rebased, and `$TIP` corresponds to the Merge Request's
> rebased tip commit, then the following will revert that Merge Request:
>
> 	git merge-tree --merge-base $TIP HEAD $BASE
>
> The upside is that this can potentially avoid a lot of unnecessary merge
> conflicts. The downside is that it does not revert the rebased Merge
> Request commit by commit.
>
> The patch itself looks fine to me, if a bit too extensive on the side of
> adding tests


Agreed. Looking at the tests again, I can consolidate several of them:
- The bare repo test can be merged with the basic revert test
- The multiple commits test overlaps with the basic functionality

I will trim down to essential coverage in v2: basic revert, conflict 
handling, and Reapply behavior.

Thanks,
Siddharth


> : Remember, a nimble test suite that catches a bug once is
> better than a long-running test suite that would catch a bug several times
> _iff_ it didn't tax the developer's patience so much that it is
> interrupted and aborted. You probably agree that Git's CI runtimes are
> already counter-productively long.
>
> Ciao,
> Johannes
>
>> The revert message generation logic is extracted into a new shared
>> `sequencer_format_revert_header()` function in `sequencer.c`, allowing
>> code reuse between `sequencer.c` and `builtin/replay.c`. The commit
>> messages follow `git revert` conventions, including "Revert"/"Reapply"
>> prefixes and the original commit SHA.
>>
>> This patch includes comprehensive tests covering various scenarios:
>> bare repositories, --advance mode, conflicts, reapply behavior, and
>> multiple commits.
>>
>> Siddharth Asthana (1):
>>    replay: add --revert option to reverse commit changes
>>
>>   Documentation/git-replay.adoc |  35 +++++++-
>>   builtin/replay.c              |  86 ++++++++++++++----
>>   sequencer.c                   |  23 +++++
>>   sequencer.h                   |   8 ++
>>   t/t3650-replay-basics.sh      | 160 ++++++++++++++++++++++++++++++++++
>>   5 files changed, 295 insertions(+), 17 deletions(-)
>>
>> -- 
>> 2.51.0
>>
>>

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-25 19:22   ` Junio C Hamano
  2025-11-25 19:30     ` Junio C Hamano
@ 2025-11-26 19:26     ` Siddharth Asthana
  2025-11-26 21:13       ` Junio C Hamano
  1 sibling, 1 reply; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-26 19:26 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 26/11/25 00:52, Junio C Hamano wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> The revert message generation logic (handling "Revert" and "Reapply"
>> cases) is extracted into a new `sequencer_format_revert_header()`
>> function in `sequencer.c`, which can be shared between `sequencer.c`
>> and `builtin/replay.c`. The `builtin/replay.c` code calls this shared
>> function and then appends the commit OID using `oid_to_hex()` directly,
>> since git replay is designed for simpler server-side operations without
>> the interactive features and `replay_opts` framework used by
>> `sequencer.c`.
> When I review a patch that claims to refactor existing logic into a
> separate helper function to reuse it in more places, I look at the
> diffstat to see how many lines are removed.


You are right - in v1 I added the helper but didn't update 
do_pick_commit() to use it. I have fixed this in my local tree; the 
dedup change is:


     sequencer.c | 14 +-------------
     1 file changed, 1 insertion(+), 13 deletions(-)


> The logic for
> generating the message does not seem to be "extracted into", but
> rather "duplicated to", the new helper function.  It gives the two
> message sources opportunity to drift apart over time, which is not
> what you want.
>
> In do_pick_commit() where TODO_REVERT command is handled, we find a
> code block that is almost identical to what this patch adds to the
> new helper function; it should be rewritten to call the new helper
> function or perhaps a shared helper function is introduced and
> called from there and also from the sequencer_format_revert_header()
> function, if there is still some impedance mismatch.  If such a
> refactoring is done as a separate preliminary patch in a N-patch
> series, the resulting patch series may be easier to follow (and
> there may be other opportunities to reuse existing code more).
>
>> Mark the option as incompatible with `--contained` since reverting
>> changes across multiple branches simultaneously could lead to
>> inconsistent repository states.
> This, and the documentation part, does not seem to tell what
> "inconsistent state" we are worried about.


Elijah's reply clarified this perfectly - `--contained` is a modifier 
for `--onto`, and as he points out, `--revert` should be a new mode 
entirely, not a modifier. Once `--revert` is its own mode (like `--onto` 
and `--advance`), the incompatibility with `--contained` becomes clear:
`--contained` only makes sense with `--onto`.


> Is it just a buggy
> design of --revert can be implemented that produces wrong result
> when used with --contened, or are these two options inherently try
> to achieve contradicting goals?  I am guessing that it is the
> latter, but if so, can we make it clear why?
>
>> +--revert::
>> +	Revert the changes introduced by the commits in the revision range
>> +	instead of applying them. This reverses the diff direction and creates
>> +	new commits that undo the changes, similar to `git revert`.
>> ++
>> +The commit messages are prefixed with "Revert" and include the original
>> +commit SHA. If reverting a commit whose message starts with "Revert", the new
>> +message will start with "Reapply" instead. The author of the new commits
>> +will be the current user, not the original commit author.
>> ++
>> +This option is incompatible with `--contained`.
> I have never used the `--contained` option, but is it so obvious to
> those who have why these two have to be made incompatible that the
> above statement does not have to be followed by "because ..."?
>
>> @@ -141,6 +153,27 @@ 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 a range of commits:
>> +
>> +------------
>> +$ git replay --revert --onto main feature~3..feature
>> +------------
>> +
>> +This creates new commits on top of 'main' that reverse the changes introduced
>> +by the last three commits on 'feature'. The 'feature' branch is updated to
>> +point at the last of these revert commits. The 'main' branch is not updated
>> +in this case.
> Is there any topological requirement between 'main' and 'feature'
> branches?


Yes, and I failed to explain this. For reverts to produce meaningful 
non-empty commits, the commits being reverted should already be in the 
target branch's history. I will clarify the examples to show this 
topology requirement explicitly.


>    Naïvely, I would expect that it would be perfect if
> 'feature' branch has been merged to 'main' (then you'd be reverting
> the top 3 commits of that branch), but that would be something you
> would do to correct 'main', and not 'feature', but the description
> explains this is a way to update 'feature' to lose the three topmost
> commits, so I am not sure what this example really does and when it
> would be useful.
>
>> +To revert commits and advance a branch:
>> +
>> +------------
>> +$ git replay --revert --advance main feature~2..feature
>> +------------
>> +
>> +This reverts the last two commits from 'feature', applies those reverts
>> +on top of 'main', and updates 'main' to point at the result. The 'feature'
>> +branch is not updated in this case.
> The same question.  If I assume that 'main' has merged 'feature'
> before, this I can understand and match what I often do quite well
> while working on integrating topic branches.  I may merge a topic
> that is not yet well cooked enough into 'next', regret that the two
> commits at the tip of the topic were premature, and revert these two
> commits out of 'next', or something.  This example can be explained
> well if there is topological requirement that 'main' has at least
> these two commits from 'feature'.
>
>> @@ -261,7 +286,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>   					  kh_oid_map_t *replayed_commits,
>>   					  struct commit *onto,
>>   					  struct merge_options *merge_opt,
>> -					  struct merge_result *result)
>> +					  struct merge_result *result,
>> +					  int is_revert)
> Are there other ways to pick commit imaginable (if not planned to be
> implemented), other than "revert"?  I am wondering if this is better
> done as "enum { CHERRY_PICK, REVERT, } pick_variant" for readability
> and maintainability.


Good point. An enum would be clearer and more maintainable. I wll change 
to `enum replay_action { REPLAY_PICK, REPLAY_REVERT }`.


>
>> @@ -273,21 +299,41 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>   	pickme_tree = repo_get_commit_tree(repo, pickme);
>>   	base_tree = repo_get_commit_tree(repo, base);
>>   
>> -	merge_opt->branch1 = short_commit_name(repo, replayed_base);
>> -	merge_opt->branch2 = short_commit_name(repo, pickme);
>> -	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
>> +	if (is_revert) {
> It may be just me, but it would have been easier to follow if
> !revert case is given first, as that is the common variant the
> pick_regular_commit() function.


Makes sense - the common case (cherry-pick) should come first. I will 
reorder the if/else.


>
>> +		/* For revert: swap base and pickme to reverse the diff */
>> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
>> +		merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme));
> That is an overly long line (sorry, I notice these things when a
> line does not even fit in 92-col terminal).


Fixed in my local tree by introducing a `pickme_name` variable.


>
>> +		merge_opt->ancestor = short_commit_name(repo, pickme);
>> -	merge_incore_nonrecursive(merge_opt,
>> -				  base_tree,
>> -				  result->tree,
>> -				  pickme_tree,
>> -				  result);
>> +		merge_incore_nonrecursive(merge_opt,
>> +					  pickme_tree,
>> +					  result->tree,
>> +					  base_tree,
>> +					  result);
> OK.  These are applications of the standard 3-way merge trick to
> (ab)use ancestor to implement cherry-pick and revert.  Looking good.
>
>> +
>> +		/* branch2 was allocated with xstrfmt, needs freeing */
>> +		free((char *)merge_opt->branch2);
>> +	} else {
>> +		/* For cherry-pick: normal order */
>> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
>> +		merge_opt->branch2 = short_commit_name(repo, pickme);
>> +		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
>> +
>> +		merge_incore_nonrecursive(merge_opt,
>> +					  base_tree,
>> +					  result->tree,
>> +					  pickme_tree,
>> +					  result);
>> +
>> +		/* ancestor was allocated with xstrfmt, needs freeing */
>> +		free((char *)merge_opt->ancestor);
> And the "else" block has the original sequence of statements.
>
>> +	}
>>   
>> -	free((char*)merge_opt->ancestor);
>>   	merge_opt->ancestor = NULL;
>> +	merge_opt->branch2 = NULL;
> Not a new problem, but what is the point of setting these two (but
> not branch1) to NULL?


You're right, this is inconsistent. The intent is to prevent 
use-after-free, but setting only some fields to NULL is incomplete. I 
will either set all three to NULL or add a comment explaining the rationale.


> If a later caller misuses ->ancestor left
> behind without setting its own, it would result in an access after
> free, but if such a caller misuses ->branch1 left behind without
> setting its own, because it is not allocated, it won't be an access
> after free, *but* it is nevertheless wrong as the string in ->branch1
> is *not* computed suitably for that caller, isn't it?
>
>>   	if (!result->clean)
>>   		return NULL;
>> -	return create_commit(repo, result->tree, pickme, replayed_base);
>> +	return create_commit(repo, result->tree, pickme, replayed_base, is_revert);
>>   }
>
>> @@ -350,6 +396,7 @@ int cmd_replay(int argc,
>>   	int contained = 0;
>>   	const char *ref_action = NULL;
>>   	enum ref_action_mode ref_mode;
>> +	int is_revert = 0;
> Ditto on "revert,cherry-pick".
>
>> diff --git a/sequencer.c b/sequencer.c
>> index 5476d39ba9..e6d82c8368 100644
>> --- a/sequencer.c
>> +++ b/sequencer.c
>> @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r,
>>   	return res;
>>   }
>>   
>> +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)
>> +{
>> +	const char *revert_subject;
>> +
>> +	if (skip_prefix(orig_subject, "Revert \"", &revert_subject) &&
>> +	    /*
>> +	     * We don't touch pre-existing repeated reverts, because
>> +	     * theoretically these can be nested arbitrarily deeply,
>> +	     * thus requiring excessive complexity to deal with.
>> +	     */
>> +	    !starts_with(revert_subject, "Revert \"")) {
>> +		strbuf_addstr(out, "Reapply \"");
>> +		strbuf_addstr(out, revert_subject);
>> +		strbuf_addch(out, '\n');
>> +	} else {
>> +		strbuf_addstr(out, "Revert \"");
>> +		strbuf_addstr(out, orig_subject);
>> +		strbuf_addstr(out, "\"\n");
>> +	}
>> +
>> +	strbuf_addstr(out, "\nThis reverts commit ");
>> +}
>> +
> Dedup with do_pick_commit() where this was taken from.  Possibly in
> a separte patch before the main one.


I have applied your suggested patch and will split this into a 2-patch 
series: (1) extract and reuse sequencer_format_revert_header(), (2) add 
--revert to replay.

Thanks,
Siddharth


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-25 19:39       ` Junio C Hamano
  2025-11-25 20:06         ` Junio C Hamano
@ 2025-11-26 19:28         ` Siddharth Asthana
  1 sibling, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-26 19:28 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 26/11/25 01:09, Junio C Hamano wrote:
> Junio C Hamano <gitster@pobox.com> writes:
>
>>> Dedup with do_pick_commit() where this was taken from.  Possibly in
>>> a separte patch before the main one.
>> Forgot to attach this at the end.  What I meant was that something
>> along this line may be a good starting point.
>>
>>   sequencer.c | 14 +-------------
>>   1 file changed, 1 insertion(+), 13 deletions(-)
>>
>> diff --git c/sequencer.c w/sequencer.c
>> index e6d82c8368..29909952d4 100644
>> --- c/sequencer.c
>> +++ w/sequencer.c
>> @@ -2365,20 +2365,8 @@ static int do_pick_commit(struct repository *r,
>>   		if (opts->commit_use_reference) {
>>   			strbuf_commented_addf(&ctx->message, comment_line_str,
>>   				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
>> -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
>> -			   /*
>> -			    * We don't touch pre-existing repeated reverts, because
>> -			    * theoretically these can be nested arbitrarily deeply,
>> -			    * thus requiring excessive complexity to deal with.
>> -			    */
>> -			   !starts_with(orig_subject, "Revert \"")) {
>> -			strbuf_addstr(&ctx->message, "Reapply \"");
>> -			strbuf_addstr(&ctx->message, orig_subject);
>> -			strbuf_addstr(&ctx->message, "\n");
>>   		} else {
>> -			strbuf_addstr(&ctx->message, "Revert \"");
>> -			strbuf_addstr(&ctx->message, msg.subject);
>> -			strbuf_addstr(&ctx->message, "\"\n");
>> +			sequencer_format_revert_header(&ctx->message, msg.subject);
>>   		}
>>   		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>   		refer_to_commit(opts, &ctx->message, commit);
> By the way, I probably would not be queuing this version today, as
> this has obvious conflict with a large code movement made by
> Patrick's "history" series, which itself is expecting a reroll.
>
> Perhaps collect review comments on this iteration a bit more and
> wait for that other topic to be rerolled, and if it turns out to be
> solid enough, base a v2 of this patch on top of it?


Understood. I will wait for Patrick's "history" series to be rerolled 
and base v2 on top of that to avoid conflicts. In the meantime, I will 
address all the review feedback locally.

Thanks,
Siddharth


>
> Thanks.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-25 20:06         ` Junio C Hamano
@ 2025-11-26 19:31           ` Siddharth Asthana
  0 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-26 19:31 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 26/11/25 01:36, Junio C Hamano wrote:
> Junio C Hamano <gitster@pobox.com> writes:
>
>> By the way, I probably would not be queuing this version today, as
>> this has obvious conflict with a large code movement made by
>> Patrick's "history" series, which itself is expecting a reroll.
>>
>> Perhaps collect review comments on this iteration a bit more and
>> wait for that other topic to be rerolled, and if it turns out to be
>> solid enough, base a v2 of this patch on top of it?
> While I cannot test it with other topics, I had a chance to run
> tests after applying the patch directly on top of 'master':
>
>      $ make CC=clang SANITIZE=address,leak test
>      ...
>      Test Summary Report
>      -------------------
>      t3650-replay-basics.sh                           (Wstat: 256 (exited 1) Tests: 31 Failed: 5)
>        Failed tests:  23-25, 27, 31
>        Non-zero exit status: 1
>
> The first failure was this one
>
>      expecting success of 3650.23 'using replay with --revert to revert a commit':
>              # Revert commits D and E from topic2
>              git replay --revert --onto topic1 topic1..topic2 >result &&
>
>              test_line_count = 1 result &&
>              NEW_TOPIC2=$(cut -f 3 -d " " result) &&
>
>              # Verify the result updates the topic2 branch
>              printf "update refs/heads/topic2 " >expect &&
>              printf "%s " $NEW_TOPIC2 >>expect &&
>              git rev-parse topic2 >>expect &&
>
>              test_cmp expect result &&
>
>              # Verify the commit messages contain "Revert"
>              # topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A)
>              git log --format=%s $NEW_TOPIC2 >actual &&
>              test_line_count = 6 actual &&
>              head -n 1 actual >first-line &&
>              test_grep "^Revert" first-line
>
>      test_line_count: line count for result != 1
>
> The "result" file has 0 bytes (hence 0 lines).


Ah, this is because my patch was based on a tree that had atomic ref 
updates as the default (REF_ACTION_UPDATE), which produces no stdout 
output. The tests were written for --ref-action=print behavior.

I have fixed the tests to either:
1. Use --ref-action=print explicitly when expecting output, or
2. Check the ref state directly rather than parsing stdout

The test failures you saw should be fixed in v2.

Thanks,
Siddharth


>
> Actually, address or leak sanitizing build is not needed to
> reproduce this problem, it seems.
>
>      $ make CC=clang test
>
> Was sufficient to see the same first failure.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-26 11:10   ` Phillip Wood
  2025-11-26 17:35     ` Elijah Newren
@ 2025-11-26 19:39     ` Siddharth Asthana
  2025-11-27 16:21       ` Phillip Wood
  1 sibling, 1 reply; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-26 19:39 UTC (permalink / raw)
  To: phillip.wood, git
  Cc: christian.couder, ps, newren, gitster, karthik.188, code,
	rybak.a.v, jltobler, toon, johncai86, johannes.schindelin


On 26/11/25 16:40, Phillip Wood wrote:
> Hi Siddharth
>
> On 25/11/2025 17:00, Siddharth Asthana wrote:
>>
>> diff --git a/Documentation/git-replay.adoc 
>> b/Documentation/git-replay.adoc
>> index dcb26e8a8e..ad7dc08622 100644
>> --- a/Documentation/git-replay.adoc
>> +++ b/Documentation/git-replay.adoc
>> @@ -54,6 +54,18 @@ which uses the target only as a starting point 
>> without updating it.
>> [...]
>> +To revert a range of commits:
>> +
>> +------------
>> +$ git replay --revert --onto main feature~3..feature
>> +------------
>> +
>> +This creates new commits on top of 'main' that reverse the changes 
>> introduced
>> +by the last three commits on 'feature'. The 'feature' branch is 
>> updated to
>> +point at the last of these revert commits. The 'main' branch is not 
>> updated
>> +in this case.


Hi Phillip,

Thanks for the detailed analysis!


>
> I'm struggling to understand when I'd want to do this. Why would I 
> want to update 'feature' to point to the reverted version of its last 
> tree commits rebased onto 'main'?


You are absolutely right - the `--onto` example I provided doesn't make 
practical sense. Elijah's reply clarified the architecture: `--revert` 
should be its own mode, not a modifier that combines with `--onto` or 
`--advance`.


The realistic use case is reverting commits from a branch where those 
commits already exist. For example:

     git replay --revert main~3..main

This would revert the last 3 commits on main, creating revert commits on 
top of main.


>  In order to understand I ran the first tests case which does
>
>     git replay --onto topic1 --revert topic1..topic2
>
> after fixing it by adding --ref-action=print the resulting commit log 
> looks like
>
> commit d337fab78e90008835f74e890039b464a0308cbe
> Author: author@name <bogus@email@address>
> Date:   Thu Apr 7 15:30:13 2005 -0700
>
>     Revert "E
>     "
>
>     This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276.
>
> commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a
> Author: author@name <bogus@email@address>
> Date:   Thu Apr 7 15:30:13 2005 -0700
>
>     Revert "D
>     "
>
>     This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7.
>
>
> The commits are empty because the original they are reverting each 
> create a new file which is then present in the base revision but not 
> in either of the merge heads when we revert.


This confirms the tests aren't realistic. In v2, I will create tests 
where the commits being reverted are ancestors of the replay target, so 
the reverts produce meaningful diffs.


>  This suggests to me that it is not a very realistic test and I'm 
> still scratching my head to see where "git replay --onto <commit> 
> --revert" is useful.
>
> If '--revert' does not make sense with '--onto' then perhaps it should 
> be a new mode that takes a ref and acts like '--advance' but reverts 
> the commits rather than cherry-picking them. When reverting a range of 
> commits it would reduce the likelihood of conflicts to revert then in 
> reverse order so we should either recommend passing '--reverse' or 
> make that the default when '--revert' is given.
>
> As you can see in the log output above the new function to format the 
> revert subject lines is buggy.


Good catch! The bug is in `generate_revert_message()` - I am passing 
`orig_message` (which points to the full message including body) to 
`sequencer_format_revert_header()`, but that function expects just the 
subject line.

Looking at how sequencer.c does it, they use `msg.subject` which is 
properly extracted. I need to use `commit_subject_length()` to get just 
the subject:

     int subject_len = find_commit_subject(message, &orig_message);
     char *subject = xmemdupz(orig_message, subject_len);
     generate_revert_message(&msg, subject, &based_on->object.oid);
     free(subject);


> If you had used test_commit_message() to check the commit message, 
> rather than just grepping for ^Revert the tests would have picked that 
> up.


You are right. I will use test_commit_message() for proper validation in v2.

Thanks,
Siddharth


>
> Thanks
>
> Phillip
>

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-26 17:35     ` Elijah Newren
  2025-11-26 18:41       ` Junio C Hamano
@ 2025-11-26 19:50       ` Siddharth Asthana
  1 sibling, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-26 19:50 UTC (permalink / raw)
  To: Elijah Newren, phillip.wood
  Cc: git, christian.couder, ps, gitster, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86, johannes.schindelin


On 26/11/25 23:05, Elijah Newren wrote:
> On Wed, Nov 26, 2025 at 3:10 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>> Hi Siddharth
>>
>> On 25/11/2025 17:00, Siddharth Asthana wrote:
>>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
>>> index dcb26e8a8e..ad7dc08622 100644
>>> --- a/Documentation/git-replay.adoc
>>> +++ b/Documentation/git-replay.adoc
>>> @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it.
>>> [...]
>>> +To revert a range of commits:
>>> +
>>> +------------
>>> +$ git replay --revert --onto main feature~3..feature
>>> +------------
>>> +
>>> +This creates new commits on top of 'main' that reverse the changes introduced
>>> +by the last three commits on 'feature'. The 'feature' branch is updated to
>>> +point at the last of these revert commits. The 'main' branch is not updated
>>> +in this case.
>> I'm struggling to understand when I'd want to do this. Why would I want
>> to update 'feature' to point to the reverted version of its last tree
>> commits rebased onto 'main'? In order to understand I ran the first
>> tests case which does
>>
>>          git replay --onto topic1 --revert topic1..topic2
>>
>> after fixing it by adding --ref-action=print the resulting commit log
>> looks like
>>
>> commit d337fab78e90008835f74e890039b464a0308cbe
>> Author: author@name <bogus@email@address>
>> Date:   Thu Apr 7 15:30:13 2005 -0700
>>
>>       Revert "E
>>       "
>>
>>       This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276.
>>
>> commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a
>> Author: author@name <bogus@email@address>
>> Date:   Thu Apr 7 15:30:13 2005 -0700
>>
>>       Revert "D
>>       "
>>
>>       This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7.
>>
>>
>> The commits are empty because the original they are reverting each
>> create a new file which is then present in the base revision but not in
>> either of the merge heads when we revert. This suggests to me that it is
>> not a very realistic test and I'm still scratching my head to see where
>> "git replay --onto <commit> --revert" is useful.
>>
>> If '--revert' does not make sense with '--onto' then perhaps it should
>> be a new mode that takes a ref and acts like '--advance' but reverts the
>> commits rather than cherry-picking them. When reverting a range of
>> commits it would reduce the likelihood of conflicts to revert then in
>> reverse order so we should either recommend passing '--reverse' or make
>> that the default when '--revert' is given.
>>
>> As you can see in the log output above the new function to format the
>> revert subject lines is buggy. If you had used test_commit_message() to
>> check the commit message, rather than just grepping for ^Revert the
>> tests would have picked that up.
>>
>> Thanks
>>
>> Phillip
> I was going to say the same thing, but from a different angle.


Hi Elijah,

thanks for the architectural clarity!


>
> The sequencer in git is used for three different types of operations:
> rebasing, cherry-picking, and reverting a range (with a sequence of
> reverts rather than one big revert).  In replay, these correspond to
> --onto, --advance, and the new thing you are trying to add.  As such,
> it should be its own new mode.


This makes complete sense. I was treating `--revert` as a modifier when 
it should be a third mode alongside `--onto` and `--advance`.

I will restructure so that the user specifies exactly one of:
   --onto <newbase>
   --advance <branch>
   --revert <target>

Where `--revert <target>` applies the reverts on top of <target> and 
updates that ref.


>
> (I do tend to see ranges reverted by a single big revert, the way
> Johannes suggested, rather than as a range of individual reverts,


The commit-by-commit approach is useful when you need:
- Individual revert commits with proper "This reverts commit X" messages
- The ability to later cherry-pick specific reverts
- Clear history showing which commit caused which revert

But I will add documentation noting the `merge-tree` alternative for 
cases where a single combined revert is preferred.

Thanks,
Siddharth


>   so
> to me the utility of the new mode looks low, but perhaps others find
> more utility in it.  Or maybe the intent is to only use it with a
> revision range that is only one commit long?)
>
> Phillip also went into more detail about why "--onto $COMMIT --revert"
> specifically doesn't make sense.  I'd also say



> "--advance $BRANCH
> --revert" doesn't read well because to users, "revert" means going
> back while "advance" means going forward,


Exactly - combining these is semantically confusing even if it could be 
made to work technically.


>   so it's a rather confusing
> command line to make them wrap their head around.
>
> And yes, Siddharth, you were right that the new mode should be
> incompatible with --contained, but that's because --contained is a
> special modifier of --onto.  --onto, --advance, and --revert are three
> different modes that are incompatible with each other.  Once you've
> checked for that incompatibility between the three modes, then you can
> either check that whenever --contained is specified, either --onto is
> as well, or neither --advance nor --revert are.



Right. The check becomes:
1. Exactly one of --onto, --advance, --revert must be specified
2. --contained requires --onto

This is much cleaner than my current approach of pairwise 
incompatibility checks.


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  2025-11-26 19:18   ` Siddharth Asthana
@ 2025-11-26 21:04     ` Junio C Hamano
  2025-11-27 19:21       ` Siddharth Asthana
  0 siblings, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2025-11-26 21:04 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: Johannes Schindelin, git, christian.couder, ps, newren,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> 1. For quick undoing an entire MR, the `merge-tree` approach you 
> suggest is indeed more efficient and avoids unnecessary intermediate 
> conflicts.
>
> 2. For commit-by-commit reverts, we need individual revert commits with 
> proper attribution (which commit is being reverted) for auditability and 
> history clarity. This is particularly useful when only specific commits 
> from a merged branch need to be reverted.

These are both good workflows with appropriate uses.  To make the
tool useful for #2, it needs to be able to allow "I have merged a
topic with 7 commits, but the first commit and the fourth commit are
faulty and I need to revert them", i.e., not just a range (like
"rebase" and "cherry-pick" workflows take), but a set of commits
that are potentially disconnected.  The current command line
arguments "git replay" supports, or "git revert A..B" for that
matter, are not exactly a good fit for such a use case, although the
user can of course run two single-commit revert operations in a row.


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-26 19:26     ` Siddharth Asthana
@ 2025-11-26 21:13       ` Junio C Hamano
  2025-11-27 19:23         ` Siddharth Asthana
  0 siblings, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2025-11-26 21:13 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

>>> +This creates new commits on top of 'main' that reverse the changes introduced
>>> +by the last three commits on 'feature'. The 'feature' branch is updated to
>>> +point at the last of these revert commits. The 'main' branch is not updated
>>> +in this case.
>> Is there any topological requirement between 'main' and 'feature'
>> branches?
>
> Yes, and I failed to explain this. For reverts to produce meaningful 
> non-empty commits, the commits being reverted should already be in the 
> target branch's history. I will clarify the examples to show this 
> topology requirement explicitly.

We need to be a bit careful, though.  Strictly speaking, what we
have is not a requirement on the shape of the history.  If a topic
that was merged to the development branch gets cherry-picked to the
master branch, and then it turns out to be faulty and needs to be
reverted, we can still "revert" the original topic out of the master
branch, even though topologically, the original topic is *not* in
'master'.

>>> +		/* For revert: swap base and pickme to reverse the diff */
>>> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
>>> +		merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme));
>> That is an overly long line (sorry, I notice these things when a
>> line does not even fit in 92-col terminal).
>
>
> Fixed in my local tree by introducing a `pickme_name` variable.

Just a line-folding at an appropriate column may be sufficient, e.g.,

		merge_opt->branch2 = xstrfmt("parent of %s",
					short_commit_name(repo, pickme));

>>> -	free((char*)merge_opt->ancestor);
>>>   	merge_opt->ancestor = NULL;
>>> +	merge_opt->branch2 = NULL;
>> Not a new problem, but what is the point of setting these two (but
>> not branch1) to NULL?
>
>
> You're right, this is inconsistent. The intent is to prevent 
> use-after-free, but setting only some fields to NULL is incomplete. I 
> will either set all three to NULL or add a comment explaining the rationale.

Is this the only place that resets a subset of merge_opt members for
reuse?  If not, are these multiple places want to reset the same
subset of the members?  Perhaps we can use a helper function to
clarify in such a case.


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-26 18:41       ` Junio C Hamano
@ 2025-11-26 21:17         ` Junio C Hamano
  2025-11-26 23:06           ` Elijah Newren
  0 siblings, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2025-11-26 21:17 UTC (permalink / raw)
  To: Elijah Newren
  Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Junio C Hamano <gitster@pobox.com> writes:

> Elijah Newren <newren@gmail.com> writes:
>
>>> I'm struggling to understand when I'd want to do this. Why would I want
>>> to update 'feature' to point to the reverted version of its last tree
>>> commits rebased onto 'main'?
>>> ...
>> I was going to say the same thing, but from a different angle.
>>
>> The sequencer in git is used for three different types of operations:
>> rebasing, cherry-picking, and reverting a range (with a sequence of
>> reverts rather than one big revert).  In replay, these correspond to
>> --onto, --advance, and the new thing you are trying to add.  As such,
>> it should be its own new mode.
>
> This is a great comment that clarifies what the problem is with this.

Stepping back a bit, is it just me who thinks that the "--onto"
option is a misnamed "--rebase", and the "--advance" option is a
misnamed "--cherry-pick"?

Perhaps it is already way too late to remedy, but if we ever want to
change it, we should do so while "replay" is still marked experimental.



^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-26 21:17         ` Junio C Hamano
@ 2025-11-26 23:06           ` Elijah Newren
  2025-11-26 23:14             ` Junio C Hamano
  0 siblings, 1 reply; 92+ messages in thread
From: Elijah Newren @ 2025-11-26 23:06 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Wed, Nov 26, 2025 at 1:17 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Junio C Hamano <gitster@pobox.com> writes:
>
> > Elijah Newren <newren@gmail.com> writes:
> >
> >>> I'm struggling to understand when I'd want to do this. Why would I want
> >>> to update 'feature' to point to the reverted version of its last tree
> >>> commits rebased onto 'main'?
> >>> ...
> >> I was going to say the same thing, but from a different angle.
> >>
> >> The sequencer in git is used for three different types of operations:
> >> rebasing, cherry-picking, and reverting a range (with a sequence of
> >> reverts rather than one big revert).  In replay, these correspond to
> >> --onto, --advance, and the new thing you are trying to add.  As such,
> >> it should be its own new mode.
> >
> > This is a great comment that clarifies what the problem is with this.
>
> Stepping back a bit, is it just me who thinks that the "--onto"
> option is a misnamed "--rebase", and the "--advance" option is a
> misnamed "--cherry-pick"?

Is the goal to make connections between existing commands for folks
already very familiar with git, at the expense of comprehensibility
for new users and command lines that look somewhat illogical?

For either a rebase or a cherry-pick operation you have: (A) a range
of commits to be transplanted, (B) a base on which to build from, and
(C) the choice of which ref(s) should be updated to point to the
transplanted commits.  cherry-pick assumed HEAD for both (B) and (C).
rebase formed an implicit range instead of letting the user specify
(in a way which has always made it difficult to teach to new users,
IMO, but I digress), which involves HEAD and also used HEAD for (C).
git replay removes all assumptions about HEAD, which means there is
much more freedom for (A), (B), and (C), but I think it also makes it
more important to try to make command lines at least a bit more
self-describing for users to learn.

== Example command lines today ==

  git replay --onto main feature~3..feature

This command replays the commits in the range feature~3..feature onto
main, and updates feature to point at the result.

  git replay --advance main feature~3..feature

This command replays the commits in the range feature~3..feature onto
main, and advances main to point at the result.

(Both replay the same commit range on the same base, they differ only
in which refs are updated at the end.)

== Example command lines from your proposal ==

   git replay --rebase main feature~3..feature

This command to me would suggest that main is being rebased, but it
isn't -- it rebases feature~3..feature onto main while updating
feature to point at the result.  I find the "--rebase main" part of
this command line confusing.

   git replay --cherry-pick main feature~3..feature

This command to me would suggest that main is being cherry-picked, but
it isn't -- it cherry-picks feature~3..feature onto main while
updating main to point at the result.  Again, I find the
"--cherry-pick main" part of this command line confusing.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-26 23:06           ` Elijah Newren
@ 2025-11-26 23:14             ` Junio C Hamano
  2025-11-26 23:57               ` Elijah Newren
  0 siblings, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2025-11-26 23:14 UTC (permalink / raw)
  To: Elijah Newren
  Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Elijah Newren <newren@gmail.com> writes:

> == Example command lines from your proposal ==
>
>    git replay --rebase main feature~3..feature
>
> This command to me would suggest that main is being rebased, but it
> isn't -- it rebases feature~3..feature onto main while updating
> feature to point at the result.  I find the "--rebase main" part of
> this command line confusing.
>
>    git replay --cherry-pick main feature~3..feature
>
> This command to me would suggest that main is being cherry-picked, but
> it isn't -- it cherry-picks feature~3..feature onto main while
> updating main to point at the result.  Again, I find the
> "--cherry-pick main" part of this command line confusing.

That only tells us that if you want to help users by limiting the
vocabulary to a single set (i.e. both command names, and mode names
used in replay), you'd need to make sure you have the order of
<branch> and <range> given to the replay command in logical order,
in line with the option name, no?  Of course, if you want to say
"cherry-pick", cherry-picked range would have to come near the
option flag that says "cherry-pick", naturally.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-26 23:14             ` Junio C Hamano
@ 2025-11-26 23:57               ` Elijah Newren
  0 siblings, 0 replies; 92+ messages in thread
From: Elijah Newren @ 2025-11-26 23:57 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Wed, Nov 26, 2025 at 3:14 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> > == Example command lines from your proposal ==
> >
> >    git replay --rebase main feature~3..feature
> >
> > This command to me would suggest that main is being rebased, but it
> > isn't -- it rebases feature~3..feature onto main while updating
> > feature to point at the result.  I find the "--rebase main" part of
> > this command line confusing.
> >
> >    git replay --cherry-pick main feature~3..feature
> >
> > This command to me would suggest that main is being cherry-picked, but
> > it isn't -- it cherry-picks feature~3..feature onto main while
> > updating main to point at the result.  Again, I find the
> > "--cherry-pick main" part of this command line confusing.
>
> That only tells us that if you want to help users by limiting the
> vocabulary to a single set (i.e. both command names, and mode names
> used in replay), you'd need to make sure you have the order of
> <branch> and <range> given to the replay command in logical order,
> in line with the option name, no?  Of course, if you want to say
> "cherry-pick", cherry-picked range would have to come near the
> option flag that says "cherry-pick", naturally.

--advance and --onto are flags that require an argument -- in this
case, "main".  So, now you're suggesting more than renaming, in
particular some bigger refactoring such as making these flags now
require the <range> rather than the <base>.  Let's follow that path a
bit further...

Does your proposal assume that <range> is simple, such as
"feature~3..feature" above (i.e. something that an argument parser
would view as a single argument)?  What if the <range> were "^main
feature1 feature2"?  Or ""^$COMMIT --ancestry-path --branches"?  (I
don't see how to have the option parser easily be able to stuff the
arguments to "--rebase ^$COMMIT --ancestry-path --branches" into a
range variable that eats all of "^$COMMIT --ancestry-path
--branches".)  While I use simple ranges to describe the feature, I
specifically built the command to be able to do things like those
other two examples and use it for those.  Those more complicated
examples are things the rebase command just can't do.

Also, just like `git log` allows `git log [<options>] [<revision
range>]`, I wanted git replay to allow `git replay [<options>]
[<revision range>]`.  Instead of doing magic to get an implicit
revision range as rebase does (and with rather limited options because
of that magic), suddenly people can use what they've learned from `git
log` in another place.  But that piece of knowledge only really
transfers if we do similarly to `git log`, i.e. the revision range
comes after other options.

Perhaps one way to avoid the first problem above is to make
`--onto/--advance/--rebase/--cherry-pick" stop requiring (or
accepting) an argument and turn them into simple mode toggles, and
then make both <base> and <range> be positional arguments, with some
well-defined ordering.  However, if <base> comes before <revision>
then we still have the same problem as my previous email, whereas if
it comes after, then we weaken or destroy the connection to `git log`
I made above.  Maybe the connection to `git log` isn't that important.
What I think is important either way, though, is if we use positional
arguments for both things instead of making (at least one) an option,
then I feel we are copying one of the designs of `git rebase` that
makes it hard for even me to use: I hate that it uses multiple
positional arguments to define the operation; despite using the
command heavily for 16-17 years and sending in lots of patches to
improve it, I still can't remember the order of those positional
arguments and have to look it up again when teaching others.  Maybe
that's a personal shortcoming, but I would really rather that either
<base> or <revision> was an option flag.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-26 19:39     ` Siddharth Asthana
@ 2025-11-27 16:21       ` Phillip Wood
  2025-11-27 19:24         ` Siddharth Asthana
  0 siblings, 1 reply; 92+ messages in thread
From: Phillip Wood @ 2025-11-27 16:21 UTC (permalink / raw)
  To: Siddharth Asthana, phillip.wood, git
  Cc: christian.couder, ps, newren, gitster, karthik.188, code,
	rybak.a.v, jltobler, toon, johncai86, johannes.schindelin

Hi Siddharth

On 26/11/2025 19:39, Siddharth Asthana wrote:
> 
> The realistic use case is reverting commits from a branch where those 
> commits already exist. For example:
> 
>      git replay --revert main~3..main
> 
> This would revert the last 3 commits on main, creating revert commits on 
> top of main.

We want to be able to revert an arbitary range of commits. That means we 
need to give --revert a branch name to update in addition to the range 
of commits to revert. The following example would update "main", 
reverting all the commits from the branch "feature"

	git replay --revert main main..feature

Thanks

Phillip


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  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
  0 siblings, 2 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-27 19:21 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Johannes Schindelin, git, christian.couder, ps, newren,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86


On 27/11/25 02:34, Junio C Hamano wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> 1. For quick undoing an entire MR, the `merge-tree` approach you
>> suggest is indeed more efficient and avoids unnecessary intermediate
>> conflicts.
>>
>> 2. For commit-by-commit reverts, we need individual revert commits with
>> proper attribution (which commit is being reverted) for auditability and
>> history clarity. This is particularly useful when only specific commits
>> from a merged branch need to be reverted.
> These are both good workflows with appropriate uses.  To make the
> tool useful for #2, it needs to be able to allow "I have merged a
> topic with 7 commits, but the first commit and the fourth commit are
> faulty and I need to revert them", i.e., not just a range


Since replay uses the same rev-list machinery as `git log`, users can 
already specify disconnected commits:

     git replay --revert <target> <commit1> <commit4>

I will add a test to verify this works and document the capability.

Thanks,
Siddharth


> (like
> "rebase" and "cherry-pick" workflows take), but a set of commits
> that are potentially disconnected.  The current command line
> arguments "git replay" supports, or "git revert A..B" for that
> matter, are not exactly a good fit for such a use case, although the
> user can of course run two single-commit revert operations in a row.
>

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-26 21:13       ` Junio C Hamano
@ 2025-11-27 19:23         ` Siddharth Asthana
  0 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-27 19:23 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 27/11/25 02:43, Junio C Hamano wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>>>> +This creates new commits on top of 'main' that reverse the changes introduced
>>>> +by the last three commits on 'feature'. The 'feature' branch is updated to
>>>> +point at the last of these revert commits. The 'main' branch is not updated
>>>> +in this case.
>>> Is there any topological requirement between 'main' and 'feature'
>>> branches?
>> Yes, and I failed to explain this. For reverts to produce meaningful
>> non-empty commits, the commits being reverted should already be in the
>> target branch's history. I will clarify the examples to show this
>> topology requirement explicitly.
> We need to be a bit careful, though.  Strictly speaking, what we
> have is not a requirement on the shape of the history.


Right - it's the changes that need to exist in the target tree, not the 
commits themselves. Cherry-picked commits can be reverted even without 
topological ancestry. I will fix the documentation.


>    If a topic
> that was merged to the development branch gets cherry-picked to the
> master branch, and then it turns out to be faulty and needs to be
> reverted, we can still "revert" the original topic out of the master
> branch, even though topologically, the original topic is *not* in
> 'master'.
>
>>>> +		/* For revert: swap base and pickme to reverse the diff */
>>>> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
>>>> +		merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme));
>>> That is an overly long line (sorry, I notice these things when a
>>> line does not even fit in 92-col terminal).
>>
>> Fixed in my local tree by introducing a `pickme_name` variable.
> Just a line-folding at an appropriate column may be sufficient, e.g.,


Will do.


>
> 		merge_opt->branch2 = xstrfmt("parent of %s",
> 					short_commit_name(repo, pickme));
>
>>>> -	free((char*)merge_opt->ancestor);
>>>>    	merge_opt->ancestor = NULL;
>>>> +	merge_opt->branch2 = NULL;
>>> Not a new problem, but what is the point of setting these two (but
>>> not branch1) to NULL?
>>
>> You're right, this is inconsistent. The intent is to prevent
>> use-after-free, but setting only some fields to NULL is incomplete. I
>> will either set all three to NULL or add a comment explaining the rationale.
> Is this the only place that resets a subset of merge_opt members for
> reuse?  If not, are these multiple places want to reset the same
> subset of the members?  Perhaps we can use a helper function to
> clarify in such a case.


This is the only place in replay.c. The reason we only NULL ancestor and 
branch2 is that those are the ones allocated with xstrfmt() - branch1 
points to short_commit_name() which doesn't need freeing.

A helper would make the intent clearer. I will look into adding one.

Thanks,
Siddharth



^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes
  2025-11-27 16:21       ` Phillip Wood
@ 2025-11-27 19:24         ` Siddharth Asthana
  0 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-27 19:24 UTC (permalink / raw)
  To: phillip.wood, git
  Cc: christian.couder, ps, newren, gitster, karthik.188, code,
	rybak.a.v, jltobler, toon, johncai86, johannes.schindelin


On 27/11/25 21:51, Phillip Wood wrote:
> Hi Siddharth
>
> On 26/11/2025 19:39, Siddharth Asthana wrote:
>>
>> The realistic use case is reverting commits from a branch where those 
>> commits already exist. For example:
>>
>>      git replay --revert main~3..main
>>
>> This would revert the last 3 commits on main, creating revert commits 
>> on top of main.
>
> We want to be able to revert an arbitary range of commits. That means 
> we need to give --revert a branch name to update in addition to the 
> range of commits to revert. The following example would update "main", 
> reverting all the commits from the branch "feature"
>
>     git replay --revert main main..feature


Makes sense. I will restructure --revert to take a branch argument, 
making it a proper mode alongside --onto and --advance:

     git replay --revert <branch> <revision-range>

This keeps the syntax consistent with the other modes.

Thanks,
Siddharth



>
> Thanks
>
> Phillip
>

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  2025-11-27 19:21       ` Siddharth Asthana
@ 2025-11-27 20:17         ` Junio C Hamano
  2025-11-28  8:07         ` Elijah Newren
  1 sibling, 0 replies; 92+ messages in thread
From: Junio C Hamano @ 2025-11-27 20:17 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: Johannes Schindelin, git, christian.couder, ps, newren,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

>> These are both good workflows with appropriate uses.  To make the
>> tool useful for #2, it needs to be able to allow "I have merged a
>> topic with 7 commits, but the first commit and the fourth commit are
>> faulty and I need to revert them", i.e., not just a range
>
>
> Since replay uses the same rev-list machinery as `git log`, users can 
> already specify disconnected commits:
>
>      git replay --revert <target> <commit1> <commit4>

Ah, OK.

Yes, you can use setup_revisions() command line parsing, intercept
the result of parsing before prepare_revilimit_list() reduces that
to a contiguous set of commits that are reachable from some of the
positive refs and are not reachable from any of the negative ones,
so it certainly is doable.


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  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
  1 sibling, 1 reply; 92+ messages in thread
From: Elijah Newren @ 2025-11-28  8:07 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: Junio C Hamano, Johannes Schindelin, git, christian.couder, ps,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86

On Thu, Nov 27, 2025 at 11:21 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> On 27/11/25 02:34, Junio C Hamano wrote:
> > Siddharth Asthana <siddharthasthana31@gmail.com> writes:
> >
> >> 1. For quick undoing an entire MR, the `merge-tree` approach you
> >> suggest is indeed more efficient and avoids unnecessary intermediate
> >> conflicts.
> >>
> >> 2. For commit-by-commit reverts, we need individual revert commits with
> >> proper attribution (which commit is being reverted) for auditability and
> >> history clarity. This is particularly useful when only specific commits
> >> from a merged branch need to be reverted.
> > These are both good workflows with appropriate uses.  To make the
> > tool useful for #2, it needs to be able to allow "I have merged a
> > topic with 7 commits, but the first commit and the fourth commit are
> > faulty and I need to revert them", i.e., not just a range
>
>
> Since replay uses the same rev-list machinery as `git log`, users can
> already specify disconnected commits:
>
>      git replay --revert <target> <commit1> <commit4>

No, this command does not specify disconnected commits.  A <range> of
"<commit1> <commit4>" specifies all commits in the history of either
<commit1> or <commit4>.  Thus, this example command line would be
asking to revert all commits in the history of either <commit1> or
<commit4> (all the way back to the initial commit), rather than just
reverting those two commits.  This is just like how
   git log <commit1> <commit4>
shows all commits in the history of either <commit1> or <commit4>
instead of just showing those two commits.

There isn't really a mechanism in replay right now to handle a
disconnected set of commits for either --advance or --revert.  If
there were, it'd probably look like

   git replay --advance <branch> --no-walk <commit1> <commit4>

but the code isn't set up to check whether you specified --no-walk,
and thinks "Um, you specified multiple branches here and it's not
clear the order in which to cherry-pick them" so it throws an error:

$ git replay --advance main --no-walk Commit1 Commit7
fatal: cannot advance target with multiple sources because ordering
would be ill-defined

If you comment out the relevant check which dies with that error, then
you end up in some codepath that segfaults instead (not-properly
initialized commit/tree objects or something?).  I'm sure that could
be fixed, but "users can already specify disconnected commits" is just
not accurate.

> I will add a test to verify this works and document the capability.

Supporting --no-walk so that folks can do disconnected commits for
both --advance and --revert may be nice, but given that it's missing
for --advance already, it might be considered a separate change from
your current submission.  I'll leave that up to you.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  2025-11-28  8:07         ` Elijah Newren
@ 2025-11-28  8:24           ` Siddharth Asthana
  2025-11-28 16:35             ` Junio C Hamano
  0 siblings, 1 reply; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-28  8:24 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Junio C Hamano, Johannes Schindelin, git, christian.couder, ps,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86


On 28/11/25 13:37, Elijah Newren wrote:
> On Thu, Nov 27, 2025 at 11:21 AM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> On 27/11/25 02:34, Junio C Hamano wrote:
>>> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>>>
>>>> 1. For quick undoing an entire MR, the `merge-tree` approach you
>>>> suggest is indeed more efficient and avoids unnecessary intermediate
>>>> conflicts.
>>>>
>>>> 2. For commit-by-commit reverts, we need individual revert commits with
>>>> proper attribution (which commit is being reverted) for auditability and
>>>> history clarity. This is particularly useful when only specific commits
>>>> from a merged branch need to be reverted.
>>> These are both good workflows with appropriate uses.  To make the
>>> tool useful for #2, it needs to be able to allow "I have merged a
>>> topic with 7 commits, but the first commit and the fourth commit are
>>> faulty and I need to revert them", i.e., not just a range
>>
>> Since replay uses the same rev-list machinery as `git log`, users can
>> already specify disconnected commits:
>>
>>       git replay --revert <target> <commit1> <commit4>


Hi Elijah,


> No, this command does not specify disconnected commits.  A <range> of
> "<commit1> <commit4>" specifies all commits in the history of either
> <commit1> or <commit4>.


You are right, I misspoke. I was conflating the command-line syntax with 
what the revision machinery actually produces after prepare_revision_walk().


>    Thus, this example command line would be
> asking to revert all commits in the history of either <commit1> or
> <commit4> (all the way back to the initial commit), rather than just
> reverting those two commits.  This is just like how
>     git log <commit1> <commit4>
> shows all commits in the history of either <commit1> or <commit4>
> instead of just showing those two commits.
>
> There isn't really a mechanism in replay right now to handle a
> disconnected set of commits for either --advance or --revert.


Right, the check at line 190 in replay.c:

     if (rinfo.positive_refexprs > 1)
         die(_("cannot advance target with multiple sources..."));

fires before we even get to the revision walk.


> If there were, it'd probably look like
>
>     git replay --advance <branch> --no-walk <commit1> <commit4>
>
> but the code isn't set up to check whether you specified --no-walk,
> and thinks "Um, you specified multiple branches here and it's not
> clear the order in which to cherry-pick them" so it throws an error:
>
> $ git replay --advance main --no-walk Commit1 Commit7
> fatal: cannot advance target with multiple sources because ordering
> would be ill-defined
>
> If you comment out the relevant check which dies with that error, then
> you end up in some codepath that segfaults instead (not-properly
> initialized commit/tree objects or something?).  I'm sure that could
> be fixed, but "users can already specify disconnected commits" is just
> not accurate.
>
>> I will add a test to verify this works and document the capability.
> Supporting --no-walk so that folks can do disconnected commits for
> both --advance and --revert may be nice, but given that it's missing
> for --advance already, it might be considered a separate change from
> your current submission.  I'll leave that up to you.



Agreed. I will keep the current submission focused on basic --revert 
functionality. Supporting --no-walk for disconnected commits (benefiting 
both --advance and --revert) would make a nice follow-up series.

Thanks for the correction.

Siddharth


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  2025-11-28  8:24           ` Siddharth Asthana
@ 2025-11-28 16:35             ` Junio C Hamano
  2025-11-28 17:07               ` Elijah Newren
  0 siblings, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2025-11-28 16:35 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: Elijah Newren, Johannes Schindelin, git, christian.couder, ps,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> Agreed. I will keep the current submission focused on basic --revert 
> functionality. Supporting --no-walk for disconnected commits (benefiting 
> both --advance and --revert) would make a nice follow-up series.

If we want to have a useful support for disconnected set of commits,
"--no-walk" is not the way to go, I would say.

Imagine "among the 7-patch topic merged, the second commit (i.e.,
topic~5) and the final 3 (i.e., topic~3..topic) need to go".  You'd
want to be able to say (without going into details of the syntax)

    revert topic~5 topic~3..topic

The setup_revisions() parser is still the right thing to use to
parse the command line arguments and pick out "topic~5" and
"topic~3..topic", but instead of letting prepare_revision_walk()
turn them into a single contiguous set of revisions, you'd need to
check revs->cmdline->rev[] and

 (1) treat singleton as its own disconnected island that require no
     walking,

 (2) treat A..B as a range and independently walk them, and

 (3) dedup the result from cmdline->rev[] elements into a set of
     commits that are potentially disconnected.

I agree 100% that this topic should not attempt to deal with a
disconnected set of commits.  That can and should be done as a
separate series.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  2025-11-28 16:35             ` Junio C Hamano
@ 2025-11-28 17:07               ` Elijah Newren
  2025-11-28 20:50                 ` Junio C Hamano
  0 siblings, 1 reply; 92+ messages in thread
From: Elijah Newren @ 2025-11-28 17:07 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Siddharth Asthana, Johannes Schindelin, git, christian.couder, ps,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86

On Fri, Nov 28, 2025 at 8:35 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
> > Agreed. I will keep the current submission focused on basic --revert
> > functionality. Supporting --no-walk for disconnected commits (benefiting
> > both --advance and --revert) would make a nice follow-up series.
>
> If we want to have a useful support for disconnected set of commits,
> "--no-walk" is not the way to go, I would say.
>
> Imagine "among the 7-patch topic merged, the second commit (i.e.,
> topic~5) and the final 3 (i.e., topic~3..topic) need to go".  You'd
> want to be able to say (without going into details of the syntax)
>
>     revert topic~5 topic~3..topic
>
> The setup_revisions() parser is still the right thing to use to
> parse the command line arguments and pick out "topic~5" and
> "topic~3..topic", but instead of letting prepare_revision_walk()
> turn them into a single contiguous set of revisions, you'd need to
> check revs->cmdline->rev[] and
>
>  (1) treat singleton as its own disconnected island that require no
>      walking,
>
>  (2) treat A..B as a range and independently walk them, and
>
>  (3) dedup the result from cmdline->rev[] elements into a set of
>      commits that are potentially disconnected.
>
> I agree 100% that this topic should not attempt to deal with a
> disconnected set of commits.  That can and should be done as a
> separate series.

How does one distinguish the "topic~5" in the range "topic~5
topic~3..topic" from
  * the topic~5 in "^topic~7 topic~5"
  * the "topic1" and "topic2" in "^$OLD_COMMIT --ancestry-path topic1 topic2"
?
I kind of think we still need some kind of flag (possibly implied by
default for --advance and --revert but not for --onto?), though I
agree it'd need to be a new one rather than --no-walk for your example
to work.

And if one can do this, should this flag also be added to other
commands, so that e.g. `git log <someflag> topic~5 topic~3..topic"
would also show the commits in topic~3..topic plus topic~5?

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  2025-11-28 17:07               ` Elijah Newren
@ 2025-11-28 20:50                 ` Junio C Hamano
  2025-11-28 22:03                   ` Elijah Newren
  0 siblings, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2025-11-28 20:50 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Siddharth Asthana, Johannes Schindelin, git, christian.couder, ps,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86

Elijah Newren <newren@gmail.com> writes:

> How does one distinguish the "topic~5" in the range "topic~5
> topic~3..topic" from
>   * the topic~5 in "^topic~7 topic~5"

Two answers.

(1) You don't have to.  When you scan cmdline->rev[], you can notice
    the ^topic-7 form and reject, saying "we accept A..B but not ^A B."

(2) Or you design and document the interpretation you implement when
    you see a negative commit while you scan over cmdline->rev[].
    Perhaps you may make "^topic-7" to require a positive commit
    after it and convert "^topic-7 topic5" as if the user gave you a
    single "topic~7..topic~5".  Or you may do something else.

My assumption has been (1).


>   * the "topic1" and "topic2" in "^$OLD_COMMIT --ancestry-path topic1 topic2"

I haven't thought it through, but doesn't ancetry-path imply you are
really interested in the traditional connected set of commits?  The
path is a connected subset inside those commits after all, no?

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  2025-11-28 20:50                 ` Junio C Hamano
@ 2025-11-28 22:03                   ` Elijah Newren
  2025-11-29  5:59                     ` Junio C Hamano
  0 siblings, 1 reply; 92+ messages in thread
From: Elijah Newren @ 2025-11-28 22:03 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Siddharth Asthana, Johannes Schindelin, git, christian.couder, ps,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86

On Fri, Nov 28, 2025 at 12:50 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> > How does one distinguish the "topic~5" in the range "topic~5
> > topic~3..topic" from
> >   * the topic~5 in "^topic~7 topic~5"
>
> Two answers.
>
> (1) You don't have to.  When you scan cmdline->rev[], you can notice
>     the ^topic-7 form and reject, saying "we accept A..B but not ^A B."

Using revision ranges like ^A B C with --onto (rebasing several
branches at once) was one of the major usecases for git replay.
Because of that, even when I only have one branch, I often use ^A B
over A..B.

It's also called out pretty explicitly in the manual:
```
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:

------------
$ git replay --onto origin/main ^base branch1 branch2 branch3
------------

This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
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.
```

Granted, unlike --onto, using --advance (or --revert) with multiple
branches at once doesn't make sense because cherry-picking/reverting
multiple branches into one has an ill-defined ordering problem.  Since
only one branch is allowed, maybe we could make special rules for
--advance and --revert, but it would feel a little weird to have a
revision range not actually be a revision range for some modes of the
command while also having them be an actual revision range for the
other mode.

> (2) Or you design and document the interpretation you implement when
>     you see a negative commit while you scan over cmdline->rev[].
>     Perhaps you may make "^topic-7" to require a positive commit
>     after it and convert "^topic-7 topic5" as if the user gave you a
>     single "topic~7..topic~5".  Or you may do something else.

Part of the point of replay was to "use the same revision ranges that
git log allows".  I agree that having a way to specify "topic~3..topic
plus the commit topic~5" is useful for this command, but it's equally
likely to be useful for `git log`; people have asked for it before.
However, there is currently no way to specify that set of revisions to
`git log`.  Perhaps we should add some flag to revision walking that
allows that kind of alternate rule?

If we do that, then both log and replay would benefit.  We could even
consider making that flag be implied by both --advance and --revert
(but NOT by --onto).  If we do that, we would also need a way to
negate that flag, since someone will probably want a way to
cherry-pick some disconnected branch down to the root commit, meaning
they really would want "topic~5" to be treated as a traditional range
and not as a single commit.

> >   * the "topic1" and "topic2" in "^$OLD_COMMIT --ancestry-path topic1 topic2"
>
> I haven't thought it through, but doesn't ancetry-path imply you are
> really interested in the traditional connected set of commits?  The
> path is a connected subset inside those commits after all, no?

Yes, I believe --ancestry-path makes it clear that a range is wanted.
It's not clear to me that we can always correctly guess
old-style-range vs. new-style-range from the existing command line
arguments alone, particularly given the example above of single commit
vs. commits back to the root.  I think an explicit flag is needed, if
we want to go this route.

(Also, this feels like a fair amount of work, when people can easily
just invoke replay multiple times...)

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes
  2025-11-28 22:03                   ` Elijah Newren
@ 2025-11-29  5:59                     ` Junio C Hamano
  0 siblings, 0 replies; 92+ messages in thread
From: Junio C Hamano @ 2025-11-29  5:59 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Siddharth Asthana, Johannes Schindelin, git, christian.couder, ps,
	phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v,
	jltobler, toon, johncai86

Elijah Newren <newren@gmail.com> writes:

> It's also called out pretty explicitly in the manual:
> ```
> 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:

When we use the phrase "any range expression" to refer to what
prepare_revision_walk() produces, it by definition means we only
deal with a connected set.

It often is very handy to allow saying "master..topic1 topic2" or
"^master topic1 topic2" or "topic1 topic2 --not master" to mean
"commits on these branches", of course, and there are many such
useful use cases that do not require disjoint set (and that is why
we survived without any disjoint set support on "git log" side,
except for individually specifying commits and say "--no-walk",
which is still technically a "set" but not very useful one when the
number of commits you individually have to specify becomes more than
a handful).

It only means that the documentation needs to be updated if we ever
want to introduce an extended form of the command line syntax that
allows users to specify an unconnected set of commits.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v2 0/2] replay: add --revert mode to reverse commit changes
  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 17:25 ` [PATCH 0/1] " Johannes Schindelin
@ 2025-12-02 20:16 ` Siddharth Asthana
  2025-12-02 20:16   ` [PATCH v2 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
                     ` (2 more replies)
  2 siblings, 3 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-12-02 20:16 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon,
	Siddharth Asthana

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.

The implementation follows the same approach as sequencer.c where
cherry-pick and revert are the same merge operation but with swapped
arguments. For cherry-pick we merge(ancestor=parent, ours=current,
theirs=commit), while for revert we merge(ancestor=commit, ours=current,
theirs=parent). By swapping the base and pickme trees when calling
merge_incore_nonrecursive(), we effectively reverse the diff direction.

The series is structured as follows:

Patch 1 extracts the revert message formatting logic into a shared
sequencer_format_revert_header() function, eliminating code duplication
between sequencer.c and the upcoming replay code. This follows Junio's
suggestion to split the changes.

Patch 2 adds the --revert <branch> mode to git replay. Following the
architectural pattern suggested by Elijah and Phillip, --revert is a
standalone mode (like --onto and --advance) that takes a branch argument
and updates that branch with the revert commits.

The series is based on top of f0ef5b6d9b (The fifth batch, 2025-11-29).

CI: https://gitlab.com/gitlab-org/git/-/pipelines/2191734674
The static-analysis and Windows CI failures are pre-existing
infrastructure issues unrelated to this series.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
Changes in v2:
- Split into two patches as Junio suggested
- Changed --revert from a modifier flag to a standalone mode
  (--revert <branch>) that is mutually exclusive with --onto/--advance
- Used enum replay_action instead of int is_revert
- Fixed subject extraction bug in generate_revert_message()
- Put common (cherry-pick) case first in if/else block
- Fixed repo_unuse_commit_buffer() to use repo parameter consistently
- Improved tests with test_commit_message() and atomic ref update handling
- Link to v1: https://lore.kernel.org/git/20251125170055.64991-1-siddharthasthana31@gmail.com/

Siddharth Asthana (2):
  sequencer: extract revert message formatting into shared function
  replay: add --revert mode to reverse commit changes

 Documentation/git-replay.adoc |  36 ++++++++-
 builtin/replay.c              | 145 ++++++++++++++++++++++++++++------
 sequencer.c                   |  39 +++++----
 sequencer.h                   |   8 ++
 t/t3650-replay-basics.sh      | 111 ++++++++++++++++++++++++++
 5 files changed, 300 insertions(+), 39 deletions(-)


Range-diff against v1:
-:  ---------- > 1:  bfd75484b4 sequencer: extract revert message formatting into shared function
1:  dd47a89a5b ! 2:  a2f99bc8c2 replay: add --revert option to reverse commit changes
    @@ Metadata
     Author: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Commit message ##
    -    replay: add --revert option to reverse commit changes
    +    replay: add --revert mode to reverse commit changes
     
         The `git replay` command performs server-side history rewriting without
         requiring a working tree. While it currently supports cherry-picking
    -    commits, it lacks the ability to revert them.
    +    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` option that reverses the changes introduced by the
    -    specified commits. The implementation follows the same approach as
    -    `sequencer.c` (around lines 2358-2390), where cherry-pick and revert
    -    are essentially the same merge operation but with swapped arguments:
    +    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.
    +
    +    The implementation follows the same approach as sequencer.c (lines
    +    2360-2399), where cherry-pick and revert are the same merge operation
    +    but with swapped arguments:
     
           - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
           - Revert: merge(ancestor=commit, ours=current, theirs=parent)
     
         We swap the base and pickme trees when calling
    -    `merge_incore_nonrecursive()`, effectively reversing the diff direction.
    +    merge_incore_nonrecursive(), effectively reversing the diff direction.
         The existing conflict handling, ref updates, and atomic transaction
         support work unchanged.
     
    -    The revert message generation logic (handling "Revert" and "Reapply"
    -    cases) is extracted into a new `sequencer_format_revert_header()`
    -    function in `sequencer.c`, which can be shared between `sequencer.c`
    -    and `builtin/replay.c`. The existing code in `do_pick_commit()` is
    -    updated to use this shared function, eliminating code duplication.
    -    The `builtin/replay.c` code calls this shared function and then appends
    -    the commit OID using `oid_to_hex()` directly, since git replay is
    -    designed for simpler server-side operations without the interactive
    -    features and `replay_opts` framework used by `sequencer.c`.
    -
    -    The commit messages follow `git revert` conventions: prefixed with
    +    The commit messages follow git revert conventions: prefixed with
         "Revert" and including the original commit SHA. When reverting a commit
         that itself starts with "Revert", the message uses "Reapply" instead.
         Unlike cherry-pick which preserves the original author, revert commits
    -    use the current user as the author, matching the behavior of `git
    -    revert`.
    -
    -    Mark the option as incompatible with `--contained` because reverting
    -    changes across multiple branches simultaneously could produce unexpected
    -    results when branches have interdependencies or shared history.
    +    use the current user as the author, matching the behavior of git revert.
     
         Helped-by: Christian Couder <christian.couder@gmail.com>
         Helped-by: Patrick Steinhardt <ps@pks.im>
    +    Helped-by: Elijah Newren <newren@gmail.com>
    +    Helped-by: Phillip Wood <phillip.wood123@gmail.com>
    +    Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
    +    Helped-by: Junio C Hamano <gitster@pobox.com>
         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Documentation/git-replay.adoc ##
    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne
      --------
      [verse]
     -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...
    -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] [--revert] <revision-range>...
    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...
      
      DESCRIPTION
      -----------
    -@@ Documentation/git-replay.adoc: which uses the target only as a starting point without updating it.
    - +
    - The default mode can be configured via the `replay.refAction` configuration variable.
    +@@ Documentation/git-replay.adoc: 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::
    -+	Revert the changes introduced by the commits in the revision range
    -+	instead of applying them. This reverses the diff direction and creates
    -+	new commits that undo the changes, similar to `git revert`.
    ++--revert <branch>::
    ++	Starting point at which to create the new revert commits; must be a
    ++	branch name.
     ++
    -+The commit messages are prefixed with "Revert" and include the original
    -+commit SHA. If reverting a commit whose message starts with "Revert", the new
    -+message will start with "Reapply" instead. The author of the new commits
    -+will be the current user, not the original commit author.
    ++When `--revert` is specified, the commits in the revision range are reverted
    ++(their changes are undone) and the revert commits 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.
     ++
    -+This option is incompatible with `--contained` because reverting changes
    -+across multiple branches simultaneously could produce unexpected results
    -+when branches have interdependencies or shared history.
    ++The commit messages follow `git revert` conventions: prefixed with "Revert" and
    ++including the original commit SHA. 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).
    ++
     +
    - <revision-range>::
    - 	Range of commits to replay. More than one <revision-range> can
    - 	be passed, but in `--advance <branch>` mode, they should have
    + --ref-action[=<mode>]::
    + 	Control how references are updated. The mode can be:
    + +
     @@ Documentation/git-replay.adoc: 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 a range of commits:
    ++To revert commits from a branch:
     +
     +------------
    -+$ git replay --revert --onto main feature~3..feature
    ++$ git replay --revert main feature~2..feature
     +------------
     +
    -+This creates new commits on top of 'main' that reverse the changes introduced
    -+by the last three commits on 'feature'. The 'feature' branch is updated to
    -+point at the last of these revert commits. The 'main' branch is not updated
    -+in this case.
    ++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.
     +
    -+To revert commits and advance a branch:
    ++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.
     +
    -+------------
    -+$ git replay --revert --advance main feature~2..feature
    -+------------
    -+
    -+This reverts the last two commits from 'feature', applies those reverts
    -+on top of 'main', and updates 'main' to point at the result. The 'feature'
    -+branch is not updated in this case.
     +
      GIT
      ---
    @@ builtin/replay.c
      #include "strmap.h"
      #include <oidset.h>
      #include <tree.h>
    +@@ builtin/replay.c: enum ref_action_mode {
    + 	REF_ACTION_PRINT,
    + };
    + 
    ++enum replay_action {
    ++	REPLAY_PICK,
    ++	REPLAY_REVERT,
    ++};
    ++
    + static const char *short_commit_name(struct repository *repo,
    + 				     struct commit *commit)
    + {
     @@ builtin/replay.c: static char *get_author(const char *message)
      	return NULL;
      }
      
    -+/*
    -+ * Generates a revert commit message using the shared sequencer function.
    -+ * We use oid_to_hex() directly instead of refer_to_commit() since git replay
    -+ * is designed for simpler server-side operations without interactive features.
    -+ */
     +static void generate_revert_message(struct strbuf *msg,
    -+				    const char *orig_message,
    -+				    const struct object_id *oid)
    ++				    struct commit *commit,
    ++				    struct repository *repo)
     +{
    -+	sequencer_format_revert_header(msg, orig_message);
    -+	strbuf_addstr(msg, oid_to_hex(oid));
    ++	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");
    ++
    ++	free(subject);
    ++	repo_unuse_commit_buffer(repo, commit, message);
     +}
     +
      static struct commit *create_commit(struct repository *repo,
    @@ builtin/replay.c: static char *get_author(const char *message)
      				    struct commit *based_on,
     -				    struct commit *parent)
     +				    struct commit *parent,
    -+				    int is_revert)
    ++				    enum replay_action action)
      {
      	struct object_id ret;
      	struct object *obj = NULL;
     @@ builtin/replay.c: 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);
    +-	find_commit_subject(message, &orig_message);
     -	strbuf_addstr(&msg, orig_message);
     -	author = get_author(message);
    -+
    -+	if (is_revert) {
    -+		generate_revert_message(&msg, orig_message, &based_on->object.oid);
    -+		/* For revert, use current user as author */
    -+		author = NULL;
    ++	if (action == REPLAY_REVERT) {
    ++		generate_revert_message(&msg, based_on, repo);
    ++		author = xstrdup(git_author_info(IDENT_STRICT));
     +	} else {
    -+		/* Cherry-pick mode: use original commit message and author */
    ++		find_commit_subject(message, &orig_message);
     +		strbuf_addstr(&msg, orig_message);
     +		author = get_author(message);
     +	}
    -+
      	reset_ident_date();
      	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
      				 &ret, author, NULL, sign_commit, extra)) {
    +@@ builtin/replay.c: static struct commit *create_commit(struct repository *repo,
    + 	obj = parse_object(repo, &ret);
    + 
    + out:
    +-	repo_unuse_commit_buffer(the_repository, based_on, message);
    ++	repo_unuse_commit_buffer(repo, based_on, message);
    + 	free_commit_extra_headers(extra);
    + 	free_commit_list(parents);
    + 	strbuf_release(&msg);
    +@@ builtin/replay.c: static void determine_replay_mode(struct repository *repo,
    + 				  struct rev_cmdline_info *cmd_info,
    + 				  const char *onto_name,
    + 				  char **advance_name,
    ++				  char **revert_name,
    + 				  struct commit **onto,
    + 				  struct strset **update_refs)
    + {
    +@@ builtin/replay.c: 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"));
    + 	} else {
    + 		int positive_refs_complete = (
    + 			rinfo.positive_refexprs ==
     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *repo,
      					  kh_oid_map_t *replayed_commits,
      					  struct commit *onto,
      					  struct merge_options *merge_opt,
     -					  struct merge_result *result)
     +					  struct merge_result *result,
    -+					  int is_revert)
    ++					  enum replay_action action)
      {
      	struct commit *base, *replayed_base;
      	struct tree *pickme_tree, *base_tree;
    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r
     -	merge_opt->branch1 = short_commit_name(repo, replayed_base);
     -	merge_opt->branch2 = short_commit_name(repo, pickme);
     -	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
    --
    ++	if (action == REPLAY_PICK) {
    ++		/* Cherry-pick: normal order */
    ++		merge_opt->branch1 = short_commit_name(repo, replayed_base);
    ++		merge_opt->branch2 = short_commit_name(repo, pickme);
    ++		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
    + 
     -	merge_incore_nonrecursive(merge_opt,
     -				  base_tree,
     -				  result->tree,
     -				  pickme_tree,
     -				  result);
    -+	if (is_revert) {
    -+		/* For revert: swap base and pickme to reverse the diff */
    ++		merge_incore_nonrecursive(merge_opt,
    ++					  base_tree,
    ++					  result->tree,
    ++					  pickme_tree,
    ++					  result);
    + 
    +-	free((char*)merge_opt->ancestor);
    ++		free((char *)merge_opt->ancestor);
    ++	} else {
    ++		/* Revert: swap base and pickme to reverse the diff */
     +		const char *pickme_name = short_commit_name(repo, pickme);
     +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
     +		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r
     +					  base_tree,
     +					  result);
     +
    -+		/* branch2 was allocated with xstrfmt, needs freeing */
     +		free((char *)merge_opt->branch2);
    -+	} else {
    -+		/* For cherry-pick: normal order */
    -+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
    -+		merge_opt->branch2 = short_commit_name(repo, pickme);
    -+		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
    -+
    -+		merge_incore_nonrecursive(merge_opt,
    -+					  base_tree,
    -+					  result->tree,
    -+					  pickme_tree,
    -+					  result);
    -+
    -+		/* ancestor was allocated with xstrfmt, needs freeing */
    -+		free((char *)merge_opt->ancestor);
     +	}
    - 
    --	free((char*)merge_opt->ancestor);
      	merge_opt->ancestor = NULL;
     +	merge_opt->branch2 = NULL;
      	if (!result->clean)
      		return NULL;
     -	return create_commit(repo, result->tree, pickme, replayed_base);
    -+	return create_commit(repo, result->tree, pickme, replayed_base, is_revert);
    ++	return create_commit(repo, result->tree, pickme, replayed_base, action);
      }
      
      static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
     @@ builtin/replay.c: int cmd_replay(int argc,
    + {
    + 	const char *advance_name_opt = NULL;
    + 	char *advance_name = NULL;
    ++	const char *revert_name_opt = NULL;
    ++	char *revert_name = NULL;
    ++	enum replay_action action = REPLAY_PICK;
    + 	struct commit *onto = NULL;
    + 	const char *onto_name = NULL;
      	int contained = 0;
    - 	const char *ref_action = NULL;
    - 	enum ref_action_mode ref_mode;
    -+	int is_revert = 0;
    - 
    - 	struct rev_info revs;
    - 	struct commit *last_commit = NULL;
     @@ builtin/replay.c: int cmd_replay(int argc,
    + 
      	const char *const replay_usage[] = {
      		N_("(EXPERIMENTAL!) git replay "
    - 		   "([--contained] --onto <newbase> | --advance <branch>) "
    --		   "[--ref-action[=<mode>]] <revision-range>..."),
    -+		   "[--ref-action[=<mode>]] [--revert] <revision-range>..."),
    +-		   "([--contained] --onto <newbase> | --advance <branch>) "
    ++		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
    + 		   "[--ref-action[=<mode>]] <revision-range>..."),
      		NULL
      	};
    - 	struct option replay_options[] = {
     @@ builtin/replay.c: int cmd_replay(int argc,
    + 			   N_("replay onto given commit")),
    + 		OPT_BOOL(0, "contained", &contained,
    + 			 N_("advance all branches contained in revision-range")),
    ++		OPT_STRING(0, "revert", &revert_name_opt,
    ++			   N_("branch"),
    ++			   N_("revert commits onto given branch")),
      		OPT_STRING(0, "ref-action", &ref_action,
      			   N_("mode"),
      			   N_("control ref update behavior (update|print)")),
    -+		OPT_BOOL(0, "revert", &is_revert,
    -+			 N_("revert commits instead of cherry-picking them")),
    - 		OPT_END()
    - 	};
    - 
     @@ builtin/replay.c: int cmd_replay(int argc,
    + 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
    + 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
    + 
    +-	if (!onto_name && !advance_name_opt) {
    +-		error(_("option --onto or --advance is mandatory"));
    ++	/* Exactly one mode must be specified */
    ++	if (!onto_name && !advance_name_opt && !revert_name_opt) {
    ++		error(_("exactly one of --onto, --advance, or --revert is required"));
    + 		usage_with_options(replay_usage, replay_options);
    + 	}
    + 
      	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
    - 				  contained, "--contained");
    +-				  contained, "--contained");
    ++				  !!onto_name, "--onto");
    ++	die_for_incompatible_opt2(!!revert_name_opt, "--revert",
    ++				  !!onto_name, "--onto");
    ++	die_for_incompatible_opt2(!!revert_name_opt, "--revert",
    ++				  !!advance_name_opt, "--advance");
    ++	die_for_incompatible_opt2(contained, "--contained",
    ++				  !onto_name, "requires --onto");
      
    -+	/* --revert is incompatible with --contained */
    -+	die_for_incompatible_opt2(is_revert, "--revert",
    -+				  contained, "--contained");
    -+
      	/* Parse ref action mode from command line or config */
      	ref_mode = get_ref_action_mode(repo, ref_action);
      
    + 	advance_name = xstrdup_or_null(advance_name_opt);
    ++	revert_name = xstrdup_or_null(revert_name_opt);
    ++	if (revert_name)
    ++		action = REPLAY_REVERT;
    + 
    + 	repo_init_revisions(repo, &revs, prefix);
    + 
    +@@ builtin/replay.c: int cmd_replay(int argc,
    + 	}
    + 
    + 	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
    ++			      &revert_name,
    + 			      &onto, &update_refs);
    + 
    + 	/* Build reflog message */
    +-	if (advance_name_opt)
    ++	if (revert_name_opt)
    ++		strbuf_addf(&reflog_msg, "replay --revert %s", revert_name_opt);
    ++	else if (advance_name_opt)
    + 		strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
    + 	else
    + 		strbuf_addf(&reflog_msg, "replay --onto %s",
     @@ builtin/replay.c: int cmd_replay(int argc,
      			die(_("replaying merge commits is not supported yet!"));
      
      		last_commit = pick_regular_commit(repo, commit, replayed_commits,
     -						  onto, &merge_opt, &result);
    -+						  onto, &merge_opt, &result,
    -+						  is_revert);
    ++						  onto, &merge_opt, &result, action);
      		if (!last_commit)
      			break;
      
    -
    - ## sequencer.c ##
    -@@ sequencer.c: static int do_pick_commit(struct repository *r,
    - 		if (opts->commit_use_reference) {
    - 			strbuf_commented_addf(&ctx->message, comment_line_str,
    - 				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
    --		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
    --			   /*
    --			    * We don't touch pre-existing repeated reverts, because
    --			    * theoretically these can be nested arbitrarily deeply,
    --			    * thus requiring excessive complexity to deal with.
    --			    */
    --			   !starts_with(orig_subject, "Revert \"")) {
    --			strbuf_addstr(&ctx->message, "Reapply \"");
    --			strbuf_addstr(&ctx->message, orig_subject);
    --			strbuf_addstr(&ctx->message, "\n");
    -+			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
    - 		} else {
    --			strbuf_addstr(&ctx->message, "Revert \"");
    --			strbuf_addstr(&ctx->message, msg.subject);
    --			strbuf_addstr(&ctx->message, "\"\n");
    -+			sequencer_format_revert_header(&ctx->message, msg.subject);
    - 		}
    --		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
    - 		refer_to_commit(opts, &ctx->message, commit);
    +@@ builtin/replay.c: int cmd_replay(int argc,
    + 		kh_value(replayed_commits, pos) = last_commit;
      
    - 		if (commit->parents && commit->parents->next) {
    -@@ sequencer.c: int sequencer_pick_revisions(struct repository *r,
    - 	return res;
    - }
    + 		/* Update any necessary branches */
    +-		if (advance_name)
    ++		if (advance_name || revert_name)
    + 			continue;
    + 		decoration = get_name_decoration(&commit->object);
    + 		if (!decoration)
    +@@ builtin/replay.c: int cmd_replay(int argc,
    + 		}
    + 	}
      
    -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)
    -+{
    -+	const char *revert_subject;
    -+
    -+	if (skip_prefix(orig_subject, "Revert \"", &revert_subject) &&
    -+	    /*
    -+	     * We don't touch pre-existing repeated reverts, because
    -+	     * theoretically these can be nested arbitrarily deeply,
    -+	     * thus requiring excessive complexity to deal with.
    -+	     */
    -+	    !starts_with(revert_subject, "Revert \"")) {
    -+		strbuf_addstr(out, "Reapply \"");
    -+		strbuf_addstr(out, revert_subject);
    -+		strbuf_addch(out, '\n');
    -+	} else {
    -+		strbuf_addstr(out, "Revert \"");
    -+		strbuf_addstr(out, orig_subject);
    -+		strbuf_addstr(out, "\"\n");
    +-	/* In --advance mode, advance the target ref */
    ++	/* In --advance or --revert mode, update the target ref */
    + 	if (result.clean == 1 && advance_name) {
    + 		if (handle_ref_update(ref_mode, transaction, advance_name,
    + 				      &last_commit->object.oid,
    +@@ builtin/replay.c: int cmd_replay(int argc,
    + 			goto cleanup;
    + 		}
    + 	}
    ++	if (result.clean == 1 && revert_name) {
    ++		if (handle_ref_update(ref_mode, transaction, revert_name,
    ++				      &last_commit->object.oid,
    ++				      &onto->object.oid,
    ++				      reflog_msg.buf,
    ++				      &transaction_err) < 0) {
    ++			ret = error(_("failed to update ref '%s': %s"),
    ++				    revert_name, transaction_err.buf);
    ++			goto cleanup;
    ++		}
     +	}
    -+
    -+	strbuf_addstr(out, "\nThis reverts commit ");
    -+}
    -+
    - void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
    - {
    - 	unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
    -
    - ## sequencer.h ##
    -@@ sequencer.h: int todo_list_rearrange_squash(struct todo_list *todo_list);
    -  */
    - void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag);
      
    -+/*
    -+ * Formats a revert commit message header following standard Git conventions.
    -+ * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert
    -+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit " at the end.
    -+ * The caller should append the commit OID after calling this function.
    -+ */
    -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject);
    -+
    - void append_conflicts_hint(struct index_state *istate,
    - 		struct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode);
    - enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg,
    + 	/* Commit the ref transaction if we have one */
    + 	if (transaction && result.clean == 1) {
     
      ## t/t3650-replay-basics.sh ##
     @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '
      	test_grep "invalid.*replay.refAction.*value" error
      '
      
    -+test_expect_success 'using replay with --revert to revert a commit' '
    -+	# Revert commits D and E from topic2
    -+	git replay --revert --onto topic1 topic1..topic2 >result &&
    -+
    -+	test_must_be_empty result &&
    -+
    -+	# Verify the commit messages contain "Revert"
    -+	# topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A)
    -+	git log --format=%s topic2 >actual &&
    -+	test_line_count = 6 actual &&
    -+	head -n 1 actual >first-line &&
    -+	test_grep "^Revert" first-line
    ++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
     +'
     +
    -+test_expect_success 'using replay with --revert on bare repo' '
    -+	git -C bare replay --revert --onto topic1 topic1..topic2 >result-bare &&
    -+
    -+	test_must_be_empty result-bare &&
    -+
    -+	# Verify the commit message contains "Revert"
    -+	git -C bare log --format=%s topic2 >actual-bare &&
    -+	test_line_count = 6 actual-bare &&
    -+	head -n 1 actual-bare >first-line-bare &&
    -+	test_grep "^Revert" first-line-bare
    ++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_expect_success 'using replay with --revert and --advance' '
    -+	# Revert commits from topic2 and advance main
    -+	git replay --revert --advance main topic1..topic2 >result &&
    ++test_expect_success 'git replay --revert with --ref-action=print' '
    ++	# Store original state
    ++	START=$(git rev-parse revert-test) &&
    ++	test_when_finished "git branch -f revert-test $START" &&
     +
    -+	test_must_be_empty result &&
    ++	git replay --ref-action=print --revert revert-test revert-test~2..revert-test >result &&
    ++	test_line_count = 1 result &&
     +
    -+	# Verify the commit message contains "Revert"
    -+	git log --format=%s main >actual &&
    -+	head -n 1 actual >first-line &&
    -+	test_grep "^Revert" first-line
    ++	# 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 'replay with --revert fails with --contained' '
    -+	test_must_fail git replay --revert --contained --onto main main..topic3 2>error &&
    -+	test_grep "revert.*contained.*cannot be used together" error
    -+'
    ++test_expect_success 'git replay --revert reapply behavior' '
    ++	# Store original state
    ++	START=$(git rev-parse revert-test) &&
    ++	test_when_finished "git branch -f revert-test $START" &&
     +
    -+test_expect_success 'verify revert actually reverses changes' '
    -+	# Create a branch with a simple change
    -+	git switch -c revert-test main &&
    -+	echo "new content" >test-file.txt &&
    -+	git add test-file.txt &&
    -+	git commit -m "Add test file" &&
    ++	# First revert R3
    ++	git replay --revert revert-test revert-test~1..revert-test &&
    ++	REVERT_R3=$(git rev-parse revert-test) &&
     +
    -+	# Revert the commit
    -+	git replay --revert --advance revert-test HEAD^..HEAD >result &&
    ++	# Now revert the revert (should create "Reapply" message)
    ++	git replay --revert revert-test revert-test~1..revert-test >output &&
    ++	test_must_be_empty output &&
     +
    -+	test_must_be_empty result &&
    ++	# Verify Reapply message
    ++	test_commit_message revert-test -m "Reapply \"R3\"
     +
    -+	# The file should no longer exist (reverted)
    -+	test_must_fail git show revert-test:test-file.txt
    ++This reverts commit $(git rev-parse $REVERT_R3)."
     +'
     +
    -+test_expect_success 'revert of a revert creates reapply message' '
    -+	# Create a commit
    -+	git switch -c revert-revert main &&
    -+	echo "content" >revert-test-2.txt &&
    -+	git add revert-test-2.txt &&
    -+	git commit -m "Add revert test file" &&
    -+
    -+	ORIGINAL=$(git rev-parse HEAD) &&
    -+
    -+	# First revert
    -+	git replay --revert --advance revert-revert HEAD^..HEAD >result1 &&
    -+
    -+	test_must_be_empty result1 &&
    -+
    -+	# Check first revert message starts with "Revert"
    -+	git log --format=%s -1 revert-revert >msg1 &&
    -+	test_grep "^Revert" msg1 &&
    -+
    -+	FIRST_REVERT=$(git rev-parse revert-revert) &&
    -+
    -+	# Now revert the revert
    -+	git replay --revert --advance revert-revert $ORIGINAL..$FIRST_REVERT >result2 &&
    -+
    -+	test_must_be_empty result2 &&
    -+
    -+	# Check second revert message starts with "Reapply"
    -+	git log --format=%s -1 revert-revert >msg2 &&
    -+	test_grep "^Reapply" msg2 &&
    -+
    -+	# The file should exist again (reapplied)
    -+	git show revert-revert:revert-test-2.txt >actual &&
    -+	echo "content" >expected &&
    -+	test_cmp expected actual
    ++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 'replay --revert includes commit SHA in message' '
    -+	git switch -c revert-sha-test main &&
    -+	echo "test" >sha-test.txt &&
    -+	git add sha-test.txt &&
    -+	git commit -m "Test commit for SHA" &&
    -+
    -+	COMMIT_SHA=$(git rev-parse HEAD) &&
    -+	git replay --revert --advance revert-sha-test HEAD^..HEAD >result &&
    ++test_expect_success 'git replay --revert reflog message' '
    ++	# Store original state
    ++	START=$(git rev-parse revert-test) &&
    ++	test_when_finished "git branch -f revert-test $START" &&
     +
    -+	test_must_be_empty result &&
    ++	git replay --revert revert-test revert-test~1..revert-test >output &&
    ++	test_must_be_empty output &&
     +
    -+	# Check that the commit message includes the original SHA
    -+	git log --format=%B -1 revert-sha-test >msg &&
    -+	test_grep "$COMMIT_SHA" msg
    ++	# 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 'replay --revert with conflict' '
    -+	# Create a conflicting situation
    -+	git switch -c revert-conflict main &&
    -+	echo "line1" >conflict-file.txt &&
    -+	git add conflict-file.txt &&
    -+	git commit -m "Add conflict file" &&
    -+
    -+	git switch -c revert-conflict-branch HEAD^ &&
    -+	echo "different" >conflict-file.txt &&
    -+	git add conflict-file.txt &&
    -+	git commit -m "Different content" &&
    -+
    -+	# Try to revert the first commit onto the conflicting branch
    -+	test_expect_code 1 git replay --revert --onto revert-conflict-branch revert-conflict^..revert-conflict
    ++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 'replay --revert handles multiple commits' '
    -+	# Save the original topic2 state
    -+	ORIG_TOPIC2=$(git rev-parse topic2) &&
    -+	test_when_finished "git branch -f topic2 $ORIG_TOPIC2" &&
    -+
    -+	# Revert D and E from topic2, applying the reverts onto topic1
    -+	git replay --revert --onto topic1 topic1..topic2 >result &&
    -+
    -+	test_must_be_empty result &&
    -+
    -+	# Verify both revert commits appear in the log
    -+	git log --format=%s topic2 >log &&
    -+	head -n 2 log >first-two &&
    -+	test_grep "^Revert" first-two &&
    ++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
    ++'
     +
    -+	# Verify we have both "Revert D" and "Revert E"
    -+	test_grep "Revert.*E" log &&
    -+	test_grep "Revert.*D" log
    ++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
     +'
     +
      test_done

-- 
2.51.0

base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9

Thanks
- Siddharth

^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v2 1/2] sequencer: extract revert message formatting into shared function
  2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana
@ 2025-12-02 20:16   ` Siddharth Asthana
  2025-12-05 11:33     ` Patrick Steinhardt
  2025-12-02 20:16   ` [PATCH v2 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
  2026-02-18 23:42   ` [PATCH v3 0/2] " Siddharth Asthana
  2 siblings, 1 reply; 92+ messages in thread
From: Siddharth Asthana @ 2025-12-02 20:16 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon,
	Siddharth Asthana

The logic for formatting revert commit messages (handling "Revert" and
"Reapply" cases) is currently duplicated between sequencer.c and will be
needed by builtin/replay.c.

Extract this logic into a new sequencer_format_revert_header() function
that can be shared. The function handles both regular reverts ("Revert
"<subject>"") and revert-of-revert cases ("Reapply "<subject>"").
Update do_pick_commit() to use the new helper, eliminating code
duplication while preserving the special handling for commit_use_reference.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 sequencer.c | 39 +++++++++++++++++++++++++--------------
 sequencer.h |  8 ++++++++
 2 files changed, 33 insertions(+), 14 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index 5476d39ba9..9f621aef4b 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,
 		if (opts->commit_use_reference) {
 			strbuf_commented_addf(&ctx->message, comment_line_str,
 				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
-		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
-			   /*
-			    * We don't touch pre-existing repeated reverts, because
-			    * theoretically these can be nested arbitrarily deeply,
-			    * thus requiring excessive complexity to deal with.
-			    */
-			   !starts_with(orig_subject, "Revert \"")) {
-			strbuf_addstr(&ctx->message, "Reapply \"");
-			strbuf_addstr(&ctx->message, orig_subject);
-			strbuf_addstr(&ctx->message, "\n");
+			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
 		} else {
-			strbuf_addstr(&ctx->message, "Revert \"");
-			strbuf_addstr(&ctx->message, msg.subject);
-			strbuf_addstr(&ctx->message, "\"\n");
+			sequencer_format_revert_header(&ctx->message, msg.subject);
 		}
-		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
 		refer_to_commit(opts, &ctx->message, commit);
 
 		if (commit->parents && commit->parents->next) {
@@ -5572,6 +5560,29 @@ int sequencer_pick_revisions(struct repository *r,
 	return res;
 }
 
+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)
+{
+	const char *revert_subject;
+
+	if (skip_prefix(orig_subject, "Revert \"", &revert_subject) &&
+	    /*
+	     * We don't touch pre-existing repeated reverts, because
+	     * theoretically these can be nested arbitrarily deeply,
+	     * thus requiring excessive complexity to deal with.
+	     */
+	    !starts_with(revert_subject, "Revert \"")) {
+		strbuf_addstr(out, "Reapply \"");
+		strbuf_addstr(out, revert_subject);
+		strbuf_addch(out, '\n');
+	} else {
+		strbuf_addstr(out, "Revert \"");
+		strbuf_addstr(out, orig_subject);
+		strbuf_addstr(out, "\"\n");
+	}
+
+	strbuf_addstr(out, "\nThis reverts commit ");
+}
+
 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
 {
 	unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
diff --git a/sequencer.h b/sequencer.h
index 719684c8a9..114c5d2449 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -271,4 +271,12 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)
  */
 int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
 
+/*
+ * Formats a revert commit message header following standard Git conventions.
+ * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert
+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit " at the end.
+ * The caller should append the commit OID after calling this function.
+ */
+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject);
+
 #endif /* SEQUENCER_H */
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* [PATCH v2 2/2] replay: add --revert mode to reverse commit changes
  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-02 20:16   ` Siddharth Asthana
  2025-12-05 11:33     ` Patrick Steinhardt
  2025-12-16 16:23     ` Phillip Wood
  2026-02-18 23:42   ` [PATCH v3 0/2] " Siddharth Asthana
  2 siblings, 2 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-12-02 20:16 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon,
	Siddharth Asthana, Johannes Schindelin

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.

The implementation follows the same approach as sequencer.c (lines
2360-2399), where cherry-pick and revert are the same merge operation
but with swapped arguments:

  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
  - Revert: merge(ancestor=commit, ours=current, theirs=parent)

We swap the base and pickme trees when calling
merge_incore_nonrecursive(), effectively reversing the diff direction.
The existing conflict handling, ref updates, and atomic transaction
support work unchanged.

The commit messages follow git revert conventions: prefixed with
"Revert" and including the original commit SHA. When reverting a commit
that itself starts with "Revert", the message uses "Reapply" instead.
Unlike cherry-pick which preserves the original author, revert commits
use the current user as the author, matching the behavior of git revert.

Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  36 ++++++++-
 builtin/replay.c              | 145 ++++++++++++++++++++++++++++------
 t/t3650-replay-basics.sh      | 111 ++++++++++++++++++++++++++
 3 files changed, 267 insertions(+), 25 deletions(-)

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 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.
++
+The commit messages follow `git revert` conventions: prefixed with "Revert" and
+including the original commit SHA. 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.
+
+
 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,
+};
+
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
 {
@@ -57,10 +63,32 @@ static char *get_author(const char *message)
 	return NULL;
 }
 
+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");
+
+	free(subject);
+	repo_unuse_commit_buffer(repo, commit, message);
+}
+
 static struct commit *create_commit(struct repository *repo,
 				    struct tree *tree,
 				    struct commit *based_on,
-				    struct commit *parent)
+				    struct commit *parent,
+				    enum replay_action action)
 {
 	struct object_id ret;
 	struct object *obj = NULL;
@@ -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));
+	} else {
+		find_commit_subject(message, &orig_message);
+		strbuf_addstr(&msg, orig_message);
+		author = get_author(message);
+	}
 	reset_ident_date();
 	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
 				 &ret, author, NULL, sign_commit, extra)) {
@@ -90,7 +123,7 @@ static struct commit *create_commit(struct repository *repo,
 	obj = parse_object(repo, &ret);
 
 out:
-	repo_unuse_commit_buffer(the_repository, based_on, message);
+	repo_unuse_commit_buffer(repo, based_on, message);
 	free_commit_extra_headers(extra);
 	free_commit_list(parents);
 	strbuf_release(&msg);
@@ -166,6 +199,7 @@ static void determine_replay_mode(struct repository *repo,
 				  struct rev_cmdline_info *cmd_info,
 				  const char *onto_name,
 				  char **advance_name,
+				  char **revert_name,
 				  struct commit **onto,
 				  struct strset **update_refs)
 {
@@ -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"));
 	} else {
 		int positive_refs_complete = (
 			rinfo.positive_refexprs ==
@@ -261,7 +309,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  kh_oid_map_t *replayed_commits,
 					  struct commit *onto,
 					  struct merge_options *merge_opt,
-					  struct merge_result *result)
+					  struct merge_result *result,
+					  enum replay_action action)
 {
 	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree;
@@ -273,21 +322,39 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	pickme_tree = repo_get_commit_tree(repo, pickme);
 	base_tree = repo_get_commit_tree(repo, base);
 
-	merge_opt->branch1 = short_commit_name(repo, replayed_base);
-	merge_opt->branch2 = short_commit_name(repo, pickme);
-	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+	if (action == REPLAY_PICK) {
+		/* Cherry-pick: normal order */
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = short_commit_name(repo, pickme);
+		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
 
-	merge_incore_nonrecursive(merge_opt,
-				  base_tree,
-				  result->tree,
-				  pickme_tree,
-				  result);
+		merge_incore_nonrecursive(merge_opt,
+					  base_tree,
+					  result->tree,
+					  pickme_tree,
+					  result);
 
-	free((char*)merge_opt->ancestor);
+		free((char *)merge_opt->ancestor);
+	} else {
+		/* Revert: swap base and pickme to reverse the diff */
+		const char *pickme_name = short_commit_name(repo, pickme);
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
+		merge_opt->ancestor = pickme_name;
+
+		merge_incore_nonrecursive(merge_opt,
+					  pickme_tree,
+					  result->tree,
+					  base_tree,
+					  result);
+
+		free((char *)merge_opt->branch2);
+	}
 	merge_opt->ancestor = NULL;
+	merge_opt->branch2 = NULL;
 	if (!result->clean)
 		return NULL;
-	return create_commit(repo, result->tree, pickme, replayed_base);
+	return create_commit(repo, result->tree, pickme, replayed_base, action);
 }
 
 static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
@@ -345,6 +412,9 @@ int cmd_replay(int argc,
 {
 	const char *advance_name_opt = NULL;
 	char *advance_name = NULL;
+	const char *revert_name_opt = NULL;
+	char *revert_name = NULL;
+	enum replay_action action = REPLAY_PICK;
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
 	int contained = 0;
@@ -365,7 +435,7 @@ int cmd_replay(int argc,
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch>) "
+		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
 		   "[--ref-action[=<mode>]] <revision-range>..."),
 		NULL
 	};
@@ -378,6 +448,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &contained,
 			 N_("advance all branches contained in revision-range")),
+		OPT_STRING(0, "revert", &revert_name_opt,
+			   N_("branch"),
+			   N_("revert commits onto given branch")),
 		OPT_STRING(0, "ref-action", &ref_action,
 			   N_("mode"),
 			   N_("control ref update behavior (update|print)")),
@@ -387,18 +460,28 @@ int cmd_replay(int argc,
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!onto_name && !advance_name_opt) {
-		error(_("option --onto or --advance is mandatory"));
+	/* Exactly one mode must be specified */
+	if (!onto_name && !advance_name_opt && !revert_name_opt) {
+		error(_("exactly one of --onto, --advance, or --revert is required"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
-				  contained, "--contained");
+				  !!onto_name, "--onto");
+	die_for_incompatible_opt2(!!revert_name_opt, "--revert",
+				  !!onto_name, "--onto");
+	die_for_incompatible_opt2(!!revert_name_opt, "--revert",
+				  !!advance_name_opt, "--advance");
+	die_for_incompatible_opt2(contained, "--contained",
+				  !onto_name, "requires --onto");
 
 	/* Parse ref action mode from command line or config */
 	ref_mode = get_ref_action_mode(repo, ref_action);
 
 	advance_name = xstrdup_or_null(advance_name_opt);
+	revert_name = xstrdup_or_null(revert_name_opt);
+	if (revert_name)
+		action = REPLAY_REVERT;
 
 	repo_init_revisions(repo, &revs, prefix);
 
@@ -452,10 +535,13 @@ int cmd_replay(int argc,
 	}
 
 	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
+			      &revert_name,
 			      &onto, &update_refs);
 
 	/* Build reflog message */
-	if (advance_name_opt)
+	if (revert_name_opt)
+		strbuf_addf(&reflog_msg, "replay --revert %s", revert_name_opt);
+	else if (advance_name_opt)
 		strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
 	else
 		strbuf_addf(&reflog_msg, "replay --onto %s",
@@ -496,7 +582,7 @@ int cmd_replay(int argc,
 			die(_("replaying merge commits is not supported yet!"));
 
 		last_commit = pick_regular_commit(repo, commit, replayed_commits,
-						  onto, &merge_opt, &result);
+						  onto, &merge_opt, &result, action);
 		if (!last_commit)
 			break;
 
@@ -508,7 +594,7 @@ int cmd_replay(int argc,
 		kh_value(replayed_commits, pos) = last_commit;
 
 		/* Update any necessary branches */
-		if (advance_name)
+		if (advance_name || revert_name)
 			continue;
 		decoration = get_name_decoration(&commit->object);
 		if (!decoration)
@@ -532,7 +618,7 @@ int cmd_replay(int argc,
 		}
 	}
 
-	/* In --advance mode, advance the target ref */
+	/* In --advance or --revert mode, update the target ref */
 	if (result.clean == 1 && advance_name) {
 		if (handle_ref_update(ref_mode, transaction, advance_name,
 				      &last_commit->object.oid,
@@ -544,6 +630,17 @@ int cmd_replay(int argc,
 			goto cleanup;
 		}
 	}
+	if (result.clean == 1 && revert_name) {
+		if (handle_ref_update(ref_mode, transaction, revert_name,
+				      &last_commit->object.oid,
+				      &onto->object.oid,
+				      reflog_msg.buf,
+				      &transaction_err) < 0) {
+			ret = error(_("failed to update ref '%s': %s"),
+				    revert_name, transaction_err.buf);
+			goto cleanup;
+		}
+	}
 
 	/* Commit the ref transaction if we have one */
 	if (transaction && result.clean == 1) {
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
+'
+
+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_expect_success 'git replay --revert with --ref-action=print' '
+	# 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' '
+	# 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' '
+	# 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
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function
  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
  0 siblings, 1 reply; 92+ messages in thread
From: Patrick Steinhardt @ 2025-12-05 11:33 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon

On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:
> diff --git a/sequencer.c b/sequencer.c
> index 5476d39ba9..9f621aef4b 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,
>  		if (opts->commit_use_reference) {
>  			strbuf_commented_addf(&ctx->message, comment_line_str,
>  				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
> -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
> -			   /*
> -			    * We don't touch pre-existing repeated reverts, because
> -			    * theoretically these can be nested arbitrarily deeply,
> -			    * thus requiring excessive complexity to deal with.
> -			    */
> -			   !starts_with(orig_subject, "Revert \"")) {
> -			strbuf_addstr(&ctx->message, "Reapply \"");
> -			strbuf_addstr(&ctx->message, orig_subject);
> -			strbuf_addstr(&ctx->message, "\n");
> +			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>  		} else {
> -			strbuf_addstr(&ctx->message, "Revert \"");
> -			strbuf_addstr(&ctx->message, msg.subject);
> -			strbuf_addstr(&ctx->message, "\"\n");
> +			sequencer_format_revert_header(&ctx->message, msg.subject);
>  		}
> -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>  		refer_to_commit(opts, &ctx->message, commit);
>  
>  		if (commit->parents && commit->parents->next) {

Is there any reason why we don't also handle `refer_to_commit()` in that
new function?

Patrick

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes
  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
  1 sibling, 1 reply; 92+ messages in thread
From: Patrick Steinhardt @ 2025-12-05 11:33 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon

On Wed, Dec 03, 2025 at 01:46:11AM +0530, Siddharth Asthana wrote:
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 6606a2c94b..7660f7412f 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -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));
> +	} else {
> +		find_commit_subject(message, &orig_message);
> +		strbuf_addstr(&msg, orig_message);
> +		author = get_author(message);
> +	}
>  	reset_ident_date();
>  	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
>  				 &ret, author, NULL, sign_commit, extra)) {

Do we want to be defensive in those if-chains and verify that `action ==
REPLAY_PICK` in the other case, and `BUG()` if it's not?

> @@ -273,21 +322,39 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  	pickme_tree = repo_get_commit_tree(repo, pickme);
>  	base_tree = repo_get_commit_tree(repo, base);
>  
> -	merge_opt->branch1 = short_commit_name(repo, replayed_base);
> -	merge_opt->branch2 = short_commit_name(repo, pickme);
> -	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
> +	if (action == REPLAY_PICK) {
> +		/* Cherry-pick: normal order */
> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
> +		merge_opt->branch2 = short_commit_name(repo, pickme);
> +		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
>  
> -	merge_incore_nonrecursive(merge_opt,
> -				  base_tree,
> -				  result->tree,
> -				  pickme_tree,
> -				  result);
> +		merge_incore_nonrecursive(merge_opt,
> +					  base_tree,
> +					  result->tree,
> +					  pickme_tree,
> +					  result);
>  
> -	free((char*)merge_opt->ancestor);
> +		free((char *)merge_opt->ancestor);
> +	} else {
> +		/* Revert: swap base and pickme to reverse the diff */
> +		const char *pickme_name = short_commit_name(repo, pickme);
> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
> +		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
> +		merge_opt->ancestor = pickme_name;
> +
> +		merge_incore_nonrecursive(merge_opt,
> +					  pickme_tree,
> +					  result->tree,
> +					  base_tree,
> +					  result);
> +
> +		free((char *)merge_opt->branch2);
> +	}
>  	merge_opt->ancestor = NULL;
> +	merge_opt->branch2 = NULL;

We can `FREE_AND_NULL()` instead of manually unsetting these.

> @@ -387,18 +460,28 @@ int cmd_replay(int argc,
>  	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
>  			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
>  
> -	if (!onto_name && !advance_name_opt) {
> -		error(_("option --onto or --advance is mandatory"));
> +	/* Exactly one mode must be specified */
> +	if (!onto_name && !advance_name_opt && !revert_name_opt) {
> +		error(_("exactly one of --onto, --advance, or --revert is required"));
>  		usage_with_options(replay_usage, replay_options);
>  	}
>  
>  	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
> -				  contained, "--contained");
> +				  !!onto_name, "--onto");
> +	die_for_incompatible_opt2(!!revert_name_opt, "--revert",
> +				  !!onto_name, "--onto");
> +	die_for_incompatible_opt2(!!revert_name_opt, "--revert",
> +				  !!advance_name_opt, "--advance");
> +	die_for_incompatible_opt2(contained, "--contained",
> +				  !onto_name, "requires --onto");

We have `die_for_incompatible_opt3()` that can be used here to check for
mutual exclusivity of "--revert", "--advance" and "--onto".

> @@ -508,7 +594,7 @@ int cmd_replay(int argc,
>  		kh_value(replayed_commits, pos) = last_commit;
>  
>  		/* Update any necessary branches */
> -		if (advance_name)
> +		if (advance_name || revert_name)
>  			continue;
>  		decoration = get_name_decoration(&commit->object);
>  		if (!decoration)
> @@ -532,7 +618,7 @@ int cmd_replay(int argc,
>  		}
>  	}
>  
> -	/* In --advance mode, advance the target ref */
> +	/* In --advance or --revert mode, update the target ref */
>  	if (result.clean == 1 && advance_name) {
>  		if (handle_ref_update(ref_mode, transaction, advance_name,
>  				      &last_commit->object.oid,
> @@ -544,6 +630,17 @@ int cmd_replay(int argc,
>  			goto cleanup;
>  		}
>  	}
> +	if (result.clean == 1 && revert_name) {
> +		if (handle_ref_update(ref_mode, transaction, revert_name,
> +				      &last_commit->object.oid,
> +				      &onto->object.oid,
> +				      reflog_msg.buf,
> +				      &transaction_err) < 0) {
> +			ret = error(_("failed to update ref '%s': %s"),
> +				    revert_name, transaction_err.buf);
> +			goto cleanup;
> +		}
> +	}

This conditional and the one beforehand are the exact same, except that
we use either `revert_name` or `advance_name`. Let's merge them:

    if (result.clean == 1 && (revert_name || advance_name)) {
        const char *ref = revert_name ? revert_name : advance_name;
        if (handle_ref_update(ref_mode, transaction, ref,
                      &last_commit->object.oid,
                      &onto->object.oid,
                      reflog_msg.buf,
                      &transaction_err) < 0) {
            ret = error(_("failed to update ref '%s': %s"),
                    revert_name, transaction_err.buf);
            goto cleanup;
        }
    }

Patrick

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function
  2025-12-05 11:33     ` Patrick Steinhardt
@ 2025-12-07 23:00       ` Siddharth Asthana
  2025-12-08  7:07         ` Patrick Steinhardt
  0 siblings, 1 reply; 92+ messages in thread
From: Siddharth Asthana @ 2025-12-07 23:00 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, christian.couder, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon


On 05/12/25 17:03, Patrick Steinhardt wrote:
> On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:
>> diff --git a/sequencer.c b/sequencer.c
>> index 5476d39ba9..9f621aef4b 100644
>> --- a/sequencer.c
>> +++ b/sequencer.c
>> @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,
>>   		if (opts->commit_use_reference) {
>>   			strbuf_commented_addf(&ctx->message, comment_line_str,
>>   				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
>> -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
>> -			   /*
>> -			    * We don't touch pre-existing repeated reverts, because
>> -			    * theoretically these can be nested arbitrarily deeply,
>> -			    * thus requiring excessive complexity to deal with.
>> -			    */
>> -			   !starts_with(orig_subject, "Revert \"")) {
>> -			strbuf_addstr(&ctx->message, "Reapply \"");
>> -			strbuf_addstr(&ctx->message, orig_subject);
>> -			strbuf_addstr(&ctx->message, "\n");
>> +			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>   		} else {
>> -			strbuf_addstr(&ctx->message, "Revert \"");
>> -			strbuf_addstr(&ctx->message, msg.subject);
>> -			strbuf_addstr(&ctx->message, "\"\n");
>> +			sequencer_format_revert_header(&ctx->message, msg.subject);
>>   		}
>> -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>   		refer_to_commit(opts, &ctx->message, commit);
>>   
>>   		if (commit->parents && commit->parents->next) {
> Is there any reason why we don't also handle `refer_to_commit()` in that
> new function?


The `refer_to_commit()` function depends on `struct replay_opts` and its 
`commit_use_reference` flag, which controls whether to use abbreviated 
commit info ("%h (%s, %ad)") or the full OID. This is specific to 
sequencer.c's interactive workflow where users can choose the reference 
style via --reference.

In replay.c, we always use the full OID via `oid_to_hex()` since it's 
designed for non-interactive server-side operations without the 
`replay_opts` framework. Including `refer_to_commit()` would require 
either passing `replay_opts` to the shared function (leaking sequencer 
internals) or adding a format parameter which feels like 
over-engineering for current needs.

Happy to reconsider if you think there's a cleaner way to share this.

Thanks,
Siddharth


>
> Patrick

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes
  2025-12-05 11:33     ` Patrick Steinhardt
@ 2025-12-07 23:03       ` Siddharth Asthana
  0 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-12-07 23:03 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, christian.couder, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon


On 05/12/25 17:03, Patrick Steinhardt wrote:
> On Wed, Dec 03, 2025 at 01:46:11AM +0530, Siddharth Asthana wrote:
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index 6606a2c94b..7660f7412f 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -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));
>> +	} else {
>> +		find_commit_subject(message, &orig_message);
>> +		strbuf_addstr(&msg, orig_message);
>> +		author = get_author(message);
>> +	}
>>   	reset_ident_date();
>>   	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
>>   				 &ret, author, NULL, sign_commit, extra)) {
> Do we want to be defensive in those if-chains and verify that `action ==
> REPLAY_PICK` in the other case, and `BUG()` if it's not?


Good idea. I will add BUG() for unexpected action values to catch any 
future additions that aren't properly handled.


>
>> @@ -273,21 +322,39 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>   	pickme_tree = repo_get_commit_tree(repo, pickme);
>>   	base_tree = repo_get_commit_tree(repo, base);
>>   
>> -	merge_opt->branch1 = short_commit_name(repo, replayed_base);
>> -	merge_opt->branch2 = short_commit_name(repo, pickme);
>> -	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
>> +	if (action == REPLAY_PICK) {
>> +		/* Cherry-pick: normal order */
>> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
>> +		merge_opt->branch2 = short_commit_name(repo, pickme);
>> +		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
>>   
>> -	merge_incore_nonrecursive(merge_opt,
>> -				  base_tree,
>> -				  result->tree,
>> -				  pickme_tree,
>> -				  result);
>> +		merge_incore_nonrecursive(merge_opt,
>> +					  base_tree,
>> +					  result->tree,
>> +					  pickme_tree,
>> +					  result);
>>   
>> -	free((char*)merge_opt->ancestor);
>> +		free((char *)merge_opt->ancestor);
>> +	} else {
>> +		/* Revert: swap base and pickme to reverse the diff */
>> +		const char *pickme_name = short_commit_name(repo, pickme);
>> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
>> +		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
>> +		merge_opt->ancestor = pickme_name;
>> +
>> +		merge_incore_nonrecursive(merge_opt,
>> +					  pickme_tree,
>> +					  result->tree,
>> +					  base_tree,
>> +					  result);
>> +
>> +		free((char *)merge_opt->branch2);
>> +	}
>>   	merge_opt->ancestor = NULL;
>> +	merge_opt->branch2 = NULL;
> We can `FREE_AND_NULL()` instead of manually unsetting these.


Will use FREE_AND_NULL() - cleaner and clearer intent.


>
>> @@ -387,18 +460,28 @@ int cmd_replay(int argc,
>>   	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
>>   			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
>>   
>> -	if (!onto_name && !advance_name_opt) {
>> -		error(_("option --onto or --advance is mandatory"));
>> +	/* Exactly one mode must be specified */
>> +	if (!onto_name && !advance_name_opt && !revert_name_opt) {
>> +		error(_("exactly one of --onto, --advance, or --revert is required"));
>>   		usage_with_options(replay_usage, replay_options);
>>   	}
>>   
>>   	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>> -				  contained, "--contained");
>> +				  !!onto_name, "--onto");
>> +	die_for_incompatible_opt2(!!revert_name_opt, "--revert",
>> +				  !!onto_name, "--onto");
>> +	die_for_incompatible_opt2(!!revert_name_opt, "--revert",
>> +				  !!advance_name_opt, "--advance");
>> +	die_for_incompatible_opt2(contained, "--contained",
>> +				  !onto_name, "requires --onto");
> We have `die_for_incompatible_opt3()` that can be used here to check for
> mutual exclusivity of "--revert", "--advance" and "--onto".


Nice, I wasn't aware of this helper. Will use 
die_for_incompatible_opt3() for the three-way mutual exclusivity check.


>
>> @@ -508,7 +594,7 @@ int cmd_replay(int argc,
>>   		kh_value(replayed_commits, pos) = last_commit;
>>   
>>   		/* Update any necessary branches */
>> -		if (advance_name)
>> +		if (advance_name || revert_name)
>>   			continue;
>>   		decoration = get_name_decoration(&commit->object);
>>   		if (!decoration)
>> @@ -532,7 +618,7 @@ int cmd_replay(int argc,
>>   		}
>>   	}
>>   
>> -	/* In --advance mode, advance the target ref */
>> +	/* In --advance or --revert mode, update the target ref */
>>   	if (result.clean == 1 && advance_name) {
>>   		if (handle_ref_update(ref_mode, transaction, advance_name,
>>   				      &last_commit->object.oid,
>> @@ -544,6 +630,17 @@ int cmd_replay(int argc,
>>   			goto cleanup;
>>   		}
>>   	}
>> +	if (result.clean == 1 && revert_name) {
>> +		if (handle_ref_update(ref_mode, transaction, revert_name,
>> +				      &last_commit->object.oid,
>> +				      &onto->object.oid,
>> +				      reflog_msg.buf,
>> +				      &transaction_err) < 0) {
>> +			ret = error(_("failed to update ref '%s': %s"),
>> +				    revert_name, transaction_err.buf);
>> +			goto cleanup;
>> +		}
>> +	}
> This conditional and the one beforehand are the exact same, except that
> we use either `revert_name` or `advance_name`. Let's merge them:
>
>      if (result.clean == 1 && (revert_name || advance_name)) {
>          const char *ref = revert_name ? revert_name : advance_name;
>          if (handle_ref_update(ref_mode, transaction, ref,
>                        &last_commit->object.oid,
>                        &onto->object.oid,
>                        reflog_msg.buf,
>                        &transaction_err) < 0) {
>              ret = error(_("failed to update ref '%s': %s"),
>                      revert_name, transaction_err.buf);
>              goto cleanup;
>          }
>      }


Agreed, this is much cleaner. Will merge the conditionals as you 
suggest. Will incorporate all changes in v3.

Thanks for the thorough review!

Siddharth


>
> Patrick

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function
  2025-12-07 23:00       ` Siddharth Asthana
@ 2025-12-08  7:07         ` Patrick Steinhardt
  2026-02-11 13:03           ` Toon Claes
  0 siblings, 1 reply; 92+ messages in thread
From: Patrick Steinhardt @ 2025-12-08  7:07 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon

On Mon, Dec 08, 2025 at 04:30:58AM +0530, Siddharth Asthana wrote:
> 
> On 05/12/25 17:03, Patrick Steinhardt wrote:
> > On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:
> > > diff --git a/sequencer.c b/sequencer.c
> > > index 5476d39ba9..9f621aef4b 100644
> > > --- a/sequencer.c
> > > +++ b/sequencer.c
> > > @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,
> > >   		if (opts->commit_use_reference) {
> > >   			strbuf_commented_addf(&ctx->message, comment_line_str,
> > >   				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
> > > -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
> > > -			   /*
> > > -			    * We don't touch pre-existing repeated reverts, because
> > > -			    * theoretically these can be nested arbitrarily deeply,
> > > -			    * thus requiring excessive complexity to deal with.
> > > -			    */
> > > -			   !starts_with(orig_subject, "Revert \"")) {
> > > -			strbuf_addstr(&ctx->message, "Reapply \"");
> > > -			strbuf_addstr(&ctx->message, orig_subject);
> > > -			strbuf_addstr(&ctx->message, "\n");
> > > +			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
> > >   		} else {
> > > -			strbuf_addstr(&ctx->message, "Revert \"");
> > > -			strbuf_addstr(&ctx->message, msg.subject);
> > > -			strbuf_addstr(&ctx->message, "\"\n");
> > > +			sequencer_format_revert_header(&ctx->message, msg.subject);
> > >   		}
> > > -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
> > >   		refer_to_commit(opts, &ctx->message, commit);
> > >   		if (commit->parents && commit->parents->next) {
> > Is there any reason why we don't also handle `refer_to_commit()` in that
> > new function?
> 
> 
> The `refer_to_commit()` function depends on `struct replay_opts` and its
> `commit_use_reference` flag, which controls whether to use abbreviated
> commit info ("%h (%s, %ad)") or the full OID. This is specific to
> sequencer.c's interactive workflow where users can choose the reference
> style via --reference.
> 
> In replay.c, we always use the full OID via `oid_to_hex()` since it's
> designed for non-interactive server-side operations without the
> `replay_opts` framework. Including `refer_to_commit()` would require either
> passing `replay_opts` to the shared function (leaking sequencer internals)
> or adding a format parameter which feels like over-engineering for current
> needs.
> 
> Happy to reconsider if you think there's a cleaner way to share this.

A simple alternative might be to convert the `struct replay_opts`
parameter into a `flags` field that tells the function whether it is
expected to use the object ID or whether it should try using the
abbreviated commit info instead.

Patrick

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes
  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-16 16:23     ` Phillip Wood
  1 sibling, 0 replies; 92+ messages in thread
From: Phillip Wood @ 2025-12-16 16:23 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood, karthik.188,
	johannes.schindelin, toon

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


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function
  2025-12-08  7:07         ` Patrick Steinhardt
@ 2026-02-11 13:03           ` Toon Claes
  2026-02-11 13:40             ` Patrick Steinhardt
                               ` (2 more replies)
  0 siblings, 3 replies; 92+ messages in thread
From: Toon Claes @ 2026-02-11 13:03 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin,
	Patrick Steinhardt

Patrick Steinhardt <ps@pks.im> writes:

> On Mon, Dec 08, 2025 at 04:30:58AM +0530, Siddharth Asthana wrote:
>> 
>> On 05/12/25 17:03, Patrick Steinhardt wrote:
>> > On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:
>> > > diff --git a/sequencer.c b/sequencer.c
>> > > index 5476d39ba9..9f621aef4b 100644
>> > > --- a/sequencer.c
>> > > +++ b/sequencer.c
>> > > @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,
>> > >   		if (opts->commit_use_reference) {
>> > >   			strbuf_commented_addf(&ctx->message, comment_line_str,
>> > >   				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
>> > > -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
>> > > -			   /*
>> > > -			    * We don't touch pre-existing repeated reverts, because
>> > > -			    * theoretically these can be nested arbitrarily deeply,
>> > > -			    * thus requiring excessive complexity to deal with.
>> > > -			    */
>> > > -			   !starts_with(orig_subject, "Revert \"")) {
>> > > -			strbuf_addstr(&ctx->message, "Reapply \"");
>> > > -			strbuf_addstr(&ctx->message, orig_subject);
>> > > -			strbuf_addstr(&ctx->message, "\n");
>> > > +			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>> > >   		} else {
>> > > -			strbuf_addstr(&ctx->message, "Revert \"");
>> > > -			strbuf_addstr(&ctx->message, msg.subject);
>> > > -			strbuf_addstr(&ctx->message, "\"\n");
>> > > +			sequencer_format_revert_header(&ctx->message, msg.subject);
>> > >   		}
>> > > -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>> > >   		refer_to_commit(opts, &ctx->message, commit);
>> > >   		if (commit->parents && commit->parents->next) {
>> > Is there any reason why we don't also handle `refer_to_commit()` in that
>> > new function?
>> 
>> 
>> The `refer_to_commit()` function depends on `struct replay_opts` and its
>> `commit_use_reference` flag, which controls whether to use abbreviated
>> commit info ("%h (%s, %ad)") or the full OID. This is specific to
>> sequencer.c's interactive workflow where users can choose the reference
>> style via --reference.
>> 
>> In replay.c, we always use the full OID via `oid_to_hex()` since it's
>> designed for non-interactive server-side operations without the
>> `replay_opts` framework.

Even if it's non-interactive, I wonder if we should make it obey the
config 'revert.reference' as well? To me it makes sense git-replay(1)
and git-revert(1) give the same outcome if that config is set.

>> Including `refer_to_commit()` would require either
>> passing `replay_opts` to the shared function (leaking sequencer internals)
>> or adding a format parameter which feels like over-engineering for current
>> needs.
>> 
>> Happy to reconsider if you think there's a cleaner way to share this.
>
> A simple alternative might be to convert the `struct replay_opts`
> parameter into a `flags` field that tells the function whether it is
> expected to use the object ID or whether it should try using the
> abbreviated commit info instead.

I was considering to add a bool for this option alone, but I agree flags
is probably more future-proof.

Patrick, I assume you don't mean to revamp the `struct replay_opts`
completely, but only the parameter that would be passed into
sequencer_format_revert_header() and refer_to_commit()?

Siddharth, I see you have plenty of good reviews on this version of the
series ([PATCH 2/2] in particular). I'd love to see you post v3. Or do
you have any open questions you need answers to before you can send it
out? Let me know if I can help with any decision-making.

-- 
Cheers,
Toon

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function
  2026-02-11 13:03           ` Toon Claes
@ 2026-02-11 13:40             ` Patrick Steinhardt
  2026-02-11 15:23             ` Kristoffer Haugsbakk
  2026-02-18 22:53             ` Siddharth Asthana
  2 siblings, 0 replies; 92+ messages in thread
From: Patrick Steinhardt @ 2026-02-11 13:40 UTC (permalink / raw)
  To: Toon Claes
  Cc: Siddharth Asthana, git, christian.couder, newren, gitster,
	phillip.wood123, phillip.wood, karthik.188, johannes.schindelin

On Wed, Feb 11, 2026 at 02:03:22PM +0100, Toon Claes wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > parameter into a `flags` field that tells the function whether it is
> > expected to use the object ID or whether it should try using the
> > abbreviated commit info instead.
> 
> I was considering to add a bool for this option alone, but I agree flags
> is probably more future-proof.
> 
> Patrick, I assume you don't mean to revamp the `struct replay_opts`
> completely, but only the parameter that would be passed into
> sequencer_format_revert_header() and refer_to_commit()?

Yes, exactly.

Patrick

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function
  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
  2 siblings, 1 reply; 92+ messages in thread
From: Kristoffer Haugsbakk @ 2026-02-11 15:23 UTC (permalink / raw)
  To: Toon Claes, Siddharth Asthana
  Cc: git, Christian Couder, Elijah Newren, Junio C Hamano,
	Phillip Wood, Phillip Wood, Karthik Nayak, Johannes Schindelin,
	Patrick Steinhardt

On Wed, Feb 11, 2026, at 14:03, Toon Claes wrote:
>>>[snip]
>>> In replay.c, we always use the full OID via `oid_to_hex()` since it's
>>> designed for non-interactive server-side operations without the
>>> `replay_opts` framework.
>
> Even if it's non-interactive, I wonder if we should make it obey the
> config 'revert.reference' as well? To me it makes sense git-replay(1)
> and git-revert(1) give the same outcome if that config is set.

I don’t understand the position on plumbing commands. Should plumbing
commands ignore user configs so that results don’t change based on that?
Or should implementers that use this command set the config files to
`/dev/null` in order to opt out of the behavior?

>[snip]

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function
  2026-02-11 15:23             ` Kristoffer Haugsbakk
@ 2026-02-11 17:41               ` Junio C Hamano
  0 siblings, 0 replies; 92+ messages in thread
From: Junio C Hamano @ 2026-02-11 17:41 UTC (permalink / raw)
  To: Kristoffer Haugsbakk
  Cc: Toon Claes, Siddharth Asthana, git, Christian Couder,
	Elijah Newren, Phillip Wood, Phillip Wood, Karthik Nayak,
	Johannes Schindelin, Patrick Steinhardt

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:

> I don’t understand the position on plumbing commands. Should plumbing
> commands ignore user configs so that results don’t change based on that?
> Or should implementers that use this command set the config files to
> `/dev/null` in order to opt out of the behavior?

The former.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function
  2026-02-11 13:03           ` Toon Claes
  2026-02-11 13:40             ` Patrick Steinhardt
  2026-02-11 15:23             ` Kristoffer Haugsbakk
@ 2026-02-18 22:53             ` Siddharth Asthana
  2 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-02-18 22:53 UTC (permalink / raw)
  To: Toon Claes
  Cc: git, christian.couder, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin,
	Patrick Steinhardt


On 11/02/26 18:33, Toon Claes wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
>> On Mon, Dec 08, 2025 at 04:30:58AM +0530, Siddharth Asthana wrote:
>>> On 05/12/25 17:03, Patrick Steinhardt wrote:
>>>> On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:
>>>>> diff --git a/sequencer.c b/sequencer.c
>>>>> index 5476d39ba9..9f621aef4b 100644
>>>>> --- a/sequencer.c
>>>>> +++ b/sequencer.c
>>>>> @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,
>>>>>    		if (opts->commit_use_reference) {
>>>>>    			strbuf_commented_addf(&ctx->message, comment_line_str,
>>>>>    				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
>>>>> -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
>>>>> -			   /*
>>>>> -			    * We don't touch pre-existing repeated reverts, because
>>>>> -			    * theoretically these can be nested arbitrarily deeply,
>>>>> -			    * thus requiring excessive complexity to deal with.
>>>>> -			    */
>>>>> -			   !starts_with(orig_subject, "Revert \"")) {
>>>>> -			strbuf_addstr(&ctx->message, "Reapply \"");
>>>>> -			strbuf_addstr(&ctx->message, orig_subject);
>>>>> -			strbuf_addstr(&ctx->message, "\n");
>>>>> +			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>>>>    		} else {
>>>>> -			strbuf_addstr(&ctx->message, "Revert \"");
>>>>> -			strbuf_addstr(&ctx->message, msg.subject);
>>>>> -			strbuf_addstr(&ctx->message, "\"\n");
>>>>> +			sequencer_format_revert_header(&ctx->message, msg.subject);
>>>>>    		}
>>>>> -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>>>>    		refer_to_commit(opts, &ctx->message, commit);
>>>>>    		if (commit->parents && commit->parents->next) {
>>>> Is there any reason why we don't also handle `refer_to_commit()` in that
>>>> new function?
>>>
>>> The `refer_to_commit()` function depends on `struct replay_opts` and its
>>> `commit_use_reference` flag, which controls whether to use abbreviated
>>> commit info ("%h (%s, %ad)") or the full OID. This is specific to
>>> sequencer.c's interactive workflow where users can choose the reference
>>> style via --reference.
>>>
>>> In replay.c, we always use the full OID via `oid_to_hex()` since it's
>>> designed for non-interactive server-side operations without the
>>> `replay_opts` framework.
> Even if it's non-interactive, I wonder if we should make it obey the
> config 'revert.reference' as well? To me it makes sense git-replay(1)
> and git-revert(1) give the same outcome if that config is set.


Junio clarified downthread that plumbing commands should ignore user 
configs, so I think sticking with the full OID in replay is the right 
thing to do here.


>
>>> Including `refer_to_commit()` would require either
>>> passing `replay_opts` to the shared function (leaking sequencer internals)
>>> or adding a format parameter which feels like over-engineering for current
>>> needs.
>>>
>>> Happy to reconsider if you think there's a cleaner way to share this.
>> A simple alternative might be to convert the `struct replay_opts`
>> parameter into a `flags` field that tells the function whether it is
>> expected to use the object ID or whether it should try using the
>> abbreviated commit info instead.
> I was considering to add a bool for this option alone, but I agree flags
> is probably more future-proof.


For v3 i went with an optional `oid` parameter on 
sequencer_format_revert_header() -- when non-NULL the function appends 
the full hash itself, when NULL the caller (sequencer) handles the 
reference via refer_to_commit(). it is a simpler split than flags but 
gets the job done for now. I am more happy to switch to a flag approach 
if you and Patrick feel strongly about it.


>
> Patrick, I assume you don't mean to revamp the `struct replay_opts`
> completely, but only the parameter that would be passed into
> sequencer_format_revert_header() and refer_to_commit()?
>
> Siddharth, I see you have plenty of good reviews on this version of the
> series ([PATCH 2/2] in particular). I'd love to see you post v3. Or do
> you have any open questions you need answers to before you can send it
> out? Let me know if I can help with any decision-making.
>

Thanks Toon! v3 is ready, will share on mailing list thread soon. The 
main change beside addressing Patrick's and Phillip's review comments is 
a rebase on top of the latest upstream, which moved the replay logic 
into a separate library (replay.c / replay.h), so the diff looks quite 
different from v2 but the approach is the same.

- Siddharth


^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v3 0/2] replay: add --revert mode to reverse commit changes
  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-02 20:16   ` [PATCH v2 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
@ 2026-02-18 23:42   ` Siddharth Asthana
  2026-02-18 23:42     ` [PATCH v3 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
                       ` (2 more replies)
  2 siblings, 3 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-02-18 23:42 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon,
	Siddharth Asthana

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.

The implementation follows the same approach as sequencer.c where
cherry-pick and revert are the same merge operation but with swapped
arguments. For cherry-pick we merge(ancestor=parent, ours=current,
theirs=commit), while for revert we merge(ancestor=commit, ours=current,
theirs=parent). By swapping the base and pickme trees when calling
merge_incore_nonrecursive(), we effectively reverse the diff direction.

The series is structured as follows:

Patch 1 extracts the revert message formatting logic into a shared
sequencer_format_revert_header() function, eliminating code duplication
between sequencer.c and the upcoming replay code. This follows Junio's
suggestion to split the changes.

Patch 2 adds the --revert <branch> mode to git replay. Following the
architectural pattern suggested by Elijah and Phillip, --revert is a
standalone mode (like --onto and --advance) that takes a branch argument
and updates that branch with the revert commits.

The series is based on top of 864f55e190 (The second batch, 2026-02-07).

CI: https://gitlab.com/gitlab-org/git/-/pipelines/2329880894
The Windows CI failures (t4041, t4059, t4060, t4205, t6006) are
pre-existing infrastructure issues (missing iconv, submodule pathspec
errors) unrelated to this series.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
Changes in v3:
- Rebased on top of the latest upstream which refactored replay into
  a library (replay.c / replay.h). The --revert logic now lives in
  replay.c alongside the existing pick logic, while builtin/replay.c
  is a thin CLI wrapper.
- sequencer_format_revert_header() now takes an optional oid parameter
  so it can handle the full commit reference internally, per Patrick's
  suggestion about refer_to_commit()
- Removed now-unused `orig_subject` variable in do_pick_commit()
- Switched to die_for_incompatible_opt3() for --onto/--advance/--revert
  mutual exclusivity, per Patrick
- --contained now just checks "die(_("--contained requires --onto"))"
  instead of going through die_for_incompatible_opt2, per Phillip
- Added BUG() guards for unhandled replay_mode values, per Patrick
- Merged the separate advance/revert ref update blocks into one
- author is set to NULL for revert commits so commit_tree_extended()
  picks up the current user, per Phillip
- Factored out common --advance/--revert branch validation into a
  set_up_branch_mode() helper, per Phillip
- Doc wording fixes: "reverted commits", "they are prefixed",
  "hash" instead of "SHA", per Phillip
- Tests now reuse topic4 instead of creating new branches, use heredoc
  for test_commit_message, and the reflog check is folded into the
  main revert test
- Added tests for bare repo revert, error cases (argument validation,
  multiple sources)
- Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/t/#u
- Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/t/#u

---
 Documentation/git-replay.adoc |  37 +++++++-
 builtin/replay.c              |  25 ++++--
 replay.c                      | 162 ++++++++++++++++++++++++----------
 replay.h                      |  11 ++-
 sequencer.c                   |  47 ++++++----
 sequencer.h                   |  11 +++
 t/t3650-replay-basics.sh      | 107 ++++++++++++++++++++--
 7 files changed, 319 insertions(+), 81 deletions(-)

Siddharth Asthana (2):
  sequencer: extract revert message formatting into shared function
  replay: add --revert mode to reverse commit changes

Range-diff versus v2:

1:  bfd75484b4 ! 1:  9d686bcdfe sequencer: extract revert message formatting into shared function
    @@ Commit message
         Extract this logic into a new sequencer_format_revert_header() function
         that can be shared. The function handles both regular reverts ("Revert
         "<subject>"") and revert-of-revert cases ("Reapply "<subject>"").
    +    When an oid is provided, the function appends the full commit hash and
    +    period; otherwise the caller should append the commit reference.
    +
         Update do_pick_commit() to use the new helper, eliminating code
         duplication while preserving the special handling for commit_use_reference.
     
         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## sequencer.c ##
    +@@ sequencer.c: static int do_pick_commit(struct repository *r,
    + 	 */
    + 
    + 	if (command == TODO_REVERT) {
    +-		const char *orig_subject;
    +-
    + 		base = commit;
    + 		base_label = msg.label;
    + 		next = parent;
     @@ sequencer.c: static int do_pick_commit(struct repository *r,
      		if (opts->commit_use_reference) {
      			strbuf_commented_addf(&ctx->message, comment_line_str,
     @@ sequencer.c: static int do_pick_commit(struct repository *r,
     -			strbuf_addstr(&ctx->message, "Revert \"");
     -			strbuf_addstr(&ctx->message, msg.subject);
     -			strbuf_addstr(&ctx->message, "\"\n");
    -+			sequencer_format_revert_header(&ctx->message, msg.subject);
    ++			sequencer_format_revert_header(&ctx->message, msg.subject, NULL);
      		}
     -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
      		refer_to_commit(opts, &ctx->message, commit);
    @@ sequencer.c: int sequencer_pick_revisions(struct repository *r,
      	return res;
      }
      
    -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)
    ++void sequencer_format_revert_header(struct strbuf *out,
    ++				    const char *orig_subject,
    ++				    const struct object_id *oid)
     +{
     +	const char *revert_subject;
     +
    @@ sequencer.c: int sequencer_pick_revisions(struct repository *r,
     +	}
     +
     +	strbuf_addstr(out, "\nThis reverts commit ");
    ++	if (oid) {
    ++		strbuf_addstr(out, oid_to_hex(oid));
    ++		strbuf_addstr(out, ".\n");
    ++	}
     +}
     +
      void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
    @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh
      int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
      
     +/*
    -+ * Formats a revert commit message header following standard Git conventions.
    ++ * Formats a revert commit message following standard Git conventions.
     + * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert
    -+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit " at the end.
    -+ * The caller should append the commit OID after calling this function.
    ++ * cases ("Reapply \"<subject>\""). Adds "This reverts commit <oid>." if oid
    ++ * is provided, otherwise just adds "This reverts commit " and the caller
    ++ * should append the commit reference.
     + */
    -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject);
    ++void sequencer_format_revert_header(struct strbuf *out,
    ++				    const char *orig_subject,
    ++				    const struct object_id *oid);
     +
      #endif /* SEQUENCER_H */
2:  a2f99bc8c2 < -:  ---------- replay: add --revert mode to reverse commit changes
-:  ---------- > 2:  a8eae7b802 replay: add --revert mode to reverse commit changes


base-commit: 864f55e1906897b630333675a52874c0fec2a45c

Thanks
- Siddharth

^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v3 1/2] sequencer: extract revert message formatting into shared function
  2026-02-18 23:42   ` [PATCH v3 0/2] " Siddharth Asthana
@ 2026-02-18 23:42     ` Siddharth Asthana
  2026-02-20 17:01       ` Toon Claes
  2026-02-26 14:27       ` Phillip Wood
  2026-02-18 23:42     ` [PATCH v3 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
  2026-03-13  5:40     ` [PATCH v4 0/2] " Siddharth Asthana
  2 siblings, 2 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-02-18 23:42 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon,
	Siddharth Asthana

The logic for formatting revert commit messages (handling "Revert" and
"Reapply" cases) is currently duplicated between sequencer.c and will be
needed by builtin/replay.c.

Extract this logic into a new sequencer_format_revert_header() function
that can be shared. The function handles both regular reverts ("Revert
"<subject>"") and revert-of-revert cases ("Reapply "<subject>"").
When an oid is provided, the function appends the full commit hash and
period; otherwise the caller should append the commit reference.

Update do_pick_commit() to use the new helper, eliminating code
duplication while preserving the special handling for commit_use_reference.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 sequencer.c | 47 +++++++++++++++++++++++++++++++----------------
 sequencer.h | 11 +++++++++++
 2 files changed, 42 insertions(+), 16 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index 1f492f8460..b32347c853 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2356,8 +2356,6 @@ static int do_pick_commit(struct repository *r,
 	 */
 
 	if (command == TODO_REVERT) {
-		const char *orig_subject;
-
 		base = commit;
 		base_label = msg.label;
 		next = parent;
@@ -2365,22 +2363,10 @@ static int do_pick_commit(struct repository *r,
 		if (opts->commit_use_reference) {
 			strbuf_commented_addf(&ctx->message, comment_line_str,
 				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
-		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
-			   /*
-			    * We don't touch pre-existing repeated reverts, because
-			    * theoretically these can be nested arbitrarily deeply,
-			    * thus requiring excessive complexity to deal with.
-			    */
-			   !starts_with(orig_subject, "Revert \"")) {
-			strbuf_addstr(&ctx->message, "Reapply \"");
-			strbuf_addstr(&ctx->message, orig_subject);
-			strbuf_addstr(&ctx->message, "\n");
+			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
 		} else {
-			strbuf_addstr(&ctx->message, "Revert \"");
-			strbuf_addstr(&ctx->message, msg.subject);
-			strbuf_addstr(&ctx->message, "\"\n");
+			sequencer_format_revert_header(&ctx->message, msg.subject, NULL);
 		}
-		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
 		refer_to_commit(opts, &ctx->message, commit);
 
 		if (commit->parents && commit->parents->next) {
@@ -5572,6 +5558,35 @@ int sequencer_pick_revisions(struct repository *r,
 	return res;
 }
 
+void sequencer_format_revert_header(struct strbuf *out,
+				    const char *orig_subject,
+				    const struct object_id *oid)
+{
+	const char *revert_subject;
+
+	if (skip_prefix(orig_subject, "Revert \"", &revert_subject) &&
+	    /*
+	     * We don't touch pre-existing repeated reverts, because
+	     * theoretically these can be nested arbitrarily deeply,
+	     * thus requiring excessive complexity to deal with.
+	     */
+	    !starts_with(revert_subject, "Revert \"")) {
+		strbuf_addstr(out, "Reapply \"");
+		strbuf_addstr(out, revert_subject);
+		strbuf_addch(out, '\n');
+	} else {
+		strbuf_addstr(out, "Revert \"");
+		strbuf_addstr(out, orig_subject);
+		strbuf_addstr(out, "\"\n");
+	}
+
+	strbuf_addstr(out, "\nThis reverts commit ");
+	if (oid) {
+		strbuf_addstr(out, oid_to_hex(oid));
+		strbuf_addstr(out, ".\n");
+	}
+}
+
 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
 {
 	unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
diff --git a/sequencer.h b/sequencer.h
index 719684c8a9..b7291cc52d 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -271,4 +271,15 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)
  */
 int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
 
+/*
+ * Formats a revert commit message following standard Git conventions.
+ * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert
+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit <oid>." if oid
+ * is provided, otherwise just adds "This reverts commit " and the caller
+ * should append the commit reference.
+ */
+void sequencer_format_revert_header(struct strbuf *out,
+				    const char *orig_subject,
+				    const struct object_id *oid);
+
 #endif /* SEQUENCER_H */
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  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-18 23:42     ` Siddharth Asthana
  2026-02-20 17:35       ` Toon Claes
  2026-02-26 14:45       ` Phillip Wood
  2026-03-13  5:40     ` [PATCH v4 0/2] " Siddharth Asthana
  2 siblings, 2 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-02-18 23:42 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, toon,
	Siddharth Asthana, Johannes Schindelin

Add a `--revert <branch>` mode to git replay that undoes the changes
introduced by the specified commits. Like --onto and --advance, --revert
is a standalone mode: it takes a branch argument and updates that branch
with the newly created revert commits.

At GitLab, we need this in Gitaly for reverting commits directly on bare
repositories without requiring a working tree checkout.

The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick
and revert are just the same three-way merge with swapped arguments:

  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
  - Revert: merge(ancestor=commit, ours=current, theirs=parent)

We swap the base and pickme trees passed to merge_incore_nonrecursive()
to reverse the diff direction.

Revert commit messages follow the usual git revert conventions: prefixed
with "Revert" (or "Reapply" when reverting a revert), and including
"This reverts commit <hash>.". The author is set to the current user
rather than preserving the original author, matching git revert behavior.

Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  37 +++++++-
 builtin/replay.c              |  25 ++++--
 replay.c                      | 162 ++++++++++++++++++++++++----------
 replay.h                      |  11 ++-
 t/t3650-replay-basics.sh      | 107 ++++++++++++++++++++--
 5 files changed, 277 insertions(+), 65 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 8d696ce3ab..ffdf790278 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,25 @@ 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 reverted commits; must be a
+	branch name.
++
+When `--revert` is specified, the commits in the revision range are reverted
+(their changes are undone) and the reverted commits are created on top of
+<branch>. The <branch> is then updated to point at the new commits. This is
+the same as running `git revert <revision-range>` but does not update the
+working tree.
++
+The commit messages follow `git revert` conventions: they are prefixed with
+"Revert" and include "This reverts commit <hash>." When reverting a commit
+whose message starts with "Revert", the new message uses "Reapply" instead.
+Unlike cherry-pick which preserves the original author, revert commits use
+the current user as the author, matching the behavior of `git revert`.
++
+This option is mutually exclusive with `--onto` and `--advance`. It is also
+incompatible with `--contained` (which is a modifier for `--onto` only).
+
 --contained::
 	Update all branches that point at commits in
 	<revision-range>. Requires `--onto`.
@@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 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).
+the shape of the history being replayed.  When using `--advance` or
+`--revert`, the number of refs updated is always one, but for `--onto`,
+it can be one or more (rebasing multiple branches simultaneously is
+supported).
 
 There is no stderr output on conflicts; see the <<exit-status,EXIT
 STATUS>> section below.
@@ -152,6 +172,15 @@ 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 on a branch:
+
+------------
+$ git replay --revert main main~2..main
+------------
+
+This reverts the last two commits on `main`, creating two revert commits
+on top of `main`, and updates `main` to point at the result.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 2cdde830a8..28ce5196db 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -83,8 +83,8 @@ int cmd_replay(int argc,
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "[--ref-action[=<mode>]] <revision-range>"),
+		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
+		   "[--ref-action[=<mode>]] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -96,6 +96,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &opts.contained,
 			 N_("update all branches that point at commits in <revision-range>")),
+		OPT_STRING(0, "revert", &opts.revert,
+			   N_("branch"),
+			   N_("revert commits onto given branch")),
 		OPT_STRING(0, "ref-action", &ref_action,
 			   N_("mode"),
 			   N_("control ref update behavior (update|print)")),
@@ -105,15 +108,17 @@ int cmd_replay(int argc,
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!opts.onto && !opts.advance) {
-		error(_("option --onto or --advance is mandatory"));
+	/* Exactly one mode must be specified */
+	if (!opts.onto && !opts.advance && !opts.revert) {
+		error(_("exactly one of --onto, --advance, or --revert is required"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	die_for_incompatible_opt2(!!opts.advance, "--advance",
-				  opts.contained, "--contained");
-	die_for_incompatible_opt2(!!opts.advance, "--advance",
-				  !!opts.onto, "--onto");
+	die_for_incompatible_opt3(!!opts.onto, "--onto",
+				  !!opts.advance, "--advance",
+				  !!opts.revert, "--revert");
+	if (opts.contained && !opts.onto)
+		die(_("--contained requires --onto"));
 
 	/* Parse ref action mode from command line or config */
 	ref_mode = get_ref_action_mode(repo, ref_action);
@@ -174,7 +179,9 @@ int cmd_replay(int argc,
 		goto cleanup;
 
 	/* Build reflog message */
-	if (opts.advance) {
+	if (opts.revert) {
+		strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
+	} else if (opts.advance) {
 		strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
 	} else {
 		struct object_id oid;
diff --git a/replay.c b/replay.c
index f97d652f33..6f8b5720b3 100644
--- a/replay.c
+++ b/replay.c
@@ -8,9 +8,15 @@
 #include "refs.h"
 #include "replay.h"
 #include "revision.h"
+#include "sequencer.h"
 #include "strmap.h"
 #include "tree.h"
 
+enum replay_mode {
+	REPLAY_MODE_PICK,
+	REPLAY_MODE_REVERT,
+};
+
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
 {
@@ -44,15 +50,35 @@ static char *get_author(const char *message)
 	return NULL;
 }
 
+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, &commit->object.oid);
+
+	free(subject);
+	repo_unuse_commit_buffer(repo, commit, message);
+}
+
 static struct commit *create_commit(struct repository *repo,
 				    struct tree *tree,
 				    struct commit *based_on,
-				    struct commit *parent)
+				    struct commit *parent,
+				    enum replay_mode mode)
 {
 	struct object_id ret;
 	struct object *obj = NULL;
 	struct commit_list *parents = NULL;
-	char *author;
+	char *author = NULL;
 	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra = NULL;
 	struct strbuf msg = STRBUF_INIT;
@@ -64,9 +90,16 @@ 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 (mode == REPLAY_MODE_REVERT) {
+		generate_revert_message(&msg, based_on, repo);
+		/* For revert, use current user as author (NULL = use default) */
+	} else if (mode == REPLAY_MODE_PICK) {
+		find_commit_subject(message, &orig_message);
+		strbuf_addstr(&msg, orig_message);
+		author = get_author(message);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	reset_ident_date();
 	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
 				 &ret, author, NULL, sign_commit, extra)) {
@@ -147,11 +180,34 @@ static void get_ref_information(struct repository *repo,
 	}
 }
 
+static void set_up_branch_mode(struct repository *repo,
+			       char **branch_name,
+			       const char *option_name,
+			       struct ref_info *rinfo,
+			       struct commit **onto)
+{
+	struct object_id oid;
+	char *fullname = NULL;
+
+	if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
+			  &oid, &fullname, 0) == 1) {
+		free(*branch_name);
+		*branch_name = fullname;
+	} else {
+		die(_("argument to %s must be a reference"), option_name);
+	}
+	*onto = peel_committish(repo, *branch_name, option_name);
+	if (rinfo->positive_refexprs > 1)
+		die(_("cannot %s target with multiple sources because ordering would be ill-defined"),
+		    option_name + 2); /* skip "--" prefix */
+}
+
 static void set_up_replay_mode(struct repository *repo,
 			       struct rev_cmdline_info *cmd_info,
 			       const char *onto_name,
 			       bool *detached_head,
 			       char **advance_name,
+			       char **revert_name,
 			       struct commit **onto,
 			       struct strset **update_refs)
 {
@@ -166,9 +222,6 @@ static void set_up_replay_mode(struct repository *repo,
 	if (!rinfo.positive_refexprs)
 		die(_("need some commits to replay"));
 
-	if (!onto_name == !*advance_name)
-		BUG("one and only one of onto_name and *advance_name must be given");
-
 	if (onto_name) {
 		*onto = peel_committish(repo, onto_name, "--onto");
 		if (rinfo.positive_refexprs <
@@ -177,23 +230,12 @@ static void set_up_replay_mode(struct repository *repo,
 		*update_refs = xcalloc(1, sizeof(**update_refs));
 		**update_refs = rinfo.positive_refs;
 		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	} else if (*advance_name) {
+		set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
+	} else if (*revert_name) {
+		set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
 	} else {
-		struct object_id oid;
-		char *fullname = NULL;
-
-		if (!*advance_name)
-			BUG("expected either onto_name or *advance_name in this function");
-
-		if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
-			     &oid, &fullname, 0) == 1) {
-			free(*advance_name);
-			*advance_name = fullname;
-		} else {
-			die(_("argument to --advance must be a reference"));
-		}
-		*onto = peel_committish(repo, *advance_name, "--advance");
-		if (rinfo.positive_refexprs > 1)
-			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+		BUG("expected one of onto_name, *advance_name, or *revert_name");
 	}
 	strset_clear(&rinfo.negative_refs);
 	strset_clear(&rinfo.positive_refs);
@@ -214,7 +256,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  kh_oid_map_t *replayed_commits,
 					  struct commit *onto,
 					  struct merge_options *merge_opt,
-					  struct merge_result *result)
+					  struct merge_result *result,
+					  enum replay_mode mode)
 {
 	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	pickme_tree = repo_get_commit_tree(repo, pickme);
 	base_tree = repo_get_commit_tree(repo, base);
 
-	merge_opt->branch1 = short_commit_name(repo, replayed_base);
-	merge_opt->branch2 = short_commit_name(repo, pickme);
-	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
-
-	merge_incore_nonrecursive(merge_opt,
-				  base_tree,
-				  replayed_base_tree,
-				  pickme_tree,
-				  result);
-
-	free((char*)merge_opt->ancestor);
+	if (mode == REPLAY_MODE_PICK) {
+		/* Cherry-pick: normal order */
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = short_commit_name(repo, pickme);
+		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+		merge_incore_nonrecursive(merge_opt,
+					  base_tree,
+					  replayed_base_tree,
+					  pickme_tree,
+					  result);
+
+		free((char *)merge_opt->ancestor);
+	} else if (mode == REPLAY_MODE_REVERT) {
+		/* Revert: swap base and pickme to reverse the diff */
+		const char *pickme_name = short_commit_name(repo, pickme);
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
+		merge_opt->ancestor = pickme_name;
+
+		merge_incore_nonrecursive(merge_opt,
+					  pickme_tree,
+					  replayed_base_tree,
+					  base_tree,
+					  result);
+
+		free((char *)merge_opt->branch2);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	merge_opt->ancestor = NULL;
+	merge_opt->branch2 = NULL;
 	if (!result->clean)
 		return NULL;
-	/* Drop commits that become empty */
-	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
+	/* Drop commits that become empty (only for picks) */
+	if (mode == REPLAY_MODE_PICK &&
+	    oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
 	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
 		return replayed_base;
-	return create_commit(repo, result->tree, pickme, replayed_base);
+	return create_commit(repo, result->tree, pickme, replayed_base, mode);
 }
 
 void replay_result_release(struct replay_result *result)
@@ -281,11 +345,16 @@ int replay_revisions(struct rev_info *revs,
 	};
 	bool detached_head;
 	char *advance;
+	char *revert;
+	enum replay_mode mode = REPLAY_MODE_PICK;
 	int ret;
 
 	advance = xstrdup_or_null(opts->advance);
+	revert = xstrdup_or_null(opts->revert);
+	if (revert)
+		mode = REPLAY_MODE_REVERT;
 	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
-			   &detached_head, &advance, &onto, &update_refs);
+			   &detached_head, &advance, &revert, &onto, &update_refs);
 
 	/* FIXME: Should allow replaying commits with the first as a root commit */
 
@@ -309,7 +378,7 @@ int replay_revisions(struct rev_info *revs,
 			die(_("replaying merge commits is not supported yet!"));
 
 		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
-						  onto, &merge_opt, &result);
+						  onto, &merge_opt, &result, mode);
 		if (!last_commit)
 			break;
 
@@ -321,7 +390,7 @@ int replay_revisions(struct rev_info *revs,
 		kh_value(replayed_commits, pos) = last_commit;
 
 		/* Update any necessary branches */
-		if (advance)
+		if (advance || revert)
 			continue;
 
 		for (decoration = get_name_decoration(&commit->object);
@@ -355,11 +424,13 @@ int replay_revisions(struct rev_info *revs,
 		goto out;
 	}
 
-	/* In --advance mode, advance the target ref */
-	if (advance)
-		replay_result_queue_update(out, advance,
+	/* In --advance or --revert mode, update the target ref */
+	if (advance || revert) {
+		const char *ref = advance ? advance : revert;
+		replay_result_queue_update(out, ref,
 					   &onto->object.oid,
 					   &last_commit->object.oid);
+	}
 
 	ret = 0;
 
@@ -371,5 +442,6 @@ int replay_revisions(struct rev_info *revs,
 	kh_destroy_oid_map(replayed_commits);
 	merge_finalize(&merge_opt, &result);
 	free(advance);
+	free(revert);
 	return ret;
 }
diff --git a/replay.h b/replay.h
index d8407dc7f7..e916a5f975 100644
--- a/replay.h
+++ b/replay.h
@@ -13,7 +13,7 @@ struct replay_revisions_options {
 	/*
 	 * Starting point at which to create the new commits; must be a branch
 	 * name. The branch will be updated to point to the rewritten commits.
-	 * This option is mutually exclusive with `onto`.
+	 * This option is mutually exclusive with `onto` and `revert`.
 	 */
 	const char *advance;
 
@@ -22,7 +22,14 @@ struct replay_revisions_options {
 	 * committish. References pointing at decendants of `onto` will be
 	 * updated to point to the new commits.
 	 */
-	 const char *onto;
+	const char *onto;
+
+	/*
+	 * Starting point at which to create revert commits; must be a branch
+	 * name. The branch will be updated to point to the revert commits.
+	 * This option is mutually exclusive with `onto` and `advance`.
+	 */
+	const char *revert;
 
 	/*
 	 * Update branches that point at commits in the given revision range.
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a03f8f9293..ca517cf607 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '
 	test_cmp expect actual
 '
 
-test_expect_success 'option --onto or --advance is mandatory' '
-	echo "error: option --onto or --advance is mandatory" >expect &&
+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '
+	echo "error: exactly one of --onto, --advance, or --revert is required" >expect &&
 	test_might_fail git replay -h >>expect &&
 	test_must_fail git replay topic1..topic2 2>actual &&
 	test_cmp expect actual
@@ -87,9 +87,8 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err
 	test_cmp expect actual
 '
 
-test_expect_success 'options --advance and --contained cannot be used together' '
-	printf "fatal: options ${SQ}--advance${SQ} " >expect &&
-	printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
+test_expect_success '--contained requires --onto' '
+	echo "fatal: --contained requires --onto" >expect &&
 	test_must_fail git replay --advance=main --contained \
 		topic1..topic2 2>actual &&
 	test_cmp expect actual
@@ -398,4 +397,102 @@ test_expect_success 'invalid replay.refAction value' '
 	test_grep "invalid.*replay.refAction.*value" error
 '
 
+test_expect_success 'argument to --revert must be a reference' '
+	echo "fatal: argument to --revert must be a reference" >expect &&
+	oid=$(git rev-parse main) &&
+	test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'cannot revert with multiple sources' '
+	echo "fatal: cannot revert target with multiple sources because ordering would be ill-defined" >expect &&
+	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using replay --revert to revert commits' '
+	# Reuse existing topic4 branch (has commits I and J on top of main)
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# Revert commits I and J
+	git replay --revert topic4 topic4~2..topic4 &&
+
+	# Verify the revert commits were created
+	git log --format=%s -4 topic4 >actual &&
+	cat >expect <<-\EOF &&
+	Revert "J"
+	Revert "I"
+	J
+	I
+	EOF
+	test_cmp expect actual &&
+
+	# Verify commit message format includes hash
+	test_commit_message topic4 <<-EOF &&
+	Revert "J"
+
+	This reverts commit $(git rev-parse J).
+	EOF
+
+	# Verify reflog message
+	git reflog topic4 -1 --format=%gs >reflog-msg &&
+	echo "replay --revert topic4" >expect-reflog &&
+	test_cmp expect-reflog reflog-msg
+'
+
+test_expect_success 'using replay --revert in bare repo' '
+	# Reuse existing topic4 in bare repo
+	START=$(git -C bare rev-parse topic4) &&
+	test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
+
+	# Revert commit J in bare repo
+	git -C bare replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify revert was created
+	git -C bare log -1 --format=%s topic4 >actual &&
+	echo "Revert \"J\"" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'revert of revert uses Reapply' '
+	# Use topic4 and first revert J, then revert the revert
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# First revert J
+	git replay --revert topic4 topic4~1..topic4 &&
+	REVERT_J=$(git rev-parse topic4) &&
+
+	# Now revert the revert - should become Reapply
+	git replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify Reapply prefix and message format
+	test_commit_message topic4 <<-EOF
+	Reapply "J"
+
+	This reverts commit $REVERT_J.
+	EOF
+'
+
+test_expect_success 'git replay --revert with conflict' '
+	# conflict branch has C.conflict which conflicts with topic1s C
+	test_expect_code 1 git replay --revert conflict B..topic1
+'
+
+test_expect_success 'git replay --revert incompatible with --contained' '
+	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
+	test_grep "requires --onto" error
+'
+
+test_expect_success 'git replay --revert incompatible with --onto' '
+	test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --advance' '
+	test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function
  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:31         ` Siddharth Asthana
  2026-02-26 14:27       ` Phillip Wood
  1 sibling, 2 replies; 92+ messages in thread
From: Toon Claes @ 2026-02-20 17:01 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, Siddharth Asthana

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> The logic for formatting revert commit messages (handling "Revert" and
> "Reapply" cases) is currently duplicated between sequencer.c and will be
> needed by builtin/replay.c.
>
> Extract this logic into a new sequencer_format_revert_header() function
> that can be shared. The function handles both regular reverts ("Revert
> "<subject>"") and revert-of-revert cases ("Reapply "<subject>"").
> When an oid is provided, the function appends the full commit hash and
> period; otherwise the caller should append the commit reference.
>
> Update do_pick_commit() to use the new helper, eliminating code
> duplication while preserving the special handling for commit_use_reference.
>
> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> ---
>  sequencer.c | 47 +++++++++++++++++++++++++++++++----------------
>  sequencer.h | 11 +++++++++++
>  2 files changed, 42 insertions(+), 16 deletions(-)
>
> diff --git a/sequencer.c b/sequencer.c
> index 1f492f8460..b32347c853 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -2356,8 +2356,6 @@ static int do_pick_commit(struct repository *r,
>  	 */
>  
>  	if (command == TODO_REVERT) {
> -		const char *orig_subject;
> -
>  		base = commit;
>  		base_label = msg.label;
>  		next = parent;
> @@ -2365,22 +2363,10 @@ static int do_pick_commit(struct repository *r,
>  		if (opts->commit_use_reference) {
>  			strbuf_commented_addf(&ctx->message, comment_line_str,
>  				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
> -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
> -			   /*
> -			    * We don't touch pre-existing repeated reverts, because
> -			    * theoretically these can be nested arbitrarily deeply,
> -			    * thus requiring excessive complexity to deal with.
> -			    */
> -			   !starts_with(orig_subject, "Revert \"")) {
> -			strbuf_addstr(&ctx->message, "Reapply \"");
> -			strbuf_addstr(&ctx->message, orig_subject);
> -			strbuf_addstr(&ctx->message, "\n");
> +			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>  		} else {
> -			strbuf_addstr(&ctx->message, "Revert \"");
> -			strbuf_addstr(&ctx->message, msg.subject);
> -			strbuf_addstr(&ctx->message, "\"\n");
> +			sequencer_format_revert_header(&ctx->message, msg.subject, NULL);
>  		}
> -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>  		refer_to_commit(opts, &ctx->message, commit);

I still find it somewhat confusing we have some the code that deals with
`opts->commit_use_reference` partly in here and partly in
sequencer_format_revert_header().

Part of the confusion comes from sequencer_format_revert_header() being
called with NULL for the commit OID.

Was is not possible to incorporate Patrick's suggestion[1]?

[1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/

-- 
Cheers,
Toon

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  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
                           ` (2 more replies)
  2026-02-26 14:45       ` Phillip Wood
  1 sibling, 3 replies; 92+ messages in thread
From: Toon Claes @ 2026-02-20 17:35 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, Siddharth Asthana,
	Johannes Schindelin, Elijah Newren

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> Add a `--revert <branch>` mode to git replay that undoes the changes
> introduced by the specified commits. Like --onto and --advance, --revert
> is a standalone mode: it takes a branch argument and updates that branch
> with the newly created revert commits.
>
> At GitLab, we need this in Gitaly for reverting commits directly on bare
> repositories without requiring a working tree checkout.
>
> The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick
> and revert are just the same three-way merge with swapped arguments:
>
>   - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
>   - Revert: merge(ancestor=commit, ours=current, theirs=parent)
>
> We swap the base and pickme trees passed to merge_incore_nonrecursive()
> to reverse the diff direction.
>
> Revert commit messages follow the usual git revert conventions: prefixed
> with "Revert" (or "Reapply" when reverting a revert), and including
> "This reverts commit <hash>.". The author is set to the current user
> rather than preserving the original author, matching git revert behavior.
>
> Helped-by: Christian Couder <christian.couder@gmail.com>
> Helped-by: Patrick Steinhardt <ps@pks.im>
> Helped-by: Elijah Newren <newren@gmail.com>
> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
> Helped-by: Junio C Hamano <gitster@pobox.com>
> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> ---
>  Documentation/git-replay.adoc |  37 +++++++-
>  builtin/replay.c              |  25 ++++--
>  replay.c                      | 162 ++++++++++++++++++++++++----------
>  replay.h                      |  11 ++-
>  t/t3650-replay-basics.sh      | 107 ++++++++++++++++++++--
>  5 files changed, 277 insertions(+), 65 deletions(-)
>
> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> index 8d696ce3ab..ffdf790278 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>...

The modes `--onto`, `--advance` and `--revert` seem to be extremely
different from each other. So I'm starting to wonder whether it won't
make more sense to instead create subcommands instead of options for
these. Maybe something like:

    git replay revert --base=<branch> <revision-range>
    git replay pick --base=<branch> <revision-range>
    git replay replay --base=<branch> <revision-range>


-- 
Cheers,
Toon

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  2026-02-20 17:35       ` Toon Claes
@ 2026-02-20 20:23         ` Junio C Hamano
  2026-02-23  9:13         ` Christian Couder
  2026-03-06  5:05         ` Siddharth Asthana
  2 siblings, 0 replies; 92+ messages in thread
From: Junio C Hamano @ 2026-02-20 20:23 UTC (permalink / raw)
  To: Toon Claes
  Cc: Siddharth Asthana, git, christian.couder, ps, newren,
	phillip.wood123, phillip.wood, karthik.188, johannes.schindelin

Toon Claes <toon@iotcl.com> writes:

>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
>> index 8d696ce3ab..ffdf790278 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>...
>
> The modes `--onto`, `--advance` and `--revert` seem to be extremely
> different from each other. So I'm starting to wonder whether it won't
> make more sense to instead create subcommands instead of options for
> these. Maybe something like:
>
>     git replay revert --base=<branch> <revision-range>
>     git replay pick --base=<branch> <revision-range>
>     git replay replay --base=<branch> <revision-range>

Given the earlier discussion on confusions (mostly by those like me
who weren't closely watching the topics around this command) on what
options among --onto, --advance, etc. were incompatible, etc., that
certainly does sound like a good way to make these distinctive modes
more clearly stand out.

Thanks.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  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
  2 siblings, 1 reply; 92+ messages in thread
From: Christian Couder @ 2026-02-23  9:13 UTC (permalink / raw)
  To: Toon Claes
  Cc: Siddharth Asthana, git, ps, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, Elijah Newren

On Fri, Feb 20, 2026 at 6:35 PM Toon Claes <toon@iotcl.com> wrote:
>
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> > index 8d696ce3ab..ffdf790278 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>...
>
> The modes `--onto`, `--advance` and `--revert` seem to be extremely
> different from each other. So I'm starting to wonder whether it won't
> make more sense to instead create subcommands instead of options for
> these. Maybe something like:
>
>     git replay revert --base=<branch> <revision-range>
>     git replay pick --base=<branch> <revision-range>
>     git replay replay --base=<branch> <revision-range>

(I think you mean `git replay rebase` in the above line, no?)

I agree that we should consider this. But I think we should do it
separately in another series, after this one about --revert is merged.
We might even consider waiting until we have more experience using
`git replay --revert` to make a more informed decision. We shouldn't
wait for too long either though...

Also if we nearly always need a base, then why not:

  git replay rebase <base> <revision-range>
  git replay pick <base> <revision-range>
  git replay revert <base> <revision-range>

?

Or what was the reason for introducing --base=<branch>?

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  2026-02-23  9:13         ` Christian Couder
@ 2026-02-23 11:23           ` Toon Claes
  0 siblings, 0 replies; 92+ messages in thread
From: Toon Claes @ 2026-02-23 11:23 UTC (permalink / raw)
  To: Christian Couder
  Cc: Siddharth Asthana, git, ps, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin, Elijah Newren

Christian Couder <christian.couder@gmail.com> writes:

> On Fri, Feb 20, 2026 at 6:35 PM Toon Claes <toon@iotcl.com> wrote:
>>
>> The modes `--onto`, `--advance` and `--revert` seem to be extremely
>> different from each other. So I'm starting to wonder whether it won't
>> make more sense to instead create subcommands instead of options for
>> these. Maybe something like:
>>
>>     git replay revert --base=<branch> <revision-range>
>>     git replay pick --base=<branch> <revision-range>
>>     git replay replay --base=<branch> <revision-range>
>
> (I think you mean `git replay rebase` in the above line, no?)

I'm fine either way, but I agree saying "replay" twice looks weird.

> I agree that we should consider this. But I think we should do it
> separately in another series, after this one about --revert is merged.
> We might even consider waiting until we have more experience using
> `git replay --revert` to make a more informed decision. We shouldn't
> wait for too long either though...

I can agree with that.

> Also if we nearly always need a base, then why not:
>
>   git replay rebase <base> <revision-range>
>   git replay pick <base> <revision-range>
>   git replay revert <base> <revision-range>
>
> ?
>
> Or what was the reason for introducing --base=<branch>?

Well, the modes 'revert' and 'pick' work different from 'replay'. The
latter looks in <revision-range> to determine which refs need updating.
This can lead to multiple refs that will be updated (with option
--contained). The first two only operate on one ref and ignore whatever
refs are in <revision-range>. (I think, correct me if I'm wrong)

That's why I suggest to take it one step further:

     git replay revert --ref=<branch> <revision-range>
     git replay pick --ref=<branch> <revision-range>
     git replay rebase --onto=<branch> <revision-range>

That's why the first two use --ref instead of --onto.

As a benefit, this also enables me to address another issue I have:
git-replay(1) cannot be used on bare commit IDs. This issue was also
raised by Yee Cheng Chin[1].

With options `--onto` and `--ref` we can fix this. Because you can use
them together:

    git replay replay --ref=ref/heads/branch --onto=112233 aabbcc..ddeeff
    git replay pick --ref=ref/heads/branch --onto=112233 aabbcc..ddeeff
    git replay rebase --ref=ref/heads/branch --onto=112233 aabbcc..ddeeff

The value of --onto doesn't need to be a ref, but --ref needs. For the
'rebase' subcommand option --ref is optional, for the other two --onto
is optional. And when one of both is omitted, one defaults to the other.

What do you think?

[1]: https://lore.kernel.org/git/CAHTeOx-SMLh_idKhGczPKzZNOKy04uYXmUhL8Z79yRuNpmE4eA@mail.gmail.com/

-- 
Cheers,
Toon

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function
  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
  1 sibling, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2026-02-25 21:53 UTC (permalink / raw)
  To: Toon Claes
  Cc: Siddharth Asthana, git, christian.couder, ps, newren,
	phillip.wood123, phillip.wood, karthik.188, johannes.schindelin

Toon Claes <toon@iotcl.com> writes:

>> -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
>> -			   /*
>> -			    * We don't touch pre-existing repeated reverts, because
>> -			    * theoretically these can be nested arbitrarily deeply,
>> -			    * thus requiring excessive complexity to deal with.
>> -			    */
>> -			   !starts_with(orig_subject, "Revert \"")) {
>> -			strbuf_addstr(&ctx->message, "Reapply \"");
>> -			strbuf_addstr(&ctx->message, orig_subject);
>> -			strbuf_addstr(&ctx->message, "\n");
>> +			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>  		} else {
>> -			strbuf_addstr(&ctx->message, "Revert \"");
>> -			strbuf_addstr(&ctx->message, msg.subject);
>> -			strbuf_addstr(&ctx->message, "\"\n");
>> +			sequencer_format_revert_header(&ctx->message, msg.subject, NULL);
>>  		}
>> -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>  		refer_to_commit(opts, &ctx->message, commit);
>
> I still find it somewhat confusing we have some the code that deals with
> `opts->commit_use_reference` partly in here and partly in
> sequencer_format_revert_header().

True.  Making sure plumbing commands are unaffected by random
end-user configuration is a good thing, but I am not sure if this
command is truly a plumbing.

> Part of the confusion comes from sequencer_format_revert_header() being
> called with NULL for the commit OID.
>
> Was is not possible to incorporate Patrick's suggestion[1]?
>
> [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function
  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-26 14:27       ` Phillip Wood
  2026-03-06  5:00         ` Siddharth Asthana
  1 sibling, 1 reply; 92+ messages in thread
From: Phillip Wood @ 2026-02-26 14:27 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood, karthik.188,
	johannes.schindelin, toon

Hi Siddharth

On 18/02/2026 23:42, Siddharth Asthana wrote:
> The logic for formatting revert commit messages (handling "Revert" and
> "Reapply" cases) is currently duplicated between sequencer.c and will be
> needed by builtin/replay.c.
> 
> Extract this logic into a new sequencer_format_revert_header() function
> that can be shared. The function handles both regular reverts ("Revert
> "<subject>"") and revert-of-revert cases ("Reapply "<subject>"").
> When an oid is provided, the function appends the full commit hash and
> period; otherwise the caller should append the commit reference.
> 
> Update do_pick_commit() to use the new helper, eliminating code
> duplication while preserving the special handling for commit_use_reference.

I agree with the other comments that this ends up being a bit awkward, I think
something like the diff below which moves all of the revert message formatting
into a helper function would be a better approach. Note that I've also added a
repository argument to refer_to_commit(). You might want to do that in a
separate commit, but I think it is worth doing if we're adding more callers.
I've also just used a bool for the use_commit_reference flag, if we want to add
more flags in the future we can convert it to an unsigned int when we do that.

Thanks

Phillip


---- 8< ----
diff --git a/sequencer.c b/sequencer.c
index a3eb39bb252..30f6da6f959 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2198,21 +2198,55 @@ static int should_edit(struct replay_opts *opts) {
  	return opts->edit;
  }
  
-static void refer_to_commit(struct replay_opts *opts,
-			    struct strbuf *msgbuf, struct commit *commit)
+static void refer_to_commit(struct repository*r, struct strbuf *msgbuf,
+		const struct commit *commit, bool use_commit_reference)
  {
-	if (opts->commit_use_reference) {
+	if (use_commit_reference) {
  		struct pretty_print_context ctx = {
  			.abbrev = DEFAULT_ABBREV,
  			.date_mode.type = DATE_SHORT,
  		};
-		repo_format_commit_message(the_repository, commit,
+		repo_format_commit_message(r, commit,
  					   "%h (%s, %ad)", msgbuf, &ctx);
  	} else {
  		strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));
  	}
  }
  
+void sequencer_format_revert_message(struct repository *r, const char *subject,
+			const struct commit *commit, const struct commit *parent,
+			bool use_commit_reference, struct strbuf *message)
+{
+	const char *orig_subject;
+
+	if (use_commit_reference) {
+			strbuf_commented_addf(message, comment_line_str,
+				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
+		} else if (skip_prefix(subject, "Revert \"", &orig_subject) &&
+			   /*
+			    * We don't touch pre-existing repeated reverts, because
+			    * theoretically these can be nested arbitrarily deeply,
+			    * thus requiring excessive complexity to deal with.
+			    */
+			   !starts_with(orig_subject, "Revert \"")) {
+			strbuf_addstr(message, "Reapply \"");
+			strbuf_addstr(message, orig_subject);
+			strbuf_addstr(message, "\n");
+		} else {
+			strbuf_addstr(message, "Revert \"");
+			strbuf_addstr(message, subject);
+			strbuf_addstr(message, "\"\n");
+		}
+		strbuf_addstr(message, "\nThis reverts commit ");
+		refer_to_commit(r, message, commit, use_commit_reference);
+
+		if (commit->parents && commit->parents->next) {
+			strbuf_addstr(message, ", reversing\nchanges made to ");
+			refer_to_commit(r, message, parent, use_commit_reference);
+		}
+		strbuf_addstr(message, ".\n");
+}
+
  static const char *sequencer_reflog_action(struct replay_opts *opts)
  {
  	if (!opts->reflog_action) {
@@ -2356,38 +2390,13 @@ static int do_pick_commit(struct repository *r,
  	 */
  
  	if (command == TODO_REVERT) {
-		const char *orig_subject;
-
  		base = commit;
  		base_label = msg.label;
  		next = parent;
  		next_label = msg.parent_label;
-		if (opts->commit_use_reference) {
-			strbuf_commented_addf(&ctx->message, comment_line_str,
-				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
-		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
-			   /*
-			    * We don't touch pre-existing repeated reverts, because
-			    * theoretically these can be nested arbitrarily deeply,
-			    * thus requiring excessive complexity to deal with.
-			    */
-			   !starts_with(orig_subject, "Revert \"")) {
-			strbuf_addstr(&ctx->message, "Reapply \"");
-			strbuf_addstr(&ctx->message, orig_subject);
-			strbuf_addstr(&ctx->message, "\n");
-		} else {
-			strbuf_addstr(&ctx->message, "Revert \"");
-			strbuf_addstr(&ctx->message, msg.subject);
-			strbuf_addstr(&ctx->message, "\"\n");
-		}
-		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
-		refer_to_commit(opts, &ctx->message, commit);
-
-		if (commit->parents && commit->parents->next) {
-			strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
-			refer_to_commit(opts, &ctx->message, parent);
-		}
-		strbuf_addstr(&ctx->message, ".\n");
+		sequencer_format_revert_message(r,msg.subject, commit, parent,
+						opts->commit_use_reference,
+						&ctx->message);
  	} else {
  		const char *p;
  
diff --git a/sequencer.h b/sequencer.h
index 719684c8a9f..a61ec6d81d4 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -271,4 +271,8 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)
   */
  int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
  
+void sequencer_format_revert_message(struct repository *r, const char *subject,
+				     const struct commit *commit, const struct commit *parent,
+				     bool use_commit_reference, struct strbuf *message);
+
  #endif /* SEQUENCER_H */


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  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-26 14:45       ` Phillip Wood
  2026-03-06  5:28         ` Siddharth Asthana
  1 sibling, 1 reply; 92+ messages in thread
From: Phillip Wood @ 2026-02-26 14:45 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood, karthik.188,
	johannes.schindelin, toon

Hi Siddharth

On 18/02/2026 23:42, Siddharth Asthana wrote:
> @@ -42,6 +42,25 @@ 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 reverted commits; must be a
> +	branch name.
> ++
> +When `--revert` is specified, the commits in the revision range are reverted
> +(their changes are undone) and the reverted commits are created on top of
> +<branch>. The <branch> is then updated to point at the new commits. This is
> +the same as running `git revert <revision-range>` but does not update the
> +working tree.
> ++
> +The commit messages follow `git revert` conventions: they are prefixed with
> +"Revert" and include "This reverts commit <hash>." When reverting a commit
> +whose message starts with "Revert", the new message uses "Reapply" instead.
> +Unlike cherry-pick which preserves the original author, revert commits use
> +the current user as the author, matching the behavior of `git revert`.
> ++
> +This option is mutually exclusive with `--onto` and `--advance`. It is also
> +incompatible with `--contained` (which is a modifier for `--onto` only).

We seem to have lost

     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.

from V2 which is a shame.

I do think we should seriously consider reverting commits in the reverse 
order that they were created (i.e. do not set '--reverse' when setting 
up the rev-list options) to reduce the likely-hood of conflicts when 
reverting a sequence of commits.

> @@ -152,6 +172,15 @@ 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 on a branch:
> +
> +------------
> +$ git replay --revert main main~2..main

It might be more realistic to revert some commits from a different 
branch, for example

   git replay --revert main topic~2..topic

> +static void set_up_branch_mode(struct repository *repo,
> +			       char **branch_name,
> +			       const char *option_name,
> +			       struct ref_info *rinfo,
> +			       struct commit **onto)
> [...]
> +	if (rinfo->positive_refexprs > 1)
> +		die(_("cannot %s target with multiple sources because ordering would be ill-defined"),
> +		    option_name + 2); /* skip "--" prefix */

This is a bit of a nasty hack as it stuffs an English word into the 
middle of a translated sentence. Using the option name as below might be 
nicer

     die(_("'%s' cannot be used with multiple revision ranges because 
the ordering would be ill defined", option_name);
> @@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  [...]
> -	/* Drop commits that become empty */
> -	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
> +	/* Drop commits that become empty (only for picks) */

Why? What's the advantage in creating empty revert commits?

> +	if (mode == REPLAY_MODE_PICK &&
> +	    oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&

Thanks

Phillip

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function
  2026-02-20 17:01       ` Toon Claes
  2026-02-25 21:53         ` Junio C Hamano
@ 2026-03-06  4:31         ` Siddharth Asthana
  1 sibling, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-06  4:31 UTC (permalink / raw)
  To: Toon Claes, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin



On 20/02/26 22:31, Toon Claes wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
> 
>> The logic for formatting revert commit messages (handling "Revert" and
>> "Reapply" cases) is currently duplicated between sequencer.c and will be
>> needed by builtin/replay.c.
>>
>> Extract this logic into a new sequencer_format_revert_header() function
>> that can be shared. The function handles both regular reverts ("Revert
>> "<subject>"") and revert-of-revert cases ("Reapply "<subject>"").
>> When an oid is provided, the function appends the full commit hash and
>> period; otherwise the caller should append the commit reference.
>>
>> Update do_pick_commit() to use the new helper, eliminating code
>> duplication while preserving the special handling for commit_use_reference.
>>
>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>> ---
>>   sequencer.c | 47 +++++++++++++++++++++++++++++++----------------
>>   sequencer.h | 11 +++++++++++
>>   2 files changed, 42 insertions(+), 16 deletions(-)
>>
>> diff --git a/sequencer.c b/sequencer.c
>> index 1f492f8460..b32347c853 100644
>> --- a/sequencer.c
>> +++ b/sequencer.c
>> @@ -2356,8 +2356,6 @@ static int do_pick_commit(struct repository *r,
>>   	 */
>>   
>>   	if (command == TODO_REVERT) {
>> -		const char *orig_subject;
>> -
>>   		base = commit;
>>   		base_label = msg.label;
>>   		next = parent;
>> @@ -2365,22 +2363,10 @@ static int do_pick_commit(struct repository *r,
>>   		if (opts->commit_use_reference) {
>>   			strbuf_commented_addf(&ctx->message, comment_line_str,
>>   				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
>> -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
>> -			   /*
>> -			    * We don't touch pre-existing repeated reverts, because
>> -			    * theoretically these can be nested arbitrarily deeply,
>> -			    * thus requiring excessive complexity to deal with.
>> -			    */
>> -			   !starts_with(orig_subject, "Revert \"")) {
>> -			strbuf_addstr(&ctx->message, "Reapply \"");
>> -			strbuf_addstr(&ctx->message, orig_subject);
>> -			strbuf_addstr(&ctx->message, "\n");
>> +			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>   		} else {
>> -			strbuf_addstr(&ctx->message, "Revert \"");
>> -			strbuf_addstr(&ctx->message, msg.subject);
>> -			strbuf_addstr(&ctx->message, "\"\n");
>> +			sequencer_format_revert_header(&ctx->message, msg.subject, NULL);
>>   		}
>> -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>   		refer_to_commit(opts, &ctx->message, commit);
> 
> I still find it somewhat confusing we have some the code that deals with
> `opts->commit_use_reference` partly in here and partly in
> sequencer_format_revert_header().
> 
> Part of the confusion comes from sequencer_format_revert_header() being
> called with NULL for the commit OID.
> 
> Was is not possible to incorporate Patrick's suggestion[1]?

You're right, the split is awkward. I tried to keep 
sequencer_format_revert_header() minimal so it didn't pull in 
replay_opts or refer_to_commit(), but the NULL oid path is confusing.

Phillip posted a cleaner approach in his reply to this patch -- he moves 
everything (title, body, refer_to_commit, merge-parent handling) into 
one sequencer_format_revert_message() with a bool use_commit_reference. 
That eliminates the NULL oid entirely and addresses Patrick's suggestion 
at the same time. I will go with that for v4.

> 
> [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/
> 


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function
  2026-02-25 21:53         ` Junio C Hamano
@ 2026-03-06  4:55           ` Siddharth Asthana
  0 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-06  4:55 UTC (permalink / raw)
  To: Junio C Hamano, Toon Claes
  Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood,
	karthik.188, johannes.schindelin



On 26/02/26 03:23, Junio C Hamano wrote:
> Toon Claes <toon@iotcl.com> writes:
> 
>>> -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
>>> -			   /*
>>> -			    * We don't touch pre-existing repeated reverts, because
>>> -			    * theoretically these can be nested arbitrarily deeply,
>>> -			    * thus requiring excessive complexity to deal with.
>>> -			    */
>>> -			   !starts_with(orig_subject, "Revert \"")) {
>>> -			strbuf_addstr(&ctx->message, "Reapply \"");
>>> -			strbuf_addstr(&ctx->message, orig_subject);
>>> -			strbuf_addstr(&ctx->message, "\n");
>>> +			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>>   		} else {
>>> -			strbuf_addstr(&ctx->message, "Revert \"");
>>> -			strbuf_addstr(&ctx->message, msg.subject);
>>> -			strbuf_addstr(&ctx->message, "\"\n");
>>> +			sequencer_format_revert_header(&ctx->message, msg.subject, NULL);
>>>   		}
>>> -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>>>   		refer_to_commit(opts, &ctx->message, commit);
>>
>> I still find it somewhat confusing we have some the code that deals with
>> `opts->commit_use_reference` partly in here and partly in
>> sequencer_format_revert_header().
> 
> True.  Making sure plumbing commands are unaffected by random
> end-user configuration is a good thing, but I am not sure if this
> command is truly a plumbing.


With Phillip's sequencer_format_revert_message() approach, replay just 
passes use_commit_reference=false and gets the full OID path. The split 
logic goes away, so this concern is resolved regardless of how we 
classify replay.


> 
>> Part of the confusion comes from sequencer_format_revert_header() being
>> called with NULL for the commit OID.
>>
>> Was is not possible to incorporate Patrick's suggestion[1]?
>>
>> [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function
  2026-02-26 14:27       ` Phillip Wood
@ 2026-03-06  5:00         ` Siddharth Asthana
  0 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-06  5:00 UTC (permalink / raw)
  To: phillip.wood, git
  Cc: christian.couder, ps, newren, gitster, karthik.188,
	johannes.schindelin, toon



On 26/02/26 19:57, Phillip Wood wrote:
> Hi Siddharth
> 
> On 18/02/2026 23:42, Siddharth Asthana wrote:
>> The logic for formatting revert commit messages (handling "Revert" and
>> "Reapply" cases) is currently duplicated between sequencer.c and will be
>> needed by builtin/replay.c.
>>
>> Extract this logic into a new sequencer_format_revert_header() function
>> that can be shared. The function handles both regular reverts ("Revert
>> "<subject>"") and revert-of-revert cases ("Reapply "<subject>"").
>> When an oid is provided, the function appends the full commit hash and
>> period; otherwise the caller should append the commit reference.
>>
>> Update do_pick_commit() to use the new helper, eliminating code
>> duplication while preserving the special handling for 
>> commit_use_reference.
> 
> I agree with the other comments that this ends up being a bit awkward, I 
> think
> something like the diff below which moves all of the revert message 
> formatting
> into a helper function would be a better approach. Note that I've also 
> added a
> repository argument to refer_to_commit(). You might want to do that in a
> separate commit, but I think it is worth doing if we're adding more 
> callers.
> I've also just used a bool for the use_commit_reference flag, if we want 
> to add
> more flags in the future we can convert it to an unsigned int when we do 
> that.

Thanks, this is much cleaner. Moving refer_to_commit() and the 
merge-parent handling into the same function gets rid of the awkward 
NULL oid path that Toon and Junio pointed out.

I will split the refer_to_commit() signature change (adding struct 
repository *r) into a preparatory commit as you suggested, then have the 
second commit introduce the full sequencer_format_revert_message() helper.

For replay, I will call it with use_commit_reference=false -- that gives 
the full OID through refer_to_commit() directly, no special causing needed.


> 
> Thanks
> 
> Phillip
> 
> 
> ---- 8< ----
> diff --git a/sequencer.c b/sequencer.c
> index a3eb39bb252..30f6da6f959 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -2198,21 +2198,55 @@ static int should_edit(struct replay_opts *opts) {
>       return opts->edit;
>   }
> 
> -static void refer_to_commit(struct replay_opts *opts,
> -                struct strbuf *msgbuf, struct commit *commit)
> +static void refer_to_commit(struct repository*r, struct strbuf *msgbuf,
> +        const struct commit *commit, bool use_commit_reference)
>   {
> -    if (opts->commit_use_reference) {
> +    if (use_commit_reference) {
>           struct pretty_print_context ctx = {
>               .abbrev = DEFAULT_ABBREV,
>               .date_mode.type = DATE_SHORT,
>           };
> -        repo_format_commit_message(the_repository, commit,
> +        repo_format_commit_message(r, commit,
>                          "%h (%s, %ad)", msgbuf, &ctx);
>       } else {
>           strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));
>       }
>   }
> 
> +void sequencer_format_revert_message(struct repository *r, const char 
> *subject,
> +            const struct commit *commit, const struct commit *parent,
> +            bool use_commit_reference, struct strbuf *message)
> +{
> +    const char *orig_subject;
> +
> +    if (use_commit_reference) {
> +            strbuf_commented_addf(message, comment_line_str,
> +                "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
> +        } else if (skip_prefix(subject, "Revert \"", &orig_subject) &&
> +               /*
> +                * We don't touch pre-existing repeated reverts, because
> +                * theoretically these can be nested arbitrarily deeply,
> +                * thus requiring excessive complexity to deal with.
> +                */
> +               !starts_with(orig_subject, "Revert \"")) {
> +            strbuf_addstr(message, "Reapply \"");
> +            strbuf_addstr(message, orig_subject);
> +            strbuf_addstr(message, "\n");
> +        } else {
> +            strbuf_addstr(message, "Revert \"");
> +            strbuf_addstr(message, subject);
> +            strbuf_addstr(message, "\"\n");
> +        }
> +        strbuf_addstr(message, "\nThis reverts commit ");
> +        refer_to_commit(r, message, commit, use_commit_reference);
> +
> +        if (commit->parents && commit->parents->next) {
> +            strbuf_addstr(message, ", reversing\nchanges made to ");
> +            refer_to_commit(r, message, parent, use_commit_reference);
> +        }
> +        strbuf_addstr(message, ".\n");
> +}
> +
>   static const char *sequencer_reflog_action(struct replay_opts *opts)
>   {
>       if (!opts->reflog_action) {
> @@ -2356,38 +2390,13 @@ static int do_pick_commit(struct repository *r,
>        */
> 
>       if (command == TODO_REVERT) {
> -        const char *orig_subject;
> -
>           base = commit;
>           base_label = msg.label;
>           next = parent;
>           next_label = msg.parent_label;
> -        if (opts->commit_use_reference) {
> -            strbuf_commented_addf(&ctx->message, comment_line_str,
> -                "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
> -        } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
> -               /*
> -                * We don't touch pre-existing repeated reverts, because
> -                * theoretically these can be nested arbitrarily deeply,
> -                * thus requiring excessive complexity to deal with.
> -                */
> -               !starts_with(orig_subject, "Revert \"")) {
> -            strbuf_addstr(&ctx->message, "Reapply \"");
> -            strbuf_addstr(&ctx->message, orig_subject);
> -            strbuf_addstr(&ctx->message, "\n");
> -        } else {
> -            strbuf_addstr(&ctx->message, "Revert \"");
> -            strbuf_addstr(&ctx->message, msg.subject);
> -            strbuf_addstr(&ctx->message, "\"\n");
> -        }
> -        strbuf_addstr(&ctx->message, "\nThis reverts commit ");
> -        refer_to_commit(opts, &ctx->message, commit);
> -
> -        if (commit->parents && commit->parents->next) {
> -            strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
> -            refer_to_commit(opts, &ctx->message, parent);
> -        }
> -        strbuf_addstr(&ctx->message, ".\n");
> +        sequencer_format_revert_message(r,msg.subject, commit, parent,
> +                        opts->commit_use_reference,
> +                        &ctx->message);
>       } else {
>           const char *p;
> 
> diff --git a/sequencer.h b/sequencer.h
> index 719684c8a9f..a61ec6d81d4 100644
> --- a/sequencer.h
> +++ b/sequencer.h
> @@ -271,4 +271,8 @@ int sequencer_determine_whence(struct repository *r, 
> enum commit_whence *whence)
>    */
>   int sequencer_get_update_refs_state(const char *wt_dir, struct 
> string_list *refs);
> 
> +void sequencer_format_revert_message(struct repository *r, const char 
> *subject,
> +                     const struct commit *commit, const struct commit 
> *parent,
> +                     bool use_commit_reference, struct strbuf *message);
> +
>   #endif /* SEQUENCER_H */
> 


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  2026-02-20 17:35       ` Toon Claes
  2026-02-20 20:23         ` Junio C Hamano
  2026-02-23  9:13         ` Christian Couder
@ 2026-03-06  5:05         ` Siddharth Asthana
  2 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-06  5:05 UTC (permalink / raw)
  To: Toon Claes, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	phillip.wood, karthik.188, johannes.schindelin



On 20/02/26 23:05, Toon Claes wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
> 
>> Add a `--revert <branch>` mode to git replay that undoes the changes
>> introduced by the specified commits. Like --onto and --advance, --revert
>> is a standalone mode: it takes a branch argument and updates that branch
>> with the newly created revert commits.
>>
>> At GitLab, we need this in Gitaly for reverting commits directly on bare
>> repositories without requiring a working tree checkout.
>>
>> The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick
>> and revert are just the same three-way merge with swapped arguments:
>>
>>    - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
>>    - Revert: merge(ancestor=commit, ours=current, theirs=parent)
>>
>> We swap the base and pickme trees passed to merge_incore_nonrecursive()
>> to reverse the diff direction.
>>
>> Revert commit messages follow the usual git revert conventions: prefixed
>> with "Revert" (or "Reapply" when reverting a revert), and including
>> "This reverts commit <hash>.". The author is set to the current user
>> rather than preserving the original author, matching git revert behavior.
>>
>> Helped-by: Christian Couder <christian.couder@gmail.com>
>> Helped-by: Patrick Steinhardt <ps@pks.im>
>> Helped-by: Elijah Newren <newren@gmail.com>
>> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
>> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
>> Helped-by: Junio C Hamano <gitster@pobox.com>
>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>> ---
>>   Documentation/git-replay.adoc |  37 +++++++-
>>   builtin/replay.c              |  25 ++++--
>>   replay.c                      | 162 ++++++++++++++++++++++++----------
>>   replay.h                      |  11 ++-
>>   t/t3650-replay-basics.sh      | 107 ++++++++++++++++++++--
>>   5 files changed, 277 insertions(+), 65 deletions(-)
>>
>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
>> index 8d696ce3ab..ffdf790278 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>...
> 
> The modes `--onto`, `--advance` and `--revert` seem to be extremely
> different from each other. So I'm starting to wonder whether it won't
> make more sense to instead create subcommands instead of options for
> these. Maybe something like:


Agree the interface could be cleaner as subcommands. I think Christian's 
suggestion to do this separate series after --revert lands make sense -- 
we would get real-world usage feedback first, and it avoids scope creep 
here.


> 
>      git replay revert --base=<branch> <revision-range>
>      git replay pick --base=<branch> <revision-range>
>      git replay replay --base=<branch> <revision-range>
> 
> 


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  2026-02-26 14:45       ` Phillip Wood
@ 2026-03-06  5:28         ` Siddharth Asthana
  2026-03-06 15:52           ` Phillip Wood
  0 siblings, 1 reply; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-06  5:28 UTC (permalink / raw)
  To: phillip.wood, git
  Cc: christian.couder, ps, newren, gitster, karthik.188,
	johannes.schindelin, toon



On 26/02/26 20:15, Phillip Wood wrote:
> Hi Siddharth
> 
> On 18/02/2026 23:42, Siddharth Asthana wrote:
>> @@ -42,6 +42,25 @@ 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 reverted commits; must be a
>> +    branch name.
>> ++
>> +When `--revert` is specified, the commits in the revision range are 
>> reverted
>> +(their changes are undone) and the reverted commits are created on 
>> top of
>> +<branch>. The <branch> is then updated to point at the new commits. 
>> This is
>> +the same as running `git revert <revision-range>` but does not update 
>> the
>> +working tree.
>> ++
>> +The commit messages follow `git revert` conventions: they are 
>> prefixed with
>> +"Revert" and include "This reverts commit <hash>." When reverting a 
>> commit
>> +whose message starts with "Revert", the new message uses "Reapply" 
>> instead.
>> +Unlike cherry-pick which preserves the original author, revert 
>> commits use
>> +the current user as the author, matching the behavior of `git revert`.
>> ++
>> +This option is mutually exclusive with `--onto` and `--advance`. It 
>> is also
>> +incompatible with `--contained` (which is a modifier for `--onto` only).
> 
> We seem to have lost
> 
>      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.
> 
> from V2 which is a shame.


Yeah, I dropped it during the v3 cleanup when I was trimming the example 
text. will  add it back.

> 
> I do think we should seriously consider reverting commits in the reverse 
> order that they were created (i.e. do not set '--reverse' when setting 
> up the rev-list options) to reduce the likely-hood of conflicts when 
> reverting a sequence of commits.


Good catch. sequencer.c does exactly this in prepare_revs() -- it only 
sets reverse for REPLAY_PICK, not REPLAY_REVERT, so git revert processes 
newest-first.

The complication in replay is that pick_regular_commit() chains commits 
through mapped_commit(base, onto). With oldest-first, the parent is 
always already in replayed_commits so the chain works. With 
newest-first, the parent hasn't been processed yet and mapped_commit() 
falls back to onto -- so each revert be independently based on the 
original branch tip instead of chaining.

The fix is straightforward: for revert mode, pass last_commit instead of 
onto as the fallback in the main loop:

	pick_regular_commit(repo, commit, replayed_commits,
                         mode == REPLAY_MODE_REVERT ? last_commit : onto,
                         &merge_opt, &result, mode);

That way each revert builds on the previous one regardless of walk 
order. I will do this in v4 together with skipping the reverse=1 
override for revert mode.

> 
>> @@ -152,6 +172,15 @@ 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 on a branch:
>> +
>> +------------
>> +$ git replay --revert main main~2..main
> 
> It might be more realistic to revert some commits from a different 
> branch, for example
> 
>    git replay --revert main topic~2..topic


Makes sense. v2 had `git replay --revert main feature~2..feature` for 
this reason but I simplified it in v3. I will go back to something like 
your example:

	git replay --revert main topic2..topic


> 
>> +static void set_up_branch_mode(struct repository *repo,
>> +                   char **branch_name,
>> +                   const char *option_name,
>> +                   struct ref_info *rinfo,
>> +                   struct commit **onto)
>> [...]
>> +    if (rinfo->positive_refexprs > 1)
>> +        die(_("cannot %s target with multiple sources because 
>> ordering would be ill-defined"),
>> +            option_name + 2); /* skip "--" prefix */
> 
> This is a bit of a nasty hack as it stuffs an English word into the 
> middle of a translated sentence. Using the option name as below might be 
> nicer


Agreed, will use your suggested form:

	die(_("'%s' cannot be used with multiple revision ranges "
           "because the ordering would be ill-defined"), option_name);


> 
>      die(_("'%s' cannot be used with multiple revision ranges because 
> the ordering would be ill defined", option_name);
>> @@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct 
>> repository *repo,
>>  [...]
>> -    /* Drop commits that become empty */
>> -    if (oideq(&replayed_base_tree->object.oid, &result->tree- 
>> >object.oid) &&
>> +    /* Drop commits that become empty (only for picks) */
> 
> Why? What's the advantage in creating empty revert commits?


Consistency with git revert, which doesn't silently drop empty reverts 
either -- it stops and asks the user to deal with it. Since replay is 
non-interactive and can't prompt, I kept them rather than silently 
dropping, to avoid hiding that something unexpected happened.

That being said, I don't feel strong about it. If you think dropping is 
the better default for replay, I am  happy to change it. Or we could 
error out (exit code 1) like we do for conflicts?


Thanks,
Siddharth


> 
>> +    if (mode == REPLAY_MODE_PICK &&
>> +        oideq(&replayed_base_tree->object.oid, &result->tree- 
>> >object.oid) &&
> 
> Thanks
> 
> Phillip


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  2026-03-06  5:28         ` Siddharth Asthana
@ 2026-03-06 15:52           ` Phillip Wood
  2026-03-06 16:20             ` Siddharth Asthana
  0 siblings, 1 reply; 92+ messages in thread
From: Phillip Wood @ 2026-03-06 15:52 UTC (permalink / raw)
  To: Siddharth Asthana, phillip.wood, git
  Cc: christian.couder, ps, newren, gitster, karthik.188,
	johannes.schindelin, toon

On 06/03/2026 05:28, Siddharth Asthana wrote:
> On 26/02/26 20:15, Phillip Wood wrote:
>> On 18/02/2026 23:42, Siddharth Asthana wrote:
> 
>> I do think we should seriously consider reverting commits in the 
>> reverse order that they were created (i.e. do not set '--reverse' when 
>> setting up the rev-list options) to reduce the likely-hood of 
>> conflicts when reverting a sequence of commits.
> 
> Good catch. sequencer.c does exactly this in prepare_revs() -- it only 
> sets reverse for REPLAY_PICK, not REPLAY_REVERT, so git revert processes 
> newest-first.
> 
> The complication in replay is that pick_regular_commit() chains commits 
> through mapped_commit(base, onto). With oldest-first, the parent is 
> always already in replayed_commits so the chain works. With newest- 
> first, the parent hasn't been processed yet and mapped_commit() falls 
> back to onto -- so each revert be independently based on the original 
> branch tip instead of chaining.
> 
> The fix is straightforward: for revert mode, pass last_commit instead of 
> onto as the fallback in the main loop:
> 
>      pick_regular_commit(repo, commit, replayed_commits,
>                          mode == REPLAY_MODE_REVERT ? last_commit : onto,
>                          &merge_opt, &result, mode);

As we only allow a single range of commits with --revert that should work.

> That way each revert builds on the previous one regardless of walk 
> order. I will do this in v4 together with skipping the reverse=1 
> override for revert mode.

Great

>>> @@ -226,25 +269,46 @@ static struct commit 
>>> *pick_regular_commit(struct repository *repo,
>>>  [...]
>>> -    /* Drop commits that become empty */
>>> -    if (oideq(&replayed_base_tree->object.oid, &result->tree- 
>>> >object.oid) &&
>>> +    /* Drop commits that become empty (only for picks) */
>>
>> Why? What's the advantage in creating empty revert commits?
> 
> 
> Consistency with git revert, which doesn't silently drop empty reverts 
> either -- it stops and asks the user to deal with it. 

So does "git cherry-pick" unless you pass --empty=drop or --empty=keep 
(I was surprised that "git revert" does not support --empty, that seems 
to be an oversight)

> Since replay is 
> non-interactive and can't prompt, I kept them rather than silently 
> dropping, to avoid hiding that something unexpected happened.

I don't think creating empty commits for revert is very helpful, when 
cherry-picking one could argue that the user may want to preserve the 
commit message (though I think that's unlikely in practice which is why 
we drop commits that become empty) but that does not apply to revert.

> That being said, I don't feel strong about it. If you think dropping is 
> the better default for replay, I am  happy to change it. Or we could 
> error out (exit code 1) like we do for conflicts?

We don't error out when cherry-picking and so we shouldn't do that when 
reverting.

Thanks

Phillip

> 
> Thanks,
> Siddharth
> 
> 
>>
>>> +    if (mode == REPLAY_MODE_PICK &&
>>> +        oideq(&replayed_base_tree->object.oid, &result->tree- 
>>> >object.oid) &&
>>
>> Thanks
>>
>> Phillip
> 


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes
  2026-03-06 15:52           ` Phillip Wood
@ 2026-03-06 16:20             ` Siddharth Asthana
  0 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-06 16:20 UTC (permalink / raw)
  To: phillip.wood, git
  Cc: christian.couder, ps, newren, gitster, karthik.188,
	johannes.schindelin, toon



On 06/03/26 21:22, Phillip Wood wrote:
> On 06/03/2026 05:28, Siddharth Asthana wrote:
>> On 26/02/26 20:15, Phillip Wood wrote:
>>> On 18/02/2026 23:42, Siddharth Asthana wrote:
>>
>>> I do think we should seriously consider reverting commits in the 
>>> reverse order that they were created (i.e. do not set '--reverse' 
>>> when setting up the rev-list options) to reduce the likely-hood of 
>>> conflicts when reverting a sequence of commits.
>>
>> Good catch. sequencer.c does exactly this in prepare_revs() -- it only 
>> sets reverse for REPLAY_PICK, not REPLAY_REVERT, so git revert 
>> processes newest-first.
>>
>> The complication in replay is that pick_regular_commit() chains 
>> commits through mapped_commit(base, onto). With oldest-first, the 
>> parent is always already in replayed_commits so the chain works. With 
>> newest- first, the parent hasn't been processed yet and 
>> mapped_commit() falls back to onto -- so each revert be independently 
>> based on the original branch tip instead of chaining.
>>
>> The fix is straightforward: for revert mode, pass last_commit instead 
>> of onto as the fallback in the main loop:
>>
>>      pick_regular_commit(repo, commit, replayed_commits,
>>                          mode == REPLAY_MODE_REVERT ? last_commit : onto,
>>                          &merge_opt, &result, mode);
> 
> As we only allow a single range of commits with --revert that should work.
> 
>> That way each revert builds on the previous one regardless of walk 
>> order. I will do this in v4 together with skipping the reverse=1 
>> override for revert mode.
> 
> Great
> 
>>>> @@ -226,25 +269,46 @@ static struct commit 
>>>> *pick_regular_commit(struct repository *repo,
>>>>  [...]
>>>> -    /* Drop commits that become empty */
>>>> -    if (oideq(&replayed_base_tree->object.oid, &result->tree- 
>>>> >object.oid) &&
>>>> +    /* Drop commits that become empty (only for picks) */
>>>
>>> Why? What's the advantage in creating empty revert commits?
>>
>>
>> Consistency with git revert, which doesn't silently drop empty reverts 
>> either -- it stops and asks the user to deal with it. 
> 
> So does "git cherry-pick" unless you pass --empty=drop or --empty=keep 
> (I was surprised that "git revert" does not support --empty, that seems 
> to be an oversight)
> 
>> Since replay is non-interactive and can't prompt, I kept them rather 
>> than silently dropping, to avoid hiding that something unexpected 
>> happened.
> 
> I don't think creating empty commits for revert is very helpful, when 
> cherry-picking one could argue that the user may want to preserve the 
> commit message (though I think that's unlikely in practice which is why 
> we drop commits that become empty) but that does not apply to revert.
> 
>> That being said, I don't feel strong about it. If you think dropping 
>> is the better default for replay, I am  happy to change it. Or we 
>> could error out (exit code 1) like we do for conflicts?
> 
> We don't error out when cherry-picking and so we shouldn't do that when 
> reverting.


Right, will drop empties for revert too then.


> 
> Thanks
> 
> Phillip
> 
>>
>> Thanks,
>> Siddharth
>>
>>
>>>
>>>> +    if (mode == REPLAY_MODE_PICK &&
>>>> +        oideq(&replayed_base_tree->object.oid, &result->tree- 
>>>> >object.oid) &&
>>>
>>> Thanks
>>>
>>> Phillip 


^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v4 0/2] replay: add --revert mode to reverse commit changes
  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-18 23:42     ` [PATCH v3 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
@ 2026-03-13  5:40     ` Siddharth Asthana
  2026-03-13  5:40       ` [PATCH v4 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
                         ` (4 more replies)
  2 siblings, 5 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-13  5:40 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana

Hi,

git replay currently supports cherry-picking (--advance) and rebasing
(--onto), but not reverting. We need this at GitLab for Gitaly to
reverse commits directly on bare repositories without a checkout.

The approach is the same as sequencer.c -- cherry-pick and revert are
just the same three-way merge with swapped arguments. We swap the base
and pickme trees passed to merge_incore_nonrecursive() to reverse the
diff direction.

Patch 1 extracts the full revert message formatting logic into a new
sequencer_format_revert_message() function, following Phillip's
suggestion to move everything into one shared function rather than
just the header. refer_to_commit() is updated to take a struct
repository and a bool instead of replay_opts so it works outside the
sequencer.

Patch 2 adds --revert <branch> as a standalone mode. Reverts are
processed newest-first (matching git revert) to reduce conflicts by
peeling off changes from the top.

The series is based on top of d181b9354c (The 13th batch, 2026-03-07).

CI: https://gitlab.com/gitlab-org/git/-/pipelines/2329880894
The msvc-meson / Chocolatey failures are pre-existing infrastructure
issues unrelated to this series.

Changes in v4:
- Replaced sequencer_format_revert_header() with a more complete
  sequencer_format_revert_message() that handles everything: subject
  prefix, commit reference via refer_to_commit(), and merge-parent
  references -- per Phillip
- Updated refer_to_commit() signature to take (struct repository *r,
  bool use_commit_reference) instead of (struct replay_opts *opts)
- Reverts are now newest-first (revs.reverse = 0 for --revert),
  chaining on last_commit rather than the parent mapping
- Changed doc example to cross-branch scenario and restored the
  merge-tree NOTE
- Updated error message format to "'--revert' cannot be used with
  multiple revision ranges..." (and same for --advance)
- Empty revert commits are now dropped, consistent with cherry-pick
- Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/
- Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/
- Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/

Thanks,
Siddharth

---
Siddharth Asthana (2):
  sequencer: extract revert message formatting into shared function
  replay: add --revert mode to reverse commit changes

 Documentation/git-replay.adoc |  43 ++++++++-
 builtin/replay.c              |  46 ++++++----
 replay.c                      | 165 ++++++++++++++++++++++++----------
 replay.h                      |  11 ++-
 sequencer.c                   |  78 +++++++++-------
 sequencer.h                   |  14 +++
 t/t3650-replay-basics.sh      | 114 +++++++++++++++++++++--
 7 files changed, 364 insertions(+), 107 deletions(-)

Range-diff versus v3:

1:  9d686bcdfe ! 1:  bdc710b265 sequencer: extract revert message formatting into shared function
    @@ Commit message
         sequencer: extract revert message formatting into shared function
     
         The logic for formatting revert commit messages (handling "Revert" and
    -    "Reapply" cases) is currently duplicated between sequencer.c and will be
    -    needed by builtin/replay.c.
    +    "Reapply" cases, appending "This reverts commit <ref>.", and handling
    +    merge-parent references) currently lives inline in do_pick_commit().
    +    The upcoming replay --revert mode needs to reuse this logic.
     
    -    Extract this logic into a new sequencer_format_revert_header() function
    -    that can be shared. The function handles both regular reverts ("Revert
    -    "<subject>"") and revert-of-revert cases ("Reapply "<subject>"").
    -    When an oid is provided, the function appends the full commit hash and
    -    period; otherwise the caller should append the commit reference.
    +    Extract all of this into a new sequencer_format_revert_message()
    +    function. The function takes a repository, the subject line, commit,
    +    parent, a use_commit_reference flag, and the output strbuf. It handles
    +    both regular reverts ("Revert "<subject>"") and revert-of-revert cases
    +    ("Reapply "<subject>""), and uses refer_to_commit() internally to
    +    format the commit reference.
     
    -    Update do_pick_commit() to use the new helper, eliminating code
    -    duplication while preserving the special handling for commit_use_reference.
    +    Update refer_to_commit() to take a struct repository parameter instead
    +    of relying on the_repository, and a bool instead of reading from
    +    replay_opts directly. This makes it usable from the new shared function
    +    without pulling in sequencer-specific state.
     
         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## sequencer.c ##
    +@@ sequencer.c: static int should_edit(struct replay_opts *opts) {
    + 	return opts->edit;
    + }
    + 
    +-static void refer_to_commit(struct replay_opts *opts,
    +-			    struct strbuf *msgbuf, struct commit *commit)
    ++static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,
    ++			    const struct commit *commit,
    ++			    bool use_commit_reference)
    + {
    +-	if (opts->commit_use_reference) {
    ++	if (use_commit_reference) {
    + 		struct pretty_print_context ctx = {
    + 			.abbrev = DEFAULT_ABBREV,
    + 			.date_mode.type = DATE_SHORT,
    + 		};
    +-		repo_format_commit_message(the_repository, commit,
    ++		repo_format_commit_message(r, commit,
    + 					   "%h (%s, %ad)", msgbuf, &ctx);
    + 	} else {
    + 		strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));
     @@ sequencer.c: static int do_pick_commit(struct repository *r,
      	 */
      
    @@ sequencer.c: static int do_pick_commit(struct repository *r,
      		base = commit;
      		base_label = msg.label;
      		next = parent;
    -@@ sequencer.c: static int do_pick_commit(struct repository *r,
    - 		if (opts->commit_use_reference) {
    - 			strbuf_commented_addf(&ctx->message, comment_line_str,
    - 				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
    + 		next_label = msg.parent_label;
    +-		if (opts->commit_use_reference) {
    +-			strbuf_commented_addf(&ctx->message, comment_line_str,
    +-				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
     -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
     -			   /*
     -			    * We don't touch pre-existing repeated reverts, because
    @@ sequencer.c: static int do_pick_commit(struct repository *r,
     -			strbuf_addstr(&ctx->message, "Reapply \"");
     -			strbuf_addstr(&ctx->message, orig_subject);
     -			strbuf_addstr(&ctx->message, "\n");
    -+			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
    - 		} else {
    +-		} else {
     -			strbuf_addstr(&ctx->message, "Revert \"");
     -			strbuf_addstr(&ctx->message, msg.subject);
     -			strbuf_addstr(&ctx->message, "\"\n");
    -+			sequencer_format_revert_header(&ctx->message, msg.subject, NULL);
    - 		}
    +-		}
     -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
    - 		refer_to_commit(opts, &ctx->message, commit);
    +-		refer_to_commit(opts, &ctx->message, commit);
    +-
    +-		if (commit->parents && commit->parents->next) {
    +-			strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
    +-			refer_to_commit(opts, &ctx->message, parent);
    +-		}
    +-		strbuf_addstr(&ctx->message, ".\n");
    ++		sequencer_format_revert_message(r, msg.subject, commit,
    ++						parent,
    ++						opts->commit_use_reference,
    ++						&ctx->message);
    + 	} else {
    + 		const char *p;
      
    - 		if (commit->parents && commit->parents->next) {
     @@ sequencer.c: int sequencer_pick_revisions(struct repository *r,
      	return res;
      }
      
    -+void sequencer_format_revert_header(struct strbuf *out,
    -+				    const char *orig_subject,
    -+				    const struct object_id *oid)
    ++void sequencer_format_revert_message(struct repository *r,
    ++				     const char *subject,
    ++				     const struct commit *commit,
    ++				     const struct commit *parent,
    ++				     bool use_commit_reference,
    ++				     struct strbuf *message)
     +{
    -+	const char *revert_subject;
    ++	const char *orig_subject;
     +
    -+	if (skip_prefix(orig_subject, "Revert \"", &revert_subject) &&
    -+	    /*
    -+	     * We don't touch pre-existing repeated reverts, because
    -+	     * theoretically these can be nested arbitrarily deeply,
    -+	     * thus requiring excessive complexity to deal with.
    -+	     */
    -+	    !starts_with(revert_subject, "Revert \"")) {
    -+		strbuf_addstr(out, "Reapply \"");
    -+		strbuf_addstr(out, revert_subject);
    -+		strbuf_addch(out, '\n');
    ++	if (use_commit_reference) {
    ++		strbuf_commented_addf(message, comment_line_str,
    ++				      "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
    ++	} else if (skip_prefix(subject, "Revert \"", &orig_subject) &&
    ++		   /*
    ++		    * We don't touch pre-existing repeated reverts, because
    ++		    * theoretically these can be nested arbitrarily deeply,
    ++		    * thus requiring excessive complexity to deal with.
    ++		    */
    ++		   !starts_with(orig_subject, "Revert \"")) {
    ++		strbuf_addstr(message, "Reapply \"");
    ++		strbuf_addstr(message, orig_subject);
    ++		strbuf_addstr(message, "\n");
     +	} else {
    -+		strbuf_addstr(out, "Revert \"");
    -+		strbuf_addstr(out, orig_subject);
    -+		strbuf_addstr(out, "\"\n");
    ++		strbuf_addstr(message, "Revert \"");
    ++		strbuf_addstr(message, subject);
    ++		strbuf_addstr(message, "\"\n");
     +	}
    ++	strbuf_addstr(message, "\nThis reverts commit ");
    ++	refer_to_commit(r, message, commit, use_commit_reference);
     +
    -+	strbuf_addstr(out, "\nThis reverts commit ");
    -+	if (oid) {
    -+		strbuf_addstr(out, oid_to_hex(oid));
    -+		strbuf_addstr(out, ".\n");
    ++	if (commit->parents && commit->parents->next) {
    ++		strbuf_addstr(message, ", reversing\nchanges made to ");
    ++		refer_to_commit(r, message, parent, use_commit_reference);
     +	}
    ++	strbuf_addstr(message, ".\n");
     +}
     +
      void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
    @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh
      int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
      
     +/*
    -+ * Formats a revert commit message following standard Git conventions.
    -+ * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert
    -+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit <oid>." if oid
    -+ * is provided, otherwise just adds "This reverts commit " and the caller
    -+ * should append the commit reference.
    ++ * Formats a complete revert commit message following standard Git conventions.
    ++ * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases
    ++ * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts
    ++ * commit <ref>." using either the abbreviated or full commit reference
    ++ * depending on use_commit_reference. Also handles merge-parent references.
     + */
    -+void sequencer_format_revert_header(struct strbuf *out,
    -+				    const char *orig_subject,
    -+				    const struct object_id *oid);
    ++void sequencer_format_revert_message(struct repository *r,
    ++				     const char *subject,
    ++				     const struct commit *commit,
    ++				     const struct commit *parent,
    ++				     bool use_commit_reference,
    ++				     struct strbuf *message);
     +
      #endif /* SEQUENCER_H */
2:  066269706e ! 2:  bea6229575 replay: add --revert mode to reverse commit changes
    @@ Commit message
         We swap the base and pickme trees passed to merge_incore_nonrecursive()
         to reverse the diff direction.
     
    +    Reverts are processed newest-first (matching git revert behavior) to
    +    reduce conflicts by peeling off changes from the top. Each revert
    +    builds on the result of the previous one via the last_commit fallback
    +    in the main replay loop, rather than relying on the parent-mapping
    +    used for cherry-pick.
    +
         Revert commit messages follow the usual git revert conventions: prefixed
         with "Revert" (or "Reapply" when reverting a revert), and including
         "This reverts commit <hash>.". The author is set to the current user
    @@ Commit message
         Helped-by: Phillip Wood <phillip.wood123@gmail.com>
         Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
         Helped-by: Junio C Hamano <gitster@pobox.com>
    +    Helped-by: Toon Claes <toon@iotcl.com>
         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Documentation/git-replay.adoc ##
    @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them
     +To revert commits on a branch:
     +
     +------------
    -+$ git replay --revert main main~2..main
    ++$ git replay --revert main topic~2..topic
     +------------
     +
    -+This reverts the last two commits on `main`, creating two revert commits
    -+on top of `main`, and updates `main` to point at the result.
    ++This reverts the last two commits from `topic`, creating revert commits on
    ++top of `main`, and updates `main` to point at the result. This is useful when
    ++commits from `topic` 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.
     +
      GIT
      ---
    @@ builtin/replay.c: int cmd_replay(int argc,
      
      	/* Parse ref action mode from command line or config */
      	ref_mode = get_ref_action_mode(repo, ref_action);
    +@@ builtin/replay.c: int cmd_replay(int argc,
    + 	 * some options changing these values if we think they could
    + 	 * be useful.
    + 	 */
    +-	revs.reverse = 1;
    ++	/*
    ++	 * Cherry-pick/rebase need oldest-first ordering so that each
    ++	 * replayed commit can build on its already-replayed parent.
    ++	 * Revert needs newest-first ordering (like git revert) to
    ++	 * reduce conflicts by peeling off changes from the top.
    ++	 */
    ++	revs.reverse = opts.revert ? 0 : 1;
    + 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
    + 	revs.topo_order = 1;
    + 	revs.simplify_history = 0;
    +@@ builtin/replay.c: int cmd_replay(int argc,
    + 	 * Detect and warn if we override some user specified rev
    + 	 * walking options.
    + 	 */
    +-	if (revs.reverse != 1) {
    +-		warning(_("some rev walking options will be overridden as "
    +-			  "'%s' bit in 'struct rev_info' will be forced"),
    +-			"reverse");
    +-		revs.reverse = 1;
    ++	{
    ++		int desired_reverse = opts.revert ? 0 : 1;
    ++		if (revs.reverse != desired_reverse) {
    ++			warning(_("some rev walking options will be overridden as "
    ++				  "'%s' bit in 'struct rev_info' will be forced"),
    ++				"reverse");
    ++			revs.reverse = desired_reverse;
    ++		}
    + 	}
    + 	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
    + 		warning(_("some rev walking options will be overridden as "
     @@ builtin/replay.c: int cmd_replay(int argc,
      		goto cleanup;
      
    @@ replay.c
      #include "strmap.h"
      #include "tree.h"
      
    +-/*
    +- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but
    +- * do not want to use the_repository.
    +- */
    +-#define the_repository DO_NOT_USE_THE_REPOSITORY
     +enum replay_mode {
     +	REPLAY_MODE_PICK,
     +	REPLAY_MODE_REVERT,
     +};
    -+
    + 
      static const char *short_commit_name(struct repository *repo,
      				     struct commit *commit)
    - {
     @@ replay.c: static char *get_author(const char *message)
      	return NULL;
      }
    @@ replay.c: static char *get_author(const char *message)
     +	subject_len = find_commit_subject(message, &subject_start);
     +	subject = xmemdupz(subject_start, subject_len);
     +
    -+	sequencer_format_revert_header(msg, subject, &commit->object.oid);
    ++	sequencer_format_revert_message(repo, subject, commit,
    ++					commit->parents ? commit->parents->item : NULL,
    ++					false, msg);
     +
     +	free(subject);
     +	repo_unuse_commit_buffer(repo, commit, message);
    @@ replay.c: static void get_ref_information(struct repository *repo,
     +	}
     +	*onto = peel_committish(repo, *branch_name, option_name);
     +	if (rinfo->positive_refexprs > 1)
    -+		die(_("cannot %s target with multiple sources because ordering would be ill-defined"),
    -+		    option_name + 2); /* skip "--" prefix */
    ++		die(_("'%s' cannot be used with multiple revision ranges "
    ++		      "because the ordering would be ill-defined"),
    ++		    option_name);
     +}
     +
      static void set_up_replay_mode(struct repository *repo,
    @@ replay.c: static struct commit *pick_regular_commit(struct repository *repo,
     +	merge_opt->branch2 = NULL;
      	if (!result->clean)
      		return NULL;
    --	/* Drop commits that become empty */
    --	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
    -+	/* Drop commits that become empty (only for picks) */
    -+	if (mode == REPLAY_MODE_PICK &&
    -+	    oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
    + 	/* Drop commits that become empty */
    + 	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
      	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
      		return replayed_base;
     -	return create_commit(repo, result->tree, pickme, replayed_base);
    @@ replay.c: int replay_revisions(struct rev_info *revs,
      
      		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
     -						  onto, &merge_opt, &result);
    -+						  onto, &merge_opt, &result, mode);
    ++						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
    ++						  &merge_opt, &result, mode);
      		if (!last_commit)
      			break;
      
    @@ t/t3650-replay-basics.sh: test_expect_success 'no base or negative ref gives no-
      	test_must_fail git replay --advance=main --contained \
      		topic1..topic2 2>actual &&
      	test_cmp expect actual
    + '
    + 
    + test_expect_success 'cannot advance target ... ordering would be ill-defined' '
    +-	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
    ++	cat >expect <<-\EOF &&
    ++	fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
    ++	EOF
    + 	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
    + 	test_cmp expect actual
    + '
     @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '
      	test_grep "invalid.*replay.refAction.*value" error
      '
    @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '
     +'
     +
     +test_expect_success 'cannot revert with multiple sources' '
    -+	echo "fatal: cannot revert target with multiple sources because ordering would be ill-defined" >expect &&
    ++	cat >expect <<-\EOF &&
    ++	fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
    ++	EOF
     +	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
     +	test_cmp expect actual
     +'
    @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '
     +	# Revert commits I and J
     +	git replay --revert topic4 topic4~2..topic4 &&
     +
    -+	# Verify the revert commits were created
    ++	# Verify the revert commits were created (newest-first ordering
    ++	# means J is reverted first, then I on top)
     +	git log --format=%s -4 topic4 >actual &&
     +	cat >expect <<-\EOF &&
    -+	Revert "J"
     +	Revert "I"
    ++	Revert "J"
     +	J
     +	I
     +	EOF
     +	test_cmp expect actual &&
     +
    -+	# Verify commit message format includes hash
    ++	# Verify commit message format includes hash (tip is Revert "I")
     +	test_commit_message topic4 <<-EOF &&
    -+	Revert "J"
    ++	Revert "I"
     +
    -+	This reverts commit $(git rev-parse J).
    ++	This reverts commit $(git rev-parse I).
     +	EOF
     +
     +	# Verify reflog message


base-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2


^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v4 1/2] sequencer: extract revert message formatting into shared function
  2026-03-13  5:40     ` [PATCH v4 0/2] " Siddharth Asthana
@ 2026-03-13  5:40       ` Siddharth Asthana
  2026-03-13 15:53         ` Junio C Hamano
  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
                         ` (3 subsequent siblings)
  4 siblings, 2 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-13  5:40 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana

The logic for formatting revert commit messages (handling "Revert" and
"Reapply" cases, appending "This reverts commit <ref>.", and handling
merge-parent references) currently lives inline in do_pick_commit().
The upcoming replay --revert mode needs to reuse this logic.

Extract all of this into a new sequencer_format_revert_message()
function. The function takes a repository, the subject line, commit,
parent, a use_commit_reference flag, and the output strbuf. It handles
both regular reverts ("Revert "<subject>"") and revert-of-revert cases
("Reapply "<subject>""), and uses refer_to_commit() internally to
format the commit reference.

Update refer_to_commit() to take a struct repository parameter instead
of relying on the_repository, and a bool instead of reading from
replay_opts directly. This makes it usable from the new shared function
without pulling in sequencer-specific state.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------
 sequencer.h | 14 ++++++++++
 2 files changed, 60 insertions(+), 32 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index aafd0bc959..7bf9d6ad19 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) {
 	return opts->edit;
 }
 
-static void refer_to_commit(struct replay_opts *opts,
-			    struct strbuf *msgbuf, struct commit *commit)
+static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,
+			    const struct commit *commit,
+			    bool use_commit_reference)
 {
-	if (opts->commit_use_reference) {
+	if (use_commit_reference) {
 		struct pretty_print_context ctx = {
 			.abbrev = DEFAULT_ABBREV,
 			.date_mode.type = DATE_SHORT,
 		};
-		repo_format_commit_message(the_repository, commit,
+		repo_format_commit_message(r, commit,
 					   "%h (%s, %ad)", msgbuf, &ctx);
 	} else {
 		strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));
@@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r,
 	 */
 
 	if (command == TODO_REVERT) {
-		const char *orig_subject;
-
 		base = commit;
 		base_label = msg.label;
 		next = parent;
 		next_label = msg.parent_label;
-		if (opts->commit_use_reference) {
-			strbuf_commented_addf(&ctx->message, comment_line_str,
-				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
-		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
-			   /*
-			    * We don't touch pre-existing repeated reverts, because
-			    * theoretically these can be nested arbitrarily deeply,
-			    * thus requiring excessive complexity to deal with.
-			    */
-			   !starts_with(orig_subject, "Revert \"")) {
-			strbuf_addstr(&ctx->message, "Reapply \"");
-			strbuf_addstr(&ctx->message, orig_subject);
-			strbuf_addstr(&ctx->message, "\n");
-		} else {
-			strbuf_addstr(&ctx->message, "Revert \"");
-			strbuf_addstr(&ctx->message, msg.subject);
-			strbuf_addstr(&ctx->message, "\"\n");
-		}
-		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
-		refer_to_commit(opts, &ctx->message, commit);
-
-		if (commit->parents && commit->parents->next) {
-			strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
-			refer_to_commit(opts, &ctx->message, parent);
-		}
-		strbuf_addstr(&ctx->message, ".\n");
+		sequencer_format_revert_message(r, msg.subject, commit,
+						parent,
+						opts->commit_use_reference,
+						&ctx->message);
 	} else {
 		const char *p;
 
@@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r,
 	return res;
 }
 
+void sequencer_format_revert_message(struct repository *r,
+				     const char *subject,
+				     const struct commit *commit,
+				     const struct commit *parent,
+				     bool use_commit_reference,
+				     struct strbuf *message)
+{
+	const char *orig_subject;
+
+	if (use_commit_reference) {
+		strbuf_commented_addf(message, comment_line_str,
+				      "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
+	} else if (skip_prefix(subject, "Revert \"", &orig_subject) &&
+		   /*
+		    * We don't touch pre-existing repeated reverts, because
+		    * theoretically these can be nested arbitrarily deeply,
+		    * thus requiring excessive complexity to deal with.
+		    */
+		   !starts_with(orig_subject, "Revert \"")) {
+		strbuf_addstr(message, "Reapply \"");
+		strbuf_addstr(message, orig_subject);
+		strbuf_addstr(message, "\n");
+	} else {
+		strbuf_addstr(message, "Revert \"");
+		strbuf_addstr(message, subject);
+		strbuf_addstr(message, "\"\n");
+	}
+	strbuf_addstr(message, "\nThis reverts commit ");
+	refer_to_commit(r, message, commit, use_commit_reference);
+
+	if (commit->parents && commit->parents->next) {
+		strbuf_addstr(message, ", reversing\nchanges made to ");
+		refer_to_commit(r, message, parent, use_commit_reference);
+	}
+	strbuf_addstr(message, ".\n");
+}
+
 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
 {
 	unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
diff --git a/sequencer.h b/sequencer.h
index 719684c8a9..de0bd6e8a2 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -271,4 +271,18 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)
  */
 int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
 
+/*
+ * Formats a complete revert commit message following standard Git conventions.
+ * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases
+ * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts
+ * commit <ref>." using either the abbreviated or full commit reference
+ * depending on use_commit_reference. Also handles merge-parent references.
+ */
+void sequencer_format_revert_message(struct repository *r,
+				     const char *subject,
+				     const struct commit *commit,
+				     const struct commit *parent,
+				     bool use_commit_reference,
+				     struct strbuf *message);
+
 #endif /* SEQUENCER_H */
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* [PATCH v4 2/2] replay: add --revert mode to reverse commit changes
  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  5:40       ` Siddharth Asthana
  2026-03-16 16:57         ` Phillip Wood
  2026-03-16 19:52         ` Toon Claes
  2026-03-16 16:59       ` [PATCH v4 0/2] " Phillip Wood
                         ` (2 subsequent siblings)
  4 siblings, 2 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-13  5:40 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana,
	Johannes Schindelin

Add a `--revert <branch>` mode to git replay that undoes the changes
introduced by the specified commits. Like --onto and --advance, --revert
is a standalone mode: it takes a branch argument and updates that branch
with the newly created revert commits.

At GitLab, we need this in Gitaly for reverting commits directly on bare
repositories without requiring a working tree checkout.

The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick
and revert are just the same three-way merge with swapped arguments:

  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
  - Revert: merge(ancestor=commit, ours=current, theirs=parent)

We swap the base and pickme trees passed to merge_incore_nonrecursive()
to reverse the diff direction.

Reverts are processed newest-first (matching git revert behavior) to
reduce conflicts by peeling off changes from the top. Each revert
builds on the result of the previous one via the last_commit fallback
in the main replay loop, rather than relying on the parent-mapping
used for cherry-pick.

Revert commit messages follow the usual git revert conventions: prefixed
with "Revert" (or "Reapply" when reverting a revert), and including
"This reverts commit <hash>.". The author is set to the current user
rather than preserving the original author, matching git revert behavior.

Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  43 ++++++++-
 builtin/replay.c              |  46 ++++++----
 replay.c                      | 165 ++++++++++++++++++++++++----------
 replay.h                      |  11 ++-
 t/t3650-replay-basics.sh      | 114 +++++++++++++++++++++--
 5 files changed, 304 insertions(+), 75 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 8d696ce3ab..6698cfc047 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,25 @@ 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 reverted commits; must be a
+	branch name.
++
+When `--revert` is specified, the commits in the revision range are reverted
+(their changes are undone) and the reverted commits are created on top of
+<branch>. The <branch> is then updated to point at the new commits. This is
+the same as running `git revert <revision-range>` but does not update the
+working tree.
++
+The commit messages follow `git revert` conventions: they are prefixed with
+"Revert" and include "This reverts commit <hash>." When reverting a commit
+whose message starts with "Revert", the new message uses "Reapply" instead.
+Unlike cherry-pick which preserves the original author, revert commits use
+the current user as the author, matching the behavior of `git revert`.
++
+This option is mutually exclusive with `--onto` and `--advance`. It is also
+incompatible with `--contained` (which is a modifier for `--onto` only).
+
 --contained::
 	Update all branches that point at commits in
 	<revision-range>. Requires `--onto`.
@@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 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).
+the shape of the history being replayed.  When using `--advance` or
+`--revert`, the number of refs updated is always one, but for `--onto`,
+it can be one or more (rebasing multiple branches simultaneously is
+supported).
 
 There is no stderr output on conflicts; see the <<exit-status,EXIT
 STATUS>> section below.
@@ -152,6 +172,21 @@ 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 on a branch:
+
+------------
+$ git replay --revert main topic~2..topic
+------------
+
+This reverts the last two commits from `topic`, creating revert commits on
+top of `main`, and updates `main` to point at the result. This is useful when
+commits from `topic` 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.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 2cdde830a8..fe69f6f8ce 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -83,8 +83,8 @@ int cmd_replay(int argc,
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "[--ref-action[=<mode>]] <revision-range>"),
+		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
+		   "[--ref-action[=<mode>]] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -96,6 +96,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &opts.contained,
 			 N_("update all branches that point at commits in <revision-range>")),
+		OPT_STRING(0, "revert", &opts.revert,
+			   N_("branch"),
+			   N_("revert commits onto given branch")),
 		OPT_STRING(0, "ref-action", &ref_action,
 			   N_("mode"),
 			   N_("control ref update behavior (update|print)")),
@@ -105,15 +108,17 @@ int cmd_replay(int argc,
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!opts.onto && !opts.advance) {
-		error(_("option --onto or --advance is mandatory"));
+	/* Exactly one mode must be specified */
+	if (!opts.onto && !opts.advance && !opts.revert) {
+		error(_("exactly one of --onto, --advance, or --revert is required"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	die_for_incompatible_opt2(!!opts.advance, "--advance",
-				  opts.contained, "--contained");
-	die_for_incompatible_opt2(!!opts.advance, "--advance",
-				  !!opts.onto, "--onto");
+	die_for_incompatible_opt3(!!opts.onto, "--onto",
+				  !!opts.advance, "--advance",
+				  !!opts.revert, "--revert");
+	if (opts.contained && !opts.onto)
+		die(_("--contained requires --onto"));
 
 	/* Parse ref action mode from command line or config */
 	ref_mode = get_ref_action_mode(repo, ref_action);
@@ -129,7 +134,13 @@ int cmd_replay(int argc,
 	 * some options changing these values if we think they could
 	 * be useful.
 	 */
-	revs.reverse = 1;
+	/*
+	 * Cherry-pick/rebase need oldest-first ordering so that each
+	 * replayed commit can build on its already-replayed parent.
+	 * Revert needs newest-first ordering (like git revert) to
+	 * reduce conflicts by peeling off changes from the top.
+	 */
+	revs.reverse = opts.revert ? 0 : 1;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
@@ -144,11 +155,14 @@ int cmd_replay(int argc,
 	 * Detect and warn if we override some user specified rev
 	 * walking options.
 	 */
-	if (revs.reverse != 1) {
-		warning(_("some rev walking options will be overridden as "
-			  "'%s' bit in 'struct rev_info' will be forced"),
-			"reverse");
-		revs.reverse = 1;
+	{
+		int desired_reverse = opts.revert ? 0 : 1;
+		if (revs.reverse != desired_reverse) {
+			warning(_("some rev walking options will be overridden as "
+				  "'%s' bit in 'struct rev_info' will be forced"),
+				"reverse");
+			revs.reverse = desired_reverse;
+		}
 	}
 	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
 		warning(_("some rev walking options will be overridden as "
@@ -174,7 +188,9 @@ int cmd_replay(int argc,
 		goto cleanup;
 
 	/* Build reflog message */
-	if (opts.advance) {
+	if (opts.revert) {
+		strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
+	} else if (opts.advance) {
 		strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
 	} else {
 		struct object_id oid;
diff --git a/replay.c b/replay.c
index a63f6714c4..199066f6b3 100644
--- a/replay.c
+++ b/replay.c
@@ -8,14 +8,14 @@
 #include "refs.h"
 #include "replay.h"
 #include "revision.h"
+#include "sequencer.h"
 #include "strmap.h"
 #include "tree.h"
 
-/*
- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but
- * do not want to use the_repository.
- */
-#define the_repository DO_NOT_USE_THE_REPOSITORY
+enum replay_mode {
+	REPLAY_MODE_PICK,
+	REPLAY_MODE_REVERT,
+};
 
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
@@ -50,15 +50,37 @@ static char *get_author(const char *message)
 	return NULL;
 }
 
+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_message(repo, subject, commit,
+					commit->parents ? commit->parents->item : NULL,
+					false, msg);
+
+	free(subject);
+	repo_unuse_commit_buffer(repo, commit, message);
+}
+
 static struct commit *create_commit(struct repository *repo,
 				    struct tree *tree,
 				    struct commit *based_on,
-				    struct commit *parent)
+				    struct commit *parent,
+				    enum replay_mode mode)
 {
 	struct object_id ret;
 	struct object *obj = NULL;
 	struct commit_list *parents = NULL;
-	char *author;
+	char *author = NULL;
 	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra = NULL;
 	struct strbuf msg = STRBUF_INIT;
@@ -70,9 +92,16 @@ 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 (mode == REPLAY_MODE_REVERT) {
+		generate_revert_message(&msg, based_on, repo);
+		/* For revert, use current user as author (NULL = use default) */
+	} else if (mode == REPLAY_MODE_PICK) {
+		find_commit_subject(message, &orig_message);
+		strbuf_addstr(&msg, orig_message);
+		author = get_author(message);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	reset_ident_date();
 	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
 				 &ret, author, NULL, sign_commit, extra)) {
@@ -153,11 +182,35 @@ static void get_ref_information(struct repository *repo,
 	}
 }
 
+static void set_up_branch_mode(struct repository *repo,
+			       char **branch_name,
+			       const char *option_name,
+			       struct ref_info *rinfo,
+			       struct commit **onto)
+{
+	struct object_id oid;
+	char *fullname = NULL;
+
+	if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
+			  &oid, &fullname, 0) == 1) {
+		free(*branch_name);
+		*branch_name = fullname;
+	} else {
+		die(_("argument to %s must be a reference"), option_name);
+	}
+	*onto = peel_committish(repo, *branch_name, option_name);
+	if (rinfo->positive_refexprs > 1)
+		die(_("'%s' cannot be used with multiple revision ranges "
+		      "because the ordering would be ill-defined"),
+		    option_name);
+}
+
 static void set_up_replay_mode(struct repository *repo,
 			       struct rev_cmdline_info *cmd_info,
 			       const char *onto_name,
 			       bool *detached_head,
 			       char **advance_name,
+			       char **revert_name,
 			       struct commit **onto,
 			       struct strset **update_refs)
 {
@@ -172,9 +225,6 @@ static void set_up_replay_mode(struct repository *repo,
 	if (!rinfo.positive_refexprs)
 		die(_("need some commits to replay"));
 
-	if (!onto_name == !*advance_name)
-		BUG("one and only one of onto_name and *advance_name must be given");
-
 	if (onto_name) {
 		*onto = peel_committish(repo, onto_name, "--onto");
 		if (rinfo.positive_refexprs <
@@ -183,23 +233,12 @@ static void set_up_replay_mode(struct repository *repo,
 		*update_refs = xcalloc(1, sizeof(**update_refs));
 		**update_refs = rinfo.positive_refs;
 		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	} else if (*advance_name) {
+		set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
+	} else if (*revert_name) {
+		set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
 	} else {
-		struct object_id oid;
-		char *fullname = NULL;
-
-		if (!*advance_name)
-			BUG("expected either onto_name or *advance_name in this function");
-
-		if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
-			     &oid, &fullname, 0) == 1) {
-			free(*advance_name);
-			*advance_name = fullname;
-		} else {
-			die(_("argument to --advance must be a reference"));
-		}
-		*onto = peel_committish(repo, *advance_name, "--advance");
-		if (rinfo.positive_refexprs > 1)
-			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+		BUG("expected one of onto_name, *advance_name, or *revert_name");
 	}
 	strset_clear(&rinfo.negative_refs);
 	strset_clear(&rinfo.positive_refs);
@@ -220,7 +259,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  kh_oid_map_t *replayed_commits,
 					  struct commit *onto,
 					  struct merge_options *merge_opt,
-					  struct merge_result *result)
+					  struct merge_result *result,
+					  enum replay_mode mode)
 {
 	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -232,25 +272,45 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	pickme_tree = repo_get_commit_tree(repo, pickme);
 	base_tree = repo_get_commit_tree(repo, base);
 
-	merge_opt->branch1 = short_commit_name(repo, replayed_base);
-	merge_opt->branch2 = short_commit_name(repo, pickme);
-	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
-
-	merge_incore_nonrecursive(merge_opt,
-				  base_tree,
-				  replayed_base_tree,
-				  pickme_tree,
-				  result);
-
-	free((char*)merge_opt->ancestor);
+	if (mode == REPLAY_MODE_PICK) {
+		/* Cherry-pick: normal order */
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = short_commit_name(repo, pickme);
+		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+		merge_incore_nonrecursive(merge_opt,
+					  base_tree,
+					  replayed_base_tree,
+					  pickme_tree,
+					  result);
+
+		free((char *)merge_opt->ancestor);
+	} else if (mode == REPLAY_MODE_REVERT) {
+		/* Revert: swap base and pickme to reverse the diff */
+		const char *pickme_name = short_commit_name(repo, pickme);
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
+		merge_opt->ancestor = pickme_name;
+
+		merge_incore_nonrecursive(merge_opt,
+					  pickme_tree,
+					  replayed_base_tree,
+					  base_tree,
+					  result);
+
+		free((char *)merge_opt->branch2);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	merge_opt->ancestor = NULL;
+	merge_opt->branch2 = NULL;
 	if (!result->clean)
 		return NULL;
 	/* Drop commits that become empty */
 	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
 	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
 		return replayed_base;
-	return create_commit(repo, result->tree, pickme, replayed_base);
+	return create_commit(repo, result->tree, pickme, replayed_base, mode);
 }
 
 void replay_result_release(struct replay_result *result)
@@ -287,11 +347,16 @@ int replay_revisions(struct rev_info *revs,
 	};
 	bool detached_head;
 	char *advance;
+	char *revert;
+	enum replay_mode mode = REPLAY_MODE_PICK;
 	int ret;
 
 	advance = xstrdup_or_null(opts->advance);
+	revert = xstrdup_or_null(opts->revert);
+	if (revert)
+		mode = REPLAY_MODE_REVERT;
 	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
-			   &detached_head, &advance, &onto, &update_refs);
+			   &detached_head, &advance, &revert, &onto, &update_refs);
 
 	/* FIXME: Should allow replaying commits with the first as a root commit */
 
@@ -315,7 +380,8 @@ int replay_revisions(struct rev_info *revs,
 			die(_("replaying merge commits is not supported yet!"));
 
 		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
-						  onto, &merge_opt, &result);
+						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
+						  &merge_opt, &result, mode);
 		if (!last_commit)
 			break;
 
@@ -327,7 +393,7 @@ int replay_revisions(struct rev_info *revs,
 		kh_value(replayed_commits, pos) = last_commit;
 
 		/* Update any necessary branches */
-		if (advance)
+		if (advance || revert)
 			continue;
 
 		for (decoration = get_name_decoration(&commit->object);
@@ -361,11 +427,13 @@ int replay_revisions(struct rev_info *revs,
 		goto out;
 	}
 
-	/* In --advance mode, advance the target ref */
-	if (advance)
-		replay_result_queue_update(out, advance,
+	/* In --advance or --revert mode, update the target ref */
+	if (advance || revert) {
+		const char *ref = advance ? advance : revert;
+		replay_result_queue_update(out, ref,
 					   &onto->object.oid,
 					   &last_commit->object.oid);
+	}
 
 	ret = 0;
 
@@ -377,5 +445,6 @@ int replay_revisions(struct rev_info *revs,
 	kh_destroy_oid_map(replayed_commits);
 	merge_finalize(&merge_opt, &result);
 	free(advance);
+	free(revert);
 	return ret;
 }
diff --git a/replay.h b/replay.h
index d8407dc7f7..e916a5f975 100644
--- a/replay.h
+++ b/replay.h
@@ -13,7 +13,7 @@ struct replay_revisions_options {
 	/*
 	 * Starting point at which to create the new commits; must be a branch
 	 * name. The branch will be updated to point to the rewritten commits.
-	 * This option is mutually exclusive with `onto`.
+	 * This option is mutually exclusive with `onto` and `revert`.
 	 */
 	const char *advance;
 
@@ -22,7 +22,14 @@ struct replay_revisions_options {
 	 * committish. References pointing at decendants of `onto` will be
 	 * updated to point to the new commits.
 	 */
-	 const char *onto;
+	const char *onto;
+
+	/*
+	 * Starting point at which to create revert commits; must be a branch
+	 * name. The branch will be updated to point to the revert commits.
+	 * This option is mutually exclusive with `onto` and `advance`.
+	 */
+	const char *revert;
 
 	/*
 	 * Update branches that point at commits in the given revision range.
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a03f8f9293..0c1e03e0fb 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '
 	test_cmp expect actual
 '
 
-test_expect_success 'option --onto or --advance is mandatory' '
-	echo "error: option --onto or --advance is mandatory" >expect &&
+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '
+	echo "error: exactly one of --onto, --advance, or --revert is required" >expect &&
 	test_might_fail git replay -h >>expect &&
 	test_must_fail git replay topic1..topic2 2>actual &&
 	test_cmp expect actual
@@ -87,16 +87,17 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err
 	test_cmp expect actual
 '
 
-test_expect_success 'options --advance and --contained cannot be used together' '
-	printf "fatal: options ${SQ}--advance${SQ} " >expect &&
-	printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
+test_expect_success '--contained requires --onto' '
+	echo "fatal: --contained requires --onto" >expect &&
 	test_must_fail git replay --advance=main --contained \
 		topic1..topic2 2>actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'cannot advance target ... ordering would be ill-defined' '
-	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
+	cat >expect <<-\EOF &&
+	fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
+	EOF
 	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
 	test_cmp expect actual
 '
@@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' '
 	test_grep "invalid.*replay.refAction.*value" error
 '
 
+test_expect_success 'argument to --revert must be a reference' '
+	echo "fatal: argument to --revert must be a reference" >expect &&
+	oid=$(git rev-parse main) &&
+	test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'cannot revert with multiple sources' '
+	cat >expect <<-\EOF &&
+	fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
+	EOF
+	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using replay --revert to revert commits' '
+	# Reuse existing topic4 branch (has commits I and J on top of main)
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# Revert commits I and J
+	git replay --revert topic4 topic4~2..topic4 &&
+
+	# Verify the revert commits were created (newest-first ordering
+	# means J is reverted first, then I on top)
+	git log --format=%s -4 topic4 >actual &&
+	cat >expect <<-\EOF &&
+	Revert "I"
+	Revert "J"
+	J
+	I
+	EOF
+	test_cmp expect actual &&
+
+	# Verify commit message format includes hash (tip is Revert "I")
+	test_commit_message topic4 <<-EOF &&
+	Revert "I"
+
+	This reverts commit $(git rev-parse I).
+	EOF
+
+	# Verify reflog message
+	git reflog topic4 -1 --format=%gs >reflog-msg &&
+	echo "replay --revert topic4" >expect-reflog &&
+	test_cmp expect-reflog reflog-msg
+'
+
+test_expect_success 'using replay --revert in bare repo' '
+	# Reuse existing topic4 in bare repo
+	START=$(git -C bare rev-parse topic4) &&
+	test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
+
+	# Revert commit J in bare repo
+	git -C bare replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify revert was created
+	git -C bare log -1 --format=%s topic4 >actual &&
+	echo "Revert \"J\"" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'revert of revert uses Reapply' '
+	# Use topic4 and first revert J, then revert the revert
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# First revert J
+	git replay --revert topic4 topic4~1..topic4 &&
+	REVERT_J=$(git rev-parse topic4) &&
+
+	# Now revert the revert - should become Reapply
+	git replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify Reapply prefix and message format
+	test_commit_message topic4 <<-EOF
+	Reapply "J"
+
+	This reverts commit $REVERT_J.
+	EOF
+'
+
+test_expect_success 'git replay --revert with conflict' '
+	# conflict branch has C.conflict which conflicts with topic1s C
+	test_expect_code 1 git replay --revert conflict B..topic1
+'
+
+test_expect_success 'git replay --revert incompatible with --contained' '
+	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
+	test_grep "requires --onto" error
+'
+
+test_expect_success 'git replay --revert incompatible with --onto' '
+	test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --advance' '
+	test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* Re: [PATCH v4 1/2] sequencer: extract revert message formatting into shared function
  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
  1 sibling, 1 reply; 92+ messages in thread
From: Junio C Hamano @ 2026-03-13 15:53 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188,
	johannes.schindelin, toon

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> The logic for formatting revert commit messages (handling "Revert" and
> "Reapply" cases, appending "This reverts commit <ref>.", and handling
> merge-parent references) currently lives inline in do_pick_commit().
> The upcoming replay --revert mode needs to reuse this logic.
>
> Extract all of this into a new sequencer_format_revert_message()
> function. The function takes a repository, the subject line, commit,
> parent, a use_commit_reference flag, and the output strbuf. It handles
> both regular reverts ("Revert "<subject>"") and revert-of-revert cases
> ("Reapply "<subject>""), and uses refer_to_commit() internally to
> format the commit reference.
>
> Update refer_to_commit() to take a struct repository parameter instead
> of relying on the_repository, and a bool instead of reading from
> replay_opts directly. This makes it usable from the new shared function
> without pulling in sequencer-specific state.
>
> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> ---
>  sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------
>  sequencer.h | 14 ++++++++++
>  2 files changed, 60 insertions(+), 32 deletions(-)

Relative to the previous round, sequencer_format_revert_message()
that does a bit more than sequencer_format_revert_header() we had
makes the existing code easier to follow, even though the total
codeflow amounts to the same thing.  A new caller that will use the
function now has to do less.

Also, even though this is an internal implementation detail,
changing the list of parameters refer_to_commit() takes makes it
easier to understand which part of the replay_opts structure is used
(i.e., we only care about "do we use the commit reference, or not?"
bit, and we have no interest in any other members of the struct).

> +/*
> + * Formats a complete revert commit message following standard Git conventions.
> + * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases
> + * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts
> + * commit <ref>." using either the abbreviated or full commit reference
> + * depending on use_commit_reference. Also handles merge-parent references.
> + */
> +void sequencer_format_revert_message(struct repository *r,
> +				     const char *subject,
> +				     const struct commit *commit,
> +				     const struct commit *parent,
> +				     bool use_commit_reference,
> +				     struct strbuf *message);
> +
>  #endif /* SEQUENCER_H */

OK.

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v4 1/2] sequencer: extract revert message formatting into shared function
  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 16:57         ` Phillip Wood
  1 sibling, 0 replies; 92+ messages in thread
From: Phillip Wood @ 2026-03-16 16:57 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, karthik.188,
	johannes.schindelin, toon

On 13/03/2026 05:40, Siddharth Asthana wrote:
> The logic for formatting revert commit messages (handling "Revert" and
> "Reapply" cases, appending "This reverts commit <ref>.", and handling
> merge-parent references) currently lives inline in do_pick_commit().
> The upcoming replay --revert mode needs to reuse this logic.
> 
> Extract all of this into a new sequencer_format_revert_message()
> function. The function takes a repository, the subject line, commit,
> parent, a use_commit_reference flag, and the output strbuf. It handles
> both regular reverts ("Revert "<subject>"") and revert-of-revert cases
> ("Reapply "<subject>""), and uses refer_to_commit() internally to
> format the commit reference.
> 
> Update refer_to_commit() to take a struct repository parameter instead
> of relying on the_repository, and a bool instead of reading from
> replay_opts directly. This makes it usable from the new shared function
> without pulling in sequencer-specific state.

This looks good.

> +/*
> + * Formats a complete revert commit message following standard Git conventions.
> + * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases

Not worth a re-roll on its own, but for future reference in comments you 
can use single quotes to avoid nesting double quotes i.e. ('Revert 
"<subject>"').

Thanks

Phillip

> + * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts
> + * commit <ref>." using either the abbreviated or full commit reference
> + * depending on use_commit_reference. Also handles merge-parent references.
> + */
> +void sequencer_format_revert_message(struct repository *r,
> +				     const char *subject,
> +				     const struct commit *commit,
> +				     const struct commit *parent,
> +				     bool use_commit_reference,
> +				     struct strbuf *message);
> +
>   #endif /* SEQUENCER_H */


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v4 2/2] replay: add --revert mode to reverse commit changes
  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
  1 sibling, 0 replies; 92+ messages in thread
From: Phillip Wood @ 2026-03-16 16:57 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, karthik.188,
	johannes.schindelin, toon

Hi Siddharth

On 13/03/2026 05:40, Siddharth Asthana wrote:
> Add a `--revert <branch>` mode to git replay that undoes the changes
> introduced by the specified commits. Like --onto and --advance, --revert
> is a standalone mode: it takes a branch argument and updates that branch
> with the newly created revert commits.
> 
> At GitLab, we need this in Gitaly for reverting commits directly on bare
> repositories without requiring a working tree checkout.
> 
> The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick
> and revert are just the same three-way merge with swapped arguments:
> 
>    - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
>    - Revert: merge(ancestor=commit, ours=current, theirs=parent)
> 
> We swap the base and pickme trees passed to merge_incore_nonrecursive()
> to reverse the diff direction.
> 
> Reverts are processed newest-first (matching git revert behavior) to
> reduce conflicts by peeling off changes from the top. Each revert
> builds on the result of the previous one via the last_commit fallback
> in the main replay loop, rather than relying on the parent-mapping
> used for cherry-pick.
> 
> Revert commit messages follow the usual git revert conventions: prefixed
> with "Revert" (or "Reapply" when reverting a revert), and including
> "This reverts commit <hash>.". The author is set to the current user
> rather than preserving the original author, matching git revert behavior.

This addresses all my comments on the previous version. I've one minor 
comment below but I'm not sure its worth a re-roll on its own.

>   
>   test_expect_success 'cannot advance target ... ordering would be ill-defined' '
> -	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
> +	cat >expect <<-\EOF &&
> +	fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined

This quoting is a bit strange - we'd normally drop the '\' form '\EOF' 
above use ${SQ} instead. git grep shows there are 220 instances of ${SQ} 
vs 31 instances of '"'"' in the test suite.

Thanks

Phillip


> +	EOF
>   	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
>   	test_cmp expect actual
>   '
> @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' '
>   	test_grep "invalid.*replay.refAction.*value" error
>   '
>   
> +test_expect_success 'argument to --revert must be a reference' '
> +	echo "fatal: argument to --revert must be a reference" >expect &&
> +	oid=$(git rev-parse main) &&
> +	test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'cannot revert with multiple sources' '
> +	cat >expect <<-\EOF &&
> +	fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
> +	EOF
> +	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'using replay --revert to revert commits' '
> +	# Reuse existing topic4 branch (has commits I and J on top of main)
> +	START=$(git rev-parse topic4) &&
> +	test_when_finished "git branch -f topic4 $START" &&
> +
> +	# Revert commits I and J
> +	git replay --revert topic4 topic4~2..topic4 &&
> +
> +	# Verify the revert commits were created (newest-first ordering
> +	# means J is reverted first, then I on top)
> +	git log --format=%s -4 topic4 >actual &&
> +	cat >expect <<-\EOF &&
> +	Revert "I"
> +	Revert "J"
> +	J
> +	I
> +	EOF
> +	test_cmp expect actual &&
> +
> +	# Verify commit message format includes hash (tip is Revert "I")
> +	test_commit_message topic4 <<-EOF &&
> +	Revert "I"
> +
> +	This reverts commit $(git rev-parse I).
> +	EOF
> +
> +	# Verify reflog message
> +	git reflog topic4 -1 --format=%gs >reflog-msg &&
> +	echo "replay --revert topic4" >expect-reflog &&
> +	test_cmp expect-reflog reflog-msg
> +'
> +
> +test_expect_success 'using replay --revert in bare repo' '
> +	# Reuse existing topic4 in bare repo
> +	START=$(git -C bare rev-parse topic4) &&
> +	test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
> +
> +	# Revert commit J in bare repo
> +	git -C bare replay --revert topic4 topic4~1..topic4 &&
> +
> +	# Verify revert was created
> +	git -C bare log -1 --format=%s topic4 >actual &&
> +	echo "Revert \"J\"" >expect &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'revert of revert uses Reapply' '
> +	# Use topic4 and first revert J, then revert the revert
> +	START=$(git rev-parse topic4) &&
> +	test_when_finished "git branch -f topic4 $START" &&
> +
> +	# First revert J
> +	git replay --revert topic4 topic4~1..topic4 &&
> +	REVERT_J=$(git rev-parse topic4) &&
> +
> +	# Now revert the revert - should become Reapply
> +	git replay --revert topic4 topic4~1..topic4 &&
> +
> +	# Verify Reapply prefix and message format
> +	test_commit_message topic4 <<-EOF
> +	Reapply "J"
> +
> +	This reverts commit $REVERT_J.
> +	EOF
> +'
> +
> +test_expect_success 'git replay --revert with conflict' '
> +	# conflict branch has C.conflict which conflicts with topic1s C
> +	test_expect_code 1 git replay --revert conflict B..topic1
> +'
> +
> +test_expect_success 'git replay --revert incompatible with --contained' '
> +	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
> +	test_grep "requires --onto" error
> +'
> +
> +test_expect_success 'git replay --revert incompatible with --onto' '
> +	test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
> +	test_grep "cannot be used together" error
> +'
> +
> +test_expect_success 'git replay --revert incompatible with --advance' '
> +	test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
> +	test_grep "cannot be used together" error
> +'
> +
>   test_done


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v4 0/2] replay: add --revert mode to reverse commit changes
  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  5:40       ` [PATCH v4 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
@ 2026-03-16 16:59       ` Phillip Wood
  2026-03-16 19:53       ` Toon Claes
  2026-03-24 22:03       ` [PATCH v5 " Siddharth Asthana
  4 siblings, 0 replies; 92+ messages in thread
From: Phillip Wood @ 2026-03-16 16:59 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, karthik.188,
	johannes.schindelin, toon

On 13/03/2026 05:40, Siddharth Asthana wrote:
> 
> Changes in v4:
> - Replaced sequencer_format_revert_header() with a more complete
>    sequencer_format_revert_message() that handles everything: subject
>    prefix, commit reference via refer_to_commit(), and merge-parent
>    references -- per Phillip
> - Updated refer_to_commit() signature to take (struct repository *r,
>    bool use_commit_reference) instead of (struct replay_opts *opts)
> - Reverts are now newest-first (revs.reverse = 0 for --revert),
>    chaining on last_commit rather than the parent mapping
> - Changed doc example to cross-branch scenario and restored the
>    merge-tree NOTE
> - Updated error message format to "'--revert' cannot be used with
>    multiple revision ranges..." (and same for --advance)
> - Empty revert commits are now dropped, consistent with cherry-pick

This looks good to me. I've left a couple of nitpicking comments but 
they're mainly for future reference - I'd be happy to see this merged as is.

Thanks

Phillip

> - Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/
> - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/
> - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/
> 
> Thanks,
> Siddharth
> 
> ---
> Siddharth Asthana (2):
>    sequencer: extract revert message formatting into shared function
>    replay: add --revert mode to reverse commit changes
> 
>   Documentation/git-replay.adoc |  43 ++++++++-
>   builtin/replay.c              |  46 ++++++----
>   replay.c                      | 165 ++++++++++++++++++++++++----------
>   replay.h                      |  11 ++-
>   sequencer.c                   |  78 +++++++++-------
>   sequencer.h                   |  14 +++
>   t/t3650-replay-basics.sh      | 114 +++++++++++++++++++++--
>   7 files changed, 364 insertions(+), 107 deletions(-)
> 
> Range-diff versus v3:
> 
> 1:  9d686bcdfe ! 1:  bdc710b265 sequencer: extract revert message formatting into shared function
>      @@ Commit message
>           sequencer: extract revert message formatting into shared function
>       
>           The logic for formatting revert commit messages (handling "Revert" and
>      -    "Reapply" cases) is currently duplicated between sequencer.c and will be
>      -    needed by builtin/replay.c.
>      +    "Reapply" cases, appending "This reverts commit <ref>.", and handling
>      +    merge-parent references) currently lives inline in do_pick_commit().
>      +    The upcoming replay --revert mode needs to reuse this logic.
>       
>      -    Extract this logic into a new sequencer_format_revert_header() function
>      -    that can be shared. The function handles both regular reverts ("Revert
>      -    "<subject>"") and revert-of-revert cases ("Reapply "<subject>"").
>      -    When an oid is provided, the function appends the full commit hash and
>      -    period; otherwise the caller should append the commit reference.
>      +    Extract all of this into a new sequencer_format_revert_message()
>      +    function. The function takes a repository, the subject line, commit,
>      +    parent, a use_commit_reference flag, and the output strbuf. It handles
>      +    both regular reverts ("Revert "<subject>"") and revert-of-revert cases
>      +    ("Reapply "<subject>""), and uses refer_to_commit() internally to
>      +    format the commit reference.
>       
>      -    Update do_pick_commit() to use the new helper, eliminating code
>      -    duplication while preserving the special handling for commit_use_reference.
>      +    Update refer_to_commit() to take a struct repository parameter instead
>      +    of relying on the_repository, and a bool instead of reading from
>      +    replay_opts directly. This makes it usable from the new shared function
>      +    without pulling in sequencer-specific state.
>       
>           Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>       
>        ## sequencer.c ##
>      +@@ sequencer.c: static int should_edit(struct replay_opts *opts) {
>      + 	return opts->edit;
>      + }
>      +
>      +-static void refer_to_commit(struct replay_opts *opts,
>      +-			    struct strbuf *msgbuf, struct commit *commit)
>      ++static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,
>      ++			    const struct commit *commit,
>      ++			    bool use_commit_reference)
>      + {
>      +-	if (opts->commit_use_reference) {
>      ++	if (use_commit_reference) {
>      + 		struct pretty_print_context ctx = {
>      + 			.abbrev = DEFAULT_ABBREV,
>      + 			.date_mode.type = DATE_SHORT,
>      + 		};
>      +-		repo_format_commit_message(the_repository, commit,
>      ++		repo_format_commit_message(r, commit,
>      + 					   "%h (%s, %ad)", msgbuf, &ctx);
>      + 	} else {
>      + 		strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));
>       @@ sequencer.c: static int do_pick_commit(struct repository *r,
>        	 */
>        
>      @@ sequencer.c: static int do_pick_commit(struct repository *r,
>        		base = commit;
>        		base_label = msg.label;
>        		next = parent;
>      -@@ sequencer.c: static int do_pick_commit(struct repository *r,
>      - 		if (opts->commit_use_reference) {
>      - 			strbuf_commented_addf(&ctx->message, comment_line_str,
>      - 				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
>      + 		next_label = msg.parent_label;
>      +-		if (opts->commit_use_reference) {
>      +-			strbuf_commented_addf(&ctx->message, comment_line_str,
>      +-				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
>       -		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
>       -			   /*
>       -			    * We don't touch pre-existing repeated reverts, because
>      @@ sequencer.c: static int do_pick_commit(struct repository *r,
>       -			strbuf_addstr(&ctx->message, "Reapply \"");
>       -			strbuf_addstr(&ctx->message, orig_subject);
>       -			strbuf_addstr(&ctx->message, "\n");
>      -+			strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>      - 		} else {
>      +-		} else {
>       -			strbuf_addstr(&ctx->message, "Revert \"");
>       -			strbuf_addstr(&ctx->message, msg.subject);
>       -			strbuf_addstr(&ctx->message, "\"\n");
>      -+			sequencer_format_revert_header(&ctx->message, msg.subject, NULL);
>      - 		}
>      +-		}
>       -		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
>      - 		refer_to_commit(opts, &ctx->message, commit);
>      +-		refer_to_commit(opts, &ctx->message, commit);
>      +-
>      +-		if (commit->parents && commit->parents->next) {
>      +-			strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
>      +-			refer_to_commit(opts, &ctx->message, parent);
>      +-		}
>      +-		strbuf_addstr(&ctx->message, ".\n");
>      ++		sequencer_format_revert_message(r, msg.subject, commit,
>      ++						parent,
>      ++						opts->commit_use_reference,
>      ++						&ctx->message);
>      + 	} else {
>      + 		const char *p;
>        
>      - 		if (commit->parents && commit->parents->next) {
>       @@ sequencer.c: int sequencer_pick_revisions(struct repository *r,
>        	return res;
>        }
>        
>      -+void sequencer_format_revert_header(struct strbuf *out,
>      -+				    const char *orig_subject,
>      -+				    const struct object_id *oid)
>      ++void sequencer_format_revert_message(struct repository *r,
>      ++				     const char *subject,
>      ++				     const struct commit *commit,
>      ++				     const struct commit *parent,
>      ++				     bool use_commit_reference,
>      ++				     struct strbuf *message)
>       +{
>      -+	const char *revert_subject;
>      ++	const char *orig_subject;
>       +
>      -+	if (skip_prefix(orig_subject, "Revert \"", &revert_subject) &&
>      -+	    /*
>      -+	     * We don't touch pre-existing repeated reverts, because
>      -+	     * theoretically these can be nested arbitrarily deeply,
>      -+	     * thus requiring excessive complexity to deal with.
>      -+	     */
>      -+	    !starts_with(revert_subject, "Revert \"")) {
>      -+		strbuf_addstr(out, "Reapply \"");
>      -+		strbuf_addstr(out, revert_subject);
>      -+		strbuf_addch(out, '\n');
>      ++	if (use_commit_reference) {
>      ++		strbuf_commented_addf(message, comment_line_str,
>      ++				      "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
>      ++	} else if (skip_prefix(subject, "Revert \"", &orig_subject) &&
>      ++		   /*
>      ++		    * We don't touch pre-existing repeated reverts, because
>      ++		    * theoretically these can be nested arbitrarily deeply,
>      ++		    * thus requiring excessive complexity to deal with.
>      ++		    */
>      ++		   !starts_with(orig_subject, "Revert \"")) {
>      ++		strbuf_addstr(message, "Reapply \"");
>      ++		strbuf_addstr(message, orig_subject);
>      ++		strbuf_addstr(message, "\n");
>       +	} else {
>      -+		strbuf_addstr(out, "Revert \"");
>      -+		strbuf_addstr(out, orig_subject);
>      -+		strbuf_addstr(out, "\"\n");
>      ++		strbuf_addstr(message, "Revert \"");
>      ++		strbuf_addstr(message, subject);
>      ++		strbuf_addstr(message, "\"\n");
>       +	}
>      ++	strbuf_addstr(message, "\nThis reverts commit ");
>      ++	refer_to_commit(r, message, commit, use_commit_reference);
>       +
>      -+	strbuf_addstr(out, "\nThis reverts commit ");
>      -+	if (oid) {
>      -+		strbuf_addstr(out, oid_to_hex(oid));
>      -+		strbuf_addstr(out, ".\n");
>      ++	if (commit->parents && commit->parents->next) {
>      ++		strbuf_addstr(message, ", reversing\nchanges made to ");
>      ++		refer_to_commit(r, message, parent, use_commit_reference);
>       +	}
>      ++	strbuf_addstr(message, ".\n");
>       +}
>       +
>        void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
>      @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh
>        int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
>        
>       +/*
>      -+ * Formats a revert commit message following standard Git conventions.
>      -+ * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert
>      -+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit <oid>." if oid
>      -+ * is provided, otherwise just adds "This reverts commit " and the caller
>      -+ * should append the commit reference.
>      ++ * Formats a complete revert commit message following standard Git conventions.
>      ++ * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases
>      ++ * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts
>      ++ * commit <ref>." using either the abbreviated or full commit reference
>      ++ * depending on use_commit_reference. Also handles merge-parent references.
>       + */
>      -+void sequencer_format_revert_header(struct strbuf *out,
>      -+				    const char *orig_subject,
>      -+				    const struct object_id *oid);
>      ++void sequencer_format_revert_message(struct repository *r,
>      ++				     const char *subject,
>      ++				     const struct commit *commit,
>      ++				     const struct commit *parent,
>      ++				     bool use_commit_reference,
>      ++				     struct strbuf *message);
>       +
>        #endif /* SEQUENCER_H */
> 2:  066269706e ! 2:  bea6229575 replay: add --revert mode to reverse commit changes
>      @@ Commit message
>           We swap the base and pickme trees passed to merge_incore_nonrecursive()
>           to reverse the diff direction.
>       
>      +    Reverts are processed newest-first (matching git revert behavior) to
>      +    reduce conflicts by peeling off changes from the top. Each revert
>      +    builds on the result of the previous one via the last_commit fallback
>      +    in the main replay loop, rather than relying on the parent-mapping
>      +    used for cherry-pick.
>      +
>           Revert commit messages follow the usual git revert conventions: prefixed
>           with "Revert" (or "Reapply" when reverting a revert), and including
>           "This reverts commit <hash>.". The author is set to the current user
>      @@ Commit message
>           Helped-by: Phillip Wood <phillip.wood123@gmail.com>
>           Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
>           Helped-by: Junio C Hamano <gitster@pobox.com>
>      +    Helped-by: Toon Claes <toon@iotcl.com>
>           Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>       
>        ## Documentation/git-replay.adoc ##
>      @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them
>       +To revert commits on a branch:
>       +
>       +------------
>      -+$ git replay --revert main main~2..main
>      ++$ git replay --revert main topic~2..topic
>       +------------
>       +
>      -+This reverts the last two commits on `main`, creating two revert commits
>      -+on top of `main`, and updates `main` to point at the result.
>      ++This reverts the last two commits from `topic`, creating revert commits on
>      ++top of `main`, and updates `main` to point at the result. This is useful when
>      ++commits from `topic` 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.
>       +
>        GIT
>        ---
>      @@ builtin/replay.c: int cmd_replay(int argc,
>        
>        	/* Parse ref action mode from command line or config */
>        	ref_mode = get_ref_action_mode(repo, ref_action);
>      +@@ builtin/replay.c: int cmd_replay(int argc,
>      + 	 * some options changing these values if we think they could
>      + 	 * be useful.
>      + 	 */
>      +-	revs.reverse = 1;
>      ++	/*
>      ++	 * Cherry-pick/rebase need oldest-first ordering so that each
>      ++	 * replayed commit can build on its already-replayed parent.
>      ++	 * Revert needs newest-first ordering (like git revert) to
>      ++	 * reduce conflicts by peeling off changes from the top.
>      ++	 */
>      ++	revs.reverse = opts.revert ? 0 : 1;
>      + 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
>      + 	revs.topo_order = 1;
>      + 	revs.simplify_history = 0;
>      +@@ builtin/replay.c: int cmd_replay(int argc,
>      + 	 * Detect and warn if we override some user specified rev
>      + 	 * walking options.
>      + 	 */
>      +-	if (revs.reverse != 1) {
>      +-		warning(_("some rev walking options will be overridden as "
>      +-			  "'%s' bit in 'struct rev_info' will be forced"),
>      +-			"reverse");
>      +-		revs.reverse = 1;
>      ++	{
>      ++		int desired_reverse = opts.revert ? 0 : 1;
>      ++		if (revs.reverse != desired_reverse) {
>      ++			warning(_("some rev walking options will be overridden as "
>      ++				  "'%s' bit in 'struct rev_info' will be forced"),
>      ++				"reverse");
>      ++			revs.reverse = desired_reverse;
>      ++		}
>      + 	}
>      + 	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
>      + 		warning(_("some rev walking options will be overridden as "
>       @@ builtin/replay.c: int cmd_replay(int argc,
>        		goto cleanup;
>        
>      @@ replay.c
>        #include "strmap.h"
>        #include "tree.h"
>        
>      +-/*
>      +- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but
>      +- * do not want to use the_repository.
>      +- */
>      +-#define the_repository DO_NOT_USE_THE_REPOSITORY
>       +enum replay_mode {
>       +	REPLAY_MODE_PICK,
>       +	REPLAY_MODE_REVERT,
>       +};
>      -+
>      +
>        static const char *short_commit_name(struct repository *repo,
>        				     struct commit *commit)
>      - {
>       @@ replay.c: static char *get_author(const char *message)
>        	return NULL;
>        }
>      @@ replay.c: static char *get_author(const char *message)
>       +	subject_len = find_commit_subject(message, &subject_start);
>       +	subject = xmemdupz(subject_start, subject_len);
>       +
>      -+	sequencer_format_revert_header(msg, subject, &commit->object.oid);
>      ++	sequencer_format_revert_message(repo, subject, commit,
>      ++					commit->parents ? commit->parents->item : NULL,
>      ++					false, msg);
>       +
>       +	free(subject);
>       +	repo_unuse_commit_buffer(repo, commit, message);
>      @@ replay.c: static void get_ref_information(struct repository *repo,
>       +	}
>       +	*onto = peel_committish(repo, *branch_name, option_name);
>       +	if (rinfo->positive_refexprs > 1)
>      -+		die(_("cannot %s target with multiple sources because ordering would be ill-defined"),
>      -+		    option_name + 2); /* skip "--" prefix */
>      ++		die(_("'%s' cannot be used with multiple revision ranges "
>      ++		      "because the ordering would be ill-defined"),
>      ++		    option_name);
>       +}
>       +
>        static void set_up_replay_mode(struct repository *repo,
>      @@ replay.c: static struct commit *pick_regular_commit(struct repository *repo,
>       +	merge_opt->branch2 = NULL;
>        	if (!result->clean)
>        		return NULL;
>      --	/* Drop commits that become empty */
>      --	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
>      -+	/* Drop commits that become empty (only for picks) */
>      -+	if (mode == REPLAY_MODE_PICK &&
>      -+	    oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
>      + 	/* Drop commits that become empty */
>      + 	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
>        	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
>        		return replayed_base;
>       -	return create_commit(repo, result->tree, pickme, replayed_base);
>      @@ replay.c: int replay_revisions(struct rev_info *revs,
>        
>        		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
>       -						  onto, &merge_opt, &result);
>      -+						  onto, &merge_opt, &result, mode);
>      ++						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
>      ++						  &merge_opt, &result, mode);
>        		if (!last_commit)
>        			break;
>        
>      @@ t/t3650-replay-basics.sh: test_expect_success 'no base or negative ref gives no-
>        	test_must_fail git replay --advance=main --contained \
>        		topic1..topic2 2>actual &&
>        	test_cmp expect actual
>      + '
>      +
>      + test_expect_success 'cannot advance target ... ordering would be ill-defined' '
>      +-	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
>      ++	cat >expect <<-\EOF &&
>      ++	fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
>      ++	EOF
>      + 	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
>      + 	test_cmp expect actual
>      + '
>       @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '
>        	test_grep "invalid.*replay.refAction.*value" error
>        '
>      @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '
>       +'
>       +
>       +test_expect_success 'cannot revert with multiple sources' '
>      -+	echo "fatal: cannot revert target with multiple sources because ordering would be ill-defined" >expect &&
>      ++	cat >expect <<-\EOF &&
>      ++	fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
>      ++	EOF
>       +	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
>       +	test_cmp expect actual
>       +'
>      @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '
>       +	# Revert commits I and J
>       +	git replay --revert topic4 topic4~2..topic4 &&
>       +
>      -+	# Verify the revert commits were created
>      ++	# Verify the revert commits were created (newest-first ordering
>      ++	# means J is reverted first, then I on top)
>       +	git log --format=%s -4 topic4 >actual &&
>       +	cat >expect <<-\EOF &&
>      -+	Revert "J"
>       +	Revert "I"
>      ++	Revert "J"
>       +	J
>       +	I
>       +	EOF
>       +	test_cmp expect actual &&
>       +
>      -+	# Verify commit message format includes hash
>      ++	# Verify commit message format includes hash (tip is Revert "I")
>       +	test_commit_message topic4 <<-EOF &&
>      -+	Revert "J"
>      ++	Revert "I"
>       +
>      -+	This reverts commit $(git rev-parse J).
>      ++	This reverts commit $(git rev-parse I).
>       +	EOF
>       +
>       +	# Verify reflog message
> 
> 
> base-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2
> 
> 


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v4 1/2] sequencer: extract revert message formatting into shared function
  2026-03-13 15:53         ` Junio C Hamano
@ 2026-03-16 19:12           ` Toon Claes
  0 siblings, 0 replies; 92+ messages in thread
From: Toon Claes @ 2026-03-16 19:12 UTC (permalink / raw)
  To: Junio C Hamano, Siddharth Asthana
  Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188,
	johannes.schindelin

Junio C Hamano <gitster@pobox.com> writes:

> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> The logic for formatting revert commit messages (handling "Revert" and
>> "Reapply" cases, appending "This reverts commit <ref>.", and handling
>> merge-parent references) currently lives inline in do_pick_commit().
>> The upcoming replay --revert mode needs to reuse this logic.
>>
>> Extract all of this into a new sequencer_format_revert_message()
>> function. The function takes a repository, the subject line, commit,
>> parent, a use_commit_reference flag, and the output strbuf. It handles
>> both regular reverts ("Revert "<subject>"") and revert-of-revert cases
>> ("Reapply "<subject>""), and uses refer_to_commit() internally to
>> format the commit reference.
>>
>> Update refer_to_commit() to take a struct repository parameter instead
>> of relying on the_repository, and a bool instead of reading from
>> replay_opts directly. This makes it usable from the new shared function
>> without pulling in sequencer-specific state.

I wouldn't mind if you put removing the use of `the_repository` in a
separate commit.

>>
>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>> ---
>>  sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------
>>  sequencer.h | 14 ++++++++++
>>  2 files changed, 60 insertions(+), 32 deletions(-)
>
> Relative to the previous round, sequencer_format_revert_message()
> that does a bit more than sequencer_format_revert_header() we had
> makes the existing code easier to follow, even though the total
> codeflow amounts to the same thing.  A new caller that will use the
> function now has to do less.
>
> Also, even though this is an internal implementation detail,
> changing the list of parameters refer_to_commit() takes makes it
> easier to understand which part of the replay_opts structure is used
> (i.e., we only care about "do we use the commit reference, or not?"
> bit, and we have no interest in any other members of the struct).

Patrick suggested[1] to use flags, but it makes sense to keep it simple
for now.

[1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/

>
>> +/*
>> + * Formats a complete revert commit message following standard Git conventions.
>> + * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases
>> + * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts
>> + * commit <ref>." using either the abbreviated or full commit reference
>> + * depending on use_commit_reference. Also handles merge-parent references.
>> + */

I think you're trying to put too much in the comments here. I would
suggest to be a bit more concise. Maybe something like:

/*
 * Format a revert commit message with appropriate "Revert" or "Reapply"
 * prefix and "This reverts commit <ref>." body. When use_commit_reference
 * is set, <ref> is an abbreviated hash with subject and date.
 */

>> +void sequencer_format_revert_message(struct repository *r,
>> +				     const char *subject,
>> +				     const struct commit *commit,
>> +				     const struct commit *parent,
>> +				     bool use_commit_reference,
>> +				     struct strbuf *message);
>> +
>>  #endif /* SEQUENCER_H */
>
> OK.
>

-- 
Cheers,
Toon

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v4 2/2] replay: add --revert mode to reverse commit changes
  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
  1 sibling, 1 reply; 92+ messages in thread
From: Toon Claes @ 2026-03-16 19:52 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, Siddharth Asthana,
	Johannes Schindelin

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> Add a `--revert <branch>` mode to git replay that undoes the changes
> introduced by the specified commits. Like --onto and --advance, --revert
> is a standalone mode: it takes a branch argument and updates that branch
> with the newly created revert commits.
>
> At GitLab, we need this in Gitaly for reverting commits directly on bare
> repositories without requiring a working tree checkout.
>
> The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick
> and revert are just the same three-way merge with swapped arguments:
>
>   - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
>   - Revert: merge(ancestor=commit, ours=current, theirs=parent)
>
> We swap the base and pickme trees passed to merge_incore_nonrecursive()
> to reverse the diff direction.
>
> Reverts are processed newest-first (matching git revert behavior) to
> reduce conflicts by peeling off changes from the top. Each revert
> builds on the result of the previous one via the last_commit fallback
> in the main replay loop, rather than relying on the parent-mapping
> used for cherry-pick.
>
> Revert commit messages follow the usual git revert conventions: prefixed
> with "Revert" (or "Reapply" when reverting a revert), and including
> "This reverts commit <hash>.". The author is set to the current user
> rather than preserving the original author, matching git revert behavior.
>
> Helped-by: Christian Couder <christian.couder@gmail.com>
> Helped-by: Patrick Steinhardt <ps@pks.im>
> Helped-by: Elijah Newren <newren@gmail.com>
> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
> Helped-by: Junio C Hamano <gitster@pobox.com>
> Helped-by: Toon Claes <toon@iotcl.com>
> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> ---
>  Documentation/git-replay.adoc |  43 ++++++++-
>  builtin/replay.c              |  46 ++++++----
>  replay.c                      | 165 ++++++++++++++++++++++++----------
>  replay.h                      |  11 ++-
>  t/t3650-replay-basics.sh      | 114 +++++++++++++++++++++--
>  5 files changed, 304 insertions(+), 75 deletions(-)
>
> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> index 8d696ce3ab..6698cfc047 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>...

The ellipsis (...) should be dropped from revision-range. They were
removed not so long ago in 136f86abc0 (Documentation/git-replay.adoc:
fix errors around revision range, 2025-11-29).

>  
>  DESCRIPTION
>  -----------
> @@ -42,6 +42,25 @@ 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 reverted commits; must be a
> +	branch name.
> ++
> +When `--revert` is specified, the commits in the revision range are reverted
> +(their changes are undone) and the reverted commits are created on top of
> +<branch>. The <branch> is then updated to point at the new commits. This is
> +the same as running `git revert <revision-range>` but does not update the
> +working tree.
> ++
> +The commit messages follow `git revert` conventions: they are prefixed with
> +"Revert" and include "This reverts commit <hash>." When reverting a commit
> +whose message starts with "Revert", the new message uses "Reapply" instead.
> +Unlike cherry-pick which preserves the original author, revert commits use
> +the current user as the author, matching the behavior of `git revert`.
> ++
> +This option is mutually exclusive with `--onto` and `--advance`. It is also
> +incompatible with `--contained` (which is a modifier for `--onto` only).
> +
>  --contained::
>  	Update all branches that point at commits in
>  	<revision-range>. Requires `--onto`.
> @@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to
>  	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
>  
>  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).
> +the shape of the history being replayed.  When using `--advance` or
> +`--revert`, the number of refs updated is always one, but for `--onto`,
> +it can be one or more (rebasing multiple branches simultaneously is
> +supported).
>  
>  There is no stderr output on conflicts; see the <<exit-status,EXIT
>  STATUS>> section below.
> @@ -152,6 +172,21 @@ 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 on a branch:
> +
> +------------
> +$ git replay --revert main topic~2..topic
> +------------
> +
> +This reverts the last two commits from `topic`, creating revert commits on
> +top of `main`, and updates `main` to point at the result. This is useful when
> +commits from `topic` 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.
> +
>  GIT
>  ---
>  Part of the linkgit:git[1] suite
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 2cdde830a8..fe69f6f8ce 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -83,8 +83,8 @@ int cmd_replay(int argc,
>  
>  	const char *const replay_usage[] = {
>  		N_("(EXPERIMENTAL!) git replay "
> -		   "([--contained] --onto <newbase> | --advance <branch>) "
> -		   "[--ref-action[=<mode>]] <revision-range>"),
> +		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
> +		   "[--ref-action[=<mode>]] <revision-range>..."),
>  		NULL
>  	};
>  	struct option replay_options[] = {
> @@ -96,6 +96,9 @@ int cmd_replay(int argc,
>  			   N_("replay onto given commit")),
>  		OPT_BOOL(0, "contained", &opts.contained,
>  			 N_("update all branches that point at commits in <revision-range>")),
> +		OPT_STRING(0, "revert", &opts.revert,
> +			   N_("branch"),
> +			   N_("revert commits onto given branch")),
>  		OPT_STRING(0, "ref-action", &ref_action,
>  			   N_("mode"),
>  			   N_("control ref update behavior (update|print)")),
> @@ -105,15 +108,17 @@ int cmd_replay(int argc,
>  	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
>  			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
>  
> -	if (!opts.onto && !opts.advance) {
> -		error(_("option --onto or --advance is mandatory"));
> +	/* Exactly one mode must be specified */
> +	if (!opts.onto && !opts.advance && !opts.revert) {
> +		error(_("exactly one of --onto, --advance, or --revert is required"));
>  		usage_with_options(replay_usage, replay_options);
>  	}
>  
> -	die_for_incompatible_opt2(!!opts.advance, "--advance",
> -				  opts.contained, "--contained");
> -	die_for_incompatible_opt2(!!opts.advance, "--advance",
> -				  !!opts.onto, "--onto");
> +	die_for_incompatible_opt3(!!opts.onto, "--onto",
> +				  !!opts.advance, "--advance",
> +				  !!opts.revert, "--revert");
> +	if (opts.contained && !opts.onto)
> +		die(_("--contained requires --onto"));

I think it would be more clear if you say they cannot be used together:

	die_for_incompatible_opt2(!!opts.advance, "--advance",
				  opts.contained, "--contained");
	die_for_incompatible_opt2(!!opts.revert, "--revert",
				  opts.contained, "--contained");


>  	/* Parse ref action mode from command line or config */
>  	ref_mode = get_ref_action_mode(repo, ref_action);
> @@ -129,7 +134,13 @@ int cmd_replay(int argc,
>  	 * some options changing these values if we think they could
>  	 * be useful.
>  	 */
> -	revs.reverse = 1;
> +	/*
> +	 * Cherry-pick/rebase need oldest-first ordering so that each
> +	 * replayed commit can build on its already-replayed parent.
> +	 * Revert needs newest-first ordering (like git revert) to
> +	 * reduce conflicts by peeling off changes from the top.
> +	 */
> +	revs.reverse = opts.revert ? 0 : 1;
>  	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
>  	revs.topo_order = 1;
>  	revs.simplify_history = 0;
> @@ -144,11 +155,14 @@ int cmd_replay(int argc,
>  	 * Detect and warn if we override some user specified rev
>  	 * walking options.
>  	 */
> -	if (revs.reverse != 1) {
> -		warning(_("some rev walking options will be overridden as "
> -			  "'%s' bit in 'struct rev_info' will be forced"),
> -			"reverse");
> -		revs.reverse = 1;
> +	{

Do we want to keep these braces?

> +		int desired_reverse = opts.revert ? 0 : 1;
> +		if (revs.reverse != desired_reverse) {
> +			warning(_("some rev walking options will be overridden as "
> +				  "'%s' bit in 'struct rev_info' will be forced"),
> +				"reverse");
> +			revs.reverse = desired_reverse;
> +		}
>  	}
>  	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
>  		warning(_("some rev walking options will be overridden as "
> @@ -174,7 +188,9 @@ int cmd_replay(int argc,
>  		goto cleanup;
>  
>  	/* Build reflog message */
> -	if (opts.advance) {
> +	if (opts.revert) {
> +		strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
> +	} else if (opts.advance) {
>  		strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
>  	} else {
>  		struct object_id oid;
> diff --git a/replay.c b/replay.c
> index a63f6714c4..199066f6b3 100644
> --- a/replay.c
> +++ b/replay.c
> @@ -8,14 +8,14 @@
>  #include "refs.h"
>  #include "replay.h"
>  #include "revision.h"
> +#include "sequencer.h"
>  #include "strmap.h"
>  #include "tree.h"
>  
> -/*
> - * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but
> - * do not want to use the_repository.
> - */
> -#define the_repository DO_NOT_USE_THE_REPOSITORY

Why are you removing this? We're still setting
USE_THE_REPOSITORY_VARIABLE, and not using 'the_repository' in this file?

> +enum replay_mode {
> +	REPLAY_MODE_PICK,
> +	REPLAY_MODE_REVERT,
> +};
>  
>  static const char *short_commit_name(struct repository *repo,
>  				     struct commit *commit)
> @@ -50,15 +50,37 @@ static char *get_author(const char *message)
>  	return NULL;
>  }
>  
> +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_message(repo, subject, commit,
> +					commit->parents ? commit->parents->item : NULL,
> +					false, msg);
> +
> +	free(subject);
> +	repo_unuse_commit_buffer(repo, commit, message);
> +}
> +
>  static struct commit *create_commit(struct repository *repo,
>  				    struct tree *tree,
>  				    struct commit *based_on,
> -				    struct commit *parent)
> +				    struct commit *parent,
> +				    enum replay_mode mode)
>  {
>  	struct object_id ret;
>  	struct object *obj = NULL;
>  	struct commit_list *parents = NULL;
> -	char *author;
> +	char *author = NULL;
>  	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
>  	struct commit_extra_header *extra = NULL;
>  	struct strbuf msg = STRBUF_INIT;
> @@ -70,9 +92,16 @@ 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 (mode == REPLAY_MODE_REVERT) {
> +		generate_revert_message(&msg, based_on, repo);
> +		/* For revert, use current user as author (NULL = use default) */
> +	} else if (mode == REPLAY_MODE_PICK) {
> +		find_commit_subject(message, &orig_message);
> +		strbuf_addstr(&msg, orig_message);
> +		author = get_author(message);
> +	} else {
> +		BUG("unexpected replay mode %d", mode);
> +	}
>  	reset_ident_date();
>  	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
>  				 &ret, author, NULL, sign_commit, extra)) {
> @@ -153,11 +182,35 @@ static void get_ref_information(struct repository *repo,
>  	}
>  }
>  
> +static void set_up_branch_mode(struct repository *repo,
> +			       char **branch_name,
> +			       const char *option_name,
> +			       struct ref_info *rinfo,
> +			       struct commit **onto)
> +{
> +	struct object_id oid;
> +	char *fullname = NULL;
> +
> +	if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
> +			  &oid, &fullname, 0) == 1) {
> +		free(*branch_name);
> +		*branch_name = fullname;
> +	} else {
> +		die(_("argument to %s must be a reference"), option_name);
> +	}
> +	*onto = peel_committish(repo, *branch_name, option_name);
> +	if (rinfo->positive_refexprs > 1)
> +		die(_("'%s' cannot be used with multiple revision ranges "
> +		      "because the ordering would be ill-defined"),
> +		    option_name);
> +}
> +
>  static void set_up_replay_mode(struct repository *repo,
>  			       struct rev_cmdline_info *cmd_info,
>  			       const char *onto_name,
>  			       bool *detached_head,
>  			       char **advance_name,
> +			       char **revert_name,
>  			       struct commit **onto,
>  			       struct strset **update_refs)
>  {
> @@ -172,9 +225,6 @@ static void set_up_replay_mode(struct repository *repo,
>  	if (!rinfo.positive_refexprs)
>  		die(_("need some commits to replay"));
>  
> -	if (!onto_name == !*advance_name)
> -		BUG("one and only one of onto_name and *advance_name must be given");
> -
>  	if (onto_name) {
>  		*onto = peel_committish(repo, onto_name, "--onto");
>  		if (rinfo.positive_refexprs <
> @@ -183,23 +233,12 @@ static void set_up_replay_mode(struct repository *repo,
>  		*update_refs = xcalloc(1, sizeof(**update_refs));
>  		**update_refs = rinfo.positive_refs;
>  		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
> +	} else if (*advance_name) {
> +		set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
> +	} else if (*revert_name) {
> +		set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
>  	} else {
> -		struct object_id oid;
> -		char *fullname = NULL;
> -
> -		if (!*advance_name)
> -			BUG("expected either onto_name or *advance_name in this function");
> -
> -		if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
> -			     &oid, &fullname, 0) == 1) {
> -			free(*advance_name);
> -			*advance_name = fullname;
> -		} else {
> -			die(_("argument to --advance must be a reference"));
> -		}
> -		*onto = peel_committish(repo, *advance_name, "--advance");
> -		if (rinfo.positive_refexprs > 1)
> -			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
> +		BUG("expected one of onto_name, *advance_name, or *revert_name");
>  	}
>  	strset_clear(&rinfo.negative_refs);
>  	strset_clear(&rinfo.positive_refs);
> @@ -220,7 +259,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  					  kh_oid_map_t *replayed_commits,
>  					  struct commit *onto,
>  					  struct merge_options *merge_opt,
> -					  struct merge_result *result)
> +					  struct merge_result *result,
> +					  enum replay_mode mode)
>  {
>  	struct commit *base, *replayed_base;
>  	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
> @@ -232,25 +272,45 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  	pickme_tree = repo_get_commit_tree(repo, pickme);
>  	base_tree = repo_get_commit_tree(repo, base);
>  
> -	merge_opt->branch1 = short_commit_name(repo, replayed_base);
> -	merge_opt->branch2 = short_commit_name(repo, pickme);
> -	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
> -
> -	merge_incore_nonrecursive(merge_opt,
> -				  base_tree,
> -				  replayed_base_tree,
> -				  pickme_tree,
> -				  result);
> -
> -	free((char*)merge_opt->ancestor);
> +	if (mode == REPLAY_MODE_PICK) {
> +		/* Cherry-pick: normal order */
> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
> +		merge_opt->branch2 = short_commit_name(repo, pickme);
> +		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
> +
> +		merge_incore_nonrecursive(merge_opt,
> +					  base_tree,
> +					  replayed_base_tree,
> +					  pickme_tree,
> +					  result);
> +
> +		free((char *)merge_opt->ancestor);
> +	} else if (mode == REPLAY_MODE_REVERT) {
> +		/* Revert: swap base and pickme to reverse the diff */
> +		const char *pickme_name = short_commit_name(repo, pickme);
> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
> +		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
> +		merge_opt->ancestor = pickme_name;
> +
> +		merge_incore_nonrecursive(merge_opt,
> +					  pickme_tree,
> +					  replayed_base_tree,
> +					  base_tree,
> +					  result);
> +
> +		free((char *)merge_opt->branch2);
> +	} else {
> +		BUG("unexpected replay mode %d", mode);
> +	}
>  	merge_opt->ancestor = NULL;
> +	merge_opt->branch2 = NULL;

It took me a while to understand the ownership of these buffers,
especially those returned by short_commit_name(). But it seems to call
repo_find_unique_abbrev() which has 4 buffers which it cycles through.

So this seems to be okay (this was also not changed in the MODE_PICK
path. 

>  	if (!result->clean)
>  		return NULL;
>  	/* Drop commits that become empty */
>  	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
>  	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
>  		return replayed_base;
> -	return create_commit(repo, result->tree, pickme, replayed_base);
> +	return create_commit(repo, result->tree, pickme, replayed_base, mode);
>  }
>  
>  void replay_result_release(struct replay_result *result)
> @@ -287,11 +347,16 @@ int replay_revisions(struct rev_info *revs,
>  	};
>  	bool detached_head;
>  	char *advance;
> +	char *revert;
> +	enum replay_mode mode = REPLAY_MODE_PICK;
>  	int ret;
>  
>  	advance = xstrdup_or_null(opts->advance);
> +	revert = xstrdup_or_null(opts->revert);
> +	if (revert)
> +		mode = REPLAY_MODE_REVERT;
>  	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
> -			   &detached_head, &advance, &onto, &update_refs);
> +			   &detached_head, &advance, &revert, &onto, &update_refs);
>  
>  	/* FIXME: Should allow replaying commits with the first as a root commit */
>  
> @@ -315,7 +380,8 @@ int replay_revisions(struct rev_info *revs,
>  			die(_("replaying merge commits is not supported yet!"));
>  
>  		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
> -						  onto, &merge_opt, &result);
> +						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
> +						  &merge_opt, &result, mode);
>  		if (!last_commit)
>  			break;
>  
> @@ -327,7 +393,7 @@ int replay_revisions(struct rev_info *revs,
>  		kh_value(replayed_commits, pos) = last_commit;
>  
>  		/* Update any necessary branches */
> -		if (advance)
> +		if (advance || revert)
>  			continue;
>  
>  		for (decoration = get_name_decoration(&commit->object);
> @@ -361,11 +427,13 @@ int replay_revisions(struct rev_info *revs,
>  		goto out;
>  	}
>  
> -	/* In --advance mode, advance the target ref */
> -	if (advance)
> -		replay_result_queue_update(out, advance,
> +	/* In --advance or --revert mode, update the target ref */
> +	if (advance || revert) {
> +		const char *ref = advance ? advance : revert;
> +		replay_result_queue_update(out, ref,
>  					   &onto->object.oid,
>  					   &last_commit->object.oid);
> +	}
>  
>  	ret = 0;
>  
> @@ -377,5 +445,6 @@ int replay_revisions(struct rev_info *revs,
>  	kh_destroy_oid_map(replayed_commits);
>  	merge_finalize(&merge_opt, &result);
>  	free(advance);
> +	free(revert);
>  	return ret;
>  }
> diff --git a/replay.h b/replay.h
> index d8407dc7f7..e916a5f975 100644
> --- a/replay.h
> +++ b/replay.h
> @@ -13,7 +13,7 @@ struct replay_revisions_options {
>  	/*
>  	 * Starting point at which to create the new commits; must be a branch
>  	 * name. The branch will be updated to point to the rewritten commits.
> -	 * This option is mutually exclusive with `onto`.
> +	 * This option is mutually exclusive with `onto` and `revert`.
>  	 */
>  	const char *advance;
>  
> @@ -22,7 +22,14 @@ struct replay_revisions_options {
>  	 * committish. References pointing at decendants of `onto` will be
>  	 * updated to point to the new commits.
>  	 */
> -	 const char *onto;
> +	const char *onto;
> +
> +	/*
> +	 * Starting point at which to create revert commits; must be a branch
> +	 * name. The branch will be updated to point to the revert commits.
> +	 * This option is mutually exclusive with `onto` and `advance`.
> +	 */
> +	const char *revert;
>  
>  	/*
>  	 * Update branches that point at commits in the given revision range.
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index a03f8f9293..0c1e03e0fb 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '
>  	test_cmp expect actual
>  '
>  
> -test_expect_success 'option --onto or --advance is mandatory' '
> -	echo "error: option --onto or --advance is mandatory" >expect &&
> +test_expect_success 'exactly one of --onto, --advance, or --revert is required' '
> +	echo "error: exactly one of --onto, --advance, or --revert is required" >expect &&
>  	test_might_fail git replay -h >>expect &&
>  	test_must_fail git replay topic1..topic2 2>actual &&
>  	test_cmp expect actual
> @@ -87,16 +87,17 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err
>  	test_cmp expect actual
>  '
>  
> -test_expect_success 'options --advance and --contained cannot be used together' '
> -	printf "fatal: options ${SQ}--advance${SQ} " >expect &&
> -	printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
> +test_expect_success '--contained requires --onto' '
> +	echo "fatal: --contained requires --onto" >expect &&
>  	test_must_fail git replay --advance=main --contained \
>  		topic1..topic2 2>actual &&
>  	test_cmp expect actual
>  '
>  
>  test_expect_success 'cannot advance target ... ordering would be ill-defined' '
> -	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
> +	cat >expect <<-\EOF &&
> +	fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
> +	EOF
>  	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
>  	test_cmp expect actual
>  '
> @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' '
>  	test_grep "invalid.*replay.refAction.*value" error
>  '
>  
> +test_expect_success 'argument to --revert must be a reference' '
> +	echo "fatal: argument to --revert must be a reference" >expect &&
> +	oid=$(git rev-parse main) &&
> +	test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'cannot revert with multiple sources' '
> +	cat >expect <<-\EOF &&
> +	fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
> +	EOF
> +	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'using replay --revert to revert commits' '
> +	# Reuse existing topic4 branch (has commits I and J on top of main)
> +	START=$(git rev-parse topic4) &&
> +	test_when_finished "git branch -f topic4 $START" &&
> +
> +	# Revert commits I and J
> +	git replay --revert topic4 topic4~2..topic4 &&
> +
> +	# Verify the revert commits were created (newest-first ordering
> +	# means J is reverted first, then I on top)
> +	git log --format=%s -4 topic4 >actual &&
> +	cat >expect <<-\EOF &&
> +	Revert "I"
> +	Revert "J"
> +	J
> +	I
> +	EOF
> +	test_cmp expect actual &&
> +
> +	# Verify commit message format includes hash (tip is Revert "I")
> +	test_commit_message topic4 <<-EOF &&
> +	Revert "I"
> +
> +	This reverts commit $(git rev-parse I).
> +	EOF
> +
> +	# Verify reflog message
> +	git reflog topic4 -1 --format=%gs >reflog-msg &&
> +	echo "replay --revert topic4" >expect-reflog &&
> +	test_cmp expect-reflog reflog-msg
> +'
> +
> +test_expect_success 'using replay --revert in bare repo' '
> +	# Reuse existing topic4 in bare repo
> +	START=$(git -C bare rev-parse topic4) &&
> +	test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
> +
> +	# Revert commit J in bare repo
> +	git -C bare replay --revert topic4 topic4~1..topic4 &&
> +
> +	# Verify revert was created
> +	git -C bare log -1 --format=%s topic4 >actual &&
> +	echo "Revert \"J\"" >expect &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'revert of revert uses Reapply' '
> +	# Use topic4 and first revert J, then revert the revert
> +	START=$(git rev-parse topic4) &&
> +	test_when_finished "git branch -f topic4 $START" &&
> +
> +	# First revert J
> +	git replay --revert topic4 topic4~1..topic4 &&
> +	REVERT_J=$(git rev-parse topic4) &&
> +
> +	# Now revert the revert - should become Reapply
> +	git replay --revert topic4 topic4~1..topic4 &&
> +
> +	# Verify Reapply prefix and message format
> +	test_commit_message topic4 <<-EOF
> +	Reapply "J"
> +
> +	This reverts commit $REVERT_J.
> +	EOF
> +'
> +
> +test_expect_success 'git replay --revert with conflict' '
> +	# conflict branch has C.conflict which conflicts with topic1s C
> +	test_expect_code 1 git replay --revert conflict B..topic1
> +'
> +
> +test_expect_success 'git replay --revert incompatible with --contained' '
> +	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
> +	test_grep "requires --onto" error
> +'
> +
> +test_expect_success 'git replay --revert incompatible with --onto' '
> +	test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
> +	test_grep "cannot be used together" error
> +'
> +
> +test_expect_success 'git replay --revert incompatible with --advance' '
> +	test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
> +	test_grep "cannot be used together" error
> +'
> +
>  test_done
> -- 
> 2.51.0
>
>

-- 
Cheers,
Toon

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v4 0/2] replay: add --revert mode to reverse commit changes
  2026-03-13  5:40     ` [PATCH v4 0/2] " Siddharth Asthana
                         ` (2 preceding siblings ...)
  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
  4 siblings, 0 replies; 92+ messages in thread
From: Toon Claes @ 2026-03-16 19:53 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, Siddharth Asthana

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> Hi,
>
> git replay currently supports cherry-picking (--advance) and rebasing
> (--onto), but not reverting. We need this at GitLab for Gitaly to
> reverse commits directly on bare repositories without a checkout.
>
> The approach is the same as sequencer.c -- cherry-pick and revert are
> just the same three-way merge with swapped arguments. We swap the base
> and pickme trees passed to merge_incore_nonrecursive() to reverse the
> diff direction.
>
> Patch 1 extracts the full revert message formatting logic into a new
> sequencer_format_revert_message() function, following Phillip's
> suggestion to move everything into one shared function rather than
> just the header. refer_to_commit() is updated to take a struct
> repository and a bool instead of replay_opts so it works outside the
> sequencer.
>
> Patch 2 adds --revert <branch> as a standalone mode. Reverts are
> processed newest-first (matching git revert) to reduce conflicts by
> peeling off changes from the top.
>
> The series is based on top of d181b9354c (The 13th batch, 2026-03-07).
>
> CI: https://gitlab.com/gitlab-org/git/-/pipelines/2329880894
> The msvc-meson / Chocolatey failures are pre-existing infrastructure
> issues unrelated to this series.
>
> Changes in v4:
> - Replaced sequencer_format_revert_header() with a more complete
>   sequencer_format_revert_message() that handles everything: subject
>   prefix, commit reference via refer_to_commit(), and merge-parent
>   references -- per Phillip
> - Updated refer_to_commit() signature to take (struct repository *r,
>   bool use_commit_reference) instead of (struct replay_opts *opts)
> - Reverts are now newest-first (revs.reverse = 0 for --revert),
>   chaining on last_commit rather than the parent mapping
> - Changed doc example to cross-branch scenario and restored the
>   merge-tree NOTE
> - Updated error message format to "'--revert' cannot be used with
>   multiple revision ranges..." (and same for --advance)
> - Empty revert commits are now dropped, consistent with cherry-pick
> - Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/
> - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/
> - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/
>
> Thanks,
> Siddharth

I've added various nitpicks here and there. Overall nothing really
blocking for a reroll I think, but maybe the combination of them all is.

-- 
Cheers,
Toon

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v4 2/2] replay: add --revert mode to reverse commit changes
  2026-03-16 19:52         ` Toon Claes
@ 2026-03-17 10:11           ` Phillip Wood
  0 siblings, 0 replies; 92+ messages in thread
From: Phillip Wood @ 2026-03-17 10:11 UTC (permalink / raw)
  To: Toon Claes, Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, karthik.188,
	johannes.schindelin

On 16/03/2026 19:52, Toon Claes wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
> 
>> @@ -144,11 +155,14 @@ int cmd_replay(int argc,
>>   	 * Detect and warn if we override some user specified rev
>>   	 * walking options.
>>   	 */
>> -	if (revs.reverse != 1) {
>> -		warning(_("some rev walking options will be overridden as "
>> -			  "'%s' bit in 'struct rev_info' will be forced"),
>> -			"reverse");
>> -		revs.reverse = 1;
>> +	{
> 
> Do we want to keep these braces?

That's a good point which I think is worth a re-roll. It would be better 
to move the declaration of "desired_reverse" up so that we have

	int desired_reverse = !opts.revert;
	...
	revs.reverse = desired_reverse
	...
	if (revs.reverse != desired_reverse)
		warning(...);

That way we know we are using the same expected value for revs.reverse 
when we set it and when we check it hasn't changed later.

Thanks

Phillip

>> +		int desired_reverse = opts.revert ? 0 : 1;
>> +		if (revs.reverse != desired_reverse) {
>> +			warning(_("some rev walking options will be overridden as "
>> +				  "'%s' bit in 'struct rev_info' will be forced"),
>> +				"reverse");
>> +			revs.reverse = desired_reverse;
>> +		}
>>   	}
>>   	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
>>   		warning(_("some rev walking options will be overridden as "
>> @@ -174,7 +188,9 @@ int cmd_replay(int argc,
>>   		goto cleanup;
>>   
>>   	/* Build reflog message */
>> -	if (opts.advance) {
>> +	if (opts.revert) {
>> +		strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
>> +	} else if (opts.advance) {
>>   		strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
>>   	} else {
>>   		struct object_id oid;
>> diff --git a/replay.c b/replay.c
>> index a63f6714c4..199066f6b3 100644
>> --- a/replay.c
>> +++ b/replay.c
>> @@ -8,14 +8,14 @@
>>   #include "refs.h"
>>   #include "replay.h"
>>   #include "revision.h"
>> +#include "sequencer.h"
>>   #include "strmap.h"
>>   #include "tree.h"
>>   
>> -/*
>> - * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but
>> - * do not want to use the_repository.
>> - */
>> -#define the_repository DO_NOT_USE_THE_REPOSITORY
> 
> Why are you removing this? We're still setting
> USE_THE_REPOSITORY_VARIABLE, and not using 'the_repository' in this file?
> 
>> +enum replay_mode {
>> +	REPLAY_MODE_PICK,
>> +	REPLAY_MODE_REVERT,
>> +};
>>   
>>   static const char *short_commit_name(struct repository *repo,
>>   				     struct commit *commit)
>> @@ -50,15 +50,37 @@ static char *get_author(const char *message)
>>   	return NULL;
>>   }
>>   
>> +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_message(repo, subject, commit,
>> +					commit->parents ? commit->parents->item : NULL,
>> +					false, msg);
>> +
>> +	free(subject);
>> +	repo_unuse_commit_buffer(repo, commit, message);
>> +}
>> +
>>   static struct commit *create_commit(struct repository *repo,
>>   				    struct tree *tree,
>>   				    struct commit *based_on,
>> -				    struct commit *parent)
>> +				    struct commit *parent,
>> +				    enum replay_mode mode)
>>   {
>>   	struct object_id ret;
>>   	struct object *obj = NULL;
>>   	struct commit_list *parents = NULL;
>> -	char *author;
>> +	char *author = NULL;
>>   	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
>>   	struct commit_extra_header *extra = NULL;
>>   	struct strbuf msg = STRBUF_INIT;
>> @@ -70,9 +92,16 @@ 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 (mode == REPLAY_MODE_REVERT) {
>> +		generate_revert_message(&msg, based_on, repo);
>> +		/* For revert, use current user as author (NULL = use default) */
>> +	} else if (mode == REPLAY_MODE_PICK) {
>> +		find_commit_subject(message, &orig_message);
>> +		strbuf_addstr(&msg, orig_message);
>> +		author = get_author(message);
>> +	} else {
>> +		BUG("unexpected replay mode %d", mode);
>> +	}
>>   	reset_ident_date();
>>   	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
>>   				 &ret, author, NULL, sign_commit, extra)) {
>> @@ -153,11 +182,35 @@ static void get_ref_information(struct repository *repo,
>>   	}
>>   }
>>   
>> +static void set_up_branch_mode(struct repository *repo,
>> +			       char **branch_name,
>> +			       const char *option_name,
>> +			       struct ref_info *rinfo,
>> +			       struct commit **onto)
>> +{
>> +	struct object_id oid;
>> +	char *fullname = NULL;
>> +
>> +	if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
>> +			  &oid, &fullname, 0) == 1) {
>> +		free(*branch_name);
>> +		*branch_name = fullname;
>> +	} else {
>> +		die(_("argument to %s must be a reference"), option_name);
>> +	}
>> +	*onto = peel_committish(repo, *branch_name, option_name);
>> +	if (rinfo->positive_refexprs > 1)
>> +		die(_("'%s' cannot be used with multiple revision ranges "
>> +		      "because the ordering would be ill-defined"),
>> +		    option_name);
>> +}
>> +
>>   static void set_up_replay_mode(struct repository *repo,
>>   			       struct rev_cmdline_info *cmd_info,
>>   			       const char *onto_name,
>>   			       bool *detached_head,
>>   			       char **advance_name,
>> +			       char **revert_name,
>>   			       struct commit **onto,
>>   			       struct strset **update_refs)
>>   {
>> @@ -172,9 +225,6 @@ static void set_up_replay_mode(struct repository *repo,
>>   	if (!rinfo.positive_refexprs)
>>   		die(_("need some commits to replay"));
>>   
>> -	if (!onto_name == !*advance_name)
>> -		BUG("one and only one of onto_name and *advance_name must be given");
>> -
>>   	if (onto_name) {
>>   		*onto = peel_committish(repo, onto_name, "--onto");
>>   		if (rinfo.positive_refexprs <
>> @@ -183,23 +233,12 @@ static void set_up_replay_mode(struct repository *repo,
>>   		*update_refs = xcalloc(1, sizeof(**update_refs));
>>   		**update_refs = rinfo.positive_refs;
>>   		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
>> +	} else if (*advance_name) {
>> +		set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
>> +	} else if (*revert_name) {
>> +		set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
>>   	} else {
>> -		struct object_id oid;
>> -		char *fullname = NULL;
>> -
>> -		if (!*advance_name)
>> -			BUG("expected either onto_name or *advance_name in this function");
>> -
>> -		if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
>> -			     &oid, &fullname, 0) == 1) {
>> -			free(*advance_name);
>> -			*advance_name = fullname;
>> -		} else {
>> -			die(_("argument to --advance must be a reference"));
>> -		}
>> -		*onto = peel_committish(repo, *advance_name, "--advance");
>> -		if (rinfo.positive_refexprs > 1)
>> -			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
>> +		BUG("expected one of onto_name, *advance_name, or *revert_name");
>>   	}
>>   	strset_clear(&rinfo.negative_refs);
>>   	strset_clear(&rinfo.positive_refs);
>> @@ -220,7 +259,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>   					  kh_oid_map_t *replayed_commits,
>>   					  struct commit *onto,
>>   					  struct merge_options *merge_opt,
>> -					  struct merge_result *result)
>> +					  struct merge_result *result,
>> +					  enum replay_mode mode)
>>   {
>>   	struct commit *base, *replayed_base;
>>   	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
>> @@ -232,25 +272,45 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>   	pickme_tree = repo_get_commit_tree(repo, pickme);
>>   	base_tree = repo_get_commit_tree(repo, base);
>>   
>> -	merge_opt->branch1 = short_commit_name(repo, replayed_base);
>> -	merge_opt->branch2 = short_commit_name(repo, pickme);
>> -	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
>> -
>> -	merge_incore_nonrecursive(merge_opt,
>> -				  base_tree,
>> -				  replayed_base_tree,
>> -				  pickme_tree,
>> -				  result);
>> -
>> -	free((char*)merge_opt->ancestor);
>> +	if (mode == REPLAY_MODE_PICK) {
>> +		/* Cherry-pick: normal order */
>> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
>> +		merge_opt->branch2 = short_commit_name(repo, pickme);
>> +		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
>> +
>> +		merge_incore_nonrecursive(merge_opt,
>> +					  base_tree,
>> +					  replayed_base_tree,
>> +					  pickme_tree,
>> +					  result);
>> +
>> +		free((char *)merge_opt->ancestor);
>> +	} else if (mode == REPLAY_MODE_REVERT) {
>> +		/* Revert: swap base and pickme to reverse the diff */
>> +		const char *pickme_name = short_commit_name(repo, pickme);
>> +		merge_opt->branch1 = short_commit_name(repo, replayed_base);
>> +		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
>> +		merge_opt->ancestor = pickme_name;
>> +
>> +		merge_incore_nonrecursive(merge_opt,
>> +					  pickme_tree,
>> +					  replayed_base_tree,
>> +					  base_tree,
>> +					  result);
>> +
>> +		free((char *)merge_opt->branch2);
>> +	} else {
>> +		BUG("unexpected replay mode %d", mode);
>> +	}
>>   	merge_opt->ancestor = NULL;
>> +	merge_opt->branch2 = NULL;
> 
> It took me a while to understand the ownership of these buffers,
> especially those returned by short_commit_name(). But it seems to call
> repo_find_unique_abbrev() which has 4 buffers which it cycles through.
> 
> So this seems to be okay (this was also not changed in the MODE_PICK
> path.
> 
>>   	if (!result->clean)
>>   		return NULL;
>>   	/* Drop commits that become empty */
>>   	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
>>   	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
>>   		return replayed_base;
>> -	return create_commit(repo, result->tree, pickme, replayed_base);
>> +	return create_commit(repo, result->tree, pickme, replayed_base, mode);
>>   }
>>   
>>   void replay_result_release(struct replay_result *result)
>> @@ -287,11 +347,16 @@ int replay_revisions(struct rev_info *revs,
>>   	};
>>   	bool detached_head;
>>   	char *advance;
>> +	char *revert;
>> +	enum replay_mode mode = REPLAY_MODE_PICK;
>>   	int ret;
>>   
>>   	advance = xstrdup_or_null(opts->advance);
>> +	revert = xstrdup_or_null(opts->revert);
>> +	if (revert)
>> +		mode = REPLAY_MODE_REVERT;
>>   	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
>> -			   &detached_head, &advance, &onto, &update_refs);
>> +			   &detached_head, &advance, &revert, &onto, &update_refs);
>>   
>>   	/* FIXME: Should allow replaying commits with the first as a root commit */
>>   
>> @@ -315,7 +380,8 @@ int replay_revisions(struct rev_info *revs,
>>   			die(_("replaying merge commits is not supported yet!"));
>>   
>>   		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
>> -						  onto, &merge_opt, &result);
>> +						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
>> +						  &merge_opt, &result, mode);
>>   		if (!last_commit)
>>   			break;
>>   
>> @@ -327,7 +393,7 @@ int replay_revisions(struct rev_info *revs,
>>   		kh_value(replayed_commits, pos) = last_commit;
>>   
>>   		/* Update any necessary branches */
>> -		if (advance)
>> +		if (advance || revert)
>>   			continue;
>>   
>>   		for (decoration = get_name_decoration(&commit->object);
>> @@ -361,11 +427,13 @@ int replay_revisions(struct rev_info *revs,
>>   		goto out;
>>   	}
>>   
>> -	/* In --advance mode, advance the target ref */
>> -	if (advance)
>> -		replay_result_queue_update(out, advance,
>> +	/* In --advance or --revert mode, update the target ref */
>> +	if (advance || revert) {
>> +		const char *ref = advance ? advance : revert;
>> +		replay_result_queue_update(out, ref,
>>   					   &onto->object.oid,
>>   					   &last_commit->object.oid);
>> +	}
>>   
>>   	ret = 0;
>>   
>> @@ -377,5 +445,6 @@ int replay_revisions(struct rev_info *revs,
>>   	kh_destroy_oid_map(replayed_commits);
>>   	merge_finalize(&merge_opt, &result);
>>   	free(advance);
>> +	free(revert);
>>   	return ret;
>>   }
>> diff --git a/replay.h b/replay.h
>> index d8407dc7f7..e916a5f975 100644
>> --- a/replay.h
>> +++ b/replay.h
>> @@ -13,7 +13,7 @@ struct replay_revisions_options {
>>   	/*
>>   	 * Starting point at which to create the new commits; must be a branch
>>   	 * name. The branch will be updated to point to the rewritten commits.
>> -	 * This option is mutually exclusive with `onto`.
>> +	 * This option is mutually exclusive with `onto` and `revert`.
>>   	 */
>>   	const char *advance;
>>   
>> @@ -22,7 +22,14 @@ struct replay_revisions_options {
>>   	 * committish. References pointing at decendants of `onto` will be
>>   	 * updated to point to the new commits.
>>   	 */
>> -	 const char *onto;
>> +	const char *onto;
>> +
>> +	/*
>> +	 * Starting point at which to create revert commits; must be a branch
>> +	 * name. The branch will be updated to point to the revert commits.
>> +	 * This option is mutually exclusive with `onto` and `advance`.
>> +	 */
>> +	const char *revert;
>>   
>>   	/*
>>   	 * Update branches that point at commits in the given revision range.
>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index a03f8f9293..0c1e03e0fb 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '
>>   	test_cmp expect actual
>>   '
>>   
>> -test_expect_success 'option --onto or --advance is mandatory' '
>> -	echo "error: option --onto or --advance is mandatory" >expect &&
>> +test_expect_success 'exactly one of --onto, --advance, or --revert is required' '
>> +	echo "error: exactly one of --onto, --advance, or --revert is required" >expect &&
>>   	test_might_fail git replay -h >>expect &&
>>   	test_must_fail git replay topic1..topic2 2>actual &&
>>   	test_cmp expect actual
>> @@ -87,16 +87,17 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err
>>   	test_cmp expect actual
>>   '
>>   
>> -test_expect_success 'options --advance and --contained cannot be used together' '
>> -	printf "fatal: options ${SQ}--advance${SQ} " >expect &&
>> -	printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
>> +test_expect_success '--contained requires --onto' '
>> +	echo "fatal: --contained requires --onto" >expect &&
>>   	test_must_fail git replay --advance=main --contained \
>>   		topic1..topic2 2>actual &&
>>   	test_cmp expect actual
>>   '
>>   
>>   test_expect_success 'cannot advance target ... ordering would be ill-defined' '
>> -	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
>> +	cat >expect <<-\EOF &&
>> +	fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
>> +	EOF
>>   	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
>>   	test_cmp expect actual
>>   '
>> @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' '
>>   	test_grep "invalid.*replay.refAction.*value" error
>>   '
>>   
>> +test_expect_success 'argument to --revert must be a reference' '
>> +	echo "fatal: argument to --revert must be a reference" >expect &&
>> +	oid=$(git rev-parse main) &&
>> +	test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
>> +	test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'cannot revert with multiple sources' '
>> +	cat >expect <<-\EOF &&
>> +	fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
>> +	EOF
>> +	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
>> +	test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'using replay --revert to revert commits' '
>> +	# Reuse existing topic4 branch (has commits I and J on top of main)
>> +	START=$(git rev-parse topic4) &&
>> +	test_when_finished "git branch -f topic4 $START" &&
>> +
>> +	# Revert commits I and J
>> +	git replay --revert topic4 topic4~2..topic4 &&
>> +
>> +	# Verify the revert commits were created (newest-first ordering
>> +	# means J is reverted first, then I on top)
>> +	git log --format=%s -4 topic4 >actual &&
>> +	cat >expect <<-\EOF &&
>> +	Revert "I"
>> +	Revert "J"
>> +	J
>> +	I
>> +	EOF
>> +	test_cmp expect actual &&
>> +
>> +	# Verify commit message format includes hash (tip is Revert "I")
>> +	test_commit_message topic4 <<-EOF &&
>> +	Revert "I"
>> +
>> +	This reverts commit $(git rev-parse I).
>> +	EOF
>> +
>> +	# Verify reflog message
>> +	git reflog topic4 -1 --format=%gs >reflog-msg &&
>> +	echo "replay --revert topic4" >expect-reflog &&
>> +	test_cmp expect-reflog reflog-msg
>> +'
>> +
>> +test_expect_success 'using replay --revert in bare repo' '
>> +	# Reuse existing topic4 in bare repo
>> +	START=$(git -C bare rev-parse topic4) &&
>> +	test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
>> +
>> +	# Revert commit J in bare repo
>> +	git -C bare replay --revert topic4 topic4~1..topic4 &&
>> +
>> +	# Verify revert was created
>> +	git -C bare log -1 --format=%s topic4 >actual &&
>> +	echo "Revert \"J\"" >expect &&
>> +	test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'revert of revert uses Reapply' '
>> +	# Use topic4 and first revert J, then revert the revert
>> +	START=$(git rev-parse topic4) &&
>> +	test_when_finished "git branch -f topic4 $START" &&
>> +
>> +	# First revert J
>> +	git replay --revert topic4 topic4~1..topic4 &&
>> +	REVERT_J=$(git rev-parse topic4) &&
>> +
>> +	# Now revert the revert - should become Reapply
>> +	git replay --revert topic4 topic4~1..topic4 &&
>> +
>> +	# Verify Reapply prefix and message format
>> +	test_commit_message topic4 <<-EOF
>> +	Reapply "J"
>> +
>> +	This reverts commit $REVERT_J.
>> +	EOF
>> +'
>> +
>> +test_expect_success 'git replay --revert with conflict' '
>> +	# conflict branch has C.conflict which conflicts with topic1s C
>> +	test_expect_code 1 git replay --revert conflict B..topic1
>> +'
>> +
>> +test_expect_success 'git replay --revert incompatible with --contained' '
>> +	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
>> +	test_grep "requires --onto" error
>> +'
>> +
>> +test_expect_success 'git replay --revert incompatible with --onto' '
>> +	test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
>> +	test_grep "cannot be used together" error
>> +'
>> +
>> +test_expect_success 'git replay --revert incompatible with --advance' '
>> +	test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
>> +	test_grep "cannot be used together" error
>> +'
>> +
>>   test_done
>> -- 
>> 2.51.0
>>
>>
> 


^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v5 0/2] replay: add --revert mode to reverse commit changes
  2026-03-13  5:40     ` [PATCH v4 0/2] " Siddharth Asthana
                         ` (3 preceding siblings ...)
  2026-03-16 19:53       ` Toon Claes
@ 2026-03-24 22:03       ` Siddharth Asthana
  2026-03-24 22:04         ` [PATCH v5 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
                           ` (2 more replies)
  4 siblings, 3 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-24 22:03 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana

Hi,

git replay currently supports cherry-picking (--advance) and rebasing
(--onto), but not reverting. We need this at GitLab for Gitaly to
reverse commits directly on bare repositories without a checkout.

The approach is the same as sequencer.c -- cherry-pick and revert are
just the same three-way merge with swapped arguments. We swap the base
and pickme trees passed to merge_incore_nonrecursive() to reverse the
diff direction.

Patch 1 extracts the full revert message formatting logic into a new
sequencer_format_revert_message() function that handles everything in
one shared function rather than just the header. refer_to_commit() is
updated to take a struct repository and a bool instead of replay_opts
so it works outside the sequencer.

Patch 2 adds --revert <branch> as a standalone mode. Reverts are
processed newest-first (matching git revert) to reduce conflicts by
peeling off changes from the top.

The series is based on top of d181b9354c (The 13th batch, 2026-03-07).

Changes in v5:
- Made sequencer_format_revert_message() header comment more concise,
  using single quotes to avoid nested escaped double quotes
- Moved desired_reverse declaration up and removed the bare braces, so
  the same named variable is used when setting and checking
  revs.reverse
- Used die_for_incompatible_opt2() for --advance/--contained and
  --revert/--contained instead of die("--contained requires --onto")
- Dropped the erroneously re-added ellipsis from <revision-range>...
  in the SYNOPSIS
- Restored #define the_repository DO_NOT_USE_THE_REPOSITORY guard in
  replay.c
- Used ${SQ} instead of '"'"' for single-quote escaping in tests
- Link to v4: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/
- Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/
- Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/
- Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/

Thanks,
Siddharth

---
Siddharth Asthana (2):
  sequencer: extract revert message formatting into shared function
  replay: add --revert mode to reverse commit changes

 Documentation/git-replay.adoc |  43 ++++++++-
 builtin/replay.c              |  35 ++++++--
 replay.c                      | 161 +++++++++++++++++++++++++---------
 replay.h                      |  11 ++-
 sequencer.c                   |  78 +++++++++-------
 sequencer.h                   |  13 +++
 t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--
 7 files changed, 355 insertions(+), 97 deletions(-)

Range-diff versus v4:

1:  bdc710b265 ! 1:  6bd2ce4515 sequencer: extract revert message formatting into shared function
    @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh
      int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
      
     +/*
    -+ * Formats a complete revert commit message following standard Git conventions.
    -+ * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases
    -+ * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts
    -+ * commit <ref>." using either the abbreviated or full commit reference
    -+ * depending on use_commit_reference. Also handles merge-parent references.
    ++ * Format a revert commit message with appropriate 'Revert "<subject>"' or
    ++ * 'Reapply "<subject>"' prefix and 'This reverts commit <ref>.' body.
    ++ * When use_commit_reference is set, <ref> is an abbreviated hash with
    ++ * subject and date; otherwise the full hex hash is used.
     + */
     +void sequencer_format_revert_message(struct repository *r,
     +				     const char *subject,
2:  bea6229575 ! 2:  9fd92497b9 replay: add --revert mode to reverse commit changes
    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne
      --------
      [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>...
    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>
      
      DESCRIPTION
      -----------
    @@ builtin/replay.c: int cmd_replay(int argc,
      	const char *const replay_usage[] = {
      		N_("(EXPERIMENTAL!) git replay "
     -		   "([--contained] --onto <newbase> | --advance <branch>) "
    --		   "[--ref-action[=<mode>]] <revision-range>"),
     +		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
    -+		   "[--ref-action[=<mode>]] <revision-range>..."),
    + 		   "[--ref-action[=<mode>]] <revision-range>"),
      		NULL
      	};
    - 	struct option replay_options[] = {
     @@ builtin/replay.c: int cmd_replay(int argc,
      			   N_("replay onto given commit")),
      		OPT_BOOL(0, "contained", &opts.contained,
    @@ builtin/replay.c: int cmd_replay(int argc,
      		usage_with_options(replay_usage, replay_options);
      	}
      
    --	die_for_incompatible_opt2(!!opts.advance, "--advance",
    --				  opts.contained, "--contained");
    --	die_for_incompatible_opt2(!!opts.advance, "--advance",
    --				  !!opts.onto, "--onto");
     +	die_for_incompatible_opt3(!!opts.onto, "--onto",
     +				  !!opts.advance, "--advance",
     +				  !!opts.revert, "--revert");
    -+	if (opts.contained && !opts.onto)
    -+		die(_("--contained requires --onto"));
    + 	die_for_incompatible_opt2(!!opts.advance, "--advance",
    + 				  opts.contained, "--contained");
    +-	die_for_incompatible_opt2(!!opts.advance, "--advance",
    +-				  !!opts.onto, "--onto");
    ++	die_for_incompatible_opt2(!!opts.revert, "--revert",
    ++				  opts.contained, "--contained");
      
      	/* Parse ref action mode from command line or config */
      	ref_mode = get_ref_action_mode(repo, ref_action);
    -@@ builtin/replay.c: int cmd_replay(int argc,
    - 	 * some options changing these values if we think they could
    - 	 * be useful.
    - 	 */
    --	revs.reverse = 1;
    + 
     +	/*
     +	 * Cherry-pick/rebase need oldest-first ordering so that each
     +	 * replayed commit can build on its already-replayed parent.
     +	 * Revert needs newest-first ordering (like git revert) to
     +	 * reduce conflicts by peeling off changes from the top.
     +	 */
    -+	revs.reverse = opts.revert ? 0 : 1;
    ++	int desired_reverse = !opts.revert;
    ++
    + 	repo_init_revisions(repo, &revs, prefix);
    + 
    + 	/*
    +@@ builtin/replay.c: int cmd_replay(int argc,
    + 	 * some options changing these values if we think they could
    + 	 * be useful.
    + 	 */
    +-	revs.reverse = 1;
    ++	revs.reverse = desired_reverse;
      	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
      	revs.topo_order = 1;
      	revs.simplify_history = 0;
    @@ builtin/replay.c: int cmd_replay(int argc,
      	 * walking options.
      	 */
     -	if (revs.reverse != 1) {
    --		warning(_("some rev walking options will be overridden as "
    --			  "'%s' bit in 'struct rev_info' will be forced"),
    --			"reverse");
    ++	if (revs.reverse != desired_reverse) {
    + 		warning(_("some rev walking options will be overridden as "
    + 			  "'%s' bit in 'struct rev_info' will be forced"),
    + 			"reverse");
     -		revs.reverse = 1;
    -+	{
    -+		int desired_reverse = opts.revert ? 0 : 1;
    -+		if (revs.reverse != desired_reverse) {
    -+			warning(_("some rev walking options will be overridden as "
    -+				  "'%s' bit in 'struct rev_info' will be forced"),
    -+				"reverse");
    -+			revs.reverse = desired_reverse;
    -+		}
    ++		revs.reverse = desired_reverse;
      	}
      	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
      		warning(_("some rev walking options will be overridden as "
    @@ replay.c
      #include "strmap.h"
      #include "tree.h"
      
    --/*
    -- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but
    -- * do not want to use the_repository.
    -- */
    --#define the_repository DO_NOT_USE_THE_REPOSITORY
    +@@
    +  */
    + #define the_repository DO_NOT_USE_THE_REPOSITORY
    + 
     +enum replay_mode {
     +	REPLAY_MODE_PICK,
     +	REPLAY_MODE_REVERT,
     +};
    - 
    ++
      static const char *short_commit_name(struct repository *repo,
      				     struct commit *commit)
    + {
     @@ replay.c: static char *get_author(const char *message)
      	return NULL;
      }
    @@ t/t3650-replay-basics.sh: test_expect_success 'no base or negative ref gives no-
     -test_expect_success 'options --advance and --contained cannot be used together' '
     -	printf "fatal: options ${SQ}--advance${SQ} " >expect &&
     -	printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
    -+test_expect_success '--contained requires --onto' '
    -+	echo "fatal: --contained requires --onto" >expect &&
    ++test_expect_success '--advance and --contained cannot be used together' '
      	test_must_fail git replay --advance=main --contained \
      		topic1..topic2 2>actual &&
    - 	test_cmp expect actual
    +-	test_cmp expect actual
    ++	test_grep "cannot be used together" actual
      '
      
      test_expect_success 'cannot advance target ... ordering would be ill-defined' '
     -	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
    -+	cat >expect <<-\EOF &&
    -+	fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
    -+	EOF
    ++	echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
      	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
      	test_cmp expect actual
      '
    @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '
     +'
     +
     +test_expect_success 'cannot revert with multiple sources' '
    -+	cat >expect <<-\EOF &&
    -+	fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
    -+	EOF
    ++	echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
     +	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
     +	test_cmp expect actual
     +'
    @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '
     +
     +test_expect_success 'git replay --revert incompatible with --contained' '
     +	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
    -+	test_grep "requires --onto" error
    ++	test_grep "cannot be used together" error
     +'
     +
     +test_expect_success 'git replay --revert incompatible with --onto' '

base-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2
-- 
2.51.0


^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v5 1/2] sequencer: extract revert message formatting into shared function
  2026-03-24 22:03       ` [PATCH v5 " Siddharth Asthana
@ 2026-03-24 22:04         ` Siddharth Asthana
  2026-03-24 22:04         ` [PATCH v5 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
  2026-03-25 20:23         ` [PATCH v6 0/2] " Siddharth Asthana
  2 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-24 22:04 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana

The logic for formatting revert commit messages (handling "Revert" and
"Reapply" cases, appending "This reverts commit <ref>.", and handling
merge-parent references) currently lives inline in do_pick_commit().
The upcoming replay --revert mode needs to reuse this logic.

Extract all of this into a new sequencer_format_revert_message()
function. The function takes a repository, the subject line, commit,
parent, a use_commit_reference flag, and the output strbuf. It handles
both regular reverts ("Revert "<subject>"") and revert-of-revert cases
("Reapply "<subject>""), and uses refer_to_commit() internally to
format the commit reference.

Update refer_to_commit() to take a struct repository parameter instead
of relying on the_repository, and a bool instead of reading from
replay_opts directly. This makes it usable from the new shared function
without pulling in sequencer-specific state.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------
 sequencer.h | 13 +++++++++
 2 files changed, 59 insertions(+), 32 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index aafd0bc959..7bf9d6ad19 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) {
 	return opts->edit;
 }
 
-static void refer_to_commit(struct replay_opts *opts,
-			    struct strbuf *msgbuf, struct commit *commit)
+static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,
+			    const struct commit *commit,
+			    bool use_commit_reference)
 {
-	if (opts->commit_use_reference) {
+	if (use_commit_reference) {
 		struct pretty_print_context ctx = {
 			.abbrev = DEFAULT_ABBREV,
 			.date_mode.type = DATE_SHORT,
 		};
-		repo_format_commit_message(the_repository, commit,
+		repo_format_commit_message(r, commit,
 					   "%h (%s, %ad)", msgbuf, &ctx);
 	} else {
 		strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));
@@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r,
 	 */
 
 	if (command == TODO_REVERT) {
-		const char *orig_subject;
-
 		base = commit;
 		base_label = msg.label;
 		next = parent;
 		next_label = msg.parent_label;
-		if (opts->commit_use_reference) {
-			strbuf_commented_addf(&ctx->message, comment_line_str,
-				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
-		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
-			   /*
-			    * We don't touch pre-existing repeated reverts, because
-			    * theoretically these can be nested arbitrarily deeply,
-			    * thus requiring excessive complexity to deal with.
-			    */
-			   !starts_with(orig_subject, "Revert \"")) {
-			strbuf_addstr(&ctx->message, "Reapply \"");
-			strbuf_addstr(&ctx->message, orig_subject);
-			strbuf_addstr(&ctx->message, "\n");
-		} else {
-			strbuf_addstr(&ctx->message, "Revert \"");
-			strbuf_addstr(&ctx->message, msg.subject);
-			strbuf_addstr(&ctx->message, "\"\n");
-		}
-		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
-		refer_to_commit(opts, &ctx->message, commit);
-
-		if (commit->parents && commit->parents->next) {
-			strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
-			refer_to_commit(opts, &ctx->message, parent);
-		}
-		strbuf_addstr(&ctx->message, ".\n");
+		sequencer_format_revert_message(r, msg.subject, commit,
+						parent,
+						opts->commit_use_reference,
+						&ctx->message);
 	} else {
 		const char *p;
 
@@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r,
 	return res;
 }
 
+void sequencer_format_revert_message(struct repository *r,
+				     const char *subject,
+				     const struct commit *commit,
+				     const struct commit *parent,
+				     bool use_commit_reference,
+				     struct strbuf *message)
+{
+	const char *orig_subject;
+
+	if (use_commit_reference) {
+		strbuf_commented_addf(message, comment_line_str,
+				      "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
+	} else if (skip_prefix(subject, "Revert \"", &orig_subject) &&
+		   /*
+		    * We don't touch pre-existing repeated reverts, because
+		    * theoretically these can be nested arbitrarily deeply,
+		    * thus requiring excessive complexity to deal with.
+		    */
+		   !starts_with(orig_subject, "Revert \"")) {
+		strbuf_addstr(message, "Reapply \"");
+		strbuf_addstr(message, orig_subject);
+		strbuf_addstr(message, "\n");
+	} else {
+		strbuf_addstr(message, "Revert \"");
+		strbuf_addstr(message, subject);
+		strbuf_addstr(message, "\"\n");
+	}
+	strbuf_addstr(message, "\nThis reverts commit ");
+	refer_to_commit(r, message, commit, use_commit_reference);
+
+	if (commit->parents && commit->parents->next) {
+		strbuf_addstr(message, ", reversing\nchanges made to ");
+		refer_to_commit(r, message, parent, use_commit_reference);
+	}
+	strbuf_addstr(message, ".\n");
+}
+
 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
 {
 	unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
diff --git a/sequencer.h b/sequencer.h
index 719684c8a9..56cd50233a 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -271,4 +271,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)
  */
 int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
 
+/*
+ * Format a revert commit message with appropriate 'Revert "<subject>"' or
+ * 'Reapply "<subject>"' prefix and 'This reverts commit <ref>.' body.
+ * When use_commit_reference is set, <ref> is an abbreviated hash with
+ * subject and date; otherwise the full hex hash is used.
+ */
+void sequencer_format_revert_message(struct repository *r,
+				     const char *subject,
+				     const struct commit *commit,
+				     const struct commit *parent,
+				     bool use_commit_reference,
+				     struct strbuf *message);
+
 #endif /* SEQUENCER_H */
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* [PATCH v5 2/2] replay: add --revert mode to reverse commit changes
  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         ` Siddharth Asthana
  2026-03-25  6:29           ` Junio C Hamano
  2026-03-25 20:23         ` [PATCH v6 0/2] " Siddharth Asthana
  2 siblings, 1 reply; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-24 22:04 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana,
	Johannes Schindelin

Add a `--revert <branch>` mode to git replay that undoes the changes
introduced by the specified commits. Like --onto and --advance, --revert
is a standalone mode: it takes a branch argument and updates that branch
with the newly created revert commits.

At GitLab, we need this in Gitaly for reverting commits directly on bare
repositories without requiring a working tree checkout.

The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick
and revert are just the same three-way merge with swapped arguments:

  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
  - Revert: merge(ancestor=commit, ours=current, theirs=parent)

We swap the base and pickme trees passed to merge_incore_nonrecursive()
to reverse the diff direction.

Reverts are processed newest-first (matching git revert behavior) to
reduce conflicts by peeling off changes from the top. Each revert
builds on the result of the previous one via the last_commit fallback
in the main replay loop, rather than relying on the parent-mapping
used for cherry-pick.

Revert commit messages follow the usual git revert conventions: prefixed
with "Revert" (or "Reapply" when reverting a revert), and including
"This reverts commit <hash>.". The author is set to the current user
rather than preserving the original author, matching git revert behavior.

Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  43 ++++++++-
 builtin/replay.c              |  35 ++++++--
 replay.c                      | 161 +++++++++++++++++++++++++---------
 replay.h                      |  11 ++-
 t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--
 5 files changed, 296 insertions(+), 65 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 8d696ce3ab..746312e9c6 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,25 @@ 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 reverted commits; must be a
+	branch name.
++
+When `--revert` is specified, the commits in the revision range are reverted
+(their changes are undone) and the reverted commits are created on top of
+<branch>. The <branch> is then updated to point at the new commits. This is
+the same as running `git revert <revision-range>` but does not update the
+working tree.
++
+The commit messages follow `git revert` conventions: they are prefixed with
+"Revert" and include "This reverts commit <hash>." When reverting a commit
+whose message starts with "Revert", the new message uses "Reapply" instead.
+Unlike cherry-pick which preserves the original author, revert commits use
+the current user as the author, matching the behavior of `git revert`.
++
+This option is mutually exclusive with `--onto` and `--advance`. It is also
+incompatible with `--contained` (which is a modifier for `--onto` only).
+
 --contained::
 	Update all branches that point at commits in
 	<revision-range>. Requires `--onto`.
@@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 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).
+the shape of the history being replayed.  When using `--advance` or
+`--revert`, the number of refs updated is always one, but for `--onto`,
+it can be one or more (rebasing multiple branches simultaneously is
+supported).
 
 There is no stderr output on conflicts; see the <<exit-status,EXIT
 STATUS>> section below.
@@ -152,6 +172,21 @@ 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 on a branch:
+
+------------
+$ git replay --revert main topic~2..topic
+------------
+
+This reverts the last two commits from `topic`, creating revert commits on
+top of `main`, and updates `main` to point at the result. This is useful when
+commits from `topic` 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.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 2cdde830a8..d3c1d920f0 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -83,7 +83,7 @@ int cmd_replay(int argc,
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch>) "
+		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
 		   "[--ref-action[=<mode>]] <revision-range>"),
 		NULL
 	};
@@ -96,6 +96,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &opts.contained,
 			 N_("update all branches that point at commits in <revision-range>")),
+		OPT_STRING(0, "revert", &opts.revert,
+			   N_("branch"),
+			   N_("revert commits onto given branch")),
 		OPT_STRING(0, "ref-action", &ref_action,
 			   N_("mode"),
 			   N_("control ref update behavior (update|print)")),
@@ -105,19 +108,31 @@ int cmd_replay(int argc,
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!opts.onto && !opts.advance) {
-		error(_("option --onto or --advance is mandatory"));
+	/* Exactly one mode must be specified */
+	if (!opts.onto && !opts.advance && !opts.revert) {
+		error(_("exactly one of --onto, --advance, or --revert is required"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	die_for_incompatible_opt3(!!opts.onto, "--onto",
+				  !!opts.advance, "--advance",
+				  !!opts.revert, "--revert");
 	die_for_incompatible_opt2(!!opts.advance, "--advance",
 				  opts.contained, "--contained");
-	die_for_incompatible_opt2(!!opts.advance, "--advance",
-				  !!opts.onto, "--onto");
+	die_for_incompatible_opt2(!!opts.revert, "--revert",
+				  opts.contained, "--contained");
 
 	/* Parse ref action mode from command line or config */
 	ref_mode = get_ref_action_mode(repo, ref_action);
 
+	/*
+	 * Cherry-pick/rebase need oldest-first ordering so that each
+	 * replayed commit can build on its already-replayed parent.
+	 * Revert needs newest-first ordering (like git revert) to
+	 * reduce conflicts by peeling off changes from the top.
+	 */
+	int desired_reverse = !opts.revert;
+
 	repo_init_revisions(repo, &revs, prefix);
 
 	/*
@@ -129,7 +144,7 @@ int cmd_replay(int argc,
 	 * some options changing these values if we think they could
 	 * be useful.
 	 */
-	revs.reverse = 1;
+	revs.reverse = desired_reverse;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
@@ -144,11 +159,11 @@ int cmd_replay(int argc,
 	 * Detect and warn if we override some user specified rev
 	 * walking options.
 	 */
-	if (revs.reverse != 1) {
+	if (revs.reverse != desired_reverse) {
 		warning(_("some rev walking options will be overridden as "
 			  "'%s' bit in 'struct rev_info' will be forced"),
 			"reverse");
-		revs.reverse = 1;
+		revs.reverse = desired_reverse;
 	}
 	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
 		warning(_("some rev walking options will be overridden as "
@@ -174,7 +189,9 @@ int cmd_replay(int argc,
 		goto cleanup;
 
 	/* Build reflog message */
-	if (opts.advance) {
+	if (opts.revert) {
+		strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
+	} else if (opts.advance) {
 		strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
 	} else {
 		struct object_id oid;
diff --git a/replay.c b/replay.c
index a63f6714c4..d7239d4c83 100644
--- a/replay.c
+++ b/replay.c
@@ -8,6 +8,7 @@
 #include "refs.h"
 #include "replay.h"
 #include "revision.h"
+#include "sequencer.h"
 #include "strmap.h"
 #include "tree.h"
 
@@ -17,6 +18,11 @@
  */
 #define the_repository DO_NOT_USE_THE_REPOSITORY
 
+enum replay_mode {
+	REPLAY_MODE_PICK,
+	REPLAY_MODE_REVERT,
+};
+
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
 {
@@ -50,15 +56,37 @@ static char *get_author(const char *message)
 	return NULL;
 }
 
+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_message(repo, subject, commit,
+					commit->parents ? commit->parents->item : NULL,
+					false, msg);
+
+	free(subject);
+	repo_unuse_commit_buffer(repo, commit, message);
+}
+
 static struct commit *create_commit(struct repository *repo,
 				    struct tree *tree,
 				    struct commit *based_on,
-				    struct commit *parent)
+				    struct commit *parent,
+				    enum replay_mode mode)
 {
 	struct object_id ret;
 	struct object *obj = NULL;
 	struct commit_list *parents = NULL;
-	char *author;
+	char *author = NULL;
 	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra = NULL;
 	struct strbuf msg = STRBUF_INIT;
@@ -70,9 +98,16 @@ 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 (mode == REPLAY_MODE_REVERT) {
+		generate_revert_message(&msg, based_on, repo);
+		/* For revert, use current user as author (NULL = use default) */
+	} else if (mode == REPLAY_MODE_PICK) {
+		find_commit_subject(message, &orig_message);
+		strbuf_addstr(&msg, orig_message);
+		author = get_author(message);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	reset_ident_date();
 	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
 				 &ret, author, NULL, sign_commit, extra)) {
@@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo,
 	}
 }
 
+static void set_up_branch_mode(struct repository *repo,
+			       char **branch_name,
+			       const char *option_name,
+			       struct ref_info *rinfo,
+			       struct commit **onto)
+{
+	struct object_id oid;
+	char *fullname = NULL;
+
+	if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
+			  &oid, &fullname, 0) == 1) {
+		free(*branch_name);
+		*branch_name = fullname;
+	} else {
+		die(_("argument to %s must be a reference"), option_name);
+	}
+	*onto = peel_committish(repo, *branch_name, option_name);
+	if (rinfo->positive_refexprs > 1)
+		die(_("'%s' cannot be used with multiple revision ranges "
+		      "because the ordering would be ill-defined"),
+		    option_name);
+}
+
 static void set_up_replay_mode(struct repository *repo,
 			       struct rev_cmdline_info *cmd_info,
 			       const char *onto_name,
 			       bool *detached_head,
 			       char **advance_name,
+			       char **revert_name,
 			       struct commit **onto,
 			       struct strset **update_refs)
 {
@@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo,
 	if (!rinfo.positive_refexprs)
 		die(_("need some commits to replay"));
 
-	if (!onto_name == !*advance_name)
-		BUG("one and only one of onto_name and *advance_name must be given");
-
 	if (onto_name) {
 		*onto = peel_committish(repo, onto_name, "--onto");
 		if (rinfo.positive_refexprs <
@@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo,
 		*update_refs = xcalloc(1, sizeof(**update_refs));
 		**update_refs = rinfo.positive_refs;
 		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	} else if (*advance_name) {
+		set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
+	} else if (*revert_name) {
+		set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
 	} else {
-		struct object_id oid;
-		char *fullname = NULL;
-
-		if (!*advance_name)
-			BUG("expected either onto_name or *advance_name in this function");
-
-		if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
-			     &oid, &fullname, 0) == 1) {
-			free(*advance_name);
-			*advance_name = fullname;
-		} else {
-			die(_("argument to --advance must be a reference"));
-		}
-		*onto = peel_committish(repo, *advance_name, "--advance");
-		if (rinfo.positive_refexprs > 1)
-			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+		BUG("expected one of onto_name, *advance_name, or *revert_name");
 	}
 	strset_clear(&rinfo.negative_refs);
 	strset_clear(&rinfo.positive_refs);
@@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  kh_oid_map_t *replayed_commits,
 					  struct commit *onto,
 					  struct merge_options *merge_opt,
-					  struct merge_result *result)
+					  struct merge_result *result,
+					  enum replay_mode mode)
 {
 	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	pickme_tree = repo_get_commit_tree(repo, pickme);
 	base_tree = repo_get_commit_tree(repo, base);
 
-	merge_opt->branch1 = short_commit_name(repo, replayed_base);
-	merge_opt->branch2 = short_commit_name(repo, pickme);
-	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
-
-	merge_incore_nonrecursive(merge_opt,
-				  base_tree,
-				  replayed_base_tree,
-				  pickme_tree,
-				  result);
-
-	free((char*)merge_opt->ancestor);
+	if (mode == REPLAY_MODE_PICK) {
+		/* Cherry-pick: normal order */
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = short_commit_name(repo, pickme);
+		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+		merge_incore_nonrecursive(merge_opt,
+					  base_tree,
+					  replayed_base_tree,
+					  pickme_tree,
+					  result);
+
+		free((char *)merge_opt->ancestor);
+	} else if (mode == REPLAY_MODE_REVERT) {
+		/* Revert: swap base and pickme to reverse the diff */
+		const char *pickme_name = short_commit_name(repo, pickme);
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
+		merge_opt->ancestor = pickme_name;
+
+		merge_incore_nonrecursive(merge_opt,
+					  pickme_tree,
+					  replayed_base_tree,
+					  base_tree,
+					  result);
+
+		free((char *)merge_opt->branch2);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	merge_opt->ancestor = NULL;
+	merge_opt->branch2 = NULL;
 	if (!result->clean)
 		return NULL;
 	/* Drop commits that become empty */
 	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
 	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
 		return replayed_base;
-	return create_commit(repo, result->tree, pickme, replayed_base);
+	return create_commit(repo, result->tree, pickme, replayed_base, mode);
 }
 
 void replay_result_release(struct replay_result *result)
@@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs,
 	};
 	bool detached_head;
 	char *advance;
+	char *revert;
+	enum replay_mode mode = REPLAY_MODE_PICK;
 	int ret;
 
 	advance = xstrdup_or_null(opts->advance);
+	revert = xstrdup_or_null(opts->revert);
+	if (revert)
+		mode = REPLAY_MODE_REVERT;
 	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
-			   &detached_head, &advance, &onto, &update_refs);
+			   &detached_head, &advance, &revert, &onto, &update_refs);
 
 	/* FIXME: Should allow replaying commits with the first as a root commit */
 
@@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs,
 			die(_("replaying merge commits is not supported yet!"));
 
 		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
-						  onto, &merge_opt, &result);
+						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
+						  &merge_opt, &result, mode);
 		if (!last_commit)
 			break;
 
@@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs,
 		kh_value(replayed_commits, pos) = last_commit;
 
 		/* Update any necessary branches */
-		if (advance)
+		if (advance || revert)
 			continue;
 
 		for (decoration = get_name_decoration(&commit->object);
@@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs,
 		goto out;
 	}
 
-	/* In --advance mode, advance the target ref */
-	if (advance)
-		replay_result_queue_update(out, advance,
+	/* In --advance or --revert mode, update the target ref */
+	if (advance || revert) {
+		const char *ref = advance ? advance : revert;
+		replay_result_queue_update(out, ref,
 					   &onto->object.oid,
 					   &last_commit->object.oid);
+	}
 
 	ret = 0;
 
@@ -377,5 +451,6 @@ int replay_revisions(struct rev_info *revs,
 	kh_destroy_oid_map(replayed_commits);
 	merge_finalize(&merge_opt, &result);
 	free(advance);
+	free(revert);
 	return ret;
 }
diff --git a/replay.h b/replay.h
index d8407dc7f7..e916a5f975 100644
--- a/replay.h
+++ b/replay.h
@@ -13,7 +13,7 @@ struct replay_revisions_options {
 	/*
 	 * Starting point at which to create the new commits; must be a branch
 	 * name. The branch will be updated to point to the rewritten commits.
-	 * This option is mutually exclusive with `onto`.
+	 * This option is mutually exclusive with `onto` and `revert`.
 	 */
 	const char *advance;
 
@@ -22,7 +22,14 @@ struct replay_revisions_options {
 	 * committish. References pointing at decendants of `onto` will be
 	 * updated to point to the new commits.
 	 */
-	 const char *onto;
+	const char *onto;
+
+	/*
+	 * Starting point at which to create revert commits; must be a branch
+	 * name. The branch will be updated to point to the revert commits.
+	 * This option is mutually exclusive with `onto` and `advance`.
+	 */
+	const char *revert;
 
 	/*
 	 * Update branches that point at commits in the given revision range.
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a03f8f9293..217f6fb292 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '
 	test_cmp expect actual
 '
 
-test_expect_success 'option --onto or --advance is mandatory' '
-	echo "error: option --onto or --advance is mandatory" >expect &&
+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '
+	echo "error: exactly one of --onto, --advance, or --revert is required" >expect &&
 	test_might_fail git replay -h >>expect &&
 	test_must_fail git replay topic1..topic2 2>actual &&
 	test_cmp expect actual
@@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err
 	test_cmp expect actual
 '
 
-test_expect_success 'options --advance and --contained cannot be used together' '
-	printf "fatal: options ${SQ}--advance${SQ} " >expect &&
-	printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
+test_expect_success '--advance and --contained cannot be used together' '
 	test_must_fail git replay --advance=main --contained \
 		topic1..topic2 2>actual &&
-	test_cmp expect actual
+	test_grep "cannot be used together" actual
 '
 
 test_expect_success 'cannot advance target ... ordering would be ill-defined' '
-	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
+	echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
 	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
 	test_cmp expect actual
 '
@@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' '
 	test_grep "invalid.*replay.refAction.*value" error
 '
 
+test_expect_success 'argument to --revert must be a reference' '
+	echo "fatal: argument to --revert must be a reference" >expect &&
+	oid=$(git rev-parse main) &&
+	test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'cannot revert with multiple sources' '
+	echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
+	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using replay --revert to revert commits' '
+	# Reuse existing topic4 branch (has commits I and J on top of main)
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# Revert commits I and J
+	git replay --revert topic4 topic4~2..topic4 &&
+
+	# Verify the revert commits were created (newest-first ordering
+	# means J is reverted first, then I on top)
+	git log --format=%s -4 topic4 >actual &&
+	cat >expect <<-\EOF &&
+	Revert "I"
+	Revert "J"
+	J
+	I
+	EOF
+	test_cmp expect actual &&
+
+	# Verify commit message format includes hash (tip is Revert "I")
+	test_commit_message topic4 <<-EOF &&
+	Revert "I"
+
+	This reverts commit $(git rev-parse I).
+	EOF
+
+	# Verify reflog message
+	git reflog topic4 -1 --format=%gs >reflog-msg &&
+	echo "replay --revert topic4" >expect-reflog &&
+	test_cmp expect-reflog reflog-msg
+'
+
+test_expect_success 'using replay --revert in bare repo' '
+	# Reuse existing topic4 in bare repo
+	START=$(git -C bare rev-parse topic4) &&
+	test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
+
+	# Revert commit J in bare repo
+	git -C bare replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify revert was created
+	git -C bare log -1 --format=%s topic4 >actual &&
+	echo "Revert \"J\"" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'revert of revert uses Reapply' '
+	# Use topic4 and first revert J, then revert the revert
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# First revert J
+	git replay --revert topic4 topic4~1..topic4 &&
+	REVERT_J=$(git rev-parse topic4) &&
+
+	# Now revert the revert - should become Reapply
+	git replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify Reapply prefix and message format
+	test_commit_message topic4 <<-EOF
+	Reapply "J"
+
+	This reverts commit $REVERT_J.
+	EOF
+'
+
+test_expect_success 'git replay --revert with conflict' '
+	# conflict branch has C.conflict which conflicts with topic1s C
+	test_expect_code 1 git replay --revert conflict B..topic1
+'
+
+test_expect_success 'git replay --revert incompatible with --contained' '
+	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --onto' '
+	test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --advance' '
+	test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes
  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:36             ` Siddharth Asthana
  0 siblings, 2 replies; 92+ messages in thread
From: Junio C Hamano @ 2026-03-25  6:29 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188,
	johannes.schindelin, toon

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> diff --git a/builtin/replay.c b/builtin/replay.c
> index 2cdde830a8..d3c1d920f0 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -83,7 +83,7 @@ int cmd_replay(int argc,
> ... 
>  	/* Parse ref action mode from command line or config */
>  	ref_mode = get_ref_action_mode(repo, ref_action);
>  
> +	/*
> +	 * Cherry-pick/rebase need oldest-first ordering so that each
> +	 * replayed commit can build on its already-replayed parent.
> +	 * Revert needs newest-first ordering (like git revert) to
> +	 * reduce conflicts by peeling off changes from the top.
> +	 */
> +	int desired_reverse = !opts.revert;
> +

Compiler notices -Werror=declaration-after-statement error here.

>  	repo_init_revisions(repo, &revs, prefix);

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes
  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
  1 sibling, 2 replies; 92+ messages in thread
From: Toon Claes @ 2026-03-25 15:10 UTC (permalink / raw)
  To: Junio C Hamano, Siddharth Asthana
  Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188,
	johannes.schindelin

Junio C Hamano <gitster@pobox.com> writes:

> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index 2cdde830a8..d3c1d920f0 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -83,7 +83,7 @@ int cmd_replay(int argc,
>> ... 
>>  	/* Parse ref action mode from command line or config */
>>  	ref_mode = get_ref_action_mode(repo, ref_action);
>>  
>> +	/*
>> +	 * Cherry-pick/rebase need oldest-first ordering so that each
>> +	 * replayed commit can build on its already-replayed parent.
>> +	 * Revert needs newest-first ordering (like git revert) to
>> +	 * reduce conflicts by peeling off changes from the top.
>> +	 */
>> +	int desired_reverse = !opts.revert;
>> +
>
> Compiler notices -Werror=declaration-after-statement error here.

That's basically the only comment I have on this series.

Except for one micro-hit on the existing docs about <revision-range>:

    <revision-range>::
    	Range of commits to replay; see "Specifying Ranges" in
    	linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
    	range should have a single tip, so that it's clear to which tip the
    	advanced <branch> should point. Any commits in the range whose
    	changes are already present in the branch the commits are being
    	replayed onto will be dropped.

Next to --advance, we should also mention --revert. But that's totally
not worth a reroll and can be addressed in any other later series.

-- 
Cheers,
Toon

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes
  2026-03-25  6:29           ` Junio C Hamano
  2026-03-25 15:10             ` Toon Claes
@ 2026-03-25 15:36             ` Siddharth Asthana
  1 sibling, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-25 15:36 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188,
	johannes.schindelin, toon



On 25/03/26 11:59, Junio C Hamano wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
> 
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index 2cdde830a8..d3c1d920f0 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -83,7 +83,7 @@ int cmd_replay(int argc,
>> ...
>>   	/* Parse ref action mode from command line or config */
>>   	ref_mode = get_ref_action_mode(repo, ref_action);
>>   
>> +	/*
>> +	 * Cherry-pick/rebase need oldest-first ordering so that each
>> +	 * replayed commit can build on its already-replayed parent.
>> +	 * Revert needs newest-first ordering (like git revert) to
>> +	 * reduce conflicts by peeling off changes from the top.
>> +	 */
>> +	int desired_reverse = !opts.revert;
>> +
> 
> Compiler notices -Werror=declaration-after-statement error here.


Ah right, I should have caught that. I have moved the declaration to the 
top of cmd_replay() with other variables and the CI is passing now [1]. 
Will send a v6.

[1] https://gitlab.com/gitlab-org/git/-/pipelines/2406607541

Thanks,
Siddharth

> 
>>   	repo_init_revisions(repo, &revs, prefix);


^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes
  2026-03-25 15:10             ` Toon Claes
@ 2026-03-25 15:38               ` Siddharth Asthana
  2026-03-25 16:44               ` Phillip Wood
  1 sibling, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-25 15:38 UTC (permalink / raw)
  To: Toon Claes, Junio C Hamano
  Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188,
	johannes.schindelin



On 25/03/26 20:40, Toon Claes wrote:
> Junio C Hamano <gitster@pobox.com> writes:
> 
>> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>>
>>> diff --git a/builtin/replay.c b/builtin/replay.c
>>> index 2cdde830a8..d3c1d920f0 100644
>>> --- a/builtin/replay.c
>>> +++ b/builtin/replay.c
>>> @@ -83,7 +83,7 @@ int cmd_replay(int argc,
>>> ...
>>>   	/* Parse ref action mode from command line or config */
>>>   	ref_mode = get_ref_action_mode(repo, ref_action);
>>>   
>>> +	/*
>>> +	 * Cherry-pick/rebase need oldest-first ordering so that each
>>> +	 * replayed commit can build on its already-replayed parent.
>>> +	 * Revert needs newest-first ordering (like git revert) to
>>> +	 * reduce conflicts by peeling off changes from the top.
>>> +	 */
>>> +	int desired_reverse = !opts.revert;
>>> +
>>
>> Compiler notices -Werror=declaration-after-statement error here.
> 
> That's basically the only comment I have on this series.
> 
> Except for one micro-hit on the existing docs about <revision-range>:
> 
>      <revision-range>::
>      	Range of commits to replay; see "Specifying Ranges" in
>      	linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
>      	range should have a single tip, so that it's clear to which tip the
>      	advanced <branch> should point. Any commits in the range whose
>      	changes are already present in the branch the commits are being
>      	replayed onto will be dropped.
> 
> Next to --advance, we should also mention --revert. But that's totally
> not worth a reroll and can be addressed in any other later series.


Yeah thanks for catching this, I will include that in v6 since I am 
rerolling anyway for the declaration fix.

Thanks,
Siddharth

^ permalink raw reply	[flat|nested] 92+ messages in thread

* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes
  2026-03-25 15:10             ` Toon Claes
  2026-03-25 15:38               ` Siddharth Asthana
@ 2026-03-25 16:44               ` Phillip Wood
  1 sibling, 0 replies; 92+ messages in thread
From: Phillip Wood @ 2026-03-25 16:44 UTC (permalink / raw)
  To: Toon Claes, Junio C Hamano, Siddharth Asthana
  Cc: git, christian.couder, ps, newren, karthik.188,
	johannes.schindelin



On 25/03/2026 15:10, Toon Claes wrote:
> Junio C Hamano <gitster@pobox.com> writes:
> 
>> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>>
>>> diff --git a/builtin/replay.c b/builtin/replay.c
>>> index 2cdde830a8..d3c1d920f0 100644
>>> --- a/builtin/replay.c
>>> +++ b/builtin/replay.c
>>> @@ -83,7 +83,7 @@ int cmd_replay(int argc,
>>> ...
>>>   	/* Parse ref action mode from command line or config */
>>>   	ref_mode = get_ref_action_mode(repo, ref_action);
>>>   
>>> +	/*
>>> +	 * Cherry-pick/rebase need oldest-first ordering so that each
>>> +	 * replayed commit can build on its already-replayed parent.
>>> +	 * Revert needs newest-first ordering (like git revert) to
>>> +	 * reduce conflicts by peeling off changes from the top.
>>> +	 */
>>> +	int desired_reverse = !opts.revert;
>>> +
>>
>> Compiler notices -Werror=declaration-after-statement error here.
> 
> That's basically the only comment I have on this series.

Yes, everything else in the range-diff looks good to me

Thanks

Phillip

> Except for one micro-hit on the existing docs about <revision-range>:
> 
>      <revision-range>::
>      	Range of commits to replay; see "Specifying Ranges" in
>      	linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
>      	range should have a single tip, so that it's clear to which tip the
>      	advanced <branch> should point. Any commits in the range whose
>      	changes are already present in the branch the commits are being
>      	replayed onto will be dropped.
> 
> Next to --advance, we should also mention --revert. But that's totally
> not worth a reroll and can be addressed in any other later series.
> 


^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v6 0/2] replay: add --revert mode to reverse commit changes
  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 20:23         ` Siddharth Asthana
  2026-03-25 20:23           ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
                             ` (3 more replies)
  2 siblings, 4 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana

Hi,

git replay currently supports cherry-picking (--advance) and rebasing
(--onto), but not reverting. We need this at GitLab for Gitaly to
reverse commits directly on bare repositories without a checkout.

The approach is the same as sequencer.c -- cherry-pick and revert are
just the same three-way merge with swapped arguments. We swap the base
and pickme trees passed to merge_incore_nonrecursive() to reverse the
diff direction.

Patch 1 extracts the full revert message formatting logic into a new
sequencer_format_revert_message() function that handles everything in
one shared function rather than just the header. refer_to_commit() is
updated to take a struct repository and a bool instead of replay_opts
so it works outside the sequencer.

Patch 2 adds --revert <branch> as a standalone mode. Reverts are
processed newest-first (matching git revert) to reduce conflicts by
peeling off changes from the top.

The series is based on top of d181b9354c (The 13th batch, 2026-03-07).

CI: https://gitlab.com/gitlab-org/git/-/pipelines/2408816732

Changes in v6:
- Moved desired_reverse declaration to the top of cmd_replay() with
  other variable declarations to fix -Wdeclaration-after-statement
- Updated <revision-range> doc to mention --revert alongside --advance
- Link to v5: https://lore.kernel.org/git/20260324220401.47040-1-siddharthasthana31@gmail.com/
- Link to v4: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/
- Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/
- Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/
- Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/

Thanks,
Siddharth

---
Siddharth Asthana (2):
  sequencer: extract revert message formatting into shared function
  replay: add --revert mode to reverse commit changes

 Documentation/git-replay.adoc |  52 +++++++++--
 builtin/replay.c              |  36 ++++++--
 replay.c                      | 161 +++++++++++++++++++++++++---------
 replay.h                      |  11 ++-
 sequencer.c                   |  78 +++++++++-------
 sequencer.h                   |  13 +++
 t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--
 7 files changed, 361 insertions(+), 101 deletions(-)

Range-diff versus v5:

1:  6bd2ce4515 = 1:  6bd2ce4515 sequencer: extract revert message formatting into shared function
2:  9fd92497b9 ! 2:  41fe4861a2 replay: add --revert mode to reverse commit changes
    @@ Documentation/git-replay.adoc: The history is replayed on top of the <branch> an
      --contained::
      	Update all branches that point at commits in
      	<revision-range>. Requires `--onto`.
    +@@ Documentation/git-replay.adoc: The default mode can be configured via the `replay.refAction` configuration vari
    + 
    + <revision-range>::
    + 	Range of commits to replay; see "Specifying Ranges" in
    +-	linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
    +-	range should have a single tip, so that it's clear to which tip the
    +-	advanced <branch> should point. Any commits in the range whose
    +-	changes are already present in the branch the commits are being
    ++	linkgit:git-rev-parse[1]. In `--advance <branch>` or
    ++	`--revert <branch>` mode, the range should have a single tip,
    ++	so that it's clear to which tip the advanced or reverted
    ++	<branch> should point. Any commits in the range whose changes
    ++	are already present in the branch the commits are being
    + 	replayed onto will be dropped.
    + 
    + :git-replay: 1
     @@ Documentation/git-replay.adoc: When using `--ref-action=print`, the output is usable as input to
      	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
      
    @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them
     
      ## builtin/replay.c ##
     @@ builtin/replay.c: int cmd_replay(int argc,
    + 	struct ref_transaction *transaction = NULL;
    + 	struct strbuf transaction_err = STRBUF_INIT;
    + 	struct strbuf reflog_msg = STRBUF_INIT;
    ++	int desired_reverse;
    + 	int ret = 0;
      
      	const char *const replay_usage[] = {
      		N_("(EXPERIMENTAL!) git replay "
    @@ builtin/replay.c: int cmd_replay(int argc,
     +	 * Revert needs newest-first ordering (like git revert) to
     +	 * reduce conflicts by peeling off changes from the top.
     +	 */
    -+	int desired_reverse = !opts.revert;
    ++	desired_reverse = !opts.revert;
     +
      	repo_init_revisions(repo, &revs, prefix);
      

base-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2
-- 
2.51.0


^ permalink raw reply	[flat|nested] 92+ messages in thread

* [PATCH v6 1/2] sequencer: extract revert message formatting into shared function
  2026-03-25 20:23         ` [PATCH v6 0/2] " Siddharth Asthana
@ 2026-03-25 20:23           ` Siddharth Asthana
  2026-03-25 20:23           ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
                             ` (2 subsequent siblings)
  3 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana

The logic for formatting revert commit messages (handling "Revert" and
"Reapply" cases, appending "This reverts commit <ref>.", and handling
merge-parent references) currently lives inline in do_pick_commit().
The upcoming replay --revert mode needs to reuse this logic.

Extract all of this into a new sequencer_format_revert_message()
function. The function takes a repository, the subject line, commit,
parent, a use_commit_reference flag, and the output strbuf. It handles
both regular reverts ("Revert "<subject>"") and revert-of-revert cases
("Reapply "<subject>""), and uses refer_to_commit() internally to
format the commit reference.

Update refer_to_commit() to take a struct repository parameter instead
of relying on the_repository, and a bool instead of reading from
replay_opts directly. This makes it usable from the new shared function
without pulling in sequencer-specific state.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------
 sequencer.h | 13 +++++++++
 2 files changed, 59 insertions(+), 32 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index aafd0bc959..7bf9d6ad19 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) {
 	return opts->edit;
 }
 
-static void refer_to_commit(struct replay_opts *opts,
-			    struct strbuf *msgbuf, struct commit *commit)
+static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,
+			    const struct commit *commit,
+			    bool use_commit_reference)
 {
-	if (opts->commit_use_reference) {
+	if (use_commit_reference) {
 		struct pretty_print_context ctx = {
 			.abbrev = DEFAULT_ABBREV,
 			.date_mode.type = DATE_SHORT,
 		};
-		repo_format_commit_message(the_repository, commit,
+		repo_format_commit_message(r, commit,
 					   "%h (%s, %ad)", msgbuf, &ctx);
 	} else {
 		strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));
@@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r,
 	 */
 
 	if (command == TODO_REVERT) {
-		const char *orig_subject;
-
 		base = commit;
 		base_label = msg.label;
 		next = parent;
 		next_label = msg.parent_label;
-		if (opts->commit_use_reference) {
-			strbuf_commented_addf(&ctx->message, comment_line_str,
-				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
-		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
-			   /*
-			    * We don't touch pre-existing repeated reverts, because
-			    * theoretically these can be nested arbitrarily deeply,
-			    * thus requiring excessive complexity to deal with.
-			    */
-			   !starts_with(orig_subject, "Revert \"")) {
-			strbuf_addstr(&ctx->message, "Reapply \"");
-			strbuf_addstr(&ctx->message, orig_subject);
-			strbuf_addstr(&ctx->message, "\n");
-		} else {
-			strbuf_addstr(&ctx->message, "Revert \"");
-			strbuf_addstr(&ctx->message, msg.subject);
-			strbuf_addstr(&ctx->message, "\"\n");
-		}
-		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
-		refer_to_commit(opts, &ctx->message, commit);
-
-		if (commit->parents && commit->parents->next) {
-			strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
-			refer_to_commit(opts, &ctx->message, parent);
-		}
-		strbuf_addstr(&ctx->message, ".\n");
+		sequencer_format_revert_message(r, msg.subject, commit,
+						parent,
+						opts->commit_use_reference,
+						&ctx->message);
 	} else {
 		const char *p;
 
@@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r,
 	return res;
 }
 
+void sequencer_format_revert_message(struct repository *r,
+				     const char *subject,
+				     const struct commit *commit,
+				     const struct commit *parent,
+				     bool use_commit_reference,
+				     struct strbuf *message)
+{
+	const char *orig_subject;
+
+	if (use_commit_reference) {
+		strbuf_commented_addf(message, comment_line_str,
+				      "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
+	} else if (skip_prefix(subject, "Revert \"", &orig_subject) &&
+		   /*
+		    * We don't touch pre-existing repeated reverts, because
+		    * theoretically these can be nested arbitrarily deeply,
+		    * thus requiring excessive complexity to deal with.
+		    */
+		   !starts_with(orig_subject, "Revert \"")) {
+		strbuf_addstr(message, "Reapply \"");
+		strbuf_addstr(message, orig_subject);
+		strbuf_addstr(message, "\n");
+	} else {
+		strbuf_addstr(message, "Revert \"");
+		strbuf_addstr(message, subject);
+		strbuf_addstr(message, "\"\n");
+	}
+	strbuf_addstr(message, "\nThis reverts commit ");
+	refer_to_commit(r, message, commit, use_commit_reference);
+
+	if (commit->parents && commit->parents->next) {
+		strbuf_addstr(message, ", reversing\nchanges made to ");
+		refer_to_commit(r, message, parent, use_commit_reference);
+	}
+	strbuf_addstr(message, ".\n");
+}
+
 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
 {
 	unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
diff --git a/sequencer.h b/sequencer.h
index 719684c8a9..56cd50233a 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -271,4 +271,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)
  */
 int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
 
+/*
+ * Format a revert commit message with appropriate 'Revert "<subject>"' or
+ * 'Reapply "<subject>"' prefix and 'This reverts commit <ref>.' body.
+ * When use_commit_reference is set, <ref> is an abbreviated hash with
+ * subject and date; otherwise the full hex hash is used.
+ */
+void sequencer_format_revert_message(struct repository *r,
+				     const char *subject,
+				     const struct commit *commit,
+				     const struct commit *parent,
+				     bool use_commit_reference,
+				     struct strbuf *message);
+
 #endif /* SEQUENCER_H */
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* [PATCH v6 2/2] replay: add --revert mode to reverse commit changes
  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           ` Siddharth Asthana
  2026-03-28  4:33             ` Tian Yuchen
  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
  3 siblings, 1 reply; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana,
	Johannes Schindelin

Add a `--revert <branch>` mode to git replay that undoes the changes
introduced by the specified commits. Like --onto and --advance, --revert
is a standalone mode: it takes a branch argument and updates that branch
with the newly created revert commits.

At GitLab, we need this in Gitaly for reverting commits directly on bare
repositories without requiring a working tree checkout.

The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick
and revert are just the same three-way merge with swapped arguments:

  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
  - Revert: merge(ancestor=commit, ours=current, theirs=parent)

We swap the base and pickme trees passed to merge_incore_nonrecursive()
to reverse the diff direction.

Reverts are processed newest-first (matching git revert behavior) to
reduce conflicts by peeling off changes from the top. Each revert
builds on the result of the previous one via the last_commit fallback
in the main replay loop, rather than relying on the parent-mapping
used for cherry-pick.

Revert commit messages follow the usual git revert conventions: prefixed
with "Revert" (or "Reapply" when reverting a revert), and including
"This reverts commit <hash>.". The author is set to the current user
rather than preserving the original author, matching git revert behavior.

Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  52 +++++++++--
 builtin/replay.c              |  36 ++++++--
 replay.c                      | 161 +++++++++++++++++++++++++---------
 replay.h                      |  11 ++-
 t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--
 5 files changed, 302 insertions(+), 69 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 8d696ce3ab..997097e420 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,25 @@ 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 reverted commits; must be a
+	branch name.
++
+When `--revert` is specified, the commits in the revision range are reverted
+(their changes are undone) and the reverted commits are created on top of
+<branch>. The <branch> is then updated to point at the new commits. This is
+the same as running `git revert <revision-range>` but does not update the
+working tree.
++
+The commit messages follow `git revert` conventions: they are prefixed with
+"Revert" and include "This reverts commit <hash>." When reverting a commit
+whose message starts with "Revert", the new message uses "Reapply" instead.
+Unlike cherry-pick which preserves the original author, revert commits use
+the current user as the author, matching the behavior of `git revert`.
++
+This option is mutually exclusive with `--onto` and `--advance`. It is also
+incompatible with `--contained` (which is a modifier for `--onto` only).
+
 --contained::
 	Update all branches that point at commits in
 	<revision-range>. Requires `--onto`.
@@ -60,10 +79,11 @@ The default mode can be configured via the `replay.refAction` configuration vari
 
 <revision-range>::
 	Range of commits to replay; see "Specifying Ranges" in
-	linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
-	range should have a single tip, so that it's clear to which tip the
-	advanced <branch> should point. Any commits in the range whose
-	changes are already present in the branch the commits are being
+	linkgit:git-rev-parse[1]. In `--advance <branch>` or
+	`--revert <branch>` mode, the range should have a single tip,
+	so that it's clear to which tip the advanced or reverted
+	<branch> should point. Any commits in the range whose changes
+	are already present in the branch the commits are being
 	replayed onto will be dropped.
 
 :git-replay: 1
@@ -84,9 +104,10 @@ When using `--ref-action=print`, the output is usable as input to
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 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).
+the shape of the history being replayed.  When using `--advance` or
+`--revert`, the number of refs updated is always one, but for `--onto`,
+it can be one or more (rebasing multiple branches simultaneously is
+supported).
 
 There is no stderr output on conflicts; see the <<exit-status,EXIT
 STATUS>> section below.
@@ -152,6 +173,21 @@ 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 on a branch:
+
+------------
+$ git replay --revert main topic~2..topic
+------------
+
+This reverts the last two commits from `topic`, creating revert commits on
+top of `main`, and updates `main` to point at the result. This is useful when
+commits from `topic` 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.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 2cdde830a8..a0879b020f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -79,11 +79,12 @@ int cmd_replay(int argc,
 	struct ref_transaction *transaction = NULL;
 	struct strbuf transaction_err = STRBUF_INIT;
 	struct strbuf reflog_msg = STRBUF_INIT;
+	int desired_reverse;
 	int ret = 0;
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch>) "
+		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
 		   "[--ref-action[=<mode>]] <revision-range>"),
 		NULL
 	};
@@ -96,6 +97,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &opts.contained,
 			 N_("update all branches that point at commits in <revision-range>")),
+		OPT_STRING(0, "revert", &opts.revert,
+			   N_("branch"),
+			   N_("revert commits onto given branch")),
 		OPT_STRING(0, "ref-action", &ref_action,
 			   N_("mode"),
 			   N_("control ref update behavior (update|print)")),
@@ -105,19 +109,31 @@ int cmd_replay(int argc,
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!opts.onto && !opts.advance) {
-		error(_("option --onto or --advance is mandatory"));
+	/* Exactly one mode must be specified */
+	if (!opts.onto && !opts.advance && !opts.revert) {
+		error(_("exactly one of --onto, --advance, or --revert is required"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	die_for_incompatible_opt3(!!opts.onto, "--onto",
+				  !!opts.advance, "--advance",
+				  !!opts.revert, "--revert");
 	die_for_incompatible_opt2(!!opts.advance, "--advance",
 				  opts.contained, "--contained");
-	die_for_incompatible_opt2(!!opts.advance, "--advance",
-				  !!opts.onto, "--onto");
+	die_for_incompatible_opt2(!!opts.revert, "--revert",
+				  opts.contained, "--contained");
 
 	/* Parse ref action mode from command line or config */
 	ref_mode = get_ref_action_mode(repo, ref_action);
 
+	/*
+	 * Cherry-pick/rebase need oldest-first ordering so that each
+	 * replayed commit can build on its already-replayed parent.
+	 * Revert needs newest-first ordering (like git revert) to
+	 * reduce conflicts by peeling off changes from the top.
+	 */
+	desired_reverse = !opts.revert;
+
 	repo_init_revisions(repo, &revs, prefix);
 
 	/*
@@ -129,7 +145,7 @@ int cmd_replay(int argc,
 	 * some options changing these values if we think they could
 	 * be useful.
 	 */
-	revs.reverse = 1;
+	revs.reverse = desired_reverse;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
@@ -144,11 +160,11 @@ int cmd_replay(int argc,
 	 * Detect and warn if we override some user specified rev
 	 * walking options.
 	 */
-	if (revs.reverse != 1) {
+	if (revs.reverse != desired_reverse) {
 		warning(_("some rev walking options will be overridden as "
 			  "'%s' bit in 'struct rev_info' will be forced"),
 			"reverse");
-		revs.reverse = 1;
+		revs.reverse = desired_reverse;
 	}
 	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
 		warning(_("some rev walking options will be overridden as "
@@ -174,7 +190,9 @@ int cmd_replay(int argc,
 		goto cleanup;
 
 	/* Build reflog message */
-	if (opts.advance) {
+	if (opts.revert) {
+		strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
+	} else if (opts.advance) {
 		strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
 	} else {
 		struct object_id oid;
diff --git a/replay.c b/replay.c
index a63f6714c4..d7239d4c83 100644
--- a/replay.c
+++ b/replay.c
@@ -8,6 +8,7 @@
 #include "refs.h"
 #include "replay.h"
 #include "revision.h"
+#include "sequencer.h"
 #include "strmap.h"
 #include "tree.h"
 
@@ -17,6 +18,11 @@
  */
 #define the_repository DO_NOT_USE_THE_REPOSITORY
 
+enum replay_mode {
+	REPLAY_MODE_PICK,
+	REPLAY_MODE_REVERT,
+};
+
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
 {
@@ -50,15 +56,37 @@ static char *get_author(const char *message)
 	return NULL;
 }
 
+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_message(repo, subject, commit,
+					commit->parents ? commit->parents->item : NULL,
+					false, msg);
+
+	free(subject);
+	repo_unuse_commit_buffer(repo, commit, message);
+}
+
 static struct commit *create_commit(struct repository *repo,
 				    struct tree *tree,
 				    struct commit *based_on,
-				    struct commit *parent)
+				    struct commit *parent,
+				    enum replay_mode mode)
 {
 	struct object_id ret;
 	struct object *obj = NULL;
 	struct commit_list *parents = NULL;
-	char *author;
+	char *author = NULL;
 	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra = NULL;
 	struct strbuf msg = STRBUF_INIT;
@@ -70,9 +98,16 @@ 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 (mode == REPLAY_MODE_REVERT) {
+		generate_revert_message(&msg, based_on, repo);
+		/* For revert, use current user as author (NULL = use default) */
+	} else if (mode == REPLAY_MODE_PICK) {
+		find_commit_subject(message, &orig_message);
+		strbuf_addstr(&msg, orig_message);
+		author = get_author(message);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	reset_ident_date();
 	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
 				 &ret, author, NULL, sign_commit, extra)) {
@@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo,
 	}
 }
 
+static void set_up_branch_mode(struct repository *repo,
+			       char **branch_name,
+			       const char *option_name,
+			       struct ref_info *rinfo,
+			       struct commit **onto)
+{
+	struct object_id oid;
+	char *fullname = NULL;
+
+	if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
+			  &oid, &fullname, 0) == 1) {
+		free(*branch_name);
+		*branch_name = fullname;
+	} else {
+		die(_("argument to %s must be a reference"), option_name);
+	}
+	*onto = peel_committish(repo, *branch_name, option_name);
+	if (rinfo->positive_refexprs > 1)
+		die(_("'%s' cannot be used with multiple revision ranges "
+		      "because the ordering would be ill-defined"),
+		    option_name);
+}
+
 static void set_up_replay_mode(struct repository *repo,
 			       struct rev_cmdline_info *cmd_info,
 			       const char *onto_name,
 			       bool *detached_head,
 			       char **advance_name,
+			       char **revert_name,
 			       struct commit **onto,
 			       struct strset **update_refs)
 {
@@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo,
 	if (!rinfo.positive_refexprs)
 		die(_("need some commits to replay"));
 
-	if (!onto_name == !*advance_name)
-		BUG("one and only one of onto_name and *advance_name must be given");
-
 	if (onto_name) {
 		*onto = peel_committish(repo, onto_name, "--onto");
 		if (rinfo.positive_refexprs <
@@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo,
 		*update_refs = xcalloc(1, sizeof(**update_refs));
 		**update_refs = rinfo.positive_refs;
 		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	} else if (*advance_name) {
+		set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
+	} else if (*revert_name) {
+		set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
 	} else {
-		struct object_id oid;
-		char *fullname = NULL;
-
-		if (!*advance_name)
-			BUG("expected either onto_name or *advance_name in this function");
-
-		if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
-			     &oid, &fullname, 0) == 1) {
-			free(*advance_name);
-			*advance_name = fullname;
-		} else {
-			die(_("argument to --advance must be a reference"));
-		}
-		*onto = peel_committish(repo, *advance_name, "--advance");
-		if (rinfo.positive_refexprs > 1)
-			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+		BUG("expected one of onto_name, *advance_name, or *revert_name");
 	}
 	strset_clear(&rinfo.negative_refs);
 	strset_clear(&rinfo.positive_refs);
@@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  kh_oid_map_t *replayed_commits,
 					  struct commit *onto,
 					  struct merge_options *merge_opt,
-					  struct merge_result *result)
+					  struct merge_result *result,
+					  enum replay_mode mode)
 {
 	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	pickme_tree = repo_get_commit_tree(repo, pickme);
 	base_tree = repo_get_commit_tree(repo, base);
 
-	merge_opt->branch1 = short_commit_name(repo, replayed_base);
-	merge_opt->branch2 = short_commit_name(repo, pickme);
-	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
-
-	merge_incore_nonrecursive(merge_opt,
-				  base_tree,
-				  replayed_base_tree,
-				  pickme_tree,
-				  result);
-
-	free((char*)merge_opt->ancestor);
+	if (mode == REPLAY_MODE_PICK) {
+		/* Cherry-pick: normal order */
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = short_commit_name(repo, pickme);
+		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+		merge_incore_nonrecursive(merge_opt,
+					  base_tree,
+					  replayed_base_tree,
+					  pickme_tree,
+					  result);
+
+		free((char *)merge_opt->ancestor);
+	} else if (mode == REPLAY_MODE_REVERT) {
+		/* Revert: swap base and pickme to reverse the diff */
+		const char *pickme_name = short_commit_name(repo, pickme);
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
+		merge_opt->ancestor = pickme_name;
+
+		merge_incore_nonrecursive(merge_opt,
+					  pickme_tree,
+					  replayed_base_tree,
+					  base_tree,
+					  result);
+
+		free((char *)merge_opt->branch2);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	merge_opt->ancestor = NULL;
+	merge_opt->branch2 = NULL;
 	if (!result->clean)
 		return NULL;
 	/* Drop commits that become empty */
 	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
 	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
 		return replayed_base;
-	return create_commit(repo, result->tree, pickme, replayed_base);
+	return create_commit(repo, result->tree, pickme, replayed_base, mode);
 }
 
 void replay_result_release(struct replay_result *result)
@@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs,
 	};
 	bool detached_head;
 	char *advance;
+	char *revert;
+	enum replay_mode mode = REPLAY_MODE_PICK;
 	int ret;
 
 	advance = xstrdup_or_null(opts->advance);
+	revert = xstrdup_or_null(opts->revert);
+	if (revert)
+		mode = REPLAY_MODE_REVERT;
 	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
-			   &detached_head, &advance, &onto, &update_refs);
+			   &detached_head, &advance, &revert, &onto, &update_refs);
 
 	/* FIXME: Should allow replaying commits with the first as a root commit */
 
@@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs,
 			die(_("replaying merge commits is not supported yet!"));
 
 		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
-						  onto, &merge_opt, &result);
+						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
+						  &merge_opt, &result, mode);
 		if (!last_commit)
 			break;
 
@@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs,
 		kh_value(replayed_commits, pos) = last_commit;
 
 		/* Update any necessary branches */
-		if (advance)
+		if (advance || revert)
 			continue;
 
 		for (decoration = get_name_decoration(&commit->object);
@@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs,
 		goto out;
 	}
 
-	/* In --advance mode, advance the target ref */
-	if (advance)
-		replay_result_queue_update(out, advance,
+	/* In --advance or --revert mode, update the target ref */
+	if (advance || revert) {
+		const char *ref = advance ? advance : revert;
+		replay_result_queue_update(out, ref,
 					   &onto->object.oid,
 					   &last_commit->object.oid);
+	}
 
 	ret = 0;
 
@@ -377,5 +451,6 @@ int replay_revisions(struct rev_info *revs,
 	kh_destroy_oid_map(replayed_commits);
 	merge_finalize(&merge_opt, &result);
 	free(advance);
+	free(revert);
 	return ret;
 }
diff --git a/replay.h b/replay.h
index d8407dc7f7..e916a5f975 100644
--- a/replay.h
+++ b/replay.h
@@ -13,7 +13,7 @@ struct replay_revisions_options {
 	/*
 	 * Starting point at which to create the new commits; must be a branch
 	 * name. The branch will be updated to point to the rewritten commits.
-	 * This option is mutually exclusive with `onto`.
+	 * This option is mutually exclusive with `onto` and `revert`.
 	 */
 	const char *advance;
 
@@ -22,7 +22,14 @@ struct replay_revisions_options {
 	 * committish. References pointing at decendants of `onto` will be
 	 * updated to point to the new commits.
 	 */
-	 const char *onto;
+	const char *onto;
+
+	/*
+	 * Starting point at which to create revert commits; must be a branch
+	 * name. The branch will be updated to point to the revert commits.
+	 * This option is mutually exclusive with `onto` and `advance`.
+	 */
+	const char *revert;
 
 	/*
 	 * Update branches that point at commits in the given revision range.
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a03f8f9293..217f6fb292 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '
 	test_cmp expect actual
 '
 
-test_expect_success 'option --onto or --advance is mandatory' '
-	echo "error: option --onto or --advance is mandatory" >expect &&
+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '
+	echo "error: exactly one of --onto, --advance, or --revert is required" >expect &&
 	test_might_fail git replay -h >>expect &&
 	test_must_fail git replay topic1..topic2 2>actual &&
 	test_cmp expect actual
@@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err
 	test_cmp expect actual
 '
 
-test_expect_success 'options --advance and --contained cannot be used together' '
-	printf "fatal: options ${SQ}--advance${SQ} " >expect &&
-	printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
+test_expect_success '--advance and --contained cannot be used together' '
 	test_must_fail git replay --advance=main --contained \
 		topic1..topic2 2>actual &&
-	test_cmp expect actual
+	test_grep "cannot be used together" actual
 '
 
 test_expect_success 'cannot advance target ... ordering would be ill-defined' '
-	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
+	echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
 	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
 	test_cmp expect actual
 '
@@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' '
 	test_grep "invalid.*replay.refAction.*value" error
 '
 
+test_expect_success 'argument to --revert must be a reference' '
+	echo "fatal: argument to --revert must be a reference" >expect &&
+	oid=$(git rev-parse main) &&
+	test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'cannot revert with multiple sources' '
+	echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
+	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using replay --revert to revert commits' '
+	# Reuse existing topic4 branch (has commits I and J on top of main)
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# Revert commits I and J
+	git replay --revert topic4 topic4~2..topic4 &&
+
+	# Verify the revert commits were created (newest-first ordering
+	# means J is reverted first, then I on top)
+	git log --format=%s -4 topic4 >actual &&
+	cat >expect <<-\EOF &&
+	Revert "I"
+	Revert "J"
+	J
+	I
+	EOF
+	test_cmp expect actual &&
+
+	# Verify commit message format includes hash (tip is Revert "I")
+	test_commit_message topic4 <<-EOF &&
+	Revert "I"
+
+	This reverts commit $(git rev-parse I).
+	EOF
+
+	# Verify reflog message
+	git reflog topic4 -1 --format=%gs >reflog-msg &&
+	echo "replay --revert topic4" >expect-reflog &&
+	test_cmp expect-reflog reflog-msg
+'
+
+test_expect_success 'using replay --revert in bare repo' '
+	# Reuse existing topic4 in bare repo
+	START=$(git -C bare rev-parse topic4) &&
+	test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
+
+	# Revert commit J in bare repo
+	git -C bare replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify revert was created
+	git -C bare log -1 --format=%s topic4 >actual &&
+	echo "Revert \"J\"" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'revert of revert uses Reapply' '
+	# Use topic4 and first revert J, then revert the revert
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# First revert J
+	git replay --revert topic4 topic4~1..topic4 &&
+	REVERT_J=$(git rev-parse topic4) &&
+
+	# Now revert the revert - should become Reapply
+	git replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify Reapply prefix and message format
+	test_commit_message topic4 <<-EOF
+	Reapply "J"
+
+	This reverts commit $REVERT_J.
+	EOF
+'
+
+test_expect_success 'git replay --revert with conflict' '
+	# conflict branch has C.conflict which conflicts with topic1s C
+	test_expect_code 1 git replay --revert conflict B..topic1
+'
+
+test_expect_success 'git replay --revert incompatible with --contained' '
+	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --onto' '
+	test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --advance' '
+	test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* [PATCH v6 1/2] sequencer: extract revert message formatting into shared function
  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-25 20:23           ` Siddharth Asthana
  2026-03-25 20:23           ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
  3 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana

The logic for formatting revert commit messages (handling "Revert" and
"Reapply" cases, appending "This reverts commit <ref>.", and handling
merge-parent references) currently lives inline in do_pick_commit().
The upcoming replay --revert mode needs to reuse this logic.

Extract all of this into a new sequencer_format_revert_message()
function. The function takes a repository, the subject line, commit,
parent, a use_commit_reference flag, and the output strbuf. It handles
both regular reverts ("Revert "<subject>"") and revert-of-revert cases
("Reapply "<subject>""), and uses refer_to_commit() internally to
format the commit reference.

Update refer_to_commit() to take a struct repository parameter instead
of relying on the_repository, and a bool instead of reading from
replay_opts directly. This makes it usable from the new shared function
without pulling in sequencer-specific state.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------
 sequencer.h | 13 +++++++++
 2 files changed, 59 insertions(+), 32 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index aafd0bc959..7bf9d6ad19 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) {
 	return opts->edit;
 }
 
-static void refer_to_commit(struct replay_opts *opts,
-			    struct strbuf *msgbuf, struct commit *commit)
+static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,
+			    const struct commit *commit,
+			    bool use_commit_reference)
 {
-	if (opts->commit_use_reference) {
+	if (use_commit_reference) {
 		struct pretty_print_context ctx = {
 			.abbrev = DEFAULT_ABBREV,
 			.date_mode.type = DATE_SHORT,
 		};
-		repo_format_commit_message(the_repository, commit,
+		repo_format_commit_message(r, commit,
 					   "%h (%s, %ad)", msgbuf, &ctx);
 	} else {
 		strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));
@@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r,
 	 */
 
 	if (command == TODO_REVERT) {
-		const char *orig_subject;
-
 		base = commit;
 		base_label = msg.label;
 		next = parent;
 		next_label = msg.parent_label;
-		if (opts->commit_use_reference) {
-			strbuf_commented_addf(&ctx->message, comment_line_str,
-				"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
-		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
-			   /*
-			    * We don't touch pre-existing repeated reverts, because
-			    * theoretically these can be nested arbitrarily deeply,
-			    * thus requiring excessive complexity to deal with.
-			    */
-			   !starts_with(orig_subject, "Revert \"")) {
-			strbuf_addstr(&ctx->message, "Reapply \"");
-			strbuf_addstr(&ctx->message, orig_subject);
-			strbuf_addstr(&ctx->message, "\n");
-		} else {
-			strbuf_addstr(&ctx->message, "Revert \"");
-			strbuf_addstr(&ctx->message, msg.subject);
-			strbuf_addstr(&ctx->message, "\"\n");
-		}
-		strbuf_addstr(&ctx->message, "\nThis reverts commit ");
-		refer_to_commit(opts, &ctx->message, commit);
-
-		if (commit->parents && commit->parents->next) {
-			strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
-			refer_to_commit(opts, &ctx->message, parent);
-		}
-		strbuf_addstr(&ctx->message, ".\n");
+		sequencer_format_revert_message(r, msg.subject, commit,
+						parent,
+						opts->commit_use_reference,
+						&ctx->message);
 	} else {
 		const char *p;
 
@@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r,
 	return res;
 }
 
+void sequencer_format_revert_message(struct repository *r,
+				     const char *subject,
+				     const struct commit *commit,
+				     const struct commit *parent,
+				     bool use_commit_reference,
+				     struct strbuf *message)
+{
+	const char *orig_subject;
+
+	if (use_commit_reference) {
+		strbuf_commented_addf(message, comment_line_str,
+				      "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
+	} else if (skip_prefix(subject, "Revert \"", &orig_subject) &&
+		   /*
+		    * We don't touch pre-existing repeated reverts, because
+		    * theoretically these can be nested arbitrarily deeply,
+		    * thus requiring excessive complexity to deal with.
+		    */
+		   !starts_with(orig_subject, "Revert \"")) {
+		strbuf_addstr(message, "Reapply \"");
+		strbuf_addstr(message, orig_subject);
+		strbuf_addstr(message, "\n");
+	} else {
+		strbuf_addstr(message, "Revert \"");
+		strbuf_addstr(message, subject);
+		strbuf_addstr(message, "\"\n");
+	}
+	strbuf_addstr(message, "\nThis reverts commit ");
+	refer_to_commit(r, message, commit, use_commit_reference);
+
+	if (commit->parents && commit->parents->next) {
+		strbuf_addstr(message, ", reversing\nchanges made to ");
+		refer_to_commit(r, message, parent, use_commit_reference);
+	}
+	strbuf_addstr(message, ".\n");
+}
+
 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
 {
 	unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
diff --git a/sequencer.h b/sequencer.h
index 719684c8a9..56cd50233a 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -271,4 +271,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)
  */
 int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
 
+/*
+ * Format a revert commit message with appropriate 'Revert "<subject>"' or
+ * 'Reapply "<subject>"' prefix and 'This reverts commit <ref>.' body.
+ * When use_commit_reference is set, <ref> is an abbreviated hash with
+ * subject and date; otherwise the full hex hash is used.
+ */
+void sequencer_format_revert_message(struct repository *r,
+				     const char *subject,
+				     const struct commit *commit,
+				     const struct commit *parent,
+				     bool use_commit_reference,
+				     struct strbuf *message);
+
 #endif /* SEQUENCER_H */
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* [PATCH v6 2/2] replay: add --revert mode to reverse commit changes
  2026-03-25 20:23         ` [PATCH v6 0/2] " Siddharth Asthana
                             ` (2 preceding siblings ...)
  2026-03-25 20:23           ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana
@ 2026-03-25 20:23           ` Siddharth Asthana
  3 siblings, 0 replies; 92+ messages in thread
From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw)
  To: git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon, Siddharth Asthana,
	Johannes Schindelin

Add a `--revert <branch>` mode to git replay that undoes the changes
introduced by the specified commits. Like --onto and --advance, --revert
is a standalone mode: it takes a branch argument and updates that branch
with the newly created revert commits.

At GitLab, we need this in Gitaly for reverting commits directly on bare
repositories without requiring a working tree checkout.

The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick
and revert are just the same three-way merge with swapped arguments:

  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)
  - Revert: merge(ancestor=commit, ours=current, theirs=parent)

We swap the base and pickme trees passed to merge_incore_nonrecursive()
to reverse the diff direction.

Reverts are processed newest-first (matching git revert behavior) to
reduce conflicts by peeling off changes from the top. Each revert
builds on the result of the previous one via the last_commit fallback
in the main replay loop, rather than relying on the parent-mapping
used for cherry-pick.

Revert commit messages follow the usual git revert conventions: prefixed
with "Revert" (or "Reapply" when reverting a revert), and including
"This reverts commit <hash>.". The author is set to the current user
rather than preserving the original author, matching git revert behavior.

Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  52 +++++++++--
 builtin/replay.c              |  36 ++++++--
 replay.c                      | 161 +++++++++++++++++++++++++---------
 replay.h                      |  11 ++-
 t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--
 5 files changed, 302 insertions(+), 69 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 8d696ce3ab..997097e420 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,25 @@ 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 reverted commits; must be a
+	branch name.
++
+When `--revert` is specified, the commits in the revision range are reverted
+(their changes are undone) and the reverted commits are created on top of
+<branch>. The <branch> is then updated to point at the new commits. This is
+the same as running `git revert <revision-range>` but does not update the
+working tree.
++
+The commit messages follow `git revert` conventions: they are prefixed with
+"Revert" and include "This reverts commit <hash>." When reverting a commit
+whose message starts with "Revert", the new message uses "Reapply" instead.
+Unlike cherry-pick which preserves the original author, revert commits use
+the current user as the author, matching the behavior of `git revert`.
++
+This option is mutually exclusive with `--onto` and `--advance`. It is also
+incompatible with `--contained` (which is a modifier for `--onto` only).
+
 --contained::
 	Update all branches that point at commits in
 	<revision-range>. Requires `--onto`.
@@ -60,10 +79,11 @@ The default mode can be configured via the `replay.refAction` configuration vari
 
 <revision-range>::
 	Range of commits to replay; see "Specifying Ranges" in
-	linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
-	range should have a single tip, so that it's clear to which tip the
-	advanced <branch> should point. Any commits in the range whose
-	changes are already present in the branch the commits are being
+	linkgit:git-rev-parse[1]. In `--advance <branch>` or
+	`--revert <branch>` mode, the range should have a single tip,
+	so that it's clear to which tip the advanced or reverted
+	<branch> should point. Any commits in the range whose changes
+	are already present in the branch the commits are being
 	replayed onto will be dropped.
 
 :git-replay: 1
@@ -84,9 +104,10 @@ When using `--ref-action=print`, the output is usable as input to
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 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).
+the shape of the history being replayed.  When using `--advance` or
+`--revert`, the number of refs updated is always one, but for `--onto`,
+it can be one or more (rebasing multiple branches simultaneously is
+supported).
 
 There is no stderr output on conflicts; see the <<exit-status,EXIT
 STATUS>> section below.
@@ -152,6 +173,21 @@ 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 on a branch:
+
+------------
+$ git replay --revert main topic~2..topic
+------------
+
+This reverts the last two commits from `topic`, creating revert commits on
+top of `main`, and updates `main` to point at the result. This is useful when
+commits from `topic` 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.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index 2cdde830a8..a0879b020f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -79,11 +79,12 @@ int cmd_replay(int argc,
 	struct ref_transaction *transaction = NULL;
 	struct strbuf transaction_err = STRBUF_INIT;
 	struct strbuf reflog_msg = STRBUF_INIT;
+	int desired_reverse;
 	int ret = 0;
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch>) "
+		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
 		   "[--ref-action[=<mode>]] <revision-range>"),
 		NULL
 	};
@@ -96,6 +97,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &opts.contained,
 			 N_("update all branches that point at commits in <revision-range>")),
+		OPT_STRING(0, "revert", &opts.revert,
+			   N_("branch"),
+			   N_("revert commits onto given branch")),
 		OPT_STRING(0, "ref-action", &ref_action,
 			   N_("mode"),
 			   N_("control ref update behavior (update|print)")),
@@ -105,19 +109,31 @@ int cmd_replay(int argc,
 	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
 
-	if (!opts.onto && !opts.advance) {
-		error(_("option --onto or --advance is mandatory"));
+	/* Exactly one mode must be specified */
+	if (!opts.onto && !opts.advance && !opts.revert) {
+		error(_("exactly one of --onto, --advance, or --revert is required"));
 		usage_with_options(replay_usage, replay_options);
 	}
 
+	die_for_incompatible_opt3(!!opts.onto, "--onto",
+				  !!opts.advance, "--advance",
+				  !!opts.revert, "--revert");
 	die_for_incompatible_opt2(!!opts.advance, "--advance",
 				  opts.contained, "--contained");
-	die_for_incompatible_opt2(!!opts.advance, "--advance",
-				  !!opts.onto, "--onto");
+	die_for_incompatible_opt2(!!opts.revert, "--revert",
+				  opts.contained, "--contained");
 
 	/* Parse ref action mode from command line or config */
 	ref_mode = get_ref_action_mode(repo, ref_action);
 
+	/*
+	 * Cherry-pick/rebase need oldest-first ordering so that each
+	 * replayed commit can build on its already-replayed parent.
+	 * Revert needs newest-first ordering (like git revert) to
+	 * reduce conflicts by peeling off changes from the top.
+	 */
+	desired_reverse = !opts.revert;
+
 	repo_init_revisions(repo, &revs, prefix);
 
 	/*
@@ -129,7 +145,7 @@ int cmd_replay(int argc,
 	 * some options changing these values if we think they could
 	 * be useful.
 	 */
-	revs.reverse = 1;
+	revs.reverse = desired_reverse;
 	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
 	revs.topo_order = 1;
 	revs.simplify_history = 0;
@@ -144,11 +160,11 @@ int cmd_replay(int argc,
 	 * Detect and warn if we override some user specified rev
 	 * walking options.
 	 */
-	if (revs.reverse != 1) {
+	if (revs.reverse != desired_reverse) {
 		warning(_("some rev walking options will be overridden as "
 			  "'%s' bit in 'struct rev_info' will be forced"),
 			"reverse");
-		revs.reverse = 1;
+		revs.reverse = desired_reverse;
 	}
 	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
 		warning(_("some rev walking options will be overridden as "
@@ -174,7 +190,9 @@ int cmd_replay(int argc,
 		goto cleanup;
 
 	/* Build reflog message */
-	if (opts.advance) {
+	if (opts.revert) {
+		strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
+	} else if (opts.advance) {
 		strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
 	} else {
 		struct object_id oid;
diff --git a/replay.c b/replay.c
index a63f6714c4..d7239d4c83 100644
--- a/replay.c
+++ b/replay.c
@@ -8,6 +8,7 @@
 #include "refs.h"
 #include "replay.h"
 #include "revision.h"
+#include "sequencer.h"
 #include "strmap.h"
 #include "tree.h"
 
@@ -17,6 +18,11 @@
  */
 #define the_repository DO_NOT_USE_THE_REPOSITORY
 
+enum replay_mode {
+	REPLAY_MODE_PICK,
+	REPLAY_MODE_REVERT,
+};
+
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
 {
@@ -50,15 +56,37 @@ static char *get_author(const char *message)
 	return NULL;
 }
 
+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_message(repo, subject, commit,
+					commit->parents ? commit->parents->item : NULL,
+					false, msg);
+
+	free(subject);
+	repo_unuse_commit_buffer(repo, commit, message);
+}
+
 static struct commit *create_commit(struct repository *repo,
 				    struct tree *tree,
 				    struct commit *based_on,
-				    struct commit *parent)
+				    struct commit *parent,
+				    enum replay_mode mode)
 {
 	struct object_id ret;
 	struct object *obj = NULL;
 	struct commit_list *parents = NULL;
-	char *author;
+	char *author = NULL;
 	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra = NULL;
 	struct strbuf msg = STRBUF_INIT;
@@ -70,9 +98,16 @@ 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 (mode == REPLAY_MODE_REVERT) {
+		generate_revert_message(&msg, based_on, repo);
+		/* For revert, use current user as author (NULL = use default) */
+	} else if (mode == REPLAY_MODE_PICK) {
+		find_commit_subject(message, &orig_message);
+		strbuf_addstr(&msg, orig_message);
+		author = get_author(message);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	reset_ident_date();
 	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
 				 &ret, author, NULL, sign_commit, extra)) {
@@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo,
 	}
 }
 
+static void set_up_branch_mode(struct repository *repo,
+			       char **branch_name,
+			       const char *option_name,
+			       struct ref_info *rinfo,
+			       struct commit **onto)
+{
+	struct object_id oid;
+	char *fullname = NULL;
+
+	if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
+			  &oid, &fullname, 0) == 1) {
+		free(*branch_name);
+		*branch_name = fullname;
+	} else {
+		die(_("argument to %s must be a reference"), option_name);
+	}
+	*onto = peel_committish(repo, *branch_name, option_name);
+	if (rinfo->positive_refexprs > 1)
+		die(_("'%s' cannot be used with multiple revision ranges "
+		      "because the ordering would be ill-defined"),
+		    option_name);
+}
+
 static void set_up_replay_mode(struct repository *repo,
 			       struct rev_cmdline_info *cmd_info,
 			       const char *onto_name,
 			       bool *detached_head,
 			       char **advance_name,
+			       char **revert_name,
 			       struct commit **onto,
 			       struct strset **update_refs)
 {
@@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo,
 	if (!rinfo.positive_refexprs)
 		die(_("need some commits to replay"));
 
-	if (!onto_name == !*advance_name)
-		BUG("one and only one of onto_name and *advance_name must be given");
-
 	if (onto_name) {
 		*onto = peel_committish(repo, onto_name, "--onto");
 		if (rinfo.positive_refexprs <
@@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo,
 		*update_refs = xcalloc(1, sizeof(**update_refs));
 		**update_refs = rinfo.positive_refs;
 		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+	} else if (*advance_name) {
+		set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
+	} else if (*revert_name) {
+		set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
 	} else {
-		struct object_id oid;
-		char *fullname = NULL;
-
-		if (!*advance_name)
-			BUG("expected either onto_name or *advance_name in this function");
-
-		if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
-			     &oid, &fullname, 0) == 1) {
-			free(*advance_name);
-			*advance_name = fullname;
-		} else {
-			die(_("argument to --advance must be a reference"));
-		}
-		*onto = peel_committish(repo, *advance_name, "--advance");
-		if (rinfo.positive_refexprs > 1)
-			die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+		BUG("expected one of onto_name, *advance_name, or *revert_name");
 	}
 	strset_clear(&rinfo.negative_refs);
 	strset_clear(&rinfo.positive_refs);
@@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  kh_oid_map_t *replayed_commits,
 					  struct commit *onto,
 					  struct merge_options *merge_opt,
-					  struct merge_result *result)
+					  struct merge_result *result,
+					  enum replay_mode mode)
 {
 	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	pickme_tree = repo_get_commit_tree(repo, pickme);
 	base_tree = repo_get_commit_tree(repo, base);
 
-	merge_opt->branch1 = short_commit_name(repo, replayed_base);
-	merge_opt->branch2 = short_commit_name(repo, pickme);
-	merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
-
-	merge_incore_nonrecursive(merge_opt,
-				  base_tree,
-				  replayed_base_tree,
-				  pickme_tree,
-				  result);
-
-	free((char*)merge_opt->ancestor);
+	if (mode == REPLAY_MODE_PICK) {
+		/* Cherry-pick: normal order */
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = short_commit_name(repo, pickme);
+		merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+		merge_incore_nonrecursive(merge_opt,
+					  base_tree,
+					  replayed_base_tree,
+					  pickme_tree,
+					  result);
+
+		free((char *)merge_opt->ancestor);
+	} else if (mode == REPLAY_MODE_REVERT) {
+		/* Revert: swap base and pickme to reverse the diff */
+		const char *pickme_name = short_commit_name(repo, pickme);
+		merge_opt->branch1 = short_commit_name(repo, replayed_base);
+		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
+		merge_opt->ancestor = pickme_name;
+
+		merge_incore_nonrecursive(merge_opt,
+					  pickme_tree,
+					  replayed_base_tree,
+					  base_tree,
+					  result);
+
+		free((char *)merge_opt->branch2);
+	} else {
+		BUG("unexpected replay mode %d", mode);
+	}
 	merge_opt->ancestor = NULL;
+	merge_opt->branch2 = NULL;
 	if (!result->clean)
 		return NULL;
 	/* Drop commits that become empty */
 	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
 	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
 		return replayed_base;
-	return create_commit(repo, result->tree, pickme, replayed_base);
+	return create_commit(repo, result->tree, pickme, replayed_base, mode);
 }
 
 void replay_result_release(struct replay_result *result)
@@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs,
 	};
 	bool detached_head;
 	char *advance;
+	char *revert;
+	enum replay_mode mode = REPLAY_MODE_PICK;
 	int ret;
 
 	advance = xstrdup_or_null(opts->advance);
+	revert = xstrdup_or_null(opts->revert);
+	if (revert)
+		mode = REPLAY_MODE_REVERT;
 	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
-			   &detached_head, &advance, &onto, &update_refs);
+			   &detached_head, &advance, &revert, &onto, &update_refs);
 
 	/* FIXME: Should allow replaying commits with the first as a root commit */
 
@@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs,
 			die(_("replaying merge commits is not supported yet!"));
 
 		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
-						  onto, &merge_opt, &result);
+						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
+						  &merge_opt, &result, mode);
 		if (!last_commit)
 			break;
 
@@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs,
 		kh_value(replayed_commits, pos) = last_commit;
 
 		/* Update any necessary branches */
-		if (advance)
+		if (advance || revert)
 			continue;
 
 		for (decoration = get_name_decoration(&commit->object);
@@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs,
 		goto out;
 	}
 
-	/* In --advance mode, advance the target ref */
-	if (advance)
-		replay_result_queue_update(out, advance,
+	/* In --advance or --revert mode, update the target ref */
+	if (advance || revert) {
+		const char *ref = advance ? advance : revert;
+		replay_result_queue_update(out, ref,
 					   &onto->object.oid,
 					   &last_commit->object.oid);
+	}
 
 	ret = 0;
 
@@ -377,5 +451,6 @@ int replay_revisions(struct rev_info *revs,
 	kh_destroy_oid_map(replayed_commits);
 	merge_finalize(&merge_opt, &result);
 	free(advance);
+	free(revert);
 	return ret;
 }
diff --git a/replay.h b/replay.h
index d8407dc7f7..e916a5f975 100644
--- a/replay.h
+++ b/replay.h
@@ -13,7 +13,7 @@ struct replay_revisions_options {
 	/*
 	 * Starting point at which to create the new commits; must be a branch
 	 * name. The branch will be updated to point to the rewritten commits.
-	 * This option is mutually exclusive with `onto`.
+	 * This option is mutually exclusive with `onto` and `revert`.
 	 */
 	const char *advance;
 
@@ -22,7 +22,14 @@ struct replay_revisions_options {
 	 * committish. References pointing at decendants of `onto` will be
 	 * updated to point to the new commits.
 	 */
-	 const char *onto;
+	const char *onto;
+
+	/*
+	 * Starting point at which to create revert commits; must be a branch
+	 * name. The branch will be updated to point to the revert commits.
+	 * This option is mutually exclusive with `onto` and `advance`.
+	 */
+	const char *revert;
 
 	/*
 	 * Update branches that point at commits in the given revision range.
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a03f8f9293..217f6fb292 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '
 	test_cmp expect actual
 '
 
-test_expect_success 'option --onto or --advance is mandatory' '
-	echo "error: option --onto or --advance is mandatory" >expect &&
+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '
+	echo "error: exactly one of --onto, --advance, or --revert is required" >expect &&
 	test_might_fail git replay -h >>expect &&
 	test_must_fail git replay topic1..topic2 2>actual &&
 	test_cmp expect actual
@@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err
 	test_cmp expect actual
 '
 
-test_expect_success 'options --advance and --contained cannot be used together' '
-	printf "fatal: options ${SQ}--advance${SQ} " >expect &&
-	printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
+test_expect_success '--advance and --contained cannot be used together' '
 	test_must_fail git replay --advance=main --contained \
 		topic1..topic2 2>actual &&
-	test_cmp expect actual
+	test_grep "cannot be used together" actual
 '
 
 test_expect_success 'cannot advance target ... ordering would be ill-defined' '
-	echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
+	echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
 	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
 	test_cmp expect actual
 '
@@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' '
 	test_grep "invalid.*replay.refAction.*value" error
 '
 
+test_expect_success 'argument to --revert must be a reference' '
+	echo "fatal: argument to --revert must be a reference" >expect &&
+	oid=$(git rev-parse main) &&
+	test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'cannot revert with multiple sources' '
+	echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
+	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using replay --revert to revert commits' '
+	# Reuse existing topic4 branch (has commits I and J on top of main)
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# Revert commits I and J
+	git replay --revert topic4 topic4~2..topic4 &&
+
+	# Verify the revert commits were created (newest-first ordering
+	# means J is reverted first, then I on top)
+	git log --format=%s -4 topic4 >actual &&
+	cat >expect <<-\EOF &&
+	Revert "I"
+	Revert "J"
+	J
+	I
+	EOF
+	test_cmp expect actual &&
+
+	# Verify commit message format includes hash (tip is Revert "I")
+	test_commit_message topic4 <<-EOF &&
+	Revert "I"
+
+	This reverts commit $(git rev-parse I).
+	EOF
+
+	# Verify reflog message
+	git reflog topic4 -1 --format=%gs >reflog-msg &&
+	echo "replay --revert topic4" >expect-reflog &&
+	test_cmp expect-reflog reflog-msg
+'
+
+test_expect_success 'using replay --revert in bare repo' '
+	# Reuse existing topic4 in bare repo
+	START=$(git -C bare rev-parse topic4) &&
+	test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
+
+	# Revert commit J in bare repo
+	git -C bare replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify revert was created
+	git -C bare log -1 --format=%s topic4 >actual &&
+	echo "Revert \"J\"" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'revert of revert uses Reapply' '
+	# Use topic4 and first revert J, then revert the revert
+	START=$(git rev-parse topic4) &&
+	test_when_finished "git branch -f topic4 $START" &&
+
+	# First revert J
+	git replay --revert topic4 topic4~1..topic4 &&
+	REVERT_J=$(git rev-parse topic4) &&
+
+	# Now revert the revert - should become Reapply
+	git replay --revert topic4 topic4~1..topic4 &&
+
+	# Verify Reapply prefix and message format
+	test_commit_message topic4 <<-EOF
+	Reapply "J"
+
+	This reverts commit $REVERT_J.
+	EOF
+'
+
+test_expect_success 'git replay --revert with conflict' '
+	# conflict branch has C.conflict which conflicts with topic1s C
+	test_expect_code 1 git replay --revert conflict B..topic1
+'
+
+test_expect_success 'git replay --revert incompatible with --contained' '
+	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --onto' '
+	test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --advance' '
+	test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
+	test_grep "cannot be used together" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 92+ messages in thread

* Re: [PATCH v6 2/2] replay: add --revert mode to reverse commit changes
  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
  0 siblings, 0 replies; 92+ messages in thread
From: Tian Yuchen @ 2026-03-28  4:33 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, ps, newren, gitster, phillip.wood123,
	karthik.188, johannes.schindelin, toon

Hi Siddharth,

The patch itself looks pretty good to me, but I have some reservations 
about its functionality:

On 3/26/26 04:23, Siddharth Asthana wrote:
>   static struct commit *create_commit(struct repository *repo,
>   				    struct tree *tree,
>   				    struct commit *based_on,
> -				    struct commit *parent)
> +				    struct commit *parent,
> +				    enum replay_mode mode)
>   {

...

>   	extra = read_commit_extra_headers(based_on, exclude_gpgsig);

...


>   	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
>   				 &ret, author, NULL, sign_commit, extra)) {
> @@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo,
>   	}
>   }

It seems there isn’t a distinction made here between how 'cherry-pick' 
and 'revert' handle the `extra` header. But doesn’t the 'revert' 
operation actually create a *new* commit with the *current time* and 
*current author*? Is it appropriate to inherit the 'extra' header?




> +static void set_up_branch_mode(struct repository *repo,

...

> +	*onto = peel_committish(repo, *branch_name, option_name);
> +	if (rinfo->positive_refexprs > 1)
> +		die(_("'%s' cannot be used with multiple revision ranges "
> +		      "because the ordering would be ill-defined"),
> +		    option_name);
> +}

This is a fail-safe design intended to prevent users from entering 
commands like:

	git replay --revert main f1 f2

This operation is indeed undefined which should be intercepted. However, 
considering:
	
	git replay --revert main HEAD~5..HEAD~3 HEAD~1..HEAD

Is this operation also intercepted? I think the reason is that the 
condition 'rinfo->positive_refexprs > 1' is a bit too simplistic.




> +	if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
> +			  &oid, &fullname, 0) == 1) {
> +		free(*branch_name);
> +		*branch_name = fullname;
> +	} else {
> +		die(_("argument to %s must be a reference"), option_name);
> +	}

I think it would be great if a low-level command supported something like:

	git replay --revert new-branch HEAD~3..HEAD

Even if it just saves the step of creating a new branch ;)




These are just my thoughts on the matter. Hope to spark discussion.

Regards, Yuchen


^ permalink raw reply	[flat|nested] 92+ messages in thread

end of thread, other threads:[~2026-03-28  4:33 UTC | newest]

Thread overview: 92+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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
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-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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox