{"thread":{"id":"64529","subject":"[PATCH 0/1] replay: add --revert option to reverse commit changes","startedAt":"2025-11-25T17:01:12Z","lastAt":"2026-03-31T08:11:53Z","messageCount":96,"participants":["Siddharth Asthana","Johannes Schindelin","Junio C Hamano","Phillip Wood","Elijah Newren","Patrick Steinhardt","Toon Claes","Kristoffer Haugsbakk","Christian Couder","Tian Yuchen"],"isPatch":true,"patchVersion":1,"patchTotal":1},"messages":[{"id":"531262","messageId":"20251125170056.34489-1-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":null,"subject":"[PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-25T17:00:55Z","receivedAt":"2025-11-25T17:01:12Z","isPatch":true,"body":"The `git replay` command currently supports cherry-picking commits for\nserver-side history rewriting, but lacks the ability to revert them.\nThis patch adds a `--revert` option to enable reversing commits directly\non bare repositories.\n\nAt GitLab, we use replay in Gitaly for efficient server-side operations.\nAdding revert functionality enables us to reverse problematic commits\nwithout client-side roundtrips, reducing network overhead.\n\nThe implementation leverages the insight that cherry-pick and revert are\nessentially the same merge operation with swapped arguments. By swapping\nthe base and pickme trees when calling `merge_incore_nonrecursive()`, we\neffectively reverse the diff direction. The existing conflict handling,\nref updates, and atomic transaction support work unchanged.\n\nThe revert message generation logic is extracted into a new shared\n`sequencer_format_revert_header()` function in `sequencer.c`, allowing\ncode reuse between `sequencer.c` and `builtin/replay.c`. The commit\nmessages follow `git revert` conventions, including \"Revert\"/\"Reapply\"\nprefixes and the original commit SHA.\n\nThis patch includes comprehensive tests covering various scenarios:\nbare repositories, --advance mode, conflicts, reapply behavior, and\nmultiple commits.\n\nSiddharth Asthana (1):\n  replay: add --revert option to reverse commit changes\n\n Documentation/git-replay.adoc |  35 +++++++-\n builtin/replay.c              |  86 ++++++++++++++----\n sequencer.c                   |  23 +++++\n sequencer.h                   |   8 ++\n t/t3650-replay-basics.sh      | 160 ++++++++++++++++++++++++++++++++++\n 5 files changed, 295 insertions(+), 17 deletions(-)\n\n-- \n2.51.0\n\n"},{"id":"531263","messageId":"20251125170056.34489-2-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20251125170056.34489-1-siddharthasthana31@gmail.com","subject":"[PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-25T17:00:56Z","receivedAt":"2025-11-25T17:01:20Z","isPatch":true,"body":"The `git replay` command performs server-side history rewriting without\nrequiring a working tree. While it currently supports cherry-picking\ncommits, it lacks the ability to revert them.\n\nAt GitLab, we use replay in Gitaly for efficient server-side operations\non bare repositories. Adding revert functionality enables us to reverse\nproblematic commits directly on the server, eliminating client-side\nroundtrips and reducing network overhead.\n\nAdd a `--revert` option that reverses the changes introduced by the\nspecified commits. The implementation follows the same approach as\n`sequencer.c` (around lines 2358-2390), where cherry-pick and revert\nare essentially the same merge operation but with swapped arguments:\n\n  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n  - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n\nWe swap the base and pickme trees when calling\n`merge_incore_nonrecursive()`, effectively reversing the diff direction.\nThe existing conflict handling, ref updates, and atomic transaction\nsupport work unchanged.\n\nThe revert message generation logic (handling \"Revert\" and \"Reapply\"\ncases) is extracted into a new `sequencer_format_revert_header()`\nfunction in `sequencer.c`, which can be shared between `sequencer.c`\nand `builtin/replay.c`. The `builtin/replay.c` code calls this shared\nfunction and then appends the commit OID using `oid_to_hex()` directly,\nsince git replay is designed for simpler server-side operations without\nthe interactive features and `replay_opts` framework used by\n`sequencer.c`.\n\nThe commit messages follow `git revert` conventions: prefixed with\n\"Revert\" and including the original commit SHA. When reverting a commit\nthat itself starts with \"Revert\", the message uses \"Reapply\" instead.\nUnlike cherry-pick which preserves the original author, revert commits\nuse the current user as the author, matching the behavior of `git\nrevert`.\n\nMark the option as incompatible with `--contained` since reverting\nchanges across multiple branches simultaneously could lead to\ninconsistent repository states.\n\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  35 +++++++-\n builtin/replay.c              |  86 ++++++++++++++----\n sequencer.c                   |  23 +++++\n sequencer.h                   |   8 ++\n t/t3650-replay-basics.sh      | 160 ++++++++++++++++++++++++++++++++++\n 5 files changed, 295 insertions(+), 17 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex dcb26e8a8e..ad7dc08622 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] [--revert] <revision-range>...\n \n DESCRIPTION\n -----------\n@@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it.\n +\n The default mode can be configured via the `replay.refAction` configuration variable.\n \n+--revert::\n+\tRevert the changes introduced by the commits in the revision range\n+\tinstead of applying them. This reverses the diff direction and creates\n+\tnew commits that undo the changes, similar to `git revert`.\n++\n+The commit messages are prefixed with \"Revert\" and include the original\n+commit SHA. If reverting a commit whose message starts with \"Revert\", the new\n+message will start with \"Reapply\" instead. The author of the new commits\n+will be the current user, not the original commit author.\n++\n+This option is incompatible with `--contained`.\n+\n <revision-range>::\n \tRange of commits to replay. More than one <revision-range> can\n \tbe passed, but in `--advance <branch>` mode, they should have\n@@ -141,6 +153,27 @@ all commits they have since `base`, playing them on top of\n `origin/main`. These three branches may have commits on top of `base`\n that they have in common, but that does not need to be the case.\n \n+To revert a range of commits:\n+\n+------------\n+$ git replay --revert --onto main feature~3..feature\n+------------\n+\n+This creates new commits on top of 'main' that reverse the changes introduced\n+by the last three commits on 'feature'. The 'feature' branch is updated to\n+point at the last of these revert commits. The 'main' branch is not updated\n+in this case.\n+\n+To revert commits and advance a branch:\n+\n+------------\n+$ git replay --revert --advance main feature~2..feature\n+------------\n+\n+This reverts the last two commits from 'feature', applies those reverts\n+on top of 'main', and updates 'main' to point at the result. The 'feature'\n+branch is not updated in this case.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 6606a2c94b..7258d0bbc5 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -17,6 +17,7 @@\n #include \"parse-options.h\"\n #include \"refs.h\"\n #include \"revision.h\"\n+#include \"sequencer.h\"\n #include \"strmap.h\"\n #include <oidset.h>\n #include <tree.h>\n@@ -57,10 +58,25 @@ static char *get_author(const char *message)\n \treturn NULL;\n }\n \n+/*\n+ * Generates a revert commit message using the shared sequencer function.\n+ * We use oid_to_hex() directly instead of refer_to_commit() since git replay\n+ * is designed for simpler server-side operations without interactive features.\n+ */\n+static void generate_revert_message(struct strbuf *msg,\n+\t\t\t\t    const char *orig_message,\n+\t\t\t\t    const struct object_id *oid)\n+{\n+\tsequencer_format_revert_header(msg, orig_message);\n+\tstrbuf_addstr(msg, oid_to_hex(oid));\n+\tstrbuf_addstr(msg, \".\\n\");\n+}\n+\n static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n+\t\t\t\t    struct commit *parent,\n+\t\t\t\t    int is_revert)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n@@ -78,8 +94,17 @@ static struct commit *create_commit(struct repository *repo,\n \tcommit_list_insert(parent, &parents);\n \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n \tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n+\n+\tif (is_revert) {\n+\t\tgenerate_revert_message(&msg, orig_message, &based_on->object.oid);\n+\t\t/* For revert, use current user as author */\n+\t\tauthor = NULL;\n+\t} else {\n+\t\t/* Cherry-pick mode: use original commit message and author */\n+\t\tstrbuf_addstr(&msg, orig_message);\n+\t\tauthor = get_author(message);\n+\t}\n+\n \treset_ident_date();\n \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n@@ -261,7 +286,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n \t\t\t\t\t  struct commit *onto,\n \t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n+\t\t\t\t\t  struct merge_result *result,\n+\t\t\t\t\t  int is_revert)\n {\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree;\n@@ -273,21 +299,41 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\tif (is_revert) {\n+\t\t/* For revert: swap base and pickme to reverse the diff */\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", short_commit_name(repo, pickme));\n+\t\tmerge_opt->ancestor = short_commit_name(repo, pickme);\n \n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  result->tree,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  result);\n+\n+\t\t/* branch2 was allocated with xstrfmt, needs freeing */\n+\t\tfree((char *)merge_opt->branch2);\n+\t} else {\n+\t\t/* For cherry-pick: normal order */\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  result->tree,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  result);\n+\n+\t\t/* ancestor was allocated with xstrfmt, needs freeing */\n+\t\tfree((char *)merge_opt->ancestor);\n+\t}\n \n-\tfree((char*)merge_opt->ancestor);\n \tmerge_opt->ancestor = NULL;\n+\tmerge_opt->branch2 = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, is_revert);\n }\n \n static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n@@ -350,6 +396,7 @@ int cmd_replay(int argc,\n \tint contained = 0;\n \tconst char *ref_action = NULL;\n \tenum ref_action_mode ref_mode;\n+\tint is_revert = 0;\n \n \tstruct rev_info revs;\n \tstruct commit *last_commit = NULL;\n@@ -366,7 +413,7 @@ int cmd_replay(int argc,\n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n+\t\t   \"[--ref-action[=<mode>]] [--revert] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -381,6 +428,8 @@ int cmd_replay(int argc,\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n \t\t\t   N_(\"control ref update behavior (update|print)\")),\n+\t\tOPT_BOOL(0, \"revert\", &is_revert,\n+\t\t\t N_(\"revert commits instead of cherry-picking them\")),\n \t\tOPT_END()\n \t};\n \n@@ -395,6 +444,10 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n+\t/* --revert is incompatible with --contained */\n+\tdie_for_incompatible_opt2(is_revert, \"--revert\",\n+\t\t\t\t  contained, \"--contained\");\n+\n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n \n@@ -496,7 +549,8 @@ int cmd_replay(int argc,\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n \t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\t\t\t\t\t  onto, &merge_opt, &result,\n+\t\t\t\t\t\t  is_revert);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \ndiff --git a/sequencer.c b/sequencer.c\nindex 5476d39ba9..e6d82c8368 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r,\n \treturn res;\n }\n \n+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)\n+{\n+\tconst char *revert_subject;\n+\n+\tif (skip_prefix(orig_subject, \"Revert \\\"\", &revert_subject) &&\n+\t    /*\n+\t     * We don't touch pre-existing repeated reverts, because\n+\t     * theoretically these can be nested arbitrarily deeply,\n+\t     * thus requiring excessive complexity to deal with.\n+\t     */\n+\t    !starts_with(revert_subject, \"Revert \\\"\")) {\n+\t\tstrbuf_addstr(out, \"Reapply \\\"\");\n+\t\tstrbuf_addstr(out, revert_subject);\n+\t\tstrbuf_addch(out, '\\n');\n+\t} else {\n+\t\tstrbuf_addstr(out, \"Revert \\\"\");\n+\t\tstrbuf_addstr(out, orig_subject);\n+\t\tstrbuf_addstr(out, \"\\\"\\n\");\n+\t}\n+\n+\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n+}\n+\n void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n {\n \tunsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;\ndiff --git a/sequencer.h b/sequencer.h\nindex 719684c8a9..2d4a2d3fac 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -205,6 +205,14 @@ int todo_list_rearrange_squash(struct todo_list *todo_list);\n  */\n void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag);\n \n+/*\n+ * Formats a revert commit message header following standard Git conventions.\n+ * Handles both regular reverts (\"Revert \\\"<subject>\\\"\") and revert of revert\n+ * cases (\"Reapply \\\"<subject>\\\"\"). Adds \"This reverts commit \" at the end.\n+ * The caller should append the commit OID after calling this function.\n+ */\n+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject);\n+\n void append_conflicts_hint(struct index_state *istate,\n \t\tstruct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode);\n enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg,\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex cf3aacf355..5fcd730b54 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -314,4 +314,164 @@ test_expect_success 'invalid replay.refAction value' '\n \ttest_grep \"invalid.*replay.refAction.*value\" error\n '\n \n+test_expect_success 'using replay with --revert to revert a commit' '\n+\t# Revert commits D and E from topic2\n+\tgit replay --revert --onto topic1 topic1..topic2 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\tNEW_TOPIC2=$(cut -f 3 -d \" \" result) &&\n+\n+\t# Verify the result updates the topic2 branch\n+\tprintf \"update refs/heads/topic2 \" >expect &&\n+\tprintf \"%s \" $NEW_TOPIC2 >>expect &&\n+\tgit rev-parse topic2 >>expect &&\n+\n+\ttest_cmp expect result &&\n+\n+\t# Verify the commit messages contain \"Revert\"\n+\t# topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A)\n+\tgit log --format=%s $NEW_TOPIC2 >actual &&\n+\ttest_line_count = 6 actual &&\n+\thead -n 1 actual >first-line &&\n+\ttest_grep \"^Revert\" first-line\n+'\n+\n+test_expect_success 'using replay with --revert on bare repo' '\n+\tgit -C bare replay --revert --onto topic1 topic1..topic2 >result-bare &&\n+\n+\ttest_line_count = 1 result-bare &&\n+\tNEW_COMMIT=$(cut -f 3 -d \" \" result-bare) &&\n+\n+\t# Verify the commit message contains \"Revert\"\n+\tgit -C bare log --format=%s $NEW_COMMIT >actual-bare &&\n+\ttest_line_count = 6 actual-bare &&\n+\thead -n 1 actual-bare >first-line-bare &&\n+\ttest_grep \"^Revert\" first-line-bare\n+'\n+\n+test_expect_success 'using replay with --revert and --advance' '\n+\t# Revert commits from topic2 and advance main\n+\tgit replay --revert --advance main topic1..topic2 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\tNEW_MAIN=$(cut -f 3 -d \" \" result) &&\n+\n+\t# Verify the result updates the main branch\n+\tprintf \"update refs/heads/main \" >expect &&\n+\tprintf \"%s \" $NEW_MAIN >>expect &&\n+\tgit rev-parse main >>expect &&\n+\n+\ttest_cmp expect result &&\n+\n+\t# Verify the commit message contains \"Revert\"\n+\tgit log --format=%s $NEW_MAIN >actual &&\n+\thead -n 1 actual >first-line &&\n+\ttest_grep \"^Revert\" first-line\n+'\n+\n+test_expect_success 'replay with --revert fails with --contained' '\n+\ttest_must_fail git replay --revert --contained --onto main main..topic3 2>error &&\n+\ttest_grep \"revert.*contained.*cannot be used together\" error\n+'\n+\n+test_expect_success 'verify revert actually reverses changes' '\n+\t# Create a branch with a simple change\n+\tgit switch -c revert-test main &&\n+\techo \"new content\" >test-file.txt &&\n+\tgit add test-file.txt &&\n+\tgit commit -m \"Add test file\" &&\n+\n+\t# Revert the commit\n+\tgit replay --revert --advance revert-test HEAD^..HEAD >result &&\n+\tREVERTED=$(cut -f 3 -d \" \" result) &&\n+\n+\t# The file should no longer exist (reverted)\n+\ttest_must_fail git show $REVERTED:test-file.txt\n+'\n+\n+test_expect_success 'revert of a revert creates reapply message' '\n+\t# Create a commit\n+\tgit switch -c revert-revert main &&\n+\techo \"content\" >revert-test-2.txt &&\n+\tgit add revert-test-2.txt &&\n+\tgit commit -m \"Add revert test file\" &&\n+\n+\tORIGINAL=$(git rev-parse HEAD) &&\n+\n+\t# First revert\n+\tgit replay --revert --advance revert-revert HEAD^..HEAD >result1 &&\n+\tFIRST_REVERT=$(cut -f 3 -d \" \" result1) &&\n+\n+\t# Check first revert message starts with \"Revert\"\n+\tgit log --format=%s -1 $FIRST_REVERT >msg1 &&\n+\ttest_grep \"^Revert\" msg1 &&\n+\n+\t# Now revert the revert\n+\tgit replay --revert --advance revert-revert $ORIGINAL..$FIRST_REVERT >result2 &&\n+\tREAPPLY=$(cut -f 3 -d \" \" result2) &&\n+\n+\t# Check second revert message starts with \"Reapply\"\n+\tgit log --format=%s -1 $REAPPLY >msg2 &&\n+\ttest_grep \"^Reapply\" msg2 &&\n+\n+\t# The file should exist again (reapplied)\n+\tgit show $REAPPLY:revert-test-2.txt >actual &&\n+\techo \"content\" >expected &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'replay --revert includes commit SHA in message' '\n+\tgit switch -c revert-sha-test main &&\n+\techo \"test\" >sha-test.txt &&\n+\tgit add sha-test.txt &&\n+\tgit commit -m \"Test commit for SHA\" &&\n+\n+\tCOMMIT_SHA=$(git rev-parse HEAD) &&\n+\tgit replay --revert --advance revert-sha-test HEAD^..HEAD >result &&\n+\tREVERT_COMMIT=$(cut -f 3 -d \" \" result) &&\n+\n+\t# Check that the commit message includes the original SHA\n+\tgit log --format=%B -1 $REVERT_COMMIT >msg &&\n+\ttest_grep \"$COMMIT_SHA\" msg\n+'\n+\n+test_expect_success 'replay --revert with conflict' '\n+\t# Create a conflicting situation\n+\tgit switch -c revert-conflict main &&\n+\techo \"line1\" >conflict-file.txt &&\n+\tgit add conflict-file.txt &&\n+\tgit commit -m \"Add conflict file\" &&\n+\n+\tgit switch -c revert-conflict-branch HEAD^ &&\n+\techo \"different\" >conflict-file.txt &&\n+\tgit add conflict-file.txt &&\n+\tgit commit -m \"Different content\" &&\n+\n+\t# Try to revert the first commit onto the conflicting branch\n+\ttest_expect_code 1 git replay --revert --onto revert-conflict-branch revert-conflict^..revert-conflict\n+'\n+\n+test_expect_success 'replay --revert handles multiple commits' '\n+\t# Verify that reverting multiple commits works correctly\n+\t# The output should show both revert commits in the history\n+\tgit log --format=%s topic2 >topic2-log &&\n+\ttest_write_lines E D C B A >expected-topic2 &&\n+\ttest_cmp expected-topic2 topic2-log &&\n+\n+\t# Revert D and E from topic2, applying the reverts onto topic1\n+\tgit replay --revert --onto topic1 topic1..topic2 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\tFINAL=$(cut -f 3 -d \" \" result) &&\n+\n+\t# Verify both revert commits appear in the log\n+\tgit log --format=%s $FINAL >log &&\n+\thead -n 2 log >first-two &&\n+\ttest_grep \"^Revert\" first-two &&\n+\n+\t# Verify we have both \"Revert D\" and \"Revert E\"\n+\ttest_grep \"Revert.*E\" log &&\n+\ttest_grep \"Revert.*D\" log\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"531266","messageId":"4d75da90-7e85-573a-bb10-0c63a02b076c@gmx.de","threadId":"64529","inReplyTo":"20251125170056.34489-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2025-11-25T17:25:41Z","receivedAt":"2025-11-25T17:25:51Z","isPatch":true,"body":"Hi Siddharth,\n\nOn Tue, 25 Nov 2025, Siddharth Asthana wrote:\n\n> The `git replay` command currently supports cherry-picking commits for\n> server-side history rewriting, but lacks the ability to revert them.\n> This patch adds a `--revert` option to enable reversing commits directly\n> on bare repositories.\n> \n> At GitLab, we use replay in Gitaly for efficient server-side operations.\n> Adding revert functionality enables us to reverse problematic commits\n> without client-side roundtrips, reducing network overhead.\n> \n> The implementation leverages the insight that cherry-pick and revert are\n> essentially the same merge operation with swapped arguments. By swapping\n> the base and pickme trees when calling `merge_incore_nonrecursive()`, we\n> effectively reverse the diff direction. The existing conflict handling,\n> ref updates, and atomic transaction support work unchanged.\n\nAre you reverting rebased Merge Requests commit by commit? If not, I would\nsuggest the shortcut to use `merge-tree` directly for the entire Merge\nRequest. That is, if `$BASE` corresponds to the base branch onto which the\nMerge Request was rebased, and `$TIP` corresponds to the Merge Request's\nrebased tip commit, then the following will revert that Merge Request:\n\n\tgit merge-tree --merge-base $TIP HEAD $BASE\n\nThe upside is that this can potentially avoid a lot of unnecessary merge\nconflicts. The downside is that it does not revert the rebased Merge\nRequest commit by commit.\n\nThe patch itself looks fine to me, if a bit too extensive on the side of\nadding tests: Remember, a nimble test suite that catches a bug once is\nbetter than a long-running test suite that would catch a bug several times\n_iff_ it didn't tax the developer's patience so much that it is\ninterrupted and aborted. You probably agree that Git's CI runtimes are\nalready counter-productively long.\n\nCiao,\nJohannes\n\n> The revert message generation logic is extracted into a new shared\n> `sequencer_format_revert_header()` function in `sequencer.c`, allowing\n> code reuse between `sequencer.c` and `builtin/replay.c`. The commit\n> messages follow `git revert` conventions, including \"Revert\"/\"Reapply\"\n> prefixes and the original commit SHA.\n> \n> This patch includes comprehensive tests covering various scenarios:\n> bare repositories, --advance mode, conflicts, reapply behavior, and\n> multiple commits.\n> \n> Siddharth Asthana (1):\n>   replay: add --revert option to reverse commit changes\n> \n>  Documentation/git-replay.adoc |  35 +++++++-\n>  builtin/replay.c              |  86 ++++++++++++++----\n>  sequencer.c                   |  23 +++++\n>  sequencer.h                   |   8 ++\n>  t/t3650-replay-basics.sh      | 160 ++++++++++++++++++++++++++++++++++\n>  5 files changed, 295 insertions(+), 17 deletions(-)\n> \n> -- \n> 2.51.0\n> \n> \n"},{"id":"531270","messageId":"xmqqldjurnw2.fsf@gitster.g","threadId":"64529","inReplyTo":"4d75da90-7e85-573a-bb10-0c63a02b076c@gmx.de","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-25T18:02:53Z","receivedAt":"2025-11-25T18:02:57Z","isPatch":true,"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> The patch itself looks fine to me, if a bit too extensive on the side of\n> adding tests: Remember, a nimble test suite that catches a bug once is\n> better than a long-running test suite that would catch a bug several times\n> _iff_ it didn't tax the developer's patience so much that it is\n> interrupted and aborted. You probably agree that Git's CI runtimes are\n> already counter-productively long.\n\nI am not sure about some of the negations in the above, but it is\nvery good to point out that tests want to cover widely but without\noverlap.  Two tests that try to see the tool works well under\nidentical scenarios can be better done as a single test.  We do not\nneed to catch the same bug in multiple tests, as people tend to see\ntest breakages, update the code to fix the first one, and continue,\nwanting to fix more and different kind of breakages.\n\nThanks.\n\n\n"},{"id":"531271","messageId":"xmqqwm3drk6m.fsf@gitster.g","threadId":"64529","inReplyTo":"20251125170056.34489-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-25T19:22:57Z","receivedAt":"2025-11-25T19:23:02Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> The revert message generation logic (handling \"Revert\" and \"Reapply\"\n> cases) is extracted into a new `sequencer_format_revert_header()`\n> function in `sequencer.c`, which can be shared between `sequencer.c`\n> and `builtin/replay.c`. The `builtin/replay.c` code calls this shared\n> function and then appends the commit OID using `oid_to_hex()` directly,\n> since git replay is designed for simpler server-side operations without\n> the interactive features and `replay_opts` framework used by\n> `sequencer.c`.\n\nWhen I review a patch that claims to refactor existing logic into a\nseparate helper function to reuse it in more places, I look at the\ndiffstat to see how many lines are removed.  The logic for\ngenerating the message does not seem to be \"extracted into\", but\nrather \"duplicated to\", the new helper function.  It gives the two\nmessage sources opportunity to drift apart over time, which is not\nwhat you want.\n\nIn do_pick_commit() where TODO_REVERT command is handled, we find a\ncode block that is almost identical to what this patch adds to the\nnew helper function; it should be rewritten to call the new helper\nfunction or perhaps a shared helper function is introduced and\ncalled from there and also from the sequencer_format_revert_header()\nfunction, if there is still some impedance mismatch.  If such a\nrefactoring is done as a separate preliminary patch in a N-patch\nseries, the resulting patch series may be easier to follow (and\nthere may be other opportunities to reuse existing code more).\n\n> Mark the option as incompatible with `--contained` since reverting\n> changes across multiple branches simultaneously could lead to\n> inconsistent repository states.\n\nThis, and the documentation part, does not seem to tell what\n\"inconsistent state\" we are worried about.  Is it just a buggy\ndesign of --revert can be implemented that produces wrong result\nwhen used with --contened, or are these two options inherently try\nto achieve contradicting goals?  I am guessing that it is the\nlatter, but if so, can we make it clear why?\n\n> +--revert::\n> +\tRevert the changes introduced by the commits in the revision range\n> +\tinstead of applying them. This reverses the diff direction and creates\n> +\tnew commits that undo the changes, similar to `git revert`.\n> ++\n> +The commit messages are prefixed with \"Revert\" and include the original\n> +commit SHA. If reverting a commit whose message starts with \"Revert\", the new\n> +message will start with \"Reapply\" instead. The author of the new commits\n> +will be the current user, not the original commit author.\n> ++\n> +This option is incompatible with `--contained`.\n\nI have never used the `--contained` option, but is it so obvious to\nthose who have why these two have to be made incompatible that the\nabove statement does not have to be followed by \"because ...\"?\n\n> @@ -141,6 +153,27 @@ all commits they have since `base`, playing them on top of\n>  `origin/main`. These three branches may have commits on top of `base`\n>  that they have in common, but that does not need to be the case.\n>  \n> +To revert a range of commits:\n> +\n> +------------\n> +$ git replay --revert --onto main feature~3..feature\n> +------------\n> +\n> +This creates new commits on top of 'main' that reverse the changes introduced\n> +by the last three commits on 'feature'. The 'feature' branch is updated to\n> +point at the last of these revert commits. The 'main' branch is not updated\n> +in this case.\n\nIs there any topological requirement between 'main' and 'feature'\nbranches?  Naïvely, I would expect that it would be perfect if\n'feature' branch has been merged to 'main' (then you'd be reverting\nthe top 3 commits of that branch), but that would be something you\nwould do to correct 'main', and not 'feature', but the description\nexplains this is a way to update 'feature' to lose the three topmost\ncommits, so I am not sure what this example really does and when it\nwould be useful.\n\n> +To revert commits and advance a branch:\n> +\n> +------------\n> +$ git replay --revert --advance main feature~2..feature\n> +------------\n> +\n> +This reverts the last two commits from 'feature', applies those reverts\n> +on top of 'main', and updates 'main' to point at the result. The 'feature'\n> +branch is not updated in this case.\n\nThe same question.  If I assume that 'main' has merged 'feature'\nbefore, this I can understand and match what I often do quite well\nwhile working on integrating topic branches.  I may merge a topic\nthat is not yet well cooked enough into 'next', regret that the two\ncommits at the tip of the topic were premature, and revert these two\ncommits out of 'next', or something.  This example can be explained\nwell if there is topological requirement that 'main' has at least\nthese two commits from 'feature'.\n\n> @@ -261,7 +286,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>  \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n>  \t\t\t\t\t  struct commit *onto,\n>  \t\t\t\t\t  struct merge_options *merge_opt,\n> -\t\t\t\t\t  struct merge_result *result)\n> +\t\t\t\t\t  struct merge_result *result,\n> +\t\t\t\t\t  int is_revert)\n\nAre there other ways to pick commit imaginable (if not planned to be\nimplemented), other than \"revert\"?  I am wondering if this is better\ndone as \"enum { CHERRY_PICK, REVERT, } pick_variant\" for readability\nand maintainability.\n\n> @@ -273,21 +299,41 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>  \tpickme_tree = repo_get_commit_tree(repo, pickme);\n>  \tbase_tree = repo_get_commit_tree(repo, base);\n>  \n> -\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> -\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n> -\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n> +\tif (is_revert) {\n\nIt may be just me, but it would have been easier to follow if\n!revert case is given first, as that is the common variant the\npick_regular_commit() function.\n\n> +\t\t/* For revert: swap base and pickme to reverse the diff */\n> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> +\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", short_commit_name(repo, pickme));\n\nThat is an overly long line (sorry, I notice these things when a\nline does not even fit in 92-col terminal).\n\n> +\t\tmerge_opt->ancestor = short_commit_name(repo, pickme);\n\n> -\tmerge_incore_nonrecursive(merge_opt,\n> -\t\t\t\t  base_tree,\n> -\t\t\t\t  result->tree,\n> -\t\t\t\t  pickme_tree,\n> -\t\t\t\t  result);\n> +\t\tmerge_incore_nonrecursive(merge_opt,\n> +\t\t\t\t\t  pickme_tree,\n> +\t\t\t\t\t  result->tree,\n> +\t\t\t\t\t  base_tree,\n> +\t\t\t\t\t  result);\n\nOK.  These are applications of the standard 3-way merge trick to\n(ab)use ancestor to implement cherry-pick and revert.  Looking good.\n\n> +\n> +\t\t/* branch2 was allocated with xstrfmt, needs freeing */\n> +\t\tfree((char *)merge_opt->branch2);\n> +\t} else {\n> +\t\t/* For cherry-pick: normal order */\n> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> +\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n> +\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n> +\n> +\t\tmerge_incore_nonrecursive(merge_opt,\n> +\t\t\t\t\t  base_tree,\n> +\t\t\t\t\t  result->tree,\n> +\t\t\t\t\t  pickme_tree,\n> +\t\t\t\t\t  result);\n> +\n> +\t\t/* ancestor was allocated with xstrfmt, needs freeing */\n> +\t\tfree((char *)merge_opt->ancestor);\n\nAnd the \"else\" block has the original sequence of statements.\n\n> +\t}\n>  \n> -\tfree((char*)merge_opt->ancestor);\n>  \tmerge_opt->ancestor = NULL;\n> +\tmerge_opt->branch2 = NULL;\n\nNot a new problem, but what is the point of setting these two (but\nnot branch1) to NULL?  If a later caller misuses ->ancestor left\nbehind without setting its own, it would result in an access after\nfree, but if such a caller misuses ->branch1 left behind without\nsetting its own, because it is not allocated, it won't be an access\nafter free, *but* it is nevertheless wrong as the string in ->branch1\nis *not* computed suitably for that caller, isn't it?\n\n>  \tif (!result->clean)\n>  \t\treturn NULL;\n> -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n> +\treturn create_commit(repo, result->tree, pickme, replayed_base, is_revert);\n>  }\n\n\n> @@ -350,6 +396,7 @@ int cmd_replay(int argc,\n>  \tint contained = 0;\n>  \tconst char *ref_action = NULL;\n>  \tenum ref_action_mode ref_mode;\n> +\tint is_revert = 0;\n\nDitto on \"revert,cherry-pick\".\n\n> diff --git a/sequencer.c b/sequencer.c\n> index 5476d39ba9..e6d82c8368 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r,\n>  \treturn res;\n>  }\n>  \n> +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)\n> +{\n> +\tconst char *revert_subject;\n> +\n> +\tif (skip_prefix(orig_subject, \"Revert \\\"\", &revert_subject) &&\n> +\t    /*\n> +\t     * We don't touch pre-existing repeated reverts, because\n> +\t     * theoretically these can be nested arbitrarily deeply,\n> +\t     * thus requiring excessive complexity to deal with.\n> +\t     */\n> +\t    !starts_with(revert_subject, \"Revert \\\"\")) {\n> +\t\tstrbuf_addstr(out, \"Reapply \\\"\");\n> +\t\tstrbuf_addstr(out, revert_subject);\n> +\t\tstrbuf_addch(out, '\\n');\n> +\t} else {\n> +\t\tstrbuf_addstr(out, \"Revert \\\"\");\n> +\t\tstrbuf_addstr(out, orig_subject);\n> +\t\tstrbuf_addstr(out, \"\\\"\\n\");\n> +\t}\n> +\n> +\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n> +}\n> +\n\nDedup with do_pick_commit() where this was taken from.  Possibly in\na separte patch before the main one.\n\n"},{"id":"531273","messageId":"xmqqo6oprjti.fsf@gitster.g","threadId":"64529","inReplyTo":"xmqqwm3drk6m.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-25T19:30:49Z","receivedAt":"2025-11-25T19:30:52Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> In do_pick_commit() where TODO_REVERT command is handled, we find a\n> code block that is almost identical to what this patch adds to the\n> new helper function; it should be rewritten to call the new helper\n> function or perhaps a shared helper function is introduced and\n> called from there and also from the sequencer_format_revert_header()\n> function, if there is still some impedance mismatch.  If such a\n> refactoring is done as a separate preliminary patch in a N-patch\n> series, the resulting patch series may be easier to follow (and\n> there may be other opportunities to reuse existing code more).\n> ...\n>> diff --git a/sequencer.c b/sequencer.c\n>> index 5476d39ba9..e6d82c8368 100644\n>> --- a/sequencer.c\n>> +++ b/sequencer.c\n>> @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r,\n>>  \treturn res;\n>>  }\n>>  \n>> +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)\n>> +{\n>> +\tconst char *revert_subject;\n>> +\n>> +\tif (skip_prefix(orig_subject, \"Revert \\\"\", &revert_subject) &&\n>> +\t    /*\n>> +\t     * We don't touch pre-existing repeated reverts, because\n>> +\t     * theoretically these can be nested arbitrarily deeply,\n>> +\t     * thus requiring excessive complexity to deal with.\n>> +\t     */\n>> +\t    !starts_with(revert_subject, \"Revert \\\"\")) {\n>> +\t\tstrbuf_addstr(out, \"Reapply \\\"\");\n>> +\t\tstrbuf_addstr(out, revert_subject);\n>> +\t\tstrbuf_addch(out, '\\n');\n>> +\t} else {\n>> +\t\tstrbuf_addstr(out, \"Revert \\\"\");\n>> +\t\tstrbuf_addstr(out, orig_subject);\n>> +\t\tstrbuf_addstr(out, \"\\\"\\n\");\n>> +\t}\n>> +\n>> +\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n>> +}\n>> +\n>\n> Dedup with do_pick_commit() where this was taken from.  Possibly in\n> a separte patch before the main one.\n\nForgot to attach this at the end.  What I meant was that something\nalong this line may be a good starting point.\n\n sequencer.c | 14 +-------------\n 1 file changed, 1 insertion(+), 13 deletions(-)\n\ndiff --git c/sequencer.c w/sequencer.c\nindex e6d82c8368..29909952d4 100644\n--- c/sequencer.c\n+++ w/sequencer.c\n@@ -2365,20 +2365,8 @@ static int do_pick_commit(struct repository *r,\n \t\tif (opts->commit_use_reference) {\n \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n-\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n-\t\t\t   /*\n-\t\t\t    * We don't touch pre-existing repeated reverts, because\n-\t\t\t    * theoretically these can be nested arbitrarily deeply,\n-\t\t\t    * thus requiring excessive complexity to deal with.\n-\t\t\t    */\n-\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n \t\t} else {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n+\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n \t\t}\n \t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n \t\trefer_to_commit(opts, &ctx->message, commit);\n"},{"id":"531274","messageId":"xmqqjyzdrjf1.fsf@gitster.g","threadId":"64529","inReplyTo":"xmqqo6oprjti.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-25T19:39:30Z","receivedAt":"2025-11-25T19:39:33Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n>> Dedup with do_pick_commit() where this was taken from.  Possibly in\n>> a separte patch before the main one.\n>\n> Forgot to attach this at the end.  What I meant was that something\n> along this line may be a good starting point.\n>\n>  sequencer.c | 14 +-------------\n>  1 file changed, 1 insertion(+), 13 deletions(-)\n>\n> diff --git c/sequencer.c w/sequencer.c\n> index e6d82c8368..29909952d4 100644\n> --- c/sequencer.c\n> +++ w/sequencer.c\n> @@ -2365,20 +2365,8 @@ static int do_pick_commit(struct repository *r,\n>  \t\tif (opts->commit_use_reference) {\n>  \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>  \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n> -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n> -\t\t\t   /*\n> -\t\t\t    * We don't touch pre-existing repeated reverts, because\n> -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n> -\t\t\t    * thus requiring excessive complexity to deal with.\n> -\t\t\t    */\n> -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n> -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n> -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n> -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n>  \t\t} else {\n> -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n> -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n> -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n> +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n>  \t\t}\n>  \t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>  \t\trefer_to_commit(opts, &ctx->message, commit);\n\nBy the way, I probably would not be queuing this version today, as\nthis has obvious conflict with a large code movement made by\nPatrick's \"history\" series, which itself is expecting a reroll.\n\nPerhaps collect review comments on this iteration a bit more and\nwait for that other topic to be rerolled, and if it turns out to be\nsolid enough, base a v2 of this patch on top of it?\n\nThanks.\n"},{"id":"531275","messageId":"xmqqfra1ri5n.fsf@gitster.g","threadId":"64529","inReplyTo":"xmqqjyzdrjf1.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-25T20:06:44Z","receivedAt":"2025-11-25T20:06:48Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> By the way, I probably would not be queuing this version today, as\n> this has obvious conflict with a large code movement made by\n> Patrick's \"history\" series, which itself is expecting a reroll.\n>\n> Perhaps collect review comments on this iteration a bit more and\n> wait for that other topic to be rerolled, and if it turns out to be\n> solid enough, base a v2 of this patch on top of it?\n\nWhile I cannot test it with other topics, I had a chance to run\ntests after applying the patch directly on top of 'master':\n\n    $ make CC=clang SANITIZE=address,leak test\n    ...\n    Test Summary Report\n    -------------------\n    t3650-replay-basics.sh                           (Wstat: 256 (exited 1) Tests: 31 Failed: 5)\n      Failed tests:  23-25, 27, 31\n      Non-zero exit status: 1\n\nThe first failure was this one\n\n    expecting success of 3650.23 'using replay with --revert to revert a commit': \n            # Revert commits D and E from topic2\n            git replay --revert --onto topic1 topic1..topic2 >result &&\n\n            test_line_count = 1 result &&\n            NEW_TOPIC2=$(cut -f 3 -d \" \" result) &&\n\n            # Verify the result updates the topic2 branch\n            printf \"update refs/heads/topic2 \" >expect &&\n            printf \"%s \" $NEW_TOPIC2 >>expect &&\n            git rev-parse topic2 >>expect &&\n\n            test_cmp expect result &&\n\n            # Verify the commit messages contain \"Revert\"\n            # topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A)\n            git log --format=%s $NEW_TOPIC2 >actual &&\n            test_line_count = 6 actual &&\n            head -n 1 actual >first-line &&\n            test_grep \"^Revert\" first-line\n\n    test_line_count: line count for result != 1\n\nThe \"result\" file has 0 bytes (hence 0 lines).\n\nActually, address or leak sanitizing build is not needed to\nreproduce this problem, it seems.\n\n    $ make CC=clang test\n\nWas sufficient to see the same first failure.\n"},{"id":"531288","messageId":"d563b68b-e01d-4b18-bd84-86f36e61a70d@gmail.com","threadId":"64529","inReplyTo":"20251125170056.34489-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-26T11:10:50Z","receivedAt":"2025-11-26T11:10:56Z","isPatch":true,"body":"Hi Siddharth\n\nOn 25/11/2025 17:00, Siddharth Asthana wrote:\n> \n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index dcb26e8a8e..ad7dc08622 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it.\n> [...]\n> +To revert a range of commits:\n> +\n> +------------\n> +$ git replay --revert --onto main feature~3..feature\n> +------------\n> +\n> +This creates new commits on top of 'main' that reverse the changes introduced\n> +by the last three commits on 'feature'. The 'feature' branch is updated to\n> +point at the last of these revert commits. The 'main' branch is not updated\n> +in this case.\n\nI'm struggling to understand when I'd want to do this. Why would I want \nto update 'feature' to point to the reverted version of its last tree \ncommits rebased onto 'main'? In order to understand I ran the first \ntests case which does\n\n\tgit replay --onto topic1 --revert topic1..topic2\n\nafter fixing it by adding --ref-action=print the resulting commit log \nlooks like\n\ncommit d337fab78e90008835f74e890039b464a0308cbe\nAuthor: author@name <bogus@email@address>\nDate:   Thu Apr 7 15:30:13 2005 -0700\n\n     Revert \"E\n     \"\n\n     This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276.\n\ncommit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a\nAuthor: author@name <bogus@email@address>\nDate:   Thu Apr 7 15:30:13 2005 -0700\n\n     Revert \"D\n     \"\n\n     This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7.\n\n\nThe commits are empty because the original they are reverting each \ncreate a new file which is then present in the base revision but not in \neither of the merge heads when we revert. This suggests to me that it is \nnot a very realistic test and I'm still scratching my head to see where \n\"git replay --onto <commit> --revert\" is useful.\n\nIf '--revert' does not make sense with '--onto' then perhaps it should \nbe a new mode that takes a ref and acts like '--advance' but reverts the \ncommits rather than cherry-picking them. When reverting a range of \ncommits it would reduce the likelihood of conflicts to revert then in \nreverse order so we should either recommend passing '--reverse' or make \nthat the default when '--revert' is given.\n\nAs you can see in the log output above the new function to format the \nrevert subject lines is buggy. If you had used test_commit_message() to \ncheck the commit message, rather than just grepping for ^Revert the \ntests would have picked that up.\n\nThanks\n\nPhillip\n\n"},{"id":"531318","messageId":"CABPp-BEF1RVs7n4xf6ihyPUmxrhgpG6VEpZLrX=MTgcBWBRbfA@mail.gmail.com","threadId":"64529","inReplyTo":"d563b68b-e01d-4b18-bd84-86f36e61a70d@gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-26T17:35:46Z","receivedAt":"2025-11-26T17:35:58Z","isPatch":true,"body":"On Wed, Nov 26, 2025 at 3:10 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> Hi Siddharth\n>\n> On 25/11/2025 17:00, Siddharth Asthana wrote:\n> >\n> > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> > index dcb26e8a8e..ad7dc08622 100644\n> > --- a/Documentation/git-replay.adoc\n> > +++ b/Documentation/git-replay.adoc\n> > @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it.\n> > [...]\n> > +To revert a range of commits:\n> > +\n> > +------------\n> > +$ git replay --revert --onto main feature~3..feature\n> > +------------\n> > +\n> > +This creates new commits on top of 'main' that reverse the changes introduced\n> > +by the last three commits on 'feature'. The 'feature' branch is updated to\n> > +point at the last of these revert commits. The 'main' branch is not updated\n> > +in this case.\n>\n> I'm struggling to understand when I'd want to do this. Why would I want\n> to update 'feature' to point to the reverted version of its last tree\n> commits rebased onto 'main'? In order to understand I ran the first\n> tests case which does\n>\n>         git replay --onto topic1 --revert topic1..topic2\n>\n> after fixing it by adding --ref-action=print the resulting commit log\n> looks like\n>\n> commit d337fab78e90008835f74e890039b464a0308cbe\n> Author: author@name <bogus@email@address>\n> Date:   Thu Apr 7 15:30:13 2005 -0700\n>\n>      Revert \"E\n>      \"\n>\n>      This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276.\n>\n> commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a\n> Author: author@name <bogus@email@address>\n> Date:   Thu Apr 7 15:30:13 2005 -0700\n>\n>      Revert \"D\n>      \"\n>\n>      This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7.\n>\n>\n> The commits are empty because the original they are reverting each\n> create a new file which is then present in the base revision but not in\n> either of the merge heads when we revert. This suggests to me that it is\n> not a very realistic test and I'm still scratching my head to see where\n> \"git replay --onto <commit> --revert\" is useful.\n>\n> If '--revert' does not make sense with '--onto' then perhaps it should\n> be a new mode that takes a ref and acts like '--advance' but reverts the\n> commits rather than cherry-picking them. When reverting a range of\n> commits it would reduce the likelihood of conflicts to revert then in\n> reverse order so we should either recommend passing '--reverse' or make\n> that the default when '--revert' is given.\n>\n> As you can see in the log output above the new function to format the\n> revert subject lines is buggy. If you had used test_commit_message() to\n> check the commit message, rather than just grepping for ^Revert the\n> tests would have picked that up.\n>\n> Thanks\n>\n> Phillip\n\nI was going to say the same thing, but from a different angle.\n\nThe sequencer in git is used for three different types of operations:\nrebasing, cherry-picking, and reverting a range (with a sequence of\nreverts rather than one big revert).  In replay, these correspond to\n--onto, --advance, and the new thing you are trying to add.  As such,\nit should be its own new mode.\n\n(I do tend to see ranges reverted by a single big revert, the way\nJohannes suggested, rather than as a range of individual reverts, so\nto me the utility of the new mode looks low, but perhaps others find\nmore utility in it.  Or maybe the intent is to only use it with a\nrevision range that is only one commit long?)\n\nPhillip also went into more detail about why \"--onto $COMMIT --revert\"\nspecifically doesn't make sense.  I'd also say \"--advance $BRANCH\n--revert\" doesn't read well because to users, \"revert\" means going\nback while \"advance\" means going forward, so it's a rather confusing\ncommand line to make them wrap their head around.\n\nAnd yes, Siddharth, you were right that the new mode should be\nincompatible with --contained, but that's because --contained is a\nspecial modifier of --onto.  --onto, --advance, and --revert are three\ndifferent modes that are incompatible with each other.  Once you've\nchecked for that incompatibility between the three modes, then you can\neither check that whenever --contained is specified, either --onto is\nas well, or neither --advance nor --revert are.\n"},{"id":"531321","messageId":"xmqq3460ocv7.fsf@gitster.g","threadId":"64529","inReplyTo":"CABPp-BEF1RVs7n4xf6ihyPUmxrhgpG6VEpZLrX=MTgcBWBRbfA@mail.gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-26T18:41:32Z","receivedAt":"2025-11-26T18:41:36Z","isPatch":true,"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> I'm struggling to understand when I'd want to do this. Why would I want\n>> to update 'feature' to point to the reverted version of its last tree\n>> commits rebased onto 'main'?\n>> ...\n> I was going to say the same thing, but from a different angle.\n>\n> The sequencer in git is used for three different types of operations:\n> rebasing, cherry-picking, and reverting a range (with a sequence of\n> reverts rather than one big revert).  In replay, these correspond to\n> --onto, --advance, and the new thing you are trying to add.  As such,\n> it should be its own new mode.\n\nThis is a great comment that clarifies what the problem is with this.\n\n> And yes, Siddharth, you were right that the new mode should be\n> incompatible with --contained, but that's because --contained is a\n> special modifier of --onto.  --onto, --advance, and --revert are three\n> different modes that are incompatible with each other.\n\nThis answers the question I had on the patch perfectly.\n"},{"id":"531324","messageId":"27fef9e1-bf26-48af-b3df-35948937c891@gmail.com","threadId":"64529","inReplyTo":"4d75da90-7e85-573a-bb10-0c63a02b076c@gmx.de","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-26T19:18:53Z","receivedAt":"2025-11-26T19:19:02Z","isPatch":true,"body":"\nOn 25/11/25 22:55, Johannes Schindelin wrote:\n> Hi Siddharth,\n>\n> On Tue, 25 Nov 2025, Siddharth Asthana wrote:\n>\n>> The `git replay` command currently supports cherry-picking commits for\n>> server-side history rewriting, but lacks the ability to revert them.\n>> This patch adds a `--revert` option to enable reversing commits directly\n>> on bare repositories.\n>>\n>> At GitLab, we use replay in Gitaly for efficient server-side operations.\n>> Adding revert functionality enables us to reverse problematic commits\n>> without client-side roundtrips, reducing network overhead.\n>>\n>> The implementation leverages the insight that cherry-pick and revert are\n>> essentially the same merge operation with swapped arguments. By swapping\n>> the base and pickme trees when calling `merge_incore_nonrecursive()`, we\n>> effectively reverse the diff direction. The existing conflict handling,\n>> ref updates, and atomic transaction support work unchanged.\n\n\nHi Johannes,\nThanks for the review!\n\n\n> Are you reverting rebased Merge Requests commit by commit? If not, I would\n> suggest the shortcut to use `merge-tree` directly for the entire Merge\n> Request.\n\n\nThat's a great point. At GitLab, we have use cases for both approaches:\n\n1.  For quick undoing an entire MR, the `merge-tree` approach you \nsuggest is indeed more efficient and avoids unnecessary intermediate \nconflicts.\n\n2. For commit-by-commit reverts, we need individual revert commits with \nproper attribution (which commit is being reverted) for auditability and \nhistory clarity. This is particularly useful when only specific commits \nfrom a merged branch need to be reverted.\n\n\nI will add a note in the documentation mentioning the `merge-tree` \nalternative for whole-MR reverts.\n\n\n> That is, if `$BASE` corresponds to the base branch onto which the\n> Merge Request was rebased, and `$TIP` corresponds to the Merge Request's\n> rebased tip commit, then the following will revert that Merge Request:\n>\n> \tgit merge-tree --merge-base $TIP HEAD $BASE\n>\n> The upside is that this can potentially avoid a lot of unnecessary merge\n> conflicts. The downside is that it does not revert the rebased Merge\n> Request commit by commit.\n>\n> The patch itself looks fine to me, if a bit too extensive on the side of\n> adding tests\n\n\nAgreed. Looking at the tests again, I can consolidate several of them:\n- The bare repo test can be merged with the basic revert test\n- The multiple commits test overlaps with the basic functionality\n\nI will trim down to essential coverage in v2: basic revert, conflict \nhandling, and Reapply behavior.\n\nThanks,\nSiddharth\n\n\n> : Remember, a nimble test suite that catches a bug once is\n> better than a long-running test suite that would catch a bug several times\n> _iff_ it didn't tax the developer's patience so much that it is\n> interrupted and aborted. You probably agree that Git's CI runtimes are\n> already counter-productively long.\n>\n> Ciao,\n> Johannes\n>\n>> The revert message generation logic is extracted into a new shared\n>> `sequencer_format_revert_header()` function in `sequencer.c`, allowing\n>> code reuse between `sequencer.c` and `builtin/replay.c`. The commit\n>> messages follow `git revert` conventions, including \"Revert\"/\"Reapply\"\n>> prefixes and the original commit SHA.\n>>\n>> This patch includes comprehensive tests covering various scenarios:\n>> bare repositories, --advance mode, conflicts, reapply behavior, and\n>> multiple commits.\n>>\n>> Siddharth Asthana (1):\n>>    replay: add --revert option to reverse commit changes\n>>\n>>   Documentation/git-replay.adoc |  35 +++++++-\n>>   builtin/replay.c              |  86 ++++++++++++++----\n>>   sequencer.c                   |  23 +++++\n>>   sequencer.h                   |   8 ++\n>>   t/t3650-replay-basics.sh      | 160 ++++++++++++++++++++++++++++++++++\n>>   5 files changed, 295 insertions(+), 17 deletions(-)\n>>\n>> -- \n>> 2.51.0\n>>\n>>\n"},{"id":"531325","messageId":"706e2875-a3f9-447f-9f43-690990a2342d@gmail.com","threadId":"64529","inReplyTo":"xmqqwm3drk6m.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-26T19:26:48Z","receivedAt":"2025-11-26T19:26:55Z","isPatch":true,"body":"\nOn 26/11/25 00:52, Junio C Hamano wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> The revert message generation logic (handling \"Revert\" and \"Reapply\"\n>> cases) is extracted into a new `sequencer_format_revert_header()`\n>> function in `sequencer.c`, which can be shared between `sequencer.c`\n>> and `builtin/replay.c`. The `builtin/replay.c` code calls this shared\n>> function and then appends the commit OID using `oid_to_hex()` directly,\n>> since git replay is designed for simpler server-side operations without\n>> the interactive features and `replay_opts` framework used by\n>> `sequencer.c`.\n> When I review a patch that claims to refactor existing logic into a\n> separate helper function to reuse it in more places, I look at the\n> diffstat to see how many lines are removed.\n\n\nYou are right - in v1 I added the helper but didn't update \ndo_pick_commit() to use it. I have fixed this in my local tree; the \ndedup change is:\n\n\n     sequencer.c | 14 +-------------\n     1 file changed, 1 insertion(+), 13 deletions(-)\n\n\n> The logic for\n> generating the message does not seem to be \"extracted into\", but\n> rather \"duplicated to\", the new helper function.  It gives the two\n> message sources opportunity to drift apart over time, which is not\n> what you want.\n>\n> In do_pick_commit() where TODO_REVERT command is handled, we find a\n> code block that is almost identical to what this patch adds to the\n> new helper function; it should be rewritten to call the new helper\n> function or perhaps a shared helper function is introduced and\n> called from there and also from the sequencer_format_revert_header()\n> function, if there is still some impedance mismatch.  If such a\n> refactoring is done as a separate preliminary patch in a N-patch\n> series, the resulting patch series may be easier to follow (and\n> there may be other opportunities to reuse existing code more).\n>\n>> Mark the option as incompatible with `--contained` since reverting\n>> changes across multiple branches simultaneously could lead to\n>> inconsistent repository states.\n> This, and the documentation part, does not seem to tell what\n> \"inconsistent state\" we are worried about.\n\n\nElijah's reply clarified this perfectly - `--contained` is a modifier \nfor `--onto`, and as he points out, `--revert` should be a new mode \nentirely, not a modifier. Once `--revert` is its own mode (like `--onto` \nand `--advance`), the incompatibility with `--contained` becomes clear:\n`--contained` only makes sense with `--onto`.\n\n\n> Is it just a buggy\n> design of --revert can be implemented that produces wrong result\n> when used with --contened, or are these two options inherently try\n> to achieve contradicting goals?  I am guessing that it is the\n> latter, but if so, can we make it clear why?\n>\n>> +--revert::\n>> +\tRevert the changes introduced by the commits in the revision range\n>> +\tinstead of applying them. This reverses the diff direction and creates\n>> +\tnew commits that undo the changes, similar to `git revert`.\n>> ++\n>> +The commit messages are prefixed with \"Revert\" and include the original\n>> +commit SHA. If reverting a commit whose message starts with \"Revert\", the new\n>> +message will start with \"Reapply\" instead. The author of the new commits\n>> +will be the current user, not the original commit author.\n>> ++\n>> +This option is incompatible with `--contained`.\n> I have never used the `--contained` option, but is it so obvious to\n> those who have why these two have to be made incompatible that the\n> above statement does not have to be followed by \"because ...\"?\n>\n>> @@ -141,6 +153,27 @@ all commits they have since `base`, playing them on top of\n>>   `origin/main`. These three branches may have commits on top of `base`\n>>   that they have in common, but that does not need to be the case.\n>>   \n>> +To revert a range of commits:\n>> +\n>> +------------\n>> +$ git replay --revert --onto main feature~3..feature\n>> +------------\n>> +\n>> +This creates new commits on top of 'main' that reverse the changes introduced\n>> +by the last three commits on 'feature'. The 'feature' branch is updated to\n>> +point at the last of these revert commits. The 'main' branch is not updated\n>> +in this case.\n> Is there any topological requirement between 'main' and 'feature'\n> branches?\n\n\nYes, and I failed to explain this. For reverts to produce meaningful \nnon-empty commits, the commits being reverted should already be in the \ntarget branch's history. I will clarify the examples to show this \ntopology requirement explicitly.\n\n\n>    Naïvely, I would expect that it would be perfect if\n> 'feature' branch has been merged to 'main' (then you'd be reverting\n> the top 3 commits of that branch), but that would be something you\n> would do to correct 'main', and not 'feature', but the description\n> explains this is a way to update 'feature' to lose the three topmost\n> commits, so I am not sure what this example really does and when it\n> would be useful.\n>\n>> +To revert commits and advance a branch:\n>> +\n>> +------------\n>> +$ git replay --revert --advance main feature~2..feature\n>> +------------\n>> +\n>> +This reverts the last two commits from 'feature', applies those reverts\n>> +on top of 'main', and updates 'main' to point at the result. The 'feature'\n>> +branch is not updated in this case.\n> The same question.  If I assume that 'main' has merged 'feature'\n> before, this I can understand and match what I often do quite well\n> while working on integrating topic branches.  I may merge a topic\n> that is not yet well cooked enough into 'next', regret that the two\n> commits at the tip of the topic were premature, and revert these two\n> commits out of 'next', or something.  This example can be explained\n> well if there is topological requirement that 'main' has at least\n> these two commits from 'feature'.\n>\n>> @@ -261,7 +286,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>>   \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n>>   \t\t\t\t\t  struct commit *onto,\n>>   \t\t\t\t\t  struct merge_options *merge_opt,\n>> -\t\t\t\t\t  struct merge_result *result)\n>> +\t\t\t\t\t  struct merge_result *result,\n>> +\t\t\t\t\t  int is_revert)\n> Are there other ways to pick commit imaginable (if not planned to be\n> implemented), other than \"revert\"?  I am wondering if this is better\n> done as \"enum { CHERRY_PICK, REVERT, } pick_variant\" for readability\n> and maintainability.\n\n\nGood point. An enum would be clearer and more maintainable. I wll change \nto `enum replay_action { REPLAY_PICK, REPLAY_REVERT }`.\n\n\n>\n>> @@ -273,21 +299,41 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>>   \tpickme_tree = repo_get_commit_tree(repo, pickme);\n>>   \tbase_tree = repo_get_commit_tree(repo, base);\n>>   \n>> -\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>> -\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n>> -\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n>> +\tif (is_revert) {\n> It may be just me, but it would have been easier to follow if\n> !revert case is given first, as that is the common variant the\n> pick_regular_commit() function.\n\n\nMakes sense - the common case (cherry-pick) should come first. I will \nreorder the if/else.\n\n\n>\n>> +\t\t/* For revert: swap base and pickme to reverse the diff */\n>> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>> +\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", short_commit_name(repo, pickme));\n> That is an overly long line (sorry, I notice these things when a\n> line does not even fit in 92-col terminal).\n\n\nFixed in my local tree by introducing a `pickme_name` variable.\n\n\n>\n>> +\t\tmerge_opt->ancestor = short_commit_name(repo, pickme);\n>> -\tmerge_incore_nonrecursive(merge_opt,\n>> -\t\t\t\t  base_tree,\n>> -\t\t\t\t  result->tree,\n>> -\t\t\t\t  pickme_tree,\n>> -\t\t\t\t  result);\n>> +\t\tmerge_incore_nonrecursive(merge_opt,\n>> +\t\t\t\t\t  pickme_tree,\n>> +\t\t\t\t\t  result->tree,\n>> +\t\t\t\t\t  base_tree,\n>> +\t\t\t\t\t  result);\n> OK.  These are applications of the standard 3-way merge trick to\n> (ab)use ancestor to implement cherry-pick and revert.  Looking good.\n>\n>> +\n>> +\t\t/* branch2 was allocated with xstrfmt, needs freeing */\n>> +\t\tfree((char *)merge_opt->branch2);\n>> +\t} else {\n>> +\t\t/* For cherry-pick: normal order */\n>> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>> +\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n>> +\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n>> +\n>> +\t\tmerge_incore_nonrecursive(merge_opt,\n>> +\t\t\t\t\t  base_tree,\n>> +\t\t\t\t\t  result->tree,\n>> +\t\t\t\t\t  pickme_tree,\n>> +\t\t\t\t\t  result);\n>> +\n>> +\t\t/* ancestor was allocated with xstrfmt, needs freeing */\n>> +\t\tfree((char *)merge_opt->ancestor);\n> And the \"else\" block has the original sequence of statements.\n>\n>> +\t}\n>>   \n>> -\tfree((char*)merge_opt->ancestor);\n>>   \tmerge_opt->ancestor = NULL;\n>> +\tmerge_opt->branch2 = NULL;\n> Not a new problem, but what is the point of setting these two (but\n> not branch1) to NULL?\n\n\nYou're right, this is inconsistent. The intent is to prevent \nuse-after-free, but setting only some fields to NULL is incomplete. I \nwill either set all three to NULL or add a comment explaining the rationale.\n\n\n> If a later caller misuses ->ancestor left\n> behind without setting its own, it would result in an access after\n> free, but if such a caller misuses ->branch1 left behind without\n> setting its own, because it is not allocated, it won't be an access\n> after free, *but* it is nevertheless wrong as the string in ->branch1\n> is *not* computed suitably for that caller, isn't it?\n>\n>>   \tif (!result->clean)\n>>   \t\treturn NULL;\n>> -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n>> +\treturn create_commit(repo, result->tree, pickme, replayed_base, is_revert);\n>>   }\n>\n>> @@ -350,6 +396,7 @@ int cmd_replay(int argc,\n>>   \tint contained = 0;\n>>   \tconst char *ref_action = NULL;\n>>   \tenum ref_action_mode ref_mode;\n>> +\tint is_revert = 0;\n> Ditto on \"revert,cherry-pick\".\n>\n>> diff --git a/sequencer.c b/sequencer.c\n>> index 5476d39ba9..e6d82c8368 100644\n>> --- a/sequencer.c\n>> +++ b/sequencer.c\n>> @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r,\n>>   \treturn res;\n>>   }\n>>   \n>> +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)\n>> +{\n>> +\tconst char *revert_subject;\n>> +\n>> +\tif (skip_prefix(orig_subject, \"Revert \\\"\", &revert_subject) &&\n>> +\t    /*\n>> +\t     * We don't touch pre-existing repeated reverts, because\n>> +\t     * theoretically these can be nested arbitrarily deeply,\n>> +\t     * thus requiring excessive complexity to deal with.\n>> +\t     */\n>> +\t    !starts_with(revert_subject, \"Revert \\\"\")) {\n>> +\t\tstrbuf_addstr(out, \"Reapply \\\"\");\n>> +\t\tstrbuf_addstr(out, revert_subject);\n>> +\t\tstrbuf_addch(out, '\\n');\n>> +\t} else {\n>> +\t\tstrbuf_addstr(out, \"Revert \\\"\");\n>> +\t\tstrbuf_addstr(out, orig_subject);\n>> +\t\tstrbuf_addstr(out, \"\\\"\\n\");\n>> +\t}\n>> +\n>> +\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n>> +}\n>> +\n> Dedup with do_pick_commit() where this was taken from.  Possibly in\n> a separte patch before the main one.\n\n\nI have applied your suggested patch and will split this into a 2-patch \nseries: (1) extract and reuse sequencer_format_revert_header(), (2) add \n--revert to replay.\n\nThanks,\nSiddharth\n\n"},{"id":"531326","messageId":"515c997f-f562-4081-9c44-5a3049d75121@gmail.com","threadId":"64529","inReplyTo":"xmqqjyzdrjf1.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-26T19:28:40Z","receivedAt":"2025-11-26T19:28:47Z","isPatch":true,"body":"\nOn 26/11/25 01:09, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>>> Dedup with do_pick_commit() where this was taken from.  Possibly in\n>>> a separte patch before the main one.\n>> Forgot to attach this at the end.  What I meant was that something\n>> along this line may be a good starting point.\n>>\n>>   sequencer.c | 14 +-------------\n>>   1 file changed, 1 insertion(+), 13 deletions(-)\n>>\n>> diff --git c/sequencer.c w/sequencer.c\n>> index e6d82c8368..29909952d4 100644\n>> --- c/sequencer.c\n>> +++ w/sequencer.c\n>> @@ -2365,20 +2365,8 @@ static int do_pick_commit(struct repository *r,\n>>   \t\tif (opts->commit_use_reference) {\n>>   \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>>   \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n>> -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n>> -\t\t\t   /*\n>> -\t\t\t    * We don't touch pre-existing repeated reverts, because\n>> -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n>> -\t\t\t    * thus requiring excessive complexity to deal with.\n>> -\t\t\t    */\n>> -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n>> -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n>>   \t\t} else {\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n>> -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n>> +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n>>   \t\t}\n>>   \t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>   \t\trefer_to_commit(opts, &ctx->message, commit);\n> By the way, I probably would not be queuing this version today, as\n> this has obvious conflict with a large code movement made by\n> Patrick's \"history\" series, which itself is expecting a reroll.\n>\n> Perhaps collect review comments on this iteration a bit more and\n> wait for that other topic to be rerolled, and if it turns out to be\n> solid enough, base a v2 of this patch on top of it?\n\n\nUnderstood. I will wait for Patrick's \"history\" series to be rerolled \nand base v2 on top of that to avoid conflicts. In the meantime, I will \naddress all the review feedback locally.\n\nThanks,\nSiddharth\n\n\n>\n> Thanks.\n"},{"id":"531327","messageId":"cc5cc77d-5d78-4a16-b4b5-91a903436788@gmail.com","threadId":"64529","inReplyTo":"xmqqfra1ri5n.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-26T19:31:03Z","receivedAt":"2025-11-26T19:31:10Z","isPatch":true,"body":"\nOn 26/11/25 01:36, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> By the way, I probably would not be queuing this version today, as\n>> this has obvious conflict with a large code movement made by\n>> Patrick's \"history\" series, which itself is expecting a reroll.\n>>\n>> Perhaps collect review comments on this iteration a bit more and\n>> wait for that other topic to be rerolled, and if it turns out to be\n>> solid enough, base a v2 of this patch on top of it?\n> While I cannot test it with other topics, I had a chance to run\n> tests after applying the patch directly on top of 'master':\n>\n>      $ make CC=clang SANITIZE=address,leak test\n>      ...\n>      Test Summary Report\n>      -------------------\n>      t3650-replay-basics.sh                           (Wstat: 256 (exited 1) Tests: 31 Failed: 5)\n>        Failed tests:  23-25, 27, 31\n>        Non-zero exit status: 1\n>\n> The first failure was this one\n>\n>      expecting success of 3650.23 'using replay with --revert to revert a commit':\n>              # Revert commits D and E from topic2\n>              git replay --revert --onto topic1 topic1..topic2 >result &&\n>\n>              test_line_count = 1 result &&\n>              NEW_TOPIC2=$(cut -f 3 -d \" \" result) &&\n>\n>              # Verify the result updates the topic2 branch\n>              printf \"update refs/heads/topic2 \" >expect &&\n>              printf \"%s \" $NEW_TOPIC2 >>expect &&\n>              git rev-parse topic2 >>expect &&\n>\n>              test_cmp expect result &&\n>\n>              # Verify the commit messages contain \"Revert\"\n>              # topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A)\n>              git log --format=%s $NEW_TOPIC2 >actual &&\n>              test_line_count = 6 actual &&\n>              head -n 1 actual >first-line &&\n>              test_grep \"^Revert\" first-line\n>\n>      test_line_count: line count for result != 1\n>\n> The \"result\" file has 0 bytes (hence 0 lines).\n\n\nAh, this is because my patch was based on a tree that had atomic ref \nupdates as the default (REF_ACTION_UPDATE), which produces no stdout \noutput. The tests were written for --ref-action=print behavior.\n\nI have fixed the tests to either:\n1. Use --ref-action=print explicitly when expecting output, or\n2. Check the ref state directly rather than parsing stdout\n\nThe test failures you saw should be fixed in v2.\n\nThanks,\nSiddharth\n\n\n>\n> Actually, address or leak sanitizing build is not needed to\n> reproduce this problem, it seems.\n>\n>      $ make CC=clang test\n>\n> Was sufficient to see the same first failure.\n"},{"id":"531328","messageId":"38b51e19-7939-4a5e-8ad0-2d8168bc0fac@gmail.com","threadId":"64529","inReplyTo":"d563b68b-e01d-4b18-bd84-86f36e61a70d@gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-26T19:39:23Z","receivedAt":"2025-11-26T19:39:30Z","isPatch":true,"body":"\nOn 26/11/25 16:40, Phillip Wood wrote:\n> Hi Siddharth\n>\n> On 25/11/2025 17:00, Siddharth Asthana wrote:\n>>\n>> diff --git a/Documentation/git-replay.adoc \n>> b/Documentation/git-replay.adoc\n>> index dcb26e8a8e..ad7dc08622 100644\n>> --- a/Documentation/git-replay.adoc\n>> +++ b/Documentation/git-replay.adoc\n>> @@ -54,6 +54,18 @@ which uses the target only as a starting point \n>> without updating it.\n>> [...]\n>> +To revert a range of commits:\n>> +\n>> +------------\n>> +$ git replay --revert --onto main feature~3..feature\n>> +------------\n>> +\n>> +This creates new commits on top of 'main' that reverse the changes \n>> introduced\n>> +by the last three commits on 'feature'. The 'feature' branch is \n>> updated to\n>> +point at the last of these revert commits. The 'main' branch is not \n>> updated\n>> +in this case.\n\n\nHi Phillip,\n\nThanks for the detailed analysis!\n\n\n>\n> I'm struggling to understand when I'd want to do this. Why would I \n> want to update 'feature' to point to the reverted version of its last \n> tree commits rebased onto 'main'?\n\n\nYou are absolutely right - the `--onto` example I provided doesn't make \npractical sense. Elijah's reply clarified the architecture: `--revert` \nshould be its own mode, not a modifier that combines with `--onto` or \n`--advance`.\n\n\nThe realistic use case is reverting commits from a branch where those \ncommits already exist. For example:\n\n     git replay --revert main~3..main\n\nThis would revert the last 3 commits on main, creating revert commits on \ntop of main.\n\n\n>  In order to understand I ran the first tests case which does\n>\n>     git replay --onto topic1 --revert topic1..topic2\n>\n> after fixing it by adding --ref-action=print the resulting commit log \n> looks like\n>\n> commit d337fab78e90008835f74e890039b464a0308cbe\n> Author: author@name <bogus@email@address>\n> Date:   Thu Apr 7 15:30:13 2005 -0700\n>\n>     Revert \"E\n>     \"\n>\n>     This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276.\n>\n> commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a\n> Author: author@name <bogus@email@address>\n> Date:   Thu Apr 7 15:30:13 2005 -0700\n>\n>     Revert \"D\n>     \"\n>\n>     This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7.\n>\n>\n> The commits are empty because the original they are reverting each \n> create a new file which is then present in the base revision but not \n> in either of the merge heads when we revert.\n\n\nThis confirms the tests aren't realistic. In v2, I will create tests \nwhere the commits being reverted are ancestors of the replay target, so \nthe reverts produce meaningful diffs.\n\n\n>  This suggests to me that it is not a very realistic test and I'm \n> still scratching my head to see where \"git replay --onto <commit> \n> --revert\" is useful.\n>\n> If '--revert' does not make sense with '--onto' then perhaps it should \n> be a new mode that takes a ref and acts like '--advance' but reverts \n> the commits rather than cherry-picking them. When reverting a range of \n> commits it would reduce the likelihood of conflicts to revert then in \n> reverse order so we should either recommend passing '--reverse' or \n> make that the default when '--revert' is given.\n>\n> As you can see in the log output above the new function to format the \n> revert subject lines is buggy.\n\n\nGood catch! The bug is in `generate_revert_message()` - I am passing \n`orig_message` (which points to the full message including body) to \n`sequencer_format_revert_header()`, but that function expects just the \nsubject line.\n\nLooking at how sequencer.c does it, they use `msg.subject` which is \nproperly extracted. I need to use `commit_subject_length()` to get just \nthe subject:\n\n     int subject_len = find_commit_subject(message, &orig_message);\n     char *subject = xmemdupz(orig_message, subject_len);\n     generate_revert_message(&msg, subject, &based_on->object.oid);\n     free(subject);\n\n\n> If you had used test_commit_message() to check the commit message, \n> rather than just grepping for ^Revert the tests would have picked that \n> up.\n\n\nYou are right. I will use test_commit_message() for proper validation in v2.\n\nThanks,\nSiddharth\n\n\n>\n> Thanks\n>\n> Phillip\n>\n"},{"id":"531330","messageId":"d4362def-9be6-4191-8846-f3180a5a5d59@gmail.com","threadId":"64529","inReplyTo":"CABPp-BEF1RVs7n4xf6ihyPUmxrhgpG6VEpZLrX=MTgcBWBRbfA@mail.gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-26T19:50:15Z","receivedAt":"2025-11-26T19:50:23Z","isPatch":true,"body":"\nOn 26/11/25 23:05, Elijah Newren wrote:\n> On Wed, Nov 26, 2025 at 3:10 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>> Hi Siddharth\n>>\n>> On 25/11/2025 17:00, Siddharth Asthana wrote:\n>>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n>>> index dcb26e8a8e..ad7dc08622 100644\n>>> --- a/Documentation/git-replay.adoc\n>>> +++ b/Documentation/git-replay.adoc\n>>> @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it.\n>>> [...]\n>>> +To revert a range of commits:\n>>> +\n>>> +------------\n>>> +$ git replay --revert --onto main feature~3..feature\n>>> +------------\n>>> +\n>>> +This creates new commits on top of 'main' that reverse the changes introduced\n>>> +by the last three commits on 'feature'. The 'feature' branch is updated to\n>>> +point at the last of these revert commits. The 'main' branch is not updated\n>>> +in this case.\n>> I'm struggling to understand when I'd want to do this. Why would I want\n>> to update 'feature' to point to the reverted version of its last tree\n>> commits rebased onto 'main'? In order to understand I ran the first\n>> tests case which does\n>>\n>>          git replay --onto topic1 --revert topic1..topic2\n>>\n>> after fixing it by adding --ref-action=print the resulting commit log\n>> looks like\n>>\n>> commit d337fab78e90008835f74e890039b464a0308cbe\n>> Author: author@name <bogus@email@address>\n>> Date:   Thu Apr 7 15:30:13 2005 -0700\n>>\n>>       Revert \"E\n>>       \"\n>>\n>>       This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276.\n>>\n>> commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a\n>> Author: author@name <bogus@email@address>\n>> Date:   Thu Apr 7 15:30:13 2005 -0700\n>>\n>>       Revert \"D\n>>       \"\n>>\n>>       This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7.\n>>\n>>\n>> The commits are empty because the original they are reverting each\n>> create a new file which is then present in the base revision but not in\n>> either of the merge heads when we revert. This suggests to me that it is\n>> not a very realistic test and I'm still scratching my head to see where\n>> \"git replay --onto <commit> --revert\" is useful.\n>>\n>> If '--revert' does not make sense with '--onto' then perhaps it should\n>> be a new mode that takes a ref and acts like '--advance' but reverts the\n>> commits rather than cherry-picking them. When reverting a range of\n>> commits it would reduce the likelihood of conflicts to revert then in\n>> reverse order so we should either recommend passing '--reverse' or make\n>> that the default when '--revert' is given.\n>>\n>> As you can see in the log output above the new function to format the\n>> revert subject lines is buggy. If you had used test_commit_message() to\n>> check the commit message, rather than just grepping for ^Revert the\n>> tests would have picked that up.\n>>\n>> Thanks\n>>\n>> Phillip\n> I was going to say the same thing, but from a different angle.\n\n\nHi Elijah,\n\nthanks for the architectural clarity!\n\n\n>\n> The sequencer in git is used for three different types of operations:\n> rebasing, cherry-picking, and reverting a range (with a sequence of\n> reverts rather than one big revert).  In replay, these correspond to\n> --onto, --advance, and the new thing you are trying to add.  As such,\n> it should be its own new mode.\n\n\nThis makes complete sense. I was treating `--revert` as a modifier when \nit should be a third mode alongside `--onto` and `--advance`.\n\nI will restructure so that the user specifies exactly one of:\n   --onto <newbase>\n   --advance <branch>\n   --revert <target>\n\nWhere `--revert <target>` applies the reverts on top of <target> and \nupdates that ref.\n\n\n>\n> (I do tend to see ranges reverted by a single big revert, the way\n> Johannes suggested, rather than as a range of individual reverts,\n\n\nThe commit-by-commit approach is useful when you need:\n- Individual revert commits with proper \"This reverts commit X\" messages\n- The ability to later cherry-pick specific reverts\n- Clear history showing which commit caused which revert\n\nBut I will add documentation noting the `merge-tree` alternative for \ncases where a single combined revert is preferred.\n\nThanks,\nSiddharth\n\n\n>   so\n> to me the utility of the new mode looks low, but perhaps others find\n> more utility in it.  Or maybe the intent is to only use it with a\n> revision range that is only one commit long?)\n>\n> Phillip also went into more detail about why \"--onto $COMMIT --revert\"\n> specifically doesn't make sense.  I'd also say\n\n\n\n> \"--advance $BRANCH\n> --revert\" doesn't read well because to users, \"revert\" means going\n> back while \"advance\" means going forward,\n\n\nExactly - combining these is semantically confusing even if it could be \nmade to work technically.\n\n\n>   so it's a rather confusing\n> command line to make them wrap their head around.\n>\n> And yes, Siddharth, you were right that the new mode should be\n> incompatible with --contained, but that's because --contained is a\n> special modifier of --onto.  --onto, --advance, and --revert are three\n> different modes that are incompatible with each other.  Once you've\n> checked for that incompatibility between the three modes, then you can\n> either check that whenever --contained is specified, either --onto is\n> as well, or neither --advance nor --revert are.\n\n\n\nRight. The check becomes:\n1. Exactly one of --onto, --advance, --revert must be specified\n2. --contained requires --onto\n\nThis is much cleaner than my current approach of pairwise \nincompatibility checks.\n\n"},{"id":"531333","messageId":"xmqqcy54mro6.fsf@gitster.g","threadId":"64529","inReplyTo":"27fef9e1-bf26-48af-b3df-35948937c891@gmail.com","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-26T21:04:41Z","receivedAt":"2025-11-26T21:04:45Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> 1. For quick undoing an entire MR, the `merge-tree` approach you \n> suggest is indeed more efficient and avoids unnecessary intermediate \n> conflicts.\n>\n> 2. For commit-by-commit reverts, we need individual revert commits with \n> proper attribution (which commit is being reverted) for auditability and \n> history clarity. This is particularly useful when only specific commits \n> from a merged branch need to be reverted.\n\nThese are both good workflows with appropriate uses.  To make the\ntool useful for #2, it needs to be able to allow \"I have merged a\ntopic with 7 commits, but the first commit and the fourth commit are\nfaulty and I need to revert them\", i.e., not just a range (like\n\"rebase\" and \"cherry-pick\" workflows take), but a set of commits\nthat are potentially disconnected.  The current command line\narguments \"git replay\" supports, or \"git revert A..B\" for that\nmatter, are not exactly a good fit for such a use case, although the\nuser can of course run two single-commit revert operations in a row.\n\n"},{"id":"531334","messageId":"xmqq8qfsmraa.fsf@gitster.g","threadId":"64529","inReplyTo":"706e2875-a3f9-447f-9f43-690990a2342d@gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-26T21:13:01Z","receivedAt":"2025-11-26T21:13:04Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n>>> +This creates new commits on top of 'main' that reverse the changes introduced\n>>> +by the last three commits on 'feature'. The 'feature' branch is updated to\n>>> +point at the last of these revert commits. The 'main' branch is not updated\n>>> +in this case.\n>> Is there any topological requirement between 'main' and 'feature'\n>> branches?\n>\n> Yes, and I failed to explain this. For reverts to produce meaningful \n> non-empty commits, the commits being reverted should already be in the \n> target branch's history. I will clarify the examples to show this \n> topology requirement explicitly.\n\nWe need to be a bit careful, though.  Strictly speaking, what we\nhave is not a requirement on the shape of the history.  If a topic\nthat was merged to the development branch gets cherry-picked to the\nmaster branch, and then it turns out to be faulty and needs to be\nreverted, we can still \"revert\" the original topic out of the master\nbranch, even though topologically, the original topic is *not* in\n'master'.\n\n>>> +\t\t/* For revert: swap base and pickme to reverse the diff */\n>>> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>>> +\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", short_commit_name(repo, pickme));\n>> That is an overly long line (sorry, I notice these things when a\n>> line does not even fit in 92-col terminal).\n>\n>\n> Fixed in my local tree by introducing a `pickme_name` variable.\n\nJust a line-folding at an appropriate column may be sufficient, e.g.,\n\n\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\",\n\t\t\t\t\tshort_commit_name(repo, pickme));\n\n>>> -\tfree((char*)merge_opt->ancestor);\n>>>   \tmerge_opt->ancestor = NULL;\n>>> +\tmerge_opt->branch2 = NULL;\n>> Not a new problem, but what is the point of setting these two (but\n>> not branch1) to NULL?\n>\n>\n> You're right, this is inconsistent. The intent is to prevent \n> use-after-free, but setting only some fields to NULL is incomplete. I \n> will either set all three to NULL or add a comment explaining the rationale.\n\nIs this the only place that resets a subset of merge_opt members for\nreuse?  If not, are these multiple places want to reset the same\nsubset of the members?  Perhaps we can use a helper function to\nclarify in such a case.\n\n"},{"id":"531335","messageId":"xmqq3460mr3c.fsf@gitster.g","threadId":"64529","inReplyTo":"xmqq3460ocv7.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-26T21:17:11Z","receivedAt":"2025-11-26T21:17:14Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Elijah Newren <newren@gmail.com> writes:\n>\n>>> I'm struggling to understand when I'd want to do this. Why would I want\n>>> to update 'feature' to point to the reverted version of its last tree\n>>> commits rebased onto 'main'?\n>>> ...\n>> I was going to say the same thing, but from a different angle.\n>>\n>> The sequencer in git is used for three different types of operations:\n>> rebasing, cherry-picking, and reverting a range (with a sequence of\n>> reverts rather than one big revert).  In replay, these correspond to\n>> --onto, --advance, and the new thing you are trying to add.  As such,\n>> it should be its own new mode.\n>\n> This is a great comment that clarifies what the problem is with this.\n\nStepping back a bit, is it just me who thinks that the \"--onto\"\noption is a misnamed \"--rebase\", and the \"--advance\" option is a\nmisnamed \"--cherry-pick\"?\n\nPerhaps it is already way too late to remedy, but if we ever want to\nchange it, we should do so while \"replay\" is still marked experimental.\n\n\n"},{"id":"531342","messageId":"CABPp-BHcCX8LDccRoarsqNO=YVr7a8gp67oc87b7taAmjch4dQ@mail.gmail.com","threadId":"64529","inReplyTo":"xmqq3460mr3c.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-26T23:06:19Z","receivedAt":"2025-11-26T23:06:31Z","isPatch":true,"body":"On Wed, Nov 26, 2025 at 1:17 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n> > Elijah Newren <newren@gmail.com> writes:\n> >\n> >>> I'm struggling to understand when I'd want to do this. Why would I want\n> >>> to update 'feature' to point to the reverted version of its last tree\n> >>> commits rebased onto 'main'?\n> >>> ...\n> >> I was going to say the same thing, but from a different angle.\n> >>\n> >> The sequencer in git is used for three different types of operations:\n> >> rebasing, cherry-picking, and reverting a range (with a sequence of\n> >> reverts rather than one big revert).  In replay, these correspond to\n> >> --onto, --advance, and the new thing you are trying to add.  As such,\n> >> it should be its own new mode.\n> >\n> > This is a great comment that clarifies what the problem is with this.\n>\n> Stepping back a bit, is it just me who thinks that the \"--onto\"\n> option is a misnamed \"--rebase\", and the \"--advance\" option is a\n> misnamed \"--cherry-pick\"?\n\nIs the goal to make connections between existing commands for folks\nalready very familiar with git, at the expense of comprehensibility\nfor new users and command lines that look somewhat illogical?\n\nFor either a rebase or a cherry-pick operation you have: (A) a range\nof commits to be transplanted, (B) a base on which to build from, and\n(C) the choice of which ref(s) should be updated to point to the\ntransplanted commits.  cherry-pick assumed HEAD for both (B) and (C).\nrebase formed an implicit range instead of letting the user specify\n(in a way which has always made it difficult to teach to new users,\nIMO, but I digress), which involves HEAD and also used HEAD for (C).\ngit replay removes all assumptions about HEAD, which means there is\nmuch more freedom for (A), (B), and (C), but I think it also makes it\nmore important to try to make command lines at least a bit more\nself-describing for users to learn.\n\n== Example command lines today ==\n\n  git replay --onto main feature~3..feature\n\nThis command replays the commits in the range feature~3..feature onto\nmain, and updates feature to point at the result.\n\n  git replay --advance main feature~3..feature\n\nThis command replays the commits in the range feature~3..feature onto\nmain, and advances main to point at the result.\n\n(Both replay the same commit range on the same base, they differ only\nin which refs are updated at the end.)\n\n== Example command lines from your proposal ==\n\n   git replay --rebase main feature~3..feature\n\nThis command to me would suggest that main is being rebased, but it\nisn't -- it rebases feature~3..feature onto main while updating\nfeature to point at the result.  I find the \"--rebase main\" part of\nthis command line confusing.\n\n   git replay --cherry-pick main feature~3..feature\n\nThis command to me would suggest that main is being cherry-picked, but\nit isn't -- it cherry-picks feature~3..feature onto main while\nupdating main to point at the result.  Again, I find the\n\"--cherry-pick main\" part of this command line confusing.\n"},{"id":"531343","messageId":"xmqqy0nsl741.fsf@gitster.g","threadId":"64529","inReplyTo":"CABPp-BHcCX8LDccRoarsqNO=YVr7a8gp67oc87b7taAmjch4dQ@mail.gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-26T23:14:06Z","receivedAt":"2025-11-26T23:14:10Z","isPatch":true,"body":"Elijah Newren <newren@gmail.com> writes:\n\n> == Example command lines from your proposal ==\n>\n>    git replay --rebase main feature~3..feature\n>\n> This command to me would suggest that main is being rebased, but it\n> isn't -- it rebases feature~3..feature onto main while updating\n> feature to point at the result.  I find the \"--rebase main\" part of\n> this command line confusing.\n>\n>    git replay --cherry-pick main feature~3..feature\n>\n> This command to me would suggest that main is being cherry-picked, but\n> it isn't -- it cherry-picks feature~3..feature onto main while\n> updating main to point at the result.  Again, I find the\n> \"--cherry-pick main\" part of this command line confusing.\n\nThat only tells us that if you want to help users by limiting the\nvocabulary to a single set (i.e. both command names, and mode names\nused in replay), you'd need to make sure you have the order of\n<branch> and <range> given to the replay command in logical order,\nin line with the option name, no?  Of course, if you want to say\n\"cherry-pick\", cherry-picked range would have to come near the\noption flag that says \"cherry-pick\", naturally.\n"},{"id":"531345","messageId":"CABPp-BESM4PC+QVXZ-X_Y0m3PrSQGuc-jfB2pCJ+hXy0Gi-T5A@mail.gmail.com","threadId":"64529","inReplyTo":"xmqqy0nsl741.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-26T23:57:06Z","receivedAt":"2025-11-26T23:57:18Z","isPatch":true,"body":"On Wed, Nov 26, 2025 at 3:14 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> > == Example command lines from your proposal ==\n> >\n> >    git replay --rebase main feature~3..feature\n> >\n> > This command to me would suggest that main is being rebased, but it\n> > isn't -- it rebases feature~3..feature onto main while updating\n> > feature to point at the result.  I find the \"--rebase main\" part of\n> > this command line confusing.\n> >\n> >    git replay --cherry-pick main feature~3..feature\n> >\n> > This command to me would suggest that main is being cherry-picked, but\n> > it isn't -- it cherry-picks feature~3..feature onto main while\n> > updating main to point at the result.  Again, I find the\n> > \"--cherry-pick main\" part of this command line confusing.\n>\n> That only tells us that if you want to help users by limiting the\n> vocabulary to a single set (i.e. both command names, and mode names\n> used in replay), you'd need to make sure you have the order of\n> <branch> and <range> given to the replay command in logical order,\n> in line with the option name, no?  Of course, if you want to say\n> \"cherry-pick\", cherry-picked range would have to come near the\n> option flag that says \"cherry-pick\", naturally.\n\n--advance and --onto are flags that require an argument -- in this\ncase, \"main\".  So, now you're suggesting more than renaming, in\nparticular some bigger refactoring such as making these flags now\nrequire the <range> rather than the <base>.  Let's follow that path a\nbit further...\n\nDoes your proposal assume that <range> is simple, such as\n\"feature~3..feature\" above (i.e. something that an argument parser\nwould view as a single argument)?  What if the <range> were \"^main\nfeature1 feature2\"?  Or \"\"^$COMMIT --ancestry-path --branches\"?  (I\ndon't see how to have the option parser easily be able to stuff the\narguments to \"--rebase ^$COMMIT --ancestry-path --branches\" into a\nrange variable that eats all of \"^$COMMIT --ancestry-path\n--branches\".)  While I use simple ranges to describe the feature, I\nspecifically built the command to be able to do things like those\nother two examples and use it for those.  Those more complicated\nexamples are things the rebase command just can't do.\n\nAlso, just like `git log` allows `git log [<options>] [<revision\nrange>]`, I wanted git replay to allow `git replay [<options>]\n[<revision range>]`.  Instead of doing magic to get an implicit\nrevision range as rebase does (and with rather limited options because\nof that magic), suddenly people can use what they've learned from `git\nlog` in another place.  But that piece of knowledge only really\ntransfers if we do similarly to `git log`, i.e. the revision range\ncomes after other options.\n\nPerhaps one way to avoid the first problem above is to make\n`--onto/--advance/--rebase/--cherry-pick\" stop requiring (or\naccepting) an argument and turn them into simple mode toggles, and\nthen make both <base> and <range> be positional arguments, with some\nwell-defined ordering.  However, if <base> comes before <revision>\nthen we still have the same problem as my previous email, whereas if\nit comes after, then we weaken or destroy the connection to `git log`\nI made above.  Maybe the connection to `git log` isn't that important.\nWhat I think is important either way, though, is if we use positional\narguments for both things instead of making (at least one) an option,\nthen I feel we are copying one of the designs of `git rebase` that\nmakes it hard for even me to use: I hate that it uses multiple\npositional arguments to define the operation; despite using the\ncommand heavily for 16-17 years and sending in lots of patches to\nimprove it, I still can't remember the order of those positional\narguments and have to look it up again when teaching others.  Maybe\nthat's a personal shortcoming, but I would really rather that either\n<base> or <revision> was an option flag.\n"},{"id":"531378","messageId":"bc974d63-b3f1-4ef9-8fc5-e7e719b6ae89@gmail.com","threadId":"64529","inReplyTo":"38b51e19-7939-4a5e-8ad0-2d8168bc0fac@gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-27T16:21:03Z","receivedAt":"2025-11-27T16:21:08Z","isPatch":true,"body":"Hi Siddharth\n\nOn 26/11/2025 19:39, Siddharth Asthana wrote:\n> \n> The realistic use case is reverting commits from a branch where those \n> commits already exist. For example:\n> \n>      git replay --revert main~3..main\n> \n> This would revert the last 3 commits on main, creating revert commits on \n> top of main.\n\nWe want to be able to revert an arbitary range of commits. That means we \nneed to give --revert a branch name to update in addition to the range \nof commits to revert. The following example would update \"main\", \nreverting all the commits from the branch \"feature\"\n\n\tgit replay --revert main main..feature\n\nThanks\n\nPhillip\n\n"},{"id":"531379","messageId":"fa403239-cae3-463b-8c62-8761116ec652@gmail.com","threadId":"64529","inReplyTo":"xmqqcy54mro6.fsf@gitster.g","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-27T19:21:41Z","receivedAt":"2025-11-27T19:21:49Z","isPatch":true,"body":"\nOn 27/11/25 02:34, Junio C Hamano wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> 1. For quick undoing an entire MR, the `merge-tree` approach you\n>> suggest is indeed more efficient and avoids unnecessary intermediate\n>> conflicts.\n>>\n>> 2. For commit-by-commit reverts, we need individual revert commits with\n>> proper attribution (which commit is being reverted) for auditability and\n>> history clarity. This is particularly useful when only specific commits\n>> from a merged branch need to be reverted.\n> These are both good workflows with appropriate uses.  To make the\n> tool useful for #2, it needs to be able to allow \"I have merged a\n> topic with 7 commits, but the first commit and the fourth commit are\n> faulty and I need to revert them\", i.e., not just a range\n\n\nSince replay uses the same rev-list machinery as `git log`, users can \nalready specify disconnected commits:\n\n     git replay --revert <target> <commit1> <commit4>\n\nI will add a test to verify this works and document the capability.\n\nThanks,\nSiddharth\n\n\n> (like\n> \"rebase\" and \"cherry-pick\" workflows take), but a set of commits\n> that are potentially disconnected.  The current command line\n> arguments \"git replay\" supports, or \"git revert A..B\" for that\n> matter, are not exactly a good fit for such a use case, although the\n> user can of course run two single-commit revert operations in a row.\n>\n"},{"id":"531380","messageId":"78e31e24-5eab-44d8-a7e0-d3efae6ca6cf@gmail.com","threadId":"64529","inReplyTo":"xmqq8qfsmraa.fsf@gitster.g","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-27T19:23:46Z","receivedAt":"2025-11-27T19:23:54Z","isPatch":true,"body":"\nOn 27/11/25 02:43, Junio C Hamano wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>>>> +This creates new commits on top of 'main' that reverse the changes introduced\n>>>> +by the last three commits on 'feature'. The 'feature' branch is updated to\n>>>> +point at the last of these revert commits. The 'main' branch is not updated\n>>>> +in this case.\n>>> Is there any topological requirement between 'main' and 'feature'\n>>> branches?\n>> Yes, and I failed to explain this. For reverts to produce meaningful\n>> non-empty commits, the commits being reverted should already be in the\n>> target branch's history. I will clarify the examples to show this\n>> topology requirement explicitly.\n> We need to be a bit careful, though.  Strictly speaking, what we\n> have is not a requirement on the shape of the history.\n\n\nRight - it's the changes that need to exist in the target tree, not the \ncommits themselves. Cherry-picked commits can be reverted even without \ntopological ancestry. I will fix the documentation.\n\n\n>    If a topic\n> that was merged to the development branch gets cherry-picked to the\n> master branch, and then it turns out to be faulty and needs to be\n> reverted, we can still \"revert\" the original topic out of the master\n> branch, even though topologically, the original topic is *not* in\n> 'master'.\n>\n>>>> +\t\t/* For revert: swap base and pickme to reverse the diff */\n>>>> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>>>> +\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", short_commit_name(repo, pickme));\n>>> That is an overly long line (sorry, I notice these things when a\n>>> line does not even fit in 92-col terminal).\n>>\n>> Fixed in my local tree by introducing a `pickme_name` variable.\n> Just a line-folding at an appropriate column may be sufficient, e.g.,\n\n\nWill do.\n\n\n>\n> \t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\",\n> \t\t\t\t\tshort_commit_name(repo, pickme));\n>\n>>>> -\tfree((char*)merge_opt->ancestor);\n>>>>    \tmerge_opt->ancestor = NULL;\n>>>> +\tmerge_opt->branch2 = NULL;\n>>> Not a new problem, but what is the point of setting these two (but\n>>> not branch1) to NULL?\n>>\n>> You're right, this is inconsistent. The intent is to prevent\n>> use-after-free, but setting only some fields to NULL is incomplete. I\n>> will either set all three to NULL or add a comment explaining the rationale.\n> Is this the only place that resets a subset of merge_opt members for\n> reuse?  If not, are these multiple places want to reset the same\n> subset of the members?  Perhaps we can use a helper function to\n> clarify in such a case.\n\n\nThis is the only place in replay.c. The reason we only NULL ancestor and \nbranch2 is that those are the ones allocated with xstrfmt() - branch1 \npoints to short_commit_name() which doesn't need freeing.\n\nA helper would make the intent clearer. I will look into adding one.\n\nThanks,\nSiddharth\n\n\n"},{"id":"531381","messageId":"20b34c44-a04e-4632-b69a-586ac7c97aa4@gmail.com","threadId":"64529","inReplyTo":"bc974d63-b3f1-4ef9-8fc5-e7e719b6ae89@gmail.com","subject":"Re: [PATCH 1/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-27T19:24:43Z","receivedAt":"2025-11-27T19:24:51Z","isPatch":true,"body":"\nOn 27/11/25 21:51, Phillip Wood wrote:\n> Hi Siddharth\n>\n> On 26/11/2025 19:39, Siddharth Asthana wrote:\n>>\n>> The realistic use case is reverting commits from a branch where those \n>> commits already exist. For example:\n>>\n>>      git replay --revert main~3..main\n>>\n>> This would revert the last 3 commits on main, creating revert commits \n>> on top of main.\n>\n> We want to be able to revert an arbitary range of commits. That means \n> we need to give --revert a branch name to update in addition to the \n> range of commits to revert. The following example would update \"main\", \n> reverting all the commits from the branch \"feature\"\n>\n>     git replay --revert main main..feature\n\n\nMakes sense. I will restructure --revert to take a branch argument, \nmaking it a proper mode alongside --onto and --advance:\n\n     git replay --revert <branch> <revision-range>\n\nThis keeps the syntax consistent with the other modes.\n\nThanks,\nSiddharth\n\n\n\n>\n> Thanks\n>\n> Phillip\n>\n"},{"id":"531383","messageId":"xmqqsedzjkls.fsf@gitster.g","threadId":"64529","inReplyTo":"fa403239-cae3-463b-8c62-8761116ec652@gmail.com","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-27T20:17:51Z","receivedAt":"2025-11-27T20:17:54Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n>> These are both good workflows with appropriate uses.  To make the\n>> tool useful for #2, it needs to be able to allow \"I have merged a\n>> topic with 7 commits, but the first commit and the fourth commit are\n>> faulty and I need to revert them\", i.e., not just a range\n>\n>\n> Since replay uses the same rev-list machinery as `git log`, users can \n> already specify disconnected commits:\n>\n>      git replay --revert <target> <commit1> <commit4>\n\nAh, OK.\n\nYes, you can use setup_revisions() command line parsing, intercept\nthe result of parsing before prepare_revilimit_list() reduces that\nto a contiguous set of commits that are reachable from some of the\npositive refs and are not reachable from any of the negative ones,\nso it certainly is doable.\n\n"},{"id":"531395","messageId":"CABPp-BFsDJVtR6RV8KugCW2vmbD1=rTOKLp2jeawRfuPUEsNEA@mail.gmail.com","threadId":"64529","inReplyTo":"fa403239-cae3-463b-8c62-8761116ec652@gmail.com","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-28T08:07:56Z","receivedAt":"2025-11-28T08:08:08Z","isPatch":true,"body":"On Thu, Nov 27, 2025 at 11:21 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> On 27/11/25 02:34, Junio C Hamano wrote:\n> > Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n> >\n> >> 1. For quick undoing an entire MR, the `merge-tree` approach you\n> >> suggest is indeed more efficient and avoids unnecessary intermediate\n> >> conflicts.\n> >>\n> >> 2. For commit-by-commit reverts, we need individual revert commits with\n> >> proper attribution (which commit is being reverted) for auditability and\n> >> history clarity. This is particularly useful when only specific commits\n> >> from a merged branch need to be reverted.\n> > These are both good workflows with appropriate uses.  To make the\n> > tool useful for #2, it needs to be able to allow \"I have merged a\n> > topic with 7 commits, but the first commit and the fourth commit are\n> > faulty and I need to revert them\", i.e., not just a range\n>\n>\n> Since replay uses the same rev-list machinery as `git log`, users can\n> already specify disconnected commits:\n>\n>      git replay --revert <target> <commit1> <commit4>\n\nNo, this command does not specify disconnected commits.  A <range> of\n\"<commit1> <commit4>\" specifies all commits in the history of either\n<commit1> or <commit4>.  Thus, this example command line would be\nasking to revert all commits in the history of either <commit1> or\n<commit4> (all the way back to the initial commit), rather than just\nreverting those two commits.  This is just like how\n   git log <commit1> <commit4>\nshows all commits in the history of either <commit1> or <commit4>\ninstead of just showing those two commits.\n\nThere isn't really a mechanism in replay right now to handle a\ndisconnected set of commits for either --advance or --revert.  If\nthere were, it'd probably look like\n\n   git replay --advance <branch> --no-walk <commit1> <commit4>\n\nbut the code isn't set up to check whether you specified --no-walk,\nand thinks \"Um, you specified multiple branches here and it's not\nclear the order in which to cherry-pick them\" so it throws an error:\n\n$ git replay --advance main --no-walk Commit1 Commit7\nfatal: cannot advance target with multiple sources because ordering\nwould be ill-defined\n\nIf you comment out the relevant check which dies with that error, then\nyou end up in some codepath that segfaults instead (not-properly\ninitialized commit/tree objects or something?).  I'm sure that could\nbe fixed, but \"users can already specify disconnected commits\" is just\nnot accurate.\n\n> I will add a test to verify this works and document the capability.\n\nSupporting --no-walk so that folks can do disconnected commits for\nboth --advance and --revert may be nice, but given that it's missing\nfor --advance already, it might be considered a separate change from\nyour current submission.  I'll leave that up to you.\n"},{"id":"531396","messageId":"c930d6df-5dc4-401f-a9a1-eb2f00b2e837@gmail.com","threadId":"64529","inReplyTo":"CABPp-BFsDJVtR6RV8KugCW2vmbD1=rTOKLp2jeawRfuPUEsNEA@mail.gmail.com","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-28T08:24:07Z","receivedAt":"2025-11-28T08:24:15Z","isPatch":true,"body":"\nOn 28/11/25 13:37, Elijah Newren wrote:\n> On Thu, Nov 27, 2025 at 11:21 AM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> On 27/11/25 02:34, Junio C Hamano wrote:\n>>> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>>>\n>>>> 1. For quick undoing an entire MR, the `merge-tree` approach you\n>>>> suggest is indeed more efficient and avoids unnecessary intermediate\n>>>> conflicts.\n>>>>\n>>>> 2. For commit-by-commit reverts, we need individual revert commits with\n>>>> proper attribution (which commit is being reverted) for auditability and\n>>>> history clarity. This is particularly useful when only specific commits\n>>>> from a merged branch need to be reverted.\n>>> These are both good workflows with appropriate uses.  To make the\n>>> tool useful for #2, it needs to be able to allow \"I have merged a\n>>> topic with 7 commits, but the first commit and the fourth commit are\n>>> faulty and I need to revert them\", i.e., not just a range\n>>\n>> Since replay uses the same rev-list machinery as `git log`, users can\n>> already specify disconnected commits:\n>>\n>>       git replay --revert <target> <commit1> <commit4>\n\n\nHi Elijah,\n\n\n> No, this command does not specify disconnected commits.  A <range> of\n> \"<commit1> <commit4>\" specifies all commits in the history of either\n> <commit1> or <commit4>.\n\n\nYou are right, I misspoke. I was conflating the command-line syntax with \nwhat the revision machinery actually produces after prepare_revision_walk().\n\n\n>    Thus, this example command line would be\n> asking to revert all commits in the history of either <commit1> or\n> <commit4> (all the way back to the initial commit), rather than just\n> reverting those two commits.  This is just like how\n>     git log <commit1> <commit4>\n> shows all commits in the history of either <commit1> or <commit4>\n> instead of just showing those two commits.\n>\n> There isn't really a mechanism in replay right now to handle a\n> disconnected set of commits for either --advance or --revert.\n\n\nRight, the check at line 190 in replay.c:\n\n     if (rinfo.positive_refexprs > 1)\n         die(_(\"cannot advance target with multiple sources...\"));\n\nfires before we even get to the revision walk.\n\n\n> If there were, it'd probably look like\n>\n>     git replay --advance <branch> --no-walk <commit1> <commit4>\n>\n> but the code isn't set up to check whether you specified --no-walk,\n> and thinks \"Um, you specified multiple branches here and it's not\n> clear the order in which to cherry-pick them\" so it throws an error:\n>\n> $ git replay --advance main --no-walk Commit1 Commit7\n> fatal: cannot advance target with multiple sources because ordering\n> would be ill-defined\n>\n> If you comment out the relevant check which dies with that error, then\n> you end up in some codepath that segfaults instead (not-properly\n> initialized commit/tree objects or something?).  I'm sure that could\n> be fixed, but \"users can already specify disconnected commits\" is just\n> not accurate.\n>\n>> I will add a test to verify this works and document the capability.\n> Supporting --no-walk so that folks can do disconnected commits for\n> both --advance and --revert may be nice, but given that it's missing\n> for --advance already, it might be considered a separate change from\n> your current submission.  I'll leave that up to you.\n\n\n\nAgreed. I will keep the current submission focused on basic --revert \nfunctionality. Supporting --no-walk for disconnected commits (benefiting \nboth --advance and --revert) would make a nice follow-up series.\n\nThanks for the correction.\n\nSiddharth\n\n"},{"id":"531401","messageId":"xmqq7bvajesl.fsf@gitster.g","threadId":"64529","inReplyTo":"c930d6df-5dc4-401f-a9a1-eb2f00b2e837@gmail.com","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-28T16:35:38Z","receivedAt":"2025-11-28T16:35:41Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> Agreed. I will keep the current submission focused on basic --revert \n> functionality. Supporting --no-walk for disconnected commits (benefiting \n> both --advance and --revert) would make a nice follow-up series.\n\nIf we want to have a useful support for disconnected set of commits,\n\"--no-walk\" is not the way to go, I would say.\n\nImagine \"among the 7-patch topic merged, the second commit (i.e.,\ntopic~5) and the final 3 (i.e., topic~3..topic) need to go\".  You'd\nwant to be able to say (without going into details of the syntax)\n\n    revert topic~5 topic~3..topic\n\nThe setup_revisions() parser is still the right thing to use to\nparse the command line arguments and pick out \"topic~5\" and\n\"topic~3..topic\", but instead of letting prepare_revision_walk()\nturn them into a single contiguous set of revisions, you'd need to\ncheck revs->cmdline->rev[] and\n\n (1) treat singleton as its own disconnected island that require no\n     walking,\n\n (2) treat A..B as a range and independently walk them, and\n\n (3) dedup the result from cmdline->rev[] elements into a set of\n     commits that are potentially disconnected.\n\nI agree 100% that this topic should not attempt to deal with a\ndisconnected set of commits.  That can and should be done as a\nseparate series.\n"},{"id":"531406","messageId":"CABPp-BF48AF9qoP_pUs1X=sUV-_G5BpsxnG6AEhQYkJkE_TBjA@mail.gmail.com","threadId":"64529","inReplyTo":"xmqq7bvajesl.fsf@gitster.g","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-28T17:07:04Z","receivedAt":"2025-11-28T17:07:16Z","isPatch":true,"body":"On Fri, Nov 28, 2025 at 8:35 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n> > Agreed. I will keep the current submission focused on basic --revert\n> > functionality. Supporting --no-walk for disconnected commits (benefiting\n> > both --advance and --revert) would make a nice follow-up series.\n>\n> If we want to have a useful support for disconnected set of commits,\n> \"--no-walk\" is not the way to go, I would say.\n>\n> Imagine \"among the 7-patch topic merged, the second commit (i.e.,\n> topic~5) and the final 3 (i.e., topic~3..topic) need to go\".  You'd\n> want to be able to say (without going into details of the syntax)\n>\n>     revert topic~5 topic~3..topic\n>\n> The setup_revisions() parser is still the right thing to use to\n> parse the command line arguments and pick out \"topic~5\" and\n> \"topic~3..topic\", but instead of letting prepare_revision_walk()\n> turn them into a single contiguous set of revisions, you'd need to\n> check revs->cmdline->rev[] and\n>\n>  (1) treat singleton as its own disconnected island that require no\n>      walking,\n>\n>  (2) treat A..B as a range and independently walk them, and\n>\n>  (3) dedup the result from cmdline->rev[] elements into a set of\n>      commits that are potentially disconnected.\n>\n> I agree 100% that this topic should not attempt to deal with a\n> disconnected set of commits.  That can and should be done as a\n> separate series.\n\nHow does one distinguish the \"topic~5\" in the range \"topic~5\ntopic~3..topic\" from\n  * the topic~5 in \"^topic~7 topic~5\"\n  * the \"topic1\" and \"topic2\" in \"^$OLD_COMMIT --ancestry-path topic1 topic2\"\n?\nI kind of think we still need some kind of flag (possibly implied by\ndefault for --advance and --revert but not for --onto?), though I\nagree it'd need to be a new one rather than --no-walk for your example\nto work.\n\nAnd if one can do this, should this flag also be added to other\ncommands, so that e.g. `git log <someflag> topic~5 topic~3..topic\"\nwould also show the commits in topic~3..topic plus topic~5?\n"},{"id":"531410","messageId":"xmqqtsydj2zk.fsf@gitster.g","threadId":"64529","inReplyTo":"CABPp-BF48AF9qoP_pUs1X=sUV-_G5BpsxnG6AEhQYkJkE_TBjA@mail.gmail.com","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-28T20:50:39Z","receivedAt":"2025-11-28T20:50:43Z","isPatch":true,"body":"Elijah Newren <newren@gmail.com> writes:\n\n> How does one distinguish the \"topic~5\" in the range \"topic~5\n> topic~3..topic\" from\n>   * the topic~5 in \"^topic~7 topic~5\"\n\nTwo answers.\n\n(1) You don't have to.  When you scan cmdline->rev[], you can notice\n    the ^topic-7 form and reject, saying \"we accept A..B but not ^A B.\"\n\n(2) Or you design and document the interpretation you implement when\n    you see a negative commit while you scan over cmdline->rev[].\n    Perhaps you may make \"^topic-7\" to require a positive commit\n    after it and convert \"^topic-7 topic5\" as if the user gave you a\n    single \"topic~7..topic~5\".  Or you may do something else.\n\nMy assumption has been (1).\n\n\n>   * the \"topic1\" and \"topic2\" in \"^$OLD_COMMIT --ancestry-path topic1 topic2\"\n\nI haven't thought it through, but doesn't ancetry-path imply you are\nreally interested in the traditional connected set of commits?  The\npath is a connected subset inside those commits after all, no?\n"},{"id":"531412","messageId":"CABPp-BHbm12gW_LhtfXRCzOHtE3hod4VMTN=cwe-aesVEqyPCQ@mail.gmail.com","threadId":"64529","inReplyTo":"xmqqtsydj2zk.fsf@gitster.g","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-28T22:03:55Z","receivedAt":"2025-11-28T22:04:07Z","isPatch":true,"body":"On Fri, Nov 28, 2025 at 12:50 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> > How does one distinguish the \"topic~5\" in the range \"topic~5\n> > topic~3..topic\" from\n> >   * the topic~5 in \"^topic~7 topic~5\"\n>\n> Two answers.\n>\n> (1) You don't have to.  When you scan cmdline->rev[], you can notice\n>     the ^topic-7 form and reject, saying \"we accept A..B but not ^A B.\"\n\nUsing revision ranges like ^A B C with --onto (rebasing several\nbranches at once) was one of the major usecases for git replay.\nBecause of that, even when I only have one branch, I often use ^A B\nover A..B.\n\nIt's also called out pretty explicitly in the manual:\n```\nWhen calling `git replay`, one does not need to specify a range of\ncommits to replay using the syntax `A..B`; any range expression will\ndo:\n\n------------\n$ git replay --onto origin/main ^base branch1 branch2 branch3\n------------\n\nThis will simultaneously rebase `branch1`, `branch2`, and `branch3`,\nall commits they have since `base`, playing them on top of\n`origin/main`. These three branches may have commits on top of `base`\nthat they have in common, but that does not need to be the case.\n```\n\nGranted, unlike --onto, using --advance (or --revert) with multiple\nbranches at once doesn't make sense because cherry-picking/reverting\nmultiple branches into one has an ill-defined ordering problem.  Since\nonly one branch is allowed, maybe we could make special rules for\n--advance and --revert, but it would feel a little weird to have a\nrevision range not actually be a revision range for some modes of the\ncommand while also having them be an actual revision range for the\nother mode.\n\n> (2) Or you design and document the interpretation you implement when\n>     you see a negative commit while you scan over cmdline->rev[].\n>     Perhaps you may make \"^topic-7\" to require a positive commit\n>     after it and convert \"^topic-7 topic5\" as if the user gave you a\n>     single \"topic~7..topic~5\".  Or you may do something else.\n\nPart of the point of replay was to \"use the same revision ranges that\ngit log allows\".  I agree that having a way to specify \"topic~3..topic\nplus the commit topic~5\" is useful for this command, but it's equally\nlikely to be useful for `git log`; people have asked for it before.\nHowever, there is currently no way to specify that set of revisions to\n`git log`.  Perhaps we should add some flag to revision walking that\nallows that kind of alternate rule?\n\nIf we do that, then both log and replay would benefit.  We could even\nconsider making that flag be implied by both --advance and --revert\n(but NOT by --onto).  If we do that, we would also need a way to\nnegate that flag, since someone will probably want a way to\ncherry-pick some disconnected branch down to the root commit, meaning\nthey really would want \"topic~5\" to be treated as a traditional range\nand not as a single commit.\n\n> >   * the \"topic1\" and \"topic2\" in \"^$OLD_COMMIT --ancestry-path topic1 topic2\"\n>\n> I haven't thought it through, but doesn't ancetry-path imply you are\n> really interested in the traditional connected set of commits?  The\n> path is a connected subset inside those commits after all, no?\n\nYes, I believe --ancestry-path makes it clear that a range is wanted.\nIt's not clear to me that we can always correctly guess\nold-style-range vs. new-style-range from the existing command line\narguments alone, particularly given the example above of single commit\nvs. commits back to the root.  I think an explicit flag is needed, if\nwe want to go this route.\n\n(Also, this feels like a fair amount of work, when people can easily\njust invoke replay multiple times...)\n"},{"id":"531425","messageId":"xmqqo6olgz0l.fsf@gitster.g","threadId":"64529","inReplyTo":"CABPp-BHbm12gW_LhtfXRCzOHtE3hod4VMTN=cwe-aesVEqyPCQ@mail.gmail.com","subject":"Re: [PATCH 0/1] replay: add --revert option to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-29T05:59:22Z","receivedAt":"2025-11-29T05:59:26Z","isPatch":true,"body":"Elijah Newren <newren@gmail.com> writes:\n\n> It's also called out pretty explicitly in the manual:\n> ```\n> When calling `git replay`, one does not need to specify a range of\n> commits to replay using the syntax `A..B`; any range expression will\n> do:\n\nWhen we use the phrase \"any range expression\" to refer to what\nprepare_revision_walk() produces, it by definition means we only\ndeal with a connected set.\n\nIt often is very handy to allow saying \"master..topic1 topic2\" or\n\"^master topic1 topic2\" or \"topic1 topic2 --not master\" to mean\n\"commits on these branches\", of course, and there are many such\nuseful use cases that do not require disjoint set (and that is why\nwe survived without any disjoint set support on \"git log\" side,\nexcept for individually specifying commits and say \"--no-walk\",\nwhich is still technically a \"set\" but not very useful one when the\nnumber of commits you individually have to specify becomes more than\na handful).\n\nIt only means that the documentation needs to be updated if we ever\nwant to introduce an extended form of the command line syntax that\nallows users to specify an unconnected set of commits.\n"},{"id":"531581","messageId":"20251202201611.22137-1-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20251125170056.34489-1-siddharthasthana31@gmail.com","subject":"[PATCH v2 0/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-12-02T20:16:09Z","receivedAt":"2025-12-02T20:16:23Z","isPatch":true,"body":"The `git replay` command performs server-side history rewriting without\nrequiring a working tree. While it currently supports cherry-picking\ncommits (--advance) and rebasing (--onto), it lacks the ability to\nrevert them.\n\nAt GitLab, we use replay in Gitaly for efficient server-side operations\non bare repositories. Adding revert functionality enables us to reverse\nproblematic commits directly on the server, eliminating client-side\nroundtrips and reducing network overhead.\n\nThe implementation follows the same approach as sequencer.c where\ncherry-pick and revert are the same merge operation but with swapped\narguments. For cherry-pick we merge(ancestor=parent, ours=current,\ntheirs=commit), while for revert we merge(ancestor=commit, ours=current,\ntheirs=parent). By swapping the base and pickme trees when calling\nmerge_incore_nonrecursive(), we effectively reverse the diff direction.\n\nThe series is structured as follows:\n\nPatch 1 extracts the revert message formatting logic into a shared\nsequencer_format_revert_header() function, eliminating code duplication\nbetween sequencer.c and the upcoming replay code. This follows Junio's\nsuggestion to split the changes.\n\nPatch 2 adds the --revert <branch> mode to git replay. Following the\narchitectural pattern suggested by Elijah and Phillip, --revert is a\nstandalone mode (like --onto and --advance) that takes a branch argument\nand updates that branch with the revert commits.\n\nThe series is based on top of f0ef5b6d9b (The fifth batch, 2025-11-29).\n\nCI: https://gitlab.com/gitlab-org/git/-/pipelines/2191734674\nThe static-analysis and Windows CI failures are pre-existing\ninfrastructure issues unrelated to this series.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\nChanges in v2:\n- Split into two patches as Junio suggested\n- Changed --revert from a modifier flag to a standalone mode\n  (--revert <branch>) that is mutually exclusive with --onto/--advance\n- Used enum replay_action instead of int is_revert\n- Fixed subject extraction bug in generate_revert_message()\n- Put common (cherry-pick) case first in if/else block\n- Fixed repo_unuse_commit_buffer() to use repo parameter consistently\n- Improved tests with test_commit_message() and atomic ref update handling\n- Link to v1: https://lore.kernel.org/git/20251125170055.64991-1-siddharthasthana31@gmail.com/\n\nSiddharth Asthana (2):\n  sequencer: extract revert message formatting into shared function\n  replay: add --revert mode to reverse commit changes\n\n Documentation/git-replay.adoc |  36 ++++++++-\n builtin/replay.c              | 145 ++++++++++++++++++++++++++++------\n sequencer.c                   |  39 +++++----\n sequencer.h                   |   8 ++\n t/t3650-replay-basics.sh      | 111 ++++++++++++++++++++++++++\n 5 files changed, 300 insertions(+), 39 deletions(-)\n\n\nRange-diff against v1:\n-:  ---------- > 1:  bfd75484b4 sequencer: extract revert message formatting into shared function\n1:  dd47a89a5b ! 2:  a2f99bc8c2 replay: add --revert option to reverse commit changes\n    @@ Metadata\n     Author: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Commit message ##\n    -    replay: add --revert option to reverse commit changes\n    +    replay: add --revert mode to reverse commit changes\n     \n         The `git replay` command performs server-side history rewriting without\n         requiring a working tree. While it currently supports cherry-picking\n    -    commits, it lacks the ability to revert them.\n    +    commits (--advance) and rebasing (--onto), it lacks the ability to\n    +    revert them.\n     \n         At GitLab, we use replay in Gitaly for efficient server-side operations\n         on bare repositories. Adding revert functionality enables us to reverse\n         problematic commits directly on the server, eliminating client-side\n         roundtrips and reducing network overhead.\n     \n    -    Add a `--revert` option that reverses the changes introduced by the\n    -    specified commits. The implementation follows the same approach as\n    -    `sequencer.c` (around lines 2358-2390), where cherry-pick and revert\n    -    are essentially the same merge operation but with swapped arguments:\n    +    Add a `--revert <branch>` mode that reverses the changes introduced by\n    +    the specified commits. Following the architecture of --onto and --advance,\n    +    --revert is a standalone mode that takes a branch argument and updates\n    +    that branch with the revert commits.\n    +\n    +    The implementation follows the same approach as sequencer.c (lines\n    +    2360-2399), where cherry-pick and revert are the same merge operation\n    +    but with swapped arguments:\n     \n           - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n           - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n     \n         We swap the base and pickme trees when calling\n    -    `merge_incore_nonrecursive()`, effectively reversing the diff direction.\n    +    merge_incore_nonrecursive(), effectively reversing the diff direction.\n         The existing conflict handling, ref updates, and atomic transaction\n         support work unchanged.\n     \n    -    The revert message generation logic (handling \"Revert\" and \"Reapply\"\n    -    cases) is extracted into a new `sequencer_format_revert_header()`\n    -    function in `sequencer.c`, which can be shared between `sequencer.c`\n    -    and `builtin/replay.c`. The existing code in `do_pick_commit()` is\n    -    updated to use this shared function, eliminating code duplication.\n    -    The `builtin/replay.c` code calls this shared function and then appends\n    -    the commit OID using `oid_to_hex()` directly, since git replay is\n    -    designed for simpler server-side operations without the interactive\n    -    features and `replay_opts` framework used by `sequencer.c`.\n    -\n    -    The commit messages follow `git revert` conventions: prefixed with\n    +    The commit messages follow git revert conventions: prefixed with\n         \"Revert\" and including the original commit SHA. When reverting a commit\n         that itself starts with \"Revert\", the message uses \"Reapply\" instead.\n         Unlike cherry-pick which preserves the original author, revert commits\n    -    use the current user as the author, matching the behavior of `git\n    -    revert`.\n    -\n    -    Mark the option as incompatible with `--contained` because reverting\n    -    changes across multiple branches simultaneously could produce unexpected\n    -    results when branches have interdependencies or shared history.\n    +    use the current user as the author, matching the behavior of git revert.\n     \n         Helped-by: Christian Couder <christian.couder@gmail.com>\n         Helped-by: Patrick Steinhardt <ps@pks.im>\n    +    Helped-by: Elijah Newren <newren@gmail.com>\n    +    Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n    +    Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\n    +    Helped-by: Junio C Hamano <gitster@pobox.com>\n         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Documentation/git-replay.adoc ##\n    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne\n      --------\n      [verse]\n     -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n    -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] [--revert] <revision-range>...\n    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-replay.adoc: which uses the target only as a starting point without updating it.\n    - +\n    - The default mode can be configured via the `replay.refAction` configuration variable.\n    +@@ Documentation/git-replay.adoc: The history is replayed on top of the <branch> and <branch> is updated to\n    + point at the tip of the resulting history. This is different from `--onto`,\n    + which uses the target only as a starting point without updating it.\n      \n    -+--revert::\n    -+\tRevert the changes introduced by the commits in the revision range\n    -+\tinstead of applying them. This reverses the diff direction and creates\n    -+\tnew commits that undo the changes, similar to `git revert`.\n    ++--revert <branch>::\n    ++\tStarting point at which to create the new revert commits; must be a\n    ++\tbranch name.\n     ++\n    -+The commit messages are prefixed with \"Revert\" and include the original\n    -+commit SHA. If reverting a commit whose message starts with \"Revert\", the new\n    -+message will start with \"Reapply\" instead. The author of the new commits\n    -+will be the current user, not the original commit author.\n    ++When `--revert` is specified, the commits in the revision range are reverted\n    ++(their changes are undone) and the revert commits are applied on top of <branch>.\n    ++The <branch> is then updated to point at the new commits. This is similar to\n    ++running `git revert` for each commit in the range, but works without a working tree.\n     ++\n    -+This option is incompatible with `--contained` because reverting changes\n    -+across multiple branches simultaneously could produce unexpected results\n    -+when branches have interdependencies or shared history.\n    ++The commit messages follow `git revert` conventions: prefixed with \"Revert\" and\n    ++including the original commit SHA. When reverting a commit whose message starts\n    ++with \"Revert\", the new message uses \"Reapply\" instead. The author of the revert\n    ++commits is the current user, not the original commit author.\n    +++\n    ++This option is mutually exclusive with `--onto` and `--advance`. It is also\n    ++incompatible with `--contained` (which is a modifier for `--onto` only).\n    ++\n     +\n    - <revision-range>::\n    - \tRange of commits to replay. More than one <revision-range> can\n    - \tbe passed, but in `--advance <branch>` mode, they should have\n    + --ref-action[=<mode>]::\n    + \tControl how references are updated. The mode can be:\n    + +\n     @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them on top of\n      `origin/main`. These three branches may have commits on top of `base`\n      that they have in common, but that does not need to be the case.\n      \n    -+To revert a range of commits:\n    ++To revert commits from a branch:\n     +\n     +------------\n    -+$ git replay --revert --onto main feature~3..feature\n    ++$ git replay --revert main feature~2..feature\n     +------------\n     +\n    -+This creates new commits on top of 'main' that reverse the changes introduced\n    -+by the last three commits on 'feature'. The 'feature' branch is updated to\n    -+point at the last of these revert commits. The 'main' branch is not updated\n    -+in this case.\n    ++This reverts the last two commits from 'feature', creating revert commits on\n    ++top of 'main', and updates 'main' to point at the result. This is useful when\n    ++commits from 'feature' were previously merged or cherry-picked into 'main' and\n    ++need to be undone.\n     +\n    -+To revert commits and advance a branch:\n    ++NOTE: For reverting an entire merge request as a single commit (rather than\n    ++commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n    ++which can avoid unnecessary merge conflicts.\n     +\n    -+------------\n    -+$ git replay --revert --advance main feature~2..feature\n    -+------------\n    -+\n    -+This reverts the last two commits from 'feature', applies those reverts\n    -+on top of 'main', and updates 'main' to point at the result. The 'feature'\n    -+branch is not updated in this case.\n     +\n      GIT\n      ---\n    @@ builtin/replay.c\n      #include \"strmap.h\"\n      #include <oidset.h>\n      #include <tree.h>\n    +@@ builtin/replay.c: enum ref_action_mode {\n    + \tREF_ACTION_PRINT,\n    + };\n    + \n    ++enum replay_action {\n    ++\tREPLAY_PICK,\n    ++\tREPLAY_REVERT,\n    ++};\n    ++\n    + static const char *short_commit_name(struct repository *repo,\n    + \t\t\t\t     struct commit *commit)\n    + {\n     @@ builtin/replay.c: static char *get_author(const char *message)\n      \treturn NULL;\n      }\n      \n    -+/*\n    -+ * Generates a revert commit message using the shared sequencer function.\n    -+ * We use oid_to_hex() directly instead of refer_to_commit() since git replay\n    -+ * is designed for simpler server-side operations without interactive features.\n    -+ */\n     +static void generate_revert_message(struct strbuf *msg,\n    -+\t\t\t\t    const char *orig_message,\n    -+\t\t\t\t    const struct object_id *oid)\n    ++\t\t\t\t    struct commit *commit,\n    ++\t\t\t\t    struct repository *repo)\n     +{\n    -+\tsequencer_format_revert_header(msg, orig_message);\n    -+\tstrbuf_addstr(msg, oid_to_hex(oid));\n    ++\tconst char *out_enc = get_commit_output_encoding();\n    ++\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n    ++\tconst char *subject_start;\n    ++\tint subject_len;\n    ++\tchar *subject;\n    ++\n    ++\tsubject_len = find_commit_subject(message, &subject_start);\n    ++\tsubject = xmemdupz(subject_start, subject_len);\n    ++\n    ++\tsequencer_format_revert_header(msg, subject);\n    ++\tstrbuf_addstr(msg, oid_to_hex(&commit->object.oid));\n     +\tstrbuf_addstr(msg, \".\\n\");\n    ++\n    ++\tfree(subject);\n    ++\trepo_unuse_commit_buffer(repo, commit, message);\n     +}\n     +\n      static struct commit *create_commit(struct repository *repo,\n    @@ builtin/replay.c: static char *get_author(const char *message)\n      \t\t\t\t    struct commit *based_on,\n     -\t\t\t\t    struct commit *parent)\n     +\t\t\t\t    struct commit *parent,\n    -+\t\t\t\t    int is_revert)\n    ++\t\t\t\t    enum replay_action action)\n      {\n      \tstruct object_id ret;\n      \tstruct object *obj = NULL;\n     @@ builtin/replay.c: static struct commit *create_commit(struct repository *repo,\n    + \n      \tcommit_list_insert(parent, &parents);\n      \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n    - \tfind_commit_subject(message, &orig_message);\n    +-\tfind_commit_subject(message, &orig_message);\n     -\tstrbuf_addstr(&msg, orig_message);\n     -\tauthor = get_author(message);\n    -+\n    -+\tif (is_revert) {\n    -+\t\tgenerate_revert_message(&msg, orig_message, &based_on->object.oid);\n    -+\t\t/* For revert, use current user as author */\n    -+\t\tauthor = NULL;\n    ++\tif (action == REPLAY_REVERT) {\n    ++\t\tgenerate_revert_message(&msg, based_on, repo);\n    ++\t\tauthor = xstrdup(git_author_info(IDENT_STRICT));\n     +\t} else {\n    -+\t\t/* Cherry-pick mode: use original commit message and author */\n    ++\t\tfind_commit_subject(message, &orig_message);\n     +\t\tstrbuf_addstr(&msg, orig_message);\n     +\t\tauthor = get_author(message);\n     +\t}\n    -+\n      \treset_ident_date();\n      \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n      \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n    +@@ builtin/replay.c: static struct commit *create_commit(struct repository *repo,\n    + \tobj = parse_object(repo, &ret);\n    + \n    + out:\n    +-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n    ++\trepo_unuse_commit_buffer(repo, based_on, message);\n    + \tfree_commit_extra_headers(extra);\n    + \tfree_commit_list(parents);\n    + \tstrbuf_release(&msg);\n    +@@ builtin/replay.c: static void determine_replay_mode(struct repository *repo,\n    + \t\t\t\t  struct rev_cmdline_info *cmd_info,\n    + \t\t\t\t  const char *onto_name,\n    + \t\t\t\t  char **advance_name,\n    ++\t\t\t\t  char **revert_name,\n    + \t\t\t\t  struct commit **onto,\n    + \t\t\t\t  struct strset **update_refs)\n    + {\n    +@@ builtin/replay.c: static void determine_replay_mode(struct repository *repo,\n    + \t\t}\n    + \t\tif (rinfo.positive_refexprs > 1)\n    + \t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n    ++\t} else if (*revert_name) {\n    ++\t\tstruct object_id oid;\n    ++\t\tchar *fullname = NULL;\n    ++\n    ++\t\t*onto = peel_committish(repo, *revert_name);\n    ++\t\tif (repo_dwim_ref(repo, *revert_name, strlen(*revert_name),\n    ++\t\t\t\t  &oid, &fullname, 0) == 1) {\n    ++\t\t\tfree(*revert_name);\n    ++\t\t\t*revert_name = fullname;\n    ++\t\t} else {\n    ++\t\t\tdie(_(\"argument to --revert must be a reference\"));\n    ++\t\t}\n    ++\t\tif (rinfo.positive_refexprs > 1)\n    ++\t\t\tdie(_(\"cannot revert with multiple sources because ordering would be ill-defined\"));\n    + \t} else {\n    + \t\tint positive_refs_complete = (\n    + \t\t\trinfo.positive_refexprs ==\n     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *repo,\n      \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n      \t\t\t\t\t  struct commit *onto,\n      \t\t\t\t\t  struct merge_options *merge_opt,\n     -\t\t\t\t\t  struct merge_result *result)\n     +\t\t\t\t\t  struct merge_result *result,\n    -+\t\t\t\t\t  int is_revert)\n    ++\t\t\t\t\t  enum replay_action action)\n      {\n      \tstruct commit *base, *replayed_base;\n      \tstruct tree *pickme_tree, *base_tree;\n    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n     -\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n     -\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n     -\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n    --\n    ++\tif (action == REPLAY_PICK) {\n    ++\t\t/* Cherry-pick: normal order */\n    ++\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n    ++\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n    ++\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n    + \n     -\tmerge_incore_nonrecursive(merge_opt,\n     -\t\t\t\t  base_tree,\n     -\t\t\t\t  result->tree,\n     -\t\t\t\t  pickme_tree,\n     -\t\t\t\t  result);\n    -+\tif (is_revert) {\n    -+\t\t/* For revert: swap base and pickme to reverse the diff */\n    ++\t\tmerge_incore_nonrecursive(merge_opt,\n    ++\t\t\t\t\t  base_tree,\n    ++\t\t\t\t\t  result->tree,\n    ++\t\t\t\t\t  pickme_tree,\n    ++\t\t\t\t\t  result);\n    + \n    +-\tfree((char*)merge_opt->ancestor);\n    ++\t\tfree((char *)merge_opt->ancestor);\n    ++\t} else {\n    ++\t\t/* Revert: swap base and pickme to reverse the diff */\n     +\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n     +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n     +\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n     +\t\t\t\t\t  base_tree,\n     +\t\t\t\t\t  result);\n     +\n    -+\t\t/* branch2 was allocated with xstrfmt, needs freeing */\n     +\t\tfree((char *)merge_opt->branch2);\n    -+\t} else {\n    -+\t\t/* For cherry-pick: normal order */\n    -+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n    -+\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n    -+\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n    -+\n    -+\t\tmerge_incore_nonrecursive(merge_opt,\n    -+\t\t\t\t\t  base_tree,\n    -+\t\t\t\t\t  result->tree,\n    -+\t\t\t\t\t  pickme_tree,\n    -+\t\t\t\t\t  result);\n    -+\n    -+\t\t/* ancestor was allocated with xstrfmt, needs freeing */\n    -+\t\tfree((char *)merge_opt->ancestor);\n     +\t}\n    - \n    --\tfree((char*)merge_opt->ancestor);\n      \tmerge_opt->ancestor = NULL;\n     +\tmerge_opt->branch2 = NULL;\n      \tif (!result->clean)\n      \t\treturn NULL;\n     -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n    -+\treturn create_commit(repo, result->tree, pickme, replayed_base, is_revert);\n    ++\treturn create_commit(repo, result->tree, pickme, replayed_base, action);\n      }\n      \n      static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n     @@ builtin/replay.c: int cmd_replay(int argc,\n    + {\n    + \tconst char *advance_name_opt = NULL;\n    + \tchar *advance_name = NULL;\n    ++\tconst char *revert_name_opt = NULL;\n    ++\tchar *revert_name = NULL;\n    ++\tenum replay_action action = REPLAY_PICK;\n    + \tstruct commit *onto = NULL;\n    + \tconst char *onto_name = NULL;\n      \tint contained = 0;\n    - \tconst char *ref_action = NULL;\n    - \tenum ref_action_mode ref_mode;\n    -+\tint is_revert = 0;\n    - \n    - \tstruct rev_info revs;\n    - \tstruct commit *last_commit = NULL;\n     @@ builtin/replay.c: int cmd_replay(int argc,\n    + \n      \tconst char *const replay_usage[] = {\n      \t\tN_(\"(EXPERIMENTAL!) git replay \"\n    - \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n    --\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n    -+\t\t   \"[--ref-action[=<mode>]] [--revert] <revision-range>...\"),\n    +-\t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n    ++\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n    + \t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n      \t\tNULL\n      \t};\n    - \tstruct option replay_options[] = {\n     @@ builtin/replay.c: int cmd_replay(int argc,\n    + \t\t\t   N_(\"replay onto given commit\")),\n    + \t\tOPT_BOOL(0, \"contained\", &contained,\n    + \t\t\t N_(\"advance all branches contained in revision-range\")),\n    ++\t\tOPT_STRING(0, \"revert\", &revert_name_opt,\n    ++\t\t\t   N_(\"branch\"),\n    ++\t\t\t   N_(\"revert commits onto given branch\")),\n      \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n      \t\t\t   N_(\"mode\"),\n      \t\t\t   N_(\"control ref update behavior (update|print)\")),\n    -+\t\tOPT_BOOL(0, \"revert\", &is_revert,\n    -+\t\t\t N_(\"revert commits instead of cherry-picking them\")),\n    - \t\tOPT_END()\n    - \t};\n    - \n     @@ builtin/replay.c: int cmd_replay(int argc,\n    + \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n    + \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n    + \n    +-\tif (!onto_name && !advance_name_opt) {\n    +-\t\terror(_(\"option --onto or --advance is mandatory\"));\n    ++\t/* Exactly one mode must be specified */\n    ++\tif (!onto_name && !advance_name_opt && !revert_name_opt) {\n    ++\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n    + \t\tusage_with_options(replay_usage, replay_options);\n    + \t}\n    + \n      \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n    - \t\t\t\t  contained, \"--contained\");\n    +-\t\t\t\t  contained, \"--contained\");\n    ++\t\t\t\t  !!onto_name, \"--onto\");\n    ++\tdie_for_incompatible_opt2(!!revert_name_opt, \"--revert\",\n    ++\t\t\t\t  !!onto_name, \"--onto\");\n    ++\tdie_for_incompatible_opt2(!!revert_name_opt, \"--revert\",\n    ++\t\t\t\t  !!advance_name_opt, \"--advance\");\n    ++\tdie_for_incompatible_opt2(contained, \"--contained\",\n    ++\t\t\t\t  !onto_name, \"requires --onto\");\n      \n    -+\t/* --revert is incompatible with --contained */\n    -+\tdie_for_incompatible_opt2(is_revert, \"--revert\",\n    -+\t\t\t\t  contained, \"--contained\");\n    -+\n      \t/* Parse ref action mode from command line or config */\n      \tref_mode = get_ref_action_mode(repo, ref_action);\n      \n    + \tadvance_name = xstrdup_or_null(advance_name_opt);\n    ++\trevert_name = xstrdup_or_null(revert_name_opt);\n    ++\tif (revert_name)\n    ++\t\taction = REPLAY_REVERT;\n    + \n    + \trepo_init_revisions(repo, &revs, prefix);\n    + \n    +@@ builtin/replay.c: int cmd_replay(int argc,\n    + \t}\n    + \n    + \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n    ++\t\t\t      &revert_name,\n    + \t\t\t      &onto, &update_refs);\n    + \n    + \t/* Build reflog message */\n    +-\tif (advance_name_opt)\n    ++\tif (revert_name_opt)\n    ++\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", revert_name_opt);\n    ++\telse if (advance_name_opt)\n    + \t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n    + \telse\n    + \t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\",\n     @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n      \n      \t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n     -\t\t\t\t\t\t  onto, &merge_opt, &result);\n    -+\t\t\t\t\t\t  onto, &merge_opt, &result,\n    -+\t\t\t\t\t\t  is_revert);\n    ++\t\t\t\t\t\t  onto, &merge_opt, &result, action);\n      \t\tif (!last_commit)\n      \t\t\tbreak;\n      \n    -\n    - ## sequencer.c ##\n    -@@ sequencer.c: static int do_pick_commit(struct repository *r,\n    - \t\tif (opts->commit_use_reference) {\n    - \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n    - \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n    --\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n    --\t\t\t   /*\n    --\t\t\t    * We don't touch pre-existing repeated reverts, because\n    --\t\t\t    * theoretically these can be nested arbitrarily deeply,\n    --\t\t\t    * thus requiring excessive complexity to deal with.\n    --\t\t\t    */\n    --\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n    --\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n    --\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n    --\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n    -+\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n    - \t\t} else {\n    --\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n    --\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n    --\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n    -+\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n    - \t\t}\n    --\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n    - \t\trefer_to_commit(opts, &ctx->message, commit);\n    +@@ builtin/replay.c: int cmd_replay(int argc,\n    + \t\tkh_value(replayed_commits, pos) = last_commit;\n      \n    - \t\tif (commit->parents && commit->parents->next) {\n    -@@ sequencer.c: int sequencer_pick_revisions(struct repository *r,\n    - \treturn res;\n    - }\n    + \t\t/* Update any necessary branches */\n    +-\t\tif (advance_name)\n    ++\t\tif (advance_name || revert_name)\n    + \t\t\tcontinue;\n    + \t\tdecoration = get_name_decoration(&commit->object);\n    + \t\tif (!decoration)\n    +@@ builtin/replay.c: int cmd_replay(int argc,\n    + \t\t}\n    + \t}\n      \n    -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)\n    -+{\n    -+\tconst char *revert_subject;\n    -+\n    -+\tif (skip_prefix(orig_subject, \"Revert \\\"\", &revert_subject) &&\n    -+\t    /*\n    -+\t     * We don't touch pre-existing repeated reverts, because\n    -+\t     * theoretically these can be nested arbitrarily deeply,\n    -+\t     * thus requiring excessive complexity to deal with.\n    -+\t     */\n    -+\t    !starts_with(revert_subject, \"Revert \\\"\")) {\n    -+\t\tstrbuf_addstr(out, \"Reapply \\\"\");\n    -+\t\tstrbuf_addstr(out, revert_subject);\n    -+\t\tstrbuf_addch(out, '\\n');\n    -+\t} else {\n    -+\t\tstrbuf_addstr(out, \"Revert \\\"\");\n    -+\t\tstrbuf_addstr(out, orig_subject);\n    -+\t\tstrbuf_addstr(out, \"\\\"\\n\");\n    +-\t/* In --advance mode, advance the target ref */\n    ++\t/* In --advance or --revert mode, update the target ref */\n    + \tif (result.clean == 1 && advance_name) {\n    + \t\tif (handle_ref_update(ref_mode, transaction, advance_name,\n    + \t\t\t\t      &last_commit->object.oid,\n    +@@ builtin/replay.c: int cmd_replay(int argc,\n    + \t\t\tgoto cleanup;\n    + \t\t}\n    + \t}\n    ++\tif (result.clean == 1 && revert_name) {\n    ++\t\tif (handle_ref_update(ref_mode, transaction, revert_name,\n    ++\t\t\t\t      &last_commit->object.oid,\n    ++\t\t\t\t      &onto->object.oid,\n    ++\t\t\t\t      reflog_msg.buf,\n    ++\t\t\t\t      &transaction_err) < 0) {\n    ++\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n    ++\t\t\t\t    revert_name, transaction_err.buf);\n    ++\t\t\tgoto cleanup;\n    ++\t\t}\n     +\t}\n    -+\n    -+\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n    -+}\n    -+\n    - void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n    - {\n    - \tunsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;\n    -\n    - ## sequencer.h ##\n    -@@ sequencer.h: int todo_list_rearrange_squash(struct todo_list *todo_list);\n    -  */\n    - void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag);\n      \n    -+/*\n    -+ * Formats a revert commit message header following standard Git conventions.\n    -+ * Handles both regular reverts (\"Revert \\\"<subject>\\\"\") and revert of revert\n    -+ * cases (\"Reapply \\\"<subject>\\\"\"). Adds \"This reverts commit \" at the end.\n    -+ * The caller should append the commit OID after calling this function.\n    -+ */\n    -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject);\n    -+\n    - void append_conflicts_hint(struct index_state *istate,\n    - \t\tstruct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode);\n    - enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg,\n    + \t/* Commit the ref transaction if we have one */\n    + \tif (transaction && result.clean == 1) {\n     \n      ## t/t3650-replay-basics.sh ##\n     @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '\n      \ttest_grep \"invalid.*replay.refAction.*value\" error\n      '\n      \n    -+test_expect_success 'using replay with --revert to revert a commit' '\n    -+\t# Revert commits D and E from topic2\n    -+\tgit replay --revert --onto topic1 topic1..topic2 >result &&\n    -+\n    -+\ttest_must_be_empty result &&\n    -+\n    -+\t# Verify the commit messages contain \"Revert\"\n    -+\t# topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A)\n    -+\tgit log --format=%s topic2 >actual &&\n    -+\ttest_line_count = 6 actual &&\n    -+\thead -n 1 actual >first-line &&\n    -+\ttest_grep \"^Revert\" first-line\n    ++test_expect_success 'setup for revert tests' '\n    ++\tgit switch -c revert-test main &&\n    ++\ttest_commit R1 &&\n    ++\ttest_commit R2 &&\n    ++\ttest_commit R3 &&\n    ++\tgit switch main\n     +'\n     +\n    -+test_expect_success 'using replay with --revert on bare repo' '\n    -+\tgit -C bare replay --revert --onto topic1 topic1..topic2 >result-bare &&\n    -+\n    -+\ttest_must_be_empty result-bare &&\n    -+\n    -+\t# Verify the commit message contains \"Revert\"\n    -+\tgit -C bare log --format=%s topic2 >actual-bare &&\n    -+\ttest_line_count = 6 actual-bare &&\n    -+\thead -n 1 actual-bare >first-line-bare &&\n    -+\ttest_grep \"^Revert\" first-line-bare\n    ++test_expect_success 'git replay --revert reverts commits' '\n    ++\t# Store original state\n    ++\tSTART=$(git rev-parse revert-test) &&\n    ++\ttest_when_finished \"git branch -f revert-test $START\" &&\n    ++\n    ++\tgit replay --revert revert-test revert-test~2..revert-test >output &&\n    ++\ttest_must_be_empty output &&\n    ++\n    ++\t# Verify revert-test was updated with revert commits\n    ++\tgit log --format=%s -n 5 revert-test >actual &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\tRevert \"R3\"\n    ++\tRevert \"R2\"\n    ++\tR3\n    ++\tR2\n    ++\tR1\n    ++\tEOF\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\t# Verify commit message format\n    ++\ttest_commit_message revert-test -m \"Revert \\\"R3\\\"\n    ++\n    ++This reverts commit $(git rev-parse R3).\"\n     +'\n     +\n    -+test_expect_success 'using replay with --revert and --advance' '\n    -+\t# Revert commits from topic2 and advance main\n    -+\tgit replay --revert --advance main topic1..topic2 >result &&\n    ++test_expect_success 'git replay --revert with --ref-action=print' '\n    ++\t# Store original state\n    ++\tSTART=$(git rev-parse revert-test) &&\n    ++\ttest_when_finished \"git branch -f revert-test $START\" &&\n     +\n    -+\ttest_must_be_empty result &&\n    ++\tgit replay --ref-action=print --revert revert-test revert-test~2..revert-test >result &&\n    ++\ttest_line_count = 1 result &&\n     +\n    -+\t# Verify the commit message contains \"Revert\"\n    -+\tgit log --format=%s main >actual &&\n    -+\thead -n 1 actual >first-line &&\n    -+\ttest_grep \"^Revert\" first-line\n    ++\t# Verify output format: update refs/heads/revert-test <new> <old>\n    ++\tcut -f 3 -d \" \" result >new-tip &&\n    ++\tprintf \"update refs/heads/revert-test \" >expect &&\n    ++\tprintf \"%s \" $(cat new-tip) >>expect &&\n    ++\tprintf \"%s\\n\" $START >>expect &&\n    ++\ttest_cmp expect result\n     +'\n     +\n    -+test_expect_success 'replay with --revert fails with --contained' '\n    -+\ttest_must_fail git replay --revert --contained --onto main main..topic3 2>error &&\n    -+\ttest_grep \"revert.*contained.*cannot be used together\" error\n    -+'\n    ++test_expect_success 'git replay --revert reapply behavior' '\n    ++\t# Store original state\n    ++\tSTART=$(git rev-parse revert-test) &&\n    ++\ttest_when_finished \"git branch -f revert-test $START\" &&\n     +\n    -+test_expect_success 'verify revert actually reverses changes' '\n    -+\t# Create a branch with a simple change\n    -+\tgit switch -c revert-test main &&\n    -+\techo \"new content\" >test-file.txt &&\n    -+\tgit add test-file.txt &&\n    -+\tgit commit -m \"Add test file\" &&\n    ++\t# First revert R3\n    ++\tgit replay --revert revert-test revert-test~1..revert-test &&\n    ++\tREVERT_R3=$(git rev-parse revert-test) &&\n     +\n    -+\t# Revert the commit\n    -+\tgit replay --revert --advance revert-test HEAD^..HEAD >result &&\n    ++\t# Now revert the revert (should create \"Reapply\" message)\n    ++\tgit replay --revert revert-test revert-test~1..revert-test >output &&\n    ++\ttest_must_be_empty output &&\n     +\n    -+\ttest_must_be_empty result &&\n    ++\t# Verify Reapply message\n    ++\ttest_commit_message revert-test -m \"Reapply \\\"R3\\\"\n     +\n    -+\t# The file should no longer exist (reverted)\n    -+\ttest_must_fail git show revert-test:test-file.txt\n    ++This reverts commit $(git rev-parse $REVERT_R3).\"\n     +'\n     +\n    -+test_expect_success 'revert of a revert creates reapply message' '\n    -+\t# Create a commit\n    -+\tgit switch -c revert-revert main &&\n    -+\techo \"content\" >revert-test-2.txt &&\n    -+\tgit add revert-test-2.txt &&\n    -+\tgit commit -m \"Add revert test file\" &&\n    -+\n    -+\tORIGINAL=$(git rev-parse HEAD) &&\n    -+\n    -+\t# First revert\n    -+\tgit replay --revert --advance revert-revert HEAD^..HEAD >result1 &&\n    -+\n    -+\ttest_must_be_empty result1 &&\n    -+\n    -+\t# Check first revert message starts with \"Revert\"\n    -+\tgit log --format=%s -1 revert-revert >msg1 &&\n    -+\ttest_grep \"^Revert\" msg1 &&\n    -+\n    -+\tFIRST_REVERT=$(git rev-parse revert-revert) &&\n    -+\n    -+\t# Now revert the revert\n    -+\tgit replay --revert --advance revert-revert $ORIGINAL..$FIRST_REVERT >result2 &&\n    -+\n    -+\ttest_must_be_empty result2 &&\n    -+\n    -+\t# Check second revert message starts with \"Reapply\"\n    -+\tgit log --format=%s -1 revert-revert >msg2 &&\n    -+\ttest_grep \"^Reapply\" msg2 &&\n    -+\n    -+\t# The file should exist again (reapplied)\n    -+\tgit show revert-revert:revert-test-2.txt >actual &&\n    -+\techo \"content\" >expected &&\n    -+\ttest_cmp expected actual\n    ++test_expect_success 'git replay --revert with conflict' '\n    ++\t# Create a conflicting scenario\n    ++\tgit switch -c revert-conflict main &&\n    ++\ttest_commit C1 &&\n    ++\techo conflict >C1.t &&\n    ++\ttest_commit C2 C1.t &&\n    ++\tgit switch main &&\n    ++\techo different >C1.t &&\n    ++\ttest_commit C3 C1.t &&\n    ++\n    ++\t# Try to revert C2 onto main (which has conflicting C3)\n    ++\ttest_expect_code 1 git replay --revert main revert-conflict~1..revert-conflict\n     +'\n     +\n    -+test_expect_success 'replay --revert includes commit SHA in message' '\n    -+\tgit switch -c revert-sha-test main &&\n    -+\techo \"test\" >sha-test.txt &&\n    -+\tgit add sha-test.txt &&\n    -+\tgit commit -m \"Test commit for SHA\" &&\n    -+\n    -+\tCOMMIT_SHA=$(git rev-parse HEAD) &&\n    -+\tgit replay --revert --advance revert-sha-test HEAD^..HEAD >result &&\n    ++test_expect_success 'git replay --revert reflog message' '\n    ++\t# Store original state\n    ++\tSTART=$(git rev-parse revert-test) &&\n    ++\ttest_when_finished \"git branch -f revert-test $START\" &&\n     +\n    -+\ttest_must_be_empty result &&\n    ++\tgit replay --revert revert-test revert-test~1..revert-test >output &&\n    ++\ttest_must_be_empty output &&\n     +\n    -+\t# Check that the commit message includes the original SHA\n    -+\tgit log --format=%B -1 revert-sha-test >msg &&\n    -+\ttest_grep \"$COMMIT_SHA\" msg\n    ++\t# Verify reflog message includes --revert and branch name\n    ++\tgit reflog revert-test -1 --format=%gs >reflog-msg &&\n    ++\techo \"replay --revert revert-test\" >expect-reflog &&\n    ++\ttest_cmp expect-reflog reflog-msg\n     +'\n     +\n    -+test_expect_success 'replay --revert with conflict' '\n    -+\t# Create a conflicting situation\n    -+\tgit switch -c revert-conflict main &&\n    -+\techo \"line1\" >conflict-file.txt &&\n    -+\tgit add conflict-file.txt &&\n    -+\tgit commit -m \"Add conflict file\" &&\n    -+\n    -+\tgit switch -c revert-conflict-branch HEAD^ &&\n    -+\techo \"different\" >conflict-file.txt &&\n    -+\tgit add conflict-file.txt &&\n    -+\tgit commit -m \"Different content\" &&\n    -+\n    -+\t# Try to revert the first commit onto the conflicting branch\n    -+\ttest_expect_code 1 git replay --revert --onto revert-conflict-branch revert-conflict^..revert-conflict\n    ++test_expect_success 'git replay --revert incompatible with --contained' '\n    ++\ttest_must_fail git replay --revert revert-test --contained revert-test~1..revert-test 2>error &&\n    ++\ttest_grep \"requires --onto\" error\n     +'\n     +\n    -+test_expect_success 'replay --revert handles multiple commits' '\n    -+\t# Save the original topic2 state\n    -+\tORIG_TOPIC2=$(git rev-parse topic2) &&\n    -+\ttest_when_finished \"git branch -f topic2 $ORIG_TOPIC2\" &&\n    -+\n    -+\t# Revert D and E from topic2, applying the reverts onto topic1\n    -+\tgit replay --revert --onto topic1 topic1..topic2 >result &&\n    -+\n    -+\ttest_must_be_empty result &&\n    -+\n    -+\t# Verify both revert commits appear in the log\n    -+\tgit log --format=%s topic2 >log &&\n    -+\thead -n 2 log >first-two &&\n    -+\ttest_grep \"^Revert\" first-two &&\n    ++test_expect_success 'git replay --revert incompatible with --onto' '\n    ++\ttest_must_fail git replay --revert revert-test --onto main revert-test~1..revert-test 2>error &&\n    ++\ttest_grep \"cannot be used together\" error\n    ++'\n     +\n    -+\t# Verify we have both \"Revert D\" and \"Revert E\"\n    -+\ttest_grep \"Revert.*E\" log &&\n    -+\ttest_grep \"Revert.*D\" log\n    ++test_expect_success 'git replay --revert incompatible with --advance' '\n    ++\ttest_must_fail git replay --revert revert-test --advance main revert-test~1..revert-test 2>error &&\n    ++\ttest_grep \"cannot be used together\" error\n     +'\n     +\n      test_done\n\n-- \n2.51.0\n\nbase-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9\n\nThanks\n- Siddharth\n"},{"id":"531582","messageId":"20251202201611.22137-2-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20251202201611.22137-1-siddharthasthana31@gmail.com","subject":"[PATCH v2 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-12-02T20:16:10Z","receivedAt":"2025-12-02T20:16:29Z","isPatch":true,"body":"The logic for formatting revert commit messages (handling \"Revert\" and\n\"Reapply\" cases) is currently duplicated between sequencer.c and will be\nneeded by builtin/replay.c.\n\nExtract this logic into a new sequencer_format_revert_header() function\nthat can be shared. The function handles both regular reverts (\"Revert\n\"<subject>\"\") and revert-of-revert cases (\"Reapply \"<subject>\"\").\nUpdate do_pick_commit() to use the new helper, eliminating code\nduplication while preserving the special handling for commit_use_reference.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n sequencer.c | 39 +++++++++++++++++++++++++--------------\n sequencer.h |  8 ++++++++\n 2 files changed, 33 insertions(+), 14 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 5476d39ba9..9f621aef4b 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,\n \t\tif (opts->commit_use_reference) {\n \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n-\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n-\t\t\t   /*\n-\t\t\t    * We don't touch pre-existing repeated reverts, because\n-\t\t\t    * theoretically these can be nested arbitrarily deeply,\n-\t\t\t    * thus requiring excessive complexity to deal with.\n-\t\t\t    */\n-\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n+\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n \t\t} else {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n+\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n \t\t}\n-\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n \t\trefer_to_commit(opts, &ctx->message, commit);\n \n \t\tif (commit->parents && commit->parents->next) {\n@@ -5572,6 +5560,29 @@ int sequencer_pick_revisions(struct repository *r,\n \treturn res;\n }\n \n+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)\n+{\n+\tconst char *revert_subject;\n+\n+\tif (skip_prefix(orig_subject, \"Revert \\\"\", &revert_subject) &&\n+\t    /*\n+\t     * We don't touch pre-existing repeated reverts, because\n+\t     * theoretically these can be nested arbitrarily deeply,\n+\t     * thus requiring excessive complexity to deal with.\n+\t     */\n+\t    !starts_with(revert_subject, \"Revert \\\"\")) {\n+\t\tstrbuf_addstr(out, \"Reapply \\\"\");\n+\t\tstrbuf_addstr(out, revert_subject);\n+\t\tstrbuf_addch(out, '\\n');\n+\t} else {\n+\t\tstrbuf_addstr(out, \"Revert \\\"\");\n+\t\tstrbuf_addstr(out, orig_subject);\n+\t\tstrbuf_addstr(out, \"\\\"\\n\");\n+\t}\n+\n+\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n+}\n+\n void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n {\n \tunsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;\ndiff --git a/sequencer.h b/sequencer.h\nindex 719684c8a9..114c5d2449 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -271,4 +271,12 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n  */\n int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n \n+/*\n+ * Formats a revert commit message header following standard Git conventions.\n+ * Handles both regular reverts (\"Revert \\\"<subject>\\\"\") and revert of revert\n+ * cases (\"Reapply \\\"<subject>\\\"\"). Adds \"This reverts commit \" at the end.\n+ * The caller should append the commit OID after calling this function.\n+ */\n+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject);\n+\n #endif /* SEQUENCER_H */\n-- \n2.51.0\n\n"},{"id":"531583","messageId":"20251202201611.22137-3-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20251202201611.22137-1-siddharthasthana31@gmail.com","subject":"[PATCH v2 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-12-02T20:16:11Z","receivedAt":"2025-12-02T20:16:36Z","isPatch":true,"body":"The `git replay` command performs server-side history rewriting without\nrequiring a working tree. While it currently supports cherry-picking\ncommits (--advance) and rebasing (--onto), it lacks the ability to\nrevert them.\n\nAt GitLab, we use replay in Gitaly for efficient server-side operations\non bare repositories. Adding revert functionality enables us to reverse\nproblematic commits directly on the server, eliminating client-side\nroundtrips and reducing network overhead.\n\nAdd a `--revert <branch>` mode that reverses the changes introduced by\nthe specified commits. Following the architecture of --onto and --advance,\n--revert is a standalone mode that takes a branch argument and updates\nthat branch with the revert commits.\n\nThe implementation follows the same approach as sequencer.c (lines\n2360-2399), where cherry-pick and revert are the same merge operation\nbut with swapped arguments:\n\n  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n  - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n\nWe swap the base and pickme trees when calling\nmerge_incore_nonrecursive(), effectively reversing the diff direction.\nThe existing conflict handling, ref updates, and atomic transaction\nsupport work unchanged.\n\nThe commit messages follow git revert conventions: prefixed with\n\"Revert\" and including the original commit SHA. When reverting a commit\nthat itself starts with \"Revert\", the message uses \"Reapply\" instead.\nUnlike cherry-pick which preserves the original author, revert commits\nuse the current user as the author, matching the behavior of git revert.\n\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nHelped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  36 ++++++++-\n builtin/replay.c              | 145 ++++++++++++++++++++++++++++------\n t/t3650-replay-basics.sh      | 111 ++++++++++++++++++++++++++\n 3 files changed, 267 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex dcb26e8a8e..eb297c7530 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n \n DESCRIPTION\n -----------\n@@ -42,6 +42,24 @@ The history is replayed on top of the <branch> and <branch> is updated to\n point at the tip of the resulting history. This is different from `--onto`,\n which uses the target only as a starting point without updating it.\n \n+--revert <branch>::\n+\tStarting point at which to create the new revert commits; must be a\n+\tbranch name.\n++\n+When `--revert` is specified, the commits in the revision range are reverted\n+(their changes are undone) and the revert commits are applied on top of <branch>.\n+The <branch> is then updated to point at the new commits. This is similar to\n+running `git revert` for each commit in the range, but works without a working tree.\n++\n+The commit messages follow `git revert` conventions: prefixed with \"Revert\" and\n+including the original commit SHA. When reverting a commit whose message starts\n+with \"Revert\", the new message uses \"Reapply\" instead. The author of the revert\n+commits is the current user, not the original commit author.\n++\n+This option is mutually exclusive with `--onto` and `--advance`. It is also\n+incompatible with `--contained` (which is a modifier for `--onto` only).\n+\n+\n --ref-action[=<mode>]::\n \tControl how references are updated. The mode can be:\n +\n@@ -141,6 +159,22 @@ all commits they have since `base`, playing them on top of\n `origin/main`. These three branches may have commits on top of `base`\n that they have in common, but that does not need to be the case.\n \n+To revert commits from a branch:\n+\n+------------\n+$ git replay --revert main feature~2..feature\n+------------\n+\n+This reverts the last two commits from 'feature', creating revert commits on\n+top of 'main', and updates 'main' to point at the result. This is useful when\n+commits from 'feature' were previously merged or cherry-picked into 'main' and\n+need to be undone.\n+\n+NOTE: For reverting an entire merge request as a single commit (rather than\n+commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n+which can avoid unnecessary merge conflicts.\n+\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 6606a2c94b..7660f7412f 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -17,6 +17,7 @@\n #include \"parse-options.h\"\n #include \"refs.h\"\n #include \"revision.h\"\n+#include \"sequencer.h\"\n #include \"strmap.h\"\n #include <oidset.h>\n #include <tree.h>\n@@ -26,6 +27,11 @@ enum ref_action_mode {\n \tREF_ACTION_PRINT,\n };\n \n+enum replay_action {\n+\tREPLAY_PICK,\n+\tREPLAY_REVERT,\n+};\n+\n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n {\n@@ -57,10 +63,32 @@ static char *get_author(const char *message)\n \treturn NULL;\n }\n \n+static void generate_revert_message(struct strbuf *msg,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct repository *repo)\n+{\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n+\tconst char *subject_start;\n+\tint subject_len;\n+\tchar *subject;\n+\n+\tsubject_len = find_commit_subject(message, &subject_start);\n+\tsubject = xmemdupz(subject_start, subject_len);\n+\n+\tsequencer_format_revert_header(msg, subject);\n+\tstrbuf_addstr(msg, oid_to_hex(&commit->object.oid));\n+\tstrbuf_addstr(msg, \".\\n\");\n+\n+\tfree(subject);\n+\trepo_unuse_commit_buffer(repo, commit, message);\n+}\n+\n static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n+\t\t\t\t    struct commit *parent,\n+\t\t\t\t    enum replay_action action)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n@@ -77,9 +105,14 @@ static struct commit *create_commit(struct repository *repo,\n \n \tcommit_list_insert(parent, &parents);\n \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n+\tif (action == REPLAY_REVERT) {\n+\t\tgenerate_revert_message(&msg, based_on, repo);\n+\t\tauthor = xstrdup(git_author_info(IDENT_STRICT));\n+\t} else {\n+\t\tfind_commit_subject(message, &orig_message);\n+\t\tstrbuf_addstr(&msg, orig_message);\n+\t\tauthor = get_author(message);\n+\t}\n \treset_ident_date();\n \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n@@ -90,7 +123,7 @@ static struct commit *create_commit(struct repository *repo,\n \tobj = parse_object(repo, &ret);\n \n out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\trepo_unuse_commit_buffer(repo, based_on, message);\n \tfree_commit_extra_headers(extra);\n \tfree_commit_list(parents);\n \tstrbuf_release(&msg);\n@@ -166,6 +199,7 @@ static void determine_replay_mode(struct repository *repo,\n \t\t\t\t  struct rev_cmdline_info *cmd_info,\n \t\t\t\t  const char *onto_name,\n \t\t\t\t  char **advance_name,\n+\t\t\t\t  char **revert_name,\n \t\t\t\t  struct commit **onto,\n \t\t\t\t  struct strset **update_refs)\n {\n@@ -196,6 +230,20 @@ static void determine_replay_mode(struct repository *repo,\n \t\t}\n \t\tif (rinfo.positive_refexprs > 1)\n \t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n+\t} else if (*revert_name) {\n+\t\tstruct object_id oid;\n+\t\tchar *fullname = NULL;\n+\n+\t\t*onto = peel_committish(repo, *revert_name);\n+\t\tif (repo_dwim_ref(repo, *revert_name, strlen(*revert_name),\n+\t\t\t\t  &oid, &fullname, 0) == 1) {\n+\t\t\tfree(*revert_name);\n+\t\t\t*revert_name = fullname;\n+\t\t} else {\n+\t\t\tdie(_(\"argument to --revert must be a reference\"));\n+\t\t}\n+\t\tif (rinfo.positive_refexprs > 1)\n+\t\t\tdie(_(\"cannot revert with multiple sources because ordering would be ill-defined\"));\n \t} else {\n \t\tint positive_refs_complete = (\n \t\t\trinfo.positive_refexprs ==\n@@ -261,7 +309,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n \t\t\t\t\t  struct commit *onto,\n \t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n+\t\t\t\t\t  struct merge_result *result,\n+\t\t\t\t\t  enum replay_action action)\n {\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree;\n@@ -273,21 +322,39 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\tif (action == REPLAY_PICK) {\n+\t\t/* Cherry-pick: normal order */\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n \n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  result->tree,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  result);\n \n-\tfree((char*)merge_opt->ancestor);\n+\t\tfree((char *)merge_opt->ancestor);\n+\t} else {\n+\t\t/* Revert: swap base and pickme to reverse the diff */\n+\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n+\t\tmerge_opt->ancestor = pickme_name;\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  result->tree,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->branch2);\n+\t}\n \tmerge_opt->ancestor = NULL;\n+\tmerge_opt->branch2 = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, action);\n }\n \n static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n@@ -345,6 +412,9 @@ int cmd_replay(int argc,\n {\n \tconst char *advance_name_opt = NULL;\n \tchar *advance_name = NULL;\n+\tconst char *revert_name_opt = NULL;\n+\tchar *revert_name = NULL;\n+\tenum replay_action action = REPLAY_PICK;\n \tstruct commit *onto = NULL;\n \tconst char *onto_name = NULL;\n \tint contained = 0;\n@@ -365,7 +435,7 @@ int cmd_replay(int argc,\n \n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n+\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n \t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n \t\tNULL\n \t};\n@@ -378,6 +448,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &contained,\n \t\t\t N_(\"advance all branches contained in revision-range\")),\n+\t\tOPT_STRING(0, \"revert\", &revert_name_opt,\n+\t\t\t   N_(\"branch\"),\n+\t\t\t   N_(\"revert commits onto given branch\")),\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n \t\t\t   N_(\"control ref update behavior (update|print)\")),\n@@ -387,18 +460,28 @@ int cmd_replay(int argc,\n \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n-\tif (!onto_name && !advance_name_opt) {\n-\t\terror(_(\"option --onto or --advance is mandatory\"));\n+\t/* Exactly one mode must be specified */\n+\tif (!onto_name && !advance_name_opt && !revert_name_opt) {\n+\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n-\t\t\t\t  contained, \"--contained\");\n+\t\t\t\t  !!onto_name, \"--onto\");\n+\tdie_for_incompatible_opt2(!!revert_name_opt, \"--revert\",\n+\t\t\t\t  !!onto_name, \"--onto\");\n+\tdie_for_incompatible_opt2(!!revert_name_opt, \"--revert\",\n+\t\t\t\t  !!advance_name_opt, \"--advance\");\n+\tdie_for_incompatible_opt2(contained, \"--contained\",\n+\t\t\t\t  !onto_name, \"requires --onto\");\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n \n \tadvance_name = xstrdup_or_null(advance_name_opt);\n+\trevert_name = xstrdup_or_null(revert_name_opt);\n+\tif (revert_name)\n+\t\taction = REPLAY_REVERT;\n \n \trepo_init_revisions(repo, &revs, prefix);\n \n@@ -452,10 +535,13 @@ int cmd_replay(int argc,\n \t}\n \n \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n+\t\t\t      &revert_name,\n \t\t\t      &onto, &update_refs);\n \n \t/* Build reflog message */\n-\tif (advance_name_opt)\n+\tif (revert_name_opt)\n+\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", revert_name_opt);\n+\telse if (advance_name_opt)\n \t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n \telse\n \t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\",\n@@ -496,7 +582,7 @@ int cmd_replay(int argc,\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n \t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\t\t\t\t\t  onto, &merge_opt, &result, action);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \n@@ -508,7 +594,7 @@ int cmd_replay(int argc,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance_name)\n+\t\tif (advance_name || revert_name)\n \t\t\tcontinue;\n \t\tdecoration = get_name_decoration(&commit->object);\n \t\tif (!decoration)\n@@ -532,7 +618,7 @@ int cmd_replay(int argc,\n \t\t}\n \t}\n \n-\t/* In --advance mode, advance the target ref */\n+\t/* In --advance or --revert mode, update the target ref */\n \tif (result.clean == 1 && advance_name) {\n \t\tif (handle_ref_update(ref_mode, transaction, advance_name,\n \t\t\t\t      &last_commit->object.oid,\n@@ -544,6 +630,17 @@ int cmd_replay(int argc,\n \t\t\tgoto cleanup;\n \t\t}\n \t}\n+\tif (result.clean == 1 && revert_name) {\n+\t\tif (handle_ref_update(ref_mode, transaction, revert_name,\n+\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t      &onto->object.oid,\n+\t\t\t\t      reflog_msg.buf,\n+\t\t\t\t      &transaction_err) < 0) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    revert_name, transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n \n \t/* Commit the ref transaction if we have one */\n \tif (transaction && result.clean == 1) {\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex cf3aacf355..1c4e1cb666 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -314,4 +314,115 @@ test_expect_success 'invalid replay.refAction value' '\n \ttest_grep \"invalid.*replay.refAction.*value\" error\n '\n \n+test_expect_success 'setup for revert tests' '\n+\tgit switch -c revert-test main &&\n+\ttest_commit R1 &&\n+\ttest_commit R2 &&\n+\ttest_commit R3 &&\n+\tgit switch main\n+'\n+\n+test_expect_success 'git replay --revert reverts commits' '\n+\t# Store original state\n+\tSTART=$(git rev-parse revert-test) &&\n+\ttest_when_finished \"git branch -f revert-test $START\" &&\n+\n+\tgit replay --revert revert-test revert-test~2..revert-test >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify revert-test was updated with revert commits\n+\tgit log --format=%s -n 5 revert-test >actual &&\n+\tcat >expect <<-\\EOF &&\n+\tRevert \"R3\"\n+\tRevert \"R2\"\n+\tR3\n+\tR2\n+\tR1\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\t# Verify commit message format\n+\ttest_commit_message revert-test -m \"Revert \\\"R3\\\"\n+\n+This reverts commit $(git rev-parse R3).\"\n+'\n+\n+test_expect_success 'git replay --revert with --ref-action=print' '\n+\t# Store original state\n+\tSTART=$(git rev-parse revert-test) &&\n+\ttest_when_finished \"git branch -f revert-test $START\" &&\n+\n+\tgit replay --ref-action=print --revert revert-test revert-test~2..revert-test >result &&\n+\ttest_line_count = 1 result &&\n+\n+\t# Verify output format: update refs/heads/revert-test <new> <old>\n+\tcut -f 3 -d \" \" result >new-tip &&\n+\tprintf \"update refs/heads/revert-test \" >expect &&\n+\tprintf \"%s \" $(cat new-tip) >>expect &&\n+\tprintf \"%s\\n\" $START >>expect &&\n+\ttest_cmp expect result\n+'\n+\n+test_expect_success 'git replay --revert reapply behavior' '\n+\t# Store original state\n+\tSTART=$(git rev-parse revert-test) &&\n+\ttest_when_finished \"git branch -f revert-test $START\" &&\n+\n+\t# First revert R3\n+\tgit replay --revert revert-test revert-test~1..revert-test &&\n+\tREVERT_R3=$(git rev-parse revert-test) &&\n+\n+\t# Now revert the revert (should create \"Reapply\" message)\n+\tgit replay --revert revert-test revert-test~1..revert-test >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify Reapply message\n+\ttest_commit_message revert-test -m \"Reapply \\\"R3\\\"\n+\n+This reverts commit $(git rev-parse $REVERT_R3).\"\n+'\n+\n+test_expect_success 'git replay --revert with conflict' '\n+\t# Create a conflicting scenario\n+\tgit switch -c revert-conflict main &&\n+\ttest_commit C1 &&\n+\techo conflict >C1.t &&\n+\ttest_commit C2 C1.t &&\n+\tgit switch main &&\n+\techo different >C1.t &&\n+\ttest_commit C3 C1.t &&\n+\n+\t# Try to revert C2 onto main (which has conflicting C3)\n+\ttest_expect_code 1 git replay --revert main revert-conflict~1..revert-conflict\n+'\n+\n+test_expect_success 'git replay --revert reflog message' '\n+\t# Store original state\n+\tSTART=$(git rev-parse revert-test) &&\n+\ttest_when_finished \"git branch -f revert-test $START\" &&\n+\n+\tgit replay --revert revert-test revert-test~1..revert-test >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify reflog message includes --revert and branch name\n+\tgit reflog revert-test -1 --format=%gs >reflog-msg &&\n+\techo \"replay --revert revert-test\" >expect-reflog &&\n+\ttest_cmp expect-reflog reflog-msg\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --contained' '\n+\ttest_must_fail git replay --revert revert-test --contained revert-test~1..revert-test 2>error &&\n+\ttest_grep \"requires --onto\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --onto' '\n+\ttest_must_fail git replay --revert revert-test --onto main revert-test~1..revert-test 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --advance' '\n+\ttest_must_fail git replay --revert revert-test --advance main revert-test~1..revert-test 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"531690","messageId":"aTLDA11AKs0jlxFJ@pks.im","threadId":"64529","inReplyTo":"20251202201611.22137-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-05T11:33:23Z","receivedAt":"2025-12-05T11:33:32Z","isPatch":true,"body":"On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:\n> diff --git a/sequencer.c b/sequencer.c\n> index 5476d39ba9..9f621aef4b 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,\n>  \t\tif (opts->commit_use_reference) {\n>  \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>  \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n> -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n> -\t\t\t   /*\n> -\t\t\t    * We don't touch pre-existing repeated reverts, because\n> -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n> -\t\t\t    * thus requiring excessive complexity to deal with.\n> -\t\t\t    */\n> -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n> -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n> -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n> -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n> +\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>  \t\t} else {\n> -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n> -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n> -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n> +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n>  \t\t}\n> -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>  \t\trefer_to_commit(opts, &ctx->message, commit);\n>  \n>  \t\tif (commit->parents && commit->parents->next) {\n\nIs there any reason why we don't also handle `refer_to_commit()` in that\nnew function?\n\nPatrick\n"},{"id":"531691","messageId":"aTLDEgGzVnvU45va@pks.im","threadId":"64529","inReplyTo":"20251202201611.22137-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-05T11:33:38Z","receivedAt":"2025-12-05T11:33:46Z","isPatch":true,"body":"On Wed, Dec 03, 2025 at 01:46:11AM +0530, Siddharth Asthana wrote:\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 6606a2c94b..7660f7412f 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -77,9 +105,14 @@ static struct commit *create_commit(struct repository *repo,\n>  \n>  \tcommit_list_insert(parent, &parents);\n>  \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n> -\tfind_commit_subject(message, &orig_message);\n> -\tstrbuf_addstr(&msg, orig_message);\n> -\tauthor = get_author(message);\n> +\tif (action == REPLAY_REVERT) {\n> +\t\tgenerate_revert_message(&msg, based_on, repo);\n> +\t\tauthor = xstrdup(git_author_info(IDENT_STRICT));\n> +\t} else {\n> +\t\tfind_commit_subject(message, &orig_message);\n> +\t\tstrbuf_addstr(&msg, orig_message);\n> +\t\tauthor = get_author(message);\n> +\t}\n>  \treset_ident_date();\n>  \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n>  \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n\nDo we want to be defensive in those if-chains and verify that `action ==\nREPLAY_PICK` in the other case, and `BUG()` if it's not?\n\n> @@ -273,21 +322,39 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>  \tpickme_tree = repo_get_commit_tree(repo, pickme);\n>  \tbase_tree = repo_get_commit_tree(repo, base);\n>  \n> -\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> -\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n> -\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n> +\tif (action == REPLAY_PICK) {\n> +\t\t/* Cherry-pick: normal order */\n> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> +\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n> +\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n>  \n> -\tmerge_incore_nonrecursive(merge_opt,\n> -\t\t\t\t  base_tree,\n> -\t\t\t\t  result->tree,\n> -\t\t\t\t  pickme_tree,\n> -\t\t\t\t  result);\n> +\t\tmerge_incore_nonrecursive(merge_opt,\n> +\t\t\t\t\t  base_tree,\n> +\t\t\t\t\t  result->tree,\n> +\t\t\t\t\t  pickme_tree,\n> +\t\t\t\t\t  result);\n>  \n> -\tfree((char*)merge_opt->ancestor);\n> +\t\tfree((char *)merge_opt->ancestor);\n> +\t} else {\n> +\t\t/* Revert: swap base and pickme to reverse the diff */\n> +\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> +\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n> +\t\tmerge_opt->ancestor = pickme_name;\n> +\n> +\t\tmerge_incore_nonrecursive(merge_opt,\n> +\t\t\t\t\t  pickme_tree,\n> +\t\t\t\t\t  result->tree,\n> +\t\t\t\t\t  base_tree,\n> +\t\t\t\t\t  result);\n> +\n> +\t\tfree((char *)merge_opt->branch2);\n> +\t}\n>  \tmerge_opt->ancestor = NULL;\n> +\tmerge_opt->branch2 = NULL;\n\nWe can `FREE_AND_NULL()` instead of manually unsetting these.\n\n> @@ -387,18 +460,28 @@ int cmd_replay(int argc,\n>  \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n>  \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n>  \n> -\tif (!onto_name && !advance_name_opt) {\n> -\t\terror(_(\"option --onto or --advance is mandatory\"));\n> +\t/* Exactly one mode must be specified */\n> +\tif (!onto_name && !advance_name_opt && !revert_name_opt) {\n> +\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n>  \t\tusage_with_options(replay_usage, replay_options);\n>  \t}\n>  \n>  \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n> -\t\t\t\t  contained, \"--contained\");\n> +\t\t\t\t  !!onto_name, \"--onto\");\n> +\tdie_for_incompatible_opt2(!!revert_name_opt, \"--revert\",\n> +\t\t\t\t  !!onto_name, \"--onto\");\n> +\tdie_for_incompatible_opt2(!!revert_name_opt, \"--revert\",\n> +\t\t\t\t  !!advance_name_opt, \"--advance\");\n> +\tdie_for_incompatible_opt2(contained, \"--contained\",\n> +\t\t\t\t  !onto_name, \"requires --onto\");\n\nWe have `die_for_incompatible_opt3()` that can be used here to check for\nmutual exclusivity of \"--revert\", \"--advance\" and \"--onto\".\n\n> @@ -508,7 +594,7 @@ int cmd_replay(int argc,\n>  \t\tkh_value(replayed_commits, pos) = last_commit;\n>  \n>  \t\t/* Update any necessary branches */\n> -\t\tif (advance_name)\n> +\t\tif (advance_name || revert_name)\n>  \t\t\tcontinue;\n>  \t\tdecoration = get_name_decoration(&commit->object);\n>  \t\tif (!decoration)\n> @@ -532,7 +618,7 @@ int cmd_replay(int argc,\n>  \t\t}\n>  \t}\n>  \n> -\t/* In --advance mode, advance the target ref */\n> +\t/* In --advance or --revert mode, update the target ref */\n>  \tif (result.clean == 1 && advance_name) {\n>  \t\tif (handle_ref_update(ref_mode, transaction, advance_name,\n>  \t\t\t\t      &last_commit->object.oid,\n> @@ -544,6 +630,17 @@ int cmd_replay(int argc,\n>  \t\t\tgoto cleanup;\n>  \t\t}\n>  \t}\n> +\tif (result.clean == 1 && revert_name) {\n> +\t\tif (handle_ref_update(ref_mode, transaction, revert_name,\n> +\t\t\t\t      &last_commit->object.oid,\n> +\t\t\t\t      &onto->object.oid,\n> +\t\t\t\t      reflog_msg.buf,\n> +\t\t\t\t      &transaction_err) < 0) {\n> +\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n> +\t\t\t\t    revert_name, transaction_err.buf);\n> +\t\t\tgoto cleanup;\n> +\t\t}\n> +\t}\n\nThis conditional and the one beforehand are the exact same, except that\nwe use either `revert_name` or `advance_name`. Let's merge them:\n\n    if (result.clean == 1 && (revert_name || advance_name)) {\n        const char *ref = revert_name ? revert_name : advance_name;\n        if (handle_ref_update(ref_mode, transaction, ref,\n                      &last_commit->object.oid,\n                      &onto->object.oid,\n                      reflog_msg.buf,\n                      &transaction_err) < 0) {\n            ret = error(_(\"failed to update ref '%s': %s\"),\n                    revert_name, transaction_err.buf);\n            goto cleanup;\n        }\n    }\n\nPatrick\n"},{"id":"531805","messageId":"ac12100d-4aba-4d15-8bcf-c50e6100c95e@gmail.com","threadId":"64529","inReplyTo":"aTLDA11AKs0jlxFJ@pks.im","subject":"Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-12-07T23:00:58Z","receivedAt":"2025-12-07T23:01:06Z","isPatch":true,"body":"\nOn 05/12/25 17:03, Patrick Steinhardt wrote:\n> On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:\n>> diff --git a/sequencer.c b/sequencer.c\n>> index 5476d39ba9..9f621aef4b 100644\n>> --- a/sequencer.c\n>> +++ b/sequencer.c\n>> @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,\n>>   \t\tif (opts->commit_use_reference) {\n>>   \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>>   \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n>> -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n>> -\t\t\t   /*\n>> -\t\t\t    * We don't touch pre-existing repeated reverts, because\n>> -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n>> -\t\t\t    * thus requiring excessive complexity to deal with.\n>> -\t\t\t    */\n>> -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n>> -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n>> +\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>   \t\t} else {\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n>> -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n>> +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n>>   \t\t}\n>> -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>   \t\trefer_to_commit(opts, &ctx->message, commit);\n>>   \n>>   \t\tif (commit->parents && commit->parents->next) {\n> Is there any reason why we don't also handle `refer_to_commit()` in that\n> new function?\n\n\nThe `refer_to_commit()` function depends on `struct replay_opts` and its \n`commit_use_reference` flag, which controls whether to use abbreviated \ncommit info (\"%h (%s, %ad)\") or the full OID. This is specific to \nsequencer.c's interactive workflow where users can choose the reference \nstyle via --reference.\n\nIn replay.c, we always use the full OID via `oid_to_hex()` since it's \ndesigned for non-interactive server-side operations without the \n`replay_opts` framework. Including `refer_to_commit()` would require \neither passing `replay_opts` to the shared function (leaking sequencer \ninternals) or adding a format parameter which feels like \nover-engineering for current needs.\n\nHappy to reconsider if you think there's a cleaner way to share this.\n\nThanks,\nSiddharth\n\n\n>\n> Patrick\n"},{"id":"531806","messageId":"3f3dd1f1-3127-45a2-9afc-7c452d0418d9@gmail.com","threadId":"64529","inReplyTo":"aTLDEgGzVnvU45va@pks.im","subject":"Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-12-07T23:03:02Z","receivedAt":"2025-12-07T23:03:09Z","isPatch":true,"body":"\nOn 05/12/25 17:03, Patrick Steinhardt wrote:\n> On Wed, Dec 03, 2025 at 01:46:11AM +0530, Siddharth Asthana wrote:\n>> diff --git a/builtin/replay.c b/builtin/replay.c\n>> index 6606a2c94b..7660f7412f 100644\n>> --- a/builtin/replay.c\n>> +++ b/builtin/replay.c\n>> @@ -77,9 +105,14 @@ static struct commit *create_commit(struct repository *repo,\n>>   \n>>   \tcommit_list_insert(parent, &parents);\n>>   \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n>> -\tfind_commit_subject(message, &orig_message);\n>> -\tstrbuf_addstr(&msg, orig_message);\n>> -\tauthor = get_author(message);\n>> +\tif (action == REPLAY_REVERT) {\n>> +\t\tgenerate_revert_message(&msg, based_on, repo);\n>> +\t\tauthor = xstrdup(git_author_info(IDENT_STRICT));\n>> +\t} else {\n>> +\t\tfind_commit_subject(message, &orig_message);\n>> +\t\tstrbuf_addstr(&msg, orig_message);\n>> +\t\tauthor = get_author(message);\n>> +\t}\n>>   \treset_ident_date();\n>>   \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n>>   \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n> Do we want to be defensive in those if-chains and verify that `action ==\n> REPLAY_PICK` in the other case, and `BUG()` if it's not?\n\n\nGood idea. I will add BUG() for unexpected action values to catch any \nfuture additions that aren't properly handled.\n\n\n>\n>> @@ -273,21 +322,39 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>>   \tpickme_tree = repo_get_commit_tree(repo, pickme);\n>>   \tbase_tree = repo_get_commit_tree(repo, base);\n>>   \n>> -\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>> -\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n>> -\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n>> +\tif (action == REPLAY_PICK) {\n>> +\t\t/* Cherry-pick: normal order */\n>> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>> +\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n>> +\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n>>   \n>> -\tmerge_incore_nonrecursive(merge_opt,\n>> -\t\t\t\t  base_tree,\n>> -\t\t\t\t  result->tree,\n>> -\t\t\t\t  pickme_tree,\n>> -\t\t\t\t  result);\n>> +\t\tmerge_incore_nonrecursive(merge_opt,\n>> +\t\t\t\t\t  base_tree,\n>> +\t\t\t\t\t  result->tree,\n>> +\t\t\t\t\t  pickme_tree,\n>> +\t\t\t\t\t  result);\n>>   \n>> -\tfree((char*)merge_opt->ancestor);\n>> +\t\tfree((char *)merge_opt->ancestor);\n>> +\t} else {\n>> +\t\t/* Revert: swap base and pickme to reverse the diff */\n>> +\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n>> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>> +\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n>> +\t\tmerge_opt->ancestor = pickme_name;\n>> +\n>> +\t\tmerge_incore_nonrecursive(merge_opt,\n>> +\t\t\t\t\t  pickme_tree,\n>> +\t\t\t\t\t  result->tree,\n>> +\t\t\t\t\t  base_tree,\n>> +\t\t\t\t\t  result);\n>> +\n>> +\t\tfree((char *)merge_opt->branch2);\n>> +\t}\n>>   \tmerge_opt->ancestor = NULL;\n>> +\tmerge_opt->branch2 = NULL;\n> We can `FREE_AND_NULL()` instead of manually unsetting these.\n\n\nWill use FREE_AND_NULL() - cleaner and clearer intent.\n\n\n>\n>> @@ -387,18 +460,28 @@ int cmd_replay(int argc,\n>>   \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n>>   \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n>>   \n>> -\tif (!onto_name && !advance_name_opt) {\n>> -\t\terror(_(\"option --onto or --advance is mandatory\"));\n>> +\t/* Exactly one mode must be specified */\n>> +\tif (!onto_name && !advance_name_opt && !revert_name_opt) {\n>> +\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n>>   \t\tusage_with_options(replay_usage, replay_options);\n>>   \t}\n>>   \n>>   \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>> -\t\t\t\t  contained, \"--contained\");\n>> +\t\t\t\t  !!onto_name, \"--onto\");\n>> +\tdie_for_incompatible_opt2(!!revert_name_opt, \"--revert\",\n>> +\t\t\t\t  !!onto_name, \"--onto\");\n>> +\tdie_for_incompatible_opt2(!!revert_name_opt, \"--revert\",\n>> +\t\t\t\t  !!advance_name_opt, \"--advance\");\n>> +\tdie_for_incompatible_opt2(contained, \"--contained\",\n>> +\t\t\t\t  !onto_name, \"requires --onto\");\n> We have `die_for_incompatible_opt3()` that can be used here to check for\n> mutual exclusivity of \"--revert\", \"--advance\" and \"--onto\".\n\n\nNice, I wasn't aware of this helper. Will use \ndie_for_incompatible_opt3() for the three-way mutual exclusivity check.\n\n\n>\n>> @@ -508,7 +594,7 @@ int cmd_replay(int argc,\n>>   \t\tkh_value(replayed_commits, pos) = last_commit;\n>>   \n>>   \t\t/* Update any necessary branches */\n>> -\t\tif (advance_name)\n>> +\t\tif (advance_name || revert_name)\n>>   \t\t\tcontinue;\n>>   \t\tdecoration = get_name_decoration(&commit->object);\n>>   \t\tif (!decoration)\n>> @@ -532,7 +618,7 @@ int cmd_replay(int argc,\n>>   \t\t}\n>>   \t}\n>>   \n>> -\t/* In --advance mode, advance the target ref */\n>> +\t/* In --advance or --revert mode, update the target ref */\n>>   \tif (result.clean == 1 && advance_name) {\n>>   \t\tif (handle_ref_update(ref_mode, transaction, advance_name,\n>>   \t\t\t\t      &last_commit->object.oid,\n>> @@ -544,6 +630,17 @@ int cmd_replay(int argc,\n>>   \t\t\tgoto cleanup;\n>>   \t\t}\n>>   \t}\n>> +\tif (result.clean == 1 && revert_name) {\n>> +\t\tif (handle_ref_update(ref_mode, transaction, revert_name,\n>> +\t\t\t\t      &last_commit->object.oid,\n>> +\t\t\t\t      &onto->object.oid,\n>> +\t\t\t\t      reflog_msg.buf,\n>> +\t\t\t\t      &transaction_err) < 0) {\n>> +\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n>> +\t\t\t\t    revert_name, transaction_err.buf);\n>> +\t\t\tgoto cleanup;\n>> +\t\t}\n>> +\t}\n> This conditional and the one beforehand are the exact same, except that\n> we use either `revert_name` or `advance_name`. Let's merge them:\n>\n>      if (result.clean == 1 && (revert_name || advance_name)) {\n>          const char *ref = revert_name ? revert_name : advance_name;\n>          if (handle_ref_update(ref_mode, transaction, ref,\n>                        &last_commit->object.oid,\n>                        &onto->object.oid,\n>                        reflog_msg.buf,\n>                        &transaction_err) < 0) {\n>              ret = error(_(\"failed to update ref '%s': %s\"),\n>                      revert_name, transaction_err.buf);\n>              goto cleanup;\n>          }\n>      }\n\n\nAgreed, this is much cleaner. Will merge the conditionals as you \nsuggest. Will incorporate all changes in v3.\n\nThanks for the thorough review!\n\nSiddharth\n\n\n>\n> Patrick\n"},{"id":"531808","messageId":"aTZ5RrjnwJ2ZnT7A@pks.im","threadId":"64529","inReplyTo":"ac12100d-4aba-4d15-8bcf-c50e6100c95e@gmail.com","subject":"Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-08T07:07:50Z","receivedAt":"2025-12-08T07:08:00Z","isPatch":true,"body":"On Mon, Dec 08, 2025 at 04:30:58AM +0530, Siddharth Asthana wrote:\n> \n> On 05/12/25 17:03, Patrick Steinhardt wrote:\n> > On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:\n> > > diff --git a/sequencer.c b/sequencer.c\n> > > index 5476d39ba9..9f621aef4b 100644\n> > > --- a/sequencer.c\n> > > +++ b/sequencer.c\n> > > @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,\n> > >   \t\tif (opts->commit_use_reference) {\n> > >   \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n> > >   \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n> > > -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n> > > -\t\t\t   /*\n> > > -\t\t\t    * We don't touch pre-existing repeated reverts, because\n> > > -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n> > > -\t\t\t    * thus requiring excessive complexity to deal with.\n> > > -\t\t\t    */\n> > > -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n> > > -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n> > > -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n> > > -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n> > > +\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n> > >   \t\t} else {\n> > > -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n> > > -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n> > > -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n> > > +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n> > >   \t\t}\n> > > -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n> > >   \t\trefer_to_commit(opts, &ctx->message, commit);\n> > >   \t\tif (commit->parents && commit->parents->next) {\n> > Is there any reason why we don't also handle `refer_to_commit()` in that\n> > new function?\n> \n> \n> The `refer_to_commit()` function depends on `struct replay_opts` and its\n> `commit_use_reference` flag, which controls whether to use abbreviated\n> commit info (\"%h (%s, %ad)\") or the full OID. This is specific to\n> sequencer.c's interactive workflow where users can choose the reference\n> style via --reference.\n> \n> In replay.c, we always use the full OID via `oid_to_hex()` since it's\n> designed for non-interactive server-side operations without the\n> `replay_opts` framework. Including `refer_to_commit()` would require either\n> passing `replay_opts` to the shared function (leaking sequencer internals)\n> or adding a format parameter which feels like over-engineering for current\n> needs.\n> \n> Happy to reconsider if you think there's a cleaner way to share this.\n\nA simple alternative might be to convert the `struct replay_opts`\nparameter into a `flags` field that tells the function whether it is\nexpected to use the object ID or whether it should try using the\nabbreviated commit info instead.\n\nPatrick\n"},{"id":"532285","messageId":"c49b2375-c975-4591-b3e9-aa87771a8015@gmail.com","threadId":"64529","inReplyTo":"20251202201611.22137-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-16T16:23:39Z","receivedAt":"2025-12-16T16:23:42Z","isPatch":true,"body":"Hi Siddarth\n\nI agree with Patrick's comments, I've added a few more of my own below\n\nOn 02/12/2025 20:16, Siddharth Asthana wrote:\n> The `git replay` command performs server-side history rewriting without\n> requiring a working tree. While it currently supports cherry-picking\n> commits (--advance) and rebasing (--onto), it lacks the ability to\n> revert them.\n> \n> At GitLab, we use replay in Gitaly for efficient server-side operations\n> on bare repositories. Adding revert functionality enables us to reverse\n> problematic commits directly on the server, eliminating client-side\n> roundtrips and reducing network overhead.\n> \n> Add a `--revert <branch>` mode that reverses the changes introduced by\n> the specified commits. Following the architecture of --onto and --advance,\n> --revert is a standalone mode that takes a branch argument and updates\n> that branch with the revert commits.\n\ns/revert/reverted/?\n\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index dcb26e8a8e..eb297c7530 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>   SYNOPSIS\n>   --------\n>   [verse]\n> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n>   \n>   DESCRIPTION\n>   -----------\n> @@ -42,6 +42,24 @@ The history is replayed on top of the <branch> and <branch> is updated to\n>   point at the tip of the resulting history. This is different from `--onto`,\n>   which uses the target only as a starting point without updating it.\n>   \n> +--revert <branch>::\n> +\tStarting point at which to create the new revert commits; must be a\n> +\tbranch name.\n> ++\n> +When `--revert` is specified, the commits in the revision range are reverted\n> +(their changes are undone) and the revert commits\n\ns/revert/reverted/\n\n> are applied on top of <branch>.\n> +The <branch> is then updated to point at the new commits. This is similar to\n> +running `git revert` for each commit in the range, but works without a working tree.\n\n\"git revert\" takes a revision range so it is the same as running \"git \nrevert <revision-range>\" but does not update the working tree.\n\n> ++\n> +The commit messages follow `git revert` conventions: prefixed with \"Revert\" and\n\ns/conventions: prefixed/conventions: they are prefixed/\n\n> +including the original commit SHA.\n\ns/including/include/\ns/SHA/hash/\n\n> When reverting a commit whose message starts\n> +with \"Revert\", the new message uses \"Reapply\" instead. The author of the revert\n> +commits is the current user, not the original commit author.\n> ++\n> +This option is mutually exclusive with `--onto` and `--advance`. It is also\n> +incompatible with `--contained` (which is a modifier for `--onto` only).\n> +\n> +\n>   --ref-action[=<mode>]::\n>   \tControl how references are updated. The mode can be:\n>   +\n> @@ -141,6 +159,22 @@ all commits they have since `base`, playing them on top of\n>   `origin/main`. These three branches may have commits on top of `base`\n>   that they have in common, but that does not need to be the case.\n>   \n> +To revert commits from a branch:\n> +\n> +------------\n> +$ git replay --revert main feature~2..feature\n> +------------\n> +\n> +This reverts the last two commits from 'feature', creating revert commits on\n> +top of 'main', and updates 'main' to point at the result. This is useful when\n> +commits from 'feature' were previously merged or cherry-picked into 'main' and\n> +need to be undone.\n> +\n> +NOTE: For reverting an entire merge request as a single commit (rather than\n> +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n> +which can avoid unnecessary merge conflicts.\n\nThat's a good suggestion\n\n> +\n>   GIT\n>   ---\n>   Part of the linkgit:git[1] suite\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 6606a2c94b..7660f7412f 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -17,6 +17,7 @@\n>   #include \"parse-options.h\"\n>   #include \"refs.h\"\n>   #include \"revision.h\"\n> +#include \"sequencer.h\"\n>   #include \"strmap.h\"\n>   #include <oidset.h>\n>   #include <tree.h>\n> @@ -26,6 +27,11 @@ enum ref_action_mode {\n>   \tREF_ACTION_PRINT,\n>   };\n>   \n> +enum replay_action {\n> +\tREPLAY_PICK,\n> +\tREPLAY_REVERT,\n> +};\n\nsequencer.h already defines enum replay_action with an extra member so \nthis is a bit confusing, maybe we should use a different name?\n\n> +static void generate_revert_message(struct strbuf *msg,\n> +\t\t\t\t    struct commit *commit,\n> +\t\t\t\t    struct repository *repo)\n> +{\n> +\tconst char *out_enc = get_commit_output_encoding();\n> +\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n> +\tconst char *subject_start;\n> +\tint subject_len;\n> +\tchar *subject;\n> +\n> +\tsubject_len = find_commit_subject(message, &subject_start);\n> +\tsubject = xmemdupz(subject_start, subject_len);\n> +\n> +\tsequencer_format_revert_header(msg, subject);\n> +\tstrbuf_addstr(msg, oid_to_hex(&commit->object.oid));\n> +\tstrbuf_addstr(msg, \".\\n\");\n\nIt's a bit odd that sequencer_format_revert_header() actually adds the \nbeginning of the body but we have to add the commit oid ourselves. It \nwould be nicer if we could pass the oid to that function and have it \nformat the message for us. It's a bit tricky because the sequencer needs \nto handle merges as well but it shouldn't be too difficult.\n\nThe function name is also a bit strange as header normally refers to the \ncommit metadata not the subject line.\n\n> @@ -77,9 +105,14 @@ static struct commit *create_commit(struct repository *repo,\n>   \n>   \tcommit_list_insert(parent, &parents);\n>   \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n> -\tfind_commit_subject(message, &orig_message);\n> -\tstrbuf_addstr(&msg, orig_message);\n> -\tauthor = get_author(message);\n> +\tif (action == REPLAY_REVERT) {\n> +\t\tgenerate_revert_message(&msg, based_on, repo);\n> +\t\tauthor = xstrdup(git_author_info(IDENT_STRICT));\n\nwrite_commit_tree() will look up the author for us if we just pass NULL \nso I would just set author = NULL here or delete this line and \ninitialize author to NULL at the beginning of this function.\n\n> +\t} else {\n> +\t\tfind_commit_subject(message, &orig_message);\n> +\t\tstrbuf_addstr(&msg, orig_message);\n> +\t\tauthor = get_author(message);\n> +\t}\n\nThis matches the deleted lines - good\n\n> @@ -196,6 +230,20 @@ static void determine_replay_mode(struct repository *repo,\n>   \t\t}\n>   \t\tif (rinfo.positive_refexprs > 1)\n>   \t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n> +\t} else if (*revert_name) {\n> +\t\tstruct object_id oid;\n> +\t\tchar *fullname = NULL;\n> +\n> +\t\t*onto = peel_committish(repo, *revert_name);\n> +\t\tif (repo_dwim_ref(repo, *revert_name, strlen(*revert_name),\n> +\t\t\t\t  &oid, &fullname, 0) == 1) {\n> +\t\t\tfree(*revert_name);\n> +\t\t\t*revert_name = fullname;\n> +\t\t} else {\n> +\t\t\tdie(_(\"argument to --revert must be a reference\"));\n> +\t\t}\n> +\t\tif (rinfo.positive_refexprs > 1)\n> +\t\t\tdie(_(\"cannot revert with multiple sources because ordering would be ill-defined\"));\n\nThis is a copy of what we do with --advance but with a different option \nname - can be factor this out into a common function that's called for \nboth options?\n\n> @@ -452,10 +535,13 @@ int cmd_replay(int argc,\n>   \t}\n>   \n>   \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n> +\t\t\t      &revert_name,\n>   \t\t\t      &onto, &update_refs);\n\nLet's not fold the line after \"revert_name\"\n\n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index cf3aacf355..1c4e1cb666 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -314,4 +314,115 @@ test_expect_success 'invalid replay.refAction value' '\n>   \ttest_grep \"invalid.*replay.refAction.*value\" error\n>   '\n>   \n> +test_expect_success 'setup for revert tests' '\n> +\tgit switch -c revert-test main &&\n> +\ttest_commit R1 &&\n> +\ttest_commit R2 &&\n> +\ttest_commit R3 &&\n> +\tgit switch main\n> +'\n\nWhy do we need a new branch for this? We already have topic3 built on \ntop of topic1 so we can test reverting commits with\n\n\tgit replay --revert=topic3 main..topic1\n\nwhich should revert C and F\n\n> +test_expect_success 'git replay --revert reverts commits' '\n> +\t# Store original state\n> +\tSTART=$(git rev-parse revert-test) &&\n> +\ttest_when_finished \"git branch -f revert-test $START\" &&\n> +\n> +\tgit replay --revert revert-test revert-test~2..revert-test >output &&\n> +\ttest_must_be_empty output &&\n> +\n> +\t# Verify revert-test was updated with revert commits\n> +\tgit log --format=%s -n 5 revert-test >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\tRevert \"R3\"\n> +\tRevert \"R2\"\n> +\tR3\n> +\tR2\n> +\tR1\n> +\tEOF\n> +\ttest_cmp expect actual &&\n> +\n> +\t# Verify commit message format\n> +\ttest_commit_message revert-test -m \"Revert \\\"R3\\\"\n> +\n> +This reverts commit $(git rev-parse R3).\"\n\ntest_commit_message accepts the expected message on stdin so you can write\n\n\ttest_commit_message revert-test <<-EOF\n\tRevert \"R3\"\n\n\tThis reverts commit $(git rev-parse R3)\n\tEOF\n\nwhich is cleaner as we don't have to escape the double quotes and the \nmessage is nicely indented.\n\n> +'\n> +\n> +test_expect_success 'git replay --revert with --ref-action=print' '\n\nGiven the ref updating code is independent of --advance, --revert etc \nI'm not sure what extra coverage this test adds. If the previous test \npasses what is a plausible scenario where this one fails?\n\n> +\t# Store original state\n> +\tSTART=$(git rev-parse revert-test) &&\n> +\ttest_when_finished \"git branch -f revert-test $START\" &&\n> +\n> +\tgit replay --ref-action=print --revert revert-test revert-test~2..revert-test >result &&\n> +\ttest_line_count = 1 result &&\n> +\n> +\t# Verify output format: update refs/heads/revert-test <new> <old>\n> +\tcut -f 3 -d \" \" result >new-tip &&\n> +\tprintf \"update refs/heads/revert-test \" >expect &&\n> +\tprintf \"%s \" $(cat new-tip) >>expect &&\n> +\tprintf \"%s\\n\" $START >>expect &&\n> +\ttest_cmp expect result\n> +'\n> +\n> +test_expect_success 'git replay --revert reapply behavior' '\n\nGood idea\n\n> +\t# Store original state\n> +\tSTART=$(git rev-parse revert-test) &&\n> +\ttest_when_finished \"git branch -f revert-test $START\" &&\n> +\n> +\t# First revert R3\n> +\tgit replay --revert revert-test revert-test~1..revert-test &&\n> +\tREVERT_R3=$(git rev-parse revert-test) &&\n> +\n> +\t# Now revert the revert (should create \"Reapply\" message)\n> +\tgit replay --revert revert-test revert-test~1..revert-test >output &&\n> +\ttest_must_be_empty output &&\n> +\n> +\t# Verify Reapply message\n> +\ttest_commit_message revert-test -m \"Reapply \\\"R3\\\"\n> +\n> +This reverts commit $(git rev-parse $REVERT_R3).\"\n> +'\n> +\n> +test_expect_success 'git replay --revert with conflict' '\n> +\t# Create a conflicting scenario\n> +\tgit switch -c revert-conflict main &&\n> +\ttest_commit C1 &&\n> +\techo conflict >C1.t &&\n> +\ttest_commit C2 C1.t &&\n> +\tgit switch main &&\n> +\techo different >C1.t &&\n> +\ttest_commit C3 C1.t &&\n> +\n> +\t# Try to revert C2 onto main (which has conflicting C3)\n> +\ttest_expect_code 1 git replay --revert main revert-conflict~1..revert-conflict\n> +'\n> +\n> +test_expect_success 'git replay --revert reflog message' '\n\nI think we should just check the reflog message in one of the earlier tests.\n\n> +\t# Store original state\n> +\tSTART=$(git rev-parse revert-test) &&\n> +\ttest_when_finished \"git branch -f revert-test $START\" &&\n> +\n> +\tgit replay --revert revert-test revert-test~1..revert-test >output &&\n> +\ttest_must_be_empty output &&\n> +\n> +\t# Verify reflog message includes --revert and branch name\n> +\tgit reflog revert-test -1 --format=%gs >reflog-msg &&\n> +\techo \"replay --revert revert-test\" >expect-reflog &&\n> +\ttest_cmp expect-reflog reflog-msg\n> +'\n> +\n> +test_expect_success 'git replay --revert incompatible with --contained' '\n> +\ttest_must_fail git replay --revert revert-test --contained revert-test~1..revert-test 2>error &&\n> +\ttest_grep \"requires --onto\" error\n> +'\n> +\n> +test_expect_success 'git replay --revert incompatible with --onto' '\n> +\ttest_must_fail git replay --revert revert-test --onto main revert-test~1..revert-test 2>error &&\n> +\ttest_grep \"cannot be used together\" error\n> +'\n> +\n> +test_expect_success 'git replay --revert incompatible with --advance' '\n> +\ttest_must_fail git replay --revert revert-test --advance main revert-test~1..revert-test 2>error &&\n> +\ttest_grep \"cannot be used together\" error\n> +'\n\nThese last three look good.\n\nThanks\n\nPhillip\n\n"},{"id":"535768","messageId":"87bjhvqvol.fsf@iotcl.com","threadId":"64529","inReplyTo":"aTZ5RrjnwJ2ZnT7A@pks.im","subject":"Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-02-11T13:03:22Z","receivedAt":"2026-02-11T13:03:39Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Mon, Dec 08, 2025 at 04:30:58AM +0530, Siddharth Asthana wrote:\n>> \n>> On 05/12/25 17:03, Patrick Steinhardt wrote:\n>> > On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:\n>> > > diff --git a/sequencer.c b/sequencer.c\n>> > > index 5476d39ba9..9f621aef4b 100644\n>> > > --- a/sequencer.c\n>> > > +++ b/sequencer.c\n>> > > @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,\n>> > >   \t\tif (opts->commit_use_reference) {\n>> > >   \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>> > >   \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n>> > > -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n>> > > -\t\t\t   /*\n>> > > -\t\t\t    * We don't touch pre-existing repeated reverts, because\n>> > > -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n>> > > -\t\t\t    * thus requiring excessive complexity to deal with.\n>> > > -\t\t\t    */\n>> > > -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n>> > > -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n>> > > -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n>> > > -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n>> > > +\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>> > >   \t\t} else {\n>> > > -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n>> > > -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n>> > > -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n>> > > +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n>> > >   \t\t}\n>> > > -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>> > >   \t\trefer_to_commit(opts, &ctx->message, commit);\n>> > >   \t\tif (commit->parents && commit->parents->next) {\n>> > Is there any reason why we don't also handle `refer_to_commit()` in that\n>> > new function?\n>> \n>> \n>> The `refer_to_commit()` function depends on `struct replay_opts` and its\n>> `commit_use_reference` flag, which controls whether to use abbreviated\n>> commit info (\"%h (%s, %ad)\") or the full OID. This is specific to\n>> sequencer.c's interactive workflow where users can choose the reference\n>> style via --reference.\n>> \n>> In replay.c, we always use the full OID via `oid_to_hex()` since it's\n>> designed for non-interactive server-side operations without the\n>> `replay_opts` framework.\n\nEven if it's non-interactive, I wonder if we should make it obey the\nconfig 'revert.reference' as well? To me it makes sense git-replay(1)\nand git-revert(1) give the same outcome if that config is set.\n\n>> Including `refer_to_commit()` would require either\n>> passing `replay_opts` to the shared function (leaking sequencer internals)\n>> or adding a format parameter which feels like over-engineering for current\n>> needs.\n>> \n>> Happy to reconsider if you think there's a cleaner way to share this.\n>\n> A simple alternative might be to convert the `struct replay_opts`\n> parameter into a `flags` field that tells the function whether it is\n> expected to use the object ID or whether it should try using the\n> abbreviated commit info instead.\n\nI was considering to add a bool for this option alone, but I agree flags\nis probably more future-proof.\n\nPatrick, I assume you don't mean to revamp the `struct replay_opts`\ncompletely, but only the parameter that would be passed into\nsequencer_format_revert_header() and refer_to_commit()?\n\nSiddharth, I see you have plenty of good reviews on this version of the\nseries ([PATCH 2/2] in particular). I'd love to see you post v3. Or do\nyou have any open questions you need answers to before you can send it\nout? Let me know if I can help with any decision-making.\n\n-- \nCheers,\nToon\n"},{"id":"535770","messageId":"aYyGxdYKZpzz_Euy@pks.im","threadId":"64529","inReplyTo":"87bjhvqvol.fsf@iotcl.com","subject":"Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-11T13:40:21Z","receivedAt":"2026-02-11T13:40:29Z","isPatch":true,"body":"On Wed, Feb 11, 2026 at 02:03:22PM +0100, Toon Claes wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > parameter into a `flags` field that tells the function whether it is\n> > expected to use the object ID or whether it should try using the\n> > abbreviated commit info instead.\n> \n> I was considering to add a bool for this option alone, but I agree flags\n> is probably more future-proof.\n> \n> Patrick, I assume you don't mean to revamp the `struct replay_opts`\n> completely, but only the parameter that would be passed into\n> sequencer_format_revert_header() and refer_to_commit()?\n\nYes, exactly.\n\nPatrick\n"},{"id":"535776","messageId":"a4117ddd-1cd1-46d6-978b-b946f0199ef1@app.fastmail.com","threadId":"64529","inReplyTo":"87bjhvqvol.fsf@iotcl.com","subject":"Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-11T15:23:22Z","receivedAt":"2026-02-11T15:23:44Z","isPatch":true,"body":"On Wed, Feb 11, 2026, at 14:03, Toon Claes wrote:\n>>>[snip]\n>>> In replay.c, we always use the full OID via `oid_to_hex()` since it's\n>>> designed for non-interactive server-side operations without the\n>>> `replay_opts` framework.\n>\n> Even if it's non-interactive, I wonder if we should make it obey the\n> config 'revert.reference' as well? To me it makes sense git-replay(1)\n> and git-revert(1) give the same outcome if that config is set.\n\nI don’t understand the position on plumbing commands. Should plumbing\ncommands ignore user configs so that results don’t change based on that?\nOr should implementers that use this command set the config files to\n`/dev/null` in order to opt out of the behavior?\n\n>[snip]\n"},{"id":"535788","messageId":"xmqqjywjuqi8.fsf@gitster.g","threadId":"64529","inReplyTo":"a4117ddd-1cd1-46d6-978b-b946f0199ef1@app.fastmail.com","subject":"Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-11T17:41:35Z","receivedAt":"2026-02-11T17:41:38Z","isPatch":true,"body":"\"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n\n> I don’t understand the position on plumbing commands. Should plumbing\n> commands ignore user configs so that results don’t change based on that?\n> Or should implementers that use this command set the config files to\n> `/dev/null` in order to opt out of the behavior?\n\nThe former.\n"},{"id":"536358","messageId":"77e44b68-30cd-411b-b298-7f47911357e3@gmail.com","threadId":"64529","inReplyTo":"87bjhvqvol.fsf@iotcl.com","subject":"Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-02-18T22:53:04Z","receivedAt":"2026-02-18T22:53:11Z","isPatch":true,"body":"\nOn 11/02/26 18:33, Toon Claes wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> On Mon, Dec 08, 2025 at 04:30:58AM +0530, Siddharth Asthana wrote:\n>>> On 05/12/25 17:03, Patrick Steinhardt wrote:\n>>>> On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote:\n>>>>> diff --git a/sequencer.c b/sequencer.c\n>>>>> index 5476d39ba9..9f621aef4b 100644\n>>>>> --- a/sequencer.c\n>>>>> +++ b/sequencer.c\n>>>>> @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r,\n>>>>>    \t\tif (opts->commit_use_reference) {\n>>>>>    \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>>>>>    \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n>>>>> -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n>>>>> -\t\t\t   /*\n>>>>> -\t\t\t    * We don't touch pre-existing repeated reverts, because\n>>>>> -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n>>>>> -\t\t\t    * thus requiring excessive complexity to deal with.\n>>>>> -\t\t\t    */\n>>>>> -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n>>>>> -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n>>>>> -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n>>>>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n>>>>> +\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>>>>    \t\t} else {\n>>>>> -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n>>>>> -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n>>>>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n>>>>> +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n>>>>>    \t\t}\n>>>>> -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>>>>    \t\trefer_to_commit(opts, &ctx->message, commit);\n>>>>>    \t\tif (commit->parents && commit->parents->next) {\n>>>> Is there any reason why we don't also handle `refer_to_commit()` in that\n>>>> new function?\n>>>\n>>> The `refer_to_commit()` function depends on `struct replay_opts` and its\n>>> `commit_use_reference` flag, which controls whether to use abbreviated\n>>> commit info (\"%h (%s, %ad)\") or the full OID. This is specific to\n>>> sequencer.c's interactive workflow where users can choose the reference\n>>> style via --reference.\n>>>\n>>> In replay.c, we always use the full OID via `oid_to_hex()` since it's\n>>> designed for non-interactive server-side operations without the\n>>> `replay_opts` framework.\n> Even if it's non-interactive, I wonder if we should make it obey the\n> config 'revert.reference' as well? To me it makes sense git-replay(1)\n> and git-revert(1) give the same outcome if that config is set.\n\n\nJunio clarified downthread that plumbing commands should ignore user \nconfigs, so I think sticking with the full OID in replay is the right \nthing to do here.\n\n\n>\n>>> Including `refer_to_commit()` would require either\n>>> passing `replay_opts` to the shared function (leaking sequencer internals)\n>>> or adding a format parameter which feels like over-engineering for current\n>>> needs.\n>>>\n>>> Happy to reconsider if you think there's a cleaner way to share this.\n>> A simple alternative might be to convert the `struct replay_opts`\n>> parameter into a `flags` field that tells the function whether it is\n>> expected to use the object ID or whether it should try using the\n>> abbreviated commit info instead.\n> I was considering to add a bool for this option alone, but I agree flags\n> is probably more future-proof.\n\n\nFor v3 i went with an optional `oid` parameter on \nsequencer_format_revert_header() -- when non-NULL the function appends \nthe full hash itself, when NULL the caller (sequencer) handles the \nreference via refer_to_commit(). it is a simpler split than flags but \ngets the job done for now. I am more happy to switch to a flag approach \nif you and Patrick feel strongly about it.\n\n\n>\n> Patrick, I assume you don't mean to revamp the `struct replay_opts`\n> completely, but only the parameter that would be passed into\n> sequencer_format_revert_header() and refer_to_commit()?\n>\n> Siddharth, I see you have plenty of good reviews on this version of the\n> series ([PATCH 2/2] in particular). I'd love to see you post v3. Or do\n> you have any open questions you need answers to before you can send it\n> out? Let me know if I can help with any decision-making.\n>\n\nThanks Toon! v3 is ready, will share on mailing list thread soon. The \nmain change beside addressing Patrick's and Phillip's review comments is \na rebase on top of the latest upstream, which moved the replay logic \ninto a separate library (replay.c / replay.h), so the diff looks quite \ndifferent from v2 but the approach is the same.\n\n- Siddharth\n\n"},{"id":"536359","messageId":"20260218234215.89326-1-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20251202201611.22137-1-siddharthasthana31@gmail.com","subject":"[PATCH v3 0/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-02-18T23:42:13Z","receivedAt":"2026-02-18T23:42:23Z","isPatch":true,"body":"The `git replay` command performs server-side history rewriting without\nrequiring a working tree. While it currently supports cherry-picking\ncommits (--advance) and rebasing (--onto), it lacks the ability to\nrevert them.\n\nAt GitLab, we use replay in Gitaly for efficient server-side operations\non bare repositories. Adding revert functionality enables us to reverse\nproblematic commits directly on the server, eliminating client-side\nroundtrips and reducing network overhead.\n\nThe implementation follows the same approach as sequencer.c where\ncherry-pick and revert are the same merge operation but with swapped\narguments. For cherry-pick we merge(ancestor=parent, ours=current,\ntheirs=commit), while for revert we merge(ancestor=commit, ours=current,\ntheirs=parent). By swapping the base and pickme trees when calling\nmerge_incore_nonrecursive(), we effectively reverse the diff direction.\n\nThe series is structured as follows:\n\nPatch 1 extracts the revert message formatting logic into a shared\nsequencer_format_revert_header() function, eliminating code duplication\nbetween sequencer.c and the upcoming replay code. This follows Junio's\nsuggestion to split the changes.\n\nPatch 2 adds the --revert <branch> mode to git replay. Following the\narchitectural pattern suggested by Elijah and Phillip, --revert is a\nstandalone mode (like --onto and --advance) that takes a branch argument\nand updates that branch with the revert commits.\n\nThe series is based on top of 864f55e190 (The second batch, 2026-02-07).\n\nCI: https://gitlab.com/gitlab-org/git/-/pipelines/2329880894\nThe Windows CI failures (t4041, t4059, t4060, t4205, t6006) are\npre-existing infrastructure issues (missing iconv, submodule pathspec\nerrors) unrelated to this series.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\nChanges in v3:\n- Rebased on top of the latest upstream which refactored replay into\n  a library (replay.c / replay.h). The --revert logic now lives in\n  replay.c alongside the existing pick logic, while builtin/replay.c\n  is a thin CLI wrapper.\n- sequencer_format_revert_header() now takes an optional oid parameter\n  so it can handle the full commit reference internally, per Patrick's\n  suggestion about refer_to_commit()\n- Removed now-unused `orig_subject` variable in do_pick_commit()\n- Switched to die_for_incompatible_opt3() for --onto/--advance/--revert\n  mutual exclusivity, per Patrick\n- --contained now just checks \"die(_(\"--contained requires --onto\"))\"\n  instead of going through die_for_incompatible_opt2, per Phillip\n- Added BUG() guards for unhandled replay_mode values, per Patrick\n- Merged the separate advance/revert ref update blocks into one\n- author is set to NULL for revert commits so commit_tree_extended()\n  picks up the current user, per Phillip\n- Factored out common --advance/--revert branch validation into a\n  set_up_branch_mode() helper, per Phillip\n- Doc wording fixes: \"reverted commits\", \"they are prefixed\",\n  \"hash\" instead of \"SHA\", per Phillip\n- Tests now reuse topic4 instead of creating new branches, use heredoc\n  for test_commit_message, and the reflog check is folded into the\n  main revert test\n- Added tests for bare repo revert, error cases (argument validation,\n  multiple sources)\n- Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/t/#u\n- Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/t/#u\n\n---\n Documentation/git-replay.adoc |  37 +++++++-\n builtin/replay.c              |  25 ++++--\n replay.c                      | 162 ++++++++++++++++++++++++----------\n replay.h                      |  11 ++-\n sequencer.c                   |  47 ++++++----\n sequencer.h                   |  11 +++\n t/t3650-replay-basics.sh      | 107 ++++++++++++++++++++--\n 7 files changed, 319 insertions(+), 81 deletions(-)\n\nSiddharth Asthana (2):\n  sequencer: extract revert message formatting into shared function\n  replay: add --revert mode to reverse commit changes\n\nRange-diff versus v2:\n\n1:  bfd75484b4 ! 1:  9d686bcdfe sequencer: extract revert message formatting into shared function\n    @@ Commit message\n         Extract this logic into a new sequencer_format_revert_header() function\n         that can be shared. The function handles both regular reverts (\"Revert\n         \"<subject>\"\") and revert-of-revert cases (\"Reapply \"<subject>\"\").\n    +    When an oid is provided, the function appends the full commit hash and\n    +    period; otherwise the caller should append the commit reference.\n    +\n         Update do_pick_commit() to use the new helper, eliminating code\n         duplication while preserving the special handling for commit_use_reference.\n     \n         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## sequencer.c ##\n    +@@ sequencer.c: static int do_pick_commit(struct repository *r,\n    + \t */\n    + \n    + \tif (command == TODO_REVERT) {\n    +-\t\tconst char *orig_subject;\n    +-\n    + \t\tbase = commit;\n    + \t\tbase_label = msg.label;\n    + \t\tnext = parent;\n     @@ sequencer.c: static int do_pick_commit(struct repository *r,\n      \t\tif (opts->commit_use_reference) {\n      \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n     @@ sequencer.c: static int do_pick_commit(struct repository *r,\n     -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n     -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n     -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n    -+\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject);\n    ++\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject, NULL);\n      \t\t}\n     -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n      \t\trefer_to_commit(opts, &ctx->message, commit);\n    @@ sequencer.c: int sequencer_pick_revisions(struct repository *r,\n      \treturn res;\n      }\n      \n    -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject)\n    ++void sequencer_format_revert_header(struct strbuf *out,\n    ++\t\t\t\t    const char *orig_subject,\n    ++\t\t\t\t    const struct object_id *oid)\n     +{\n     +\tconst char *revert_subject;\n     +\n    @@ sequencer.c: int sequencer_pick_revisions(struct repository *r,\n     +\t}\n     +\n     +\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n    ++\tif (oid) {\n    ++\t\tstrbuf_addstr(out, oid_to_hex(oid));\n    ++\t\tstrbuf_addstr(out, \".\\n\");\n    ++\t}\n     +}\n     +\n      void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n    @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh\n      int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n      \n     +/*\n    -+ * Formats a revert commit message header following standard Git conventions.\n    ++ * Formats a revert commit message following standard Git conventions.\n     + * Handles both regular reverts (\"Revert \\\"<subject>\\\"\") and revert of revert\n    -+ * cases (\"Reapply \\\"<subject>\\\"\"). Adds \"This reverts commit \" at the end.\n    -+ * The caller should append the commit OID after calling this function.\n    ++ * cases (\"Reapply \\\"<subject>\\\"\"). Adds \"This reverts commit <oid>.\" if oid\n    ++ * is provided, otherwise just adds \"This reverts commit \" and the caller\n    ++ * should append the commit reference.\n     + */\n    -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject);\n    ++void sequencer_format_revert_header(struct strbuf *out,\n    ++\t\t\t\t    const char *orig_subject,\n    ++\t\t\t\t    const struct object_id *oid);\n     +\n      #endif /* SEQUENCER_H */\n2:  a2f99bc8c2 < -:  ---------- replay: add --revert mode to reverse commit changes\n-:  ---------- > 2:  a8eae7b802 replay: add --revert mode to reverse commit changes\n\n\nbase-commit: 864f55e1906897b630333675a52874c0fec2a45c\n\nThanks\n- Siddharth\n"},{"id":"536360","messageId":"20260218234215.89326-2-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260218234215.89326-1-siddharthasthana31@gmail.com","subject":"[PATCH v3 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-02-18T23:42:14Z","receivedAt":"2026-02-18T23:42:31Z","isPatch":true,"body":"The logic for formatting revert commit messages (handling \"Revert\" and\n\"Reapply\" cases) is currently duplicated between sequencer.c and will be\nneeded by builtin/replay.c.\n\nExtract this logic into a new sequencer_format_revert_header() function\nthat can be shared. The function handles both regular reverts (\"Revert\n\"<subject>\"\") and revert-of-revert cases (\"Reapply \"<subject>\"\").\nWhen an oid is provided, the function appends the full commit hash and\nperiod; otherwise the caller should append the commit reference.\n\nUpdate do_pick_commit() to use the new helper, eliminating code\nduplication while preserving the special handling for commit_use_reference.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n sequencer.c | 47 +++++++++++++++++++++++++++++++----------------\n sequencer.h | 11 +++++++++++\n 2 files changed, 42 insertions(+), 16 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 1f492f8460..b32347c853 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2356,8 +2356,6 @@ static int do_pick_commit(struct repository *r,\n \t */\n \n \tif (command == TODO_REVERT) {\n-\t\tconst char *orig_subject;\n-\n \t\tbase = commit;\n \t\tbase_label = msg.label;\n \t\tnext = parent;\n@@ -2365,22 +2363,10 @@ static int do_pick_commit(struct repository *r,\n \t\tif (opts->commit_use_reference) {\n \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n-\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n-\t\t\t   /*\n-\t\t\t    * We don't touch pre-existing repeated reverts, because\n-\t\t\t    * theoretically these can be nested arbitrarily deeply,\n-\t\t\t    * thus requiring excessive complexity to deal with.\n-\t\t\t    */\n-\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n+\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n \t\t} else {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n+\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject, NULL);\n \t\t}\n-\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n \t\trefer_to_commit(opts, &ctx->message, commit);\n \n \t\tif (commit->parents && commit->parents->next) {\n@@ -5572,6 +5558,35 @@ int sequencer_pick_revisions(struct repository *r,\n \treturn res;\n }\n \n+void sequencer_format_revert_header(struct strbuf *out,\n+\t\t\t\t    const char *orig_subject,\n+\t\t\t\t    const struct object_id *oid)\n+{\n+\tconst char *revert_subject;\n+\n+\tif (skip_prefix(orig_subject, \"Revert \\\"\", &revert_subject) &&\n+\t    /*\n+\t     * We don't touch pre-existing repeated reverts, because\n+\t     * theoretically these can be nested arbitrarily deeply,\n+\t     * thus requiring excessive complexity to deal with.\n+\t     */\n+\t    !starts_with(revert_subject, \"Revert \\\"\")) {\n+\t\tstrbuf_addstr(out, \"Reapply \\\"\");\n+\t\tstrbuf_addstr(out, revert_subject);\n+\t\tstrbuf_addch(out, '\\n');\n+\t} else {\n+\t\tstrbuf_addstr(out, \"Revert \\\"\");\n+\t\tstrbuf_addstr(out, orig_subject);\n+\t\tstrbuf_addstr(out, \"\\\"\\n\");\n+\t}\n+\n+\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n+\tif (oid) {\n+\t\tstrbuf_addstr(out, oid_to_hex(oid));\n+\t\tstrbuf_addstr(out, \".\\n\");\n+\t}\n+}\n+\n void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n {\n \tunsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;\ndiff --git a/sequencer.h b/sequencer.h\nindex 719684c8a9..b7291cc52d 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -271,4 +271,15 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n  */\n int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n \n+/*\n+ * Formats a revert commit message following standard Git conventions.\n+ * Handles both regular reverts (\"Revert \\\"<subject>\\\"\") and revert of revert\n+ * cases (\"Reapply \\\"<subject>\\\"\"). Adds \"This reverts commit <oid>.\" if oid\n+ * is provided, otherwise just adds \"This reverts commit \" and the caller\n+ * should append the commit reference.\n+ */\n+void sequencer_format_revert_header(struct strbuf *out,\n+\t\t\t\t    const char *orig_subject,\n+\t\t\t\t    const struct object_id *oid);\n+\n #endif /* SEQUENCER_H */\n-- \n2.51.0\n\n"},{"id":"536361","messageId":"20260218234215.89326-3-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260218234215.89326-1-siddharthasthana31@gmail.com","subject":"[PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-02-18T23:42:15Z","receivedAt":"2026-02-18T23:42:37Z","isPatch":true,"body":"Add a `--revert <branch>` mode to git replay that undoes the changes\nintroduced by the specified commits. Like --onto and --advance, --revert\nis a standalone mode: it takes a branch argument and updates that branch\nwith the newly created revert commits.\n\nAt GitLab, we need this in Gitaly for reverting commits directly on bare\nrepositories without requiring a working tree checkout.\n\nThe approach is the same as sequencer.c's do_pick_commit() -- cherry-pick\nand revert are just the same three-way merge with swapped arguments:\n\n  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n  - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n\nWe swap the base and pickme trees passed to merge_incore_nonrecursive()\nto reverse the diff direction.\n\nRevert commit messages follow the usual git revert conventions: prefixed\nwith \"Revert\" (or \"Reapply\" when reverting a revert), and including\n\"This reverts commit <hash>.\". The author is set to the current user\nrather than preserving the original author, matching git revert behavior.\n\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nHelped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  37 +++++++-\n builtin/replay.c              |  25 ++++--\n replay.c                      | 162 ++++++++++++++++++++++++----------\n replay.h                      |  11 ++-\n t/t3650-replay-basics.sh      | 107 ++++++++++++++++++++--\n 5 files changed, 277 insertions(+), 65 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 8d696ce3ab..ffdf790278 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n \n DESCRIPTION\n -----------\n@@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to\n point at the tip of the resulting history. This is different from `--onto`,\n which uses the target only as a starting point without updating it.\n \n+--revert <branch>::\n+\tStarting point at which to create the reverted commits; must be a\n+\tbranch name.\n++\n+When `--revert` is specified, the commits in the revision range are reverted\n+(their changes are undone) and the reverted commits are created on top of\n+<branch>. The <branch> is then updated to point at the new commits. This is\n+the same as running `git revert <revision-range>` but does not update the\n+working tree.\n++\n+The commit messages follow `git revert` conventions: they are prefixed with\n+\"Revert\" and include \"This reverts commit <hash>.\" When reverting a commit\n+whose message starts with \"Revert\", the new message uses \"Reapply\" instead.\n+Unlike cherry-pick which preserves the original author, revert commits use\n+the current user as the author, matching the behavior of `git revert`.\n++\n+This option is mutually exclusive with `--onto` and `--advance`. It is also\n+incompatible with `--contained` (which is a modifier for `--onto` only).\n+\n --contained::\n \tUpdate all branches that point at commits in\n \t<revision-range>. Requires `--onto`.\n@@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to\n \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n \n where the number of refs updated depends on the arguments passed and\n-the shape of the history being replayed.  When using `--advance`, the\n-number of refs updated is always one, but for `--onto`, it can be one\n-or more (rebasing multiple branches simultaneously is supported).\n+the shape of the history being replayed.  When using `--advance` or\n+`--revert`, the number of refs updated is always one, but for `--onto`,\n+it can be one or more (rebasing multiple branches simultaneously is\n+supported).\n \n There is no stderr output on conflicts; see the <<exit-status,EXIT\n STATUS>> section below.\n@@ -152,6 +172,15 @@ all commits they have since `base`, playing them on top of\n `origin/main`. These three branches may have commits on top of `base`\n that they have in common, but that does not need to be the case.\n \n+To revert commits on a branch:\n+\n+------------\n+$ git replay --revert main main~2..main\n+------------\n+\n+This reverts the last two commits on `main`, creating two revert commits\n+on top of `main`, and updates `main` to point at the result.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 2cdde830a8..28ce5196db 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -83,8 +83,8 @@ int cmd_replay(int argc,\n \n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n+\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n+\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -96,6 +96,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n+\t\tOPT_STRING(0, \"revert\", &opts.revert,\n+\t\t\t   N_(\"branch\"),\n+\t\t\t   N_(\"revert commits onto given branch\")),\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n \t\t\t   N_(\"control ref update behavior (update|print)\")),\n@@ -105,15 +108,17 @@ int cmd_replay(int argc,\n \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n-\tif (!opts.onto && !opts.advance) {\n-\t\terror(_(\"option --onto or --advance is mandatory\"));\n+\t/* Exactly one mode must be specified */\n+\tif (!opts.onto && !opts.advance && !opts.revert) {\n+\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n-\t\t\t\t  opts.contained, \"--contained\");\n-\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n-\t\t\t\t  !!opts.onto, \"--onto\");\n+\tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n+\t\t\t\t  !!opts.advance, \"--advance\",\n+\t\t\t\t  !!opts.revert, \"--revert\");\n+\tif (opts.contained && !opts.onto)\n+\t\tdie(_(\"--contained requires --onto\"));\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n@@ -174,7 +179,9 @@ int cmd_replay(int argc,\n \t\tgoto cleanup;\n \n \t/* Build reflog message */\n-\tif (opts.advance) {\n+\tif (opts.revert) {\n+\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", opts.revert);\n+\t} else if (opts.advance) {\n \t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n \t} else {\n \t\tstruct object_id oid;\ndiff --git a/replay.c b/replay.c\nindex f97d652f33..6f8b5720b3 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -8,9 +8,15 @@\n #include \"refs.h\"\n #include \"replay.h\"\n #include \"revision.h\"\n+#include \"sequencer.h\"\n #include \"strmap.h\"\n #include \"tree.h\"\n \n+enum replay_mode {\n+\tREPLAY_MODE_PICK,\n+\tREPLAY_MODE_REVERT,\n+};\n+\n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n {\n@@ -44,15 +50,35 @@ static char *get_author(const char *message)\n \treturn NULL;\n }\n \n+static void generate_revert_message(struct strbuf *msg,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct repository *repo)\n+{\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n+\tconst char *subject_start;\n+\tint subject_len;\n+\tchar *subject;\n+\n+\tsubject_len = find_commit_subject(message, &subject_start);\n+\tsubject = xmemdupz(subject_start, subject_len);\n+\n+\tsequencer_format_revert_header(msg, subject, &commit->object.oid);\n+\n+\tfree(subject);\n+\trepo_unuse_commit_buffer(repo, commit, message);\n+}\n+\n static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n+\t\t\t\t    struct commit *parent,\n+\t\t\t\t    enum replay_mode mode)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n-\tchar *author;\n+\tchar *author = NULL;\n \tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n@@ -64,9 +90,16 @@ static struct commit *create_commit(struct repository *repo,\n \n \tcommit_list_insert(parent, &parents);\n \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n+\tif (mode == REPLAY_MODE_REVERT) {\n+\t\tgenerate_revert_message(&msg, based_on, repo);\n+\t\t/* For revert, use current user as author (NULL = use default) */\n+\t} else if (mode == REPLAY_MODE_PICK) {\n+\t\tfind_commit_subject(message, &orig_message);\n+\t\tstrbuf_addstr(&msg, orig_message);\n+\t\tauthor = get_author(message);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \treset_ident_date();\n \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n@@ -147,11 +180,34 @@ static void get_ref_information(struct repository *repo,\n \t}\n }\n \n+static void set_up_branch_mode(struct repository *repo,\n+\t\t\t       char **branch_name,\n+\t\t\t       const char *option_name,\n+\t\t\t       struct ref_info *rinfo,\n+\t\t\t       struct commit **onto)\n+{\n+\tstruct object_id oid;\n+\tchar *fullname = NULL;\n+\n+\tif (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),\n+\t\t\t  &oid, &fullname, 0) == 1) {\n+\t\tfree(*branch_name);\n+\t\t*branch_name = fullname;\n+\t} else {\n+\t\tdie(_(\"argument to %s must be a reference\"), option_name);\n+\t}\n+\t*onto = peel_committish(repo, *branch_name, option_name);\n+\tif (rinfo->positive_refexprs > 1)\n+\t\tdie(_(\"cannot %s target with multiple sources because ordering would be ill-defined\"),\n+\t\t    option_name + 2); /* skip \"--\" prefix */\n+}\n+\n static void set_up_replay_mode(struct repository *repo,\n \t\t\t       struct rev_cmdline_info *cmd_info,\n \t\t\t       const char *onto_name,\n \t\t\t       bool *detached_head,\n \t\t\t       char **advance_name,\n+\t\t\t       char **revert_name,\n \t\t\t       struct commit **onto,\n \t\t\t       struct strset **update_refs)\n {\n@@ -166,9 +222,6 @@ static void set_up_replay_mode(struct repository *repo,\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n \n-\tif (!onto_name == !*advance_name)\n-\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n-\n \tif (onto_name) {\n \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n \t\tif (rinfo.positive_refexprs <\n@@ -177,23 +230,12 @@ static void set_up_replay_mode(struct repository *repo,\n \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n \t\t**update_refs = rinfo.positive_refs;\n \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n+\t} else if (*advance_name) {\n+\t\tset_up_branch_mode(repo, advance_name, \"--advance\", &rinfo, onto);\n+\t} else if (*revert_name) {\n+\t\tset_up_branch_mode(repo, revert_name, \"--revert\", &rinfo, onto);\n \t} else {\n-\t\tstruct object_id oid;\n-\t\tchar *fullname = NULL;\n-\n-\t\tif (!*advance_name)\n-\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n-\n-\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n-\t\t\t     &oid, &fullname, 0) == 1) {\n-\t\t\tfree(*advance_name);\n-\t\t\t*advance_name = fullname;\n-\t\t} else {\n-\t\t\tdie(_(\"argument to --advance must be a reference\"));\n-\t\t}\n-\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n-\t\tif (rinfo.positive_refexprs > 1)\n-\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n+\t\tBUG(\"expected one of onto_name, *advance_name, or *revert_name\");\n \t}\n \tstrset_clear(&rinfo.negative_refs);\n \tstrset_clear(&rinfo.positive_refs);\n@@ -214,7 +256,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n \t\t\t\t\t  struct commit *onto,\n \t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n+\t\t\t\t\t  struct merge_result *result,\n+\t\t\t\t\t  enum replay_mode mode)\n {\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n@@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  replayed_base_tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n+\tif (mode == REPLAY_MODE_PICK) {\n+\t\t/* Cherry-pick: normal order */\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->ancestor);\n+\t} else if (mode == REPLAY_MODE_REVERT) {\n+\t\t/* Revert: swap base and pickme to reverse the diff */\n+\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n+\t\tmerge_opt->ancestor = pickme_name;\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->branch2);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \tmerge_opt->ancestor = NULL;\n+\tmerge_opt->branch2 = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n-\t/* Drop commits that become empty */\n-\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n+\t/* Drop commits that become empty (only for picks) */\n+\tif (mode == REPLAY_MODE_PICK &&\n+\t    oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n \t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n \t\treturn replayed_base;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -281,11 +345,16 @@ int replay_revisions(struct rev_info *revs,\n \t};\n \tbool detached_head;\n \tchar *advance;\n+\tchar *revert;\n+\tenum replay_mode mode = REPLAY_MODE_PICK;\n \tint ret;\n \n \tadvance = xstrdup_or_null(opts->advance);\n+\trevert = xstrdup_or_null(opts->revert);\n+\tif (revert)\n+\t\tmode = REPLAY_MODE_REVERT;\n \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n-\t\t\t   &detached_head, &advance, &onto, &update_refs);\n+\t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n \n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n@@ -309,7 +378,7 @@ int replay_revisions(struct rev_info *revs,\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\t\t\t\t\t  onto, &merge_opt, &result, mode);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \n@@ -321,7 +390,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance)\n+\t\tif (advance || revert)\n \t\t\tcontinue;\n \n \t\tfor (decoration = get_name_decoration(&commit->object);\n@@ -355,11 +424,13 @@ int replay_revisions(struct rev_info *revs,\n \t\tgoto out;\n \t}\n \n-\t/* In --advance mode, advance the target ref */\n-\tif (advance)\n-\t\treplay_result_queue_update(out, advance,\n+\t/* In --advance or --revert mode, update the target ref */\n+\tif (advance || revert) {\n+\t\tconst char *ref = advance ? advance : revert;\n+\t\treplay_result_queue_update(out, ref,\n \t\t\t\t\t   &onto->object.oid,\n \t\t\t\t\t   &last_commit->object.oid);\n+\t}\n \n \tret = 0;\n \n@@ -371,5 +442,6 @@ int replay_revisions(struct rev_info *revs,\n \tkh_destroy_oid_map(replayed_commits);\n \tmerge_finalize(&merge_opt, &result);\n \tfree(advance);\n+\tfree(revert);\n \treturn ret;\n }\ndiff --git a/replay.h b/replay.h\nindex d8407dc7f7..e916a5f975 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -13,7 +13,7 @@ struct replay_revisions_options {\n \t/*\n \t * Starting point at which to create the new commits; must be a branch\n \t * name. The branch will be updated to point to the rewritten commits.\n-\t * This option is mutually exclusive with `onto`.\n+\t * This option is mutually exclusive with `onto` and `revert`.\n \t */\n \tconst char *advance;\n \n@@ -22,7 +22,14 @@ struct replay_revisions_options {\n \t * committish. References pointing at decendants of `onto` will be\n \t * updated to point to the new commits.\n \t */\n-\t const char *onto;\n+\tconst char *onto;\n+\n+\t/*\n+\t * Starting point at which to create revert commits; must be a branch\n+\t * name. The branch will be updated to point to the revert commits.\n+\t * This option is mutually exclusive with `onto` and `advance`.\n+\t */\n+\tconst char *revert;\n \n \t/*\n \t * Update branches that point at commits in the given revision range.\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex a03f8f9293..ca517cf607 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'option --onto or --advance is mandatory' '\n-\techo \"error: option --onto or --advance is mandatory\" >expect &&\n+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '\n+\techo \"error: exactly one of --onto, --advance, or --revert is required\" >expect &&\n \ttest_might_fail git replay -h >>expect &&\n \ttest_must_fail git replay topic1..topic2 2>actual &&\n \ttest_cmp expect actual\n@@ -87,9 +87,8 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'options --advance and --contained cannot be used together' '\n-\tprintf \"fatal: options ${SQ}--advance${SQ} \" >expect &&\n-\tprintf \"and ${SQ}--contained${SQ} cannot be used together\\n\" >>expect &&\n+test_expect_success '--contained requires --onto' '\n+\techo \"fatal: --contained requires --onto\" >expect &&\n \ttest_must_fail git replay --advance=main --contained \\\n \t\ttopic1..topic2 2>actual &&\n \ttest_cmp expect actual\n@@ -398,4 +397,102 @@ test_expect_success 'invalid replay.refAction value' '\n \ttest_grep \"invalid.*replay.refAction.*value\" error\n '\n \n+test_expect_success 'argument to --revert must be a reference' '\n+\techo \"fatal: argument to --revert must be a reference\" >expect &&\n+\toid=$(git rev-parse main) &&\n+\ttest_must_fail git replay --revert=$oid topic1..topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'cannot revert with multiple sources' '\n+\techo \"fatal: cannot revert target with multiple sources because ordering would be ill-defined\" >expect &&\n+\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using replay --revert to revert commits' '\n+\t# Reuse existing topic4 branch (has commits I and J on top of main)\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# Revert commits I and J\n+\tgit replay --revert topic4 topic4~2..topic4 &&\n+\n+\t# Verify the revert commits were created\n+\tgit log --format=%s -4 topic4 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\tRevert \"J\"\n+\tRevert \"I\"\n+\tJ\n+\tI\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\t# Verify commit message format includes hash\n+\ttest_commit_message topic4 <<-EOF &&\n+\tRevert \"J\"\n+\n+\tThis reverts commit $(git rev-parse J).\n+\tEOF\n+\n+\t# Verify reflog message\n+\tgit reflog topic4 -1 --format=%gs >reflog-msg &&\n+\techo \"replay --revert topic4\" >expect-reflog &&\n+\ttest_cmp expect-reflog reflog-msg\n+'\n+\n+test_expect_success 'using replay --revert in bare repo' '\n+\t# Reuse existing topic4 in bare repo\n+\tSTART=$(git -C bare rev-parse topic4) &&\n+\ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n+\n+\t# Revert commit J in bare repo\n+\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify revert was created\n+\tgit -C bare log -1 --format=%s topic4 >actual &&\n+\techo \"Revert \\\"J\\\"\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'revert of revert uses Reapply' '\n+\t# Use topic4 and first revert J, then revert the revert\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# First revert J\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\tREVERT_J=$(git rev-parse topic4) &&\n+\n+\t# Now revert the revert - should become Reapply\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify Reapply prefix and message format\n+\ttest_commit_message topic4 <<-EOF\n+\tReapply \"J\"\n+\n+\tThis reverts commit $REVERT_J.\n+\tEOF\n+'\n+\n+test_expect_success 'git replay --revert with conflict' '\n+\t# conflict branch has C.conflict which conflicts with topic1s C\n+\ttest_expect_code 1 git replay --revert conflict B..topic1\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --contained' '\n+\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n+\ttest_grep \"requires --onto\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --onto' '\n+\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --advance' '\n+\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"536540","messageId":"87wm07e4ck.fsf@iotcl.com","threadId":"64529","inReplyTo":"20260218234215.89326-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-02-20T17:01:47Z","receivedAt":"2026-02-20T17:02:10Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> The logic for formatting revert commit messages (handling \"Revert\" and\n> \"Reapply\" cases) is currently duplicated between sequencer.c and will be\n> needed by builtin/replay.c.\n>\n> Extract this logic into a new sequencer_format_revert_header() function\n> that can be shared. The function handles both regular reverts (\"Revert\n> \"<subject>\"\") and revert-of-revert cases (\"Reapply \"<subject>\"\").\n> When an oid is provided, the function appends the full commit hash and\n> period; otherwise the caller should append the commit reference.\n>\n> Update do_pick_commit() to use the new helper, eliminating code\n> duplication while preserving the special handling for commit_use_reference.\n>\n> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> ---\n>  sequencer.c | 47 +++++++++++++++++++++++++++++++----------------\n>  sequencer.h | 11 +++++++++++\n>  2 files changed, 42 insertions(+), 16 deletions(-)\n>\n> diff --git a/sequencer.c b/sequencer.c\n> index 1f492f8460..b32347c853 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -2356,8 +2356,6 @@ static int do_pick_commit(struct repository *r,\n>  \t */\n>  \n>  \tif (command == TODO_REVERT) {\n> -\t\tconst char *orig_subject;\n> -\n>  \t\tbase = commit;\n>  \t\tbase_label = msg.label;\n>  \t\tnext = parent;\n> @@ -2365,22 +2363,10 @@ static int do_pick_commit(struct repository *r,\n>  \t\tif (opts->commit_use_reference) {\n>  \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>  \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n> -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n> -\t\t\t   /*\n> -\t\t\t    * We don't touch pre-existing repeated reverts, because\n> -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n> -\t\t\t    * thus requiring excessive complexity to deal with.\n> -\t\t\t    */\n> -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n> -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n> -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n> -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n> +\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>  \t\t} else {\n> -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n> -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n> -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n> +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject, NULL);\n>  \t\t}\n> -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>  \t\trefer_to_commit(opts, &ctx->message, commit);\n\nI still find it somewhat confusing we have some the code that deals with\n`opts->commit_use_reference` partly in here and partly in\nsequencer_format_revert_header().\n\nPart of the confusion comes from sequencer_format_revert_header() being\ncalled with NULL for the commit OID.\n\nWas is not possible to incorporate Patrick's suggestion[1]?\n\n[1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/\n\n-- \nCheers,\nToon\n"},{"id":"536542","messageId":"87tsvbe2sm.fsf@iotcl.com","threadId":"64529","inReplyTo":"20260218234215.89326-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-02-20T17:35:21Z","receivedAt":"2026-02-20T17:35:34Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> Add a `--revert <branch>` mode to git replay that undoes the changes\n> introduced by the specified commits. Like --onto and --advance, --revert\n> is a standalone mode: it takes a branch argument and updates that branch\n> with the newly created revert commits.\n>\n> At GitLab, we need this in Gitaly for reverting commits directly on bare\n> repositories without requiring a working tree checkout.\n>\n> The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick\n> and revert are just the same three-way merge with swapped arguments:\n>\n>   - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n>   - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n>\n> We swap the base and pickme trees passed to merge_incore_nonrecursive()\n> to reverse the diff direction.\n>\n> Revert commit messages follow the usual git revert conventions: prefixed\n> with \"Revert\" (or \"Reapply\" when reverting a revert), and including\n> \"This reverts commit <hash>.\". The author is set to the current user\n> rather than preserving the original author, matching git revert behavior.\n>\n> Helped-by: Christian Couder <christian.couder@gmail.com>\n> Helped-by: Patrick Steinhardt <ps@pks.im>\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\n> Helped-by: Junio C Hamano <gitster@pobox.com>\n> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> ---\n>  Documentation/git-replay.adoc |  37 +++++++-\n>  builtin/replay.c              |  25 ++++--\n>  replay.c                      | 162 ++++++++++++++++++++++++----------\n>  replay.h                      |  11 ++-\n>  t/t3650-replay-basics.sh      | 107 ++++++++++++++++++++--\n>  5 files changed, 277 insertions(+), 65 deletions(-)\n>\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index 8d696ce3ab..ffdf790278 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n\nThe modes `--onto`, `--advance` and `--revert` seem to be extremely\ndifferent from each other. So I'm starting to wonder whether it won't\nmake more sense to instead create subcommands instead of options for\nthese. Maybe something like:\n\n    git replay revert --base=<branch> <revision-range>\n    git replay pick --base=<branch> <revision-range>\n    git replay replay --base=<branch> <revision-range>\n\n\n-- \nCheers,\nToon\n"},{"id":"536550","messageId":"xmqqy0knw4dg.fsf@gitster.g","threadId":"64529","inReplyTo":"87tsvbe2sm.fsf@iotcl.com","subject":"Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-20T20:23:55Z","receivedAt":"2026-02-20T20:23:58Z","isPatch":true,"body":"Toon Claes <toon@iotcl.com> writes:\n\n>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n>> index 8d696ce3ab..ffdf790278 100644\n>> --- a/Documentation/git-replay.adoc\n>> +++ b/Documentation/git-replay.adoc\n>> @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>>  SYNOPSIS\n>>  --------\n>>  [verse]\n>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n>\n> The modes `--onto`, `--advance` and `--revert` seem to be extremely\n> different from each other. So I'm starting to wonder whether it won't\n> make more sense to instead create subcommands instead of options for\n> these. Maybe something like:\n>\n>     git replay revert --base=<branch> <revision-range>\n>     git replay pick --base=<branch> <revision-range>\n>     git replay replay --base=<branch> <revision-range>\n\nGiven the earlier discussion on confusions (mostly by those like me\nwho weren't closely watching the topics around this command) on what\noptions among --onto, --advance, etc. were incompatible, etc., that\ncertainly does sound like a good way to make these distinctive modes\nmore clearly stand out.\n\nThanks.\n"},{"id":"536720","messageId":"CAP8UFD1CAYZwK4x4-AZWjx3cubzu5WcndR8WJzhcegET+i22nA@mail.gmail.com","threadId":"64529","inReplyTo":"87tsvbe2sm.fsf@iotcl.com","subject":"Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-23T09:13:15Z","receivedAt":"2026-02-23T09:13:28Z","isPatch":true,"body":"On Fri, Feb 20, 2026 at 6:35 PM Toon Claes <toon@iotcl.com> wrote:\n>\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> > index 8d696ce3ab..ffdf790278 100644\n> > --- a/Documentation/git-replay.adoc\n> > +++ b/Documentation/git-replay.adoc\n> > @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n> >  SYNOPSIS\n> >  --------\n> >  [verse]\n> > -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n> > +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n>\n> The modes `--onto`, `--advance` and `--revert` seem to be extremely\n> different from each other. So I'm starting to wonder whether it won't\n> make more sense to instead create subcommands instead of options for\n> these. Maybe something like:\n>\n>     git replay revert --base=<branch> <revision-range>\n>     git replay pick --base=<branch> <revision-range>\n>     git replay replay --base=<branch> <revision-range>\n\n(I think you mean `git replay rebase` in the above line, no?)\n\nI agree that we should consider this. But I think we should do it\nseparately in another series, after this one about --revert is merged.\nWe might even consider waiting until we have more experience using\n`git replay --revert` to make a more informed decision. We shouldn't\nwait for too long either though...\n\nAlso if we nearly always need a base, then why not:\n\n  git replay rebase <base> <revision-range>\n  git replay pick <base> <revision-range>\n  git replay revert <base> <revision-range>\n\n?\n\nOr what was the reason for introducing --base=<branch>?\n"},{"id":"536746","messageId":"87pl5vem9g.fsf@iotcl.com","threadId":"64529","inReplyTo":"CAP8UFD1CAYZwK4x4-AZWjx3cubzu5WcndR8WJzhcegET+i22nA@mail.gmail.com","subject":"Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-02-23T11:23:55Z","receivedAt":"2026-02-23T11:24:16Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> On Fri, Feb 20, 2026 at 6:35 PM Toon Claes <toon@iotcl.com> wrote:\n>>\n>> The modes `--onto`, `--advance` and `--revert` seem to be extremely\n>> different from each other. So I'm starting to wonder whether it won't\n>> make more sense to instead create subcommands instead of options for\n>> these. Maybe something like:\n>>\n>>     git replay revert --base=<branch> <revision-range>\n>>     git replay pick --base=<branch> <revision-range>\n>>     git replay replay --base=<branch> <revision-range>\n>\n> (I think you mean `git replay rebase` in the above line, no?)\n\nI'm fine either way, but I agree saying \"replay\" twice looks weird.\n\n> I agree that we should consider this. But I think we should do it\n> separately in another series, after this one about --revert is merged.\n> We might even consider waiting until we have more experience using\n> `git replay --revert` to make a more informed decision. We shouldn't\n> wait for too long either though...\n\nI can agree with that.\n\n> Also if we nearly always need a base, then why not:\n>\n>   git replay rebase <base> <revision-range>\n>   git replay pick <base> <revision-range>\n>   git replay revert <base> <revision-range>\n>\n> ?\n>\n> Or what was the reason for introducing --base=<branch>?\n\nWell, the modes 'revert' and 'pick' work different from 'replay'. The\nlatter looks in <revision-range> to determine which refs need updating.\nThis can lead to multiple refs that will be updated (with option\n--contained). The first two only operate on one ref and ignore whatever\nrefs are in <revision-range>. (I think, correct me if I'm wrong)\n\nThat's why I suggest to take it one step further:\n\n     git replay revert --ref=<branch> <revision-range>\n     git replay pick --ref=<branch> <revision-range>\n     git replay rebase --onto=<branch> <revision-range>\n\nThat's why the first two use --ref instead of --onto.\n\nAs a benefit, this also enables me to address another issue I have:\ngit-replay(1) cannot be used on bare commit IDs. This issue was also\nraised by Yee Cheng Chin[1].\n\nWith options `--onto` and `--ref` we can fix this. Because you can use\nthem together:\n\n    git replay replay --ref=ref/heads/branch --onto=112233 aabbcc..ddeeff\n    git replay pick --ref=ref/heads/branch --onto=112233 aabbcc..ddeeff\n    git replay rebase --ref=ref/heads/branch --onto=112233 aabbcc..ddeeff\n\nThe value of --onto doesn't need to be a ref, but --ref needs. For the\n'rebase' subcommand option --ref is optional, for the other two --onto\nis optional. And when one of both is omitted, one defaults to the other.\n\nWhat do you think?\n\n[1]: https://lore.kernel.org/git/CAHTeOx-SMLh_idKhGczPKzZNOKy04uYXmUhL8Z79yRuNpmE4eA@mail.gmail.com/\n\n-- \nCheers,\nToon\n"},{"id":"537154","messageId":"xmqqcy1s8p81.fsf@gitster.g","threadId":"64529","inReplyTo":"87wm07e4ck.fsf@iotcl.com","subject":"Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T21:53:18Z","receivedAt":"2026-02-25T21:53:21Z","isPatch":true,"body":"Toon Claes <toon@iotcl.com> writes:\n\n>> -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n>> -\t\t\t   /*\n>> -\t\t\t    * We don't touch pre-existing repeated reverts, because\n>> -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n>> -\t\t\t    * thus requiring excessive complexity to deal with.\n>> -\t\t\t    */\n>> -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n>> -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n>> +\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>  \t\t} else {\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n>> -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n>> +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject, NULL);\n>>  \t\t}\n>> -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>  \t\trefer_to_commit(opts, &ctx->message, commit);\n>\n> I still find it somewhat confusing we have some the code that deals with\n> `opts->commit_use_reference` partly in here and partly in\n> sequencer_format_revert_header().\n\nTrue.  Making sure plumbing commands are unaffected by random\nend-user configuration is a good thing, but I am not sure if this\ncommand is truly a plumbing.\n\n> Part of the confusion comes from sequencer_format_revert_header() being\n> called with NULL for the commit OID.\n>\n> Was is not possible to incorporate Patrick's suggestion[1]?\n>\n> [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/\n"},{"id":"537192","messageId":"c2048ddf-ced4-425d-af6e-14e9442e9d99@gmail.com","threadId":"64529","inReplyTo":"20260218234215.89326-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-02-26T14:27:54Z","receivedAt":"2026-02-26T14:27:59Z","isPatch":true,"body":"Hi Siddharth\n\nOn 18/02/2026 23:42, Siddharth Asthana wrote:\n> The logic for formatting revert commit messages (handling \"Revert\" and\n> \"Reapply\" cases) is currently duplicated between sequencer.c and will be\n> needed by builtin/replay.c.\n> \n> Extract this logic into a new sequencer_format_revert_header() function\n> that can be shared. The function handles both regular reverts (\"Revert\n> \"<subject>\"\") and revert-of-revert cases (\"Reapply \"<subject>\"\").\n> When an oid is provided, the function appends the full commit hash and\n> period; otherwise the caller should append the commit reference.\n> \n> Update do_pick_commit() to use the new helper, eliminating code\n> duplication while preserving the special handling for commit_use_reference.\n\nI agree with the other comments that this ends up being a bit awkward, I think\nsomething like the diff below which moves all of the revert message formatting\ninto a helper function would be a better approach. Note that I've also added a\nrepository argument to refer_to_commit(). You might want to do that in a\nseparate commit, but I think it is worth doing if we're adding more callers.\nI've also just used a bool for the use_commit_reference flag, if we want to add\nmore flags in the future we can convert it to an unsigned int when we do that.\n\nThanks\n\nPhillip\n\n\n---- 8< ----\ndiff --git a/sequencer.c b/sequencer.c\nindex a3eb39bb252..30f6da6f959 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2198,21 +2198,55 @@ static int should_edit(struct replay_opts *opts) {\n  \treturn opts->edit;\n  }\n  \n-static void refer_to_commit(struct replay_opts *opts,\n-\t\t\t    struct strbuf *msgbuf, struct commit *commit)\n+static void refer_to_commit(struct repository*r, struct strbuf *msgbuf,\n+\t\tconst struct commit *commit, bool use_commit_reference)\n  {\n-\tif (opts->commit_use_reference) {\n+\tif (use_commit_reference) {\n  \t\tstruct pretty_print_context ctx = {\n  \t\t\t.abbrev = DEFAULT_ABBREV,\n  \t\t\t.date_mode.type = DATE_SHORT,\n  \t\t};\n-\t\trepo_format_commit_message(the_repository, commit,\n+\t\trepo_format_commit_message(r, commit,\n  \t\t\t\t\t   \"%h (%s, %ad)\", msgbuf, &ctx);\n  \t} else {\n  \t\tstrbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));\n  \t}\n  }\n  \n+void sequencer_format_revert_message(struct repository *r, const char *subject,\n+\t\t\tconst struct commit *commit, const struct commit *parent,\n+\t\t\tbool use_commit_reference, struct strbuf *message)\n+{\n+\tconst char *orig_subject;\n+\n+\tif (use_commit_reference) {\n+\t\t\tstrbuf_commented_addf(message, comment_line_str,\n+\t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n+\t\t} else if (skip_prefix(subject, \"Revert \\\"\", &orig_subject) &&\n+\t\t\t   /*\n+\t\t\t    * We don't touch pre-existing repeated reverts, because\n+\t\t\t    * theoretically these can be nested arbitrarily deeply,\n+\t\t\t    * thus requiring excessive complexity to deal with.\n+\t\t\t    */\n+\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n+\t\t\tstrbuf_addstr(message, \"Reapply \\\"\");\n+\t\t\tstrbuf_addstr(message, orig_subject);\n+\t\t\tstrbuf_addstr(message, \"\\n\");\n+\t\t} else {\n+\t\t\tstrbuf_addstr(message, \"Revert \\\"\");\n+\t\t\tstrbuf_addstr(message, subject);\n+\t\t\tstrbuf_addstr(message, \"\\\"\\n\");\n+\t\t}\n+\t\tstrbuf_addstr(message, \"\\nThis reverts commit \");\n+\t\trefer_to_commit(r, message, commit, use_commit_reference);\n+\n+\t\tif (commit->parents && commit->parents->next) {\n+\t\t\tstrbuf_addstr(message, \", reversing\\nchanges made to \");\n+\t\t\trefer_to_commit(r, message, parent, use_commit_reference);\n+\t\t}\n+\t\tstrbuf_addstr(message, \".\\n\");\n+}\n+\n  static const char *sequencer_reflog_action(struct replay_opts *opts)\n  {\n  \tif (!opts->reflog_action) {\n@@ -2356,38 +2390,13 @@ static int do_pick_commit(struct repository *r,\n  \t */\n  \n  \tif (command == TODO_REVERT) {\n-\t\tconst char *orig_subject;\n-\n  \t\tbase = commit;\n  \t\tbase_label = msg.label;\n  \t\tnext = parent;\n  \t\tnext_label = msg.parent_label;\n-\t\tif (opts->commit_use_reference) {\n-\t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n-\t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n-\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n-\t\t\t   /*\n-\t\t\t    * We don't touch pre-existing repeated reverts, because\n-\t\t\t    * theoretically these can be nested arbitrarily deeply,\n-\t\t\t    * thus requiring excessive complexity to deal with.\n-\t\t\t    */\n-\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n-\t\t} else {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n-\t\trefer_to_commit(opts, &ctx->message, commit);\n-\n-\t\tif (commit->parents && commit->parents->next) {\n-\t\t\tstrbuf_addstr(&ctx->message, \", reversing\\nchanges made to \");\n-\t\t\trefer_to_commit(opts, &ctx->message, parent);\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \".\\n\");\n+\t\tsequencer_format_revert_message(r,msg.subject, commit, parent,\n+\t\t\t\t\t\topts->commit_use_reference,\n+\t\t\t\t\t\t&ctx->message);\n  \t} else {\n  \t\tconst char *p;\n  \ndiff --git a/sequencer.h b/sequencer.h\nindex 719684c8a9f..a61ec6d81d4 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -271,4 +271,8 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n   */\n  int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n  \n+void sequencer_format_revert_message(struct repository *r, const char *subject,\n+\t\t\t\t     const struct commit *commit, const struct commit *parent,\n+\t\t\t\t     bool use_commit_reference, struct strbuf *message);\n+\n  #endif /* SEQUENCER_H */\n\n"},{"id":"537194","messageId":"405b0d34-c2ad-498d-93a1-2e7925ae11f1@gmail.com","threadId":"64529","inReplyTo":"20260218234215.89326-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-02-26T14:45:50Z","receivedAt":"2026-02-26T14:45:54Z","isPatch":true,"body":"Hi Siddharth\n\nOn 18/02/2026 23:42, Siddharth Asthana wrote:\n> @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to\n>   point at the tip of the resulting history. This is different from `--onto`,\n>   which uses the target only as a starting point without updating it.\n>   \n> +--revert <branch>::\n> +\tStarting point at which to create the reverted commits; must be a\n> +\tbranch name.\n> ++\n> +When `--revert` is specified, the commits in the revision range are reverted\n> +(their changes are undone) and the reverted commits are created on top of\n> +<branch>. The <branch> is then updated to point at the new commits. This is\n> +the same as running `git revert <revision-range>` but does not update the\n> +working tree.\n> ++\n> +The commit messages follow `git revert` conventions: they are prefixed with\n> +\"Revert\" and include \"This reverts commit <hash>.\" When reverting a commit\n> +whose message starts with \"Revert\", the new message uses \"Reapply\" instead.\n> +Unlike cherry-pick which preserves the original author, revert commits use\n> +the current user as the author, matching the behavior of `git revert`.\n> ++\n> +This option is mutually exclusive with `--onto` and `--advance`. It is also\n> +incompatible with `--contained` (which is a modifier for `--onto` only).\n\nWe seem to have lost\n\n     NOTE: For reverting an entire merge request as a single commit\n     (rather than commit-by-commit), consider using `git merge-tree\n     --merge-base $TIP HEAD $BASE` which can avoid unnecessary merge\n     conflicts.\n\nfrom V2 which is a shame.\n\nI do think we should seriously consider reverting commits in the reverse \norder that they were created (i.e. do not set '--reverse' when setting \nup the rev-list options) to reduce the likely-hood of conflicts when \nreverting a sequence of commits.\n\n> @@ -152,6 +172,15 @@ all commits they have since `base`, playing them on top of\n>   `origin/main`. These three branches may have commits on top of `base`\n>   that they have in common, but that does not need to be the case.\n>   \n> +To revert commits on a branch:\n> +\n> +------------\n> +$ git replay --revert main main~2..main\n\nIt might be more realistic to revert some commits from a different \nbranch, for example\n\n   git replay --revert main topic~2..topic\n\n> +static void set_up_branch_mode(struct repository *repo,\n> +\t\t\t       char **branch_name,\n> +\t\t\t       const char *option_name,\n> +\t\t\t       struct ref_info *rinfo,\n> +\t\t\t       struct commit **onto)\n> [...]\n> +\tif (rinfo->positive_refexprs > 1)\n> +\t\tdie(_(\"cannot %s target with multiple sources because ordering would be ill-defined\"),\n> +\t\t    option_name + 2); /* skip \"--\" prefix */\n\nThis is a bit of a nasty hack as it stuffs an English word into the \nmiddle of a translated sentence. Using the option name as below might be \nnicer\n\n     die(_(\"'%s' cannot be used with multiple revision ranges because \nthe ordering would be ill defined\", option_name);\n> @@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>  [...]\n> -\t/* Drop commits that become empty */\n> -\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n> +\t/* Drop commits that become empty (only for picks) */\n\nWhy? What's the advantage in creating empty revert commits?\n\n> +\tif (mode == REPLAY_MODE_PICK &&\n> +\t    oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n\nThanks\n\nPhillip\n"},{"id":"538050","messageId":"71de4ade-62fd-4e66-b225-d87d3d5b97fe@gmail.com","threadId":"64529","inReplyTo":"87wm07e4ck.fsf@iotcl.com","subject":"Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-06T04:31:03Z","receivedAt":"2026-03-06T04:31:17Z","isPatch":true,"body":"\n\nOn 20/02/26 22:31, Toon Claes wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n> \n>> The logic for formatting revert commit messages (handling \"Revert\" and\n>> \"Reapply\" cases) is currently duplicated between sequencer.c and will be\n>> needed by builtin/replay.c.\n>>\n>> Extract this logic into a new sequencer_format_revert_header() function\n>> that can be shared. The function handles both regular reverts (\"Revert\n>> \"<subject>\"\") and revert-of-revert cases (\"Reapply \"<subject>\"\").\n>> When an oid is provided, the function appends the full commit hash and\n>> period; otherwise the caller should append the commit reference.\n>>\n>> Update do_pick_commit() to use the new helper, eliminating code\n>> duplication while preserving the special handling for commit_use_reference.\n>>\n>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n>> ---\n>>   sequencer.c | 47 +++++++++++++++++++++++++++++++----------------\n>>   sequencer.h | 11 +++++++++++\n>>   2 files changed, 42 insertions(+), 16 deletions(-)\n>>\n>> diff --git a/sequencer.c b/sequencer.c\n>> index 1f492f8460..b32347c853 100644\n>> --- a/sequencer.c\n>> +++ b/sequencer.c\n>> @@ -2356,8 +2356,6 @@ static int do_pick_commit(struct repository *r,\n>>   \t */\n>>   \n>>   \tif (command == TODO_REVERT) {\n>> -\t\tconst char *orig_subject;\n>> -\n>>   \t\tbase = commit;\n>>   \t\tbase_label = msg.label;\n>>   \t\tnext = parent;\n>> @@ -2365,22 +2363,10 @@ static int do_pick_commit(struct repository *r,\n>>   \t\tif (opts->commit_use_reference) {\n>>   \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>>   \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n>> -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n>> -\t\t\t   /*\n>> -\t\t\t    * We don't touch pre-existing repeated reverts, because\n>> -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n>> -\t\t\t    * thus requiring excessive complexity to deal with.\n>> -\t\t\t    */\n>> -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n>> -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n>> +\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>   \t\t} else {\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n>> -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n>> +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject, NULL);\n>>   \t\t}\n>> -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>   \t\trefer_to_commit(opts, &ctx->message, commit);\n> \n> I still find it somewhat confusing we have some the code that deals with\n> `opts->commit_use_reference` partly in here and partly in\n> sequencer_format_revert_header().\n> \n> Part of the confusion comes from sequencer_format_revert_header() being\n> called with NULL for the commit OID.\n> \n> Was is not possible to incorporate Patrick's suggestion[1]?\n\nYou're right, the split is awkward. I tried to keep \nsequencer_format_revert_header() minimal so it didn't pull in \nreplay_opts or refer_to_commit(), but the NULL oid path is confusing.\n\nPhillip posted a cleaner approach in his reply to this patch -- he moves \neverything (title, body, refer_to_commit, merge-parent handling) into \none sequencer_format_revert_message() with a bool use_commit_reference. \nThat eliminates the NULL oid entirely and addresses Patrick's suggestion \nat the same time. I will go with that for v4.\n\n> \n> [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/\n> \n\n"},{"id":"538053","messageId":"272a8ce4-08a7-490d-901a-ca8b5e72eb41@gmail.com","threadId":"64529","inReplyTo":"xmqqcy1s8p81.fsf@gitster.g","subject":"Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-06T04:55:06Z","receivedAt":"2026-03-06T04:55:13Z","isPatch":true,"body":"\n\nOn 26/02/26 03:23, Junio C Hamano wrote:\n> Toon Claes <toon@iotcl.com> writes:\n> \n>>> -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n>>> -\t\t\t   /*\n>>> -\t\t\t    * We don't touch pre-existing repeated reverts, because\n>>> -\t\t\t    * theoretically these can be nested arbitrarily deeply,\n>>> -\t\t\t    * thus requiring excessive complexity to deal with.\n>>> -\t\t\t    */\n>>> -\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n>>> -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n>>> -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n>>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n>>> +\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>>   \t\t} else {\n>>> -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n>>> -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n>>> -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n>>> +\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject, NULL);\n>>>   \t\t}\n>>> -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>>>   \t\trefer_to_commit(opts, &ctx->message, commit);\n>>\n>> I still find it somewhat confusing we have some the code that deals with\n>> `opts->commit_use_reference` partly in here and partly in\n>> sequencer_format_revert_header().\n> \n> True.  Making sure plumbing commands are unaffected by random\n> end-user configuration is a good thing, but I am not sure if this\n> command is truly a plumbing.\n\n\nWith Phillip's sequencer_format_revert_message() approach, replay just \npasses use_commit_reference=false and gets the full OID path. The split \nlogic goes away, so this concern is resolved regardless of how we \nclassify replay.\n\n\n> \n>> Part of the confusion comes from sequencer_format_revert_header() being\n>> called with NULL for the commit OID.\n>>\n>> Was is not possible to incorporate Patrick's suggestion[1]?\n>>\n>> [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/\n\n"},{"id":"538054","messageId":"bb2a9b1f-5cdd-4c4e-91dc-a631beb009bb@gmail.com","threadId":"64529","inReplyTo":"c2048ddf-ced4-425d-af6e-14e9442e9d99@gmail.com","subject":"Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-06T05:00:54Z","receivedAt":"2026-03-06T05:01:02Z","isPatch":true,"body":"\n\nOn 26/02/26 19:57, Phillip Wood wrote:\n> Hi Siddharth\n> \n> On 18/02/2026 23:42, Siddharth Asthana wrote:\n>> The logic for formatting revert commit messages (handling \"Revert\" and\n>> \"Reapply\" cases) is currently duplicated between sequencer.c and will be\n>> needed by builtin/replay.c.\n>>\n>> Extract this logic into a new sequencer_format_revert_header() function\n>> that can be shared. The function handles both regular reverts (\"Revert\n>> \"<subject>\"\") and revert-of-revert cases (\"Reapply \"<subject>\"\").\n>> When an oid is provided, the function appends the full commit hash and\n>> period; otherwise the caller should append the commit reference.\n>>\n>> Update do_pick_commit() to use the new helper, eliminating code\n>> duplication while preserving the special handling for \n>> commit_use_reference.\n> \n> I agree with the other comments that this ends up being a bit awkward, I \n> think\n> something like the diff below which moves all of the revert message \n> formatting\n> into a helper function would be a better approach. Note that I've also \n> added a\n> repository argument to refer_to_commit(). You might want to do that in a\n> separate commit, but I think it is worth doing if we're adding more \n> callers.\n> I've also just used a bool for the use_commit_reference flag, if we want \n> to add\n> more flags in the future we can convert it to an unsigned int when we do \n> that.\n\nThanks, this is much cleaner. Moving refer_to_commit() and the \nmerge-parent handling into the same function gets rid of the awkward \nNULL oid path that Toon and Junio pointed out.\n\nI will split the refer_to_commit() signature change (adding struct \nrepository *r) into a preparatory commit as you suggested, then have the \nsecond commit introduce the full sequencer_format_revert_message() helper.\n\nFor replay, I will call it with use_commit_reference=false -- that gives \nthe full OID through refer_to_commit() directly, no special causing needed.\n\n\n> \n> Thanks\n> \n> Phillip\n> \n> \n> ---- 8< ----\n> diff --git a/sequencer.c b/sequencer.c\n> index a3eb39bb252..30f6da6f959 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -2198,21 +2198,55 @@ static int should_edit(struct replay_opts *opts) {\n>       return opts->edit;\n>   }\n> \n> -static void refer_to_commit(struct replay_opts *opts,\n> -                struct strbuf *msgbuf, struct commit *commit)\n> +static void refer_to_commit(struct repository*r, struct strbuf *msgbuf,\n> +        const struct commit *commit, bool use_commit_reference)\n>   {\n> -    if (opts->commit_use_reference) {\n> +    if (use_commit_reference) {\n>           struct pretty_print_context ctx = {\n>               .abbrev = DEFAULT_ABBREV,\n>               .date_mode.type = DATE_SHORT,\n>           };\n> -        repo_format_commit_message(the_repository, commit,\n> +        repo_format_commit_message(r, commit,\n>                          \"%h (%s, %ad)\", msgbuf, &ctx);\n>       } else {\n>           strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));\n>       }\n>   }\n> \n> +void sequencer_format_revert_message(struct repository *r, const char \n> *subject,\n> +            const struct commit *commit, const struct commit *parent,\n> +            bool use_commit_reference, struct strbuf *message)\n> +{\n> +    const char *orig_subject;\n> +\n> +    if (use_commit_reference) {\n> +            strbuf_commented_addf(message, comment_line_str,\n> +                \"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n> +        } else if (skip_prefix(subject, \"Revert \\\"\", &orig_subject) &&\n> +               /*\n> +                * We don't touch pre-existing repeated reverts, because\n> +                * theoretically these can be nested arbitrarily deeply,\n> +                * thus requiring excessive complexity to deal with.\n> +                */\n> +               !starts_with(orig_subject, \"Revert \\\"\")) {\n> +            strbuf_addstr(message, \"Reapply \\\"\");\n> +            strbuf_addstr(message, orig_subject);\n> +            strbuf_addstr(message, \"\\n\");\n> +        } else {\n> +            strbuf_addstr(message, \"Revert \\\"\");\n> +            strbuf_addstr(message, subject);\n> +            strbuf_addstr(message, \"\\\"\\n\");\n> +        }\n> +        strbuf_addstr(message, \"\\nThis reverts commit \");\n> +        refer_to_commit(r, message, commit, use_commit_reference);\n> +\n> +        if (commit->parents && commit->parents->next) {\n> +            strbuf_addstr(message, \", reversing\\nchanges made to \");\n> +            refer_to_commit(r, message, parent, use_commit_reference);\n> +        }\n> +        strbuf_addstr(message, \".\\n\");\n> +}\n> +\n>   static const char *sequencer_reflog_action(struct replay_opts *opts)\n>   {\n>       if (!opts->reflog_action) {\n> @@ -2356,38 +2390,13 @@ static int do_pick_commit(struct repository *r,\n>        */\n> \n>       if (command == TODO_REVERT) {\n> -        const char *orig_subject;\n> -\n>           base = commit;\n>           base_label = msg.label;\n>           next = parent;\n>           next_label = msg.parent_label;\n> -        if (opts->commit_use_reference) {\n> -            strbuf_commented_addf(&ctx->message, comment_line_str,\n> -                \"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n> -        } else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n> -               /*\n> -                * We don't touch pre-existing repeated reverts, because\n> -                * theoretically these can be nested arbitrarily deeply,\n> -                * thus requiring excessive complexity to deal with.\n> -                */\n> -               !starts_with(orig_subject, \"Revert \\\"\")) {\n> -            strbuf_addstr(&ctx->message, \"Reapply \\\"\");\n> -            strbuf_addstr(&ctx->message, orig_subject);\n> -            strbuf_addstr(&ctx->message, \"\\n\");\n> -        } else {\n> -            strbuf_addstr(&ctx->message, \"Revert \\\"\");\n> -            strbuf_addstr(&ctx->message, msg.subject);\n> -            strbuf_addstr(&ctx->message, \"\\\"\\n\");\n> -        }\n> -        strbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n> -        refer_to_commit(opts, &ctx->message, commit);\n> -\n> -        if (commit->parents && commit->parents->next) {\n> -            strbuf_addstr(&ctx->message, \", reversing\\nchanges made to \");\n> -            refer_to_commit(opts, &ctx->message, parent);\n> -        }\n> -        strbuf_addstr(&ctx->message, \".\\n\");\n> +        sequencer_format_revert_message(r,msg.subject, commit, parent,\n> +                        opts->commit_use_reference,\n> +                        &ctx->message);\n>       } else {\n>           const char *p;\n> \n> diff --git a/sequencer.h b/sequencer.h\n> index 719684c8a9f..a61ec6d81d4 100644\n> --- a/sequencer.h\n> +++ b/sequencer.h\n> @@ -271,4 +271,8 @@ int sequencer_determine_whence(struct repository *r, \n> enum commit_whence *whence)\n>    */\n>   int sequencer_get_update_refs_state(const char *wt_dir, struct \n> string_list *refs);\n> \n> +void sequencer_format_revert_message(struct repository *r, const char \n> *subject,\n> +                     const struct commit *commit, const struct commit \n> *parent,\n> +                     bool use_commit_reference, struct strbuf *message);\n> +\n>   #endif /* SEQUENCER_H */\n> \n\n"},{"id":"538055","messageId":"31384cd8-6739-40dd-a963-ce1597921969@gmail.com","threadId":"64529","inReplyTo":"87tsvbe2sm.fsf@iotcl.com","subject":"Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-06T05:05:21Z","receivedAt":"2026-03-06T05:05:29Z","isPatch":true,"body":"\n\nOn 20/02/26 23:05, Toon Claes wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n> \n>> Add a `--revert <branch>` mode to git replay that undoes the changes\n>> introduced by the specified commits. Like --onto and --advance, --revert\n>> is a standalone mode: it takes a branch argument and updates that branch\n>> with the newly created revert commits.\n>>\n>> At GitLab, we need this in Gitaly for reverting commits directly on bare\n>> repositories without requiring a working tree checkout.\n>>\n>> The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick\n>> and revert are just the same three-way merge with swapped arguments:\n>>\n>>    - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n>>    - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n>>\n>> We swap the base and pickme trees passed to merge_incore_nonrecursive()\n>> to reverse the diff direction.\n>>\n>> Revert commit messages follow the usual git revert conventions: prefixed\n>> with \"Revert\" (or \"Reapply\" when reverting a revert), and including\n>> \"This reverts commit <hash>.\". The author is set to the current user\n>> rather than preserving the original author, matching git revert behavior.\n>>\n>> Helped-by: Christian Couder <christian.couder@gmail.com>\n>> Helped-by: Patrick Steinhardt <ps@pks.im>\n>> Helped-by: Elijah Newren <newren@gmail.com>\n>> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n>> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\n>> Helped-by: Junio C Hamano <gitster@pobox.com>\n>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n>> ---\n>>   Documentation/git-replay.adoc |  37 +++++++-\n>>   builtin/replay.c              |  25 ++++--\n>>   replay.c                      | 162 ++++++++++++++++++++++++----------\n>>   replay.h                      |  11 ++-\n>>   t/t3650-replay-basics.sh      | 107 ++++++++++++++++++++--\n>>   5 files changed, 277 insertions(+), 65 deletions(-)\n>>\n>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n>> index 8d696ce3ab..ffdf790278 100644\n>> --- a/Documentation/git-replay.adoc\n>> +++ b/Documentation/git-replay.adoc\n>> @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>>   SYNOPSIS\n>>   --------\n>>   [verse]\n>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n> \n> The modes `--onto`, `--advance` and `--revert` seem to be extremely\n> different from each other. So I'm starting to wonder whether it won't\n> make more sense to instead create subcommands instead of options for\n> these. Maybe something like:\n\n\nAgree the interface could be cleaner as subcommands. I think Christian's \nsuggestion to do this separate series after --revert lands make sense -- \nwe would get real-world usage feedback first, and it avoids scope creep \nhere.\n\n\n> \n>      git replay revert --base=<branch> <revision-range>\n>      git replay pick --base=<branch> <revision-range>\n>      git replay replay --base=<branch> <revision-range>\n> \n> \n\n"},{"id":"538060","messageId":"77fa95d9-3ea3-4a32-b8fa-22c05c048160@gmail.com","threadId":"64529","inReplyTo":"405b0d34-c2ad-498d-93a1-2e7925ae11f1@gmail.com","subject":"Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-06T05:28:29Z","receivedAt":"2026-03-06T05:28:36Z","isPatch":true,"body":"\n\nOn 26/02/26 20:15, Phillip Wood wrote:\n> Hi Siddharth\n> \n> On 18/02/2026 23:42, Siddharth Asthana wrote:\n>> @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and \n>> <branch> is updated to\n>>   point at the tip of the resulting history. This is different from \n>> `--onto`,\n>>   which uses the target only as a starting point without updating it.\n>> +--revert <branch>::\n>> +    Starting point at which to create the reverted commits; must be a\n>> +    branch name.\n>> ++\n>> +When `--revert` is specified, the commits in the revision range are \n>> reverted\n>> +(their changes are undone) and the reverted commits are created on \n>> top of\n>> +<branch>. The <branch> is then updated to point at the new commits. \n>> This is\n>> +the same as running `git revert <revision-range>` but does not update \n>> the\n>> +working tree.\n>> ++\n>> +The commit messages follow `git revert` conventions: they are \n>> prefixed with\n>> +\"Revert\" and include \"This reverts commit <hash>.\" When reverting a \n>> commit\n>> +whose message starts with \"Revert\", the new message uses \"Reapply\" \n>> instead.\n>> +Unlike cherry-pick which preserves the original author, revert \n>> commits use\n>> +the current user as the author, matching the behavior of `git revert`.\n>> ++\n>> +This option is mutually exclusive with `--onto` and `--advance`. It \n>> is also\n>> +incompatible with `--contained` (which is a modifier for `--onto` only).\n> \n> We seem to have lost\n> \n>      NOTE: For reverting an entire merge request as a single commit\n>      (rather than commit-by-commit), consider using `git merge-tree\n>      --merge-base $TIP HEAD $BASE` which can avoid unnecessary merge\n>      conflicts.\n> \n> from V2 which is a shame.\n\n\nYeah, I dropped it during the v3 cleanup when I was trimming the example \ntext. will  add it back.\n\n> \n> I do think we should seriously consider reverting commits in the reverse \n> order that they were created (i.e. do not set '--reverse' when setting \n> up the rev-list options) to reduce the likely-hood of conflicts when \n> reverting a sequence of commits.\n\n\nGood catch. sequencer.c does exactly this in prepare_revs() -- it only \nsets reverse for REPLAY_PICK, not REPLAY_REVERT, so git revert processes \nnewest-first.\n\nThe complication in replay is that pick_regular_commit() chains commits \nthrough mapped_commit(base, onto). With oldest-first, the parent is \nalways already in replayed_commits so the chain works. With \nnewest-first, the parent hasn't been processed yet and mapped_commit() \nfalls back to onto -- so each revert be independently based on the \noriginal branch tip instead of chaining.\n\nThe fix is straightforward: for revert mode, pass last_commit instead of \nonto as the fallback in the main loop:\n\n\tpick_regular_commit(repo, commit, replayed_commits,\n                         mode == REPLAY_MODE_REVERT ? last_commit : onto,\n                         &merge_opt, &result, mode);\n\nThat way each revert builds on the previous one regardless of walk \norder. I will do this in v4 together with skipping the reverse=1 \noverride for revert mode.\n\n> \n>> @@ -152,6 +172,15 @@ all commits they have since `base`, playing them \n>> on top of\n>>   `origin/main`. These three branches may have commits on top of `base`\n>>   that they have in common, but that does not need to be the case.\n>> +To revert commits on a branch:\n>> +\n>> +------------\n>> +$ git replay --revert main main~2..main\n> \n> It might be more realistic to revert some commits from a different \n> branch, for example\n> \n>    git replay --revert main topic~2..topic\n\n\nMakes sense. v2 had `git replay --revert main feature~2..feature` for \nthis reason but I simplified it in v3. I will go back to something like \nyour example:\n\n\tgit replay --revert main topic2..topic\n\n\n> \n>> +static void set_up_branch_mode(struct repository *repo,\n>> +                   char **branch_name,\n>> +                   const char *option_name,\n>> +                   struct ref_info *rinfo,\n>> +                   struct commit **onto)\n>> [...]\n>> +    if (rinfo->positive_refexprs > 1)\n>> +        die(_(\"cannot %s target with multiple sources because \n>> ordering would be ill-defined\"),\n>> +            option_name + 2); /* skip \"--\" prefix */\n> \n> This is a bit of a nasty hack as it stuffs an English word into the \n> middle of a translated sentence. Using the option name as below might be \n> nicer\n\n\nAgreed, will use your suggested form:\n\n\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n           \"because the ordering would be ill-defined\"), option_name);\n\n\n> \n>      die(_(\"'%s' cannot be used with multiple revision ranges because \n> the ordering would be ill defined\", option_name);\n>> @@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct \n>> repository *repo,\n>>  [...]\n>> -    /* Drop commits that become empty */\n>> -    if (oideq(&replayed_base_tree->object.oid, &result->tree- \n>> >object.oid) &&\n>> +    /* Drop commits that become empty (only for picks) */\n> \n> Why? What's the advantage in creating empty revert commits?\n\n\nConsistency with git revert, which doesn't silently drop empty reverts \neither -- it stops and asks the user to deal with it. Since replay is \nnon-interactive and can't prompt, I kept them rather than silently \ndropping, to avoid hiding that something unexpected happened.\n\nThat being said, I don't feel strong about it. If you think dropping is \nthe better default for replay, I am  happy to change it. Or we could \nerror out (exit code 1) like we do for conflicts?\n\n\nThanks,\nSiddharth\n\n\n> \n>> +    if (mode == REPLAY_MODE_PICK &&\n>> +        oideq(&replayed_base_tree->object.oid, &result->tree- \n>> >object.oid) &&\n> \n> Thanks\n> \n> Phillip\n\n"},{"id":"538088","messageId":"ed9818d8-fe30-4967-897c-edc83c83c299@gmail.com","threadId":"64529","inReplyTo":"77fa95d9-3ea3-4a32-b8fa-22c05c048160@gmail.com","subject":"Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-03-06T15:52:44Z","receivedAt":"2026-03-06T15:52:56Z","isPatch":true,"body":"On 06/03/2026 05:28, Siddharth Asthana wrote:\n> On 26/02/26 20:15, Phillip Wood wrote:\n>> On 18/02/2026 23:42, Siddharth Asthana wrote:\n> \n>> I do think we should seriously consider reverting commits in the \n>> reverse order that they were created (i.e. do not set '--reverse' when \n>> setting up the rev-list options) to reduce the likely-hood of \n>> conflicts when reverting a sequence of commits.\n> \n> Good catch. sequencer.c does exactly this in prepare_revs() -- it only \n> sets reverse for REPLAY_PICK, not REPLAY_REVERT, so git revert processes \n> newest-first.\n> \n> The complication in replay is that pick_regular_commit() chains commits \n> through mapped_commit(base, onto). With oldest-first, the parent is \n> always already in replayed_commits so the chain works. With newest- \n> first, the parent hasn't been processed yet and mapped_commit() falls \n> back to onto -- so each revert be independently based on the original \n> branch tip instead of chaining.\n> \n> The fix is straightforward: for revert mode, pass last_commit instead of \n> onto as the fallback in the main loop:\n> \n>      pick_regular_commit(repo, commit, replayed_commits,\n>                          mode == REPLAY_MODE_REVERT ? last_commit : onto,\n>                          &merge_opt, &result, mode);\n\nAs we only allow a single range of commits with --revert that should work.\n\n> That way each revert builds on the previous one regardless of walk \n> order. I will do this in v4 together with skipping the reverse=1 \n> override for revert mode.\n\nGreat\n\n>>> @@ -226,25 +269,46 @@ static struct commit \n>>> *pick_regular_commit(struct repository *repo,\n>>>  [...]\n>>> -    /* Drop commits that become empty */\n>>> -    if (oideq(&replayed_base_tree->object.oid, &result->tree- \n>>> >object.oid) &&\n>>> +    /* Drop commits that become empty (only for picks) */\n>>\n>> Why? What's the advantage in creating empty revert commits?\n> \n> \n> Consistency with git revert, which doesn't silently drop empty reverts \n> either -- it stops and asks the user to deal with it. \n\nSo does \"git cherry-pick\" unless you pass --empty=drop or --empty=keep \n(I was surprised that \"git revert\" does not support --empty, that seems \nto be an oversight)\n\n> Since replay is \n> non-interactive and can't prompt, I kept them rather than silently \n> dropping, to avoid hiding that something unexpected happened.\n\nI don't think creating empty commits for revert is very helpful, when \ncherry-picking one could argue that the user may want to preserve the \ncommit message (though I think that's unlikely in practice which is why \nwe drop commits that become empty) but that does not apply to revert.\n\n> That being said, I don't feel strong about it. If you think dropping is \n> the better default for replay, I am  happy to change it. Or we could \n> error out (exit code 1) like we do for conflicts?\n\nWe don't error out when cherry-picking and so we shouldn't do that when \nreverting.\n\nThanks\n\nPhillip\n\n> \n> Thanks,\n> Siddharth\n> \n> \n>>\n>>> +    if (mode == REPLAY_MODE_PICK &&\n>>> +        oideq(&replayed_base_tree->object.oid, &result->tree- \n>>> >object.oid) &&\n>>\n>> Thanks\n>>\n>> Phillip\n> \n\n"},{"id":"538092","messageId":"45bf9ad0-418d-44ee-b06c-d2b546f54b7f@gmail.com","threadId":"64529","inReplyTo":"ed9818d8-fe30-4967-897c-edc83c83c299@gmail.com","subject":"Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-06T16:20:24Z","receivedAt":"2026-03-06T16:20:34Z","isPatch":true,"body":"\n\nOn 06/03/26 21:22, Phillip Wood wrote:\n> On 06/03/2026 05:28, Siddharth Asthana wrote:\n>> On 26/02/26 20:15, Phillip Wood wrote:\n>>> On 18/02/2026 23:42, Siddharth Asthana wrote:\n>>\n>>> I do think we should seriously consider reverting commits in the \n>>> reverse order that they were created (i.e. do not set '--reverse' \n>>> when setting up the rev-list options) to reduce the likely-hood of \n>>> conflicts when reverting a sequence of commits.\n>>\n>> Good catch. sequencer.c does exactly this in prepare_revs() -- it only \n>> sets reverse for REPLAY_PICK, not REPLAY_REVERT, so git revert \n>> processes newest-first.\n>>\n>> The complication in replay is that pick_regular_commit() chains \n>> commits through mapped_commit(base, onto). With oldest-first, the \n>> parent is always already in replayed_commits so the chain works. With \n>> newest- first, the parent hasn't been processed yet and \n>> mapped_commit() falls back to onto -- so each revert be independently \n>> based on the original branch tip instead of chaining.\n>>\n>> The fix is straightforward: for revert mode, pass last_commit instead \n>> of onto as the fallback in the main loop:\n>>\n>>      pick_regular_commit(repo, commit, replayed_commits,\n>>                          mode == REPLAY_MODE_REVERT ? last_commit : onto,\n>>                          &merge_opt, &result, mode);\n> \n> As we only allow a single range of commits with --revert that should work.\n> \n>> That way each revert builds on the previous one regardless of walk \n>> order. I will do this in v4 together with skipping the reverse=1 \n>> override for revert mode.\n> \n> Great\n> \n>>>> @@ -226,25 +269,46 @@ static struct commit \n>>>> *pick_regular_commit(struct repository *repo,\n>>>>  [...]\n>>>> -    /* Drop commits that become empty */\n>>>> -    if (oideq(&replayed_base_tree->object.oid, &result->tree- \n>>>> >object.oid) &&\n>>>> +    /* Drop commits that become empty (only for picks) */\n>>>\n>>> Why? What's the advantage in creating empty revert commits?\n>>\n>>\n>> Consistency with git revert, which doesn't silently drop empty reverts \n>> either -- it stops and asks the user to deal with it. \n> \n> So does \"git cherry-pick\" unless you pass --empty=drop or --empty=keep \n> (I was surprised that \"git revert\" does not support --empty, that seems \n> to be an oversight)\n> \n>> Since replay is non-interactive and can't prompt, I kept them rather \n>> than silently dropping, to avoid hiding that something unexpected \n>> happened.\n> \n> I don't think creating empty commits for revert is very helpful, when \n> cherry-picking one could argue that the user may want to preserve the \n> commit message (though I think that's unlikely in practice which is why \n> we drop commits that become empty) but that does not apply to revert.\n> \n>> That being said, I don't feel strong about it. If you think dropping \n>> is the better default for replay, I am  happy to change it. Or we \n>> could error out (exit code 1) like we do for conflicts?\n> \n> We don't error out when cherry-picking and so we shouldn't do that when \n> reverting.\n\n\nRight, will drop empties for revert too then.\n\n\n> \n> Thanks\n> \n> Phillip\n> \n>>\n>> Thanks,\n>> Siddharth\n>>\n>>\n>>>\n>>>> +    if (mode == REPLAY_MODE_PICK &&\n>>>> +        oideq(&replayed_base_tree->object.oid, &result->tree- \n>>>> >object.oid) &&\n>>>\n>>> Thanks\n>>>\n>>> Phillip \n\n"},{"id":"538845","messageId":"20260313054035.26605-1-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260218234215.89326-1-siddharthasthana31@gmail.com","subject":"[PATCH v4 0/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-13T05:40:33Z","receivedAt":"2026-03-13T05:40:41Z","isPatch":true,"body":"Hi,\n\ngit replay currently supports cherry-picking (--advance) and rebasing\n(--onto), but not reverting. We need this at GitLab for Gitaly to\nreverse commits directly on bare repositories without a checkout.\n\nThe approach is the same as sequencer.c -- cherry-pick and revert are\njust the same three-way merge with swapped arguments. We swap the base\nand pickme trees passed to merge_incore_nonrecursive() to reverse the\ndiff direction.\n\nPatch 1 extracts the full revert message formatting logic into a new\nsequencer_format_revert_message() function, following Phillip's\nsuggestion to move everything into one shared function rather than\njust the header. refer_to_commit() is updated to take a struct\nrepository and a bool instead of replay_opts so it works outside the\nsequencer.\n\nPatch 2 adds --revert <branch> as a standalone mode. Reverts are\nprocessed newest-first (matching git revert) to reduce conflicts by\npeeling off changes from the top.\n\nThe series is based on top of d181b9354c (The 13th batch, 2026-03-07).\n\nCI: https://gitlab.com/gitlab-org/git/-/pipelines/2329880894\nThe msvc-meson / Chocolatey failures are pre-existing infrastructure\nissues unrelated to this series.\n\nChanges in v4:\n- Replaced sequencer_format_revert_header() with a more complete\n  sequencer_format_revert_message() that handles everything: subject\n  prefix, commit reference via refer_to_commit(), and merge-parent\n  references -- per Phillip\n- Updated refer_to_commit() signature to take (struct repository *r,\n  bool use_commit_reference) instead of (struct replay_opts *opts)\n- Reverts are now newest-first (revs.reverse = 0 for --revert),\n  chaining on last_commit rather than the parent mapping\n- Changed doc example to cross-branch scenario and restored the\n  merge-tree NOTE\n- Updated error message format to \"'--revert' cannot be used with\n  multiple revision ranges...\" (and same for --advance)\n- Empty revert commits are now dropped, consistent with cherry-pick\n- Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/\n- Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/\n- Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/\n\nThanks,\nSiddharth\n\n---\nSiddharth Asthana (2):\n  sequencer: extract revert message formatting into shared function\n  replay: add --revert mode to reverse commit changes\n\n Documentation/git-replay.adoc |  43 ++++++++-\n builtin/replay.c              |  46 ++++++----\n replay.c                      | 165 ++++++++++++++++++++++++----------\n replay.h                      |  11 ++-\n sequencer.c                   |  78 +++++++++-------\n sequencer.h                   |  14 +++\n t/t3650-replay-basics.sh      | 114 +++++++++++++++++++++--\n 7 files changed, 364 insertions(+), 107 deletions(-)\n\nRange-diff versus v3:\n\n1:  9d686bcdfe ! 1:  bdc710b265 sequencer: extract revert message formatting into shared function\n    @@ Commit message\n         sequencer: extract revert message formatting into shared function\n     \n         The logic for formatting revert commit messages (handling \"Revert\" and\n    -    \"Reapply\" cases) is currently duplicated between sequencer.c and will be\n    -    needed by builtin/replay.c.\n    +    \"Reapply\" cases, appending \"This reverts commit <ref>.\", and handling\n    +    merge-parent references) currently lives inline in do_pick_commit().\n    +    The upcoming replay --revert mode needs to reuse this logic.\n     \n    -    Extract this logic into a new sequencer_format_revert_header() function\n    -    that can be shared. The function handles both regular reverts (\"Revert\n    -    \"<subject>\"\") and revert-of-revert cases (\"Reapply \"<subject>\"\").\n    -    When an oid is provided, the function appends the full commit hash and\n    -    period; otherwise the caller should append the commit reference.\n    +    Extract all of this into a new sequencer_format_revert_message()\n    +    function. The function takes a repository, the subject line, commit,\n    +    parent, a use_commit_reference flag, and the output strbuf. It handles\n    +    both regular reverts (\"Revert \"<subject>\"\") and revert-of-revert cases\n    +    (\"Reapply \"<subject>\"\"), and uses refer_to_commit() internally to\n    +    format the commit reference.\n     \n    -    Update do_pick_commit() to use the new helper, eliminating code\n    -    duplication while preserving the special handling for commit_use_reference.\n    +    Update refer_to_commit() to take a struct repository parameter instead\n    +    of relying on the_repository, and a bool instead of reading from\n    +    replay_opts directly. This makes it usable from the new shared function\n    +    without pulling in sequencer-specific state.\n     \n         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## sequencer.c ##\n    +@@ sequencer.c: static int should_edit(struct replay_opts *opts) {\n    + \treturn opts->edit;\n    + }\n    + \n    +-static void refer_to_commit(struct replay_opts *opts,\n    +-\t\t\t    struct strbuf *msgbuf, struct commit *commit)\n    ++static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,\n    ++\t\t\t    const struct commit *commit,\n    ++\t\t\t    bool use_commit_reference)\n    + {\n    +-\tif (opts->commit_use_reference) {\n    ++\tif (use_commit_reference) {\n    + \t\tstruct pretty_print_context ctx = {\n    + \t\t\t.abbrev = DEFAULT_ABBREV,\n    + \t\t\t.date_mode.type = DATE_SHORT,\n    + \t\t};\n    +-\t\trepo_format_commit_message(the_repository, commit,\n    ++\t\trepo_format_commit_message(r, commit,\n    + \t\t\t\t\t   \"%h (%s, %ad)\", msgbuf, &ctx);\n    + \t} else {\n    + \t\tstrbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));\n     @@ sequencer.c: static int do_pick_commit(struct repository *r,\n      \t */\n      \n    @@ sequencer.c: static int do_pick_commit(struct repository *r,\n      \t\tbase = commit;\n      \t\tbase_label = msg.label;\n      \t\tnext = parent;\n    -@@ sequencer.c: static int do_pick_commit(struct repository *r,\n    - \t\tif (opts->commit_use_reference) {\n    - \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n    - \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n    + \t\tnext_label = msg.parent_label;\n    +-\t\tif (opts->commit_use_reference) {\n    +-\t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n    +-\t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n     -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n     -\t\t\t   /*\n     -\t\t\t    * We don't touch pre-existing repeated reverts, because\n    @@ sequencer.c: static int do_pick_commit(struct repository *r,\n     -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n     -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n     -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n    -+\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n    - \t\t} else {\n    +-\t\t} else {\n     -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n     -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n     -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n    -+\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject, NULL);\n    - \t\t}\n    +-\t\t}\n     -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n    - \t\trefer_to_commit(opts, &ctx->message, commit);\n    +-\t\trefer_to_commit(opts, &ctx->message, commit);\n    +-\n    +-\t\tif (commit->parents && commit->parents->next) {\n    +-\t\t\tstrbuf_addstr(&ctx->message, \", reversing\\nchanges made to \");\n    +-\t\t\trefer_to_commit(opts, &ctx->message, parent);\n    +-\t\t}\n    +-\t\tstrbuf_addstr(&ctx->message, \".\\n\");\n    ++\t\tsequencer_format_revert_message(r, msg.subject, commit,\n    ++\t\t\t\t\t\tparent,\n    ++\t\t\t\t\t\topts->commit_use_reference,\n    ++\t\t\t\t\t\t&ctx->message);\n    + \t} else {\n    + \t\tconst char *p;\n      \n    - \t\tif (commit->parents && commit->parents->next) {\n     @@ sequencer.c: int sequencer_pick_revisions(struct repository *r,\n      \treturn res;\n      }\n      \n    -+void sequencer_format_revert_header(struct strbuf *out,\n    -+\t\t\t\t    const char *orig_subject,\n    -+\t\t\t\t    const struct object_id *oid)\n    ++void sequencer_format_revert_message(struct repository *r,\n    ++\t\t\t\t     const char *subject,\n    ++\t\t\t\t     const struct commit *commit,\n    ++\t\t\t\t     const struct commit *parent,\n    ++\t\t\t\t     bool use_commit_reference,\n    ++\t\t\t\t     struct strbuf *message)\n     +{\n    -+\tconst char *revert_subject;\n    ++\tconst char *orig_subject;\n     +\n    -+\tif (skip_prefix(orig_subject, \"Revert \\\"\", &revert_subject) &&\n    -+\t    /*\n    -+\t     * We don't touch pre-existing repeated reverts, because\n    -+\t     * theoretically these can be nested arbitrarily deeply,\n    -+\t     * thus requiring excessive complexity to deal with.\n    -+\t     */\n    -+\t    !starts_with(revert_subject, \"Revert \\\"\")) {\n    -+\t\tstrbuf_addstr(out, \"Reapply \\\"\");\n    -+\t\tstrbuf_addstr(out, revert_subject);\n    -+\t\tstrbuf_addch(out, '\\n');\n    ++\tif (use_commit_reference) {\n    ++\t\tstrbuf_commented_addf(message, comment_line_str,\n    ++\t\t\t\t      \"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n    ++\t} else if (skip_prefix(subject, \"Revert \\\"\", &orig_subject) &&\n    ++\t\t   /*\n    ++\t\t    * We don't touch pre-existing repeated reverts, because\n    ++\t\t    * theoretically these can be nested arbitrarily deeply,\n    ++\t\t    * thus requiring excessive complexity to deal with.\n    ++\t\t    */\n    ++\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n    ++\t\tstrbuf_addstr(message, \"Reapply \\\"\");\n    ++\t\tstrbuf_addstr(message, orig_subject);\n    ++\t\tstrbuf_addstr(message, \"\\n\");\n     +\t} else {\n    -+\t\tstrbuf_addstr(out, \"Revert \\\"\");\n    -+\t\tstrbuf_addstr(out, orig_subject);\n    -+\t\tstrbuf_addstr(out, \"\\\"\\n\");\n    ++\t\tstrbuf_addstr(message, \"Revert \\\"\");\n    ++\t\tstrbuf_addstr(message, subject);\n    ++\t\tstrbuf_addstr(message, \"\\\"\\n\");\n     +\t}\n    ++\tstrbuf_addstr(message, \"\\nThis reverts commit \");\n    ++\trefer_to_commit(r, message, commit, use_commit_reference);\n     +\n    -+\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n    -+\tif (oid) {\n    -+\t\tstrbuf_addstr(out, oid_to_hex(oid));\n    -+\t\tstrbuf_addstr(out, \".\\n\");\n    ++\tif (commit->parents && commit->parents->next) {\n    ++\t\tstrbuf_addstr(message, \", reversing\\nchanges made to \");\n    ++\t\trefer_to_commit(r, message, parent, use_commit_reference);\n     +\t}\n    ++\tstrbuf_addstr(message, \".\\n\");\n     +}\n     +\n      void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n    @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh\n      int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n      \n     +/*\n    -+ * Formats a revert commit message following standard Git conventions.\n    -+ * Handles both regular reverts (\"Revert \\\"<subject>\\\"\") and revert of revert\n    -+ * cases (\"Reapply \\\"<subject>\\\"\"). Adds \"This reverts commit <oid>.\" if oid\n    -+ * is provided, otherwise just adds \"This reverts commit \" and the caller\n    -+ * should append the commit reference.\n    ++ * Formats a complete revert commit message following standard Git conventions.\n    ++ * Handles regular reverts (\"Revert \\\"<subject>\\\"\"), revert of revert cases\n    ++ * (\"Reapply \\\"<subject>\\\"\"), and the --reference style. Appends \"This reverts\n    ++ * commit <ref>.\" using either the abbreviated or full commit reference\n    ++ * depending on use_commit_reference. Also handles merge-parent references.\n     + */\n    -+void sequencer_format_revert_header(struct strbuf *out,\n    -+\t\t\t\t    const char *orig_subject,\n    -+\t\t\t\t    const struct object_id *oid);\n    ++void sequencer_format_revert_message(struct repository *r,\n    ++\t\t\t\t     const char *subject,\n    ++\t\t\t\t     const struct commit *commit,\n    ++\t\t\t\t     const struct commit *parent,\n    ++\t\t\t\t     bool use_commit_reference,\n    ++\t\t\t\t     struct strbuf *message);\n     +\n      #endif /* SEQUENCER_H */\n2:  066269706e ! 2:  bea6229575 replay: add --revert mode to reverse commit changes\n    @@ Commit message\n         We swap the base and pickme trees passed to merge_incore_nonrecursive()\n         to reverse the diff direction.\n     \n    +    Reverts are processed newest-first (matching git revert behavior) to\n    +    reduce conflicts by peeling off changes from the top. Each revert\n    +    builds on the result of the previous one via the last_commit fallback\n    +    in the main replay loop, rather than relying on the parent-mapping\n    +    used for cherry-pick.\n    +\n         Revert commit messages follow the usual git revert conventions: prefixed\n         with \"Revert\" (or \"Reapply\" when reverting a revert), and including\n         \"This reverts commit <hash>.\". The author is set to the current user\n    @@ Commit message\n         Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n         Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\n         Helped-by: Junio C Hamano <gitster@pobox.com>\n    +    Helped-by: Toon Claes <toon@iotcl.com>\n         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Documentation/git-replay.adoc ##\n    @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them\n     +To revert commits on a branch:\n     +\n     +------------\n    -+$ git replay --revert main main~2..main\n    ++$ git replay --revert main topic~2..topic\n     +------------\n     +\n    -+This reverts the last two commits on `main`, creating two revert commits\n    -+on top of `main`, and updates `main` to point at the result.\n    ++This reverts the last two commits from `topic`, creating revert commits on\n    ++top of `main`, and updates `main` to point at the result. This is useful when\n    ++commits from `topic` were previously merged or cherry-picked into `main` and\n    ++need to be undone.\n    ++\n    ++NOTE: For reverting an entire merge request as a single commit (rather than\n    ++commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n    ++which can avoid unnecessary merge conflicts.\n     +\n      GIT\n      ---\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \n      \t/* Parse ref action mode from command line or config */\n      \tref_mode = get_ref_action_mode(repo, ref_action);\n    +@@ builtin/replay.c: int cmd_replay(int argc,\n    + \t * some options changing these values if we think they could\n    + \t * be useful.\n    + \t */\n    +-\trevs.reverse = 1;\n    ++\t/*\n    ++\t * Cherry-pick/rebase need oldest-first ordering so that each\n    ++\t * replayed commit can build on its already-replayed parent.\n    ++\t * Revert needs newest-first ordering (like git revert) to\n    ++\t * reduce conflicts by peeling off changes from the top.\n    ++\t */\n    ++\trevs.reverse = opts.revert ? 0 : 1;\n    + \trevs.sort_order = REV_SORT_IN_GRAPH_ORDER;\n    + \trevs.topo_order = 1;\n    + \trevs.simplify_history = 0;\n    +@@ builtin/replay.c: int cmd_replay(int argc,\n    + \t * Detect and warn if we override some user specified rev\n    + \t * walking options.\n    + \t */\n    +-\tif (revs.reverse != 1) {\n    +-\t\twarning(_(\"some rev walking options will be overridden as \"\n    +-\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n    +-\t\t\t\"reverse\");\n    +-\t\trevs.reverse = 1;\n    ++\t{\n    ++\t\tint desired_reverse = opts.revert ? 0 : 1;\n    ++\t\tif (revs.reverse != desired_reverse) {\n    ++\t\t\twarning(_(\"some rev walking options will be overridden as \"\n    ++\t\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n    ++\t\t\t\t\"reverse\");\n    ++\t\t\trevs.reverse = desired_reverse;\n    ++\t\t}\n    + \t}\n    + \tif (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {\n    + \t\twarning(_(\"some rev walking options will be overridden as \"\n     @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\tgoto cleanup;\n      \n    @@ replay.c\n      #include \"strmap.h\"\n      #include \"tree.h\"\n      \n    +-/*\n    +- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but\n    +- * do not want to use the_repository.\n    +- */\n    +-#define the_repository DO_NOT_USE_THE_REPOSITORY\n     +enum replay_mode {\n     +\tREPLAY_MODE_PICK,\n     +\tREPLAY_MODE_REVERT,\n     +};\n    -+\n    + \n      static const char *short_commit_name(struct repository *repo,\n      \t\t\t\t     struct commit *commit)\n    - {\n     @@ replay.c: static char *get_author(const char *message)\n      \treturn NULL;\n      }\n    @@ replay.c: static char *get_author(const char *message)\n     +\tsubject_len = find_commit_subject(message, &subject_start);\n     +\tsubject = xmemdupz(subject_start, subject_len);\n     +\n    -+\tsequencer_format_revert_header(msg, subject, &commit->object.oid);\n    ++\tsequencer_format_revert_message(repo, subject, commit,\n    ++\t\t\t\t\tcommit->parents ? commit->parents->item : NULL,\n    ++\t\t\t\t\tfalse, msg);\n     +\n     +\tfree(subject);\n     +\trepo_unuse_commit_buffer(repo, commit, message);\n    @@ replay.c: static void get_ref_information(struct repository *repo,\n     +\t}\n     +\t*onto = peel_committish(repo, *branch_name, option_name);\n     +\tif (rinfo->positive_refexprs > 1)\n    -+\t\tdie(_(\"cannot %s target with multiple sources because ordering would be ill-defined\"),\n    -+\t\t    option_name + 2); /* skip \"--\" prefix */\n    ++\t\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n    ++\t\t      \"because the ordering would be ill-defined\"),\n    ++\t\t    option_name);\n     +}\n     +\n      static void set_up_replay_mode(struct repository *repo,\n    @@ replay.c: static struct commit *pick_regular_commit(struct repository *repo,\n     +\tmerge_opt->branch2 = NULL;\n      \tif (!result->clean)\n      \t\treturn NULL;\n    --\t/* Drop commits that become empty */\n    --\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n    -+\t/* Drop commits that become empty (only for picks) */\n    -+\tif (mode == REPLAY_MODE_PICK &&\n    -+\t    oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n    + \t/* Drop commits that become empty */\n    + \tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n      \t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n      \t\treturn replayed_base;\n     -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n    @@ replay.c: int replay_revisions(struct rev_info *revs,\n      \n      \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n     -\t\t\t\t\t\t  onto, &merge_opt, &result);\n    -+\t\t\t\t\t\t  onto, &merge_opt, &result, mode);\n    ++\t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n    ++\t\t\t\t\t\t  &merge_opt, &result, mode);\n      \t\tif (!last_commit)\n      \t\t\tbreak;\n      \n    @@ t/t3650-replay-basics.sh: test_expect_success 'no base or negative ref gives no-\n      \ttest_must_fail git replay --advance=main --contained \\\n      \t\ttopic1..topic2 2>actual &&\n      \ttest_cmp expect actual\n    + '\n    + \n    + test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n    +-\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\tfatal: '\"'\"'--advance'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n    ++\tEOF\n    + \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n    + \ttest_cmp expect actual\n    + '\n     @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '\n      \ttest_grep \"invalid.*replay.refAction.*value\" error\n      '\n    @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '\n     +'\n     +\n     +test_expect_success 'cannot revert with multiple sources' '\n    -+\techo \"fatal: cannot revert target with multiple sources because ordering would be ill-defined\" >expect &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\tfatal: '\"'\"'--revert'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n    ++\tEOF\n     +\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n     +\ttest_cmp expect actual\n     +'\n    @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '\n     +\t# Revert commits I and J\n     +\tgit replay --revert topic4 topic4~2..topic4 &&\n     +\n    -+\t# Verify the revert commits were created\n    ++\t# Verify the revert commits were created (newest-first ordering\n    ++\t# means J is reverted first, then I on top)\n     +\tgit log --format=%s -4 topic4 >actual &&\n     +\tcat >expect <<-\\EOF &&\n    -+\tRevert \"J\"\n     +\tRevert \"I\"\n    ++\tRevert \"J\"\n     +\tJ\n     +\tI\n     +\tEOF\n     +\ttest_cmp expect actual &&\n     +\n    -+\t# Verify commit message format includes hash\n    ++\t# Verify commit message format includes hash (tip is Revert \"I\")\n     +\ttest_commit_message topic4 <<-EOF &&\n    -+\tRevert \"J\"\n    ++\tRevert \"I\"\n     +\n    -+\tThis reverts commit $(git rev-parse J).\n    ++\tThis reverts commit $(git rev-parse I).\n     +\tEOF\n     +\n     +\t# Verify reflog message\n\n\nbase-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2\n\n"},{"id":"538846","messageId":"20260313054035.26605-2-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260313054035.26605-1-siddharthasthana31@gmail.com","subject":"[PATCH v4 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-13T05:40:34Z","receivedAt":"2026-03-13T05:40:46Z","isPatch":true,"body":"The logic for formatting revert commit messages (handling \"Revert\" and\n\"Reapply\" cases, appending \"This reverts commit <ref>.\", and handling\nmerge-parent references) currently lives inline in do_pick_commit().\nThe upcoming replay --revert mode needs to reuse this logic.\n\nExtract all of this into a new sequencer_format_revert_message()\nfunction. The function takes a repository, the subject line, commit,\nparent, a use_commit_reference flag, and the output strbuf. It handles\nboth regular reverts (\"Revert \"<subject>\"\") and revert-of-revert cases\n(\"Reapply \"<subject>\"\"), and uses refer_to_commit() internally to\nformat the commit reference.\n\nUpdate refer_to_commit() to take a struct repository parameter instead\nof relying on the_repository, and a bool instead of reading from\nreplay_opts directly. This makes it usable from the new shared function\nwithout pulling in sequencer-specific state.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------\n sequencer.h | 14 ++++++++++\n 2 files changed, 60 insertions(+), 32 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex aafd0bc959..7bf9d6ad19 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) {\n \treturn opts->edit;\n }\n \n-static void refer_to_commit(struct replay_opts *opts,\n-\t\t\t    struct strbuf *msgbuf, struct commit *commit)\n+static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,\n+\t\t\t    const struct commit *commit,\n+\t\t\t    bool use_commit_reference)\n {\n-\tif (opts->commit_use_reference) {\n+\tif (use_commit_reference) {\n \t\tstruct pretty_print_context ctx = {\n \t\t\t.abbrev = DEFAULT_ABBREV,\n \t\t\t.date_mode.type = DATE_SHORT,\n \t\t};\n-\t\trepo_format_commit_message(the_repository, commit,\n+\t\trepo_format_commit_message(r, commit,\n \t\t\t\t\t   \"%h (%s, %ad)\", msgbuf, &ctx);\n \t} else {\n \t\tstrbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));\n@@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r,\n \t */\n \n \tif (command == TODO_REVERT) {\n-\t\tconst char *orig_subject;\n-\n \t\tbase = commit;\n \t\tbase_label = msg.label;\n \t\tnext = parent;\n \t\tnext_label = msg.parent_label;\n-\t\tif (opts->commit_use_reference) {\n-\t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n-\t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n-\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n-\t\t\t   /*\n-\t\t\t    * We don't touch pre-existing repeated reverts, because\n-\t\t\t    * theoretically these can be nested arbitrarily deeply,\n-\t\t\t    * thus requiring excessive complexity to deal with.\n-\t\t\t    */\n-\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n-\t\t} else {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n-\t\trefer_to_commit(opts, &ctx->message, commit);\n-\n-\t\tif (commit->parents && commit->parents->next) {\n-\t\t\tstrbuf_addstr(&ctx->message, \", reversing\\nchanges made to \");\n-\t\t\trefer_to_commit(opts, &ctx->message, parent);\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \".\\n\");\n+\t\tsequencer_format_revert_message(r, msg.subject, commit,\n+\t\t\t\t\t\tparent,\n+\t\t\t\t\t\topts->commit_use_reference,\n+\t\t\t\t\t\t&ctx->message);\n \t} else {\n \t\tconst char *p;\n \n@@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r,\n \treturn res;\n }\n \n+void sequencer_format_revert_message(struct repository *r,\n+\t\t\t\t     const char *subject,\n+\t\t\t\t     const struct commit *commit,\n+\t\t\t\t     const struct commit *parent,\n+\t\t\t\t     bool use_commit_reference,\n+\t\t\t\t     struct strbuf *message)\n+{\n+\tconst char *orig_subject;\n+\n+\tif (use_commit_reference) {\n+\t\tstrbuf_commented_addf(message, comment_line_str,\n+\t\t\t\t      \"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n+\t} else if (skip_prefix(subject, \"Revert \\\"\", &orig_subject) &&\n+\t\t   /*\n+\t\t    * We don't touch pre-existing repeated reverts, because\n+\t\t    * theoretically these can be nested arbitrarily deeply,\n+\t\t    * thus requiring excessive complexity to deal with.\n+\t\t    */\n+\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n+\t\tstrbuf_addstr(message, \"Reapply \\\"\");\n+\t\tstrbuf_addstr(message, orig_subject);\n+\t\tstrbuf_addstr(message, \"\\n\");\n+\t} else {\n+\t\tstrbuf_addstr(message, \"Revert \\\"\");\n+\t\tstrbuf_addstr(message, subject);\n+\t\tstrbuf_addstr(message, \"\\\"\\n\");\n+\t}\n+\tstrbuf_addstr(message, \"\\nThis reverts commit \");\n+\trefer_to_commit(r, message, commit, use_commit_reference);\n+\n+\tif (commit->parents && commit->parents->next) {\n+\t\tstrbuf_addstr(message, \", reversing\\nchanges made to \");\n+\t\trefer_to_commit(r, message, parent, use_commit_reference);\n+\t}\n+\tstrbuf_addstr(message, \".\\n\");\n+}\n+\n void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n {\n \tunsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;\ndiff --git a/sequencer.h b/sequencer.h\nindex 719684c8a9..de0bd6e8a2 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -271,4 +271,18 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n  */\n int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n \n+/*\n+ * Formats a complete revert commit message following standard Git conventions.\n+ * Handles regular reverts (\"Revert \\\"<subject>\\\"\"), revert of revert cases\n+ * (\"Reapply \\\"<subject>\\\"\"), and the --reference style. Appends \"This reverts\n+ * commit <ref>.\" using either the abbreviated or full commit reference\n+ * depending on use_commit_reference. Also handles merge-parent references.\n+ */\n+void sequencer_format_revert_message(struct repository *r,\n+\t\t\t\t     const char *subject,\n+\t\t\t\t     const struct commit *commit,\n+\t\t\t\t     const struct commit *parent,\n+\t\t\t\t     bool use_commit_reference,\n+\t\t\t\t     struct strbuf *message);\n+\n #endif /* SEQUENCER_H */\n-- \n2.51.0\n\n"},{"id":"538847","messageId":"20260313054035.26605-3-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260313054035.26605-1-siddharthasthana31@gmail.com","subject":"[PATCH v4 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-13T05:40:35Z","receivedAt":"2026-03-13T05:40:51Z","isPatch":true,"body":"Add a `--revert <branch>` mode to git replay that undoes the changes\nintroduced by the specified commits. Like --onto and --advance, --revert\nis a standalone mode: it takes a branch argument and updates that branch\nwith the newly created revert commits.\n\nAt GitLab, we need this in Gitaly for reverting commits directly on bare\nrepositories without requiring a working tree checkout.\n\nThe approach is the same as sequencer.c's do_pick_commit() -- cherry-pick\nand revert are just the same three-way merge with swapped arguments:\n\n  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n  - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n\nWe swap the base and pickme trees passed to merge_incore_nonrecursive()\nto reverse the diff direction.\n\nReverts are processed newest-first (matching git revert behavior) to\nreduce conflicts by peeling off changes from the top. Each revert\nbuilds on the result of the previous one via the last_commit fallback\nin the main replay loop, rather than relying on the parent-mapping\nused for cherry-pick.\n\nRevert commit messages follow the usual git revert conventions: prefixed\nwith \"Revert\" (or \"Reapply\" when reverting a revert), and including\n\"This reverts commit <hash>.\". The author is set to the current user\nrather than preserving the original author, matching git revert behavior.\n\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nHelped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Toon Claes <toon@iotcl.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  43 ++++++++-\n builtin/replay.c              |  46 ++++++----\n replay.c                      | 165 ++++++++++++++++++++++++----------\n replay.h                      |  11 ++-\n t/t3650-replay-basics.sh      | 114 +++++++++++++++++++++--\n 5 files changed, 304 insertions(+), 75 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 8d696ce3ab..6698cfc047 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n \n DESCRIPTION\n -----------\n@@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to\n point at the tip of the resulting history. This is different from `--onto`,\n which uses the target only as a starting point without updating it.\n \n+--revert <branch>::\n+\tStarting point at which to create the reverted commits; must be a\n+\tbranch name.\n++\n+When `--revert` is specified, the commits in the revision range are reverted\n+(their changes are undone) and the reverted commits are created on top of\n+<branch>. The <branch> is then updated to point at the new commits. This is\n+the same as running `git revert <revision-range>` but does not update the\n+working tree.\n++\n+The commit messages follow `git revert` conventions: they are prefixed with\n+\"Revert\" and include \"This reverts commit <hash>.\" When reverting a commit\n+whose message starts with \"Revert\", the new message uses \"Reapply\" instead.\n+Unlike cherry-pick which preserves the original author, revert commits use\n+the current user as the author, matching the behavior of `git revert`.\n++\n+This option is mutually exclusive with `--onto` and `--advance`. It is also\n+incompatible with `--contained` (which is a modifier for `--onto` only).\n+\n --contained::\n \tUpdate all branches that point at commits in\n \t<revision-range>. Requires `--onto`.\n@@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to\n \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n \n where the number of refs updated depends on the arguments passed and\n-the shape of the history being replayed.  When using `--advance`, the\n-number of refs updated is always one, but for `--onto`, it can be one\n-or more (rebasing multiple branches simultaneously is supported).\n+the shape of the history being replayed.  When using `--advance` or\n+`--revert`, the number of refs updated is always one, but for `--onto`,\n+it can be one or more (rebasing multiple branches simultaneously is\n+supported).\n \n There is no stderr output on conflicts; see the <<exit-status,EXIT\n STATUS>> section below.\n@@ -152,6 +172,21 @@ all commits they have since `base`, playing them on top of\n `origin/main`. These three branches may have commits on top of `base`\n that they have in common, but that does not need to be the case.\n \n+To revert commits on a branch:\n+\n+------------\n+$ git replay --revert main topic~2..topic\n+------------\n+\n+This reverts the last two commits from `topic`, creating revert commits on\n+top of `main`, and updates `main` to point at the result. This is useful when\n+commits from `topic` were previously merged or cherry-picked into `main` and\n+need to be undone.\n+\n+NOTE: For reverting an entire merge request as a single commit (rather than\n+commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n+which can avoid unnecessary merge conflicts.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 2cdde830a8..fe69f6f8ce 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -83,8 +83,8 @@ int cmd_replay(int argc,\n \n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n+\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n+\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -96,6 +96,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n+\t\tOPT_STRING(0, \"revert\", &opts.revert,\n+\t\t\t   N_(\"branch\"),\n+\t\t\t   N_(\"revert commits onto given branch\")),\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n \t\t\t   N_(\"control ref update behavior (update|print)\")),\n@@ -105,15 +108,17 @@ int cmd_replay(int argc,\n \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n-\tif (!opts.onto && !opts.advance) {\n-\t\terror(_(\"option --onto or --advance is mandatory\"));\n+\t/* Exactly one mode must be specified */\n+\tif (!opts.onto && !opts.advance && !opts.revert) {\n+\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n-\t\t\t\t  opts.contained, \"--contained\");\n-\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n-\t\t\t\t  !!opts.onto, \"--onto\");\n+\tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n+\t\t\t\t  !!opts.advance, \"--advance\",\n+\t\t\t\t  !!opts.revert, \"--revert\");\n+\tif (opts.contained && !opts.onto)\n+\t\tdie(_(\"--contained requires --onto\"));\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n@@ -129,7 +134,13 @@ int cmd_replay(int argc,\n \t * some options changing these values if we think they could\n \t * be useful.\n \t */\n-\trevs.reverse = 1;\n+\t/*\n+\t * Cherry-pick/rebase need oldest-first ordering so that each\n+\t * replayed commit can build on its already-replayed parent.\n+\t * Revert needs newest-first ordering (like git revert) to\n+\t * reduce conflicts by peeling off changes from the top.\n+\t */\n+\trevs.reverse = opts.revert ? 0 : 1;\n \trevs.sort_order = REV_SORT_IN_GRAPH_ORDER;\n \trevs.topo_order = 1;\n \trevs.simplify_history = 0;\n@@ -144,11 +155,14 @@ int cmd_replay(int argc,\n \t * Detect and warn if we override some user specified rev\n \t * walking options.\n \t */\n-\tif (revs.reverse != 1) {\n-\t\twarning(_(\"some rev walking options will be overridden as \"\n-\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n-\t\t\t\"reverse\");\n-\t\trevs.reverse = 1;\n+\t{\n+\t\tint desired_reverse = opts.revert ? 0 : 1;\n+\t\tif (revs.reverse != desired_reverse) {\n+\t\t\twarning(_(\"some rev walking options will be overridden as \"\n+\t\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n+\t\t\t\t\"reverse\");\n+\t\t\trevs.reverse = desired_reverse;\n+\t\t}\n \t}\n \tif (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {\n \t\twarning(_(\"some rev walking options will be overridden as \"\n@@ -174,7 +188,9 @@ int cmd_replay(int argc,\n \t\tgoto cleanup;\n \n \t/* Build reflog message */\n-\tif (opts.advance) {\n+\tif (opts.revert) {\n+\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", opts.revert);\n+\t} else if (opts.advance) {\n \t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n \t} else {\n \t\tstruct object_id oid;\ndiff --git a/replay.c b/replay.c\nindex a63f6714c4..199066f6b3 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -8,14 +8,14 @@\n #include \"refs.h\"\n #include \"replay.h\"\n #include \"revision.h\"\n+#include \"sequencer.h\"\n #include \"strmap.h\"\n #include \"tree.h\"\n \n-/*\n- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but\n- * do not want to use the_repository.\n- */\n-#define the_repository DO_NOT_USE_THE_REPOSITORY\n+enum replay_mode {\n+\tREPLAY_MODE_PICK,\n+\tREPLAY_MODE_REVERT,\n+};\n \n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n@@ -50,15 +50,37 @@ static char *get_author(const char *message)\n \treturn NULL;\n }\n \n+static void generate_revert_message(struct strbuf *msg,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct repository *repo)\n+{\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n+\tconst char *subject_start;\n+\tint subject_len;\n+\tchar *subject;\n+\n+\tsubject_len = find_commit_subject(message, &subject_start);\n+\tsubject = xmemdupz(subject_start, subject_len);\n+\n+\tsequencer_format_revert_message(repo, subject, commit,\n+\t\t\t\t\tcommit->parents ? commit->parents->item : NULL,\n+\t\t\t\t\tfalse, msg);\n+\n+\tfree(subject);\n+\trepo_unuse_commit_buffer(repo, commit, message);\n+}\n+\n static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n+\t\t\t\t    struct commit *parent,\n+\t\t\t\t    enum replay_mode mode)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n-\tchar *author;\n+\tchar *author = NULL;\n \tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n@@ -70,9 +92,16 @@ static struct commit *create_commit(struct repository *repo,\n \n \tcommit_list_insert(parent, &parents);\n \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n+\tif (mode == REPLAY_MODE_REVERT) {\n+\t\tgenerate_revert_message(&msg, based_on, repo);\n+\t\t/* For revert, use current user as author (NULL = use default) */\n+\t} else if (mode == REPLAY_MODE_PICK) {\n+\t\tfind_commit_subject(message, &orig_message);\n+\t\tstrbuf_addstr(&msg, orig_message);\n+\t\tauthor = get_author(message);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \treset_ident_date();\n \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n@@ -153,11 +182,35 @@ static void get_ref_information(struct repository *repo,\n \t}\n }\n \n+static void set_up_branch_mode(struct repository *repo,\n+\t\t\t       char **branch_name,\n+\t\t\t       const char *option_name,\n+\t\t\t       struct ref_info *rinfo,\n+\t\t\t       struct commit **onto)\n+{\n+\tstruct object_id oid;\n+\tchar *fullname = NULL;\n+\n+\tif (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),\n+\t\t\t  &oid, &fullname, 0) == 1) {\n+\t\tfree(*branch_name);\n+\t\t*branch_name = fullname;\n+\t} else {\n+\t\tdie(_(\"argument to %s must be a reference\"), option_name);\n+\t}\n+\t*onto = peel_committish(repo, *branch_name, option_name);\n+\tif (rinfo->positive_refexprs > 1)\n+\t\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n+\t\t      \"because the ordering would be ill-defined\"),\n+\t\t    option_name);\n+}\n+\n static void set_up_replay_mode(struct repository *repo,\n \t\t\t       struct rev_cmdline_info *cmd_info,\n \t\t\t       const char *onto_name,\n \t\t\t       bool *detached_head,\n \t\t\t       char **advance_name,\n+\t\t\t       char **revert_name,\n \t\t\t       struct commit **onto,\n \t\t\t       struct strset **update_refs)\n {\n@@ -172,9 +225,6 @@ static void set_up_replay_mode(struct repository *repo,\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n \n-\tif (!onto_name == !*advance_name)\n-\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n-\n \tif (onto_name) {\n \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n \t\tif (rinfo.positive_refexprs <\n@@ -183,23 +233,12 @@ static void set_up_replay_mode(struct repository *repo,\n \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n \t\t**update_refs = rinfo.positive_refs;\n \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n+\t} else if (*advance_name) {\n+\t\tset_up_branch_mode(repo, advance_name, \"--advance\", &rinfo, onto);\n+\t} else if (*revert_name) {\n+\t\tset_up_branch_mode(repo, revert_name, \"--revert\", &rinfo, onto);\n \t} else {\n-\t\tstruct object_id oid;\n-\t\tchar *fullname = NULL;\n-\n-\t\tif (!*advance_name)\n-\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n-\n-\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n-\t\t\t     &oid, &fullname, 0) == 1) {\n-\t\t\tfree(*advance_name);\n-\t\t\t*advance_name = fullname;\n-\t\t} else {\n-\t\t\tdie(_(\"argument to --advance must be a reference\"));\n-\t\t}\n-\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n-\t\tif (rinfo.positive_refexprs > 1)\n-\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n+\t\tBUG(\"expected one of onto_name, *advance_name, or *revert_name\");\n \t}\n \tstrset_clear(&rinfo.negative_refs);\n \tstrset_clear(&rinfo.positive_refs);\n@@ -220,7 +259,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n \t\t\t\t\t  struct commit *onto,\n \t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n+\t\t\t\t\t  struct merge_result *result,\n+\t\t\t\t\t  enum replay_mode mode)\n {\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n@@ -232,25 +272,45 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  replayed_base_tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n+\tif (mode == REPLAY_MODE_PICK) {\n+\t\t/* Cherry-pick: normal order */\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->ancestor);\n+\t} else if (mode == REPLAY_MODE_REVERT) {\n+\t\t/* Revert: swap base and pickme to reverse the diff */\n+\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n+\t\tmerge_opt->ancestor = pickme_name;\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->branch2);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \tmerge_opt->ancestor = NULL;\n+\tmerge_opt->branch2 = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n \t/* Drop commits that become empty */\n \tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n \t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n \t\treturn replayed_base;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -287,11 +347,16 @@ int replay_revisions(struct rev_info *revs,\n \t};\n \tbool detached_head;\n \tchar *advance;\n+\tchar *revert;\n+\tenum replay_mode mode = REPLAY_MODE_PICK;\n \tint ret;\n \n \tadvance = xstrdup_or_null(opts->advance);\n+\trevert = xstrdup_or_null(opts->revert);\n+\tif (revert)\n+\t\tmode = REPLAY_MODE_REVERT;\n \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n-\t\t\t   &detached_head, &advance, &onto, &update_refs);\n+\t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n \n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n@@ -315,7 +380,8 @@ int replay_revisions(struct rev_info *revs,\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n+\t\t\t\t\t\t  &merge_opt, &result, mode);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \n@@ -327,7 +393,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance)\n+\t\tif (advance || revert)\n \t\t\tcontinue;\n \n \t\tfor (decoration = get_name_decoration(&commit->object);\n@@ -361,11 +427,13 @@ int replay_revisions(struct rev_info *revs,\n \t\tgoto out;\n \t}\n \n-\t/* In --advance mode, advance the target ref */\n-\tif (advance)\n-\t\treplay_result_queue_update(out, advance,\n+\t/* In --advance or --revert mode, update the target ref */\n+\tif (advance || revert) {\n+\t\tconst char *ref = advance ? advance : revert;\n+\t\treplay_result_queue_update(out, ref,\n \t\t\t\t\t   &onto->object.oid,\n \t\t\t\t\t   &last_commit->object.oid);\n+\t}\n \n \tret = 0;\n \n@@ -377,5 +445,6 @@ int replay_revisions(struct rev_info *revs,\n \tkh_destroy_oid_map(replayed_commits);\n \tmerge_finalize(&merge_opt, &result);\n \tfree(advance);\n+\tfree(revert);\n \treturn ret;\n }\ndiff --git a/replay.h b/replay.h\nindex d8407dc7f7..e916a5f975 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -13,7 +13,7 @@ struct replay_revisions_options {\n \t/*\n \t * Starting point at which to create the new commits; must be a branch\n \t * name. The branch will be updated to point to the rewritten commits.\n-\t * This option is mutually exclusive with `onto`.\n+\t * This option is mutually exclusive with `onto` and `revert`.\n \t */\n \tconst char *advance;\n \n@@ -22,7 +22,14 @@ struct replay_revisions_options {\n \t * committish. References pointing at decendants of `onto` will be\n \t * updated to point to the new commits.\n \t */\n-\t const char *onto;\n+\tconst char *onto;\n+\n+\t/*\n+\t * Starting point at which to create revert commits; must be a branch\n+\t * name. The branch will be updated to point to the revert commits.\n+\t * This option is mutually exclusive with `onto` and `advance`.\n+\t */\n+\tconst char *revert;\n \n \t/*\n \t * Update branches that point at commits in the given revision range.\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex a03f8f9293..0c1e03e0fb 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'option --onto or --advance is mandatory' '\n-\techo \"error: option --onto or --advance is mandatory\" >expect &&\n+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '\n+\techo \"error: exactly one of --onto, --advance, or --revert is required\" >expect &&\n \ttest_might_fail git replay -h >>expect &&\n \ttest_must_fail git replay topic1..topic2 2>actual &&\n \ttest_cmp expect actual\n@@ -87,16 +87,17 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'options --advance and --contained cannot be used together' '\n-\tprintf \"fatal: options ${SQ}--advance${SQ} \" >expect &&\n-\tprintf \"and ${SQ}--contained${SQ} cannot be used together\\n\" >>expect &&\n+test_expect_success '--contained requires --onto' '\n+\techo \"fatal: --contained requires --onto\" >expect &&\n \ttest_must_fail git replay --advance=main --contained \\\n \t\ttopic1..topic2 2>actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n-\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n+\tcat >expect <<-\\EOF &&\n+\tfatal: '\"'\"'--advance'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n+\tEOF\n \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n \ttest_cmp expect actual\n '\n@@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' '\n \ttest_grep \"invalid.*replay.refAction.*value\" error\n '\n \n+test_expect_success 'argument to --revert must be a reference' '\n+\techo \"fatal: argument to --revert must be a reference\" >expect &&\n+\toid=$(git rev-parse main) &&\n+\ttest_must_fail git replay --revert=$oid topic1..topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'cannot revert with multiple sources' '\n+\tcat >expect <<-\\EOF &&\n+\tfatal: '\"'\"'--revert'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n+\tEOF\n+\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using replay --revert to revert commits' '\n+\t# Reuse existing topic4 branch (has commits I and J on top of main)\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# Revert commits I and J\n+\tgit replay --revert topic4 topic4~2..topic4 &&\n+\n+\t# Verify the revert commits were created (newest-first ordering\n+\t# means J is reverted first, then I on top)\n+\tgit log --format=%s -4 topic4 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\tRevert \"I\"\n+\tRevert \"J\"\n+\tJ\n+\tI\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\t# Verify commit message format includes hash (tip is Revert \"I\")\n+\ttest_commit_message topic4 <<-EOF &&\n+\tRevert \"I\"\n+\n+\tThis reverts commit $(git rev-parse I).\n+\tEOF\n+\n+\t# Verify reflog message\n+\tgit reflog topic4 -1 --format=%gs >reflog-msg &&\n+\techo \"replay --revert topic4\" >expect-reflog &&\n+\ttest_cmp expect-reflog reflog-msg\n+'\n+\n+test_expect_success 'using replay --revert in bare repo' '\n+\t# Reuse existing topic4 in bare repo\n+\tSTART=$(git -C bare rev-parse topic4) &&\n+\ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n+\n+\t# Revert commit J in bare repo\n+\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify revert was created\n+\tgit -C bare log -1 --format=%s topic4 >actual &&\n+\techo \"Revert \\\"J\\\"\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'revert of revert uses Reapply' '\n+\t# Use topic4 and first revert J, then revert the revert\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# First revert J\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\tREVERT_J=$(git rev-parse topic4) &&\n+\n+\t# Now revert the revert - should become Reapply\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify Reapply prefix and message format\n+\ttest_commit_message topic4 <<-EOF\n+\tReapply \"J\"\n+\n+\tThis reverts commit $REVERT_J.\n+\tEOF\n+'\n+\n+test_expect_success 'git replay --revert with conflict' '\n+\t# conflict branch has C.conflict which conflicts with topic1s C\n+\ttest_expect_code 1 git replay --revert conflict B..topic1\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --contained' '\n+\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n+\ttest_grep \"requires --onto\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --onto' '\n+\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --advance' '\n+\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"538905","messageId":"xmqqy0jv7ml9.fsf@gitster.g","threadId":"64529","inReplyTo":"20260313054035.26605-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 1/2] sequencer: extract revert message formatting into shared function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-13T15:53:06Z","receivedAt":"2026-03-13T15:53:09Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> The logic for formatting revert commit messages (handling \"Revert\" and\n> \"Reapply\" cases, appending \"This reverts commit <ref>.\", and handling\n> merge-parent references) currently lives inline in do_pick_commit().\n> The upcoming replay --revert mode needs to reuse this logic.\n>\n> Extract all of this into a new sequencer_format_revert_message()\n> function. The function takes a repository, the subject line, commit,\n> parent, a use_commit_reference flag, and the output strbuf. It handles\n> both regular reverts (\"Revert \"<subject>\"\") and revert-of-revert cases\n> (\"Reapply \"<subject>\"\"), and uses refer_to_commit() internally to\n> format the commit reference.\n>\n> Update refer_to_commit() to take a struct repository parameter instead\n> of relying on the_repository, and a bool instead of reading from\n> replay_opts directly. This makes it usable from the new shared function\n> without pulling in sequencer-specific state.\n>\n> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> ---\n>  sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------\n>  sequencer.h | 14 ++++++++++\n>  2 files changed, 60 insertions(+), 32 deletions(-)\n\nRelative to the previous round, sequencer_format_revert_message()\nthat does a bit more than sequencer_format_revert_header() we had\nmakes the existing code easier to follow, even though the total\ncodeflow amounts to the same thing.  A new caller that will use the\nfunction now has to do less.\n\nAlso, even though this is an internal implementation detail,\nchanging the list of parameters refer_to_commit() takes makes it\neasier to understand which part of the replay_opts structure is used\n(i.e., we only care about \"do we use the commit reference, or not?\"\nbit, and we have no interest in any other members of the struct).\n\n> +/*\n> + * Formats a complete revert commit message following standard Git conventions.\n> + * Handles regular reverts (\"Revert \\\"<subject>\\\"\"), revert of revert cases\n> + * (\"Reapply \\\"<subject>\\\"\"), and the --reference style. Appends \"This reverts\n> + * commit <ref>.\" using either the abbreviated or full commit reference\n> + * depending on use_commit_reference. Also handles merge-parent references.\n> + */\n> +void sequencer_format_revert_message(struct repository *r,\n> +\t\t\t\t     const char *subject,\n> +\t\t\t\t     const struct commit *commit,\n> +\t\t\t\t     const struct commit *parent,\n> +\t\t\t\t     bool use_commit_reference,\n> +\t\t\t\t     struct strbuf *message);\n> +\n>  #endif /* SEQUENCER_H */\n\nOK.\n"},{"id":"539132","messageId":"6033b9ea-f219-43cc-9ce0-901594364ccd@gmail.com","threadId":"64529","inReplyTo":"20260313054035.26605-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 1/2] sequencer: extract revert message formatting into shared function","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-03-16T16:57:32Z","receivedAt":"2026-03-16T16:57:35Z","isPatch":true,"body":"On 13/03/2026 05:40, Siddharth Asthana wrote:\n> The logic for formatting revert commit messages (handling \"Revert\" and\n> \"Reapply\" cases, appending \"This reverts commit <ref>.\", and handling\n> merge-parent references) currently lives inline in do_pick_commit().\n> The upcoming replay --revert mode needs to reuse this logic.\n> \n> Extract all of this into a new sequencer_format_revert_message()\n> function. The function takes a repository, the subject line, commit,\n> parent, a use_commit_reference flag, and the output strbuf. It handles\n> both regular reverts (\"Revert \"<subject>\"\") and revert-of-revert cases\n> (\"Reapply \"<subject>\"\"), and uses refer_to_commit() internally to\n> format the commit reference.\n> \n> Update refer_to_commit() to take a struct repository parameter instead\n> of relying on the_repository, and a bool instead of reading from\n> replay_opts directly. This makes it usable from the new shared function\n> without pulling in sequencer-specific state.\n\nThis looks good.\n\n> +/*\n> + * Formats a complete revert commit message following standard Git conventions.\n> + * Handles regular reverts (\"Revert \\\"<subject>\\\"\"), revert of revert cases\n\nNot worth a re-roll on its own, but for future reference in comments you \ncan use single quotes to avoid nesting double quotes i.e. ('Revert \n\"<subject>\"').\n\nThanks\n\nPhillip\n\n> + * (\"Reapply \\\"<subject>\\\"\"), and the --reference style. Appends \"This reverts\n> + * commit <ref>.\" using either the abbreviated or full commit reference\n> + * depending on use_commit_reference. Also handles merge-parent references.\n> + */\n> +void sequencer_format_revert_message(struct repository *r,\n> +\t\t\t\t     const char *subject,\n> +\t\t\t\t     const struct commit *commit,\n> +\t\t\t\t     const struct commit *parent,\n> +\t\t\t\t     bool use_commit_reference,\n> +\t\t\t\t     struct strbuf *message);\n> +\n>   #endif /* SEQUENCER_H */\n\n"},{"id":"539133","messageId":"f18f9bb4-4c30-4972-a034-a74b81a3e1e6@gmail.com","threadId":"64529","inReplyTo":"20260313054035.26605-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 2/2] replay: add --revert mode to reverse commit changes","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-03-16T16:57:52Z","receivedAt":"2026-03-16T16:57:55Z","isPatch":true,"body":"Hi Siddharth\n\nOn 13/03/2026 05:40, Siddharth Asthana wrote:\n> Add a `--revert <branch>` mode to git replay that undoes the changes\n> introduced by the specified commits. Like --onto and --advance, --revert\n> is a standalone mode: it takes a branch argument and updates that branch\n> with the newly created revert commits.\n> \n> At GitLab, we need this in Gitaly for reverting commits directly on bare\n> repositories without requiring a working tree checkout.\n> \n> The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick\n> and revert are just the same three-way merge with swapped arguments:\n> \n>    - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n>    - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n> \n> We swap the base and pickme trees passed to merge_incore_nonrecursive()\n> to reverse the diff direction.\n> \n> Reverts are processed newest-first (matching git revert behavior) to\n> reduce conflicts by peeling off changes from the top. Each revert\n> builds on the result of the previous one via the last_commit fallback\n> in the main replay loop, rather than relying on the parent-mapping\n> used for cherry-pick.\n> \n> Revert commit messages follow the usual git revert conventions: prefixed\n> with \"Revert\" (or \"Reapply\" when reverting a revert), and including\n> \"This reverts commit <hash>.\". The author is set to the current user\n> rather than preserving the original author, matching git revert behavior.\n\nThis addresses all my comments on the previous version. I've one minor \ncomment below but I'm not sure its worth a re-roll on its own.\n\n>   \n>   test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n> -\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n> +\tcat >expect <<-\\EOF &&\n> +\tfatal: '\"'\"'--advance'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n\nThis quoting is a bit strange - we'd normally drop the '\\' form '\\EOF' \nabove use ${SQ} instead. git grep shows there are 220 instances of ${SQ} \nvs 31 instances of '\"'\"' in the test suite.\n\nThanks\n\nPhillip\n\n\n> +\tEOF\n>   \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n>   \ttest_cmp expect actual\n>   '\n> @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' '\n>   \ttest_grep \"invalid.*replay.refAction.*value\" error\n>   '\n>   \n> +test_expect_success 'argument to --revert must be a reference' '\n> +\techo \"fatal: argument to --revert must be a reference\" >expect &&\n> +\toid=$(git rev-parse main) &&\n> +\ttest_must_fail git replay --revert=$oid topic1..topic2 2>actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'cannot revert with multiple sources' '\n> +\tcat >expect <<-\\EOF &&\n> +\tfatal: '\"'\"'--revert'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n> +\tEOF\n> +\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'using replay --revert to revert commits' '\n> +\t# Reuse existing topic4 branch (has commits I and J on top of main)\n> +\tSTART=$(git rev-parse topic4) &&\n> +\ttest_when_finished \"git branch -f topic4 $START\" &&\n> +\n> +\t# Revert commits I and J\n> +\tgit replay --revert topic4 topic4~2..topic4 &&\n> +\n> +\t# Verify the revert commits were created (newest-first ordering\n> +\t# means J is reverted first, then I on top)\n> +\tgit log --format=%s -4 topic4 >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\tRevert \"I\"\n> +\tRevert \"J\"\n> +\tJ\n> +\tI\n> +\tEOF\n> +\ttest_cmp expect actual &&\n> +\n> +\t# Verify commit message format includes hash (tip is Revert \"I\")\n> +\ttest_commit_message topic4 <<-EOF &&\n> +\tRevert \"I\"\n> +\n> +\tThis reverts commit $(git rev-parse I).\n> +\tEOF\n> +\n> +\t# Verify reflog message\n> +\tgit reflog topic4 -1 --format=%gs >reflog-msg &&\n> +\techo \"replay --revert topic4\" >expect-reflog &&\n> +\ttest_cmp expect-reflog reflog-msg\n> +'\n> +\n> +test_expect_success 'using replay --revert in bare repo' '\n> +\t# Reuse existing topic4 in bare repo\n> +\tSTART=$(git -C bare rev-parse topic4) &&\n> +\ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n> +\n> +\t# Revert commit J in bare repo\n> +\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n> +\n> +\t# Verify revert was created\n> +\tgit -C bare log -1 --format=%s topic4 >actual &&\n> +\techo \"Revert \\\"J\\\"\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'revert of revert uses Reapply' '\n> +\t# Use topic4 and first revert J, then revert the revert\n> +\tSTART=$(git rev-parse topic4) &&\n> +\ttest_when_finished \"git branch -f topic4 $START\" &&\n> +\n> +\t# First revert J\n> +\tgit replay --revert topic4 topic4~1..topic4 &&\n> +\tREVERT_J=$(git rev-parse topic4) &&\n> +\n> +\t# Now revert the revert - should become Reapply\n> +\tgit replay --revert topic4 topic4~1..topic4 &&\n> +\n> +\t# Verify Reapply prefix and message format\n> +\ttest_commit_message topic4 <<-EOF\n> +\tReapply \"J\"\n> +\n> +\tThis reverts commit $REVERT_J.\n> +\tEOF\n> +'\n> +\n> +test_expect_success 'git replay --revert with conflict' '\n> +\t# conflict branch has C.conflict which conflicts with topic1s C\n> +\ttest_expect_code 1 git replay --revert conflict B..topic1\n> +'\n> +\n> +test_expect_success 'git replay --revert incompatible with --contained' '\n> +\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n> +\ttest_grep \"requires --onto\" error\n> +'\n> +\n> +test_expect_success 'git replay --revert incompatible with --onto' '\n> +\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n> +\ttest_grep \"cannot be used together\" error\n> +'\n> +\n> +test_expect_success 'git replay --revert incompatible with --advance' '\n> +\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n> +\ttest_grep \"cannot be used together\" error\n> +'\n> +\n>   test_done\n\n"},{"id":"539134","messageId":"375b7285-9840-4704-9e3e-c83793e890d6@gmail.com","threadId":"64529","inReplyTo":"20260313054035.26605-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 0/2] replay: add --revert mode to reverse commit changes","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-03-16T16:59:35Z","receivedAt":"2026-03-16T16:59:38Z","isPatch":true,"body":"On 13/03/2026 05:40, Siddharth Asthana wrote:\n> \n> Changes in v4:\n> - Replaced sequencer_format_revert_header() with a more complete\n>    sequencer_format_revert_message() that handles everything: subject\n>    prefix, commit reference via refer_to_commit(), and merge-parent\n>    references -- per Phillip\n> - Updated refer_to_commit() signature to take (struct repository *r,\n>    bool use_commit_reference) instead of (struct replay_opts *opts)\n> - Reverts are now newest-first (revs.reverse = 0 for --revert),\n>    chaining on last_commit rather than the parent mapping\n> - Changed doc example to cross-branch scenario and restored the\n>    merge-tree NOTE\n> - Updated error message format to \"'--revert' cannot be used with\n>    multiple revision ranges...\" (and same for --advance)\n> - Empty revert commits are now dropped, consistent with cherry-pick\n\nThis looks good to me. I've left a couple of nitpicking comments but \nthey're mainly for future reference - I'd be happy to see this merged as is.\n\nThanks\n\nPhillip\n\n> - Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/\n> - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/\n> - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/\n> \n> Thanks,\n> Siddharth\n> \n> ---\n> Siddharth Asthana (2):\n>    sequencer: extract revert message formatting into shared function\n>    replay: add --revert mode to reverse commit changes\n> \n>   Documentation/git-replay.adoc |  43 ++++++++-\n>   builtin/replay.c              |  46 ++++++----\n>   replay.c                      | 165 ++++++++++++++++++++++++----------\n>   replay.h                      |  11 ++-\n>   sequencer.c                   |  78 +++++++++-------\n>   sequencer.h                   |  14 +++\n>   t/t3650-replay-basics.sh      | 114 +++++++++++++++++++++--\n>   7 files changed, 364 insertions(+), 107 deletions(-)\n> \n> Range-diff versus v3:\n> \n> 1:  9d686bcdfe ! 1:  bdc710b265 sequencer: extract revert message formatting into shared function\n>      @@ Commit message\n>           sequencer: extract revert message formatting into shared function\n>       \n>           The logic for formatting revert commit messages (handling \"Revert\" and\n>      -    \"Reapply\" cases) is currently duplicated between sequencer.c and will be\n>      -    needed by builtin/replay.c.\n>      +    \"Reapply\" cases, appending \"This reverts commit <ref>.\", and handling\n>      +    merge-parent references) currently lives inline in do_pick_commit().\n>      +    The upcoming replay --revert mode needs to reuse this logic.\n>       \n>      -    Extract this logic into a new sequencer_format_revert_header() function\n>      -    that can be shared. The function handles both regular reverts (\"Revert\n>      -    \"<subject>\"\") and revert-of-revert cases (\"Reapply \"<subject>\"\").\n>      -    When an oid is provided, the function appends the full commit hash and\n>      -    period; otherwise the caller should append the commit reference.\n>      +    Extract all of this into a new sequencer_format_revert_message()\n>      +    function. The function takes a repository, the subject line, commit,\n>      +    parent, a use_commit_reference flag, and the output strbuf. It handles\n>      +    both regular reverts (\"Revert \"<subject>\"\") and revert-of-revert cases\n>      +    (\"Reapply \"<subject>\"\"), and uses refer_to_commit() internally to\n>      +    format the commit reference.\n>       \n>      -    Update do_pick_commit() to use the new helper, eliminating code\n>      -    duplication while preserving the special handling for commit_use_reference.\n>      +    Update refer_to_commit() to take a struct repository parameter instead\n>      +    of relying on the_repository, and a bool instead of reading from\n>      +    replay_opts directly. This makes it usable from the new shared function\n>      +    without pulling in sequencer-specific state.\n>       \n>           Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n>       \n>        ## sequencer.c ##\n>      +@@ sequencer.c: static int should_edit(struct replay_opts *opts) {\n>      + \treturn opts->edit;\n>      + }\n>      +\n>      +-static void refer_to_commit(struct replay_opts *opts,\n>      +-\t\t\t    struct strbuf *msgbuf, struct commit *commit)\n>      ++static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,\n>      ++\t\t\t    const struct commit *commit,\n>      ++\t\t\t    bool use_commit_reference)\n>      + {\n>      +-\tif (opts->commit_use_reference) {\n>      ++\tif (use_commit_reference) {\n>      + \t\tstruct pretty_print_context ctx = {\n>      + \t\t\t.abbrev = DEFAULT_ABBREV,\n>      + \t\t\t.date_mode.type = DATE_SHORT,\n>      + \t\t};\n>      +-\t\trepo_format_commit_message(the_repository, commit,\n>      ++\t\trepo_format_commit_message(r, commit,\n>      + \t\t\t\t\t   \"%h (%s, %ad)\", msgbuf, &ctx);\n>      + \t} else {\n>      + \t\tstrbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));\n>       @@ sequencer.c: static int do_pick_commit(struct repository *r,\n>        \t */\n>        \n>      @@ sequencer.c: static int do_pick_commit(struct repository *r,\n>        \t\tbase = commit;\n>        \t\tbase_label = msg.label;\n>        \t\tnext = parent;\n>      -@@ sequencer.c: static int do_pick_commit(struct repository *r,\n>      - \t\tif (opts->commit_use_reference) {\n>      - \t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>      - \t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n>      + \t\tnext_label = msg.parent_label;\n>      +-\t\tif (opts->commit_use_reference) {\n>      +-\t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n>      +-\t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n>       -\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n>       -\t\t\t   /*\n>       -\t\t\t    * We don't touch pre-existing repeated reverts, because\n>      @@ sequencer.c: static int do_pick_commit(struct repository *r,\n>       -\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n>       -\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n>       -\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n>      -+\t\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>      - \t\t} else {\n>      +-\t\t} else {\n>       -\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n>       -\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n>       -\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n>      -+\t\t\tsequencer_format_revert_header(&ctx->message, msg.subject, NULL);\n>      - \t\t}\n>      +-\t\t}\n>       -\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n>      - \t\trefer_to_commit(opts, &ctx->message, commit);\n>      +-\t\trefer_to_commit(opts, &ctx->message, commit);\n>      +-\n>      +-\t\tif (commit->parents && commit->parents->next) {\n>      +-\t\t\tstrbuf_addstr(&ctx->message, \", reversing\\nchanges made to \");\n>      +-\t\t\trefer_to_commit(opts, &ctx->message, parent);\n>      +-\t\t}\n>      +-\t\tstrbuf_addstr(&ctx->message, \".\\n\");\n>      ++\t\tsequencer_format_revert_message(r, msg.subject, commit,\n>      ++\t\t\t\t\t\tparent,\n>      ++\t\t\t\t\t\topts->commit_use_reference,\n>      ++\t\t\t\t\t\t&ctx->message);\n>      + \t} else {\n>      + \t\tconst char *p;\n>        \n>      - \t\tif (commit->parents && commit->parents->next) {\n>       @@ sequencer.c: int sequencer_pick_revisions(struct repository *r,\n>        \treturn res;\n>        }\n>        \n>      -+void sequencer_format_revert_header(struct strbuf *out,\n>      -+\t\t\t\t    const char *orig_subject,\n>      -+\t\t\t\t    const struct object_id *oid)\n>      ++void sequencer_format_revert_message(struct repository *r,\n>      ++\t\t\t\t     const char *subject,\n>      ++\t\t\t\t     const struct commit *commit,\n>      ++\t\t\t\t     const struct commit *parent,\n>      ++\t\t\t\t     bool use_commit_reference,\n>      ++\t\t\t\t     struct strbuf *message)\n>       +{\n>      -+\tconst char *revert_subject;\n>      ++\tconst char *orig_subject;\n>       +\n>      -+\tif (skip_prefix(orig_subject, \"Revert \\\"\", &revert_subject) &&\n>      -+\t    /*\n>      -+\t     * We don't touch pre-existing repeated reverts, because\n>      -+\t     * theoretically these can be nested arbitrarily deeply,\n>      -+\t     * thus requiring excessive complexity to deal with.\n>      -+\t     */\n>      -+\t    !starts_with(revert_subject, \"Revert \\\"\")) {\n>      -+\t\tstrbuf_addstr(out, \"Reapply \\\"\");\n>      -+\t\tstrbuf_addstr(out, revert_subject);\n>      -+\t\tstrbuf_addch(out, '\\n');\n>      ++\tif (use_commit_reference) {\n>      ++\t\tstrbuf_commented_addf(message, comment_line_str,\n>      ++\t\t\t\t      \"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n>      ++\t} else if (skip_prefix(subject, \"Revert \\\"\", &orig_subject) &&\n>      ++\t\t   /*\n>      ++\t\t    * We don't touch pre-existing repeated reverts, because\n>      ++\t\t    * theoretically these can be nested arbitrarily deeply,\n>      ++\t\t    * thus requiring excessive complexity to deal with.\n>      ++\t\t    */\n>      ++\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n>      ++\t\tstrbuf_addstr(message, \"Reapply \\\"\");\n>      ++\t\tstrbuf_addstr(message, orig_subject);\n>      ++\t\tstrbuf_addstr(message, \"\\n\");\n>       +\t} else {\n>      -+\t\tstrbuf_addstr(out, \"Revert \\\"\");\n>      -+\t\tstrbuf_addstr(out, orig_subject);\n>      -+\t\tstrbuf_addstr(out, \"\\\"\\n\");\n>      ++\t\tstrbuf_addstr(message, \"Revert \\\"\");\n>      ++\t\tstrbuf_addstr(message, subject);\n>      ++\t\tstrbuf_addstr(message, \"\\\"\\n\");\n>       +\t}\n>      ++\tstrbuf_addstr(message, \"\\nThis reverts commit \");\n>      ++\trefer_to_commit(r, message, commit, use_commit_reference);\n>       +\n>      -+\tstrbuf_addstr(out, \"\\nThis reverts commit \");\n>      -+\tif (oid) {\n>      -+\t\tstrbuf_addstr(out, oid_to_hex(oid));\n>      -+\t\tstrbuf_addstr(out, \".\\n\");\n>      ++\tif (commit->parents && commit->parents->next) {\n>      ++\t\tstrbuf_addstr(message, \", reversing\\nchanges made to \");\n>      ++\t\trefer_to_commit(r, message, parent, use_commit_reference);\n>       +\t}\n>      ++\tstrbuf_addstr(message, \".\\n\");\n>       +}\n>       +\n>        void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n>      @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh\n>        int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n>        \n>       +/*\n>      -+ * Formats a revert commit message following standard Git conventions.\n>      -+ * Handles both regular reverts (\"Revert \\\"<subject>\\\"\") and revert of revert\n>      -+ * cases (\"Reapply \\\"<subject>\\\"\"). Adds \"This reverts commit <oid>.\" if oid\n>      -+ * is provided, otherwise just adds \"This reverts commit \" and the caller\n>      -+ * should append the commit reference.\n>      ++ * Formats a complete revert commit message following standard Git conventions.\n>      ++ * Handles regular reverts (\"Revert \\\"<subject>\\\"\"), revert of revert cases\n>      ++ * (\"Reapply \\\"<subject>\\\"\"), and the --reference style. Appends \"This reverts\n>      ++ * commit <ref>.\" using either the abbreviated or full commit reference\n>      ++ * depending on use_commit_reference. Also handles merge-parent references.\n>       + */\n>      -+void sequencer_format_revert_header(struct strbuf *out,\n>      -+\t\t\t\t    const char *orig_subject,\n>      -+\t\t\t\t    const struct object_id *oid);\n>      ++void sequencer_format_revert_message(struct repository *r,\n>      ++\t\t\t\t     const char *subject,\n>      ++\t\t\t\t     const struct commit *commit,\n>      ++\t\t\t\t     const struct commit *parent,\n>      ++\t\t\t\t     bool use_commit_reference,\n>      ++\t\t\t\t     struct strbuf *message);\n>       +\n>        #endif /* SEQUENCER_H */\n> 2:  066269706e ! 2:  bea6229575 replay: add --revert mode to reverse commit changes\n>      @@ Commit message\n>           We swap the base and pickme trees passed to merge_incore_nonrecursive()\n>           to reverse the diff direction.\n>       \n>      +    Reverts are processed newest-first (matching git revert behavior) to\n>      +    reduce conflicts by peeling off changes from the top. Each revert\n>      +    builds on the result of the previous one via the last_commit fallback\n>      +    in the main replay loop, rather than relying on the parent-mapping\n>      +    used for cherry-pick.\n>      +\n>           Revert commit messages follow the usual git revert conventions: prefixed\n>           with \"Revert\" (or \"Reapply\" when reverting a revert), and including\n>           \"This reverts commit <hash>.\". The author is set to the current user\n>      @@ Commit message\n>           Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n>           Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\n>           Helped-by: Junio C Hamano <gitster@pobox.com>\n>      +    Helped-by: Toon Claes <toon@iotcl.com>\n>           Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n>       \n>        ## Documentation/git-replay.adoc ##\n>      @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them\n>       +To revert commits on a branch:\n>       +\n>       +------------\n>      -+$ git replay --revert main main~2..main\n>      ++$ git replay --revert main topic~2..topic\n>       +------------\n>       +\n>      -+This reverts the last two commits on `main`, creating two revert commits\n>      -+on top of `main`, and updates `main` to point at the result.\n>      ++This reverts the last two commits from `topic`, creating revert commits on\n>      ++top of `main`, and updates `main` to point at the result. This is useful when\n>      ++commits from `topic` were previously merged or cherry-picked into `main` and\n>      ++need to be undone.\n>      ++\n>      ++NOTE: For reverting an entire merge request as a single commit (rather than\n>      ++commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n>      ++which can avoid unnecessary merge conflicts.\n>       +\n>        GIT\n>        ---\n>      @@ builtin/replay.c: int cmd_replay(int argc,\n>        \n>        \t/* Parse ref action mode from command line or config */\n>        \tref_mode = get_ref_action_mode(repo, ref_action);\n>      +@@ builtin/replay.c: int cmd_replay(int argc,\n>      + \t * some options changing these values if we think they could\n>      + \t * be useful.\n>      + \t */\n>      +-\trevs.reverse = 1;\n>      ++\t/*\n>      ++\t * Cherry-pick/rebase need oldest-first ordering so that each\n>      ++\t * replayed commit can build on its already-replayed parent.\n>      ++\t * Revert needs newest-first ordering (like git revert) to\n>      ++\t * reduce conflicts by peeling off changes from the top.\n>      ++\t */\n>      ++\trevs.reverse = opts.revert ? 0 : 1;\n>      + \trevs.sort_order = REV_SORT_IN_GRAPH_ORDER;\n>      + \trevs.topo_order = 1;\n>      + \trevs.simplify_history = 0;\n>      +@@ builtin/replay.c: int cmd_replay(int argc,\n>      + \t * Detect and warn if we override some user specified rev\n>      + \t * walking options.\n>      + \t */\n>      +-\tif (revs.reverse != 1) {\n>      +-\t\twarning(_(\"some rev walking options will be overridden as \"\n>      +-\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n>      +-\t\t\t\"reverse\");\n>      +-\t\trevs.reverse = 1;\n>      ++\t{\n>      ++\t\tint desired_reverse = opts.revert ? 0 : 1;\n>      ++\t\tif (revs.reverse != desired_reverse) {\n>      ++\t\t\twarning(_(\"some rev walking options will be overridden as \"\n>      ++\t\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n>      ++\t\t\t\t\"reverse\");\n>      ++\t\t\trevs.reverse = desired_reverse;\n>      ++\t\t}\n>      + \t}\n>      + \tif (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {\n>      + \t\twarning(_(\"some rev walking options will be overridden as \"\n>       @@ builtin/replay.c: int cmd_replay(int argc,\n>        \t\tgoto cleanup;\n>        \n>      @@ replay.c\n>        #include \"strmap.h\"\n>        #include \"tree.h\"\n>        \n>      +-/*\n>      +- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but\n>      +- * do not want to use the_repository.\n>      +- */\n>      +-#define the_repository DO_NOT_USE_THE_REPOSITORY\n>       +enum replay_mode {\n>       +\tREPLAY_MODE_PICK,\n>       +\tREPLAY_MODE_REVERT,\n>       +};\n>      -+\n>      +\n>        static const char *short_commit_name(struct repository *repo,\n>        \t\t\t\t     struct commit *commit)\n>      - {\n>       @@ replay.c: static char *get_author(const char *message)\n>        \treturn NULL;\n>        }\n>      @@ replay.c: static char *get_author(const char *message)\n>       +\tsubject_len = find_commit_subject(message, &subject_start);\n>       +\tsubject = xmemdupz(subject_start, subject_len);\n>       +\n>      -+\tsequencer_format_revert_header(msg, subject, &commit->object.oid);\n>      ++\tsequencer_format_revert_message(repo, subject, commit,\n>      ++\t\t\t\t\tcommit->parents ? commit->parents->item : NULL,\n>      ++\t\t\t\t\tfalse, msg);\n>       +\n>       +\tfree(subject);\n>       +\trepo_unuse_commit_buffer(repo, commit, message);\n>      @@ replay.c: static void get_ref_information(struct repository *repo,\n>       +\t}\n>       +\t*onto = peel_committish(repo, *branch_name, option_name);\n>       +\tif (rinfo->positive_refexprs > 1)\n>      -+\t\tdie(_(\"cannot %s target with multiple sources because ordering would be ill-defined\"),\n>      -+\t\t    option_name + 2); /* skip \"--\" prefix */\n>      ++\t\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n>      ++\t\t      \"because the ordering would be ill-defined\"),\n>      ++\t\t    option_name);\n>       +}\n>       +\n>        static void set_up_replay_mode(struct repository *repo,\n>      @@ replay.c: static struct commit *pick_regular_commit(struct repository *repo,\n>       +\tmerge_opt->branch2 = NULL;\n>        \tif (!result->clean)\n>        \t\treturn NULL;\n>      --\t/* Drop commits that become empty */\n>      --\tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n>      -+\t/* Drop commits that become empty (only for picks) */\n>      -+\tif (mode == REPLAY_MODE_PICK &&\n>      -+\t    oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n>      + \t/* Drop commits that become empty */\n>      + \tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n>        \t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n>        \t\treturn replayed_base;\n>       -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n>      @@ replay.c: int replay_revisions(struct rev_info *revs,\n>        \n>        \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n>       -\t\t\t\t\t\t  onto, &merge_opt, &result);\n>      -+\t\t\t\t\t\t  onto, &merge_opt, &result, mode);\n>      ++\t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n>      ++\t\t\t\t\t\t  &merge_opt, &result, mode);\n>        \t\tif (!last_commit)\n>        \t\t\tbreak;\n>        \n>      @@ t/t3650-replay-basics.sh: test_expect_success 'no base or negative ref gives no-\n>        \ttest_must_fail git replay --advance=main --contained \\\n>        \t\ttopic1..topic2 2>actual &&\n>        \ttest_cmp expect actual\n>      + '\n>      +\n>      + test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n>      +-\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n>      ++\tcat >expect <<-\\EOF &&\n>      ++\tfatal: '\"'\"'--advance'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n>      ++\tEOF\n>      + \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n>      + \ttest_cmp expect actual\n>      + '\n>       @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '\n>        \ttest_grep \"invalid.*replay.refAction.*value\" error\n>        '\n>      @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '\n>       +'\n>       +\n>       +test_expect_success 'cannot revert with multiple sources' '\n>      -+\techo \"fatal: cannot revert target with multiple sources because ordering would be ill-defined\" >expect &&\n>      ++\tcat >expect <<-\\EOF &&\n>      ++\tfatal: '\"'\"'--revert'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n>      ++\tEOF\n>       +\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n>       +\ttest_cmp expect actual\n>       +'\n>      @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '\n>       +\t# Revert commits I and J\n>       +\tgit replay --revert topic4 topic4~2..topic4 &&\n>       +\n>      -+\t# Verify the revert commits were created\n>      ++\t# Verify the revert commits were created (newest-first ordering\n>      ++\t# means J is reverted first, then I on top)\n>       +\tgit log --format=%s -4 topic4 >actual &&\n>       +\tcat >expect <<-\\EOF &&\n>      -+\tRevert \"J\"\n>       +\tRevert \"I\"\n>      ++\tRevert \"J\"\n>       +\tJ\n>       +\tI\n>       +\tEOF\n>       +\ttest_cmp expect actual &&\n>       +\n>      -+\t# Verify commit message format includes hash\n>      ++\t# Verify commit message format includes hash (tip is Revert \"I\")\n>       +\ttest_commit_message topic4 <<-EOF &&\n>      -+\tRevert \"J\"\n>      ++\tRevert \"I\"\n>       +\n>      -+\tThis reverts commit $(git rev-parse J).\n>      ++\tThis reverts commit $(git rev-parse I).\n>       +\tEOF\n>       +\n>       +\t# Verify reflog message\n> \n> \n> base-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2\n> \n> \n\n"},{"id":"539146","messageId":"87pl53si56.fsf@iotcl.com","threadId":"64529","inReplyTo":"xmqqy0jv7ml9.fsf@gitster.g","subject":"Re: [PATCH v4 1/2] sequencer: extract revert message formatting into shared function","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-16T19:12:37Z","receivedAt":"2026-03-16T19:13:00Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> The logic for formatting revert commit messages (handling \"Revert\" and\n>> \"Reapply\" cases, appending \"This reverts commit <ref>.\", and handling\n>> merge-parent references) currently lives inline in do_pick_commit().\n>> The upcoming replay --revert mode needs to reuse this logic.\n>>\n>> Extract all of this into a new sequencer_format_revert_message()\n>> function. The function takes a repository, the subject line, commit,\n>> parent, a use_commit_reference flag, and the output strbuf. It handles\n>> both regular reverts (\"Revert \"<subject>\"\") and revert-of-revert cases\n>> (\"Reapply \"<subject>\"\"), and uses refer_to_commit() internally to\n>> format the commit reference.\n>>\n>> Update refer_to_commit() to take a struct repository parameter instead\n>> of relying on the_repository, and a bool instead of reading from\n>> replay_opts directly. This makes it usable from the new shared function\n>> without pulling in sequencer-specific state.\n\nI wouldn't mind if you put removing the use of `the_repository` in a\nseparate commit.\n\n>>\n>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n>> ---\n>>  sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------\n>>  sequencer.h | 14 ++++++++++\n>>  2 files changed, 60 insertions(+), 32 deletions(-)\n>\n> Relative to the previous round, sequencer_format_revert_message()\n> that does a bit more than sequencer_format_revert_header() we had\n> makes the existing code easier to follow, even though the total\n> codeflow amounts to the same thing.  A new caller that will use the\n> function now has to do less.\n>\n> Also, even though this is an internal implementation detail,\n> changing the list of parameters refer_to_commit() takes makes it\n> easier to understand which part of the replay_opts structure is used\n> (i.e., we only care about \"do we use the commit reference, or not?\"\n> bit, and we have no interest in any other members of the struct).\n\nPatrick suggested[1] to use flags, but it makes sense to keep it simple\nfor now.\n\n[1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/\n\n>\n>> +/*\n>> + * Formats a complete revert commit message following standard Git conventions.\n>> + * Handles regular reverts (\"Revert \\\"<subject>\\\"\"), revert of revert cases\n>> + * (\"Reapply \\\"<subject>\\\"\"), and the --reference style. Appends \"This reverts\n>> + * commit <ref>.\" using either the abbreviated or full commit reference\n>> + * depending on use_commit_reference. Also handles merge-parent references.\n>> + */\n\nI think you're trying to put too much in the comments here. I would\nsuggest to be a bit more concise. Maybe something like:\n\n/*\n * Format a revert commit message with appropriate \"Revert\" or \"Reapply\"\n * prefix and \"This reverts commit <ref>.\" body. When use_commit_reference\n * is set, <ref> is an abbreviated hash with subject and date.\n */\n\n>> +void sequencer_format_revert_message(struct repository *r,\n>> +\t\t\t\t     const char *subject,\n>> +\t\t\t\t     const struct commit *commit,\n>> +\t\t\t\t     const struct commit *parent,\n>> +\t\t\t\t     bool use_commit_reference,\n>> +\t\t\t\t     struct strbuf *message);\n>> +\n>>  #endif /* SEQUENCER_H */\n>\n> OK.\n>\n\n-- \nCheers,\nToon\n"},{"id":"539150","messageId":"87ms07sgaq.fsf@iotcl.com","threadId":"64529","inReplyTo":"20260313054035.26605-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 2/2] replay: add --revert mode to reverse commit changes","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-16T19:52:29Z","receivedAt":"2026-03-16T19:52:43Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> Add a `--revert <branch>` mode to git replay that undoes the changes\n> introduced by the specified commits. Like --onto and --advance, --revert\n> is a standalone mode: it takes a branch argument and updates that branch\n> with the newly created revert commits.\n>\n> At GitLab, we need this in Gitaly for reverting commits directly on bare\n> repositories without requiring a working tree checkout.\n>\n> The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick\n> and revert are just the same three-way merge with swapped arguments:\n>\n>   - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n>   - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n>\n> We swap the base and pickme trees passed to merge_incore_nonrecursive()\n> to reverse the diff direction.\n>\n> Reverts are processed newest-first (matching git revert behavior) to\n> reduce conflicts by peeling off changes from the top. Each revert\n> builds on the result of the previous one via the last_commit fallback\n> in the main replay loop, rather than relying on the parent-mapping\n> used for cherry-pick.\n>\n> Revert commit messages follow the usual git revert conventions: prefixed\n> with \"Revert\" (or \"Reapply\" when reverting a revert), and including\n> \"This reverts commit <hash>.\". The author is set to the current user\n> rather than preserving the original author, matching git revert behavior.\n>\n> Helped-by: Christian Couder <christian.couder@gmail.com>\n> Helped-by: Patrick Steinhardt <ps@pks.im>\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\n> Helped-by: Junio C Hamano <gitster@pobox.com>\n> Helped-by: Toon Claes <toon@iotcl.com>\n> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> ---\n>  Documentation/git-replay.adoc |  43 ++++++++-\n>  builtin/replay.c              |  46 ++++++----\n>  replay.c                      | 165 ++++++++++++++++++++++++----------\n>  replay.h                      |  11 ++-\n>  t/t3650-replay-basics.sh      | 114 +++++++++++++++++++++--\n>  5 files changed, 304 insertions(+), 75 deletions(-)\n>\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index 8d696ce3ab..6698cfc047 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n\nThe ellipsis (...) should be dropped from revision-range. They were\nremoved not so long ago in 136f86abc0 (Documentation/git-replay.adoc:\nfix errors around revision range, 2025-11-29).\n\n>  \n>  DESCRIPTION\n>  -----------\n> @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to\n>  point at the tip of the resulting history. This is different from `--onto`,\n>  which uses the target only as a starting point without updating it.\n>  \n> +--revert <branch>::\n> +\tStarting point at which to create the reverted commits; must be a\n> +\tbranch name.\n> ++\n> +When `--revert` is specified, the commits in the revision range are reverted\n> +(their changes are undone) and the reverted commits are created on top of\n> +<branch>. The <branch> is then updated to point at the new commits. This is\n> +the same as running `git revert <revision-range>` but does not update the\n> +working tree.\n> ++\n> +The commit messages follow `git revert` conventions: they are prefixed with\n> +\"Revert\" and include \"This reverts commit <hash>.\" When reverting a commit\n> +whose message starts with \"Revert\", the new message uses \"Reapply\" instead.\n> +Unlike cherry-pick which preserves the original author, revert commits use\n> +the current user as the author, matching the behavior of `git revert`.\n> ++\n> +This option is mutually exclusive with `--onto` and `--advance`. It is also\n> +incompatible with `--contained` (which is a modifier for `--onto` only).\n> +\n>  --contained::\n>  \tUpdate all branches that point at commits in\n>  \t<revision-range>. Requires `--onto`.\n> @@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to\n>  \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n>  \n>  where the number of refs updated depends on the arguments passed and\n> -the shape of the history being replayed.  When using `--advance`, the\n> -number of refs updated is always one, but for `--onto`, it can be one\n> -or more (rebasing multiple branches simultaneously is supported).\n> +the shape of the history being replayed.  When using `--advance` or\n> +`--revert`, the number of refs updated is always one, but for `--onto`,\n> +it can be one or more (rebasing multiple branches simultaneously is\n> +supported).\n>  \n>  There is no stderr output on conflicts; see the <<exit-status,EXIT\n>  STATUS>> section below.\n> @@ -152,6 +172,21 @@ all commits they have since `base`, playing them on top of\n>  `origin/main`. These three branches may have commits on top of `base`\n>  that they have in common, but that does not need to be the case.\n>  \n> +To revert commits on a branch:\n> +\n> +------------\n> +$ git replay --revert main topic~2..topic\n> +------------\n> +\n> +This reverts the last two commits from `topic`, creating revert commits on\n> +top of `main`, and updates `main` to point at the result. This is useful when\n> +commits from `topic` were previously merged or cherry-picked into `main` and\n> +need to be undone.\n> +\n> +NOTE: For reverting an entire merge request as a single commit (rather than\n> +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n> +which can avoid unnecessary merge conflicts.\n> +\n>  GIT\n>  ---\n>  Part of the linkgit:git[1] suite\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 2cdde830a8..fe69f6f8ce 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -83,8 +83,8 @@ int cmd_replay(int argc,\n>  \n>  \tconst char *const replay_usage[] = {\n>  \t\tN_(\"(EXPERIMENTAL!) git replay \"\n> -\t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n> -\t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n> +\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n> +\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n>  \t\tNULL\n>  \t};\n>  \tstruct option replay_options[] = {\n> @@ -96,6 +96,9 @@ int cmd_replay(int argc,\n>  \t\t\t   N_(\"replay onto given commit\")),\n>  \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n>  \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n> +\t\tOPT_STRING(0, \"revert\", &opts.revert,\n> +\t\t\t   N_(\"branch\"),\n> +\t\t\t   N_(\"revert commits onto given branch\")),\n>  \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n>  \t\t\t   N_(\"mode\"),\n>  \t\t\t   N_(\"control ref update behavior (update|print)\")),\n> @@ -105,15 +108,17 @@ int cmd_replay(int argc,\n>  \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n>  \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n>  \n> -\tif (!opts.onto && !opts.advance) {\n> -\t\terror(_(\"option --onto or --advance is mandatory\"));\n> +\t/* Exactly one mode must be specified */\n> +\tif (!opts.onto && !opts.advance && !opts.revert) {\n> +\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n>  \t\tusage_with_options(replay_usage, replay_options);\n>  \t}\n>  \n> -\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n> -\t\t\t\t  opts.contained, \"--contained\");\n> -\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n> -\t\t\t\t  !!opts.onto, \"--onto\");\n> +\tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n> +\t\t\t\t  !!opts.advance, \"--advance\",\n> +\t\t\t\t  !!opts.revert, \"--revert\");\n> +\tif (opts.contained && !opts.onto)\n> +\t\tdie(_(\"--contained requires --onto\"));\n\nI think it would be more clear if you say they cannot be used together:\n\n\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n\t\t\t\t  opts.contained, \"--contained\");\n\tdie_for_incompatible_opt2(!!opts.revert, \"--revert\",\n\t\t\t\t  opts.contained, \"--contained\");\n\n\n>  \t/* Parse ref action mode from command line or config */\n>  \tref_mode = get_ref_action_mode(repo, ref_action);\n> @@ -129,7 +134,13 @@ int cmd_replay(int argc,\n>  \t * some options changing these values if we think they could\n>  \t * be useful.\n>  \t */\n> -\trevs.reverse = 1;\n> +\t/*\n> +\t * Cherry-pick/rebase need oldest-first ordering so that each\n> +\t * replayed commit can build on its already-replayed parent.\n> +\t * Revert needs newest-first ordering (like git revert) to\n> +\t * reduce conflicts by peeling off changes from the top.\n> +\t */\n> +\trevs.reverse = opts.revert ? 0 : 1;\n>  \trevs.sort_order = REV_SORT_IN_GRAPH_ORDER;\n>  \trevs.topo_order = 1;\n>  \trevs.simplify_history = 0;\n> @@ -144,11 +155,14 @@ int cmd_replay(int argc,\n>  \t * Detect and warn if we override some user specified rev\n>  \t * walking options.\n>  \t */\n> -\tif (revs.reverse != 1) {\n> -\t\twarning(_(\"some rev walking options will be overridden as \"\n> -\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n> -\t\t\t\"reverse\");\n> -\t\trevs.reverse = 1;\n> +\t{\n\nDo we want to keep these braces?\n\n> +\t\tint desired_reverse = opts.revert ? 0 : 1;\n> +\t\tif (revs.reverse != desired_reverse) {\n> +\t\t\twarning(_(\"some rev walking options will be overridden as \"\n> +\t\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n> +\t\t\t\t\"reverse\");\n> +\t\t\trevs.reverse = desired_reverse;\n> +\t\t}\n>  \t}\n>  \tif (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {\n>  \t\twarning(_(\"some rev walking options will be overridden as \"\n> @@ -174,7 +188,9 @@ int cmd_replay(int argc,\n>  \t\tgoto cleanup;\n>  \n>  \t/* Build reflog message */\n> -\tif (opts.advance) {\n> +\tif (opts.revert) {\n> +\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", opts.revert);\n> +\t} else if (opts.advance) {\n>  \t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n>  \t} else {\n>  \t\tstruct object_id oid;\n> diff --git a/replay.c b/replay.c\n> index a63f6714c4..199066f6b3 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -8,14 +8,14 @@\n>  #include \"refs.h\"\n>  #include \"replay.h\"\n>  #include \"revision.h\"\n> +#include \"sequencer.h\"\n>  #include \"strmap.h\"\n>  #include \"tree.h\"\n>  \n> -/*\n> - * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but\n> - * do not want to use the_repository.\n> - */\n> -#define the_repository DO_NOT_USE_THE_REPOSITORY\n\nWhy are you removing this? We're still setting\nUSE_THE_REPOSITORY_VARIABLE, and not using 'the_repository' in this file?\n\n> +enum replay_mode {\n> +\tREPLAY_MODE_PICK,\n> +\tREPLAY_MODE_REVERT,\n> +};\n>  \n>  static const char *short_commit_name(struct repository *repo,\n>  \t\t\t\t     struct commit *commit)\n> @@ -50,15 +50,37 @@ static char *get_author(const char *message)\n>  \treturn NULL;\n>  }\n>  \n> +static void generate_revert_message(struct strbuf *msg,\n> +\t\t\t\t    struct commit *commit,\n> +\t\t\t\t    struct repository *repo)\n> +{\n> +\tconst char *out_enc = get_commit_output_encoding();\n> +\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n> +\tconst char *subject_start;\n> +\tint subject_len;\n> +\tchar *subject;\n> +\n> +\tsubject_len = find_commit_subject(message, &subject_start);\n> +\tsubject = xmemdupz(subject_start, subject_len);\n> +\n> +\tsequencer_format_revert_message(repo, subject, commit,\n> +\t\t\t\t\tcommit->parents ? commit->parents->item : NULL,\n> +\t\t\t\t\tfalse, msg);\n> +\n> +\tfree(subject);\n> +\trepo_unuse_commit_buffer(repo, commit, message);\n> +}\n> +\n>  static struct commit *create_commit(struct repository *repo,\n>  \t\t\t\t    struct tree *tree,\n>  \t\t\t\t    struct commit *based_on,\n> -\t\t\t\t    struct commit *parent)\n> +\t\t\t\t    struct commit *parent,\n> +\t\t\t\t    enum replay_mode mode)\n>  {\n>  \tstruct object_id ret;\n>  \tstruct object *obj = NULL;\n>  \tstruct commit_list *parents = NULL;\n> -\tchar *author;\n> +\tchar *author = NULL;\n>  \tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n>  \tstruct commit_extra_header *extra = NULL;\n>  \tstruct strbuf msg = STRBUF_INIT;\n> @@ -70,9 +92,16 @@ static struct commit *create_commit(struct repository *repo,\n>  \n>  \tcommit_list_insert(parent, &parents);\n>  \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n> -\tfind_commit_subject(message, &orig_message);\n> -\tstrbuf_addstr(&msg, orig_message);\n> -\tauthor = get_author(message);\n> +\tif (mode == REPLAY_MODE_REVERT) {\n> +\t\tgenerate_revert_message(&msg, based_on, repo);\n> +\t\t/* For revert, use current user as author (NULL = use default) */\n> +\t} else if (mode == REPLAY_MODE_PICK) {\n> +\t\tfind_commit_subject(message, &orig_message);\n> +\t\tstrbuf_addstr(&msg, orig_message);\n> +\t\tauthor = get_author(message);\n> +\t} else {\n> +\t\tBUG(\"unexpected replay mode %d\", mode);\n> +\t}\n>  \treset_ident_date();\n>  \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n>  \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n> @@ -153,11 +182,35 @@ static void get_ref_information(struct repository *repo,\n>  \t}\n>  }\n>  \n> +static void set_up_branch_mode(struct repository *repo,\n> +\t\t\t       char **branch_name,\n> +\t\t\t       const char *option_name,\n> +\t\t\t       struct ref_info *rinfo,\n> +\t\t\t       struct commit **onto)\n> +{\n> +\tstruct object_id oid;\n> +\tchar *fullname = NULL;\n> +\n> +\tif (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),\n> +\t\t\t  &oid, &fullname, 0) == 1) {\n> +\t\tfree(*branch_name);\n> +\t\t*branch_name = fullname;\n> +\t} else {\n> +\t\tdie(_(\"argument to %s must be a reference\"), option_name);\n> +\t}\n> +\t*onto = peel_committish(repo, *branch_name, option_name);\n> +\tif (rinfo->positive_refexprs > 1)\n> +\t\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n> +\t\t      \"because the ordering would be ill-defined\"),\n> +\t\t    option_name);\n> +}\n> +\n>  static void set_up_replay_mode(struct repository *repo,\n>  \t\t\t       struct rev_cmdline_info *cmd_info,\n>  \t\t\t       const char *onto_name,\n>  \t\t\t       bool *detached_head,\n>  \t\t\t       char **advance_name,\n> +\t\t\t       char **revert_name,\n>  \t\t\t       struct commit **onto,\n>  \t\t\t       struct strset **update_refs)\n>  {\n> @@ -172,9 +225,6 @@ static void set_up_replay_mode(struct repository *repo,\n>  \tif (!rinfo.positive_refexprs)\n>  \t\tdie(_(\"need some commits to replay\"));\n>  \n> -\tif (!onto_name == !*advance_name)\n> -\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n> -\n>  \tif (onto_name) {\n>  \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n>  \t\tif (rinfo.positive_refexprs <\n> @@ -183,23 +233,12 @@ static void set_up_replay_mode(struct repository *repo,\n>  \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n>  \t\t**update_refs = rinfo.positive_refs;\n>  \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n> +\t} else if (*advance_name) {\n> +\t\tset_up_branch_mode(repo, advance_name, \"--advance\", &rinfo, onto);\n> +\t} else if (*revert_name) {\n> +\t\tset_up_branch_mode(repo, revert_name, \"--revert\", &rinfo, onto);\n>  \t} else {\n> -\t\tstruct object_id oid;\n> -\t\tchar *fullname = NULL;\n> -\n> -\t\tif (!*advance_name)\n> -\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n> -\n> -\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n> -\t\t\t     &oid, &fullname, 0) == 1) {\n> -\t\t\tfree(*advance_name);\n> -\t\t\t*advance_name = fullname;\n> -\t\t} else {\n> -\t\t\tdie(_(\"argument to --advance must be a reference\"));\n> -\t\t}\n> -\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n> -\t\tif (rinfo.positive_refexprs > 1)\n> -\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n> +\t\tBUG(\"expected one of onto_name, *advance_name, or *revert_name\");\n>  \t}\n>  \tstrset_clear(&rinfo.negative_refs);\n>  \tstrset_clear(&rinfo.positive_refs);\n> @@ -220,7 +259,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>  \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n>  \t\t\t\t\t  struct commit *onto,\n>  \t\t\t\t\t  struct merge_options *merge_opt,\n> -\t\t\t\t\t  struct merge_result *result)\n> +\t\t\t\t\t  struct merge_result *result,\n> +\t\t\t\t\t  enum replay_mode mode)\n>  {\n>  \tstruct commit *base, *replayed_base;\n>  \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n> @@ -232,25 +272,45 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>  \tpickme_tree = repo_get_commit_tree(repo, pickme);\n>  \tbase_tree = repo_get_commit_tree(repo, base);\n>  \n> -\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> -\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n> -\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n> -\n> -\tmerge_incore_nonrecursive(merge_opt,\n> -\t\t\t\t  base_tree,\n> -\t\t\t\t  replayed_base_tree,\n> -\t\t\t\t  pickme_tree,\n> -\t\t\t\t  result);\n> -\n> -\tfree((char*)merge_opt->ancestor);\n> +\tif (mode == REPLAY_MODE_PICK) {\n> +\t\t/* Cherry-pick: normal order */\n> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> +\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n> +\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n> +\n> +\t\tmerge_incore_nonrecursive(merge_opt,\n> +\t\t\t\t\t  base_tree,\n> +\t\t\t\t\t  replayed_base_tree,\n> +\t\t\t\t\t  pickme_tree,\n> +\t\t\t\t\t  result);\n> +\n> +\t\tfree((char *)merge_opt->ancestor);\n> +\t} else if (mode == REPLAY_MODE_REVERT) {\n> +\t\t/* Revert: swap base and pickme to reverse the diff */\n> +\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> +\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n> +\t\tmerge_opt->ancestor = pickme_name;\n> +\n> +\t\tmerge_incore_nonrecursive(merge_opt,\n> +\t\t\t\t\t  pickme_tree,\n> +\t\t\t\t\t  replayed_base_tree,\n> +\t\t\t\t\t  base_tree,\n> +\t\t\t\t\t  result);\n> +\n> +\t\tfree((char *)merge_opt->branch2);\n> +\t} else {\n> +\t\tBUG(\"unexpected replay mode %d\", mode);\n> +\t}\n>  \tmerge_opt->ancestor = NULL;\n> +\tmerge_opt->branch2 = NULL;\n\nIt took me a while to understand the ownership of these buffers,\nespecially those returned by short_commit_name(). But it seems to call\nrepo_find_unique_abbrev() which has 4 buffers which it cycles through.\n\nSo this seems to be okay (this was also not changed in the MODE_PICK\npath. \n\n>  \tif (!result->clean)\n>  \t\treturn NULL;\n>  \t/* Drop commits that become empty */\n>  \tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n>  \t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n>  \t\treturn replayed_base;\n> -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n> +\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n>  }\n>  \n>  void replay_result_release(struct replay_result *result)\n> @@ -287,11 +347,16 @@ int replay_revisions(struct rev_info *revs,\n>  \t};\n>  \tbool detached_head;\n>  \tchar *advance;\n> +\tchar *revert;\n> +\tenum replay_mode mode = REPLAY_MODE_PICK;\n>  \tint ret;\n>  \n>  \tadvance = xstrdup_or_null(opts->advance);\n> +\trevert = xstrdup_or_null(opts->revert);\n> +\tif (revert)\n> +\t\tmode = REPLAY_MODE_REVERT;\n>  \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n> -\t\t\t   &detached_head, &advance, &onto, &update_refs);\n> +\t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n>  \n>  \t/* FIXME: Should allow replaying commits with the first as a root commit */\n>  \n> @@ -315,7 +380,8 @@ int replay_revisions(struct rev_info *revs,\n>  \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n>  \n>  \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n> -\t\t\t\t\t\t  onto, &merge_opt, &result);\n> +\t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n> +\t\t\t\t\t\t  &merge_opt, &result, mode);\n>  \t\tif (!last_commit)\n>  \t\t\tbreak;\n>  \n> @@ -327,7 +393,7 @@ int replay_revisions(struct rev_info *revs,\n>  \t\tkh_value(replayed_commits, pos) = last_commit;\n>  \n>  \t\t/* Update any necessary branches */\n> -\t\tif (advance)\n> +\t\tif (advance || revert)\n>  \t\t\tcontinue;\n>  \n>  \t\tfor (decoration = get_name_decoration(&commit->object);\n> @@ -361,11 +427,13 @@ int replay_revisions(struct rev_info *revs,\n>  \t\tgoto out;\n>  \t}\n>  \n> -\t/* In --advance mode, advance the target ref */\n> -\tif (advance)\n> -\t\treplay_result_queue_update(out, advance,\n> +\t/* In --advance or --revert mode, update the target ref */\n> +\tif (advance || revert) {\n> +\t\tconst char *ref = advance ? advance : revert;\n> +\t\treplay_result_queue_update(out, ref,\n>  \t\t\t\t\t   &onto->object.oid,\n>  \t\t\t\t\t   &last_commit->object.oid);\n> +\t}\n>  \n>  \tret = 0;\n>  \n> @@ -377,5 +445,6 @@ int replay_revisions(struct rev_info *revs,\n>  \tkh_destroy_oid_map(replayed_commits);\n>  \tmerge_finalize(&merge_opt, &result);\n>  \tfree(advance);\n> +\tfree(revert);\n>  \treturn ret;\n>  }\n> diff --git a/replay.h b/replay.h\n> index d8407dc7f7..e916a5f975 100644\n> --- a/replay.h\n> +++ b/replay.h\n> @@ -13,7 +13,7 @@ struct replay_revisions_options {\n>  \t/*\n>  \t * Starting point at which to create the new commits; must be a branch\n>  \t * name. The branch will be updated to point to the rewritten commits.\n> -\t * This option is mutually exclusive with `onto`.\n> +\t * This option is mutually exclusive with `onto` and `revert`.\n>  \t */\n>  \tconst char *advance;\n>  \n> @@ -22,7 +22,14 @@ struct replay_revisions_options {\n>  \t * committish. References pointing at decendants of `onto` will be\n>  \t * updated to point to the new commits.\n>  \t */\n> -\t const char *onto;\n> +\tconst char *onto;\n> +\n> +\t/*\n> +\t * Starting point at which to create revert commits; must be a branch\n> +\t * name. The branch will be updated to point to the revert commits.\n> +\t * This option is mutually exclusive with `onto` and `advance`.\n> +\t */\n> +\tconst char *revert;\n>  \n>  \t/*\n>  \t * Update branches that point at commits in the given revision range.\n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index a03f8f9293..0c1e03e0fb 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> -test_expect_success 'option --onto or --advance is mandatory' '\n> -\techo \"error: option --onto or --advance is mandatory\" >expect &&\n> +test_expect_success 'exactly one of --onto, --advance, or --revert is required' '\n> +\techo \"error: exactly one of --onto, --advance, or --revert is required\" >expect &&\n>  \ttest_might_fail git replay -h >>expect &&\n>  \ttest_must_fail git replay topic1..topic2 2>actual &&\n>  \ttest_cmp expect actual\n> @@ -87,16 +87,17 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err\n>  \ttest_cmp expect actual\n>  '\n>  \n> -test_expect_success 'options --advance and --contained cannot be used together' '\n> -\tprintf \"fatal: options ${SQ}--advance${SQ} \" >expect &&\n> -\tprintf \"and ${SQ}--contained${SQ} cannot be used together\\n\" >>expect &&\n> +test_expect_success '--contained requires --onto' '\n> +\techo \"fatal: --contained requires --onto\" >expect &&\n>  \ttest_must_fail git replay --advance=main --contained \\\n>  \t\ttopic1..topic2 2>actual &&\n>  \ttest_cmp expect actual\n>  '\n>  \n>  test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n> -\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n> +\tcat >expect <<-\\EOF &&\n> +\tfatal: '\"'\"'--advance'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n> +\tEOF\n>  \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n>  \ttest_cmp expect actual\n>  '\n> @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' '\n>  \ttest_grep \"invalid.*replay.refAction.*value\" error\n>  '\n>  \n> +test_expect_success 'argument to --revert must be a reference' '\n> +\techo \"fatal: argument to --revert must be a reference\" >expect &&\n> +\toid=$(git rev-parse main) &&\n> +\ttest_must_fail git replay --revert=$oid topic1..topic2 2>actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'cannot revert with multiple sources' '\n> +\tcat >expect <<-\\EOF &&\n> +\tfatal: '\"'\"'--revert'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n> +\tEOF\n> +\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'using replay --revert to revert commits' '\n> +\t# Reuse existing topic4 branch (has commits I and J on top of main)\n> +\tSTART=$(git rev-parse topic4) &&\n> +\ttest_when_finished \"git branch -f topic4 $START\" &&\n> +\n> +\t# Revert commits I and J\n> +\tgit replay --revert topic4 topic4~2..topic4 &&\n> +\n> +\t# Verify the revert commits were created (newest-first ordering\n> +\t# means J is reverted first, then I on top)\n> +\tgit log --format=%s -4 topic4 >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\tRevert \"I\"\n> +\tRevert \"J\"\n> +\tJ\n> +\tI\n> +\tEOF\n> +\ttest_cmp expect actual &&\n> +\n> +\t# Verify commit message format includes hash (tip is Revert \"I\")\n> +\ttest_commit_message topic4 <<-EOF &&\n> +\tRevert \"I\"\n> +\n> +\tThis reverts commit $(git rev-parse I).\n> +\tEOF\n> +\n> +\t# Verify reflog message\n> +\tgit reflog topic4 -1 --format=%gs >reflog-msg &&\n> +\techo \"replay --revert topic4\" >expect-reflog &&\n> +\ttest_cmp expect-reflog reflog-msg\n> +'\n> +\n> +test_expect_success 'using replay --revert in bare repo' '\n> +\t# Reuse existing topic4 in bare repo\n> +\tSTART=$(git -C bare rev-parse topic4) &&\n> +\ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n> +\n> +\t# Revert commit J in bare repo\n> +\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n> +\n> +\t# Verify revert was created\n> +\tgit -C bare log -1 --format=%s topic4 >actual &&\n> +\techo \"Revert \\\"J\\\"\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'revert of revert uses Reapply' '\n> +\t# Use topic4 and first revert J, then revert the revert\n> +\tSTART=$(git rev-parse topic4) &&\n> +\ttest_when_finished \"git branch -f topic4 $START\" &&\n> +\n> +\t# First revert J\n> +\tgit replay --revert topic4 topic4~1..topic4 &&\n> +\tREVERT_J=$(git rev-parse topic4) &&\n> +\n> +\t# Now revert the revert - should become Reapply\n> +\tgit replay --revert topic4 topic4~1..topic4 &&\n> +\n> +\t# Verify Reapply prefix and message format\n> +\ttest_commit_message topic4 <<-EOF\n> +\tReapply \"J\"\n> +\n> +\tThis reverts commit $REVERT_J.\n> +\tEOF\n> +'\n> +\n> +test_expect_success 'git replay --revert with conflict' '\n> +\t# conflict branch has C.conflict which conflicts with topic1s C\n> +\ttest_expect_code 1 git replay --revert conflict B..topic1\n> +'\n> +\n> +test_expect_success 'git replay --revert incompatible with --contained' '\n> +\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n> +\ttest_grep \"requires --onto\" error\n> +'\n> +\n> +test_expect_success 'git replay --revert incompatible with --onto' '\n> +\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n> +\ttest_grep \"cannot be used together\" error\n> +'\n> +\n> +test_expect_success 'git replay --revert incompatible with --advance' '\n> +\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n> +\ttest_grep \"cannot be used together\" error\n> +'\n> +\n>  test_done\n> -- \n> 2.51.0\n>\n>\n\n-- \nCheers,\nToon\n"},{"id":"539151","messageId":"87ikavsg8f.fsf@iotcl.com","threadId":"64529","inReplyTo":"20260313054035.26605-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 0/2] replay: add --revert mode to reverse commit changes","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-16T19:53:52Z","receivedAt":"2026-03-16T19:54:08Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> Hi,\n>\n> git replay currently supports cherry-picking (--advance) and rebasing\n> (--onto), but not reverting. We need this at GitLab for Gitaly to\n> reverse commits directly on bare repositories without a checkout.\n>\n> The approach is the same as sequencer.c -- cherry-pick and revert are\n> just the same three-way merge with swapped arguments. We swap the base\n> and pickme trees passed to merge_incore_nonrecursive() to reverse the\n> diff direction.\n>\n> Patch 1 extracts the full revert message formatting logic into a new\n> sequencer_format_revert_message() function, following Phillip's\n> suggestion to move everything into one shared function rather than\n> just the header. refer_to_commit() is updated to take a struct\n> repository and a bool instead of replay_opts so it works outside the\n> sequencer.\n>\n> Patch 2 adds --revert <branch> as a standalone mode. Reverts are\n> processed newest-first (matching git revert) to reduce conflicts by\n> peeling off changes from the top.\n>\n> The series is based on top of d181b9354c (The 13th batch, 2026-03-07).\n>\n> CI: https://gitlab.com/gitlab-org/git/-/pipelines/2329880894\n> The msvc-meson / Chocolatey failures are pre-existing infrastructure\n> issues unrelated to this series.\n>\n> Changes in v4:\n> - Replaced sequencer_format_revert_header() with a more complete\n>   sequencer_format_revert_message() that handles everything: subject\n>   prefix, commit reference via refer_to_commit(), and merge-parent\n>   references -- per Phillip\n> - Updated refer_to_commit() signature to take (struct repository *r,\n>   bool use_commit_reference) instead of (struct replay_opts *opts)\n> - Reverts are now newest-first (revs.reverse = 0 for --revert),\n>   chaining on last_commit rather than the parent mapping\n> - Changed doc example to cross-branch scenario and restored the\n>   merge-tree NOTE\n> - Updated error message format to \"'--revert' cannot be used with\n>   multiple revision ranges...\" (and same for --advance)\n> - Empty revert commits are now dropped, consistent with cherry-pick\n> - Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/\n> - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/\n> - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/\n>\n> Thanks,\n> Siddharth\n\nI've added various nitpicks here and there. Overall nothing really\nblocking for a reroll I think, but maybe the combination of them all is.\n\n-- \nCheers,\nToon\n"},{"id":"539210","messageId":"dd4aeb75-c4db-4feb-8075-e65d15bd04d3@gmail.com","threadId":"64529","inReplyTo":"87ms07sgaq.fsf@iotcl.com","subject":"Re: [PATCH v4 2/2] replay: add --revert mode to reverse commit changes","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-03-17T10:11:45Z","receivedAt":"2026-03-17T10:11:49Z","isPatch":true,"body":"On 16/03/2026 19:52, Toon Claes wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n> \n>> @@ -144,11 +155,14 @@ int cmd_replay(int argc,\n>>   \t * Detect and warn if we override some user specified rev\n>>   \t * walking options.\n>>   \t */\n>> -\tif (revs.reverse != 1) {\n>> -\t\twarning(_(\"some rev walking options will be overridden as \"\n>> -\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n>> -\t\t\t\"reverse\");\n>> -\t\trevs.reverse = 1;\n>> +\t{\n> \n> Do we want to keep these braces?\n\nThat's a good point which I think is worth a re-roll. It would be better \nto move the declaration of \"desired_reverse\" up so that we have\n\n\tint desired_reverse = !opts.revert;\n\t...\n\trevs.reverse = desired_reverse\n\t...\n\tif (revs.reverse != desired_reverse)\n\t\twarning(...);\n\nThat way we know we are using the same expected value for revs.reverse \nwhen we set it and when we check it hasn't changed later.\n\nThanks\n\nPhillip\n\n>> +\t\tint desired_reverse = opts.revert ? 0 : 1;\n>> +\t\tif (revs.reverse != desired_reverse) {\n>> +\t\t\twarning(_(\"some rev walking options will be overridden as \"\n>> +\t\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n>> +\t\t\t\t\"reverse\");\n>> +\t\t\trevs.reverse = desired_reverse;\n>> +\t\t}\n>>   \t}\n>>   \tif (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {\n>>   \t\twarning(_(\"some rev walking options will be overridden as \"\n>> @@ -174,7 +188,9 @@ int cmd_replay(int argc,\n>>   \t\tgoto cleanup;\n>>   \n>>   \t/* Build reflog message */\n>> -\tif (opts.advance) {\n>> +\tif (opts.revert) {\n>> +\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", opts.revert);\n>> +\t} else if (opts.advance) {\n>>   \t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n>>   \t} else {\n>>   \t\tstruct object_id oid;\n>> diff --git a/replay.c b/replay.c\n>> index a63f6714c4..199066f6b3 100644\n>> --- a/replay.c\n>> +++ b/replay.c\n>> @@ -8,14 +8,14 @@\n>>   #include \"refs.h\"\n>>   #include \"replay.h\"\n>>   #include \"revision.h\"\n>> +#include \"sequencer.h\"\n>>   #include \"strmap.h\"\n>>   #include \"tree.h\"\n>>   \n>> -/*\n>> - * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but\n>> - * do not want to use the_repository.\n>> - */\n>> -#define the_repository DO_NOT_USE_THE_REPOSITORY\n> \n> Why are you removing this? We're still setting\n> USE_THE_REPOSITORY_VARIABLE, and not using 'the_repository' in this file?\n> \n>> +enum replay_mode {\n>> +\tREPLAY_MODE_PICK,\n>> +\tREPLAY_MODE_REVERT,\n>> +};\n>>   \n>>   static const char *short_commit_name(struct repository *repo,\n>>   \t\t\t\t     struct commit *commit)\n>> @@ -50,15 +50,37 @@ static char *get_author(const char *message)\n>>   \treturn NULL;\n>>   }\n>>   \n>> +static void generate_revert_message(struct strbuf *msg,\n>> +\t\t\t\t    struct commit *commit,\n>> +\t\t\t\t    struct repository *repo)\n>> +{\n>> +\tconst char *out_enc = get_commit_output_encoding();\n>> +\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n>> +\tconst char *subject_start;\n>> +\tint subject_len;\n>> +\tchar *subject;\n>> +\n>> +\tsubject_len = find_commit_subject(message, &subject_start);\n>> +\tsubject = xmemdupz(subject_start, subject_len);\n>> +\n>> +\tsequencer_format_revert_message(repo, subject, commit,\n>> +\t\t\t\t\tcommit->parents ? commit->parents->item : NULL,\n>> +\t\t\t\t\tfalse, msg);\n>> +\n>> +\tfree(subject);\n>> +\trepo_unuse_commit_buffer(repo, commit, message);\n>> +}\n>> +\n>>   static struct commit *create_commit(struct repository *repo,\n>>   \t\t\t\t    struct tree *tree,\n>>   \t\t\t\t    struct commit *based_on,\n>> -\t\t\t\t    struct commit *parent)\n>> +\t\t\t\t    struct commit *parent,\n>> +\t\t\t\t    enum replay_mode mode)\n>>   {\n>>   \tstruct object_id ret;\n>>   \tstruct object *obj = NULL;\n>>   \tstruct commit_list *parents = NULL;\n>> -\tchar *author;\n>> +\tchar *author = NULL;\n>>   \tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n>>   \tstruct commit_extra_header *extra = NULL;\n>>   \tstruct strbuf msg = STRBUF_INIT;\n>> @@ -70,9 +92,16 @@ static struct commit *create_commit(struct repository *repo,\n>>   \n>>   \tcommit_list_insert(parent, &parents);\n>>   \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n>> -\tfind_commit_subject(message, &orig_message);\n>> -\tstrbuf_addstr(&msg, orig_message);\n>> -\tauthor = get_author(message);\n>> +\tif (mode == REPLAY_MODE_REVERT) {\n>> +\t\tgenerate_revert_message(&msg, based_on, repo);\n>> +\t\t/* For revert, use current user as author (NULL = use default) */\n>> +\t} else if (mode == REPLAY_MODE_PICK) {\n>> +\t\tfind_commit_subject(message, &orig_message);\n>> +\t\tstrbuf_addstr(&msg, orig_message);\n>> +\t\tauthor = get_author(message);\n>> +\t} else {\n>> +\t\tBUG(\"unexpected replay mode %d\", mode);\n>> +\t}\n>>   \treset_ident_date();\n>>   \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n>>   \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n>> @@ -153,11 +182,35 @@ static void get_ref_information(struct repository *repo,\n>>   \t}\n>>   }\n>>   \n>> +static void set_up_branch_mode(struct repository *repo,\n>> +\t\t\t       char **branch_name,\n>> +\t\t\t       const char *option_name,\n>> +\t\t\t       struct ref_info *rinfo,\n>> +\t\t\t       struct commit **onto)\n>> +{\n>> +\tstruct object_id oid;\n>> +\tchar *fullname = NULL;\n>> +\n>> +\tif (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),\n>> +\t\t\t  &oid, &fullname, 0) == 1) {\n>> +\t\tfree(*branch_name);\n>> +\t\t*branch_name = fullname;\n>> +\t} else {\n>> +\t\tdie(_(\"argument to %s must be a reference\"), option_name);\n>> +\t}\n>> +\t*onto = peel_committish(repo, *branch_name, option_name);\n>> +\tif (rinfo->positive_refexprs > 1)\n>> +\t\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n>> +\t\t      \"because the ordering would be ill-defined\"),\n>> +\t\t    option_name);\n>> +}\n>> +\n>>   static void set_up_replay_mode(struct repository *repo,\n>>   \t\t\t       struct rev_cmdline_info *cmd_info,\n>>   \t\t\t       const char *onto_name,\n>>   \t\t\t       bool *detached_head,\n>>   \t\t\t       char **advance_name,\n>> +\t\t\t       char **revert_name,\n>>   \t\t\t       struct commit **onto,\n>>   \t\t\t       struct strset **update_refs)\n>>   {\n>> @@ -172,9 +225,6 @@ static void set_up_replay_mode(struct repository *repo,\n>>   \tif (!rinfo.positive_refexprs)\n>>   \t\tdie(_(\"need some commits to replay\"));\n>>   \n>> -\tif (!onto_name == !*advance_name)\n>> -\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n>> -\n>>   \tif (onto_name) {\n>>   \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n>>   \t\tif (rinfo.positive_refexprs <\n>> @@ -183,23 +233,12 @@ static void set_up_replay_mode(struct repository *repo,\n>>   \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n>>   \t\t**update_refs = rinfo.positive_refs;\n>>   \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n>> +\t} else if (*advance_name) {\n>> +\t\tset_up_branch_mode(repo, advance_name, \"--advance\", &rinfo, onto);\n>> +\t} else if (*revert_name) {\n>> +\t\tset_up_branch_mode(repo, revert_name, \"--revert\", &rinfo, onto);\n>>   \t} else {\n>> -\t\tstruct object_id oid;\n>> -\t\tchar *fullname = NULL;\n>> -\n>> -\t\tif (!*advance_name)\n>> -\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n>> -\n>> -\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n>> -\t\t\t     &oid, &fullname, 0) == 1) {\n>> -\t\t\tfree(*advance_name);\n>> -\t\t\t*advance_name = fullname;\n>> -\t\t} else {\n>> -\t\t\tdie(_(\"argument to --advance must be a reference\"));\n>> -\t\t}\n>> -\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n>> -\t\tif (rinfo.positive_refexprs > 1)\n>> -\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n>> +\t\tBUG(\"expected one of onto_name, *advance_name, or *revert_name\");\n>>   \t}\n>>   \tstrset_clear(&rinfo.negative_refs);\n>>   \tstrset_clear(&rinfo.positive_refs);\n>> @@ -220,7 +259,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>>   \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n>>   \t\t\t\t\t  struct commit *onto,\n>>   \t\t\t\t\t  struct merge_options *merge_opt,\n>> -\t\t\t\t\t  struct merge_result *result)\n>> +\t\t\t\t\t  struct merge_result *result,\n>> +\t\t\t\t\t  enum replay_mode mode)\n>>   {\n>>   \tstruct commit *base, *replayed_base;\n>>   \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n>> @@ -232,25 +272,45 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>>   \tpickme_tree = repo_get_commit_tree(repo, pickme);\n>>   \tbase_tree = repo_get_commit_tree(repo, base);\n>>   \n>> -\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>> -\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n>> -\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n>> -\n>> -\tmerge_incore_nonrecursive(merge_opt,\n>> -\t\t\t\t  base_tree,\n>> -\t\t\t\t  replayed_base_tree,\n>> -\t\t\t\t  pickme_tree,\n>> -\t\t\t\t  result);\n>> -\n>> -\tfree((char*)merge_opt->ancestor);\n>> +\tif (mode == REPLAY_MODE_PICK) {\n>> +\t\t/* Cherry-pick: normal order */\n>> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>> +\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n>> +\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n>> +\n>> +\t\tmerge_incore_nonrecursive(merge_opt,\n>> +\t\t\t\t\t  base_tree,\n>> +\t\t\t\t\t  replayed_base_tree,\n>> +\t\t\t\t\t  pickme_tree,\n>> +\t\t\t\t\t  result);\n>> +\n>> +\t\tfree((char *)merge_opt->ancestor);\n>> +\t} else if (mode == REPLAY_MODE_REVERT) {\n>> +\t\t/* Revert: swap base and pickme to reverse the diff */\n>> +\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n>> +\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n>> +\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n>> +\t\tmerge_opt->ancestor = pickme_name;\n>> +\n>> +\t\tmerge_incore_nonrecursive(merge_opt,\n>> +\t\t\t\t\t  pickme_tree,\n>> +\t\t\t\t\t  replayed_base_tree,\n>> +\t\t\t\t\t  base_tree,\n>> +\t\t\t\t\t  result);\n>> +\n>> +\t\tfree((char *)merge_opt->branch2);\n>> +\t} else {\n>> +\t\tBUG(\"unexpected replay mode %d\", mode);\n>> +\t}\n>>   \tmerge_opt->ancestor = NULL;\n>> +\tmerge_opt->branch2 = NULL;\n> \n> It took me a while to understand the ownership of these buffers,\n> especially those returned by short_commit_name(). But it seems to call\n> repo_find_unique_abbrev() which has 4 buffers which it cycles through.\n> \n> So this seems to be okay (this was also not changed in the MODE_PICK\n> path.\n> \n>>   \tif (!result->clean)\n>>   \t\treturn NULL;\n>>   \t/* Drop commits that become empty */\n>>   \tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n>>   \t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n>>   \t\treturn replayed_base;\n>> -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n>> +\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n>>   }\n>>   \n>>   void replay_result_release(struct replay_result *result)\n>> @@ -287,11 +347,16 @@ int replay_revisions(struct rev_info *revs,\n>>   \t};\n>>   \tbool detached_head;\n>>   \tchar *advance;\n>> +\tchar *revert;\n>> +\tenum replay_mode mode = REPLAY_MODE_PICK;\n>>   \tint ret;\n>>   \n>>   \tadvance = xstrdup_or_null(opts->advance);\n>> +\trevert = xstrdup_or_null(opts->revert);\n>> +\tif (revert)\n>> +\t\tmode = REPLAY_MODE_REVERT;\n>>   \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n>> -\t\t\t   &detached_head, &advance, &onto, &update_refs);\n>> +\t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n>>   \n>>   \t/* FIXME: Should allow replaying commits with the first as a root commit */\n>>   \n>> @@ -315,7 +380,8 @@ int replay_revisions(struct rev_info *revs,\n>>   \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n>>   \n>>   \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n>> -\t\t\t\t\t\t  onto, &merge_opt, &result);\n>> +\t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n>> +\t\t\t\t\t\t  &merge_opt, &result, mode);\n>>   \t\tif (!last_commit)\n>>   \t\t\tbreak;\n>>   \n>> @@ -327,7 +393,7 @@ int replay_revisions(struct rev_info *revs,\n>>   \t\tkh_value(replayed_commits, pos) = last_commit;\n>>   \n>>   \t\t/* Update any necessary branches */\n>> -\t\tif (advance)\n>> +\t\tif (advance || revert)\n>>   \t\t\tcontinue;\n>>   \n>>   \t\tfor (decoration = get_name_decoration(&commit->object);\n>> @@ -361,11 +427,13 @@ int replay_revisions(struct rev_info *revs,\n>>   \t\tgoto out;\n>>   \t}\n>>   \n>> -\t/* In --advance mode, advance the target ref */\n>> -\tif (advance)\n>> -\t\treplay_result_queue_update(out, advance,\n>> +\t/* In --advance or --revert mode, update the target ref */\n>> +\tif (advance || revert) {\n>> +\t\tconst char *ref = advance ? advance : revert;\n>> +\t\treplay_result_queue_update(out, ref,\n>>   \t\t\t\t\t   &onto->object.oid,\n>>   \t\t\t\t\t   &last_commit->object.oid);\n>> +\t}\n>>   \n>>   \tret = 0;\n>>   \n>> @@ -377,5 +445,6 @@ int replay_revisions(struct rev_info *revs,\n>>   \tkh_destroy_oid_map(replayed_commits);\n>>   \tmerge_finalize(&merge_opt, &result);\n>>   \tfree(advance);\n>> +\tfree(revert);\n>>   \treturn ret;\n>>   }\n>> diff --git a/replay.h b/replay.h\n>> index d8407dc7f7..e916a5f975 100644\n>> --- a/replay.h\n>> +++ b/replay.h\n>> @@ -13,7 +13,7 @@ struct replay_revisions_options {\n>>   \t/*\n>>   \t * Starting point at which to create the new commits; must be a branch\n>>   \t * name. The branch will be updated to point to the rewritten commits.\n>> -\t * This option is mutually exclusive with `onto`.\n>> +\t * This option is mutually exclusive with `onto` and `revert`.\n>>   \t */\n>>   \tconst char *advance;\n>>   \n>> @@ -22,7 +22,14 @@ struct replay_revisions_options {\n>>   \t * committish. References pointing at decendants of `onto` will be\n>>   \t * updated to point to the new commits.\n>>   \t */\n>> -\t const char *onto;\n>> +\tconst char *onto;\n>> +\n>> +\t/*\n>> +\t * Starting point at which to create revert commits; must be a branch\n>> +\t * name. The branch will be updated to point to the revert commits.\n>> +\t * This option is mutually exclusive with `onto` and `advance`.\n>> +\t */\n>> +\tconst char *revert;\n>>   \n>>   \t/*\n>>   \t * Update branches that point at commits in the given revision range.\n>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>> index a03f8f9293..0c1e03e0fb 100755\n>> --- a/t/t3650-replay-basics.sh\n>> +++ b/t/t3650-replay-basics.sh\n>> @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '\n>>   \ttest_cmp expect actual\n>>   '\n>>   \n>> -test_expect_success 'option --onto or --advance is mandatory' '\n>> -\techo \"error: option --onto or --advance is mandatory\" >expect &&\n>> +test_expect_success 'exactly one of --onto, --advance, or --revert is required' '\n>> +\techo \"error: exactly one of --onto, --advance, or --revert is required\" >expect &&\n>>   \ttest_might_fail git replay -h >>expect &&\n>>   \ttest_must_fail git replay topic1..topic2 2>actual &&\n>>   \ttest_cmp expect actual\n>> @@ -87,16 +87,17 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err\n>>   \ttest_cmp expect actual\n>>   '\n>>   \n>> -test_expect_success 'options --advance and --contained cannot be used together' '\n>> -\tprintf \"fatal: options ${SQ}--advance${SQ} \" >expect &&\n>> -\tprintf \"and ${SQ}--contained${SQ} cannot be used together\\n\" >>expect &&\n>> +test_expect_success '--contained requires --onto' '\n>> +\techo \"fatal: --contained requires --onto\" >expect &&\n>>   \ttest_must_fail git replay --advance=main --contained \\\n>>   \t\ttopic1..topic2 2>actual &&\n>>   \ttest_cmp expect actual\n>>   '\n>>   \n>>   test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n>> -\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n>> +\tcat >expect <<-\\EOF &&\n>> +\tfatal: '\"'\"'--advance'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n>> +\tEOF\n>>   \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n>>   \ttest_cmp expect actual\n>>   '\n>> @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' '\n>>   \ttest_grep \"invalid.*replay.refAction.*value\" error\n>>   '\n>>   \n>> +test_expect_success 'argument to --revert must be a reference' '\n>> +\techo \"fatal: argument to --revert must be a reference\" >expect &&\n>> +\toid=$(git rev-parse main) &&\n>> +\ttest_must_fail git replay --revert=$oid topic1..topic2 2>actual &&\n>> +\ttest_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'cannot revert with multiple sources' '\n>> +\tcat >expect <<-\\EOF &&\n>> +\tfatal: '\"'\"'--revert'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n>> +\tEOF\n>> +\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n>> +\ttest_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'using replay --revert to revert commits' '\n>> +\t# Reuse existing topic4 branch (has commits I and J on top of main)\n>> +\tSTART=$(git rev-parse topic4) &&\n>> +\ttest_when_finished \"git branch -f topic4 $START\" &&\n>> +\n>> +\t# Revert commits I and J\n>> +\tgit replay --revert topic4 topic4~2..topic4 &&\n>> +\n>> +\t# Verify the revert commits were created (newest-first ordering\n>> +\t# means J is reverted first, then I on top)\n>> +\tgit log --format=%s -4 topic4 >actual &&\n>> +\tcat >expect <<-\\EOF &&\n>> +\tRevert \"I\"\n>> +\tRevert \"J\"\n>> +\tJ\n>> +\tI\n>> +\tEOF\n>> +\ttest_cmp expect actual &&\n>> +\n>> +\t# Verify commit message format includes hash (tip is Revert \"I\")\n>> +\ttest_commit_message topic4 <<-EOF &&\n>> +\tRevert \"I\"\n>> +\n>> +\tThis reverts commit $(git rev-parse I).\n>> +\tEOF\n>> +\n>> +\t# Verify reflog message\n>> +\tgit reflog topic4 -1 --format=%gs >reflog-msg &&\n>> +\techo \"replay --revert topic4\" >expect-reflog &&\n>> +\ttest_cmp expect-reflog reflog-msg\n>> +'\n>> +\n>> +test_expect_success 'using replay --revert in bare repo' '\n>> +\t# Reuse existing topic4 in bare repo\n>> +\tSTART=$(git -C bare rev-parse topic4) &&\n>> +\ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n>> +\n>> +\t# Revert commit J in bare repo\n>> +\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n>> +\n>> +\t# Verify revert was created\n>> +\tgit -C bare log -1 --format=%s topic4 >actual &&\n>> +\techo \"Revert \\\"J\\\"\" >expect &&\n>> +\ttest_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'revert of revert uses Reapply' '\n>> +\t# Use topic4 and first revert J, then revert the revert\n>> +\tSTART=$(git rev-parse topic4) &&\n>> +\ttest_when_finished \"git branch -f topic4 $START\" &&\n>> +\n>> +\t# First revert J\n>> +\tgit replay --revert topic4 topic4~1..topic4 &&\n>> +\tREVERT_J=$(git rev-parse topic4) &&\n>> +\n>> +\t# Now revert the revert - should become Reapply\n>> +\tgit replay --revert topic4 topic4~1..topic4 &&\n>> +\n>> +\t# Verify Reapply prefix and message format\n>> +\ttest_commit_message topic4 <<-EOF\n>> +\tReapply \"J\"\n>> +\n>> +\tThis reverts commit $REVERT_J.\n>> +\tEOF\n>> +'\n>> +\n>> +test_expect_success 'git replay --revert with conflict' '\n>> +\t# conflict branch has C.conflict which conflicts with topic1s C\n>> +\ttest_expect_code 1 git replay --revert conflict B..topic1\n>> +'\n>> +\n>> +test_expect_success 'git replay --revert incompatible with --contained' '\n>> +\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n>> +\ttest_grep \"requires --onto\" error\n>> +'\n>> +\n>> +test_expect_success 'git replay --revert incompatible with --onto' '\n>> +\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n>> +\ttest_grep \"cannot be used together\" error\n>> +'\n>> +\n>> +test_expect_success 'git replay --revert incompatible with --advance' '\n>> +\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n>> +\ttest_grep \"cannot be used together\" error\n>> +'\n>> +\n>>   test_done\n>> -- \n>> 2.51.0\n>>\n>>\n> \n\n"},{"id":"539885","messageId":"20260324220401.47040-1-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260313054035.26605-1-siddharthasthana31@gmail.com","subject":"[PATCH v5 0/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-24T22:03:59Z","receivedAt":"2026-03-24T22:04:08Z","isPatch":true,"body":"Hi,\n\ngit replay currently supports cherry-picking (--advance) and rebasing\n(--onto), but not reverting. We need this at GitLab for Gitaly to\nreverse commits directly on bare repositories without a checkout.\n\nThe approach is the same as sequencer.c -- cherry-pick and revert are\njust the same three-way merge with swapped arguments. We swap the base\nand pickme trees passed to merge_incore_nonrecursive() to reverse the\ndiff direction.\n\nPatch 1 extracts the full revert message formatting logic into a new\nsequencer_format_revert_message() function that handles everything in\none shared function rather than just the header. refer_to_commit() is\nupdated to take a struct repository and a bool instead of replay_opts\nso it works outside the sequencer.\n\nPatch 2 adds --revert <branch> as a standalone mode. Reverts are\nprocessed newest-first (matching git revert) to reduce conflicts by\npeeling off changes from the top.\n\nThe series is based on top of d181b9354c (The 13th batch, 2026-03-07).\n\nChanges in v5:\n- Made sequencer_format_revert_message() header comment more concise,\n  using single quotes to avoid nested escaped double quotes\n- Moved desired_reverse declaration up and removed the bare braces, so\n  the same named variable is used when setting and checking\n  revs.reverse\n- Used die_for_incompatible_opt2() for --advance/--contained and\n  --revert/--contained instead of die(\"--contained requires --onto\")\n- Dropped the erroneously re-added ellipsis from <revision-range>...\n  in the SYNOPSIS\n- Restored #define the_repository DO_NOT_USE_THE_REPOSITORY guard in\n  replay.c\n- Used ${SQ} instead of '\"'\"' for single-quote escaping in tests\n- Link to v4: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/\n- Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/\n- Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/\n- Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/\n\nThanks,\nSiddharth\n\n---\nSiddharth Asthana (2):\n  sequencer: extract revert message formatting into shared function\n  replay: add --revert mode to reverse commit changes\n\n Documentation/git-replay.adoc |  43 ++++++++-\n builtin/replay.c              |  35 ++++++--\n replay.c                      | 161 +++++++++++++++++++++++++---------\n replay.h                      |  11 ++-\n sequencer.c                   |  78 +++++++++-------\n sequencer.h                   |  13 +++\n t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--\n 7 files changed, 355 insertions(+), 97 deletions(-)\n\nRange-diff versus v4:\n\n1:  bdc710b265 ! 1:  6bd2ce4515 sequencer: extract revert message formatting into shared function\n    @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh\n      int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n      \n     +/*\n    -+ * Formats a complete revert commit message following standard Git conventions.\n    -+ * Handles regular reverts (\"Revert \\\"<subject>\\\"\"), revert of revert cases\n    -+ * (\"Reapply \\\"<subject>\\\"\"), and the --reference style. Appends \"This reverts\n    -+ * commit <ref>.\" using either the abbreviated or full commit reference\n    -+ * depending on use_commit_reference. Also handles merge-parent references.\n    ++ * Format a revert commit message with appropriate 'Revert \"<subject>\"' or\n    ++ * 'Reapply \"<subject>\"' prefix and 'This reverts commit <ref>.' body.\n    ++ * When use_commit_reference is set, <ref> is an abbreviated hash with\n    ++ * subject and date; otherwise the full hex hash is used.\n     + */\n     +void sequencer_format_revert_message(struct repository *r,\n     +\t\t\t\t     const char *subject,\n2:  bea6229575 ! 2:  9fd92497b9 replay: add --revert mode to reverse commit changes\n    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne\n      --------\n      [verse]\n     -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n    -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>\n      \n      DESCRIPTION\n      -----------\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tconst char *const replay_usage[] = {\n      \t\tN_(\"(EXPERIMENTAL!) git replay \"\n     -\t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n    --\t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n     +\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n    -+\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n    + \t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n      \t\tNULL\n      \t};\n    - \tstruct option replay_options[] = {\n     @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\t\t   N_(\"replay onto given commit\")),\n      \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\tusage_with_options(replay_usage, replay_options);\n      \t}\n      \n    --\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n    --\t\t\t\t  opts.contained, \"--contained\");\n    --\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n    --\t\t\t\t  !!opts.onto, \"--onto\");\n     +\tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n     +\t\t\t\t  !!opts.advance, \"--advance\",\n     +\t\t\t\t  !!opts.revert, \"--revert\");\n    -+\tif (opts.contained && !opts.onto)\n    -+\t\tdie(_(\"--contained requires --onto\"));\n    + \tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n    + \t\t\t\t  opts.contained, \"--contained\");\n    +-\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n    +-\t\t\t\t  !!opts.onto, \"--onto\");\n    ++\tdie_for_incompatible_opt2(!!opts.revert, \"--revert\",\n    ++\t\t\t\t  opts.contained, \"--contained\");\n      \n      \t/* Parse ref action mode from command line or config */\n      \tref_mode = get_ref_action_mode(repo, ref_action);\n    -@@ builtin/replay.c: int cmd_replay(int argc,\n    - \t * some options changing these values if we think they could\n    - \t * be useful.\n    - \t */\n    --\trevs.reverse = 1;\n    + \n     +\t/*\n     +\t * Cherry-pick/rebase need oldest-first ordering so that each\n     +\t * replayed commit can build on its already-replayed parent.\n     +\t * Revert needs newest-first ordering (like git revert) to\n     +\t * reduce conflicts by peeling off changes from the top.\n     +\t */\n    -+\trevs.reverse = opts.revert ? 0 : 1;\n    ++\tint desired_reverse = !opts.revert;\n    ++\n    + \trepo_init_revisions(repo, &revs, prefix);\n    + \n    + \t/*\n    +@@ builtin/replay.c: int cmd_replay(int argc,\n    + \t * some options changing these values if we think they could\n    + \t * be useful.\n    + \t */\n    +-\trevs.reverse = 1;\n    ++\trevs.reverse = desired_reverse;\n      \trevs.sort_order = REV_SORT_IN_GRAPH_ORDER;\n      \trevs.topo_order = 1;\n      \trevs.simplify_history = 0;\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \t * walking options.\n      \t */\n     -\tif (revs.reverse != 1) {\n    --\t\twarning(_(\"some rev walking options will be overridden as \"\n    --\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n    --\t\t\t\"reverse\");\n    ++\tif (revs.reverse != desired_reverse) {\n    + \t\twarning(_(\"some rev walking options will be overridden as \"\n    + \t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n    + \t\t\t\"reverse\");\n     -\t\trevs.reverse = 1;\n    -+\t{\n    -+\t\tint desired_reverse = opts.revert ? 0 : 1;\n    -+\t\tif (revs.reverse != desired_reverse) {\n    -+\t\t\twarning(_(\"some rev walking options will be overridden as \"\n    -+\t\t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n    -+\t\t\t\t\"reverse\");\n    -+\t\t\trevs.reverse = desired_reverse;\n    -+\t\t}\n    ++\t\trevs.reverse = desired_reverse;\n      \t}\n      \tif (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {\n      \t\twarning(_(\"some rev walking options will be overridden as \"\n    @@ replay.c\n      #include \"strmap.h\"\n      #include \"tree.h\"\n      \n    --/*\n    -- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but\n    -- * do not want to use the_repository.\n    -- */\n    --#define the_repository DO_NOT_USE_THE_REPOSITORY\n    +@@\n    +  */\n    + #define the_repository DO_NOT_USE_THE_REPOSITORY\n    + \n     +enum replay_mode {\n     +\tREPLAY_MODE_PICK,\n     +\tREPLAY_MODE_REVERT,\n     +};\n    - \n    ++\n      static const char *short_commit_name(struct repository *repo,\n      \t\t\t\t     struct commit *commit)\n    + {\n     @@ replay.c: static char *get_author(const char *message)\n      \treturn NULL;\n      }\n    @@ t/t3650-replay-basics.sh: test_expect_success 'no base or negative ref gives no-\n     -test_expect_success 'options --advance and --contained cannot be used together' '\n     -\tprintf \"fatal: options ${SQ}--advance${SQ} \" >expect &&\n     -\tprintf \"and ${SQ}--contained${SQ} cannot be used together\\n\" >>expect &&\n    -+test_expect_success '--contained requires --onto' '\n    -+\techo \"fatal: --contained requires --onto\" >expect &&\n    ++test_expect_success '--advance and --contained cannot be used together' '\n      \ttest_must_fail git replay --advance=main --contained \\\n      \t\ttopic1..topic2 2>actual &&\n    - \ttest_cmp expect actual\n    +-\ttest_cmp expect actual\n    ++\ttest_grep \"cannot be used together\" actual\n      '\n      \n      test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n     -\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n    -+\tcat >expect <<-\\EOF &&\n    -+\tfatal: '\"'\"'--advance'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n    -+\tEOF\n    ++\techo \"fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\" >expect &&\n      \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n      \ttest_cmp expect actual\n      '\n    @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '\n     +'\n     +\n     +test_expect_success 'cannot revert with multiple sources' '\n    -+\tcat >expect <<-\\EOF &&\n    -+\tfatal: '\"'\"'--revert'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n    -+\tEOF\n    ++\techo \"fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\" >expect &&\n     +\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n     +\ttest_cmp expect actual\n     +'\n    @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' '\n     +\n     +test_expect_success 'git replay --revert incompatible with --contained' '\n     +\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n    -+\ttest_grep \"requires --onto\" error\n    ++\ttest_grep \"cannot be used together\" error\n     +'\n     +\n     +test_expect_success 'git replay --revert incompatible with --onto' '\n\nbase-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2\n-- \n2.51.0\n\n"},{"id":"539886","messageId":"20260324220401.47040-2-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260324220401.47040-1-siddharthasthana31@gmail.com","subject":"[PATCH v5 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-24T22:04:00Z","receivedAt":"2026-03-24T22:04:13Z","isPatch":true,"body":"The logic for formatting revert commit messages (handling \"Revert\" and\n\"Reapply\" cases, appending \"This reverts commit <ref>.\", and handling\nmerge-parent references) currently lives inline in do_pick_commit().\nThe upcoming replay --revert mode needs to reuse this logic.\n\nExtract all of this into a new sequencer_format_revert_message()\nfunction. The function takes a repository, the subject line, commit,\nparent, a use_commit_reference flag, and the output strbuf. It handles\nboth regular reverts (\"Revert \"<subject>\"\") and revert-of-revert cases\n(\"Reapply \"<subject>\"\"), and uses refer_to_commit() internally to\nformat the commit reference.\n\nUpdate refer_to_commit() to take a struct repository parameter instead\nof relying on the_repository, and a bool instead of reading from\nreplay_opts directly. This makes it usable from the new shared function\nwithout pulling in sequencer-specific state.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------\n sequencer.h | 13 +++++++++\n 2 files changed, 59 insertions(+), 32 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex aafd0bc959..7bf9d6ad19 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) {\n \treturn opts->edit;\n }\n \n-static void refer_to_commit(struct replay_opts *opts,\n-\t\t\t    struct strbuf *msgbuf, struct commit *commit)\n+static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,\n+\t\t\t    const struct commit *commit,\n+\t\t\t    bool use_commit_reference)\n {\n-\tif (opts->commit_use_reference) {\n+\tif (use_commit_reference) {\n \t\tstruct pretty_print_context ctx = {\n \t\t\t.abbrev = DEFAULT_ABBREV,\n \t\t\t.date_mode.type = DATE_SHORT,\n \t\t};\n-\t\trepo_format_commit_message(the_repository, commit,\n+\t\trepo_format_commit_message(r, commit,\n \t\t\t\t\t   \"%h (%s, %ad)\", msgbuf, &ctx);\n \t} else {\n \t\tstrbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));\n@@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r,\n \t */\n \n \tif (command == TODO_REVERT) {\n-\t\tconst char *orig_subject;\n-\n \t\tbase = commit;\n \t\tbase_label = msg.label;\n \t\tnext = parent;\n \t\tnext_label = msg.parent_label;\n-\t\tif (opts->commit_use_reference) {\n-\t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n-\t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n-\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n-\t\t\t   /*\n-\t\t\t    * We don't touch pre-existing repeated reverts, because\n-\t\t\t    * theoretically these can be nested arbitrarily deeply,\n-\t\t\t    * thus requiring excessive complexity to deal with.\n-\t\t\t    */\n-\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n-\t\t} else {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n-\t\trefer_to_commit(opts, &ctx->message, commit);\n-\n-\t\tif (commit->parents && commit->parents->next) {\n-\t\t\tstrbuf_addstr(&ctx->message, \", reversing\\nchanges made to \");\n-\t\t\trefer_to_commit(opts, &ctx->message, parent);\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \".\\n\");\n+\t\tsequencer_format_revert_message(r, msg.subject, commit,\n+\t\t\t\t\t\tparent,\n+\t\t\t\t\t\topts->commit_use_reference,\n+\t\t\t\t\t\t&ctx->message);\n \t} else {\n \t\tconst char *p;\n \n@@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r,\n \treturn res;\n }\n \n+void sequencer_format_revert_message(struct repository *r,\n+\t\t\t\t     const char *subject,\n+\t\t\t\t     const struct commit *commit,\n+\t\t\t\t     const struct commit *parent,\n+\t\t\t\t     bool use_commit_reference,\n+\t\t\t\t     struct strbuf *message)\n+{\n+\tconst char *orig_subject;\n+\n+\tif (use_commit_reference) {\n+\t\tstrbuf_commented_addf(message, comment_line_str,\n+\t\t\t\t      \"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n+\t} else if (skip_prefix(subject, \"Revert \\\"\", &orig_subject) &&\n+\t\t   /*\n+\t\t    * We don't touch pre-existing repeated reverts, because\n+\t\t    * theoretically these can be nested arbitrarily deeply,\n+\t\t    * thus requiring excessive complexity to deal with.\n+\t\t    */\n+\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n+\t\tstrbuf_addstr(message, \"Reapply \\\"\");\n+\t\tstrbuf_addstr(message, orig_subject);\n+\t\tstrbuf_addstr(message, \"\\n\");\n+\t} else {\n+\t\tstrbuf_addstr(message, \"Revert \\\"\");\n+\t\tstrbuf_addstr(message, subject);\n+\t\tstrbuf_addstr(message, \"\\\"\\n\");\n+\t}\n+\tstrbuf_addstr(message, \"\\nThis reverts commit \");\n+\trefer_to_commit(r, message, commit, use_commit_reference);\n+\n+\tif (commit->parents && commit->parents->next) {\n+\t\tstrbuf_addstr(message, \", reversing\\nchanges made to \");\n+\t\trefer_to_commit(r, message, parent, use_commit_reference);\n+\t}\n+\tstrbuf_addstr(message, \".\\n\");\n+}\n+\n void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n {\n \tunsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;\ndiff --git a/sequencer.h b/sequencer.h\nindex 719684c8a9..56cd50233a 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -271,4 +271,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n  */\n int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n \n+/*\n+ * Format a revert commit message with appropriate 'Revert \"<subject>\"' or\n+ * 'Reapply \"<subject>\"' prefix and 'This reverts commit <ref>.' body.\n+ * When use_commit_reference is set, <ref> is an abbreviated hash with\n+ * subject and date; otherwise the full hex hash is used.\n+ */\n+void sequencer_format_revert_message(struct repository *r,\n+\t\t\t\t     const char *subject,\n+\t\t\t\t     const struct commit *commit,\n+\t\t\t\t     const struct commit *parent,\n+\t\t\t\t     bool use_commit_reference,\n+\t\t\t\t     struct strbuf *message);\n+\n #endif /* SEQUENCER_H */\n-- \n2.51.0\n\n"},{"id":"539887","messageId":"20260324220401.47040-3-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260324220401.47040-1-siddharthasthana31@gmail.com","subject":"[PATCH v5 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-24T22:04:01Z","receivedAt":"2026-03-24T22:04:19Z","isPatch":true,"body":"Add a `--revert <branch>` mode to git replay that undoes the changes\nintroduced by the specified commits. Like --onto and --advance, --revert\nis a standalone mode: it takes a branch argument and updates that branch\nwith the newly created revert commits.\n\nAt GitLab, we need this in Gitaly for reverting commits directly on bare\nrepositories without requiring a working tree checkout.\n\nThe approach is the same as sequencer.c's do_pick_commit() -- cherry-pick\nand revert are just the same three-way merge with swapped arguments:\n\n  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n  - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n\nWe swap the base and pickme trees passed to merge_incore_nonrecursive()\nto reverse the diff direction.\n\nReverts are processed newest-first (matching git revert behavior) to\nreduce conflicts by peeling off changes from the top. Each revert\nbuilds on the result of the previous one via the last_commit fallback\nin the main replay loop, rather than relying on the parent-mapping\nused for cherry-pick.\n\nRevert commit messages follow the usual git revert conventions: prefixed\nwith \"Revert\" (or \"Reapply\" when reverting a revert), and including\n\"This reverts commit <hash>.\". The author is set to the current user\nrather than preserving the original author, matching git revert behavior.\n\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nHelped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Toon Claes <toon@iotcl.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  43 ++++++++-\n builtin/replay.c              |  35 ++++++--\n replay.c                      | 161 +++++++++++++++++++++++++---------\n replay.h                      |  11 ++-\n t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--\n 5 files changed, 296 insertions(+), 65 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 8d696ce3ab..746312e9c6 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to\n point at the tip of the resulting history. This is different from `--onto`,\n which uses the target only as a starting point without updating it.\n \n+--revert <branch>::\n+\tStarting point at which to create the reverted commits; must be a\n+\tbranch name.\n++\n+When `--revert` is specified, the commits in the revision range are reverted\n+(their changes are undone) and the reverted commits are created on top of\n+<branch>. The <branch> is then updated to point at the new commits. This is\n+the same as running `git revert <revision-range>` but does not update the\n+working tree.\n++\n+The commit messages follow `git revert` conventions: they are prefixed with\n+\"Revert\" and include \"This reverts commit <hash>.\" When reverting a commit\n+whose message starts with \"Revert\", the new message uses \"Reapply\" instead.\n+Unlike cherry-pick which preserves the original author, revert commits use\n+the current user as the author, matching the behavior of `git revert`.\n++\n+This option is mutually exclusive with `--onto` and `--advance`. It is also\n+incompatible with `--contained` (which is a modifier for `--onto` only).\n+\n --contained::\n \tUpdate all branches that point at commits in\n \t<revision-range>. Requires `--onto`.\n@@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to\n \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n \n where the number of refs updated depends on the arguments passed and\n-the shape of the history being replayed.  When using `--advance`, the\n-number of refs updated is always one, but for `--onto`, it can be one\n-or more (rebasing multiple branches simultaneously is supported).\n+the shape of the history being replayed.  When using `--advance` or\n+`--revert`, the number of refs updated is always one, but for `--onto`,\n+it can be one or more (rebasing multiple branches simultaneously is\n+supported).\n \n There is no stderr output on conflicts; see the <<exit-status,EXIT\n STATUS>> section below.\n@@ -152,6 +172,21 @@ all commits they have since `base`, playing them on top of\n `origin/main`. These three branches may have commits on top of `base`\n that they have in common, but that does not need to be the case.\n \n+To revert commits on a branch:\n+\n+------------\n+$ git replay --revert main topic~2..topic\n+------------\n+\n+This reverts the last two commits from `topic`, creating revert commits on\n+top of `main`, and updates `main` to point at the result. This is useful when\n+commits from `topic` were previously merged or cherry-picked into `main` and\n+need to be undone.\n+\n+NOTE: For reverting an entire merge request as a single commit (rather than\n+commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n+which can avoid unnecessary merge conflicts.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 2cdde830a8..d3c1d920f0 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -83,7 +83,7 @@ int cmd_replay(int argc,\n \n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n+\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n \t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n \t\tNULL\n \t};\n@@ -96,6 +96,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n+\t\tOPT_STRING(0, \"revert\", &opts.revert,\n+\t\t\t   N_(\"branch\"),\n+\t\t\t   N_(\"revert commits onto given branch\")),\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n \t\t\t   N_(\"control ref update behavior (update|print)\")),\n@@ -105,19 +108,31 @@ int cmd_replay(int argc,\n \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n-\tif (!opts.onto && !opts.advance) {\n-\t\terror(_(\"option --onto or --advance is mandatory\"));\n+\t/* Exactly one mode must be specified */\n+\tif (!opts.onto && !opts.advance && !opts.revert) {\n+\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n+\tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n+\t\t\t\t  !!opts.advance, \"--advance\",\n+\t\t\t\t  !!opts.revert, \"--revert\");\n \tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n \t\t\t\t  opts.contained, \"--contained\");\n-\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n-\t\t\t\t  !!opts.onto, \"--onto\");\n+\tdie_for_incompatible_opt2(!!opts.revert, \"--revert\",\n+\t\t\t\t  opts.contained, \"--contained\");\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n \n+\t/*\n+\t * Cherry-pick/rebase need oldest-first ordering so that each\n+\t * replayed commit can build on its already-replayed parent.\n+\t * Revert needs newest-first ordering (like git revert) to\n+\t * reduce conflicts by peeling off changes from the top.\n+\t */\n+\tint desired_reverse = !opts.revert;\n+\n \trepo_init_revisions(repo, &revs, prefix);\n \n \t/*\n@@ -129,7 +144,7 @@ int cmd_replay(int argc,\n \t * some options changing these values if we think they could\n \t * be useful.\n \t */\n-\trevs.reverse = 1;\n+\trevs.reverse = desired_reverse;\n \trevs.sort_order = REV_SORT_IN_GRAPH_ORDER;\n \trevs.topo_order = 1;\n \trevs.simplify_history = 0;\n@@ -144,11 +159,11 @@ int cmd_replay(int argc,\n \t * Detect and warn if we override some user specified rev\n \t * walking options.\n \t */\n-\tif (revs.reverse != 1) {\n+\tif (revs.reverse != desired_reverse) {\n \t\twarning(_(\"some rev walking options will be overridden as \"\n \t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n \t\t\t\"reverse\");\n-\t\trevs.reverse = 1;\n+\t\trevs.reverse = desired_reverse;\n \t}\n \tif (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {\n \t\twarning(_(\"some rev walking options will be overridden as \"\n@@ -174,7 +189,9 @@ int cmd_replay(int argc,\n \t\tgoto cleanup;\n \n \t/* Build reflog message */\n-\tif (opts.advance) {\n+\tif (opts.revert) {\n+\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", opts.revert);\n+\t} else if (opts.advance) {\n \t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n \t} else {\n \t\tstruct object_id oid;\ndiff --git a/replay.c b/replay.c\nindex a63f6714c4..d7239d4c83 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -8,6 +8,7 @@\n #include \"refs.h\"\n #include \"replay.h\"\n #include \"revision.h\"\n+#include \"sequencer.h\"\n #include \"strmap.h\"\n #include \"tree.h\"\n \n@@ -17,6 +18,11 @@\n  */\n #define the_repository DO_NOT_USE_THE_REPOSITORY\n \n+enum replay_mode {\n+\tREPLAY_MODE_PICK,\n+\tREPLAY_MODE_REVERT,\n+};\n+\n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n {\n@@ -50,15 +56,37 @@ static char *get_author(const char *message)\n \treturn NULL;\n }\n \n+static void generate_revert_message(struct strbuf *msg,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct repository *repo)\n+{\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n+\tconst char *subject_start;\n+\tint subject_len;\n+\tchar *subject;\n+\n+\tsubject_len = find_commit_subject(message, &subject_start);\n+\tsubject = xmemdupz(subject_start, subject_len);\n+\n+\tsequencer_format_revert_message(repo, subject, commit,\n+\t\t\t\t\tcommit->parents ? commit->parents->item : NULL,\n+\t\t\t\t\tfalse, msg);\n+\n+\tfree(subject);\n+\trepo_unuse_commit_buffer(repo, commit, message);\n+}\n+\n static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n+\t\t\t\t    struct commit *parent,\n+\t\t\t\t    enum replay_mode mode)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n-\tchar *author;\n+\tchar *author = NULL;\n \tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n@@ -70,9 +98,16 @@ static struct commit *create_commit(struct repository *repo,\n \n \tcommit_list_insert(parent, &parents);\n \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n+\tif (mode == REPLAY_MODE_REVERT) {\n+\t\tgenerate_revert_message(&msg, based_on, repo);\n+\t\t/* For revert, use current user as author (NULL = use default) */\n+\t} else if (mode == REPLAY_MODE_PICK) {\n+\t\tfind_commit_subject(message, &orig_message);\n+\t\tstrbuf_addstr(&msg, orig_message);\n+\t\tauthor = get_author(message);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \treset_ident_date();\n \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n@@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo,\n \t}\n }\n \n+static void set_up_branch_mode(struct repository *repo,\n+\t\t\t       char **branch_name,\n+\t\t\t       const char *option_name,\n+\t\t\t       struct ref_info *rinfo,\n+\t\t\t       struct commit **onto)\n+{\n+\tstruct object_id oid;\n+\tchar *fullname = NULL;\n+\n+\tif (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),\n+\t\t\t  &oid, &fullname, 0) == 1) {\n+\t\tfree(*branch_name);\n+\t\t*branch_name = fullname;\n+\t} else {\n+\t\tdie(_(\"argument to %s must be a reference\"), option_name);\n+\t}\n+\t*onto = peel_committish(repo, *branch_name, option_name);\n+\tif (rinfo->positive_refexprs > 1)\n+\t\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n+\t\t      \"because the ordering would be ill-defined\"),\n+\t\t    option_name);\n+}\n+\n static void set_up_replay_mode(struct repository *repo,\n \t\t\t       struct rev_cmdline_info *cmd_info,\n \t\t\t       const char *onto_name,\n \t\t\t       bool *detached_head,\n \t\t\t       char **advance_name,\n+\t\t\t       char **revert_name,\n \t\t\t       struct commit **onto,\n \t\t\t       struct strset **update_refs)\n {\n@@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo,\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n \n-\tif (!onto_name == !*advance_name)\n-\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n-\n \tif (onto_name) {\n \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n \t\tif (rinfo.positive_refexprs <\n@@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo,\n \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n \t\t**update_refs = rinfo.positive_refs;\n \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n+\t} else if (*advance_name) {\n+\t\tset_up_branch_mode(repo, advance_name, \"--advance\", &rinfo, onto);\n+\t} else if (*revert_name) {\n+\t\tset_up_branch_mode(repo, revert_name, \"--revert\", &rinfo, onto);\n \t} else {\n-\t\tstruct object_id oid;\n-\t\tchar *fullname = NULL;\n-\n-\t\tif (!*advance_name)\n-\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n-\n-\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n-\t\t\t     &oid, &fullname, 0) == 1) {\n-\t\t\tfree(*advance_name);\n-\t\t\t*advance_name = fullname;\n-\t\t} else {\n-\t\t\tdie(_(\"argument to --advance must be a reference\"));\n-\t\t}\n-\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n-\t\tif (rinfo.positive_refexprs > 1)\n-\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n+\t\tBUG(\"expected one of onto_name, *advance_name, or *revert_name\");\n \t}\n \tstrset_clear(&rinfo.negative_refs);\n \tstrset_clear(&rinfo.positive_refs);\n@@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n \t\t\t\t\t  struct commit *onto,\n \t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n+\t\t\t\t\t  struct merge_result *result,\n+\t\t\t\t\t  enum replay_mode mode)\n {\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n@@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  replayed_base_tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n+\tif (mode == REPLAY_MODE_PICK) {\n+\t\t/* Cherry-pick: normal order */\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->ancestor);\n+\t} else if (mode == REPLAY_MODE_REVERT) {\n+\t\t/* Revert: swap base and pickme to reverse the diff */\n+\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n+\t\tmerge_opt->ancestor = pickme_name;\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->branch2);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \tmerge_opt->ancestor = NULL;\n+\tmerge_opt->branch2 = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n \t/* Drop commits that become empty */\n \tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n \t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n \t\treturn replayed_base;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs,\n \t};\n \tbool detached_head;\n \tchar *advance;\n+\tchar *revert;\n+\tenum replay_mode mode = REPLAY_MODE_PICK;\n \tint ret;\n \n \tadvance = xstrdup_or_null(opts->advance);\n+\trevert = xstrdup_or_null(opts->revert);\n+\tif (revert)\n+\t\tmode = REPLAY_MODE_REVERT;\n \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n-\t\t\t   &detached_head, &advance, &onto, &update_refs);\n+\t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n \n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n@@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs,\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n+\t\t\t\t\t\t  &merge_opt, &result, mode);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \n@@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance)\n+\t\tif (advance || revert)\n \t\t\tcontinue;\n \n \t\tfor (decoration = get_name_decoration(&commit->object);\n@@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs,\n \t\tgoto out;\n \t}\n \n-\t/* In --advance mode, advance the target ref */\n-\tif (advance)\n-\t\treplay_result_queue_update(out, advance,\n+\t/* In --advance or --revert mode, update the target ref */\n+\tif (advance || revert) {\n+\t\tconst char *ref = advance ? advance : revert;\n+\t\treplay_result_queue_update(out, ref,\n \t\t\t\t\t   &onto->object.oid,\n \t\t\t\t\t   &last_commit->object.oid);\n+\t}\n \n \tret = 0;\n \n@@ -377,5 +451,6 @@ int replay_revisions(struct rev_info *revs,\n \tkh_destroy_oid_map(replayed_commits);\n \tmerge_finalize(&merge_opt, &result);\n \tfree(advance);\n+\tfree(revert);\n \treturn ret;\n }\ndiff --git a/replay.h b/replay.h\nindex d8407dc7f7..e916a5f975 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -13,7 +13,7 @@ struct replay_revisions_options {\n \t/*\n \t * Starting point at which to create the new commits; must be a branch\n \t * name. The branch will be updated to point to the rewritten commits.\n-\t * This option is mutually exclusive with `onto`.\n+\t * This option is mutually exclusive with `onto` and `revert`.\n \t */\n \tconst char *advance;\n \n@@ -22,7 +22,14 @@ struct replay_revisions_options {\n \t * committish. References pointing at decendants of `onto` will be\n \t * updated to point to the new commits.\n \t */\n-\t const char *onto;\n+\tconst char *onto;\n+\n+\t/*\n+\t * Starting point at which to create revert commits; must be a branch\n+\t * name. The branch will be updated to point to the revert commits.\n+\t * This option is mutually exclusive with `onto` and `advance`.\n+\t */\n+\tconst char *revert;\n \n \t/*\n \t * Update branches that point at commits in the given revision range.\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex a03f8f9293..217f6fb292 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'option --onto or --advance is mandatory' '\n-\techo \"error: option --onto or --advance is mandatory\" >expect &&\n+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '\n+\techo \"error: exactly one of --onto, --advance, or --revert is required\" >expect &&\n \ttest_might_fail git replay -h >>expect &&\n \ttest_must_fail git replay topic1..topic2 2>actual &&\n \ttest_cmp expect actual\n@@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'options --advance and --contained cannot be used together' '\n-\tprintf \"fatal: options ${SQ}--advance${SQ} \" >expect &&\n-\tprintf \"and ${SQ}--contained${SQ} cannot be used together\\n\" >>expect &&\n+test_expect_success '--advance and --contained cannot be used together' '\n \ttest_must_fail git replay --advance=main --contained \\\n \t\ttopic1..topic2 2>actual &&\n-\ttest_cmp expect actual\n+\ttest_grep \"cannot be used together\" actual\n '\n \n test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n-\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n+\techo \"fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\" >expect &&\n \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n \ttest_cmp expect actual\n '\n@@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' '\n \ttest_grep \"invalid.*replay.refAction.*value\" error\n '\n \n+test_expect_success 'argument to --revert must be a reference' '\n+\techo \"fatal: argument to --revert must be a reference\" >expect &&\n+\toid=$(git rev-parse main) &&\n+\ttest_must_fail git replay --revert=$oid topic1..topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'cannot revert with multiple sources' '\n+\techo \"fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\" >expect &&\n+\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using replay --revert to revert commits' '\n+\t# Reuse existing topic4 branch (has commits I and J on top of main)\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# Revert commits I and J\n+\tgit replay --revert topic4 topic4~2..topic4 &&\n+\n+\t# Verify the revert commits were created (newest-first ordering\n+\t# means J is reverted first, then I on top)\n+\tgit log --format=%s -4 topic4 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\tRevert \"I\"\n+\tRevert \"J\"\n+\tJ\n+\tI\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\t# Verify commit message format includes hash (tip is Revert \"I\")\n+\ttest_commit_message topic4 <<-EOF &&\n+\tRevert \"I\"\n+\n+\tThis reverts commit $(git rev-parse I).\n+\tEOF\n+\n+\t# Verify reflog message\n+\tgit reflog topic4 -1 --format=%gs >reflog-msg &&\n+\techo \"replay --revert topic4\" >expect-reflog &&\n+\ttest_cmp expect-reflog reflog-msg\n+'\n+\n+test_expect_success 'using replay --revert in bare repo' '\n+\t# Reuse existing topic4 in bare repo\n+\tSTART=$(git -C bare rev-parse topic4) &&\n+\ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n+\n+\t# Revert commit J in bare repo\n+\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify revert was created\n+\tgit -C bare log -1 --format=%s topic4 >actual &&\n+\techo \"Revert \\\"J\\\"\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'revert of revert uses Reapply' '\n+\t# Use topic4 and first revert J, then revert the revert\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# First revert J\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\tREVERT_J=$(git rev-parse topic4) &&\n+\n+\t# Now revert the revert - should become Reapply\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify Reapply prefix and message format\n+\ttest_commit_message topic4 <<-EOF\n+\tReapply \"J\"\n+\n+\tThis reverts commit $REVERT_J.\n+\tEOF\n+'\n+\n+test_expect_success 'git replay --revert with conflict' '\n+\t# conflict branch has C.conflict which conflicts with topic1s C\n+\ttest_expect_code 1 git replay --revert conflict B..topic1\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --contained' '\n+\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --onto' '\n+\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --advance' '\n+\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"539909","messageId":"xmqqh5q4xvyw.fsf@gitster.g","threadId":"64529","inReplyTo":"20260324220401.47040-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-25T06:29:59Z","receivedAt":"2026-03-25T06:30:03Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 2cdde830a8..d3c1d920f0 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -83,7 +83,7 @@ int cmd_replay(int argc,\n> ... \n>  \t/* Parse ref action mode from command line or config */\n>  \tref_mode = get_ref_action_mode(repo, ref_action);\n>  \n> +\t/*\n> +\t * Cherry-pick/rebase need oldest-first ordering so that each\n> +\t * replayed commit can build on its already-replayed parent.\n> +\t * Revert needs newest-first ordering (like git revert) to\n> +\t * reduce conflicts by peeling off changes from the top.\n> +\t */\n> +\tint desired_reverse = !opts.revert;\n> +\n\nCompiler notices -Werror=declaration-after-statement error here.\n\n>  \trepo_init_revisions(repo, &revs, prefix);\n"},{"id":"539941","messageId":"87cy0s0wt5.fsf@iotcl.com","threadId":"64529","inReplyTo":"xmqqh5q4xvyw.fsf@gitster.g","subject":"Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-25T15:10:30Z","receivedAt":"2026-03-25T15:10:40Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> diff --git a/builtin/replay.c b/builtin/replay.c\n>> index 2cdde830a8..d3c1d920f0 100644\n>> --- a/builtin/replay.c\n>> +++ b/builtin/replay.c\n>> @@ -83,7 +83,7 @@ int cmd_replay(int argc,\n>> ... \n>>  \t/* Parse ref action mode from command line or config */\n>>  \tref_mode = get_ref_action_mode(repo, ref_action);\n>>  \n>> +\t/*\n>> +\t * Cherry-pick/rebase need oldest-first ordering so that each\n>> +\t * replayed commit can build on its already-replayed parent.\n>> +\t * Revert needs newest-first ordering (like git revert) to\n>> +\t * reduce conflicts by peeling off changes from the top.\n>> +\t */\n>> +\tint desired_reverse = !opts.revert;\n>> +\n>\n> Compiler notices -Werror=declaration-after-statement error here.\n\nThat's basically the only comment I have on this series.\n\nExcept for one micro-hit on the existing docs about <revision-range>:\n\n    <revision-range>::\n    \tRange of commits to replay; see \"Specifying Ranges\" in\n    \tlinkgit:git-rev-parse[1]. In `--advance <branch>` mode, the\n    \trange should have a single tip, so that it's clear to which tip the\n    \tadvanced <branch> should point. Any commits in the range whose\n    \tchanges are already present in the branch the commits are being\n    \treplayed onto will be dropped.\n\nNext to --advance, we should also mention --revert. But that's totally\nnot worth a reroll and can be addressed in any other later series.\n\n-- \nCheers,\nToon\n"},{"id":"539944","messageId":"34248fd9-3939-472c-ba27-7ea5346e3d84@gmail.com","threadId":"64529","inReplyTo":"xmqqh5q4xvyw.fsf@gitster.g","subject":"Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-25T15:36:38Z","receivedAt":"2026-03-25T15:36:44Z","isPatch":true,"body":"\n\nOn 25/03/26 11:59, Junio C Hamano wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n> \n>> diff --git a/builtin/replay.c b/builtin/replay.c\n>> index 2cdde830a8..d3c1d920f0 100644\n>> --- a/builtin/replay.c\n>> +++ b/builtin/replay.c\n>> @@ -83,7 +83,7 @@ int cmd_replay(int argc,\n>> ...\n>>   \t/* Parse ref action mode from command line or config */\n>>   \tref_mode = get_ref_action_mode(repo, ref_action);\n>>   \n>> +\t/*\n>> +\t * Cherry-pick/rebase need oldest-first ordering so that each\n>> +\t * replayed commit can build on its already-replayed parent.\n>> +\t * Revert needs newest-first ordering (like git revert) to\n>> +\t * reduce conflicts by peeling off changes from the top.\n>> +\t */\n>> +\tint desired_reverse = !opts.revert;\n>> +\n> \n> Compiler notices -Werror=declaration-after-statement error here.\n\n\nAh right, I should have caught that. I have moved the declaration to the \ntop of cmd_replay() with other variables and the CI is passing now [1]. \nWill send a v6.\n\n[1] https://gitlab.com/gitlab-org/git/-/pipelines/2406607541\n\nThanks,\nSiddharth\n\n> \n>>   \trepo_init_revisions(repo, &revs, prefix);\n\n"},{"id":"539946","messageId":"bbfd7781-c5ae-48ab-9ac4-8ea752097cb0@gmail.com","threadId":"64529","inReplyTo":"87cy0s0wt5.fsf@iotcl.com","subject":"Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-25T15:38:50Z","receivedAt":"2026-03-25T15:38:56Z","isPatch":true,"body":"\n\nOn 25/03/26 20:40, Toon Claes wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n>> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>>\n>>> diff --git a/builtin/replay.c b/builtin/replay.c\n>>> index 2cdde830a8..d3c1d920f0 100644\n>>> --- a/builtin/replay.c\n>>> +++ b/builtin/replay.c\n>>> @@ -83,7 +83,7 @@ int cmd_replay(int argc,\n>>> ...\n>>>   \t/* Parse ref action mode from command line or config */\n>>>   \tref_mode = get_ref_action_mode(repo, ref_action);\n>>>   \n>>> +\t/*\n>>> +\t * Cherry-pick/rebase need oldest-first ordering so that each\n>>> +\t * replayed commit can build on its already-replayed parent.\n>>> +\t * Revert needs newest-first ordering (like git revert) to\n>>> +\t * reduce conflicts by peeling off changes from the top.\n>>> +\t */\n>>> +\tint desired_reverse = !opts.revert;\n>>> +\n>>\n>> Compiler notices -Werror=declaration-after-statement error here.\n> \n> That's basically the only comment I have on this series.\n> \n> Except for one micro-hit on the existing docs about <revision-range>:\n> \n>      <revision-range>::\n>      \tRange of commits to replay; see \"Specifying Ranges\" in\n>      \tlinkgit:git-rev-parse[1]. In `--advance <branch>` mode, the\n>      \trange should have a single tip, so that it's clear to which tip the\n>      \tadvanced <branch> should point. Any commits in the range whose\n>      \tchanges are already present in the branch the commits are being\n>      \treplayed onto will be dropped.\n> \n> Next to --advance, we should also mention --revert. But that's totally\n> not worth a reroll and can be addressed in any other later series.\n\n\nYeah thanks for catching this, I will include that in v6 since I am \nrerolling anyway for the declaration fix.\n\nThanks,\nSiddharth\n"},{"id":"539955","messageId":"56632ad9-d953-4884-858c-038448f30a1d@gmail.com","threadId":"64529","inReplyTo":"87cy0s0wt5.fsf@iotcl.com","subject":"Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-03-25T16:44:13Z","receivedAt":"2026-03-25T16:44:26Z","isPatch":true,"body":"\n\nOn 25/03/2026 15:10, Toon Claes wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n>> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>>\n>>> diff --git a/builtin/replay.c b/builtin/replay.c\n>>> index 2cdde830a8..d3c1d920f0 100644\n>>> --- a/builtin/replay.c\n>>> +++ b/builtin/replay.c\n>>> @@ -83,7 +83,7 @@ int cmd_replay(int argc,\n>>> ...\n>>>   \t/* Parse ref action mode from command line or config */\n>>>   \tref_mode = get_ref_action_mode(repo, ref_action);\n>>>   \n>>> +\t/*\n>>> +\t * Cherry-pick/rebase need oldest-first ordering so that each\n>>> +\t * replayed commit can build on its already-replayed parent.\n>>> +\t * Revert needs newest-first ordering (like git revert) to\n>>> +\t * reduce conflicts by peeling off changes from the top.\n>>> +\t */\n>>> +\tint desired_reverse = !opts.revert;\n>>> +\n>>\n>> Compiler notices -Werror=declaration-after-statement error here.\n> \n> That's basically the only comment I have on this series.\n\nYes, everything else in the range-diff looks good to me\n\nThanks\n\nPhillip\n\n> Except for one micro-hit on the existing docs about <revision-range>:\n> \n>      <revision-range>::\n>      \tRange of commits to replay; see \"Specifying Ranges\" in\n>      \tlinkgit:git-rev-parse[1]. In `--advance <branch>` mode, the\n>      \trange should have a single tip, so that it's clear to which tip the\n>      \tadvanced <branch> should point. Any commits in the range whose\n>      \tchanges are already present in the branch the commits are being\n>      \treplayed onto will be dropped.\n> \n> Next to --advance, we should also mention --revert. But that's totally\n> not worth a reroll and can be addressed in any other later series.\n> \n\n"},{"id":"540004","messageId":"20260325202354.10628-1-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260324220401.47040-1-siddharthasthana31@gmail.com","subject":"[PATCH v6 0/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-25T20:23:50Z","receivedAt":"2026-03-25T20:23:59Z","isPatch":true,"body":"Hi,\n\ngit replay currently supports cherry-picking (--advance) and rebasing\n(--onto), but not reverting. We need this at GitLab for Gitaly to\nreverse commits directly on bare repositories without a checkout.\n\nThe approach is the same as sequencer.c -- cherry-pick and revert are\njust the same three-way merge with swapped arguments. We swap the base\nand pickme trees passed to merge_incore_nonrecursive() to reverse the\ndiff direction.\n\nPatch 1 extracts the full revert message formatting logic into a new\nsequencer_format_revert_message() function that handles everything in\none shared function rather than just the header. refer_to_commit() is\nupdated to take a struct repository and a bool instead of replay_opts\nso it works outside the sequencer.\n\nPatch 2 adds --revert <branch> as a standalone mode. Reverts are\nprocessed newest-first (matching git revert) to reduce conflicts by\npeeling off changes from the top.\n\nThe series is based on top of d181b9354c (The 13th batch, 2026-03-07).\n\nCI: https://gitlab.com/gitlab-org/git/-/pipelines/2408816732\n\nChanges in v6:\n- Moved desired_reverse declaration to the top of cmd_replay() with\n  other variable declarations to fix -Wdeclaration-after-statement\n- Updated <revision-range> doc to mention --revert alongside --advance\n- Link to v5: https://lore.kernel.org/git/20260324220401.47040-1-siddharthasthana31@gmail.com/\n- Link to v4: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/\n- Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/\n- Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/\n- Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/\n\nThanks,\nSiddharth\n\n---\nSiddharth Asthana (2):\n  sequencer: extract revert message formatting into shared function\n  replay: add --revert mode to reverse commit changes\n\n Documentation/git-replay.adoc |  52 +++++++++--\n builtin/replay.c              |  36 ++++++--\n replay.c                      | 161 +++++++++++++++++++++++++---------\n replay.h                      |  11 ++-\n sequencer.c                   |  78 +++++++++-------\n sequencer.h                   |  13 +++\n t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--\n 7 files changed, 361 insertions(+), 101 deletions(-)\n\nRange-diff versus v5:\n\n1:  6bd2ce4515 = 1:  6bd2ce4515 sequencer: extract revert message formatting into shared function\n2:  9fd92497b9 ! 2:  41fe4861a2 replay: add --revert mode to reverse commit changes\n    @@ Documentation/git-replay.adoc: The history is replayed on top of the <branch> an\n      --contained::\n      \tUpdate all branches that point at commits in\n      \t<revision-range>. Requires `--onto`.\n    +@@ Documentation/git-replay.adoc: The default mode can be configured via the `replay.refAction` configuration vari\n    + \n    + <revision-range>::\n    + \tRange of commits to replay; see \"Specifying Ranges\" in\n    +-\tlinkgit:git-rev-parse[1]. In `--advance <branch>` mode, the\n    +-\trange should have a single tip, so that it's clear to which tip the\n    +-\tadvanced <branch> should point. Any commits in the range whose\n    +-\tchanges are already present in the branch the commits are being\n    ++\tlinkgit:git-rev-parse[1]. In `--advance <branch>` or\n    ++\t`--revert <branch>` mode, the range should have a single tip,\n    ++\tso that it's clear to which tip the advanced or reverted\n    ++\t<branch> should point. Any commits in the range whose changes\n    ++\tare already present in the branch the commits are being\n    + \treplayed onto will be dropped.\n    + \n    + :git-replay: 1\n     @@ Documentation/git-replay.adoc: When using `--ref-action=print`, the output is usable as input to\n      \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n      \n    @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them\n     \n      ## builtin/replay.c ##\n     @@ builtin/replay.c: int cmd_replay(int argc,\n    + \tstruct ref_transaction *transaction = NULL;\n    + \tstruct strbuf transaction_err = STRBUF_INIT;\n    + \tstruct strbuf reflog_msg = STRBUF_INIT;\n    ++\tint desired_reverse;\n    + \tint ret = 0;\n      \n      \tconst char *const replay_usage[] = {\n      \t\tN_(\"(EXPERIMENTAL!) git replay \"\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     +\t * Revert needs newest-first ordering (like git revert) to\n     +\t * reduce conflicts by peeling off changes from the top.\n     +\t */\n    -+\tint desired_reverse = !opts.revert;\n    ++\tdesired_reverse = !opts.revert;\n     +\n      \trepo_init_revisions(repo, &revs, prefix);\n      \n\nbase-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2\n-- \n2.51.0\n\n"},{"id":"540005","messageId":"20260325202354.10628-2-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260325202354.10628-1-siddharthasthana31@gmail.com","subject":"[PATCH v6 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-25T20:23:51Z","receivedAt":"2026-03-25T20:24:04Z","isPatch":true,"body":"The logic for formatting revert commit messages (handling \"Revert\" and\n\"Reapply\" cases, appending \"This reverts commit <ref>.\", and handling\nmerge-parent references) currently lives inline in do_pick_commit().\nThe upcoming replay --revert mode needs to reuse this logic.\n\nExtract all of this into a new sequencer_format_revert_message()\nfunction. The function takes a repository, the subject line, commit,\nparent, a use_commit_reference flag, and the output strbuf. It handles\nboth regular reverts (\"Revert \"<subject>\"\") and revert-of-revert cases\n(\"Reapply \"<subject>\"\"), and uses refer_to_commit() internally to\nformat the commit reference.\n\nUpdate refer_to_commit() to take a struct repository parameter instead\nof relying on the_repository, and a bool instead of reading from\nreplay_opts directly. This makes it usable from the new shared function\nwithout pulling in sequencer-specific state.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------\n sequencer.h | 13 +++++++++\n 2 files changed, 59 insertions(+), 32 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex aafd0bc959..7bf9d6ad19 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) {\n \treturn opts->edit;\n }\n \n-static void refer_to_commit(struct replay_opts *opts,\n-\t\t\t    struct strbuf *msgbuf, struct commit *commit)\n+static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,\n+\t\t\t    const struct commit *commit,\n+\t\t\t    bool use_commit_reference)\n {\n-\tif (opts->commit_use_reference) {\n+\tif (use_commit_reference) {\n \t\tstruct pretty_print_context ctx = {\n \t\t\t.abbrev = DEFAULT_ABBREV,\n \t\t\t.date_mode.type = DATE_SHORT,\n \t\t};\n-\t\trepo_format_commit_message(the_repository, commit,\n+\t\trepo_format_commit_message(r, commit,\n \t\t\t\t\t   \"%h (%s, %ad)\", msgbuf, &ctx);\n \t} else {\n \t\tstrbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));\n@@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r,\n \t */\n \n \tif (command == TODO_REVERT) {\n-\t\tconst char *orig_subject;\n-\n \t\tbase = commit;\n \t\tbase_label = msg.label;\n \t\tnext = parent;\n \t\tnext_label = msg.parent_label;\n-\t\tif (opts->commit_use_reference) {\n-\t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n-\t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n-\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n-\t\t\t   /*\n-\t\t\t    * We don't touch pre-existing repeated reverts, because\n-\t\t\t    * theoretically these can be nested arbitrarily deeply,\n-\t\t\t    * thus requiring excessive complexity to deal with.\n-\t\t\t    */\n-\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n-\t\t} else {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n-\t\trefer_to_commit(opts, &ctx->message, commit);\n-\n-\t\tif (commit->parents && commit->parents->next) {\n-\t\t\tstrbuf_addstr(&ctx->message, \", reversing\\nchanges made to \");\n-\t\t\trefer_to_commit(opts, &ctx->message, parent);\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \".\\n\");\n+\t\tsequencer_format_revert_message(r, msg.subject, commit,\n+\t\t\t\t\t\tparent,\n+\t\t\t\t\t\topts->commit_use_reference,\n+\t\t\t\t\t\t&ctx->message);\n \t} else {\n \t\tconst char *p;\n \n@@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r,\n \treturn res;\n }\n \n+void sequencer_format_revert_message(struct repository *r,\n+\t\t\t\t     const char *subject,\n+\t\t\t\t     const struct commit *commit,\n+\t\t\t\t     const struct commit *parent,\n+\t\t\t\t     bool use_commit_reference,\n+\t\t\t\t     struct strbuf *message)\n+{\n+\tconst char *orig_subject;\n+\n+\tif (use_commit_reference) {\n+\t\tstrbuf_commented_addf(message, comment_line_str,\n+\t\t\t\t      \"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n+\t} else if (skip_prefix(subject, \"Revert \\\"\", &orig_subject) &&\n+\t\t   /*\n+\t\t    * We don't touch pre-existing repeated reverts, because\n+\t\t    * theoretically these can be nested arbitrarily deeply,\n+\t\t    * thus requiring excessive complexity to deal with.\n+\t\t    */\n+\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n+\t\tstrbuf_addstr(message, \"Reapply \\\"\");\n+\t\tstrbuf_addstr(message, orig_subject);\n+\t\tstrbuf_addstr(message, \"\\n\");\n+\t} else {\n+\t\tstrbuf_addstr(message, \"Revert \\\"\");\n+\t\tstrbuf_addstr(message, subject);\n+\t\tstrbuf_addstr(message, \"\\\"\\n\");\n+\t}\n+\tstrbuf_addstr(message, \"\\nThis reverts commit \");\n+\trefer_to_commit(r, message, commit, use_commit_reference);\n+\n+\tif (commit->parents && commit->parents->next) {\n+\t\tstrbuf_addstr(message, \", reversing\\nchanges made to \");\n+\t\trefer_to_commit(r, message, parent, use_commit_reference);\n+\t}\n+\tstrbuf_addstr(message, \".\\n\");\n+}\n+\n void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n {\n \tunsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;\ndiff --git a/sequencer.h b/sequencer.h\nindex 719684c8a9..56cd50233a 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -271,4 +271,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n  */\n int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n \n+/*\n+ * Format a revert commit message with appropriate 'Revert \"<subject>\"' or\n+ * 'Reapply \"<subject>\"' prefix and 'This reverts commit <ref>.' body.\n+ * When use_commit_reference is set, <ref> is an abbreviated hash with\n+ * subject and date; otherwise the full hex hash is used.\n+ */\n+void sequencer_format_revert_message(struct repository *r,\n+\t\t\t\t     const char *subject,\n+\t\t\t\t     const struct commit *commit,\n+\t\t\t\t     const struct commit *parent,\n+\t\t\t\t     bool use_commit_reference,\n+\t\t\t\t     struct strbuf *message);\n+\n #endif /* SEQUENCER_H */\n-- \n2.51.0\n\n"},{"id":"540006","messageId":"20260325202354.10628-3-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260325202354.10628-1-siddharthasthana31@gmail.com","subject":"[PATCH v6 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-25T20:23:52Z","receivedAt":"2026-03-25T20:24:09Z","isPatch":true,"body":"Add a `--revert <branch>` mode to git replay that undoes the changes\nintroduced by the specified commits. Like --onto and --advance, --revert\nis a standalone mode: it takes a branch argument and updates that branch\nwith the newly created revert commits.\n\nAt GitLab, we need this in Gitaly for reverting commits directly on bare\nrepositories without requiring a working tree checkout.\n\nThe approach is the same as sequencer.c's do_pick_commit() -- cherry-pick\nand revert are just the same three-way merge with swapped arguments:\n\n  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n  - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n\nWe swap the base and pickme trees passed to merge_incore_nonrecursive()\nto reverse the diff direction.\n\nReverts are processed newest-first (matching git revert behavior) to\nreduce conflicts by peeling off changes from the top. Each revert\nbuilds on the result of the previous one via the last_commit fallback\nin the main replay loop, rather than relying on the parent-mapping\nused for cherry-pick.\n\nRevert commit messages follow the usual git revert conventions: prefixed\nwith \"Revert\" (or \"Reapply\" when reverting a revert), and including\n\"This reverts commit <hash>.\". The author is set to the current user\nrather than preserving the original author, matching git revert behavior.\n\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nHelped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Toon Claes <toon@iotcl.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  52 +++++++++--\n builtin/replay.c              |  36 ++++++--\n replay.c                      | 161 +++++++++++++++++++++++++---------\n replay.h                      |  11 ++-\n t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--\n 5 files changed, 302 insertions(+), 69 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 8d696ce3ab..997097e420 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to\n point at the tip of the resulting history. This is different from `--onto`,\n which uses the target only as a starting point without updating it.\n \n+--revert <branch>::\n+\tStarting point at which to create the reverted commits; must be a\n+\tbranch name.\n++\n+When `--revert` is specified, the commits in the revision range are reverted\n+(their changes are undone) and the reverted commits are created on top of\n+<branch>. The <branch> is then updated to point at the new commits. This is\n+the same as running `git revert <revision-range>` but does not update the\n+working tree.\n++\n+The commit messages follow `git revert` conventions: they are prefixed with\n+\"Revert\" and include \"This reverts commit <hash>.\" When reverting a commit\n+whose message starts with \"Revert\", the new message uses \"Reapply\" instead.\n+Unlike cherry-pick which preserves the original author, revert commits use\n+the current user as the author, matching the behavior of `git revert`.\n++\n+This option is mutually exclusive with `--onto` and `--advance`. It is also\n+incompatible with `--contained` (which is a modifier for `--onto` only).\n+\n --contained::\n \tUpdate all branches that point at commits in\n \t<revision-range>. Requires `--onto`.\n@@ -60,10 +79,11 @@ The default mode can be configured via the `replay.refAction` configuration vari\n \n <revision-range>::\n \tRange of commits to replay; see \"Specifying Ranges\" in\n-\tlinkgit:git-rev-parse[1]. In `--advance <branch>` mode, the\n-\trange should have a single tip, so that it's clear to which tip the\n-\tadvanced <branch> should point. Any commits in the range whose\n-\tchanges are already present in the branch the commits are being\n+\tlinkgit:git-rev-parse[1]. In `--advance <branch>` or\n+\t`--revert <branch>` mode, the range should have a single tip,\n+\tso that it's clear to which tip the advanced or reverted\n+\t<branch> should point. Any commits in the range whose changes\n+\tare already present in the branch the commits are being\n \treplayed onto will be dropped.\n \n :git-replay: 1\n@@ -84,9 +104,10 @@ When using `--ref-action=print`, the output is usable as input to\n \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n \n where the number of refs updated depends on the arguments passed and\n-the shape of the history being replayed.  When using `--advance`, the\n-number of refs updated is always one, but for `--onto`, it can be one\n-or more (rebasing multiple branches simultaneously is supported).\n+the shape of the history being replayed.  When using `--advance` or\n+`--revert`, the number of refs updated is always one, but for `--onto`,\n+it can be one or more (rebasing multiple branches simultaneously is\n+supported).\n \n There is no stderr output on conflicts; see the <<exit-status,EXIT\n STATUS>> section below.\n@@ -152,6 +173,21 @@ all commits they have since `base`, playing them on top of\n `origin/main`. These three branches may have commits on top of `base`\n that they have in common, but that does not need to be the case.\n \n+To revert commits on a branch:\n+\n+------------\n+$ git replay --revert main topic~2..topic\n+------------\n+\n+This reverts the last two commits from `topic`, creating revert commits on\n+top of `main`, and updates `main` to point at the result. This is useful when\n+commits from `topic` were previously merged or cherry-picked into `main` and\n+need to be undone.\n+\n+NOTE: For reverting an entire merge request as a single commit (rather than\n+commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n+which can avoid unnecessary merge conflicts.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 2cdde830a8..a0879b020f 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -79,11 +79,12 @@ int cmd_replay(int argc,\n \tstruct ref_transaction *transaction = NULL;\n \tstruct strbuf transaction_err = STRBUF_INIT;\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n+\tint desired_reverse;\n \tint ret = 0;\n \n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n+\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n \t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n \t\tNULL\n \t};\n@@ -96,6 +97,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n+\t\tOPT_STRING(0, \"revert\", &opts.revert,\n+\t\t\t   N_(\"branch\"),\n+\t\t\t   N_(\"revert commits onto given branch\")),\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n \t\t\t   N_(\"control ref update behavior (update|print)\")),\n@@ -105,19 +109,31 @@ int cmd_replay(int argc,\n \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n-\tif (!opts.onto && !opts.advance) {\n-\t\terror(_(\"option --onto or --advance is mandatory\"));\n+\t/* Exactly one mode must be specified */\n+\tif (!opts.onto && !opts.advance && !opts.revert) {\n+\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n+\tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n+\t\t\t\t  !!opts.advance, \"--advance\",\n+\t\t\t\t  !!opts.revert, \"--revert\");\n \tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n \t\t\t\t  opts.contained, \"--contained\");\n-\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n-\t\t\t\t  !!opts.onto, \"--onto\");\n+\tdie_for_incompatible_opt2(!!opts.revert, \"--revert\",\n+\t\t\t\t  opts.contained, \"--contained\");\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n \n+\t/*\n+\t * Cherry-pick/rebase need oldest-first ordering so that each\n+\t * replayed commit can build on its already-replayed parent.\n+\t * Revert needs newest-first ordering (like git revert) to\n+\t * reduce conflicts by peeling off changes from the top.\n+\t */\n+\tdesired_reverse = !opts.revert;\n+\n \trepo_init_revisions(repo, &revs, prefix);\n \n \t/*\n@@ -129,7 +145,7 @@ int cmd_replay(int argc,\n \t * some options changing these values if we think they could\n \t * be useful.\n \t */\n-\trevs.reverse = 1;\n+\trevs.reverse = desired_reverse;\n \trevs.sort_order = REV_SORT_IN_GRAPH_ORDER;\n \trevs.topo_order = 1;\n \trevs.simplify_history = 0;\n@@ -144,11 +160,11 @@ int cmd_replay(int argc,\n \t * Detect and warn if we override some user specified rev\n \t * walking options.\n \t */\n-\tif (revs.reverse != 1) {\n+\tif (revs.reverse != desired_reverse) {\n \t\twarning(_(\"some rev walking options will be overridden as \"\n \t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n \t\t\t\"reverse\");\n-\t\trevs.reverse = 1;\n+\t\trevs.reverse = desired_reverse;\n \t}\n \tif (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {\n \t\twarning(_(\"some rev walking options will be overridden as \"\n@@ -174,7 +190,9 @@ int cmd_replay(int argc,\n \t\tgoto cleanup;\n \n \t/* Build reflog message */\n-\tif (opts.advance) {\n+\tif (opts.revert) {\n+\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", opts.revert);\n+\t} else if (opts.advance) {\n \t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n \t} else {\n \t\tstruct object_id oid;\ndiff --git a/replay.c b/replay.c\nindex a63f6714c4..d7239d4c83 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -8,6 +8,7 @@\n #include \"refs.h\"\n #include \"replay.h\"\n #include \"revision.h\"\n+#include \"sequencer.h\"\n #include \"strmap.h\"\n #include \"tree.h\"\n \n@@ -17,6 +18,11 @@\n  */\n #define the_repository DO_NOT_USE_THE_REPOSITORY\n \n+enum replay_mode {\n+\tREPLAY_MODE_PICK,\n+\tREPLAY_MODE_REVERT,\n+};\n+\n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n {\n@@ -50,15 +56,37 @@ static char *get_author(const char *message)\n \treturn NULL;\n }\n \n+static void generate_revert_message(struct strbuf *msg,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct repository *repo)\n+{\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n+\tconst char *subject_start;\n+\tint subject_len;\n+\tchar *subject;\n+\n+\tsubject_len = find_commit_subject(message, &subject_start);\n+\tsubject = xmemdupz(subject_start, subject_len);\n+\n+\tsequencer_format_revert_message(repo, subject, commit,\n+\t\t\t\t\tcommit->parents ? commit->parents->item : NULL,\n+\t\t\t\t\tfalse, msg);\n+\n+\tfree(subject);\n+\trepo_unuse_commit_buffer(repo, commit, message);\n+}\n+\n static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n+\t\t\t\t    struct commit *parent,\n+\t\t\t\t    enum replay_mode mode)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n-\tchar *author;\n+\tchar *author = NULL;\n \tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n@@ -70,9 +98,16 @@ static struct commit *create_commit(struct repository *repo,\n \n \tcommit_list_insert(parent, &parents);\n \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n+\tif (mode == REPLAY_MODE_REVERT) {\n+\t\tgenerate_revert_message(&msg, based_on, repo);\n+\t\t/* For revert, use current user as author (NULL = use default) */\n+\t} else if (mode == REPLAY_MODE_PICK) {\n+\t\tfind_commit_subject(message, &orig_message);\n+\t\tstrbuf_addstr(&msg, orig_message);\n+\t\tauthor = get_author(message);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \treset_ident_date();\n \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n@@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo,\n \t}\n }\n \n+static void set_up_branch_mode(struct repository *repo,\n+\t\t\t       char **branch_name,\n+\t\t\t       const char *option_name,\n+\t\t\t       struct ref_info *rinfo,\n+\t\t\t       struct commit **onto)\n+{\n+\tstruct object_id oid;\n+\tchar *fullname = NULL;\n+\n+\tif (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),\n+\t\t\t  &oid, &fullname, 0) == 1) {\n+\t\tfree(*branch_name);\n+\t\t*branch_name = fullname;\n+\t} else {\n+\t\tdie(_(\"argument to %s must be a reference\"), option_name);\n+\t}\n+\t*onto = peel_committish(repo, *branch_name, option_name);\n+\tif (rinfo->positive_refexprs > 1)\n+\t\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n+\t\t      \"because the ordering would be ill-defined\"),\n+\t\t    option_name);\n+}\n+\n static void set_up_replay_mode(struct repository *repo,\n \t\t\t       struct rev_cmdline_info *cmd_info,\n \t\t\t       const char *onto_name,\n \t\t\t       bool *detached_head,\n \t\t\t       char **advance_name,\n+\t\t\t       char **revert_name,\n \t\t\t       struct commit **onto,\n \t\t\t       struct strset **update_refs)\n {\n@@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo,\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n \n-\tif (!onto_name == !*advance_name)\n-\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n-\n \tif (onto_name) {\n \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n \t\tif (rinfo.positive_refexprs <\n@@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo,\n \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n \t\t**update_refs = rinfo.positive_refs;\n \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n+\t} else if (*advance_name) {\n+\t\tset_up_branch_mode(repo, advance_name, \"--advance\", &rinfo, onto);\n+\t} else if (*revert_name) {\n+\t\tset_up_branch_mode(repo, revert_name, \"--revert\", &rinfo, onto);\n \t} else {\n-\t\tstruct object_id oid;\n-\t\tchar *fullname = NULL;\n-\n-\t\tif (!*advance_name)\n-\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n-\n-\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n-\t\t\t     &oid, &fullname, 0) == 1) {\n-\t\t\tfree(*advance_name);\n-\t\t\t*advance_name = fullname;\n-\t\t} else {\n-\t\t\tdie(_(\"argument to --advance must be a reference\"));\n-\t\t}\n-\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n-\t\tif (rinfo.positive_refexprs > 1)\n-\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n+\t\tBUG(\"expected one of onto_name, *advance_name, or *revert_name\");\n \t}\n \tstrset_clear(&rinfo.negative_refs);\n \tstrset_clear(&rinfo.positive_refs);\n@@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n \t\t\t\t\t  struct commit *onto,\n \t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n+\t\t\t\t\t  struct merge_result *result,\n+\t\t\t\t\t  enum replay_mode mode)\n {\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n@@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  replayed_base_tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n+\tif (mode == REPLAY_MODE_PICK) {\n+\t\t/* Cherry-pick: normal order */\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->ancestor);\n+\t} else if (mode == REPLAY_MODE_REVERT) {\n+\t\t/* Revert: swap base and pickme to reverse the diff */\n+\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n+\t\tmerge_opt->ancestor = pickme_name;\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->branch2);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \tmerge_opt->ancestor = NULL;\n+\tmerge_opt->branch2 = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n \t/* Drop commits that become empty */\n \tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n \t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n \t\treturn replayed_base;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs,\n \t};\n \tbool detached_head;\n \tchar *advance;\n+\tchar *revert;\n+\tenum replay_mode mode = REPLAY_MODE_PICK;\n \tint ret;\n \n \tadvance = xstrdup_or_null(opts->advance);\n+\trevert = xstrdup_or_null(opts->revert);\n+\tif (revert)\n+\t\tmode = REPLAY_MODE_REVERT;\n \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n-\t\t\t   &detached_head, &advance, &onto, &update_refs);\n+\t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n \n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n@@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs,\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n+\t\t\t\t\t\t  &merge_opt, &result, mode);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \n@@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance)\n+\t\tif (advance || revert)\n \t\t\tcontinue;\n \n \t\tfor (decoration = get_name_decoration(&commit->object);\n@@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs,\n \t\tgoto out;\n \t}\n \n-\t/* In --advance mode, advance the target ref */\n-\tif (advance)\n-\t\treplay_result_queue_update(out, advance,\n+\t/* In --advance or --revert mode, update the target ref */\n+\tif (advance || revert) {\n+\t\tconst char *ref = advance ? advance : revert;\n+\t\treplay_result_queue_update(out, ref,\n \t\t\t\t\t   &onto->object.oid,\n \t\t\t\t\t   &last_commit->object.oid);\n+\t}\n \n \tret = 0;\n \n@@ -377,5 +451,6 @@ int replay_revisions(struct rev_info *revs,\n \tkh_destroy_oid_map(replayed_commits);\n \tmerge_finalize(&merge_opt, &result);\n \tfree(advance);\n+\tfree(revert);\n \treturn ret;\n }\ndiff --git a/replay.h b/replay.h\nindex d8407dc7f7..e916a5f975 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -13,7 +13,7 @@ struct replay_revisions_options {\n \t/*\n \t * Starting point at which to create the new commits; must be a branch\n \t * name. The branch will be updated to point to the rewritten commits.\n-\t * This option is mutually exclusive with `onto`.\n+\t * This option is mutually exclusive with `onto` and `revert`.\n \t */\n \tconst char *advance;\n \n@@ -22,7 +22,14 @@ struct replay_revisions_options {\n \t * committish. References pointing at decendants of `onto` will be\n \t * updated to point to the new commits.\n \t */\n-\t const char *onto;\n+\tconst char *onto;\n+\n+\t/*\n+\t * Starting point at which to create revert commits; must be a branch\n+\t * name. The branch will be updated to point to the revert commits.\n+\t * This option is mutually exclusive with `onto` and `advance`.\n+\t */\n+\tconst char *revert;\n \n \t/*\n \t * Update branches that point at commits in the given revision range.\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex a03f8f9293..217f6fb292 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'option --onto or --advance is mandatory' '\n-\techo \"error: option --onto or --advance is mandatory\" >expect &&\n+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '\n+\techo \"error: exactly one of --onto, --advance, or --revert is required\" >expect &&\n \ttest_might_fail git replay -h >>expect &&\n \ttest_must_fail git replay topic1..topic2 2>actual &&\n \ttest_cmp expect actual\n@@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'options --advance and --contained cannot be used together' '\n-\tprintf \"fatal: options ${SQ}--advance${SQ} \" >expect &&\n-\tprintf \"and ${SQ}--contained${SQ} cannot be used together\\n\" >>expect &&\n+test_expect_success '--advance and --contained cannot be used together' '\n \ttest_must_fail git replay --advance=main --contained \\\n \t\ttopic1..topic2 2>actual &&\n-\ttest_cmp expect actual\n+\ttest_grep \"cannot be used together\" actual\n '\n \n test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n-\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n+\techo \"fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\" >expect &&\n \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n \ttest_cmp expect actual\n '\n@@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' '\n \ttest_grep \"invalid.*replay.refAction.*value\" error\n '\n \n+test_expect_success 'argument to --revert must be a reference' '\n+\techo \"fatal: argument to --revert must be a reference\" >expect &&\n+\toid=$(git rev-parse main) &&\n+\ttest_must_fail git replay --revert=$oid topic1..topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'cannot revert with multiple sources' '\n+\techo \"fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\" >expect &&\n+\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using replay --revert to revert commits' '\n+\t# Reuse existing topic4 branch (has commits I and J on top of main)\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# Revert commits I and J\n+\tgit replay --revert topic4 topic4~2..topic4 &&\n+\n+\t# Verify the revert commits were created (newest-first ordering\n+\t# means J is reverted first, then I on top)\n+\tgit log --format=%s -4 topic4 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\tRevert \"I\"\n+\tRevert \"J\"\n+\tJ\n+\tI\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\t# Verify commit message format includes hash (tip is Revert \"I\")\n+\ttest_commit_message topic4 <<-EOF &&\n+\tRevert \"I\"\n+\n+\tThis reverts commit $(git rev-parse I).\n+\tEOF\n+\n+\t# Verify reflog message\n+\tgit reflog topic4 -1 --format=%gs >reflog-msg &&\n+\techo \"replay --revert topic4\" >expect-reflog &&\n+\ttest_cmp expect-reflog reflog-msg\n+'\n+\n+test_expect_success 'using replay --revert in bare repo' '\n+\t# Reuse existing topic4 in bare repo\n+\tSTART=$(git -C bare rev-parse topic4) &&\n+\ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n+\n+\t# Revert commit J in bare repo\n+\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify revert was created\n+\tgit -C bare log -1 --format=%s topic4 >actual &&\n+\techo \"Revert \\\"J\\\"\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'revert of revert uses Reapply' '\n+\t# Use topic4 and first revert J, then revert the revert\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# First revert J\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\tREVERT_J=$(git rev-parse topic4) &&\n+\n+\t# Now revert the revert - should become Reapply\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify Reapply prefix and message format\n+\ttest_commit_message topic4 <<-EOF\n+\tReapply \"J\"\n+\n+\tThis reverts commit $REVERT_J.\n+\tEOF\n+'\n+\n+test_expect_success 'git replay --revert with conflict' '\n+\t# conflict branch has C.conflict which conflicts with topic1s C\n+\ttest_expect_code 1 git replay --revert conflict B..topic1\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --contained' '\n+\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --onto' '\n+\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --advance' '\n+\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"540007","messageId":"20260325202354.10628-4-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260325202354.10628-1-siddharthasthana31@gmail.com","subject":"[PATCH v6 1/2] sequencer: extract revert message formatting into shared function","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-25T20:23:53Z","receivedAt":"2026-03-25T20:24:18Z","isPatch":true,"body":"The logic for formatting revert commit messages (handling \"Revert\" and\n\"Reapply\" cases, appending \"This reverts commit <ref>.\", and handling\nmerge-parent references) currently lives inline in do_pick_commit().\nThe upcoming replay --revert mode needs to reuse this logic.\n\nExtract all of this into a new sequencer_format_revert_message()\nfunction. The function takes a repository, the subject line, commit,\nparent, a use_commit_reference flag, and the output strbuf. It handles\nboth regular reverts (\"Revert \"<subject>\"\") and revert-of-revert cases\n(\"Reapply \"<subject>\"\"), and uses refer_to_commit() internally to\nformat the commit reference.\n\nUpdate refer_to_commit() to take a struct repository parameter instead\nof relying on the_repository, and a bool instead of reading from\nreplay_opts directly. This makes it usable from the new shared function\nwithout pulling in sequencer-specific state.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n sequencer.c | 78 +++++++++++++++++++++++++++++++----------------------\n sequencer.h | 13 +++++++++\n 2 files changed, 59 insertions(+), 32 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex aafd0bc959..7bf9d6ad19 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) {\n \treturn opts->edit;\n }\n \n-static void refer_to_commit(struct replay_opts *opts,\n-\t\t\t    struct strbuf *msgbuf, struct commit *commit)\n+static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,\n+\t\t\t    const struct commit *commit,\n+\t\t\t    bool use_commit_reference)\n {\n-\tif (opts->commit_use_reference) {\n+\tif (use_commit_reference) {\n \t\tstruct pretty_print_context ctx = {\n \t\t\t.abbrev = DEFAULT_ABBREV,\n \t\t\t.date_mode.type = DATE_SHORT,\n \t\t};\n-\t\trepo_format_commit_message(the_repository, commit,\n+\t\trepo_format_commit_message(r, commit,\n \t\t\t\t\t   \"%h (%s, %ad)\", msgbuf, &ctx);\n \t} else {\n \t\tstrbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));\n@@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r,\n \t */\n \n \tif (command == TODO_REVERT) {\n-\t\tconst char *orig_subject;\n-\n \t\tbase = commit;\n \t\tbase_label = msg.label;\n \t\tnext = parent;\n \t\tnext_label = msg.parent_label;\n-\t\tif (opts->commit_use_reference) {\n-\t\t\tstrbuf_commented_addf(&ctx->message, comment_line_str,\n-\t\t\t\t\"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n-\t\t} else if (skip_prefix(msg.subject, \"Revert \\\"\", &orig_subject) &&\n-\t\t\t   /*\n-\t\t\t    * We don't touch pre-existing repeated reverts, because\n-\t\t\t    * theoretically these can be nested arbitrarily deeply,\n-\t\t\t    * thus requiring excessive complexity to deal with.\n-\t\t\t    */\n-\t\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Reapply \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, orig_subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\n\");\n-\t\t} else {\n-\t\t\tstrbuf_addstr(&ctx->message, \"Revert \\\"\");\n-\t\t\tstrbuf_addstr(&ctx->message, msg.subject);\n-\t\t\tstrbuf_addstr(&ctx->message, \"\\\"\\n\");\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \"\\nThis reverts commit \");\n-\t\trefer_to_commit(opts, &ctx->message, commit);\n-\n-\t\tif (commit->parents && commit->parents->next) {\n-\t\t\tstrbuf_addstr(&ctx->message, \", reversing\\nchanges made to \");\n-\t\t\trefer_to_commit(opts, &ctx->message, parent);\n-\t\t}\n-\t\tstrbuf_addstr(&ctx->message, \".\\n\");\n+\t\tsequencer_format_revert_message(r, msg.subject, commit,\n+\t\t\t\t\t\tparent,\n+\t\t\t\t\t\topts->commit_use_reference,\n+\t\t\t\t\t\t&ctx->message);\n \t} else {\n \t\tconst char *p;\n \n@@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r,\n \treturn res;\n }\n \n+void sequencer_format_revert_message(struct repository *r,\n+\t\t\t\t     const char *subject,\n+\t\t\t\t     const struct commit *commit,\n+\t\t\t\t     const struct commit *parent,\n+\t\t\t\t     bool use_commit_reference,\n+\t\t\t\t     struct strbuf *message)\n+{\n+\tconst char *orig_subject;\n+\n+\tif (use_commit_reference) {\n+\t\tstrbuf_commented_addf(message, comment_line_str,\n+\t\t\t\t      \"*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***\");\n+\t} else if (skip_prefix(subject, \"Revert \\\"\", &orig_subject) &&\n+\t\t   /*\n+\t\t    * We don't touch pre-existing repeated reverts, because\n+\t\t    * theoretically these can be nested arbitrarily deeply,\n+\t\t    * thus requiring excessive complexity to deal with.\n+\t\t    */\n+\t\t   !starts_with(orig_subject, \"Revert \\\"\")) {\n+\t\tstrbuf_addstr(message, \"Reapply \\\"\");\n+\t\tstrbuf_addstr(message, orig_subject);\n+\t\tstrbuf_addstr(message, \"\\n\");\n+\t} else {\n+\t\tstrbuf_addstr(message, \"Revert \\\"\");\n+\t\tstrbuf_addstr(message, subject);\n+\t\tstrbuf_addstr(message, \"\\\"\\n\");\n+\t}\n+\tstrbuf_addstr(message, \"\\nThis reverts commit \");\n+\trefer_to_commit(r, message, commit, use_commit_reference);\n+\n+\tif (commit->parents && commit->parents->next) {\n+\t\tstrbuf_addstr(message, \", reversing\\nchanges made to \");\n+\t\trefer_to_commit(r, message, parent, use_commit_reference);\n+\t}\n+\tstrbuf_addstr(message, \".\\n\");\n+}\n+\n void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)\n {\n \tunsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;\ndiff --git a/sequencer.h b/sequencer.h\nindex 719684c8a9..56cd50233a 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -271,4 +271,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)\n  */\n int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);\n \n+/*\n+ * Format a revert commit message with appropriate 'Revert \"<subject>\"' or\n+ * 'Reapply \"<subject>\"' prefix and 'This reverts commit <ref>.' body.\n+ * When use_commit_reference is set, <ref> is an abbreviated hash with\n+ * subject and date; otherwise the full hex hash is used.\n+ */\n+void sequencer_format_revert_message(struct repository *r,\n+\t\t\t\t     const char *subject,\n+\t\t\t\t     const struct commit *commit,\n+\t\t\t\t     const struct commit *parent,\n+\t\t\t\t     bool use_commit_reference,\n+\t\t\t\t     struct strbuf *message);\n+\n #endif /* SEQUENCER_H */\n-- \n2.51.0\n\n"},{"id":"540008","messageId":"20260325202354.10628-5-siddharthasthana31@gmail.com","threadId":"64529","inReplyTo":"20260325202354.10628-1-siddharthasthana31@gmail.com","subject":"[PATCH v6 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-25T20:23:54Z","receivedAt":"2026-03-25T20:24:32Z","isPatch":true,"body":"Add a `--revert <branch>` mode to git replay that undoes the changes\nintroduced by the specified commits. Like --onto and --advance, --revert\nis a standalone mode: it takes a branch argument and updates that branch\nwith the newly created revert commits.\n\nAt GitLab, we need this in Gitaly for reverting commits directly on bare\nrepositories without requiring a working tree checkout.\n\nThe approach is the same as sequencer.c's do_pick_commit() -- cherry-pick\nand revert are just the same three-way merge with swapped arguments:\n\n  - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit)\n  - Revert: merge(ancestor=commit, ours=current, theirs=parent)\n\nWe swap the base and pickme trees passed to merge_incore_nonrecursive()\nto reverse the diff direction.\n\nReverts are processed newest-first (matching git revert behavior) to\nreduce conflicts by peeling off changes from the top. Each revert\nbuilds on the result of the previous one via the last_commit fallback\nin the main replay loop, rather than relying on the parent-mapping\nused for cherry-pick.\n\nRevert commit messages follow the usual git revert conventions: prefixed\nwith \"Revert\" (or \"Reapply\" when reverting a revert), and including\n\"This reverts commit <hash>.\". The author is set to the current user\nrather than preserving the original author, matching git revert behavior.\n\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nHelped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Toon Claes <toon@iotcl.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  52 +++++++++--\n builtin/replay.c              |  36 ++++++--\n replay.c                      | 161 +++++++++++++++++++++++++---------\n replay.h                      |  11 ++-\n t/t3650-replay-basics.sh      | 111 +++++++++++++++++++++--\n 5 files changed, 302 insertions(+), 69 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 8d696ce3ab..997097e420 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to\n point at the tip of the resulting history. This is different from `--onto`,\n which uses the target only as a starting point without updating it.\n \n+--revert <branch>::\n+\tStarting point at which to create the reverted commits; must be a\n+\tbranch name.\n++\n+When `--revert` is specified, the commits in the revision range are reverted\n+(their changes are undone) and the reverted commits are created on top of\n+<branch>. The <branch> is then updated to point at the new commits. This is\n+the same as running `git revert <revision-range>` but does not update the\n+working tree.\n++\n+The commit messages follow `git revert` conventions: they are prefixed with\n+\"Revert\" and include \"This reverts commit <hash>.\" When reverting a commit\n+whose message starts with \"Revert\", the new message uses \"Reapply\" instead.\n+Unlike cherry-pick which preserves the original author, revert commits use\n+the current user as the author, matching the behavior of `git revert`.\n++\n+This option is mutually exclusive with `--onto` and `--advance`. It is also\n+incompatible with `--contained` (which is a modifier for `--onto` only).\n+\n --contained::\n \tUpdate all branches that point at commits in\n \t<revision-range>. Requires `--onto`.\n@@ -60,10 +79,11 @@ The default mode can be configured via the `replay.refAction` configuration vari\n \n <revision-range>::\n \tRange of commits to replay; see \"Specifying Ranges\" in\n-\tlinkgit:git-rev-parse[1]. In `--advance <branch>` mode, the\n-\trange should have a single tip, so that it's clear to which tip the\n-\tadvanced <branch> should point. Any commits in the range whose\n-\tchanges are already present in the branch the commits are being\n+\tlinkgit:git-rev-parse[1]. In `--advance <branch>` or\n+\t`--revert <branch>` mode, the range should have a single tip,\n+\tso that it's clear to which tip the advanced or reverted\n+\t<branch> should point. Any commits in the range whose changes\n+\tare already present in the branch the commits are being\n \treplayed onto will be dropped.\n \n :git-replay: 1\n@@ -84,9 +104,10 @@ When using `--ref-action=print`, the output is usable as input to\n \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n \n where the number of refs updated depends on the arguments passed and\n-the shape of the history being replayed.  When using `--advance`, the\n-number of refs updated is always one, but for `--onto`, it can be one\n-or more (rebasing multiple branches simultaneously is supported).\n+the shape of the history being replayed.  When using `--advance` or\n+`--revert`, the number of refs updated is always one, but for `--onto`,\n+it can be one or more (rebasing multiple branches simultaneously is\n+supported).\n \n There is no stderr output on conflicts; see the <<exit-status,EXIT\n STATUS>> section below.\n@@ -152,6 +173,21 @@ all commits they have since `base`, playing them on top of\n `origin/main`. These three branches may have commits on top of `base`\n that they have in common, but that does not need to be the case.\n \n+To revert commits on a branch:\n+\n+------------\n+$ git replay --revert main topic~2..topic\n+------------\n+\n+This reverts the last two commits from `topic`, creating revert commits on\n+top of `main`, and updates `main` to point at the result. This is useful when\n+commits from `topic` were previously merged or cherry-picked into `main` and\n+need to be undone.\n+\n+NOTE: For reverting an entire merge request as a single commit (rather than\n+commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n+which can avoid unnecessary merge conflicts.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 2cdde830a8..a0879b020f 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -79,11 +79,12 @@ int cmd_replay(int argc,\n \tstruct ref_transaction *transaction = NULL;\n \tstruct strbuf transaction_err = STRBUF_INIT;\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n+\tint desired_reverse;\n \tint ret = 0;\n \n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n+\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n \t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n \t\tNULL\n \t};\n@@ -96,6 +97,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n+\t\tOPT_STRING(0, \"revert\", &opts.revert,\n+\t\t\t   N_(\"branch\"),\n+\t\t\t   N_(\"revert commits onto given branch\")),\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n \t\t\t   N_(\"control ref update behavior (update|print)\")),\n@@ -105,19 +109,31 @@ int cmd_replay(int argc,\n \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n-\tif (!opts.onto && !opts.advance) {\n-\t\terror(_(\"option --onto or --advance is mandatory\"));\n+\t/* Exactly one mode must be specified */\n+\tif (!opts.onto && !opts.advance && !opts.revert) {\n+\t\terror(_(\"exactly one of --onto, --advance, or --revert is required\"));\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n+\tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n+\t\t\t\t  !!opts.advance, \"--advance\",\n+\t\t\t\t  !!opts.revert, \"--revert\");\n \tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n \t\t\t\t  opts.contained, \"--contained\");\n-\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n-\t\t\t\t  !!opts.onto, \"--onto\");\n+\tdie_for_incompatible_opt2(!!opts.revert, \"--revert\",\n+\t\t\t\t  opts.contained, \"--contained\");\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n \n+\t/*\n+\t * Cherry-pick/rebase need oldest-first ordering so that each\n+\t * replayed commit can build on its already-replayed parent.\n+\t * Revert needs newest-first ordering (like git revert) to\n+\t * reduce conflicts by peeling off changes from the top.\n+\t */\n+\tdesired_reverse = !opts.revert;\n+\n \trepo_init_revisions(repo, &revs, prefix);\n \n \t/*\n@@ -129,7 +145,7 @@ int cmd_replay(int argc,\n \t * some options changing these values if we think they could\n \t * be useful.\n \t */\n-\trevs.reverse = 1;\n+\trevs.reverse = desired_reverse;\n \trevs.sort_order = REV_SORT_IN_GRAPH_ORDER;\n \trevs.topo_order = 1;\n \trevs.simplify_history = 0;\n@@ -144,11 +160,11 @@ int cmd_replay(int argc,\n \t * Detect and warn if we override some user specified rev\n \t * walking options.\n \t */\n-\tif (revs.reverse != 1) {\n+\tif (revs.reverse != desired_reverse) {\n \t\twarning(_(\"some rev walking options will be overridden as \"\n \t\t\t  \"'%s' bit in 'struct rev_info' will be forced\"),\n \t\t\t\"reverse\");\n-\t\trevs.reverse = 1;\n+\t\trevs.reverse = desired_reverse;\n \t}\n \tif (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {\n \t\twarning(_(\"some rev walking options will be overridden as \"\n@@ -174,7 +190,9 @@ int cmd_replay(int argc,\n \t\tgoto cleanup;\n \n \t/* Build reflog message */\n-\tif (opts.advance) {\n+\tif (opts.revert) {\n+\t\tstrbuf_addf(&reflog_msg, \"replay --revert %s\", opts.revert);\n+\t} else if (opts.advance) {\n \t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n \t} else {\n \t\tstruct object_id oid;\ndiff --git a/replay.c b/replay.c\nindex a63f6714c4..d7239d4c83 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -8,6 +8,7 @@\n #include \"refs.h\"\n #include \"replay.h\"\n #include \"revision.h\"\n+#include \"sequencer.h\"\n #include \"strmap.h\"\n #include \"tree.h\"\n \n@@ -17,6 +18,11 @@\n  */\n #define the_repository DO_NOT_USE_THE_REPOSITORY\n \n+enum replay_mode {\n+\tREPLAY_MODE_PICK,\n+\tREPLAY_MODE_REVERT,\n+};\n+\n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n {\n@@ -50,15 +56,37 @@ static char *get_author(const char *message)\n \treturn NULL;\n }\n \n+static void generate_revert_message(struct strbuf *msg,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct repository *repo)\n+{\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);\n+\tconst char *subject_start;\n+\tint subject_len;\n+\tchar *subject;\n+\n+\tsubject_len = find_commit_subject(message, &subject_start);\n+\tsubject = xmemdupz(subject_start, subject_len);\n+\n+\tsequencer_format_revert_message(repo, subject, commit,\n+\t\t\t\t\tcommit->parents ? commit->parents->item : NULL,\n+\t\t\t\t\tfalse, msg);\n+\n+\tfree(subject);\n+\trepo_unuse_commit_buffer(repo, commit, message);\n+}\n+\n static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n+\t\t\t\t    struct commit *parent,\n+\t\t\t\t    enum replay_mode mode)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n-\tchar *author;\n+\tchar *author = NULL;\n \tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n@@ -70,9 +98,16 @@ static struct commit *create_commit(struct repository *repo,\n \n \tcommit_list_insert(parent, &parents);\n \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n+\tif (mode == REPLAY_MODE_REVERT) {\n+\t\tgenerate_revert_message(&msg, based_on, repo);\n+\t\t/* For revert, use current user as author (NULL = use default) */\n+\t} else if (mode == REPLAY_MODE_PICK) {\n+\t\tfind_commit_subject(message, &orig_message);\n+\t\tstrbuf_addstr(&msg, orig_message);\n+\t\tauthor = get_author(message);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \treset_ident_date();\n \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n@@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo,\n \t}\n }\n \n+static void set_up_branch_mode(struct repository *repo,\n+\t\t\t       char **branch_name,\n+\t\t\t       const char *option_name,\n+\t\t\t       struct ref_info *rinfo,\n+\t\t\t       struct commit **onto)\n+{\n+\tstruct object_id oid;\n+\tchar *fullname = NULL;\n+\n+\tif (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),\n+\t\t\t  &oid, &fullname, 0) == 1) {\n+\t\tfree(*branch_name);\n+\t\t*branch_name = fullname;\n+\t} else {\n+\t\tdie(_(\"argument to %s must be a reference\"), option_name);\n+\t}\n+\t*onto = peel_committish(repo, *branch_name, option_name);\n+\tif (rinfo->positive_refexprs > 1)\n+\t\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n+\t\t      \"because the ordering would be ill-defined\"),\n+\t\t    option_name);\n+}\n+\n static void set_up_replay_mode(struct repository *repo,\n \t\t\t       struct rev_cmdline_info *cmd_info,\n \t\t\t       const char *onto_name,\n \t\t\t       bool *detached_head,\n \t\t\t       char **advance_name,\n+\t\t\t       char **revert_name,\n \t\t\t       struct commit **onto,\n \t\t\t       struct strset **update_refs)\n {\n@@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo,\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n \n-\tif (!onto_name == !*advance_name)\n-\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n-\n \tif (onto_name) {\n \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n \t\tif (rinfo.positive_refexprs <\n@@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo,\n \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n \t\t**update_refs = rinfo.positive_refs;\n \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n+\t} else if (*advance_name) {\n+\t\tset_up_branch_mode(repo, advance_name, \"--advance\", &rinfo, onto);\n+\t} else if (*revert_name) {\n+\t\tset_up_branch_mode(repo, revert_name, \"--revert\", &rinfo, onto);\n \t} else {\n-\t\tstruct object_id oid;\n-\t\tchar *fullname = NULL;\n-\n-\t\tif (!*advance_name)\n-\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n-\n-\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n-\t\t\t     &oid, &fullname, 0) == 1) {\n-\t\t\tfree(*advance_name);\n-\t\t\t*advance_name = fullname;\n-\t\t} else {\n-\t\t\tdie(_(\"argument to --advance must be a reference\"));\n-\t\t}\n-\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n-\t\tif (rinfo.positive_refexprs > 1)\n-\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n+\t\tBUG(\"expected one of onto_name, *advance_name, or *revert_name\");\n \t}\n \tstrset_clear(&rinfo.negative_refs);\n \tstrset_clear(&rinfo.positive_refs);\n@@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  kh_oid_map_t *replayed_commits,\n \t\t\t\t\t  struct commit *onto,\n \t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n+\t\t\t\t\t  struct merge_result *result,\n+\t\t\t\t\t  enum replay_mode mode)\n {\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n@@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \tpickme_tree = repo_get_commit_tree(repo, pickme);\n \tbase_tree = repo_get_commit_tree(repo, base);\n \n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  replayed_base_tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n+\tif (mode == REPLAY_MODE_PICK) {\n+\t\t/* Cherry-pick: normal order */\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\t\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->ancestor);\n+\t} else if (mode == REPLAY_MODE_REVERT) {\n+\t\t/* Revert: swap base and pickme to reverse the diff */\n+\t\tconst char *pickme_name = short_commit_name(repo, pickme);\n+\t\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\t\tmerge_opt->branch2 = xstrfmt(\"parent of %s\", pickme_name);\n+\t\tmerge_opt->ancestor = pickme_name;\n+\n+\t\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t\t  pickme_tree,\n+\t\t\t\t\t  replayed_base_tree,\n+\t\t\t\t\t  base_tree,\n+\t\t\t\t\t  result);\n+\n+\t\tfree((char *)merge_opt->branch2);\n+\t} else {\n+\t\tBUG(\"unexpected replay mode %d\", mode);\n+\t}\n \tmerge_opt->ancestor = NULL;\n+\tmerge_opt->branch2 = NULL;\n \tif (!result->clean)\n \t\treturn NULL;\n \t/* Drop commits that become empty */\n \tif (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&\n \t    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))\n \t\treturn replayed_base;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs,\n \t};\n \tbool detached_head;\n \tchar *advance;\n+\tchar *revert;\n+\tenum replay_mode mode = REPLAY_MODE_PICK;\n \tint ret;\n \n \tadvance = xstrdup_or_null(opts->advance);\n+\trevert = xstrdup_or_null(opts->revert);\n+\tif (revert)\n+\t\tmode = REPLAY_MODE_REVERT;\n \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n-\t\t\t   &detached_head, &advance, &onto, &update_refs);\n+\t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n \n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n@@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs,\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n+\t\t\t\t\t\t  &merge_opt, &result, mode);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \n@@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance)\n+\t\tif (advance || revert)\n \t\t\tcontinue;\n \n \t\tfor (decoration = get_name_decoration(&commit->object);\n@@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs,\n \t\tgoto out;\n \t}\n \n-\t/* In --advance mode, advance the target ref */\n-\tif (advance)\n-\t\treplay_result_queue_update(out, advance,\n+\t/* In --advance or --revert mode, update the target ref */\n+\tif (advance || revert) {\n+\t\tconst char *ref = advance ? advance : revert;\n+\t\treplay_result_queue_update(out, ref,\n \t\t\t\t\t   &onto->object.oid,\n \t\t\t\t\t   &last_commit->object.oid);\n+\t}\n \n \tret = 0;\n \n@@ -377,5 +451,6 @@ int replay_revisions(struct rev_info *revs,\n \tkh_destroy_oid_map(replayed_commits);\n \tmerge_finalize(&merge_opt, &result);\n \tfree(advance);\n+\tfree(revert);\n \treturn ret;\n }\ndiff --git a/replay.h b/replay.h\nindex d8407dc7f7..e916a5f975 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -13,7 +13,7 @@ struct replay_revisions_options {\n \t/*\n \t * Starting point at which to create the new commits; must be a branch\n \t * name. The branch will be updated to point to the rewritten commits.\n-\t * This option is mutually exclusive with `onto`.\n+\t * This option is mutually exclusive with `onto` and `revert`.\n \t */\n \tconst char *advance;\n \n@@ -22,7 +22,14 @@ struct replay_revisions_options {\n \t * committish. References pointing at decendants of `onto` will be\n \t * updated to point to the new commits.\n \t */\n-\t const char *onto;\n+\tconst char *onto;\n+\n+\t/*\n+\t * Starting point at which to create revert commits; must be a branch\n+\t * name. The branch will be updated to point to the revert commits.\n+\t * This option is mutually exclusive with `onto` and `advance`.\n+\t */\n+\tconst char *revert;\n \n \t/*\n \t * Update branches that point at commits in the given revision range.\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex a03f8f9293..217f6fb292 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'option --onto or --advance is mandatory' '\n-\techo \"error: option --onto or --advance is mandatory\" >expect &&\n+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '\n+\techo \"error: exactly one of --onto, --advance, or --revert is required\" >expect &&\n \ttest_might_fail git replay -h >>expect &&\n \ttest_must_fail git replay topic1..topic2 2>actual &&\n \ttest_cmp expect actual\n@@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'options --advance and --contained cannot be used together' '\n-\tprintf \"fatal: options ${SQ}--advance${SQ} \" >expect &&\n-\tprintf \"and ${SQ}--contained${SQ} cannot be used together\\n\" >>expect &&\n+test_expect_success '--advance and --contained cannot be used together' '\n \ttest_must_fail git replay --advance=main --contained \\\n \t\ttopic1..topic2 2>actual &&\n-\ttest_cmp expect actual\n+\ttest_grep \"cannot be used together\" actual\n '\n \n test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n-\techo \"fatal: cannot advance target with multiple sources because ordering would be ill-defined\" >expect &&\n+\techo \"fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\" >expect &&\n \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n \ttest_cmp expect actual\n '\n@@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' '\n \ttest_grep \"invalid.*replay.refAction.*value\" error\n '\n \n+test_expect_success 'argument to --revert must be a reference' '\n+\techo \"fatal: argument to --revert must be a reference\" >expect &&\n+\toid=$(git rev-parse main) &&\n+\ttest_must_fail git replay --revert=$oid topic1..topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'cannot revert with multiple sources' '\n+\techo \"fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\" >expect &&\n+\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using replay --revert to revert commits' '\n+\t# Reuse existing topic4 branch (has commits I and J on top of main)\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# Revert commits I and J\n+\tgit replay --revert topic4 topic4~2..topic4 &&\n+\n+\t# Verify the revert commits were created (newest-first ordering\n+\t# means J is reverted first, then I on top)\n+\tgit log --format=%s -4 topic4 >actual &&\n+\tcat >expect <<-\\EOF &&\n+\tRevert \"I\"\n+\tRevert \"J\"\n+\tJ\n+\tI\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\t# Verify commit message format includes hash (tip is Revert \"I\")\n+\ttest_commit_message topic4 <<-EOF &&\n+\tRevert \"I\"\n+\n+\tThis reverts commit $(git rev-parse I).\n+\tEOF\n+\n+\t# Verify reflog message\n+\tgit reflog topic4 -1 --format=%gs >reflog-msg &&\n+\techo \"replay --revert topic4\" >expect-reflog &&\n+\ttest_cmp expect-reflog reflog-msg\n+'\n+\n+test_expect_success 'using replay --revert in bare repo' '\n+\t# Reuse existing topic4 in bare repo\n+\tSTART=$(git -C bare rev-parse topic4) &&\n+\ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n+\n+\t# Revert commit J in bare repo\n+\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify revert was created\n+\tgit -C bare log -1 --format=%s topic4 >actual &&\n+\techo \"Revert \\\"J\\\"\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'revert of revert uses Reapply' '\n+\t# Use topic4 and first revert J, then revert the revert\n+\tSTART=$(git rev-parse topic4) &&\n+\ttest_when_finished \"git branch -f topic4 $START\" &&\n+\n+\t# First revert J\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\tREVERT_J=$(git rev-parse topic4) &&\n+\n+\t# Now revert the revert - should become Reapply\n+\tgit replay --revert topic4 topic4~1..topic4 &&\n+\n+\t# Verify Reapply prefix and message format\n+\ttest_commit_message topic4 <<-EOF\n+\tReapply \"J\"\n+\n+\tThis reverts commit $REVERT_J.\n+\tEOF\n+'\n+\n+test_expect_success 'git replay --revert with conflict' '\n+\t# conflict branch has C.conflict which conflicts with topic1s C\n+\ttest_expect_code 1 git replay --revert conflict B..topic1\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --contained' '\n+\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --onto' '\n+\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'git replay --revert incompatible with --advance' '\n+\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n+\ttest_grep \"cannot be used together\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"540259","messageId":"05959eb8-4b8a-421e-bf5f-9e6f0b59a313@gmail.com","threadId":"64529","inReplyTo":"20260325202354.10628-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v6 2/2] replay: add --revert mode to reverse commit changes","fromName":"Tian Yuchen","fromEmail":"a3205153416@gmail.com","sentAt":"2026-03-28T04:33:49Z","receivedAt":"2026-03-28T04:33:56Z","isPatch":true,"body":"Hi Siddharth,\n\nThe patch itself looks pretty good to me, but I have some reservations \nabout its functionality:\n\nOn 3/26/26 04:23, Siddharth Asthana wrote:\n>   static struct commit *create_commit(struct repository *repo,\n>   \t\t\t\t    struct tree *tree,\n>   \t\t\t\t    struct commit *based_on,\n> -\t\t\t\t    struct commit *parent)\n> +\t\t\t\t    struct commit *parent,\n> +\t\t\t\t    enum replay_mode mode)\n>   {\n\n...\n\n>   \textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n\n...\n\n\n>   \tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n>   \t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n> @@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo,\n>   \t}\n>   }\n\nIt seems there isn’t a distinction made here between how 'cherry-pick' \nand 'revert' handle the `extra` header. But doesn’t the 'revert' \noperation actually create a *new* commit with the *current time* and \n*current author*? Is it appropriate to inherit the 'extra' header?\n\n\n\n\n> +static void set_up_branch_mode(struct repository *repo,\n\n...\n\n> +\t*onto = peel_committish(repo, *branch_name, option_name);\n> +\tif (rinfo->positive_refexprs > 1)\n> +\t\tdie(_(\"'%s' cannot be used with multiple revision ranges \"\n> +\t\t      \"because the ordering would be ill-defined\"),\n> +\t\t    option_name);\n> +}\n\nThis is a fail-safe design intended to prevent users from entering \ncommands like:\n\n\tgit replay --revert main f1 f2\n\nThis operation is indeed undefined which should be intercepted. However, \nconsidering:\n\t\n\tgit replay --revert main HEAD~5..HEAD~3 HEAD~1..HEAD\n\nIs this operation also intercepted? I think the reason is that the \ncondition 'rinfo->positive_refexprs > 1' is a bit too simplistic.\n\n\n\n\n> +\tif (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),\n> +\t\t\t  &oid, &fullname, 0) == 1) {\n> +\t\tfree(*branch_name);\n> +\t\t*branch_name = fullname;\n> +\t} else {\n> +\t\tdie(_(\"argument to %s must be a reference\"), option_name);\n> +\t}\n\nI think it would be great if a low-level command supported something like:\n\n\tgit replay --revert new-branch HEAD~3..HEAD\n\nEven if it just saves the step of creating a new branch ;)\n\n\n\n\nThese are just my thoughts on the matter. Hope to spark discussion.\n\nRegards, Yuchen\n\n"},{"id":"540333","messageId":"6427d088-e41c-47ff-ab6e-4d7679e85d3c@gmail.com","threadId":"64529","inReplyTo":"05959eb8-4b8a-421e-bf5f-9e6f0b59a313@gmail.com","subject":"Re: [PATCH v6 2/2] replay: add --revert mode to reverse commit changes","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-03-29T16:17:16Z","receivedAt":"2026-03-29T16:17:22Z","isPatch":true,"body":"\n\nOn 28/03/26 10:03, Tian Yuchen wrote:\n> Hi Siddharth,\n> \n> The patch itself looks pretty good to me, but I have some reservations \n> about its functionality:\n> \n> On 3/26/26 04:23, Siddharth Asthana wrote:\n>>   static struct commit *create_commit(struct repository *repo,\n>>                       struct tree *tree,\n>>                       struct commit *based_on,\n>> -                    struct commit *parent)\n>> +                    struct commit *parent,\n>> +                    enum replay_mode mode)\n>>   {\n> \n> ...\n> \n>>       extra = read_commit_extra_headers(based_on, exclude_gpgsig);\n> \n> ...\n> \n> \n>>       if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, \n>> parents,\n>>                    &ret, author, NULL, sign_commit, extra)) {\n>> @@ -153,11 +188,35 @@ static void get_ref_information(struct \n>> repository *repo,\n>>       }\n>>   }\n> \n> It seems there isn’t a distinction made here between how 'cherry-pick' \n> and 'revert' handle the `extra` header. But doesn’t the 'revert' \n> operation actually create a *new* commit with the *current time* and \n> *current author*? Is it appropriate to inherit the 'extra' header?\n\n\nYeah, you are right that regular git revert doesn't carry over extra \nheaders from the original commit. In practice this doesn't bite us today \nsince extra headers are mostly mergetag, and replay already rejects \nmerge commits. But it's still worth cleaning up. I will send a follow-up \npatch for it rather than rerolling the whole series.\n\n\n> \n> \n> \n> \n>> +static void set_up_branch_mode(struct repository *repo,\n> \n> ...\n> \n>> +    *onto = peel_committish(repo, *branch_name, option_name);\n>> +    if (rinfo->positive_refexprs > 1)\n>> +        die(_(\"'%s' cannot be used with multiple revision ranges \"\n>> +              \"because the ordering would be ill-defined\"),\n>> +            option_name);\n>> +}\n> \n> This is a fail-safe design intended to prevent users from entering \n> commands like:\n> \n>      git replay --revert main f1 f2\n> \n> This operation is indeed undefined which should be intercepted. However, \n> considering:\n> \n>      git replay --revert main HEAD~5..HEAD~3 HEAD~1..HEAD\n> \n> Is this operation also intercepted? I think the reason is that the \n> condition 'rinfo->positive_refexprs > 1' is a bit too simplistic.\n\n\nYes -- positive_refexprs counts each position tip, so that gives 2 and \nthe > 1 check catches it.\n\n\n> \n> \n> \n> \n>> +    if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),\n>> +              &oid, &fullname, 0) == 1) {\n>> +        free(*branch_name);\n>> +        *branch_name = fullname;\n>> +    } else {\n>> +        die(_(\"argument to %s must be a reference\"), option_name);\n>> +    }\n> \n> I think it would be great if a low-level command supported something like:\n> \n>      git replay --revert new-branch HEAD~3..HEAD\n\n\nInteresting idea. git replay is still experimental so the interface \ncould evolve. Worth considering as a follow-up but I would keep it out \nof this series for now.\n\n\nThanks for the review!\n\n> \n> Even if it just saves the step of creating a new branch ;)\n> \n> \n> \n> \n> These are just my thoughts on the matter. Hope to spark discussion.\n> \n> Regards, Yuchen\n> \n\n"},{"id":"540405","messageId":"1cf080ba-61a1-43b0-abff-c7c156c1c4b1@gmail.com","threadId":"64529","inReplyTo":"6427d088-e41c-47ff-ab6e-4d7679e85d3c@gmail.com","subject":"Re: [PATCH v6 2/2] replay: add --revert mode to reverse commit changes","fromName":"Tian Yuchen","fromEmail":"a3205153416@gmail.com","sentAt":"2026-03-30T17:23:07Z","receivedAt":"2026-03-30T17:23:29Z","isPatch":true,"body":"On 3/30/26 00:17, Siddharth Asthana wrote:\n\n>>\n>> This is a fail-safe design intended to prevent users from entering \n>> commands like:\n>>\n>>      git replay --revert main f1 f2\n>>\n>> This operation is indeed undefined which should be intercepted. \n>> However, considering:\n>>\n>>      git replay --revert main HEAD~5..HEAD~3 HEAD~1..HEAD\n>>\n>> Is this operation also intercepted? I think the reason is that the \n>> condition 'rinfo->positive_refexprs > 1' is a bit too simplistic.\n> \n> \n> Yes -- positive_refexprs counts each position tip, so that gives 2 and \n> the > 1 check catches it.\n> \n> \n\nWhat I mean is, this operation shouldn't be intercepted, right? In my \nview, it is valid to select and operate two (and more) periods from the \nsame linear commit history, but that is blocked here.\n\nNever mind, it's nothing important. The rest looks good to me. Keep it up!\n\nRegards, Yuchen\n\n"},{"id":"540492","messageId":"87o6k4l8tg.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","threadId":"64529","inReplyTo":"1cf080ba-61a1-43b0-abff-c7c156c1c4b1@gmail.com","subject":"Re: [PATCH v6 2/2] replay: add --revert mode to reverse commit changes","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-31T08:08:59Z","receivedAt":"2026-03-31T08:09:12Z","isPatch":true,"body":"Tian Yuchen <a3205153416@gmail.com> writes:\n\n> On 3/30/26 00:17, Siddharth Asthana wrote:\n>\n>>>\n>>> This is a fail-safe design intended to prevent users from entering \n>>> commands like:\n>>>\n>>>      git replay --revert main f1 f2\n>>>\n>>> This operation is indeed undefined which should be intercepted. \n>>> However, considering:\n>>>\n>>>      git replay --revert main HEAD~5..HEAD~3 HEAD~1..HEAD\n>>>\n>>> Is this operation also intercepted? I think the reason is that the \n>>> condition 'rinfo->positive_refexprs > 1' is a bit too simplistic.\n>> \n>> \n>> Yes -- positive_refexprs counts each position tip, so that gives 2 and \n>> the > 1 check catches it.\n>> \n>> \n>\n> What I mean is, this operation shouldn't be intercepted, right? In my \n> view, it is valid to select and operate two (and more) periods from the \n> same linear commit history, but that is blocked here.\n\nSo you want that command to replay the first revision range onto `main`\nand on top of that the second revision range?\n\nFor what it's worth, I think `--advance` suffers from the same issue. So\nI think this can be addressed after these patches land.\n\n-- \nCheers,\nToon\n"},{"id":"540493","messageId":"87ldf8l8ot.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","threadId":"64529","inReplyTo":"6427d088-e41c-47ff-ab6e-4d7679e85d3c@gmail.com","subject":"Re: [PATCH v6 2/2] replay: add --revert mode to reverse commit changes","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-31T08:11:46Z","receivedAt":"2026-03-31T08:11:53Z","isPatch":true,"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> On 28/03/26 10:03, Tian Yuchen wrote:\n>> I think it would be great if a low-level command supported something like:\n>> \n>>      git replay --revert new-branch HEAD~3..HEAD\n>\n>\n> Interesting idea. git replay is still experimental so the interface \n> could evolve. Worth considering as a follow-up but I would keep it out \n> of this series for now.\n\nSo you want git-replay(1) to create a new ref `new-branch`? But where\nshould it start from?\n\nI'm building a proposal for this in [1]. This proposal is a bit more\nflexible than what you're suggesting here, because it enables setting\nthe starting point.\n\n[1]: https://lore.kernel.org/git/20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com/\n\n-- \nCheers,\nToon\n"}]}