* [PATCH 0/1] replay: add --revert option to reverse commit changes
@ 2025-11-25 17:00 Siddharth Asthana
2025-11-25 17:00 ` [PATCH 1/1] " Siddharth Asthana
` (2 more replies)
0 siblings, 3 replies; 92+ messages in thread
From: Siddharth Asthana @ 2025-11-25 17:00 UTC (permalink / raw)
To: git
Cc: christian.couder, ps, newren, gitster, phillip.wood123,
phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon,
johncai86, johannes.schindelin, Siddharth Asthana
The `git replay` command currently supports cherry-picking commits for
server-side history rewriting, but lacks the ability to revert them.
This patch adds a `--revert` option to enable reversing commits directly
on bare repositories.
At GitLab, we use replay in Gitaly for efficient server-side operations.
Adding revert functionality enables us to reverse problematic commits
without client-side roundtrips, reducing network overhead.
The implementation leverages the insight that cherry-pick and revert are
essentially the same merge operation with swapped arguments. By swapping
the base and pickme trees when calling `merge_incore_nonrecursive()`, we
effectively reverse the diff direction. The existing conflict handling,
ref updates, and atomic transaction support work unchanged.
The revert message generation logic is extracted into a new shared
`sequencer_format_revert_header()` function in `sequencer.c`, allowing
code reuse between `sequencer.c` and `builtin/replay.c`. The commit
messages follow `git revert` conventions, including "Revert"/"Reapply"
prefixes and the original commit SHA.
This patch includes comprehensive tests covering various scenarios:
bare repositories, --advance mode, conflicts, reapply behavior, and
multiple commits.
Siddharth Asthana (1):
replay: add --revert option to reverse commit changes
Documentation/git-replay.adoc | 35 +++++++-
builtin/replay.c | 86 ++++++++++++++----
sequencer.c | 23 +++++
sequencer.h | 8 ++
t/t3650-replay-basics.sh | 160 ++++++++++++++++++++++++++++++++++
5 files changed, 295 insertions(+), 17 deletions(-)
--
2.51.0
^ permalink raw reply [flat|nested] 92+ messages in thread* [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-25 17:00 [PATCH 0/1] replay: add --revert option to reverse commit changes Siddharth Asthana @ 2025-11-25 17:00 ` Siddharth Asthana 2025-11-25 19:22 ` Junio C Hamano 2025-11-26 11:10 ` Phillip Wood 2025-11-25 17:25 ` [PATCH 0/1] " Johannes Schindelin 2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana 2 siblings, 2 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-11-25 17:00 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin, Siddharth Asthana The `git replay` command performs server-side history rewriting without requiring a working tree. While it currently supports cherry-picking commits, it lacks the ability to revert them. At GitLab, we use replay in Gitaly for efficient server-side operations on bare repositories. Adding revert functionality enables us to reverse problematic commits directly on the server, eliminating client-side roundtrips and reducing network overhead. Add a `--revert` option that reverses the changes introduced by the specified commits. The implementation follows the same approach as `sequencer.c` (around lines 2358-2390), where cherry-pick and revert are essentially the same merge operation but with swapped arguments: - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) - Revert: merge(ancestor=commit, ours=current, theirs=parent) We swap the base and pickme trees when calling `merge_incore_nonrecursive()`, effectively reversing the diff direction. The existing conflict handling, ref updates, and atomic transaction support work unchanged. The revert message generation logic (handling "Revert" and "Reapply" cases) is extracted into a new `sequencer_format_revert_header()` function in `sequencer.c`, which can be shared between `sequencer.c` and `builtin/replay.c`. The `builtin/replay.c` code calls this shared function and then appends the commit OID using `oid_to_hex()` directly, since git replay is designed for simpler server-side operations without the interactive features and `replay_opts` framework used by `sequencer.c`. The commit messages follow `git revert` conventions: prefixed with "Revert" and including the original commit SHA. When reverting a commit that itself starts with "Revert", the message uses "Reapply" instead. Unlike cherry-pick which preserves the original author, revert commits use the current user as the author, matching the behavior of `git revert`. Mark the option as incompatible with `--contained` since reverting changes across multiple branches simultaneously could lead to inconsistent repository states. Helped-by: Christian Couder <christian.couder@gmail.com> Helped-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- Documentation/git-replay.adoc | 35 +++++++- builtin/replay.c | 86 ++++++++++++++---- sequencer.c | 23 +++++ sequencer.h | 8 ++ t/t3650-replay-basics.sh | 160 ++++++++++++++++++++++++++++++++++ 5 files changed, 295 insertions(+), 17 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index dcb26e8a8e..ad7dc08622 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t SYNOPSIS -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>... +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] [--revert] <revision-range>... DESCRIPTION ----------- @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it. + The default mode can be configured via the `replay.refAction` configuration variable. +--revert:: + Revert the changes introduced by the commits in the revision range + instead of applying them. This reverses the diff direction and creates + new commits that undo the changes, similar to `git revert`. ++ +The commit messages are prefixed with "Revert" and include the original +commit SHA. If reverting a commit whose message starts with "Revert", the new +message will start with "Reapply" instead. The author of the new commits +will be the current user, not the original commit author. ++ +This option is incompatible with `--contained`. + <revision-range>:: Range of commits to replay. More than one <revision-range> can be passed, but in `--advance <branch>` mode, they should have @@ -141,6 +153,27 @@ all commits they have since `base`, playing them on top of `origin/main`. These three branches may have commits on top of `base` that they have in common, but that does not need to be the case. +To revert a range of commits: + +------------ +$ git replay --revert --onto main feature~3..feature +------------ + +This creates new commits on top of 'main' that reverse the changes introduced +by the last three commits on 'feature'. The 'feature' branch is updated to +point at the last of these revert commits. The 'main' branch is not updated +in this case. + +To revert commits and advance a branch: + +------------ +$ git replay --revert --advance main feature~2..feature +------------ + +This reverts the last two commits from 'feature', applies those reverts +on top of 'main', and updates 'main' to point at the result. The 'feature' +branch is not updated in this case. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/replay.c b/builtin/replay.c index 6606a2c94b..7258d0bbc5 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -17,6 +17,7 @@ #include "parse-options.h" #include "refs.h" #include "revision.h" +#include "sequencer.h" #include "strmap.h" #include <oidset.h> #include <tree.h> @@ -57,10 +58,25 @@ static char *get_author(const char *message) return NULL; } +/* + * Generates a revert commit message using the shared sequencer function. + * We use oid_to_hex() directly instead of refer_to_commit() since git replay + * is designed for simpler server-side operations without interactive features. + */ +static void generate_revert_message(struct strbuf *msg, + const char *orig_message, + const struct object_id *oid) +{ + sequencer_format_revert_header(msg, orig_message); + strbuf_addstr(msg, oid_to_hex(oid)); + strbuf_addstr(msg, ".\n"); +} + static struct commit *create_commit(struct repository *repo, struct tree *tree, struct commit *based_on, - struct commit *parent) + struct commit *parent, + int is_revert) { struct object_id ret; struct object *obj = NULL; @@ -78,8 +94,17 @@ static struct commit *create_commit(struct repository *repo, commit_list_insert(parent, &parents); extra = read_commit_extra_headers(based_on, exclude_gpgsig); find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); + + if (is_revert) { + generate_revert_message(&msg, orig_message, &based_on->object.oid); + /* For revert, use current user as author */ + author = NULL; + } else { + /* Cherry-pick mode: use original commit message and author */ + strbuf_addstr(&msg, orig_message); + author = get_author(message); + } + reset_ident_date(); if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, &ret, author, NULL, sign_commit, extra)) { @@ -261,7 +286,8 @@ static struct commit *pick_regular_commit(struct repository *repo, kh_oid_map_t *replayed_commits, struct commit *onto, struct merge_options *merge_opt, - struct merge_result *result) + struct merge_result *result, + int is_revert) { struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree; @@ -273,21 +299,41 @@ static struct commit *pick_regular_commit(struct repository *repo, pickme_tree = repo_get_commit_tree(repo, pickme); base_tree = repo_get_commit_tree(repo, base); - merge_opt->branch1 = short_commit_name(repo, replayed_base); - merge_opt->branch2 = short_commit_name(repo, pickme); - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + if (is_revert) { + /* For revert: swap base and pickme to reverse the diff */ + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme)); + merge_opt->ancestor = short_commit_name(repo, pickme); - merge_incore_nonrecursive(merge_opt, - base_tree, - result->tree, - pickme_tree, - result); + merge_incore_nonrecursive(merge_opt, + pickme_tree, + result->tree, + base_tree, + result); + + /* branch2 was allocated with xstrfmt, needs freeing */ + free((char *)merge_opt->branch2); + } else { + /* For cherry-pick: normal order */ + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = short_commit_name(repo, pickme); + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + + merge_incore_nonrecursive(merge_opt, + base_tree, + result->tree, + pickme_tree, + result); + + /* ancestor was allocated with xstrfmt, needs freeing */ + free((char *)merge_opt->ancestor); + } - free((char*)merge_opt->ancestor); merge_opt->ancestor = NULL; + merge_opt->branch2 = NULL; if (!result->clean) return NULL; - return create_commit(repo, result->tree, pickme, replayed_base); + return create_commit(repo, result->tree, pickme, replayed_base, is_revert); } static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source) @@ -350,6 +396,7 @@ int cmd_replay(int argc, int contained = 0; const char *ref_action = NULL; enum ref_action_mode ref_mode; + int is_revert = 0; struct rev_info revs; struct commit *last_commit = NULL; @@ -366,7 +413,7 @@ int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " "([--contained] --onto <newbase> | --advance <branch>) " - "[--ref-action[=<mode>]] <revision-range>..."), + "[--ref-action[=<mode>]] [--revert] <revision-range>..."), NULL }; struct option replay_options[] = { @@ -381,6 +428,8 @@ int cmd_replay(int argc, OPT_STRING(0, "ref-action", &ref_action, N_("mode"), N_("control ref update behavior (update|print)")), + OPT_BOOL(0, "revert", &is_revert, + N_("revert commits instead of cherry-picking them")), OPT_END() }; @@ -395,6 +444,10 @@ int cmd_replay(int argc, die_for_incompatible_opt2(!!advance_name_opt, "--advance", contained, "--contained"); + /* --revert is incompatible with --contained */ + die_for_incompatible_opt2(is_revert, "--revert", + contained, "--contained"); + /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); @@ -496,7 +549,8 @@ int cmd_replay(int argc, die(_("replaying merge commits is not supported yet!")); last_commit = pick_regular_commit(repo, commit, replayed_commits, - onto, &merge_opt, &result); + onto, &merge_opt, &result, + is_revert); if (!last_commit) break; diff --git a/sequencer.c b/sequencer.c index 5476d39ba9..e6d82c8368 100644 --- a/sequencer.c +++ b/sequencer.c @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r, return res; } +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject) +{ + const char *revert_subject; + + if (skip_prefix(orig_subject, "Revert \"", &revert_subject) && + /* + * We don't touch pre-existing repeated reverts, because + * theoretically these can be nested arbitrarily deeply, + * thus requiring excessive complexity to deal with. + */ + !starts_with(revert_subject, "Revert \"")) { + strbuf_addstr(out, "Reapply \""); + strbuf_addstr(out, revert_subject); + strbuf_addch(out, '\n'); + } else { + strbuf_addstr(out, "Revert \""); + strbuf_addstr(out, orig_subject); + strbuf_addstr(out, "\"\n"); + } + + strbuf_addstr(out, "\nThis reverts commit "); +} + void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) { unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP; diff --git a/sequencer.h b/sequencer.h index 719684c8a9..2d4a2d3fac 100644 --- a/sequencer.h +++ b/sequencer.h @@ -205,6 +205,14 @@ int todo_list_rearrange_squash(struct todo_list *todo_list); */ void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag); +/* + * Formats a revert commit message header following standard Git conventions. + * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert + * cases ("Reapply \"<subject>\""). Adds "This reverts commit " at the end. + * The caller should append the commit OID after calling this function. + */ +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject); + void append_conflicts_hint(struct index_state *istate, struct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode); enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg, diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index cf3aacf355..5fcd730b54 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -314,4 +314,164 @@ test_expect_success 'invalid replay.refAction value' ' test_grep "invalid.*replay.refAction.*value" error ' +test_expect_success 'using replay with --revert to revert a commit' ' + # Revert commits D and E from topic2 + git replay --revert --onto topic1 topic1..topic2 >result && + + test_line_count = 1 result && + NEW_TOPIC2=$(cut -f 3 -d " " result) && + + # Verify the result updates the topic2 branch + printf "update refs/heads/topic2 " >expect && + printf "%s " $NEW_TOPIC2 >>expect && + git rev-parse topic2 >>expect && + + test_cmp expect result && + + # Verify the commit messages contain "Revert" + # topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A) + git log --format=%s $NEW_TOPIC2 >actual && + test_line_count = 6 actual && + head -n 1 actual >first-line && + test_grep "^Revert" first-line +' + +test_expect_success 'using replay with --revert on bare repo' ' + git -C bare replay --revert --onto topic1 topic1..topic2 >result-bare && + + test_line_count = 1 result-bare && + NEW_COMMIT=$(cut -f 3 -d " " result-bare) && + + # Verify the commit message contains "Revert" + git -C bare log --format=%s $NEW_COMMIT >actual-bare && + test_line_count = 6 actual-bare && + head -n 1 actual-bare >first-line-bare && + test_grep "^Revert" first-line-bare +' + +test_expect_success 'using replay with --revert and --advance' ' + # Revert commits from topic2 and advance main + git replay --revert --advance main topic1..topic2 >result && + + test_line_count = 1 result && + NEW_MAIN=$(cut -f 3 -d " " result) && + + # Verify the result updates the main branch + printf "update refs/heads/main " >expect && + printf "%s " $NEW_MAIN >>expect && + git rev-parse main >>expect && + + test_cmp expect result && + + # Verify the commit message contains "Revert" + git log --format=%s $NEW_MAIN >actual && + head -n 1 actual >first-line && + test_grep "^Revert" first-line +' + +test_expect_success 'replay with --revert fails with --contained' ' + test_must_fail git replay --revert --contained --onto main main..topic3 2>error && + test_grep "revert.*contained.*cannot be used together" error +' + +test_expect_success 'verify revert actually reverses changes' ' + # Create a branch with a simple change + git switch -c revert-test main && + echo "new content" >test-file.txt && + git add test-file.txt && + git commit -m "Add test file" && + + # Revert the commit + git replay --revert --advance revert-test HEAD^..HEAD >result && + REVERTED=$(cut -f 3 -d " " result) && + + # The file should no longer exist (reverted) + test_must_fail git show $REVERTED:test-file.txt +' + +test_expect_success 'revert of a revert creates reapply message' ' + # Create a commit + git switch -c revert-revert main && + echo "content" >revert-test-2.txt && + git add revert-test-2.txt && + git commit -m "Add revert test file" && + + ORIGINAL=$(git rev-parse HEAD) && + + # First revert + git replay --revert --advance revert-revert HEAD^..HEAD >result1 && + FIRST_REVERT=$(cut -f 3 -d " " result1) && + + # Check first revert message starts with "Revert" + git log --format=%s -1 $FIRST_REVERT >msg1 && + test_grep "^Revert" msg1 && + + # Now revert the revert + git replay --revert --advance revert-revert $ORIGINAL..$FIRST_REVERT >result2 && + REAPPLY=$(cut -f 3 -d " " result2) && + + # Check second revert message starts with "Reapply" + git log --format=%s -1 $REAPPLY >msg2 && + test_grep "^Reapply" msg2 && + + # The file should exist again (reapplied) + git show $REAPPLY:revert-test-2.txt >actual && + echo "content" >expected && + test_cmp expected actual +' + +test_expect_success 'replay --revert includes commit SHA in message' ' + git switch -c revert-sha-test main && + echo "test" >sha-test.txt && + git add sha-test.txt && + git commit -m "Test commit for SHA" && + + COMMIT_SHA=$(git rev-parse HEAD) && + git replay --revert --advance revert-sha-test HEAD^..HEAD >result && + REVERT_COMMIT=$(cut -f 3 -d " " result) && + + # Check that the commit message includes the original SHA + git log --format=%B -1 $REVERT_COMMIT >msg && + test_grep "$COMMIT_SHA" msg +' + +test_expect_success 'replay --revert with conflict' ' + # Create a conflicting situation + git switch -c revert-conflict main && + echo "line1" >conflict-file.txt && + git add conflict-file.txt && + git commit -m "Add conflict file" && + + git switch -c revert-conflict-branch HEAD^ && + echo "different" >conflict-file.txt && + git add conflict-file.txt && + git commit -m "Different content" && + + # Try to revert the first commit onto the conflicting branch + test_expect_code 1 git replay --revert --onto revert-conflict-branch revert-conflict^..revert-conflict +' + +test_expect_success 'replay --revert handles multiple commits' ' + # Verify that reverting multiple commits works correctly + # The output should show both revert commits in the history + git log --format=%s topic2 >topic2-log && + test_write_lines E D C B A >expected-topic2 && + test_cmp expected-topic2 topic2-log && + + # Revert D and E from topic2, applying the reverts onto topic1 + git replay --revert --onto topic1 topic1..topic2 >result && + + test_line_count = 1 result && + FINAL=$(cut -f 3 -d " " result) && + + # Verify both revert commits appear in the log + git log --format=%s $FINAL >log && + head -n 2 log >first-two && + test_grep "^Revert" first-two && + + # Verify we have both "Revert D" and "Revert E" + test_grep "Revert.*E" log && + test_grep "Revert.*D" log +' + test_done -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-25 17:00 ` [PATCH 1/1] " Siddharth Asthana @ 2025-11-25 19:22 ` Junio C Hamano 2025-11-25 19:30 ` Junio C Hamano 2025-11-26 19:26 ` Siddharth Asthana 2025-11-26 11:10 ` Phillip Wood 1 sibling, 2 replies; 92+ messages in thread From: Junio C Hamano @ 2025-11-25 19:22 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Siddharth Asthana <siddharthasthana31@gmail.com> writes: > The revert message generation logic (handling "Revert" and "Reapply" > cases) is extracted into a new `sequencer_format_revert_header()` > function in `sequencer.c`, which can be shared between `sequencer.c` > and `builtin/replay.c`. The `builtin/replay.c` code calls this shared > function and then appends the commit OID using `oid_to_hex()` directly, > since git replay is designed for simpler server-side operations without > the interactive features and `replay_opts` framework used by > `sequencer.c`. When I review a patch that claims to refactor existing logic into a separate helper function to reuse it in more places, I look at the diffstat to see how many lines are removed. The logic for generating the message does not seem to be "extracted into", but rather "duplicated to", the new helper function. It gives the two message sources opportunity to drift apart over time, which is not what you want. In do_pick_commit() where TODO_REVERT command is handled, we find a code block that is almost identical to what this patch adds to the new helper function; it should be rewritten to call the new helper function or perhaps a shared helper function is introduced and called from there and also from the sequencer_format_revert_header() function, if there is still some impedance mismatch. If such a refactoring is done as a separate preliminary patch in a N-patch series, the resulting patch series may be easier to follow (and there may be other opportunities to reuse existing code more). > Mark the option as incompatible with `--contained` since reverting > changes across multiple branches simultaneously could lead to > inconsistent repository states. This, and the documentation part, does not seem to tell what "inconsistent state" we are worried about. Is it just a buggy design of --revert can be implemented that produces wrong result when used with --contened, or are these two options inherently try to achieve contradicting goals? I am guessing that it is the latter, but if so, can we make it clear why? > +--revert:: > + Revert the changes introduced by the commits in the revision range > + instead of applying them. This reverses the diff direction and creates > + new commits that undo the changes, similar to `git revert`. > ++ > +The commit messages are prefixed with "Revert" and include the original > +commit SHA. If reverting a commit whose message starts with "Revert", the new > +message will start with "Reapply" instead. The author of the new commits > +will be the current user, not the original commit author. > ++ > +This option is incompatible with `--contained`. I have never used the `--contained` option, but is it so obvious to those who have why these two have to be made incompatible that the above statement does not have to be followed by "because ..."? > @@ -141,6 +153,27 @@ all commits they have since `base`, playing them on top of > `origin/main`. These three branches may have commits on top of `base` > that they have in common, but that does not need to be the case. > > +To revert a range of commits: > + > +------------ > +$ git replay --revert --onto main feature~3..feature > +------------ > + > +This creates new commits on top of 'main' that reverse the changes introduced > +by the last three commits on 'feature'. The 'feature' branch is updated to > +point at the last of these revert commits. The 'main' branch is not updated > +in this case. Is there any topological requirement between 'main' and 'feature' branches? Naïvely, I would expect that it would be perfect if 'feature' branch has been merged to 'main' (then you'd be reverting the top 3 commits of that branch), but that would be something you would do to correct 'main', and not 'feature', but the description explains this is a way to update 'feature' to lose the three topmost commits, so I am not sure what this example really does and when it would be useful. > +To revert commits and advance a branch: > + > +------------ > +$ git replay --revert --advance main feature~2..feature > +------------ > + > +This reverts the last two commits from 'feature', applies those reverts > +on top of 'main', and updates 'main' to point at the result. The 'feature' > +branch is not updated in this case. The same question. If I assume that 'main' has merged 'feature' before, this I can understand and match what I often do quite well while working on integrating topic branches. I may merge a topic that is not yet well cooked enough into 'next', regret that the two commits at the tip of the topic were premature, and revert these two commits out of 'next', or something. This example can be explained well if there is topological requirement that 'main' has at least these two commits from 'feature'. > @@ -261,7 +286,8 @@ static struct commit *pick_regular_commit(struct repository *repo, > kh_oid_map_t *replayed_commits, > struct commit *onto, > struct merge_options *merge_opt, > - struct merge_result *result) > + struct merge_result *result, > + int is_revert) Are there other ways to pick commit imaginable (if not planned to be implemented), other than "revert"? I am wondering if this is better done as "enum { CHERRY_PICK, REVERT, } pick_variant" for readability and maintainability. > @@ -273,21 +299,41 @@ static struct commit *pick_regular_commit(struct repository *repo, > pickme_tree = repo_get_commit_tree(repo, pickme); > base_tree = repo_get_commit_tree(repo, base); > > - merge_opt->branch1 = short_commit_name(repo, replayed_base); > - merge_opt->branch2 = short_commit_name(repo, pickme); > - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); > + if (is_revert) { It may be just me, but it would have been easier to follow if !revert case is given first, as that is the common variant the pick_regular_commit() function. > + /* For revert: swap base and pickme to reverse the diff */ > + merge_opt->branch1 = short_commit_name(repo, replayed_base); > + merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme)); That is an overly long line (sorry, I notice these things when a line does not even fit in 92-col terminal). > + merge_opt->ancestor = short_commit_name(repo, pickme); > - merge_incore_nonrecursive(merge_opt, > - base_tree, > - result->tree, > - pickme_tree, > - result); > + merge_incore_nonrecursive(merge_opt, > + pickme_tree, > + result->tree, > + base_tree, > + result); OK. These are applications of the standard 3-way merge trick to (ab)use ancestor to implement cherry-pick and revert. Looking good. > + > + /* branch2 was allocated with xstrfmt, needs freeing */ > + free((char *)merge_opt->branch2); > + } else { > + /* For cherry-pick: normal order */ > + merge_opt->branch1 = short_commit_name(repo, replayed_base); > + merge_opt->branch2 = short_commit_name(repo, pickme); > + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); > + > + merge_incore_nonrecursive(merge_opt, > + base_tree, > + result->tree, > + pickme_tree, > + result); > + > + /* ancestor was allocated with xstrfmt, needs freeing */ > + free((char *)merge_opt->ancestor); And the "else" block has the original sequence of statements. > + } > > - free((char*)merge_opt->ancestor); > merge_opt->ancestor = NULL; > + merge_opt->branch2 = NULL; Not a new problem, but what is the point of setting these two (but not branch1) to NULL? If a later caller misuses ->ancestor left behind without setting its own, it would result in an access after free, but if such a caller misuses ->branch1 left behind without setting its own, because it is not allocated, it won't be an access after free, *but* it is nevertheless wrong as the string in ->branch1 is *not* computed suitably for that caller, isn't it? > if (!result->clean) > return NULL; > - return create_commit(repo, result->tree, pickme, replayed_base); > + return create_commit(repo, result->tree, pickme, replayed_base, is_revert); > } > @@ -350,6 +396,7 @@ int cmd_replay(int argc, > int contained = 0; > const char *ref_action = NULL; > enum ref_action_mode ref_mode; > + int is_revert = 0; Ditto on "revert,cherry-pick". > diff --git a/sequencer.c b/sequencer.c > index 5476d39ba9..e6d82c8368 100644 > --- a/sequencer.c > +++ b/sequencer.c > @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r, > return res; > } > > +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject) > +{ > + const char *revert_subject; > + > + if (skip_prefix(orig_subject, "Revert \"", &revert_subject) && > + /* > + * We don't touch pre-existing repeated reverts, because > + * theoretically these can be nested arbitrarily deeply, > + * thus requiring excessive complexity to deal with. > + */ > + !starts_with(revert_subject, "Revert \"")) { > + strbuf_addstr(out, "Reapply \""); > + strbuf_addstr(out, revert_subject); > + strbuf_addch(out, '\n'); > + } else { > + strbuf_addstr(out, "Revert \""); > + strbuf_addstr(out, orig_subject); > + strbuf_addstr(out, "\"\n"); > + } > + > + strbuf_addstr(out, "\nThis reverts commit "); > +} > + Dedup with do_pick_commit() where this was taken from. Possibly in a separte patch before the main one. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-25 19:22 ` Junio C Hamano @ 2025-11-25 19:30 ` Junio C Hamano 2025-11-25 19:39 ` Junio C Hamano 2025-11-26 19:26 ` Siddharth Asthana 1 sibling, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2025-11-25 19:30 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Junio C Hamano <gitster@pobox.com> writes: > In do_pick_commit() where TODO_REVERT command is handled, we find a > code block that is almost identical to what this patch adds to the > new helper function; it should be rewritten to call the new helper > function or perhaps a shared helper function is introduced and > called from there and also from the sequencer_format_revert_header() > function, if there is still some impedance mismatch. If such a > refactoring is done as a separate preliminary patch in a N-patch > series, the resulting patch series may be easier to follow (and > there may be other opportunities to reuse existing code more). > ... >> diff --git a/sequencer.c b/sequencer.c >> index 5476d39ba9..e6d82c8368 100644 >> --- a/sequencer.c >> +++ b/sequencer.c >> @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r, >> return res; >> } >> >> +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject) >> +{ >> + const char *revert_subject; >> + >> + if (skip_prefix(orig_subject, "Revert \"", &revert_subject) && >> + /* >> + * We don't touch pre-existing repeated reverts, because >> + * theoretically these can be nested arbitrarily deeply, >> + * thus requiring excessive complexity to deal with. >> + */ >> + !starts_with(revert_subject, "Revert \"")) { >> + strbuf_addstr(out, "Reapply \""); >> + strbuf_addstr(out, revert_subject); >> + strbuf_addch(out, '\n'); >> + } else { >> + strbuf_addstr(out, "Revert \""); >> + strbuf_addstr(out, orig_subject); >> + strbuf_addstr(out, "\"\n"); >> + } >> + >> + strbuf_addstr(out, "\nThis reverts commit "); >> +} >> + > > Dedup with do_pick_commit() where this was taken from. Possibly in > a separte patch before the main one. Forgot to attach this at the end. What I meant was that something along this line may be a good starting point. sequencer.c | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git c/sequencer.c w/sequencer.c index e6d82c8368..29909952d4 100644 --- c/sequencer.c +++ w/sequencer.c @@ -2365,20 +2365,8 @@ static int do_pick_commit(struct repository *r, if (opts->commit_use_reference) { strbuf_commented_addf(&ctx->message, comment_line_str, "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && - /* - * We don't touch pre-existing repeated reverts, because - * theoretically these can be nested arbitrarily deeply, - * thus requiring excessive complexity to deal with. - */ - !starts_with(orig_subject, "Revert \"")) { - strbuf_addstr(&ctx->message, "Reapply \""); - strbuf_addstr(&ctx->message, orig_subject); - strbuf_addstr(&ctx->message, "\n"); } else { - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); + sequencer_format_revert_header(&ctx->message, msg.subject); } strbuf_addstr(&ctx->message, "\nThis reverts commit "); refer_to_commit(opts, &ctx->message, commit); ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-25 19:30 ` Junio C Hamano @ 2025-11-25 19:39 ` Junio C Hamano 2025-11-25 20:06 ` Junio C Hamano 2025-11-26 19:28 ` Siddharth Asthana 0 siblings, 2 replies; 92+ messages in thread From: Junio C Hamano @ 2025-11-25 19:39 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Junio C Hamano <gitster@pobox.com> writes: >> Dedup with do_pick_commit() where this was taken from. Possibly in >> a separte patch before the main one. > > Forgot to attach this at the end. What I meant was that something > along this line may be a good starting point. > > sequencer.c | 14 +------------- > 1 file changed, 1 insertion(+), 13 deletions(-) > > diff --git c/sequencer.c w/sequencer.c > index e6d82c8368..29909952d4 100644 > --- c/sequencer.c > +++ w/sequencer.c > @@ -2365,20 +2365,8 @@ static int do_pick_commit(struct repository *r, > if (opts->commit_use_reference) { > strbuf_commented_addf(&ctx->message, comment_line_str, > "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); > - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && > - /* > - * We don't touch pre-existing repeated reverts, because > - * theoretically these can be nested arbitrarily deeply, > - * thus requiring excessive complexity to deal with. > - */ > - !starts_with(orig_subject, "Revert \"")) { > - strbuf_addstr(&ctx->message, "Reapply \""); > - strbuf_addstr(&ctx->message, orig_subject); > - strbuf_addstr(&ctx->message, "\n"); > } else { > - strbuf_addstr(&ctx->message, "Revert \""); > - strbuf_addstr(&ctx->message, msg.subject); > - strbuf_addstr(&ctx->message, "\"\n"); > + sequencer_format_revert_header(&ctx->message, msg.subject); > } > strbuf_addstr(&ctx->message, "\nThis reverts commit "); > refer_to_commit(opts, &ctx->message, commit); By the way, I probably would not be queuing this version today, as this has obvious conflict with a large code movement made by Patrick's "history" series, which itself is expecting a reroll. Perhaps collect review comments on this iteration a bit more and wait for that other topic to be rerolled, and if it turns out to be solid enough, base a v2 of this patch on top of it? Thanks. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-25 19:39 ` Junio C Hamano @ 2025-11-25 20:06 ` Junio C Hamano 2025-11-26 19:31 ` Siddharth Asthana 2025-11-26 19:28 ` Siddharth Asthana 1 sibling, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2025-11-25 20:06 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Junio C Hamano <gitster@pobox.com> writes: > By the way, I probably would not be queuing this version today, as > this has obvious conflict with a large code movement made by > Patrick's "history" series, which itself is expecting a reroll. > > Perhaps collect review comments on this iteration a bit more and > wait for that other topic to be rerolled, and if it turns out to be > solid enough, base a v2 of this patch on top of it? While I cannot test it with other topics, I had a chance to run tests after applying the patch directly on top of 'master': $ make CC=clang SANITIZE=address,leak test ... Test Summary Report ------------------- t3650-replay-basics.sh (Wstat: 256 (exited 1) Tests: 31 Failed: 5) Failed tests: 23-25, 27, 31 Non-zero exit status: 1 The first failure was this one expecting success of 3650.23 'using replay with --revert to revert a commit': # Revert commits D and E from topic2 git replay --revert --onto topic1 topic1..topic2 >result && test_line_count = 1 result && NEW_TOPIC2=$(cut -f 3 -d " " result) && # Verify the result updates the topic2 branch printf "update refs/heads/topic2 " >expect && printf "%s " $NEW_TOPIC2 >>expect && git rev-parse topic2 >>expect && test_cmp expect result && # Verify the commit messages contain "Revert" # topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A) git log --format=%s $NEW_TOPIC2 >actual && test_line_count = 6 actual && head -n 1 actual >first-line && test_grep "^Revert" first-line test_line_count: line count for result != 1 The "result" file has 0 bytes (hence 0 lines). Actually, address or leak sanitizing build is not needed to reproduce this problem, it seems. $ make CC=clang test Was sufficient to see the same first failure. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-25 20:06 ` Junio C Hamano @ 2025-11-26 19:31 ` Siddharth Asthana 0 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-11-26 19:31 UTC (permalink / raw) To: Junio C Hamano Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On 26/11/25 01:36, Junio C Hamano wrote: > Junio C Hamano <gitster@pobox.com> writes: > >> By the way, I probably would not be queuing this version today, as >> this has obvious conflict with a large code movement made by >> Patrick's "history" series, which itself is expecting a reroll. >> >> Perhaps collect review comments on this iteration a bit more and >> wait for that other topic to be rerolled, and if it turns out to be >> solid enough, base a v2 of this patch on top of it? > While I cannot test it with other topics, I had a chance to run > tests after applying the patch directly on top of 'master': > > $ make CC=clang SANITIZE=address,leak test > ... > Test Summary Report > ------------------- > t3650-replay-basics.sh (Wstat: 256 (exited 1) Tests: 31 Failed: 5) > Failed tests: 23-25, 27, 31 > Non-zero exit status: 1 > > The first failure was this one > > expecting success of 3650.23 'using replay with --revert to revert a commit': > # Revert commits D and E from topic2 > git replay --revert --onto topic1 topic1..topic2 >result && > > test_line_count = 1 result && > NEW_TOPIC2=$(cut -f 3 -d " " result) && > > # Verify the result updates the topic2 branch > printf "update refs/heads/topic2 " >expect && > printf "%s " $NEW_TOPIC2 >>expect && > git rev-parse topic2 >>expect && > > test_cmp expect result && > > # Verify the commit messages contain "Revert" > # topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A) > git log --format=%s $NEW_TOPIC2 >actual && > test_line_count = 6 actual && > head -n 1 actual >first-line && > test_grep "^Revert" first-line > > test_line_count: line count for result != 1 > > The "result" file has 0 bytes (hence 0 lines). Ah, this is because my patch was based on a tree that had atomic ref updates as the default (REF_ACTION_UPDATE), which produces no stdout output. The tests were written for --ref-action=print behavior. I have fixed the tests to either: 1. Use --ref-action=print explicitly when expecting output, or 2. Check the ref state directly rather than parsing stdout The test failures you saw should be fixed in v2. Thanks, Siddharth > > Actually, address or leak sanitizing build is not needed to > reproduce this problem, it seems. > > $ make CC=clang test > > Was sufficient to see the same first failure. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-25 19:39 ` Junio C Hamano 2025-11-25 20:06 ` Junio C Hamano @ 2025-11-26 19:28 ` Siddharth Asthana 1 sibling, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-11-26 19:28 UTC (permalink / raw) To: Junio C Hamano Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On 26/11/25 01:09, Junio C Hamano wrote: > Junio C Hamano <gitster@pobox.com> writes: > >>> Dedup with do_pick_commit() where this was taken from. Possibly in >>> a separte patch before the main one. >> Forgot to attach this at the end. What I meant was that something >> along this line may be a good starting point. >> >> sequencer.c | 14 +------------- >> 1 file changed, 1 insertion(+), 13 deletions(-) >> >> diff --git c/sequencer.c w/sequencer.c >> index e6d82c8368..29909952d4 100644 >> --- c/sequencer.c >> +++ w/sequencer.c >> @@ -2365,20 +2365,8 @@ static int do_pick_commit(struct repository *r, >> if (opts->commit_use_reference) { >> strbuf_commented_addf(&ctx->message, comment_line_str, >> "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); >> - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && >> - /* >> - * We don't touch pre-existing repeated reverts, because >> - * theoretically these can be nested arbitrarily deeply, >> - * thus requiring excessive complexity to deal with. >> - */ >> - !starts_with(orig_subject, "Revert \"")) { >> - strbuf_addstr(&ctx->message, "Reapply \""); >> - strbuf_addstr(&ctx->message, orig_subject); >> - strbuf_addstr(&ctx->message, "\n"); >> } else { >> - strbuf_addstr(&ctx->message, "Revert \""); >> - strbuf_addstr(&ctx->message, msg.subject); >> - strbuf_addstr(&ctx->message, "\"\n"); >> + sequencer_format_revert_header(&ctx->message, msg.subject); >> } >> strbuf_addstr(&ctx->message, "\nThis reverts commit "); >> refer_to_commit(opts, &ctx->message, commit); > By the way, I probably would not be queuing this version today, as > this has obvious conflict with a large code movement made by > Patrick's "history" series, which itself is expecting a reroll. > > Perhaps collect review comments on this iteration a bit more and > wait for that other topic to be rerolled, and if it turns out to be > solid enough, base a v2 of this patch on top of it? Understood. I will wait for Patrick's "history" series to be rerolled and base v2 on top of that to avoid conflicts. In the meantime, I will address all the review feedback locally. Thanks, Siddharth > > Thanks. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-25 19:22 ` Junio C Hamano 2025-11-25 19:30 ` Junio C Hamano @ 2025-11-26 19:26 ` Siddharth Asthana 2025-11-26 21:13 ` Junio C Hamano 1 sibling, 1 reply; 92+ messages in thread From: Siddharth Asthana @ 2025-11-26 19:26 UTC (permalink / raw) To: Junio C Hamano Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On 26/11/25 00:52, Junio C Hamano wrote: > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > >> The revert message generation logic (handling "Revert" and "Reapply" >> cases) is extracted into a new `sequencer_format_revert_header()` >> function in `sequencer.c`, which can be shared between `sequencer.c` >> and `builtin/replay.c`. The `builtin/replay.c` code calls this shared >> function and then appends the commit OID using `oid_to_hex()` directly, >> since git replay is designed for simpler server-side operations without >> the interactive features and `replay_opts` framework used by >> `sequencer.c`. > When I review a patch that claims to refactor existing logic into a > separate helper function to reuse it in more places, I look at the > diffstat to see how many lines are removed. You are right - in v1 I added the helper but didn't update do_pick_commit() to use it. I have fixed this in my local tree; the dedup change is: sequencer.c | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) > The logic for > generating the message does not seem to be "extracted into", but > rather "duplicated to", the new helper function. It gives the two > message sources opportunity to drift apart over time, which is not > what you want. > > In do_pick_commit() where TODO_REVERT command is handled, we find a > code block that is almost identical to what this patch adds to the > new helper function; it should be rewritten to call the new helper > function or perhaps a shared helper function is introduced and > called from there and also from the sequencer_format_revert_header() > function, if there is still some impedance mismatch. If such a > refactoring is done as a separate preliminary patch in a N-patch > series, the resulting patch series may be easier to follow (and > there may be other opportunities to reuse existing code more). > >> Mark the option as incompatible with `--contained` since reverting >> changes across multiple branches simultaneously could lead to >> inconsistent repository states. > This, and the documentation part, does not seem to tell what > "inconsistent state" we are worried about. Elijah's reply clarified this perfectly - `--contained` is a modifier for `--onto`, and as he points out, `--revert` should be a new mode entirely, not a modifier. Once `--revert` is its own mode (like `--onto` and `--advance`), the incompatibility with `--contained` becomes clear: `--contained` only makes sense with `--onto`. > Is it just a buggy > design of --revert can be implemented that produces wrong result > when used with --contened, or are these two options inherently try > to achieve contradicting goals? I am guessing that it is the > latter, but if so, can we make it clear why? > >> +--revert:: >> + Revert the changes introduced by the commits in the revision range >> + instead of applying them. This reverses the diff direction and creates >> + new commits that undo the changes, similar to `git revert`. >> ++ >> +The commit messages are prefixed with "Revert" and include the original >> +commit SHA. If reverting a commit whose message starts with "Revert", the new >> +message will start with "Reapply" instead. The author of the new commits >> +will be the current user, not the original commit author. >> ++ >> +This option is incompatible with `--contained`. > I have never used the `--contained` option, but is it so obvious to > those who have why these two have to be made incompatible that the > above statement does not have to be followed by "because ..."? > >> @@ -141,6 +153,27 @@ all commits they have since `base`, playing them on top of >> `origin/main`. These three branches may have commits on top of `base` >> that they have in common, but that does not need to be the case. >> >> +To revert a range of commits: >> + >> +------------ >> +$ git replay --revert --onto main feature~3..feature >> +------------ >> + >> +This creates new commits on top of 'main' that reverse the changes introduced >> +by the last three commits on 'feature'. The 'feature' branch is updated to >> +point at the last of these revert commits. The 'main' branch is not updated >> +in this case. > Is there any topological requirement between 'main' and 'feature' > branches? Yes, and I failed to explain this. For reverts to produce meaningful non-empty commits, the commits being reverted should already be in the target branch's history. I will clarify the examples to show this topology requirement explicitly. > Naïvely, I would expect that it would be perfect if > 'feature' branch has been merged to 'main' (then you'd be reverting > the top 3 commits of that branch), but that would be something you > would do to correct 'main', and not 'feature', but the description > explains this is a way to update 'feature' to lose the three topmost > commits, so I am not sure what this example really does and when it > would be useful. > >> +To revert commits and advance a branch: >> + >> +------------ >> +$ git replay --revert --advance main feature~2..feature >> +------------ >> + >> +This reverts the last two commits from 'feature', applies those reverts >> +on top of 'main', and updates 'main' to point at the result. The 'feature' >> +branch is not updated in this case. > The same question. If I assume that 'main' has merged 'feature' > before, this I can understand and match what I often do quite well > while working on integrating topic branches. I may merge a topic > that is not yet well cooked enough into 'next', regret that the two > commits at the tip of the topic were premature, and revert these two > commits out of 'next', or something. This example can be explained > well if there is topological requirement that 'main' has at least > these two commits from 'feature'. > >> @@ -261,7 +286,8 @@ static struct commit *pick_regular_commit(struct repository *repo, >> kh_oid_map_t *replayed_commits, >> struct commit *onto, >> struct merge_options *merge_opt, >> - struct merge_result *result) >> + struct merge_result *result, >> + int is_revert) > Are there other ways to pick commit imaginable (if not planned to be > implemented), other than "revert"? I am wondering if this is better > done as "enum { CHERRY_PICK, REVERT, } pick_variant" for readability > and maintainability. Good point. An enum would be clearer and more maintainable. I wll change to `enum replay_action { REPLAY_PICK, REPLAY_REVERT }`. > >> @@ -273,21 +299,41 @@ static struct commit *pick_regular_commit(struct repository *repo, >> pickme_tree = repo_get_commit_tree(repo, pickme); >> base_tree = repo_get_commit_tree(repo, base); >> >> - merge_opt->branch1 = short_commit_name(repo, replayed_base); >> - merge_opt->branch2 = short_commit_name(repo, pickme); >> - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); >> + if (is_revert) { > It may be just me, but it would have been easier to follow if > !revert case is given first, as that is the common variant the > pick_regular_commit() function. Makes sense - the common case (cherry-pick) should come first. I will reorder the if/else. > >> + /* For revert: swap base and pickme to reverse the diff */ >> + merge_opt->branch1 = short_commit_name(repo, replayed_base); >> + merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme)); > That is an overly long line (sorry, I notice these things when a > line does not even fit in 92-col terminal). Fixed in my local tree by introducing a `pickme_name` variable. > >> + merge_opt->ancestor = short_commit_name(repo, pickme); >> - merge_incore_nonrecursive(merge_opt, >> - base_tree, >> - result->tree, >> - pickme_tree, >> - result); >> + merge_incore_nonrecursive(merge_opt, >> + pickme_tree, >> + result->tree, >> + base_tree, >> + result); > OK. These are applications of the standard 3-way merge trick to > (ab)use ancestor to implement cherry-pick and revert. Looking good. > >> + >> + /* branch2 was allocated with xstrfmt, needs freeing */ >> + free((char *)merge_opt->branch2); >> + } else { >> + /* For cherry-pick: normal order */ >> + merge_opt->branch1 = short_commit_name(repo, replayed_base); >> + merge_opt->branch2 = short_commit_name(repo, pickme); >> + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); >> + >> + merge_incore_nonrecursive(merge_opt, >> + base_tree, >> + result->tree, >> + pickme_tree, >> + result); >> + >> + /* ancestor was allocated with xstrfmt, needs freeing */ >> + free((char *)merge_opt->ancestor); > And the "else" block has the original sequence of statements. > >> + } >> >> - free((char*)merge_opt->ancestor); >> merge_opt->ancestor = NULL; >> + merge_opt->branch2 = NULL; > Not a new problem, but what is the point of setting these two (but > not branch1) to NULL? You're right, this is inconsistent. The intent is to prevent use-after-free, but setting only some fields to NULL is incomplete. I will either set all three to NULL or add a comment explaining the rationale. > If a later caller misuses ->ancestor left > behind without setting its own, it would result in an access after > free, but if such a caller misuses ->branch1 left behind without > setting its own, because it is not allocated, it won't be an access > after free, *but* it is nevertheless wrong as the string in ->branch1 > is *not* computed suitably for that caller, isn't it? > >> if (!result->clean) >> return NULL; >> - return create_commit(repo, result->tree, pickme, replayed_base); >> + return create_commit(repo, result->tree, pickme, replayed_base, is_revert); >> } > >> @@ -350,6 +396,7 @@ int cmd_replay(int argc, >> int contained = 0; >> const char *ref_action = NULL; >> enum ref_action_mode ref_mode; >> + int is_revert = 0; > Ditto on "revert,cherry-pick". > >> diff --git a/sequencer.c b/sequencer.c >> index 5476d39ba9..e6d82c8368 100644 >> --- a/sequencer.c >> +++ b/sequencer.c >> @@ -5572,6 +5572,29 @@ int sequencer_pick_revisions(struct repository *r, >> return res; >> } >> >> +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject) >> +{ >> + const char *revert_subject; >> + >> + if (skip_prefix(orig_subject, "Revert \"", &revert_subject) && >> + /* >> + * We don't touch pre-existing repeated reverts, because >> + * theoretically these can be nested arbitrarily deeply, >> + * thus requiring excessive complexity to deal with. >> + */ >> + !starts_with(revert_subject, "Revert \"")) { >> + strbuf_addstr(out, "Reapply \""); >> + strbuf_addstr(out, revert_subject); >> + strbuf_addch(out, '\n'); >> + } else { >> + strbuf_addstr(out, "Revert \""); >> + strbuf_addstr(out, orig_subject); >> + strbuf_addstr(out, "\"\n"); >> + } >> + >> + strbuf_addstr(out, "\nThis reverts commit "); >> +} >> + > Dedup with do_pick_commit() where this was taken from. Possibly in > a separte patch before the main one. I have applied your suggested patch and will split this into a 2-patch series: (1) extract and reuse sequencer_format_revert_header(), (2) add --revert to replay. Thanks, Siddharth ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 19:26 ` Siddharth Asthana @ 2025-11-26 21:13 ` Junio C Hamano 2025-11-27 19:23 ` Siddharth Asthana 0 siblings, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2025-11-26 21:13 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Siddharth Asthana <siddharthasthana31@gmail.com> writes: >>> +This creates new commits on top of 'main' that reverse the changes introduced >>> +by the last three commits on 'feature'. The 'feature' branch is updated to >>> +point at the last of these revert commits. The 'main' branch is not updated >>> +in this case. >> Is there any topological requirement between 'main' and 'feature' >> branches? > > Yes, and I failed to explain this. For reverts to produce meaningful > non-empty commits, the commits being reverted should already be in the > target branch's history. I will clarify the examples to show this > topology requirement explicitly. We need to be a bit careful, though. Strictly speaking, what we have is not a requirement on the shape of the history. If a topic that was merged to the development branch gets cherry-picked to the master branch, and then it turns out to be faulty and needs to be reverted, we can still "revert" the original topic out of the master branch, even though topologically, the original topic is *not* in 'master'. >>> + /* For revert: swap base and pickme to reverse the diff */ >>> + merge_opt->branch1 = short_commit_name(repo, replayed_base); >>> + merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme)); >> That is an overly long line (sorry, I notice these things when a >> line does not even fit in 92-col terminal). > > > Fixed in my local tree by introducing a `pickme_name` variable. Just a line-folding at an appropriate column may be sufficient, e.g., merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme)); >>> - free((char*)merge_opt->ancestor); >>> merge_opt->ancestor = NULL; >>> + merge_opt->branch2 = NULL; >> Not a new problem, but what is the point of setting these two (but >> not branch1) to NULL? > > > You're right, this is inconsistent. The intent is to prevent > use-after-free, but setting only some fields to NULL is incomplete. I > will either set all three to NULL or add a comment explaining the rationale. Is this the only place that resets a subset of merge_opt members for reuse? If not, are these multiple places want to reset the same subset of the members? Perhaps we can use a helper function to clarify in such a case. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 21:13 ` Junio C Hamano @ 2025-11-27 19:23 ` Siddharth Asthana 0 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-11-27 19:23 UTC (permalink / raw) To: Junio C Hamano Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On 27/11/25 02:43, Junio C Hamano wrote: > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > >>>> +This creates new commits on top of 'main' that reverse the changes introduced >>>> +by the last three commits on 'feature'. The 'feature' branch is updated to >>>> +point at the last of these revert commits. The 'main' branch is not updated >>>> +in this case. >>> Is there any topological requirement between 'main' and 'feature' >>> branches? >> Yes, and I failed to explain this. For reverts to produce meaningful >> non-empty commits, the commits being reverted should already be in the >> target branch's history. I will clarify the examples to show this >> topology requirement explicitly. > We need to be a bit careful, though. Strictly speaking, what we > have is not a requirement on the shape of the history. Right - it's the changes that need to exist in the target tree, not the commits themselves. Cherry-picked commits can be reverted even without topological ancestry. I will fix the documentation. > If a topic > that was merged to the development branch gets cherry-picked to the > master branch, and then it turns out to be faulty and needs to be > reverted, we can still "revert" the original topic out of the master > branch, even though topologically, the original topic is *not* in > 'master'. > >>>> + /* For revert: swap base and pickme to reverse the diff */ >>>> + merge_opt->branch1 = short_commit_name(repo, replayed_base); >>>> + merge_opt->branch2 = xstrfmt("parent of %s", short_commit_name(repo, pickme)); >>> That is an overly long line (sorry, I notice these things when a >>> line does not even fit in 92-col terminal). >> >> Fixed in my local tree by introducing a `pickme_name` variable. > Just a line-folding at an appropriate column may be sufficient, e.g., Will do. > > merge_opt->branch2 = xstrfmt("parent of %s", > short_commit_name(repo, pickme)); > >>>> - free((char*)merge_opt->ancestor); >>>> merge_opt->ancestor = NULL; >>>> + merge_opt->branch2 = NULL; >>> Not a new problem, but what is the point of setting these two (but >>> not branch1) to NULL? >> >> You're right, this is inconsistent. The intent is to prevent >> use-after-free, but setting only some fields to NULL is incomplete. I >> will either set all three to NULL or add a comment explaining the rationale. > Is this the only place that resets a subset of merge_opt members for > reuse? If not, are these multiple places want to reset the same > subset of the members? Perhaps we can use a helper function to > clarify in such a case. This is the only place in replay.c. The reason we only NULL ancestor and branch2 is that those are the ones allocated with xstrfmt() - branch1 points to short_commit_name() which doesn't need freeing. A helper would make the intent clearer. I will look into adding one. Thanks, Siddharth ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-25 17:00 ` [PATCH 1/1] " Siddharth Asthana 2025-11-25 19:22 ` Junio C Hamano @ 2025-11-26 11:10 ` Phillip Wood 2025-11-26 17:35 ` Elijah Newren 2025-11-26 19:39 ` Siddharth Asthana 1 sibling, 2 replies; 92+ messages in thread From: Phillip Wood @ 2025-11-26 11:10 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Hi Siddharth On 25/11/2025 17:00, Siddharth Asthana wrote: > > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc > index dcb26e8a8e..ad7dc08622 100644 > --- a/Documentation/git-replay.adoc > +++ b/Documentation/git-replay.adoc > @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it. > [...] > +To revert a range of commits: > + > +------------ > +$ git replay --revert --onto main feature~3..feature > +------------ > + > +This creates new commits on top of 'main' that reverse the changes introduced > +by the last three commits on 'feature'. The 'feature' branch is updated to > +point at the last of these revert commits. The 'main' branch is not updated > +in this case. I'm struggling to understand when I'd want to do this. Why would I want to update 'feature' to point to the reverted version of its last tree commits rebased onto 'main'? In order to understand I ran the first tests case which does git replay --onto topic1 --revert topic1..topic2 after fixing it by adding --ref-action=print the resulting commit log looks like commit d337fab78e90008835f74e890039b464a0308cbe Author: author@name <bogus@email@address> Date: Thu Apr 7 15:30:13 2005 -0700 Revert "E " This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276. commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a Author: author@name <bogus@email@address> Date: Thu Apr 7 15:30:13 2005 -0700 Revert "D " This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7. The commits are empty because the original they are reverting each create a new file which is then present in the base revision but not in either of the merge heads when we revert. This suggests to me that it is not a very realistic test and I'm still scratching my head to see where "git replay --onto <commit> --revert" is useful. If '--revert' does not make sense with '--onto' then perhaps it should be a new mode that takes a ref and acts like '--advance' but reverts the commits rather than cherry-picking them. When reverting a range of commits it would reduce the likelihood of conflicts to revert then in reverse order so we should either recommend passing '--reverse' or make that the default when '--revert' is given. As you can see in the log output above the new function to format the revert subject lines is buggy. If you had used test_commit_message() to check the commit message, rather than just grepping for ^Revert the tests would have picked that up. Thanks Phillip ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 11:10 ` Phillip Wood @ 2025-11-26 17:35 ` Elijah Newren 2025-11-26 18:41 ` Junio C Hamano 2025-11-26 19:50 ` Siddharth Asthana 2025-11-26 19:39 ` Siddharth Asthana 1 sibling, 2 replies; 92+ messages in thread From: Elijah Newren @ 2025-11-26 17:35 UTC (permalink / raw) To: phillip.wood Cc: Siddharth Asthana, git, christian.couder, ps, gitster, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On Wed, Nov 26, 2025 at 3:10 AM Phillip Wood <phillip.wood123@gmail.com> wrote: > > Hi Siddharth > > On 25/11/2025 17:00, Siddharth Asthana wrote: > > > > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc > > index dcb26e8a8e..ad7dc08622 100644 > > --- a/Documentation/git-replay.adoc > > +++ b/Documentation/git-replay.adoc > > @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it. > > [...] > > +To revert a range of commits: > > + > > +------------ > > +$ git replay --revert --onto main feature~3..feature > > +------------ > > + > > +This creates new commits on top of 'main' that reverse the changes introduced > > +by the last three commits on 'feature'. The 'feature' branch is updated to > > +point at the last of these revert commits. The 'main' branch is not updated > > +in this case. > > I'm struggling to understand when I'd want to do this. Why would I want > to update 'feature' to point to the reverted version of its last tree > commits rebased onto 'main'? In order to understand I ran the first > tests case which does > > git replay --onto topic1 --revert topic1..topic2 > > after fixing it by adding --ref-action=print the resulting commit log > looks like > > commit d337fab78e90008835f74e890039b464a0308cbe > Author: author@name <bogus@email@address> > Date: Thu Apr 7 15:30:13 2005 -0700 > > Revert "E > " > > This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276. > > commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a > Author: author@name <bogus@email@address> > Date: Thu Apr 7 15:30:13 2005 -0700 > > Revert "D > " > > This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7. > > > The commits are empty because the original they are reverting each > create a new file which is then present in the base revision but not in > either of the merge heads when we revert. This suggests to me that it is > not a very realistic test and I'm still scratching my head to see where > "git replay --onto <commit> --revert" is useful. > > If '--revert' does not make sense with '--onto' then perhaps it should > be a new mode that takes a ref and acts like '--advance' but reverts the > commits rather than cherry-picking them. When reverting a range of > commits it would reduce the likelihood of conflicts to revert then in > reverse order so we should either recommend passing '--reverse' or make > that the default when '--revert' is given. > > As you can see in the log output above the new function to format the > revert subject lines is buggy. If you had used test_commit_message() to > check the commit message, rather than just grepping for ^Revert the > tests would have picked that up. > > Thanks > > Phillip I was going to say the same thing, but from a different angle. The sequencer in git is used for three different types of operations: rebasing, cherry-picking, and reverting a range (with a sequence of reverts rather than one big revert). In replay, these correspond to --onto, --advance, and the new thing you are trying to add. As such, it should be its own new mode. (I do tend to see ranges reverted by a single big revert, the way Johannes suggested, rather than as a range of individual reverts, so to me the utility of the new mode looks low, but perhaps others find more utility in it. Or maybe the intent is to only use it with a revision range that is only one commit long?) Phillip also went into more detail about why "--onto $COMMIT --revert" specifically doesn't make sense. I'd also say "--advance $BRANCH --revert" doesn't read well because to users, "revert" means going back while "advance" means going forward, so it's a rather confusing command line to make them wrap their head around. And yes, Siddharth, you were right that the new mode should be incompatible with --contained, but that's because --contained is a special modifier of --onto. --onto, --advance, and --revert are three different modes that are incompatible with each other. Once you've checked for that incompatibility between the three modes, then you can either check that whenever --contained is specified, either --onto is as well, or neither --advance nor --revert are. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 17:35 ` Elijah Newren @ 2025-11-26 18:41 ` Junio C Hamano 2025-11-26 21:17 ` Junio C Hamano 2025-11-26 19:50 ` Siddharth Asthana 1 sibling, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2025-11-26 18:41 UTC (permalink / raw) To: Elijah Newren Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Elijah Newren <newren@gmail.com> writes: >> I'm struggling to understand when I'd want to do this. Why would I want >> to update 'feature' to point to the reverted version of its last tree >> commits rebased onto 'main'? >> ... > I was going to say the same thing, but from a different angle. > > The sequencer in git is used for three different types of operations: > rebasing, cherry-picking, and reverting a range (with a sequence of > reverts rather than one big revert). In replay, these correspond to > --onto, --advance, and the new thing you are trying to add. As such, > it should be its own new mode. This is a great comment that clarifies what the problem is with this. > And yes, Siddharth, you were right that the new mode should be > incompatible with --contained, but that's because --contained is a > special modifier of --onto. --onto, --advance, and --revert are three > different modes that are incompatible with each other. This answers the question I had on the patch perfectly. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 18:41 ` Junio C Hamano @ 2025-11-26 21:17 ` Junio C Hamano 2025-11-26 23:06 ` Elijah Newren 0 siblings, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2025-11-26 21:17 UTC (permalink / raw) To: Elijah Newren Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Junio C Hamano <gitster@pobox.com> writes: > Elijah Newren <newren@gmail.com> writes: > >>> I'm struggling to understand when I'd want to do this. Why would I want >>> to update 'feature' to point to the reverted version of its last tree >>> commits rebased onto 'main'? >>> ... >> I was going to say the same thing, but from a different angle. >> >> The sequencer in git is used for three different types of operations: >> rebasing, cherry-picking, and reverting a range (with a sequence of >> reverts rather than one big revert). In replay, these correspond to >> --onto, --advance, and the new thing you are trying to add. As such, >> it should be its own new mode. > > This is a great comment that clarifies what the problem is with this. Stepping back a bit, is it just me who thinks that the "--onto" option is a misnamed "--rebase", and the "--advance" option is a misnamed "--cherry-pick"? Perhaps it is already way too late to remedy, but if we ever want to change it, we should do so while "replay" is still marked experimental. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 21:17 ` Junio C Hamano @ 2025-11-26 23:06 ` Elijah Newren 2025-11-26 23:14 ` Junio C Hamano 0 siblings, 1 reply; 92+ messages in thread From: Elijah Newren @ 2025-11-26 23:06 UTC (permalink / raw) To: Junio C Hamano Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On Wed, Nov 26, 2025 at 1:17 PM Junio C Hamano <gitster@pobox.com> wrote: > > Junio C Hamano <gitster@pobox.com> writes: > > > Elijah Newren <newren@gmail.com> writes: > > > >>> I'm struggling to understand when I'd want to do this. Why would I want > >>> to update 'feature' to point to the reverted version of its last tree > >>> commits rebased onto 'main'? > >>> ... > >> I was going to say the same thing, but from a different angle. > >> > >> The sequencer in git is used for three different types of operations: > >> rebasing, cherry-picking, and reverting a range (with a sequence of > >> reverts rather than one big revert). In replay, these correspond to > >> --onto, --advance, and the new thing you are trying to add. As such, > >> it should be its own new mode. > > > > This is a great comment that clarifies what the problem is with this. > > Stepping back a bit, is it just me who thinks that the "--onto" > option is a misnamed "--rebase", and the "--advance" option is a > misnamed "--cherry-pick"? Is the goal to make connections between existing commands for folks already very familiar with git, at the expense of comprehensibility for new users and command lines that look somewhat illogical? For either a rebase or a cherry-pick operation you have: (A) a range of commits to be transplanted, (B) a base on which to build from, and (C) the choice of which ref(s) should be updated to point to the transplanted commits. cherry-pick assumed HEAD for both (B) and (C). rebase formed an implicit range instead of letting the user specify (in a way which has always made it difficult to teach to new users, IMO, but I digress), which involves HEAD and also used HEAD for (C). git replay removes all assumptions about HEAD, which means there is much more freedom for (A), (B), and (C), but I think it also makes it more important to try to make command lines at least a bit more self-describing for users to learn. == Example command lines today == git replay --onto main feature~3..feature This command replays the commits in the range feature~3..feature onto main, and updates feature to point at the result. git replay --advance main feature~3..feature This command replays the commits in the range feature~3..feature onto main, and advances main to point at the result. (Both replay the same commit range on the same base, they differ only in which refs are updated at the end.) == Example command lines from your proposal == git replay --rebase main feature~3..feature This command to me would suggest that main is being rebased, but it isn't -- it rebases feature~3..feature onto main while updating feature to point at the result. I find the "--rebase main" part of this command line confusing. git replay --cherry-pick main feature~3..feature This command to me would suggest that main is being cherry-picked, but it isn't -- it cherry-picks feature~3..feature onto main while updating main to point at the result. Again, I find the "--cherry-pick main" part of this command line confusing. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 23:06 ` Elijah Newren @ 2025-11-26 23:14 ` Junio C Hamano 2025-11-26 23:57 ` Elijah Newren 0 siblings, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2025-11-26 23:14 UTC (permalink / raw) To: Elijah Newren Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Elijah Newren <newren@gmail.com> writes: > == Example command lines from your proposal == > > git replay --rebase main feature~3..feature > > This command to me would suggest that main is being rebased, but it > isn't -- it rebases feature~3..feature onto main while updating > feature to point at the result. I find the "--rebase main" part of > this command line confusing. > > git replay --cherry-pick main feature~3..feature > > This command to me would suggest that main is being cherry-picked, but > it isn't -- it cherry-picks feature~3..feature onto main while > updating main to point at the result. Again, I find the > "--cherry-pick main" part of this command line confusing. That only tells us that if you want to help users by limiting the vocabulary to a single set (i.e. both command names, and mode names used in replay), you'd need to make sure you have the order of <branch> and <range> given to the replay command in logical order, in line with the option name, no? Of course, if you want to say "cherry-pick", cherry-picked range would have to come near the option flag that says "cherry-pick", naturally. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 23:14 ` Junio C Hamano @ 2025-11-26 23:57 ` Elijah Newren 0 siblings, 0 replies; 92+ messages in thread From: Elijah Newren @ 2025-11-26 23:57 UTC (permalink / raw) To: Junio C Hamano Cc: phillip.wood, Siddharth Asthana, git, christian.couder, ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On Wed, Nov 26, 2025 at 3:14 PM Junio C Hamano <gitster@pobox.com> wrote: > > Elijah Newren <newren@gmail.com> writes: > > > == Example command lines from your proposal == > > > > git replay --rebase main feature~3..feature > > > > This command to me would suggest that main is being rebased, but it > > isn't -- it rebases feature~3..feature onto main while updating > > feature to point at the result. I find the "--rebase main" part of > > this command line confusing. > > > > git replay --cherry-pick main feature~3..feature > > > > This command to me would suggest that main is being cherry-picked, but > > it isn't -- it cherry-picks feature~3..feature onto main while > > updating main to point at the result. Again, I find the > > "--cherry-pick main" part of this command line confusing. > > That only tells us that if you want to help users by limiting the > vocabulary to a single set (i.e. both command names, and mode names > used in replay), you'd need to make sure you have the order of > <branch> and <range> given to the replay command in logical order, > in line with the option name, no? Of course, if you want to say > "cherry-pick", cherry-picked range would have to come near the > option flag that says "cherry-pick", naturally. --advance and --onto are flags that require an argument -- in this case, "main". So, now you're suggesting more than renaming, in particular some bigger refactoring such as making these flags now require the <range> rather than the <base>. Let's follow that path a bit further... Does your proposal assume that <range> is simple, such as "feature~3..feature" above (i.e. something that an argument parser would view as a single argument)? What if the <range> were "^main feature1 feature2"? Or ""^$COMMIT --ancestry-path --branches"? (I don't see how to have the option parser easily be able to stuff the arguments to "--rebase ^$COMMIT --ancestry-path --branches" into a range variable that eats all of "^$COMMIT --ancestry-path --branches".) While I use simple ranges to describe the feature, I specifically built the command to be able to do things like those other two examples and use it for those. Those more complicated examples are things the rebase command just can't do. Also, just like `git log` allows `git log [<options>] [<revision range>]`, I wanted git replay to allow `git replay [<options>] [<revision range>]`. Instead of doing magic to get an implicit revision range as rebase does (and with rather limited options because of that magic), suddenly people can use what they've learned from `git log` in another place. But that piece of knowledge only really transfers if we do similarly to `git log`, i.e. the revision range comes after other options. Perhaps one way to avoid the first problem above is to make `--onto/--advance/--rebase/--cherry-pick" stop requiring (or accepting) an argument and turn them into simple mode toggles, and then make both <base> and <range> be positional arguments, with some well-defined ordering. However, if <base> comes before <revision> then we still have the same problem as my previous email, whereas if it comes after, then we weaken or destroy the connection to `git log` I made above. Maybe the connection to `git log` isn't that important. What I think is important either way, though, is if we use positional arguments for both things instead of making (at least one) an option, then I feel we are copying one of the designs of `git rebase` that makes it hard for even me to use: I hate that it uses multiple positional arguments to define the operation; despite using the command heavily for 16-17 years and sending in lots of patches to improve it, I still can't remember the order of those positional arguments and have to look it up again when teaching others. Maybe that's a personal shortcoming, but I would really rather that either <base> or <revision> was an option flag. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 17:35 ` Elijah Newren 2025-11-26 18:41 ` Junio C Hamano @ 2025-11-26 19:50 ` Siddharth Asthana 1 sibling, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-11-26 19:50 UTC (permalink / raw) To: Elijah Newren, phillip.wood Cc: git, christian.couder, ps, gitster, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On 26/11/25 23:05, Elijah Newren wrote: > On Wed, Nov 26, 2025 at 3:10 AM Phillip Wood <phillip.wood123@gmail.com> wrote: >> Hi Siddharth >> >> On 25/11/2025 17:00, Siddharth Asthana wrote: >>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc >>> index dcb26e8a8e..ad7dc08622 100644 >>> --- a/Documentation/git-replay.adoc >>> +++ b/Documentation/git-replay.adoc >>> @@ -54,6 +54,18 @@ which uses the target only as a starting point without updating it. >>> [...] >>> +To revert a range of commits: >>> + >>> +------------ >>> +$ git replay --revert --onto main feature~3..feature >>> +------------ >>> + >>> +This creates new commits on top of 'main' that reverse the changes introduced >>> +by the last three commits on 'feature'. The 'feature' branch is updated to >>> +point at the last of these revert commits. The 'main' branch is not updated >>> +in this case. >> I'm struggling to understand when I'd want to do this. Why would I want >> to update 'feature' to point to the reverted version of its last tree >> commits rebased onto 'main'? In order to understand I ran the first >> tests case which does >> >> git replay --onto topic1 --revert topic1..topic2 >> >> after fixing it by adding --ref-action=print the resulting commit log >> looks like >> >> commit d337fab78e90008835f74e890039b464a0308cbe >> Author: author@name <bogus@email@address> >> Date: Thu Apr 7 15:30:13 2005 -0700 >> >> Revert "E >> " >> >> This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276. >> >> commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a >> Author: author@name <bogus@email@address> >> Date: Thu Apr 7 15:30:13 2005 -0700 >> >> Revert "D >> " >> >> This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7. >> >> >> The commits are empty because the original they are reverting each >> create a new file which is then present in the base revision but not in >> either of the merge heads when we revert. This suggests to me that it is >> not a very realistic test and I'm still scratching my head to see where >> "git replay --onto <commit> --revert" is useful. >> >> If '--revert' does not make sense with '--onto' then perhaps it should >> be a new mode that takes a ref and acts like '--advance' but reverts the >> commits rather than cherry-picking them. When reverting a range of >> commits it would reduce the likelihood of conflicts to revert then in >> reverse order so we should either recommend passing '--reverse' or make >> that the default when '--revert' is given. >> >> As you can see in the log output above the new function to format the >> revert subject lines is buggy. If you had used test_commit_message() to >> check the commit message, rather than just grepping for ^Revert the >> tests would have picked that up. >> >> Thanks >> >> Phillip > I was going to say the same thing, but from a different angle. Hi Elijah, thanks for the architectural clarity! > > The sequencer in git is used for three different types of operations: > rebasing, cherry-picking, and reverting a range (with a sequence of > reverts rather than one big revert). In replay, these correspond to > --onto, --advance, and the new thing you are trying to add. As such, > it should be its own new mode. This makes complete sense. I was treating `--revert` as a modifier when it should be a third mode alongside `--onto` and `--advance`. I will restructure so that the user specifies exactly one of: --onto <newbase> --advance <branch> --revert <target> Where `--revert <target>` applies the reverts on top of <target> and updates that ref. > > (I do tend to see ranges reverted by a single big revert, the way > Johannes suggested, rather than as a range of individual reverts, The commit-by-commit approach is useful when you need: - Individual revert commits with proper "This reverts commit X" messages - The ability to later cherry-pick specific reverts - Clear history showing which commit caused which revert But I will add documentation noting the `merge-tree` alternative for cases where a single combined revert is preferred. Thanks, Siddharth > so > to me the utility of the new mode looks low, but perhaps others find > more utility in it. Or maybe the intent is to only use it with a > revision range that is only one commit long?) > > Phillip also went into more detail about why "--onto $COMMIT --revert" > specifically doesn't make sense. I'd also say > "--advance $BRANCH > --revert" doesn't read well because to users, "revert" means going > back while "advance" means going forward, Exactly - combining these is semantically confusing even if it could be made to work technically. > so it's a rather confusing > command line to make them wrap their head around. > > And yes, Siddharth, you were right that the new mode should be > incompatible with --contained, but that's because --contained is a > special modifier of --onto. --onto, --advance, and --revert are three > different modes that are incompatible with each other. Once you've > checked for that incompatibility between the three modes, then you can > either check that whenever --contained is specified, either --onto is > as well, or neither --advance nor --revert are. Right. The check becomes: 1. Exactly one of --onto, --advance, --revert must be specified 2. --contained requires --onto This is much cleaner than my current approach of pairwise incompatibility checks. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 11:10 ` Phillip Wood 2025-11-26 17:35 ` Elijah Newren @ 2025-11-26 19:39 ` Siddharth Asthana 2025-11-27 16:21 ` Phillip Wood 1 sibling, 1 reply; 92+ messages in thread From: Siddharth Asthana @ 2025-11-26 19:39 UTC (permalink / raw) To: phillip.wood, git Cc: christian.couder, ps, newren, gitster, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On 26/11/25 16:40, Phillip Wood wrote: > Hi Siddharth > > On 25/11/2025 17:00, Siddharth Asthana wrote: >> >> diff --git a/Documentation/git-replay.adoc >> b/Documentation/git-replay.adoc >> index dcb26e8a8e..ad7dc08622 100644 >> --- a/Documentation/git-replay.adoc >> +++ b/Documentation/git-replay.adoc >> @@ -54,6 +54,18 @@ which uses the target only as a starting point >> without updating it. >> [...] >> +To revert a range of commits: >> + >> +------------ >> +$ git replay --revert --onto main feature~3..feature >> +------------ >> + >> +This creates new commits on top of 'main' that reverse the changes >> introduced >> +by the last three commits on 'feature'. The 'feature' branch is >> updated to >> +point at the last of these revert commits. The 'main' branch is not >> updated >> +in this case. Hi Phillip, Thanks for the detailed analysis! > > I'm struggling to understand when I'd want to do this. Why would I > want to update 'feature' to point to the reverted version of its last > tree commits rebased onto 'main'? You are absolutely right - the `--onto` example I provided doesn't make practical sense. Elijah's reply clarified the architecture: `--revert` should be its own mode, not a modifier that combines with `--onto` or `--advance`. The realistic use case is reverting commits from a branch where those commits already exist. For example: git replay --revert main~3..main This would revert the last 3 commits on main, creating revert commits on top of main. > In order to understand I ran the first tests case which does > > git replay --onto topic1 --revert topic1..topic2 > > after fixing it by adding --ref-action=print the resulting commit log > looks like > > commit d337fab78e90008835f74e890039b464a0308cbe > Author: author@name <bogus@email@address> > Date: Thu Apr 7 15:30:13 2005 -0700 > > Revert "E > " > > This reverts commit bceb3acd81ddd36ba0da391fffa48949a1337276. > > commit 47f0cc1c1f1911c0047a4d79d79f7c19c6c7151a > Author: author@name <bogus@email@address> > Date: Thu Apr 7 15:30:13 2005 -0700 > > Revert "D > " > > This reverts commit d953cf2dcc1da8b51934e43fd83dac72d0e267c7. > > > The commits are empty because the original they are reverting each > create a new file which is then present in the base revision but not > in either of the merge heads when we revert. This confirms the tests aren't realistic. In v2, I will create tests where the commits being reverted are ancestors of the replay target, so the reverts produce meaningful diffs. > This suggests to me that it is not a very realistic test and I'm > still scratching my head to see where "git replay --onto <commit> > --revert" is useful. > > If '--revert' does not make sense with '--onto' then perhaps it should > be a new mode that takes a ref and acts like '--advance' but reverts > the commits rather than cherry-picking them. When reverting a range of > commits it would reduce the likelihood of conflicts to revert then in > reverse order so we should either recommend passing '--reverse' or > make that the default when '--revert' is given. > > As you can see in the log output above the new function to format the > revert subject lines is buggy. Good catch! The bug is in `generate_revert_message()` - I am passing `orig_message` (which points to the full message including body) to `sequencer_format_revert_header()`, but that function expects just the subject line. Looking at how sequencer.c does it, they use `msg.subject` which is properly extracted. I need to use `commit_subject_length()` to get just the subject: int subject_len = find_commit_subject(message, &orig_message); char *subject = xmemdupz(orig_message, subject_len); generate_revert_message(&msg, subject, &based_on->object.oid); free(subject); > If you had used test_commit_message() to check the commit message, > rather than just grepping for ^Revert the tests would have picked that > up. You are right. I will use test_commit_message() for proper validation in v2. Thanks, Siddharth > > Thanks > > Phillip > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-26 19:39 ` Siddharth Asthana @ 2025-11-27 16:21 ` Phillip Wood 2025-11-27 19:24 ` Siddharth Asthana 0 siblings, 1 reply; 92+ messages in thread From: Phillip Wood @ 2025-11-27 16:21 UTC (permalink / raw) To: Siddharth Asthana, phillip.wood, git Cc: christian.couder, ps, newren, gitster, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin Hi Siddharth On 26/11/2025 19:39, Siddharth Asthana wrote: > > The realistic use case is reverting commits from a branch where those > commits already exist. For example: > > git replay --revert main~3..main > > This would revert the last 3 commits on main, creating revert commits on > top of main. We want to be able to revert an arbitary range of commits. That means we need to give --revert a branch name to update in addition to the range of commits to revert. The following example would update "main", reverting all the commits from the branch "feature" git replay --revert main main..feature Thanks Phillip ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 1/1] replay: add --revert option to reverse commit changes 2025-11-27 16:21 ` Phillip Wood @ 2025-11-27 19:24 ` Siddharth Asthana 0 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-11-27 19:24 UTC (permalink / raw) To: phillip.wood, git Cc: christian.couder, ps, newren, gitster, karthik.188, code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin On 27/11/25 21:51, Phillip Wood wrote: > Hi Siddharth > > On 26/11/2025 19:39, Siddharth Asthana wrote: >> >> The realistic use case is reverting commits from a branch where those >> commits already exist. For example: >> >> git replay --revert main~3..main >> >> This would revert the last 3 commits on main, creating revert commits >> on top of main. > > We want to be able to revert an arbitary range of commits. That means > we need to give --revert a branch name to update in addition to the > range of commits to revert. The following example would update "main", > reverting all the commits from the branch "feature" > > git replay --revert main main..feature Makes sense. I will restructure --revert to take a branch argument, making it a proper mode alongside --onto and --advance: git replay --revert <branch> <revision-range> This keeps the syntax consistent with the other modes. Thanks, Siddharth > > Thanks > > Phillip > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-25 17:00 [PATCH 0/1] replay: add --revert option to reverse commit changes Siddharth Asthana 2025-11-25 17:00 ` [PATCH 1/1] " Siddharth Asthana @ 2025-11-25 17:25 ` Johannes Schindelin 2025-11-25 18:02 ` Junio C Hamano 2025-11-26 19:18 ` Siddharth Asthana 2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana 2 siblings, 2 replies; 92+ messages in thread From: Johannes Schindelin @ 2025-11-25 17:25 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 Hi Siddharth, On Tue, 25 Nov 2025, Siddharth Asthana wrote: > The `git replay` command currently supports cherry-picking commits for > server-side history rewriting, but lacks the ability to revert them. > This patch adds a `--revert` option to enable reversing commits directly > on bare repositories. > > At GitLab, we use replay in Gitaly for efficient server-side operations. > Adding revert functionality enables us to reverse problematic commits > without client-side roundtrips, reducing network overhead. > > The implementation leverages the insight that cherry-pick and revert are > essentially the same merge operation with swapped arguments. By swapping > the base and pickme trees when calling `merge_incore_nonrecursive()`, we > effectively reverse the diff direction. The existing conflict handling, > ref updates, and atomic transaction support work unchanged. Are you reverting rebased Merge Requests commit by commit? If not, I would suggest the shortcut to use `merge-tree` directly for the entire Merge Request. That is, if `$BASE` corresponds to the base branch onto which the Merge Request was rebased, and `$TIP` corresponds to the Merge Request's rebased tip commit, then the following will revert that Merge Request: git merge-tree --merge-base $TIP HEAD $BASE The upside is that this can potentially avoid a lot of unnecessary merge conflicts. The downside is that it does not revert the rebased Merge Request commit by commit. The patch itself looks fine to me, if a bit too extensive on the side of adding tests: Remember, a nimble test suite that catches a bug once is better than a long-running test suite that would catch a bug several times _iff_ it didn't tax the developer's patience so much that it is interrupted and aborted. You probably agree that Git's CI runtimes are already counter-productively long. Ciao, Johannes > The revert message generation logic is extracted into a new shared > `sequencer_format_revert_header()` function in `sequencer.c`, allowing > code reuse between `sequencer.c` and `builtin/replay.c`. The commit > messages follow `git revert` conventions, including "Revert"/"Reapply" > prefixes and the original commit SHA. > > This patch includes comprehensive tests covering various scenarios: > bare repositories, --advance mode, conflicts, reapply behavior, and > multiple commits. > > Siddharth Asthana (1): > replay: add --revert option to reverse commit changes > > Documentation/git-replay.adoc | 35 +++++++- > builtin/replay.c | 86 ++++++++++++++---- > sequencer.c | 23 +++++ > sequencer.h | 8 ++ > t/t3650-replay-basics.sh | 160 ++++++++++++++++++++++++++++++++++ > 5 files changed, 295 insertions(+), 17 deletions(-) > > -- > 2.51.0 > > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-25 17:25 ` [PATCH 0/1] " Johannes Schindelin @ 2025-11-25 18:02 ` Junio C Hamano 2025-11-26 19:18 ` Siddharth Asthana 1 sibling, 0 replies; 92+ messages in thread From: Junio C Hamano @ 2025-11-25 18:02 UTC (permalink / raw) To: Johannes Schindelin Cc: Siddharth Asthana, git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 Johannes Schindelin <Johannes.Schindelin@gmx.de> writes: > The patch itself looks fine to me, if a bit too extensive on the side of > adding tests: Remember, a nimble test suite that catches a bug once is > better than a long-running test suite that would catch a bug several times > _iff_ it didn't tax the developer's patience so much that it is > interrupted and aborted. You probably agree that Git's CI runtimes are > already counter-productively long. I am not sure about some of the negations in the above, but it is very good to point out that tests want to cover widely but without overlap. Two tests that try to see the tool works well under identical scenarios can be better done as a single test. We do not need to catch the same bug in multiple tests, as people tend to see test breakages, update the code to fix the first one, and continue, wanting to fix more and different kind of breakages. Thanks. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-25 17:25 ` [PATCH 0/1] " Johannes Schindelin 2025-11-25 18:02 ` Junio C Hamano @ 2025-11-26 19:18 ` Siddharth Asthana 2025-11-26 21:04 ` Junio C Hamano 1 sibling, 1 reply; 92+ messages in thread From: Siddharth Asthana @ 2025-11-26 19:18 UTC (permalink / raw) To: Johannes Schindelin Cc: git, christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 On 25/11/25 22:55, Johannes Schindelin wrote: > Hi Siddharth, > > On Tue, 25 Nov 2025, Siddharth Asthana wrote: > >> The `git replay` command currently supports cherry-picking commits for >> server-side history rewriting, but lacks the ability to revert them. >> This patch adds a `--revert` option to enable reversing commits directly >> on bare repositories. >> >> At GitLab, we use replay in Gitaly for efficient server-side operations. >> Adding revert functionality enables us to reverse problematic commits >> without client-side roundtrips, reducing network overhead. >> >> The implementation leverages the insight that cherry-pick and revert are >> essentially the same merge operation with swapped arguments. By swapping >> the base and pickme trees when calling `merge_incore_nonrecursive()`, we >> effectively reverse the diff direction. The existing conflict handling, >> ref updates, and atomic transaction support work unchanged. Hi Johannes, Thanks for the review! > Are you reverting rebased Merge Requests commit by commit? If not, I would > suggest the shortcut to use `merge-tree` directly for the entire Merge > Request. That's a great point. At GitLab, we have use cases for both approaches: 1. For quick undoing an entire MR, the `merge-tree` approach you suggest is indeed more efficient and avoids unnecessary intermediate conflicts. 2. For commit-by-commit reverts, we need individual revert commits with proper attribution (which commit is being reverted) for auditability and history clarity. This is particularly useful when only specific commits from a merged branch need to be reverted. I will add a note in the documentation mentioning the `merge-tree` alternative for whole-MR reverts. > That is, if `$BASE` corresponds to the base branch onto which the > Merge Request was rebased, and `$TIP` corresponds to the Merge Request's > rebased tip commit, then the following will revert that Merge Request: > > git merge-tree --merge-base $TIP HEAD $BASE > > The upside is that this can potentially avoid a lot of unnecessary merge > conflicts. The downside is that it does not revert the rebased Merge > Request commit by commit. > > The patch itself looks fine to me, if a bit too extensive on the side of > adding tests Agreed. Looking at the tests again, I can consolidate several of them: - The bare repo test can be merged with the basic revert test - The multiple commits test overlaps with the basic functionality I will trim down to essential coverage in v2: basic revert, conflict handling, and Reapply behavior. Thanks, Siddharth > : Remember, a nimble test suite that catches a bug once is > better than a long-running test suite that would catch a bug several times > _iff_ it didn't tax the developer's patience so much that it is > interrupted and aborted. You probably agree that Git's CI runtimes are > already counter-productively long. > > Ciao, > Johannes > >> The revert message generation logic is extracted into a new shared >> `sequencer_format_revert_header()` function in `sequencer.c`, allowing >> code reuse between `sequencer.c` and `builtin/replay.c`. The commit >> messages follow `git revert` conventions, including "Revert"/"Reapply" >> prefixes and the original commit SHA. >> >> This patch includes comprehensive tests covering various scenarios: >> bare repositories, --advance mode, conflicts, reapply behavior, and >> multiple commits. >> >> Siddharth Asthana (1): >> replay: add --revert option to reverse commit changes >> >> Documentation/git-replay.adoc | 35 +++++++- >> builtin/replay.c | 86 ++++++++++++++---- >> sequencer.c | 23 +++++ >> sequencer.h | 8 ++ >> t/t3650-replay-basics.sh | 160 ++++++++++++++++++++++++++++++++++ >> 5 files changed, 295 insertions(+), 17 deletions(-) >> >> -- >> 2.51.0 >> >> ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-26 19:18 ` Siddharth Asthana @ 2025-11-26 21:04 ` Junio C Hamano 2025-11-27 19:21 ` Siddharth Asthana 0 siblings, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2025-11-26 21:04 UTC (permalink / raw) To: Siddharth Asthana Cc: Johannes Schindelin, git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 Siddharth Asthana <siddharthasthana31@gmail.com> writes: > 1. For quick undoing an entire MR, the `merge-tree` approach you > suggest is indeed more efficient and avoids unnecessary intermediate > conflicts. > > 2. For commit-by-commit reverts, we need individual revert commits with > proper attribution (which commit is being reverted) for auditability and > history clarity. This is particularly useful when only specific commits > from a merged branch need to be reverted. These are both good workflows with appropriate uses. To make the tool useful for #2, it needs to be able to allow "I have merged a topic with 7 commits, but the first commit and the fourth commit are faulty and I need to revert them", i.e., not just a range (like "rebase" and "cherry-pick" workflows take), but a set of commits that are potentially disconnected. The current command line arguments "git replay" supports, or "git revert A..B" for that matter, are not exactly a good fit for such a use case, although the user can of course run two single-commit revert operations in a row. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-26 21:04 ` Junio C Hamano @ 2025-11-27 19:21 ` Siddharth Asthana 2025-11-27 20:17 ` Junio C Hamano 2025-11-28 8:07 ` Elijah Newren 0 siblings, 2 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-11-27 19:21 UTC (permalink / raw) To: Junio C Hamano Cc: Johannes Schindelin, git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 On 27/11/25 02:34, Junio C Hamano wrote: > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > >> 1. For quick undoing an entire MR, the `merge-tree` approach you >> suggest is indeed more efficient and avoids unnecessary intermediate >> conflicts. >> >> 2. For commit-by-commit reverts, we need individual revert commits with >> proper attribution (which commit is being reverted) for auditability and >> history clarity. This is particularly useful when only specific commits >> from a merged branch need to be reverted. > These are both good workflows with appropriate uses. To make the > tool useful for #2, it needs to be able to allow "I have merged a > topic with 7 commits, but the first commit and the fourth commit are > faulty and I need to revert them", i.e., not just a range Since replay uses the same rev-list machinery as `git log`, users can already specify disconnected commits: git replay --revert <target> <commit1> <commit4> I will add a test to verify this works and document the capability. Thanks, Siddharth > (like > "rebase" and "cherry-pick" workflows take), but a set of commits > that are potentially disconnected. The current command line > arguments "git replay" supports, or "git revert A..B" for that > matter, are not exactly a good fit for such a use case, although the > user can of course run two single-commit revert operations in a row. > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-27 19:21 ` Siddharth Asthana @ 2025-11-27 20:17 ` Junio C Hamano 2025-11-28 8:07 ` Elijah Newren 1 sibling, 0 replies; 92+ messages in thread From: Junio C Hamano @ 2025-11-27 20:17 UTC (permalink / raw) To: Siddharth Asthana Cc: Johannes Schindelin, git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 Siddharth Asthana <siddharthasthana31@gmail.com> writes: >> These are both good workflows with appropriate uses. To make the >> tool useful for #2, it needs to be able to allow "I have merged a >> topic with 7 commits, but the first commit and the fourth commit are >> faulty and I need to revert them", i.e., not just a range > > > Since replay uses the same rev-list machinery as `git log`, users can > already specify disconnected commits: > > git replay --revert <target> <commit1> <commit4> Ah, OK. Yes, you can use setup_revisions() command line parsing, intercept the result of parsing before prepare_revilimit_list() reduces that to a contiguous set of commits that are reachable from some of the positive refs and are not reachable from any of the negative ones, so it certainly is doable. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-27 19:21 ` Siddharth Asthana 2025-11-27 20:17 ` Junio C Hamano @ 2025-11-28 8:07 ` Elijah Newren 2025-11-28 8:24 ` Siddharth Asthana 1 sibling, 1 reply; 92+ messages in thread From: Elijah Newren @ 2025-11-28 8:07 UTC (permalink / raw) To: Siddharth Asthana Cc: Junio C Hamano, Johannes Schindelin, git, christian.couder, ps, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 On Thu, Nov 27, 2025 at 11:21 AM Siddharth Asthana <siddharthasthana31@gmail.com> wrote: > > On 27/11/25 02:34, Junio C Hamano wrote: > > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > > > >> 1. For quick undoing an entire MR, the `merge-tree` approach you > >> suggest is indeed more efficient and avoids unnecessary intermediate > >> conflicts. > >> > >> 2. For commit-by-commit reverts, we need individual revert commits with > >> proper attribution (which commit is being reverted) for auditability and > >> history clarity. This is particularly useful when only specific commits > >> from a merged branch need to be reverted. > > These are both good workflows with appropriate uses. To make the > > tool useful for #2, it needs to be able to allow "I have merged a > > topic with 7 commits, but the first commit and the fourth commit are > > faulty and I need to revert them", i.e., not just a range > > > Since replay uses the same rev-list machinery as `git log`, users can > already specify disconnected commits: > > git replay --revert <target> <commit1> <commit4> No, this command does not specify disconnected commits. A <range> of "<commit1> <commit4>" specifies all commits in the history of either <commit1> or <commit4>. Thus, this example command line would be asking to revert all commits in the history of either <commit1> or <commit4> (all the way back to the initial commit), rather than just reverting those two commits. This is just like how git log <commit1> <commit4> shows all commits in the history of either <commit1> or <commit4> instead of just showing those two commits. There isn't really a mechanism in replay right now to handle a disconnected set of commits for either --advance or --revert. If there were, it'd probably look like git replay --advance <branch> --no-walk <commit1> <commit4> but the code isn't set up to check whether you specified --no-walk, and thinks "Um, you specified multiple branches here and it's not clear the order in which to cherry-pick them" so it throws an error: $ git replay --advance main --no-walk Commit1 Commit7 fatal: cannot advance target with multiple sources because ordering would be ill-defined If you comment out the relevant check which dies with that error, then you end up in some codepath that segfaults instead (not-properly initialized commit/tree objects or something?). I'm sure that could be fixed, but "users can already specify disconnected commits" is just not accurate. > I will add a test to verify this works and document the capability. Supporting --no-walk so that folks can do disconnected commits for both --advance and --revert may be nice, but given that it's missing for --advance already, it might be considered a separate change from your current submission. I'll leave that up to you. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-28 8:07 ` Elijah Newren @ 2025-11-28 8:24 ` Siddharth Asthana 2025-11-28 16:35 ` Junio C Hamano 0 siblings, 1 reply; 92+ messages in thread From: Siddharth Asthana @ 2025-11-28 8:24 UTC (permalink / raw) To: Elijah Newren Cc: Junio C Hamano, Johannes Schindelin, git, christian.couder, ps, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 On 28/11/25 13:37, Elijah Newren wrote: > On Thu, Nov 27, 2025 at 11:21 AM Siddharth Asthana > <siddharthasthana31@gmail.com> wrote: >> On 27/11/25 02:34, Junio C Hamano wrote: >>> Siddharth Asthana <siddharthasthana31@gmail.com> writes: >>> >>>> 1. For quick undoing an entire MR, the `merge-tree` approach you >>>> suggest is indeed more efficient and avoids unnecessary intermediate >>>> conflicts. >>>> >>>> 2. For commit-by-commit reverts, we need individual revert commits with >>>> proper attribution (which commit is being reverted) for auditability and >>>> history clarity. This is particularly useful when only specific commits >>>> from a merged branch need to be reverted. >>> These are both good workflows with appropriate uses. To make the >>> tool useful for #2, it needs to be able to allow "I have merged a >>> topic with 7 commits, but the first commit and the fourth commit are >>> faulty and I need to revert them", i.e., not just a range >> >> Since replay uses the same rev-list machinery as `git log`, users can >> already specify disconnected commits: >> >> git replay --revert <target> <commit1> <commit4> Hi Elijah, > No, this command does not specify disconnected commits. A <range> of > "<commit1> <commit4>" specifies all commits in the history of either > <commit1> or <commit4>. You are right, I misspoke. I was conflating the command-line syntax with what the revision machinery actually produces after prepare_revision_walk(). > Thus, this example command line would be > asking to revert all commits in the history of either <commit1> or > <commit4> (all the way back to the initial commit), rather than just > reverting those two commits. This is just like how > git log <commit1> <commit4> > shows all commits in the history of either <commit1> or <commit4> > instead of just showing those two commits. > > There isn't really a mechanism in replay right now to handle a > disconnected set of commits for either --advance or --revert. Right, the check at line 190 in replay.c: if (rinfo.positive_refexprs > 1) die(_("cannot advance target with multiple sources...")); fires before we even get to the revision walk. > If there were, it'd probably look like > > git replay --advance <branch> --no-walk <commit1> <commit4> > > but the code isn't set up to check whether you specified --no-walk, > and thinks "Um, you specified multiple branches here and it's not > clear the order in which to cherry-pick them" so it throws an error: > > $ git replay --advance main --no-walk Commit1 Commit7 > fatal: cannot advance target with multiple sources because ordering > would be ill-defined > > If you comment out the relevant check which dies with that error, then > you end up in some codepath that segfaults instead (not-properly > initialized commit/tree objects or something?). I'm sure that could > be fixed, but "users can already specify disconnected commits" is just > not accurate. > >> I will add a test to verify this works and document the capability. > Supporting --no-walk so that folks can do disconnected commits for > both --advance and --revert may be nice, but given that it's missing > for --advance already, it might be considered a separate change from > your current submission. I'll leave that up to you. Agreed. I will keep the current submission focused on basic --revert functionality. Supporting --no-walk for disconnected commits (benefiting both --advance and --revert) would make a nice follow-up series. Thanks for the correction. Siddharth ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-28 8:24 ` Siddharth Asthana @ 2025-11-28 16:35 ` Junio C Hamano 2025-11-28 17:07 ` Elijah Newren 0 siblings, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2025-11-28 16:35 UTC (permalink / raw) To: Siddharth Asthana Cc: Elijah Newren, Johannes Schindelin, git, christian.couder, ps, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 Siddharth Asthana <siddharthasthana31@gmail.com> writes: > Agreed. I will keep the current submission focused on basic --revert > functionality. Supporting --no-walk for disconnected commits (benefiting > both --advance and --revert) would make a nice follow-up series. If we want to have a useful support for disconnected set of commits, "--no-walk" is not the way to go, I would say. Imagine "among the 7-patch topic merged, the second commit (i.e., topic~5) and the final 3 (i.e., topic~3..topic) need to go". You'd want to be able to say (without going into details of the syntax) revert topic~5 topic~3..topic The setup_revisions() parser is still the right thing to use to parse the command line arguments and pick out "topic~5" and "topic~3..topic", but instead of letting prepare_revision_walk() turn them into a single contiguous set of revisions, you'd need to check revs->cmdline->rev[] and (1) treat singleton as its own disconnected island that require no walking, (2) treat A..B as a range and independently walk them, and (3) dedup the result from cmdline->rev[] elements into a set of commits that are potentially disconnected. I agree 100% that this topic should not attempt to deal with a disconnected set of commits. That can and should be done as a separate series. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-28 16:35 ` Junio C Hamano @ 2025-11-28 17:07 ` Elijah Newren 2025-11-28 20:50 ` Junio C Hamano 0 siblings, 1 reply; 92+ messages in thread From: Elijah Newren @ 2025-11-28 17:07 UTC (permalink / raw) To: Junio C Hamano Cc: Siddharth Asthana, Johannes Schindelin, git, christian.couder, ps, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 On Fri, Nov 28, 2025 at 8:35 AM Junio C Hamano <gitster@pobox.com> wrote: > > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > > > Agreed. I will keep the current submission focused on basic --revert > > functionality. Supporting --no-walk for disconnected commits (benefiting > > both --advance and --revert) would make a nice follow-up series. > > If we want to have a useful support for disconnected set of commits, > "--no-walk" is not the way to go, I would say. > > Imagine "among the 7-patch topic merged, the second commit (i.e., > topic~5) and the final 3 (i.e., topic~3..topic) need to go". You'd > want to be able to say (without going into details of the syntax) > > revert topic~5 topic~3..topic > > The setup_revisions() parser is still the right thing to use to > parse the command line arguments and pick out "topic~5" and > "topic~3..topic", but instead of letting prepare_revision_walk() > turn them into a single contiguous set of revisions, you'd need to > check revs->cmdline->rev[] and > > (1) treat singleton as its own disconnected island that require no > walking, > > (2) treat A..B as a range and independently walk them, and > > (3) dedup the result from cmdline->rev[] elements into a set of > commits that are potentially disconnected. > > I agree 100% that this topic should not attempt to deal with a > disconnected set of commits. That can and should be done as a > separate series. How does one distinguish the "topic~5" in the range "topic~5 topic~3..topic" from * the topic~5 in "^topic~7 topic~5" * the "topic1" and "topic2" in "^$OLD_COMMIT --ancestry-path topic1 topic2" ? I kind of think we still need some kind of flag (possibly implied by default for --advance and --revert but not for --onto?), though I agree it'd need to be a new one rather than --no-walk for your example to work. And if one can do this, should this flag also be added to other commands, so that e.g. `git log <someflag> topic~5 topic~3..topic" would also show the commits in topic~3..topic plus topic~5? ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-28 17:07 ` Elijah Newren @ 2025-11-28 20:50 ` Junio C Hamano 2025-11-28 22:03 ` Elijah Newren 0 siblings, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2025-11-28 20:50 UTC (permalink / raw) To: Elijah Newren Cc: Siddharth Asthana, Johannes Schindelin, git, christian.couder, ps, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 Elijah Newren <newren@gmail.com> writes: > How does one distinguish the "topic~5" in the range "topic~5 > topic~3..topic" from > * the topic~5 in "^topic~7 topic~5" Two answers. (1) You don't have to. When you scan cmdline->rev[], you can notice the ^topic-7 form and reject, saying "we accept A..B but not ^A B." (2) Or you design and document the interpretation you implement when you see a negative commit while you scan over cmdline->rev[]. Perhaps you may make "^topic-7" to require a positive commit after it and convert "^topic-7 topic5" as if the user gave you a single "topic~7..topic~5". Or you may do something else. My assumption has been (1). > * the "topic1" and "topic2" in "^$OLD_COMMIT --ancestry-path topic1 topic2" I haven't thought it through, but doesn't ancetry-path imply you are really interested in the traditional connected set of commits? The path is a connected subset inside those commits after all, no? ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-28 20:50 ` Junio C Hamano @ 2025-11-28 22:03 ` Elijah Newren 2025-11-29 5:59 ` Junio C Hamano 0 siblings, 1 reply; 92+ messages in thread From: Elijah Newren @ 2025-11-28 22:03 UTC (permalink / raw) To: Junio C Hamano Cc: Siddharth Asthana, Johannes Schindelin, git, christian.couder, ps, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 On Fri, Nov 28, 2025 at 12:50 PM Junio C Hamano <gitster@pobox.com> wrote: > > Elijah Newren <newren@gmail.com> writes: > > > How does one distinguish the "topic~5" in the range "topic~5 > > topic~3..topic" from > > * the topic~5 in "^topic~7 topic~5" > > Two answers. > > (1) You don't have to. When you scan cmdline->rev[], you can notice > the ^topic-7 form and reject, saying "we accept A..B but not ^A B." Using revision ranges like ^A B C with --onto (rebasing several branches at once) was one of the major usecases for git replay. Because of that, even when I only have one branch, I often use ^A B over A..B. It's also called out pretty explicitly in the manual: ``` When calling `git replay`, one does not need to specify a range of commits to replay using the syntax `A..B`; any range expression will do: ------------ $ git replay --onto origin/main ^base branch1 branch2 branch3 ------------ This will simultaneously rebase `branch1`, `branch2`, and `branch3`, all commits they have since `base`, playing them on top of `origin/main`. These three branches may have commits on top of `base` that they have in common, but that does not need to be the case. ``` Granted, unlike --onto, using --advance (or --revert) with multiple branches at once doesn't make sense because cherry-picking/reverting multiple branches into one has an ill-defined ordering problem. Since only one branch is allowed, maybe we could make special rules for --advance and --revert, but it would feel a little weird to have a revision range not actually be a revision range for some modes of the command while also having them be an actual revision range for the other mode. > (2) Or you design and document the interpretation you implement when > you see a negative commit while you scan over cmdline->rev[]. > Perhaps you may make "^topic-7" to require a positive commit > after it and convert "^topic-7 topic5" as if the user gave you a > single "topic~7..topic~5". Or you may do something else. Part of the point of replay was to "use the same revision ranges that git log allows". I agree that having a way to specify "topic~3..topic plus the commit topic~5" is useful for this command, but it's equally likely to be useful for `git log`; people have asked for it before. However, there is currently no way to specify that set of revisions to `git log`. Perhaps we should add some flag to revision walking that allows that kind of alternate rule? If we do that, then both log and replay would benefit. We could even consider making that flag be implied by both --advance and --revert (but NOT by --onto). If we do that, we would also need a way to negate that flag, since someone will probably want a way to cherry-pick some disconnected branch down to the root commit, meaning they really would want "topic~5" to be treated as a traditional range and not as a single commit. > > * the "topic1" and "topic2" in "^$OLD_COMMIT --ancestry-path topic1 topic2" > > I haven't thought it through, but doesn't ancetry-path imply you are > really interested in the traditional connected set of commits? The > path is a connected subset inside those commits after all, no? Yes, I believe --ancestry-path makes it clear that a range is wanted. It's not clear to me that we can always correctly guess old-style-range vs. new-style-range from the existing command line arguments alone, particularly given the example above of single commit vs. commits back to the root. I think an explicit flag is needed, if we want to go this route. (Also, this feels like a fair amount of work, when people can easily just invoke replay multiple times...) ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH 0/1] replay: add --revert option to reverse commit changes 2025-11-28 22:03 ` Elijah Newren @ 2025-11-29 5:59 ` Junio C Hamano 0 siblings, 0 replies; 92+ messages in thread From: Junio C Hamano @ 2025-11-29 5:59 UTC (permalink / raw) To: Elijah Newren Cc: Siddharth Asthana, Johannes Schindelin, git, christian.couder, ps, phillip.wood123, phillip.wood, karthik.188, code, rybak.a.v, jltobler, toon, johncai86 Elijah Newren <newren@gmail.com> writes: > It's also called out pretty explicitly in the manual: > ``` > When calling `git replay`, one does not need to specify a range of > commits to replay using the syntax `A..B`; any range expression will > do: When we use the phrase "any range expression" to refer to what prepare_revision_walk() produces, it by definition means we only deal with a connected set. It often is very handy to allow saying "master..topic1 topic2" or "^master topic1 topic2" or "topic1 topic2 --not master" to mean "commits on these branches", of course, and there are many such useful use cases that do not require disjoint set (and that is why we survived without any disjoint set support on "git log" side, except for individually specifying commits and say "--no-walk", which is still technically a "set" but not very useful one when the number of commits you individually have to specify becomes more than a handful). It only means that the documentation needs to be updated if we ever want to introduce an extended form of the command line syntax that allows users to specify an unconnected set of commits. ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v2 0/2] replay: add --revert mode to reverse commit changes 2025-11-25 17:00 [PATCH 0/1] replay: add --revert option to reverse commit changes Siddharth Asthana 2025-11-25 17:00 ` [PATCH 1/1] " Siddharth Asthana 2025-11-25 17:25 ` [PATCH 0/1] " Johannes Schindelin @ 2025-12-02 20:16 ` Siddharth Asthana 2025-12-02 20:16 ` [PATCH v2 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana ` (2 more replies) 2 siblings, 3 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-12-02 20:16 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon, Siddharth Asthana The `git replay` command performs server-side history rewriting without requiring a working tree. While it currently supports cherry-picking commits (--advance) and rebasing (--onto), it lacks the ability to revert them. At GitLab, we use replay in Gitaly for efficient server-side operations on bare repositories. Adding revert functionality enables us to reverse problematic commits directly on the server, eliminating client-side roundtrips and reducing network overhead. The implementation follows the same approach as sequencer.c where cherry-pick and revert are the same merge operation but with swapped arguments. For cherry-pick we merge(ancestor=parent, ours=current, theirs=commit), while for revert we merge(ancestor=commit, ours=current, theirs=parent). By swapping the base and pickme trees when calling merge_incore_nonrecursive(), we effectively reverse the diff direction. The series is structured as follows: Patch 1 extracts the revert message formatting logic into a shared sequencer_format_revert_header() function, eliminating code duplication between sequencer.c and the upcoming replay code. This follows Junio's suggestion to split the changes. Patch 2 adds the --revert <branch> mode to git replay. Following the architectural pattern suggested by Elijah and Phillip, --revert is a standalone mode (like --onto and --advance) that takes a branch argument and updates that branch with the revert commits. The series is based on top of f0ef5b6d9b (The fifth batch, 2025-11-29). CI: https://gitlab.com/gitlab-org/git/-/pipelines/2191734674 The static-analysis and Windows CI failures are pre-existing infrastructure issues unrelated to this series. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- Changes in v2: - Split into two patches as Junio suggested - Changed --revert from a modifier flag to a standalone mode (--revert <branch>) that is mutually exclusive with --onto/--advance - Used enum replay_action instead of int is_revert - Fixed subject extraction bug in generate_revert_message() - Put common (cherry-pick) case first in if/else block - Fixed repo_unuse_commit_buffer() to use repo parameter consistently - Improved tests with test_commit_message() and atomic ref update handling - Link to v1: https://lore.kernel.org/git/20251125170055.64991-1-siddharthasthana31@gmail.com/ Siddharth Asthana (2): sequencer: extract revert message formatting into shared function replay: add --revert mode to reverse commit changes Documentation/git-replay.adoc | 36 ++++++++- builtin/replay.c | 145 ++++++++++++++++++++++++++++------ sequencer.c | 39 +++++---- sequencer.h | 8 ++ t/t3650-replay-basics.sh | 111 ++++++++++++++++++++++++++ 5 files changed, 300 insertions(+), 39 deletions(-) Range-diff against v1: -: ---------- > 1: bfd75484b4 sequencer: extract revert message formatting into shared function 1: dd47a89a5b ! 2: a2f99bc8c2 replay: add --revert option to reverse commit changes @@ Metadata Author: Siddharth Asthana <siddharthasthana31@gmail.com> ## Commit message ## - replay: add --revert option to reverse commit changes + replay: add --revert mode to reverse commit changes The `git replay` command performs server-side history rewriting without requiring a working tree. While it currently supports cherry-picking - commits, it lacks the ability to revert them. + commits (--advance) and rebasing (--onto), it lacks the ability to + revert them. At GitLab, we use replay in Gitaly for efficient server-side operations on bare repositories. Adding revert functionality enables us to reverse problematic commits directly on the server, eliminating client-side roundtrips and reducing network overhead. - Add a `--revert` option that reverses the changes introduced by the - specified commits. The implementation follows the same approach as - `sequencer.c` (around lines 2358-2390), where cherry-pick and revert - are essentially the same merge operation but with swapped arguments: + Add a `--revert <branch>` mode that reverses the changes introduced by + the specified commits. Following the architecture of --onto and --advance, + --revert is a standalone mode that takes a branch argument and updates + that branch with the revert commits. + + The implementation follows the same approach as sequencer.c (lines + 2360-2399), where cherry-pick and revert are the same merge operation + but with swapped arguments: - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) - Revert: merge(ancestor=commit, ours=current, theirs=parent) We swap the base and pickme trees when calling - `merge_incore_nonrecursive()`, effectively reversing the diff direction. + merge_incore_nonrecursive(), effectively reversing the diff direction. The existing conflict handling, ref updates, and atomic transaction support work unchanged. - The revert message generation logic (handling "Revert" and "Reapply" - cases) is extracted into a new `sequencer_format_revert_header()` - function in `sequencer.c`, which can be shared between `sequencer.c` - and `builtin/replay.c`. The existing code in `do_pick_commit()` is - updated to use this shared function, eliminating code duplication. - The `builtin/replay.c` code calls this shared function and then appends - the commit OID using `oid_to_hex()` directly, since git replay is - designed for simpler server-side operations without the interactive - features and `replay_opts` framework used by `sequencer.c`. - - The commit messages follow `git revert` conventions: prefixed with + The commit messages follow git revert conventions: prefixed with "Revert" and including the original commit SHA. When reverting a commit that itself starts with "Revert", the message uses "Reapply" instead. Unlike cherry-pick which preserves the original author, revert commits - use the current user as the author, matching the behavior of `git - revert`. - - Mark the option as incompatible with `--contained` because reverting - changes across multiple branches simultaneously could produce unexpected - results when branches have interdependencies or shared history. + use the current user as the author, matching the behavior of git revert. Helped-by: Christian Couder <christian.couder@gmail.com> Helped-by: Patrick Steinhardt <ps@pks.im> + Helped-by: Elijah Newren <newren@gmail.com> + Helped-by: Phillip Wood <phillip.wood123@gmail.com> + Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> + Helped-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> ## Documentation/git-replay.adoc ## @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>... -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] [--revert] <revision-range>... ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... DESCRIPTION ----------- -@@ Documentation/git-replay.adoc: which uses the target only as a starting point without updating it. - + - The default mode can be configured via the `replay.refAction` configuration variable. +@@ Documentation/git-replay.adoc: The history is replayed on top of the <branch> and <branch> is updated to + point at the tip of the resulting history. This is different from `--onto`, + which uses the target only as a starting point without updating it. -+--revert:: -+ Revert the changes introduced by the commits in the revision range -+ instead of applying them. This reverses the diff direction and creates -+ new commits that undo the changes, similar to `git revert`. ++--revert <branch>:: ++ Starting point at which to create the new revert commits; must be a ++ branch name. ++ -+The commit messages are prefixed with "Revert" and include the original -+commit SHA. If reverting a commit whose message starts with "Revert", the new -+message will start with "Reapply" instead. The author of the new commits -+will be the current user, not the original commit author. ++When `--revert` is specified, the commits in the revision range are reverted ++(their changes are undone) and the revert commits are applied on top of <branch>. ++The <branch> is then updated to point at the new commits. This is similar to ++running `git revert` for each commit in the range, but works without a working tree. ++ -+This option is incompatible with `--contained` because reverting changes -+across multiple branches simultaneously could produce unexpected results -+when branches have interdependencies or shared history. ++The commit messages follow `git revert` conventions: prefixed with "Revert" and ++including the original commit SHA. When reverting a commit whose message starts ++with "Revert", the new message uses "Reapply" instead. The author of the revert ++commits is the current user, not the original commit author. +++ ++This option is mutually exclusive with `--onto` and `--advance`. It is also ++incompatible with `--contained` (which is a modifier for `--onto` only). ++ + - <revision-range>:: - Range of commits to replay. More than one <revision-range> can - be passed, but in `--advance <branch>` mode, they should have + --ref-action[=<mode>]:: + Control how references are updated. The mode can be: + + @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them on top of `origin/main`. These three branches may have commits on top of `base` that they have in common, but that does not need to be the case. -+To revert a range of commits: ++To revert commits from a branch: + +------------ -+$ git replay --revert --onto main feature~3..feature ++$ git replay --revert main feature~2..feature +------------ + -+This creates new commits on top of 'main' that reverse the changes introduced -+by the last three commits on 'feature'. The 'feature' branch is updated to -+point at the last of these revert commits. The 'main' branch is not updated -+in this case. ++This reverts the last two commits from 'feature', creating revert commits on ++top of 'main', and updates 'main' to point at the result. This is useful when ++commits from 'feature' were previously merged or cherry-picked into 'main' and ++need to be undone. + -+To revert commits and advance a branch: ++NOTE: For reverting an entire merge request as a single commit (rather than ++commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` ++which can avoid unnecessary merge conflicts. + -+------------ -+$ git replay --revert --advance main feature~2..feature -+------------ -+ -+This reverts the last two commits from 'feature', applies those reverts -+on top of 'main', and updates 'main' to point at the result. The 'feature' -+branch is not updated in this case. + GIT --- @@ builtin/replay.c #include "strmap.h" #include <oidset.h> #include <tree.h> +@@ builtin/replay.c: enum ref_action_mode { + REF_ACTION_PRINT, + }; + ++enum replay_action { ++ REPLAY_PICK, ++ REPLAY_REVERT, ++}; ++ + static const char *short_commit_name(struct repository *repo, + struct commit *commit) + { @@ builtin/replay.c: static char *get_author(const char *message) return NULL; } -+/* -+ * Generates a revert commit message using the shared sequencer function. -+ * We use oid_to_hex() directly instead of refer_to_commit() since git replay -+ * is designed for simpler server-side operations without interactive features. -+ */ +static void generate_revert_message(struct strbuf *msg, -+ const char *orig_message, -+ const struct object_id *oid) ++ struct commit *commit, ++ struct repository *repo) +{ -+ sequencer_format_revert_header(msg, orig_message); -+ strbuf_addstr(msg, oid_to_hex(oid)); ++ const char *out_enc = get_commit_output_encoding(); ++ const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); ++ const char *subject_start; ++ int subject_len; ++ char *subject; ++ ++ subject_len = find_commit_subject(message, &subject_start); ++ subject = xmemdupz(subject_start, subject_len); ++ ++ sequencer_format_revert_header(msg, subject); ++ strbuf_addstr(msg, oid_to_hex(&commit->object.oid)); + strbuf_addstr(msg, ".\n"); ++ ++ free(subject); ++ repo_unuse_commit_buffer(repo, commit, message); +} + static struct commit *create_commit(struct repository *repo, @@ builtin/replay.c: static char *get_author(const char *message) struct commit *based_on, - struct commit *parent) + struct commit *parent, -+ int is_revert) ++ enum replay_action action) { struct object_id ret; struct object *obj = NULL; @@ builtin/replay.c: static struct commit *create_commit(struct repository *repo, + commit_list_insert(parent, &parents); extra = read_commit_extra_headers(based_on, exclude_gpgsig); - find_commit_subject(message, &orig_message); +- find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); -+ -+ if (is_revert) { -+ generate_revert_message(&msg, orig_message, &based_on->object.oid); -+ /* For revert, use current user as author */ -+ author = NULL; ++ if (action == REPLAY_REVERT) { ++ generate_revert_message(&msg, based_on, repo); ++ author = xstrdup(git_author_info(IDENT_STRICT)); + } else { -+ /* Cherry-pick mode: use original commit message and author */ ++ find_commit_subject(message, &orig_message); + strbuf_addstr(&msg, orig_message); + author = get_author(message); + } -+ reset_ident_date(); if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, &ret, author, NULL, sign_commit, extra)) { +@@ builtin/replay.c: static struct commit *create_commit(struct repository *repo, + obj = parse_object(repo, &ret); + + out: +- repo_unuse_commit_buffer(the_repository, based_on, message); ++ repo_unuse_commit_buffer(repo, based_on, message); + free_commit_extra_headers(extra); + free_commit_list(parents); + strbuf_release(&msg); +@@ builtin/replay.c: static void determine_replay_mode(struct repository *repo, + struct rev_cmdline_info *cmd_info, + const char *onto_name, + char **advance_name, ++ char **revert_name, + struct commit **onto, + struct strset **update_refs) + { +@@ builtin/replay.c: static void determine_replay_mode(struct repository *repo, + } + if (rinfo.positive_refexprs > 1) + die(_("cannot advance target with multiple sources because ordering would be ill-defined")); ++ } else if (*revert_name) { ++ struct object_id oid; ++ char *fullname = NULL; ++ ++ *onto = peel_committish(repo, *revert_name); ++ if (repo_dwim_ref(repo, *revert_name, strlen(*revert_name), ++ &oid, &fullname, 0) == 1) { ++ free(*revert_name); ++ *revert_name = fullname; ++ } else { ++ die(_("argument to --revert must be a reference")); ++ } ++ if (rinfo.positive_refexprs > 1) ++ die(_("cannot revert with multiple sources because ordering would be ill-defined")); + } else { + int positive_refs_complete = ( + rinfo.positive_refexprs == @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *repo, kh_oid_map_t *replayed_commits, struct commit *onto, struct merge_options *merge_opt, - struct merge_result *result) + struct merge_result *result, -+ int is_revert) ++ enum replay_action action) { struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree; @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r - merge_opt->branch1 = short_commit_name(repo, replayed_base); - merge_opt->branch2 = short_commit_name(repo, pickme); - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); -- ++ if (action == REPLAY_PICK) { ++ /* Cherry-pick: normal order */ ++ merge_opt->branch1 = short_commit_name(repo, replayed_base); ++ merge_opt->branch2 = short_commit_name(repo, pickme); ++ merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + - merge_incore_nonrecursive(merge_opt, - base_tree, - result->tree, - pickme_tree, - result); -+ if (is_revert) { -+ /* For revert: swap base and pickme to reverse the diff */ ++ merge_incore_nonrecursive(merge_opt, ++ base_tree, ++ result->tree, ++ pickme_tree, ++ result); + +- free((char*)merge_opt->ancestor); ++ free((char *)merge_opt->ancestor); ++ } else { ++ /* Revert: swap base and pickme to reverse the diff */ + const char *pickme_name = short_commit_name(repo, pickme); + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r + base_tree, + result); + -+ /* branch2 was allocated with xstrfmt, needs freeing */ + free((char *)merge_opt->branch2); -+ } else { -+ /* For cherry-pick: normal order */ -+ merge_opt->branch1 = short_commit_name(repo, replayed_base); -+ merge_opt->branch2 = short_commit_name(repo, pickme); -+ merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); -+ -+ merge_incore_nonrecursive(merge_opt, -+ base_tree, -+ result->tree, -+ pickme_tree, -+ result); -+ -+ /* ancestor was allocated with xstrfmt, needs freeing */ -+ free((char *)merge_opt->ancestor); + } - -- free((char*)merge_opt->ancestor); merge_opt->ancestor = NULL; + merge_opt->branch2 = NULL; if (!result->clean) return NULL; - return create_commit(repo, result->tree, pickme, replayed_base); -+ return create_commit(repo, result->tree, pickme, replayed_base, is_revert); ++ return create_commit(repo, result->tree, pickme, replayed_base, action); } static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source) @@ builtin/replay.c: int cmd_replay(int argc, + { + const char *advance_name_opt = NULL; + char *advance_name = NULL; ++ const char *revert_name_opt = NULL; ++ char *revert_name = NULL; ++ enum replay_action action = REPLAY_PICK; + struct commit *onto = NULL; + const char *onto_name = NULL; int contained = 0; - const char *ref_action = NULL; - enum ref_action_mode ref_mode; -+ int is_revert = 0; - - struct rev_info revs; - struct commit *last_commit = NULL; @@ builtin/replay.c: int cmd_replay(int argc, + const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto <newbase> | --advance <branch>) " -- "[--ref-action[=<mode>]] <revision-range>..."), -+ "[--ref-action[=<mode>]] [--revert] <revision-range>..."), +- "([--contained] --onto <newbase> | --advance <branch>) " ++ "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) " + "[--ref-action[=<mode>]] <revision-range>..."), NULL }; - struct option replay_options[] = { @@ builtin/replay.c: int cmd_replay(int argc, + N_("replay onto given commit")), + OPT_BOOL(0, "contained", &contained, + N_("advance all branches contained in revision-range")), ++ OPT_STRING(0, "revert", &revert_name_opt, ++ N_("branch"), ++ N_("revert commits onto given branch")), OPT_STRING(0, "ref-action", &ref_action, N_("mode"), N_("control ref update behavior (update|print)")), -+ OPT_BOOL(0, "revert", &is_revert, -+ N_("revert commits instead of cherry-picking them")), - OPT_END() - }; - @@ builtin/replay.c: int cmd_replay(int argc, + argc = parse_options(argc, argv, prefix, replay_options, replay_usage, + PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); + +- if (!onto_name && !advance_name_opt) { +- error(_("option --onto or --advance is mandatory")); ++ /* Exactly one mode must be specified */ ++ if (!onto_name && !advance_name_opt && !revert_name_opt) { ++ error(_("exactly one of --onto, --advance, or --revert is required")); + usage_with_options(replay_usage, replay_options); + } + die_for_incompatible_opt2(!!advance_name_opt, "--advance", - contained, "--contained"); +- contained, "--contained"); ++ !!onto_name, "--onto"); ++ die_for_incompatible_opt2(!!revert_name_opt, "--revert", ++ !!onto_name, "--onto"); ++ die_for_incompatible_opt2(!!revert_name_opt, "--revert", ++ !!advance_name_opt, "--advance"); ++ die_for_incompatible_opt2(contained, "--contained", ++ !onto_name, "requires --onto"); -+ /* --revert is incompatible with --contained */ -+ die_for_incompatible_opt2(is_revert, "--revert", -+ contained, "--contained"); -+ /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); + advance_name = xstrdup_or_null(advance_name_opt); ++ revert_name = xstrdup_or_null(revert_name_opt); ++ if (revert_name) ++ action = REPLAY_REVERT; + + repo_init_revisions(repo, &revs, prefix); + +@@ builtin/replay.c: int cmd_replay(int argc, + } + + determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name, ++ &revert_name, + &onto, &update_refs); + + /* Build reflog message */ +- if (advance_name_opt) ++ if (revert_name_opt) ++ strbuf_addf(&reflog_msg, "replay --revert %s", revert_name_opt); ++ else if (advance_name_opt) + strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt); + else + strbuf_addf(&reflog_msg, "replay --onto %s", @@ builtin/replay.c: int cmd_replay(int argc, die(_("replaying merge commits is not supported yet!")); last_commit = pick_regular_commit(repo, commit, replayed_commits, - onto, &merge_opt, &result); -+ onto, &merge_opt, &result, -+ is_revert); ++ onto, &merge_opt, &result, action); if (!last_commit) break; - - ## sequencer.c ## -@@ sequencer.c: static int do_pick_commit(struct repository *r, - if (opts->commit_use_reference) { - strbuf_commented_addf(&ctx->message, comment_line_str, - "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); -- } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && -- /* -- * We don't touch pre-existing repeated reverts, because -- * theoretically these can be nested arbitrarily deeply, -- * thus requiring excessive complexity to deal with. -- */ -- !starts_with(orig_subject, "Revert \"")) { -- strbuf_addstr(&ctx->message, "Reapply \""); -- strbuf_addstr(&ctx->message, orig_subject); -- strbuf_addstr(&ctx->message, "\n"); -+ strbuf_addstr(&ctx->message, "\nThis reverts commit "); - } else { -- strbuf_addstr(&ctx->message, "Revert \""); -- strbuf_addstr(&ctx->message, msg.subject); -- strbuf_addstr(&ctx->message, "\"\n"); -+ sequencer_format_revert_header(&ctx->message, msg.subject); - } -- strbuf_addstr(&ctx->message, "\nThis reverts commit "); - refer_to_commit(opts, &ctx->message, commit); +@@ builtin/replay.c: int cmd_replay(int argc, + kh_value(replayed_commits, pos) = last_commit; - if (commit->parents && commit->parents->next) { -@@ sequencer.c: int sequencer_pick_revisions(struct repository *r, - return res; - } + /* Update any necessary branches */ +- if (advance_name) ++ if (advance_name || revert_name) + continue; + decoration = get_name_decoration(&commit->object); + if (!decoration) +@@ builtin/replay.c: int cmd_replay(int argc, + } + } -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject) -+{ -+ const char *revert_subject; -+ -+ if (skip_prefix(orig_subject, "Revert \"", &revert_subject) && -+ /* -+ * We don't touch pre-existing repeated reverts, because -+ * theoretically these can be nested arbitrarily deeply, -+ * thus requiring excessive complexity to deal with. -+ */ -+ !starts_with(revert_subject, "Revert \"")) { -+ strbuf_addstr(out, "Reapply \""); -+ strbuf_addstr(out, revert_subject); -+ strbuf_addch(out, '\n'); -+ } else { -+ strbuf_addstr(out, "Revert \""); -+ strbuf_addstr(out, orig_subject); -+ strbuf_addstr(out, "\"\n"); +- /* In --advance mode, advance the target ref */ ++ /* In --advance or --revert mode, update the target ref */ + if (result.clean == 1 && advance_name) { + if (handle_ref_update(ref_mode, transaction, advance_name, + &last_commit->object.oid, +@@ builtin/replay.c: int cmd_replay(int argc, + goto cleanup; + } + } ++ if (result.clean == 1 && revert_name) { ++ if (handle_ref_update(ref_mode, transaction, revert_name, ++ &last_commit->object.oid, ++ &onto->object.oid, ++ reflog_msg.buf, ++ &transaction_err) < 0) { ++ ret = error(_("failed to update ref '%s': %s"), ++ revert_name, transaction_err.buf); ++ goto cleanup; ++ } + } -+ -+ strbuf_addstr(out, "\nThis reverts commit "); -+} -+ - void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) - { - unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP; - - ## sequencer.h ## -@@ sequencer.h: int todo_list_rearrange_squash(struct todo_list *todo_list); - */ - void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag); -+/* -+ * Formats a revert commit message header following standard Git conventions. -+ * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert -+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit " at the end. -+ * The caller should append the commit OID after calling this function. -+ */ -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject); -+ - void append_conflicts_hint(struct index_state *istate, - struct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode); - enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg, + /* Commit the ref transaction if we have one */ + if (transaction && result.clean == 1) { ## t/t3650-replay-basics.sh ## @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' ' test_grep "invalid.*replay.refAction.*value" error ' -+test_expect_success 'using replay with --revert to revert a commit' ' -+ # Revert commits D and E from topic2 -+ git replay --revert --onto topic1 topic1..topic2 >result && -+ -+ test_must_be_empty result && -+ -+ # Verify the commit messages contain "Revert" -+ # topic1..topic2 contains D and E, so we get 2 reverts on top of topic1 (which has F, C, B, A) -+ git log --format=%s topic2 >actual && -+ test_line_count = 6 actual && -+ head -n 1 actual >first-line && -+ test_grep "^Revert" first-line ++test_expect_success 'setup for revert tests' ' ++ git switch -c revert-test main && ++ test_commit R1 && ++ test_commit R2 && ++ test_commit R3 && ++ git switch main +' + -+test_expect_success 'using replay with --revert on bare repo' ' -+ git -C bare replay --revert --onto topic1 topic1..topic2 >result-bare && -+ -+ test_must_be_empty result-bare && -+ -+ # Verify the commit message contains "Revert" -+ git -C bare log --format=%s topic2 >actual-bare && -+ test_line_count = 6 actual-bare && -+ head -n 1 actual-bare >first-line-bare && -+ test_grep "^Revert" first-line-bare ++test_expect_success 'git replay --revert reverts commits' ' ++ # Store original state ++ START=$(git rev-parse revert-test) && ++ test_when_finished "git branch -f revert-test $START" && ++ ++ git replay --revert revert-test revert-test~2..revert-test >output && ++ test_must_be_empty output && ++ ++ # Verify revert-test was updated with revert commits ++ git log --format=%s -n 5 revert-test >actual && ++ cat >expect <<-\EOF && ++ Revert "R3" ++ Revert "R2" ++ R3 ++ R2 ++ R1 ++ EOF ++ test_cmp expect actual && ++ ++ # Verify commit message format ++ test_commit_message revert-test -m "Revert \"R3\" ++ ++This reverts commit $(git rev-parse R3)." +' + -+test_expect_success 'using replay with --revert and --advance' ' -+ # Revert commits from topic2 and advance main -+ git replay --revert --advance main topic1..topic2 >result && ++test_expect_success 'git replay --revert with --ref-action=print' ' ++ # Store original state ++ START=$(git rev-parse revert-test) && ++ test_when_finished "git branch -f revert-test $START" && + -+ test_must_be_empty result && ++ git replay --ref-action=print --revert revert-test revert-test~2..revert-test >result && ++ test_line_count = 1 result && + -+ # Verify the commit message contains "Revert" -+ git log --format=%s main >actual && -+ head -n 1 actual >first-line && -+ test_grep "^Revert" first-line ++ # Verify output format: update refs/heads/revert-test <new> <old> ++ cut -f 3 -d " " result >new-tip && ++ printf "update refs/heads/revert-test " >expect && ++ printf "%s " $(cat new-tip) >>expect && ++ printf "%s\n" $START >>expect && ++ test_cmp expect result +' + -+test_expect_success 'replay with --revert fails with --contained' ' -+ test_must_fail git replay --revert --contained --onto main main..topic3 2>error && -+ test_grep "revert.*contained.*cannot be used together" error -+' ++test_expect_success 'git replay --revert reapply behavior' ' ++ # Store original state ++ START=$(git rev-parse revert-test) && ++ test_when_finished "git branch -f revert-test $START" && + -+test_expect_success 'verify revert actually reverses changes' ' -+ # Create a branch with a simple change -+ git switch -c revert-test main && -+ echo "new content" >test-file.txt && -+ git add test-file.txt && -+ git commit -m "Add test file" && ++ # First revert R3 ++ git replay --revert revert-test revert-test~1..revert-test && ++ REVERT_R3=$(git rev-parse revert-test) && + -+ # Revert the commit -+ git replay --revert --advance revert-test HEAD^..HEAD >result && ++ # Now revert the revert (should create "Reapply" message) ++ git replay --revert revert-test revert-test~1..revert-test >output && ++ test_must_be_empty output && + -+ test_must_be_empty result && ++ # Verify Reapply message ++ test_commit_message revert-test -m "Reapply \"R3\" + -+ # The file should no longer exist (reverted) -+ test_must_fail git show revert-test:test-file.txt ++This reverts commit $(git rev-parse $REVERT_R3)." +' + -+test_expect_success 'revert of a revert creates reapply message' ' -+ # Create a commit -+ git switch -c revert-revert main && -+ echo "content" >revert-test-2.txt && -+ git add revert-test-2.txt && -+ git commit -m "Add revert test file" && -+ -+ ORIGINAL=$(git rev-parse HEAD) && -+ -+ # First revert -+ git replay --revert --advance revert-revert HEAD^..HEAD >result1 && -+ -+ test_must_be_empty result1 && -+ -+ # Check first revert message starts with "Revert" -+ git log --format=%s -1 revert-revert >msg1 && -+ test_grep "^Revert" msg1 && -+ -+ FIRST_REVERT=$(git rev-parse revert-revert) && -+ -+ # Now revert the revert -+ git replay --revert --advance revert-revert $ORIGINAL..$FIRST_REVERT >result2 && -+ -+ test_must_be_empty result2 && -+ -+ # Check second revert message starts with "Reapply" -+ git log --format=%s -1 revert-revert >msg2 && -+ test_grep "^Reapply" msg2 && -+ -+ # The file should exist again (reapplied) -+ git show revert-revert:revert-test-2.txt >actual && -+ echo "content" >expected && -+ test_cmp expected actual ++test_expect_success 'git replay --revert with conflict' ' ++ # Create a conflicting scenario ++ git switch -c revert-conflict main && ++ test_commit C1 && ++ echo conflict >C1.t && ++ test_commit C2 C1.t && ++ git switch main && ++ echo different >C1.t && ++ test_commit C3 C1.t && ++ ++ # Try to revert C2 onto main (which has conflicting C3) ++ test_expect_code 1 git replay --revert main revert-conflict~1..revert-conflict +' + -+test_expect_success 'replay --revert includes commit SHA in message' ' -+ git switch -c revert-sha-test main && -+ echo "test" >sha-test.txt && -+ git add sha-test.txt && -+ git commit -m "Test commit for SHA" && -+ -+ COMMIT_SHA=$(git rev-parse HEAD) && -+ git replay --revert --advance revert-sha-test HEAD^..HEAD >result && ++test_expect_success 'git replay --revert reflog message' ' ++ # Store original state ++ START=$(git rev-parse revert-test) && ++ test_when_finished "git branch -f revert-test $START" && + -+ test_must_be_empty result && ++ git replay --revert revert-test revert-test~1..revert-test >output && ++ test_must_be_empty output && + -+ # Check that the commit message includes the original SHA -+ git log --format=%B -1 revert-sha-test >msg && -+ test_grep "$COMMIT_SHA" msg ++ # Verify reflog message includes --revert and branch name ++ git reflog revert-test -1 --format=%gs >reflog-msg && ++ echo "replay --revert revert-test" >expect-reflog && ++ test_cmp expect-reflog reflog-msg +' + -+test_expect_success 'replay --revert with conflict' ' -+ # Create a conflicting situation -+ git switch -c revert-conflict main && -+ echo "line1" >conflict-file.txt && -+ git add conflict-file.txt && -+ git commit -m "Add conflict file" && -+ -+ git switch -c revert-conflict-branch HEAD^ && -+ echo "different" >conflict-file.txt && -+ git add conflict-file.txt && -+ git commit -m "Different content" && -+ -+ # Try to revert the first commit onto the conflicting branch -+ test_expect_code 1 git replay --revert --onto revert-conflict-branch revert-conflict^..revert-conflict ++test_expect_success 'git replay --revert incompatible with --contained' ' ++ test_must_fail git replay --revert revert-test --contained revert-test~1..revert-test 2>error && ++ test_grep "requires --onto" error +' + -+test_expect_success 'replay --revert handles multiple commits' ' -+ # Save the original topic2 state -+ ORIG_TOPIC2=$(git rev-parse topic2) && -+ test_when_finished "git branch -f topic2 $ORIG_TOPIC2" && -+ -+ # Revert D and E from topic2, applying the reverts onto topic1 -+ git replay --revert --onto topic1 topic1..topic2 >result && -+ -+ test_must_be_empty result && -+ -+ # Verify both revert commits appear in the log -+ git log --format=%s topic2 >log && -+ head -n 2 log >first-two && -+ test_grep "^Revert" first-two && ++test_expect_success 'git replay --revert incompatible with --onto' ' ++ test_must_fail git replay --revert revert-test --onto main revert-test~1..revert-test 2>error && ++ test_grep "cannot be used together" error ++' + -+ # Verify we have both "Revert D" and "Revert E" -+ test_grep "Revert.*E" log && -+ test_grep "Revert.*D" log ++test_expect_success 'git replay --revert incompatible with --advance' ' ++ test_must_fail git replay --revert revert-test --advance main revert-test~1..revert-test 2>error && ++ test_grep "cannot be used together" error +' + test_done -- 2.51.0 base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9 Thanks - Siddharth ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v2 1/2] sequencer: extract revert message formatting into shared function 2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana @ 2025-12-02 20:16 ` Siddharth Asthana 2025-12-05 11:33 ` Patrick Steinhardt 2025-12-02 20:16 ` [PATCH v2 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2026-02-18 23:42 ` [PATCH v3 0/2] " Siddharth Asthana 2 siblings, 1 reply; 92+ messages in thread From: Siddharth Asthana @ 2025-12-02 20:16 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon, Siddharth Asthana The logic for formatting revert commit messages (handling "Revert" and "Reapply" cases) is currently duplicated between sequencer.c and will be needed by builtin/replay.c. Extract this logic into a new sequencer_format_revert_header() function that can be shared. The function handles both regular reverts ("Revert "<subject>"") and revert-of-revert cases ("Reapply "<subject>""). Update do_pick_commit() to use the new helper, eliminating code duplication while preserving the special handling for commit_use_reference. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- sequencer.c | 39 +++++++++++++++++++++++++-------------- sequencer.h | 8 ++++++++ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/sequencer.c b/sequencer.c index 5476d39ba9..9f621aef4b 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r, if (opts->commit_use_reference) { strbuf_commented_addf(&ctx->message, comment_line_str, "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && - /* - * We don't touch pre-existing repeated reverts, because - * theoretically these can be nested arbitrarily deeply, - * thus requiring excessive complexity to deal with. - */ - !starts_with(orig_subject, "Revert \"")) { - strbuf_addstr(&ctx->message, "Reapply \""); - strbuf_addstr(&ctx->message, orig_subject); - strbuf_addstr(&ctx->message, "\n"); + strbuf_addstr(&ctx->message, "\nThis reverts commit "); } else { - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); + sequencer_format_revert_header(&ctx->message, msg.subject); } - strbuf_addstr(&ctx->message, "\nThis reverts commit "); refer_to_commit(opts, &ctx->message, commit); if (commit->parents && commit->parents->next) { @@ -5572,6 +5560,29 @@ int sequencer_pick_revisions(struct repository *r, return res; } +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject) +{ + const char *revert_subject; + + if (skip_prefix(orig_subject, "Revert \"", &revert_subject) && + /* + * We don't touch pre-existing repeated reverts, because + * theoretically these can be nested arbitrarily deeply, + * thus requiring excessive complexity to deal with. + */ + !starts_with(revert_subject, "Revert \"")) { + strbuf_addstr(out, "Reapply \""); + strbuf_addstr(out, revert_subject); + strbuf_addch(out, '\n'); + } else { + strbuf_addstr(out, "Revert \""); + strbuf_addstr(out, orig_subject); + strbuf_addstr(out, "\"\n"); + } + + strbuf_addstr(out, "\nThis reverts commit "); +} + void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) { unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP; diff --git a/sequencer.h b/sequencer.h index 719684c8a9..114c5d2449 100644 --- a/sequencer.h +++ b/sequencer.h @@ -271,4 +271,12 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) */ int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +/* + * Formats a revert commit message header following standard Git conventions. + * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert + * cases ("Reapply \"<subject>\""). Adds "This reverts commit " at the end. + * The caller should append the commit OID after calling this function. + */ +void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject); + #endif /* SEQUENCER_H */ -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function 2025-12-02 20:16 ` [PATCH v2 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana @ 2025-12-05 11:33 ` Patrick Steinhardt 2025-12-07 23:00 ` Siddharth Asthana 0 siblings, 1 reply; 92+ messages in thread From: Patrick Steinhardt @ 2025-12-05 11:33 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote: > diff --git a/sequencer.c b/sequencer.c > index 5476d39ba9..9f621aef4b 100644 > --- a/sequencer.c > +++ b/sequencer.c > @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r, > if (opts->commit_use_reference) { > strbuf_commented_addf(&ctx->message, comment_line_str, > "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); > - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && > - /* > - * We don't touch pre-existing repeated reverts, because > - * theoretically these can be nested arbitrarily deeply, > - * thus requiring excessive complexity to deal with. > - */ > - !starts_with(orig_subject, "Revert \"")) { > - strbuf_addstr(&ctx->message, "Reapply \""); > - strbuf_addstr(&ctx->message, orig_subject); > - strbuf_addstr(&ctx->message, "\n"); > + strbuf_addstr(&ctx->message, "\nThis reverts commit "); > } else { > - strbuf_addstr(&ctx->message, "Revert \""); > - strbuf_addstr(&ctx->message, msg.subject); > - strbuf_addstr(&ctx->message, "\"\n"); > + sequencer_format_revert_header(&ctx->message, msg.subject); > } > - strbuf_addstr(&ctx->message, "\nThis reverts commit "); > refer_to_commit(opts, &ctx->message, commit); > > if (commit->parents && commit->parents->next) { Is there any reason why we don't also handle `refer_to_commit()` in that new function? Patrick ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function 2025-12-05 11:33 ` Patrick Steinhardt @ 2025-12-07 23:00 ` Siddharth Asthana 2025-12-08 7:07 ` Patrick Steinhardt 0 siblings, 1 reply; 92+ messages in thread From: Siddharth Asthana @ 2025-12-07 23:00 UTC (permalink / raw) To: Patrick Steinhardt Cc: git, christian.couder, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon On 05/12/25 17:03, Patrick Steinhardt wrote: > On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote: >> diff --git a/sequencer.c b/sequencer.c >> index 5476d39ba9..9f621aef4b 100644 >> --- a/sequencer.c >> +++ b/sequencer.c >> @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r, >> if (opts->commit_use_reference) { >> strbuf_commented_addf(&ctx->message, comment_line_str, >> "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); >> - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && >> - /* >> - * We don't touch pre-existing repeated reverts, because >> - * theoretically these can be nested arbitrarily deeply, >> - * thus requiring excessive complexity to deal with. >> - */ >> - !starts_with(orig_subject, "Revert \"")) { >> - strbuf_addstr(&ctx->message, "Reapply \""); >> - strbuf_addstr(&ctx->message, orig_subject); >> - strbuf_addstr(&ctx->message, "\n"); >> + strbuf_addstr(&ctx->message, "\nThis reverts commit "); >> } else { >> - strbuf_addstr(&ctx->message, "Revert \""); >> - strbuf_addstr(&ctx->message, msg.subject); >> - strbuf_addstr(&ctx->message, "\"\n"); >> + sequencer_format_revert_header(&ctx->message, msg.subject); >> } >> - strbuf_addstr(&ctx->message, "\nThis reverts commit "); >> refer_to_commit(opts, &ctx->message, commit); >> >> if (commit->parents && commit->parents->next) { > Is there any reason why we don't also handle `refer_to_commit()` in that > new function? The `refer_to_commit()` function depends on `struct replay_opts` and its `commit_use_reference` flag, which controls whether to use abbreviated commit info ("%h (%s, %ad)") or the full OID. This is specific to sequencer.c's interactive workflow where users can choose the reference style via --reference. In replay.c, we always use the full OID via `oid_to_hex()` since it's designed for non-interactive server-side operations without the `replay_opts` framework. Including `refer_to_commit()` would require either passing `replay_opts` to the shared function (leaking sequencer internals) or adding a format parameter which feels like over-engineering for current needs. Happy to reconsider if you think there's a cleaner way to share this. Thanks, Siddharth > > Patrick ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function 2025-12-07 23:00 ` Siddharth Asthana @ 2025-12-08 7:07 ` Patrick Steinhardt 2026-02-11 13:03 ` Toon Claes 0 siblings, 1 reply; 92+ messages in thread From: Patrick Steinhardt @ 2025-12-08 7:07 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon On Mon, Dec 08, 2025 at 04:30:58AM +0530, Siddharth Asthana wrote: > > On 05/12/25 17:03, Patrick Steinhardt wrote: > > On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote: > > > diff --git a/sequencer.c b/sequencer.c > > > index 5476d39ba9..9f621aef4b 100644 > > > --- a/sequencer.c > > > +++ b/sequencer.c > > > @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r, > > > if (opts->commit_use_reference) { > > > strbuf_commented_addf(&ctx->message, comment_line_str, > > > "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); > > > - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && > > > - /* > > > - * We don't touch pre-existing repeated reverts, because > > > - * theoretically these can be nested arbitrarily deeply, > > > - * thus requiring excessive complexity to deal with. > > > - */ > > > - !starts_with(orig_subject, "Revert \"")) { > > > - strbuf_addstr(&ctx->message, "Reapply \""); > > > - strbuf_addstr(&ctx->message, orig_subject); > > > - strbuf_addstr(&ctx->message, "\n"); > > > + strbuf_addstr(&ctx->message, "\nThis reverts commit "); > > > } else { > > > - strbuf_addstr(&ctx->message, "Revert \""); > > > - strbuf_addstr(&ctx->message, msg.subject); > > > - strbuf_addstr(&ctx->message, "\"\n"); > > > + sequencer_format_revert_header(&ctx->message, msg.subject); > > > } > > > - strbuf_addstr(&ctx->message, "\nThis reverts commit "); > > > refer_to_commit(opts, &ctx->message, commit); > > > if (commit->parents && commit->parents->next) { > > Is there any reason why we don't also handle `refer_to_commit()` in that > > new function? > > > The `refer_to_commit()` function depends on `struct replay_opts` and its > `commit_use_reference` flag, which controls whether to use abbreviated > commit info ("%h (%s, %ad)") or the full OID. This is specific to > sequencer.c's interactive workflow where users can choose the reference > style via --reference. > > In replay.c, we always use the full OID via `oid_to_hex()` since it's > designed for non-interactive server-side operations without the > `replay_opts` framework. Including `refer_to_commit()` would require either > passing `replay_opts` to the shared function (leaking sequencer internals) > or adding a format parameter which feels like over-engineering for current > needs. > > Happy to reconsider if you think there's a cleaner way to share this. A simple alternative might be to convert the `struct replay_opts` parameter into a `flags` field that tells the function whether it is expected to use the object ID or whether it should try using the abbreviated commit info instead. Patrick ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function 2025-12-08 7:07 ` Patrick Steinhardt @ 2026-02-11 13:03 ` Toon Claes 2026-02-11 13:40 ` Patrick Steinhardt ` (2 more replies) 0 siblings, 3 replies; 92+ messages in thread From: Toon Claes @ 2026-02-11 13:03 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, Patrick Steinhardt Patrick Steinhardt <ps@pks.im> writes: > On Mon, Dec 08, 2025 at 04:30:58AM +0530, Siddharth Asthana wrote: >> >> On 05/12/25 17:03, Patrick Steinhardt wrote: >> > On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote: >> > > diff --git a/sequencer.c b/sequencer.c >> > > index 5476d39ba9..9f621aef4b 100644 >> > > --- a/sequencer.c >> > > +++ b/sequencer.c >> > > @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r, >> > > if (opts->commit_use_reference) { >> > > strbuf_commented_addf(&ctx->message, comment_line_str, >> > > "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); >> > > - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && >> > > - /* >> > > - * We don't touch pre-existing repeated reverts, because >> > > - * theoretically these can be nested arbitrarily deeply, >> > > - * thus requiring excessive complexity to deal with. >> > > - */ >> > > - !starts_with(orig_subject, "Revert \"")) { >> > > - strbuf_addstr(&ctx->message, "Reapply \""); >> > > - strbuf_addstr(&ctx->message, orig_subject); >> > > - strbuf_addstr(&ctx->message, "\n"); >> > > + strbuf_addstr(&ctx->message, "\nThis reverts commit "); >> > > } else { >> > > - strbuf_addstr(&ctx->message, "Revert \""); >> > > - strbuf_addstr(&ctx->message, msg.subject); >> > > - strbuf_addstr(&ctx->message, "\"\n"); >> > > + sequencer_format_revert_header(&ctx->message, msg.subject); >> > > } >> > > - strbuf_addstr(&ctx->message, "\nThis reverts commit "); >> > > refer_to_commit(opts, &ctx->message, commit); >> > > if (commit->parents && commit->parents->next) { >> > Is there any reason why we don't also handle `refer_to_commit()` in that >> > new function? >> >> >> The `refer_to_commit()` function depends on `struct replay_opts` and its >> `commit_use_reference` flag, which controls whether to use abbreviated >> commit info ("%h (%s, %ad)") or the full OID. This is specific to >> sequencer.c's interactive workflow where users can choose the reference >> style via --reference. >> >> In replay.c, we always use the full OID via `oid_to_hex()` since it's >> designed for non-interactive server-side operations without the >> `replay_opts` framework. Even if it's non-interactive, I wonder if we should make it obey the config 'revert.reference' as well? To me it makes sense git-replay(1) and git-revert(1) give the same outcome if that config is set. >> Including `refer_to_commit()` would require either >> passing `replay_opts` to the shared function (leaking sequencer internals) >> or adding a format parameter which feels like over-engineering for current >> needs. >> >> Happy to reconsider if you think there's a cleaner way to share this. > > A simple alternative might be to convert the `struct replay_opts` > parameter into a `flags` field that tells the function whether it is > expected to use the object ID or whether it should try using the > abbreviated commit info instead. I was considering to add a bool for this option alone, but I agree flags is probably more future-proof. Patrick, I assume you don't mean to revamp the `struct replay_opts` completely, but only the parameter that would be passed into sequencer_format_revert_header() and refer_to_commit()? Siddharth, I see you have plenty of good reviews on this version of the series ([PATCH 2/2] in particular). I'd love to see you post v3. Or do you have any open questions you need answers to before you can send it out? Let me know if I can help with any decision-making. -- Cheers, Toon ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function 2026-02-11 13:03 ` Toon Claes @ 2026-02-11 13:40 ` Patrick Steinhardt 2026-02-11 15:23 ` Kristoffer Haugsbakk 2026-02-18 22:53 ` Siddharth Asthana 2 siblings, 0 replies; 92+ messages in thread From: Patrick Steinhardt @ 2026-02-11 13:40 UTC (permalink / raw) To: Toon Claes Cc: Siddharth Asthana, git, christian.couder, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin On Wed, Feb 11, 2026 at 02:03:22PM +0100, Toon Claes wrote: > Patrick Steinhardt <ps@pks.im> writes: > > parameter into a `flags` field that tells the function whether it is > > expected to use the object ID or whether it should try using the > > abbreviated commit info instead. > > I was considering to add a bool for this option alone, but I agree flags > is probably more future-proof. > > Patrick, I assume you don't mean to revamp the `struct replay_opts` > completely, but only the parameter that would be passed into > sequencer_format_revert_header() and refer_to_commit()? Yes, exactly. Patrick ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function 2026-02-11 13:03 ` Toon Claes 2026-02-11 13:40 ` Patrick Steinhardt @ 2026-02-11 15:23 ` Kristoffer Haugsbakk 2026-02-11 17:41 ` Junio C Hamano 2026-02-18 22:53 ` Siddharth Asthana 2 siblings, 1 reply; 92+ messages in thread From: Kristoffer Haugsbakk @ 2026-02-11 15:23 UTC (permalink / raw) To: Toon Claes, Siddharth Asthana Cc: git, Christian Couder, Elijah Newren, Junio C Hamano, Phillip Wood, Phillip Wood, Karthik Nayak, Johannes Schindelin, Patrick Steinhardt On Wed, Feb 11, 2026, at 14:03, Toon Claes wrote: >>>[snip] >>> In replay.c, we always use the full OID via `oid_to_hex()` since it's >>> designed for non-interactive server-side operations without the >>> `replay_opts` framework. > > Even if it's non-interactive, I wonder if we should make it obey the > config 'revert.reference' as well? To me it makes sense git-replay(1) > and git-revert(1) give the same outcome if that config is set. I don’t understand the position on plumbing commands. Should plumbing commands ignore user configs so that results don’t change based on that? Or should implementers that use this command set the config files to `/dev/null` in order to opt out of the behavior? >[snip] ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function 2026-02-11 15:23 ` Kristoffer Haugsbakk @ 2026-02-11 17:41 ` Junio C Hamano 0 siblings, 0 replies; 92+ messages in thread From: Junio C Hamano @ 2026-02-11 17:41 UTC (permalink / raw) To: Kristoffer Haugsbakk Cc: Toon Claes, Siddharth Asthana, git, Christian Couder, Elijah Newren, Phillip Wood, Phillip Wood, Karthik Nayak, Johannes Schindelin, Patrick Steinhardt "Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes: > I don’t understand the position on plumbing commands. Should plumbing > commands ignore user configs so that results don’t change based on that? > Or should implementers that use this command set the config files to > `/dev/null` in order to opt out of the behavior? The former. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v2 1/2] sequencer: extract revert message formatting into shared function 2026-02-11 13:03 ` Toon Claes 2026-02-11 13:40 ` Patrick Steinhardt 2026-02-11 15:23 ` Kristoffer Haugsbakk @ 2026-02-18 22:53 ` Siddharth Asthana 2 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-02-18 22:53 UTC (permalink / raw) To: Toon Claes Cc: git, christian.couder, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, Patrick Steinhardt On 11/02/26 18:33, Toon Claes wrote: > Patrick Steinhardt <ps@pks.im> writes: > >> On Mon, Dec 08, 2025 at 04:30:58AM +0530, Siddharth Asthana wrote: >>> On 05/12/25 17:03, Patrick Steinhardt wrote: >>>> On Wed, Dec 03, 2025 at 01:46:10AM +0530, Siddharth Asthana wrote: >>>>> diff --git a/sequencer.c b/sequencer.c >>>>> index 5476d39ba9..9f621aef4b 100644 >>>>> --- a/sequencer.c >>>>> +++ b/sequencer.c >>>>> @@ -2365,22 +2365,10 @@ static int do_pick_commit(struct repository *r, >>>>> if (opts->commit_use_reference) { >>>>> strbuf_commented_addf(&ctx->message, comment_line_str, >>>>> "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); >>>>> - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && >>>>> - /* >>>>> - * We don't touch pre-existing repeated reverts, because >>>>> - * theoretically these can be nested arbitrarily deeply, >>>>> - * thus requiring excessive complexity to deal with. >>>>> - */ >>>>> - !starts_with(orig_subject, "Revert \"")) { >>>>> - strbuf_addstr(&ctx->message, "Reapply \""); >>>>> - strbuf_addstr(&ctx->message, orig_subject); >>>>> - strbuf_addstr(&ctx->message, "\n"); >>>>> + strbuf_addstr(&ctx->message, "\nThis reverts commit "); >>>>> } else { >>>>> - strbuf_addstr(&ctx->message, "Revert \""); >>>>> - strbuf_addstr(&ctx->message, msg.subject); >>>>> - strbuf_addstr(&ctx->message, "\"\n"); >>>>> + sequencer_format_revert_header(&ctx->message, msg.subject); >>>>> } >>>>> - strbuf_addstr(&ctx->message, "\nThis reverts commit "); >>>>> refer_to_commit(opts, &ctx->message, commit); >>>>> if (commit->parents && commit->parents->next) { >>>> Is there any reason why we don't also handle `refer_to_commit()` in that >>>> new function? >>> >>> The `refer_to_commit()` function depends on `struct replay_opts` and its >>> `commit_use_reference` flag, which controls whether to use abbreviated >>> commit info ("%h (%s, %ad)") or the full OID. This is specific to >>> sequencer.c's interactive workflow where users can choose the reference >>> style via --reference. >>> >>> In replay.c, we always use the full OID via `oid_to_hex()` since it's >>> designed for non-interactive server-side operations without the >>> `replay_opts` framework. > Even if it's non-interactive, I wonder if we should make it obey the > config 'revert.reference' as well? To me it makes sense git-replay(1) > and git-revert(1) give the same outcome if that config is set. Junio clarified downthread that plumbing commands should ignore user configs, so I think sticking with the full OID in replay is the right thing to do here. > >>> Including `refer_to_commit()` would require either >>> passing `replay_opts` to the shared function (leaking sequencer internals) >>> or adding a format parameter which feels like over-engineering for current >>> needs. >>> >>> Happy to reconsider if you think there's a cleaner way to share this. >> A simple alternative might be to convert the `struct replay_opts` >> parameter into a `flags` field that tells the function whether it is >> expected to use the object ID or whether it should try using the >> abbreviated commit info instead. > I was considering to add a bool for this option alone, but I agree flags > is probably more future-proof. For v3 i went with an optional `oid` parameter on sequencer_format_revert_header() -- when non-NULL the function appends the full hash itself, when NULL the caller (sequencer) handles the reference via refer_to_commit(). it is a simpler split than flags but gets the job done for now. I am more happy to switch to a flag approach if you and Patrick feel strongly about it. > > Patrick, I assume you don't mean to revamp the `struct replay_opts` > completely, but only the parameter that would be passed into > sequencer_format_revert_header() and refer_to_commit()? > > Siddharth, I see you have plenty of good reviews on this version of the > series ([PATCH 2/2] in particular). I'd love to see you post v3. Or do > you have any open questions you need answers to before you can send it > out? Let me know if I can help with any decision-making. > Thanks Toon! v3 is ready, will share on mailing list thread soon. The main change beside addressing Patrick's and Phillip's review comments is a rebase on top of the latest upstream, which moved the replay logic into a separate library (replay.c / replay.h), so the diff looks quite different from v2 but the approach is the same. - Siddharth ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v2 2/2] replay: add --revert mode to reverse commit changes 2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana 2025-12-02 20:16 ` [PATCH v2 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana @ 2025-12-02 20:16 ` Siddharth Asthana 2025-12-05 11:33 ` Patrick Steinhardt 2025-12-16 16:23 ` Phillip Wood 2026-02-18 23:42 ` [PATCH v3 0/2] " Siddharth Asthana 2 siblings, 2 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-12-02 20:16 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon, Siddharth Asthana, Johannes Schindelin The `git replay` command performs server-side history rewriting without requiring a working tree. While it currently supports cherry-picking commits (--advance) and rebasing (--onto), it lacks the ability to revert them. At GitLab, we use replay in Gitaly for efficient server-side operations on bare repositories. Adding revert functionality enables us to reverse problematic commits directly on the server, eliminating client-side roundtrips and reducing network overhead. Add a `--revert <branch>` mode that reverses the changes introduced by the specified commits. Following the architecture of --onto and --advance, --revert is a standalone mode that takes a branch argument and updates that branch with the revert commits. The implementation follows the same approach as sequencer.c (lines 2360-2399), where cherry-pick and revert are the same merge operation but with swapped arguments: - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) - Revert: merge(ancestor=commit, ours=current, theirs=parent) We swap the base and pickme trees when calling merge_incore_nonrecursive(), effectively reversing the diff direction. The existing conflict handling, ref updates, and atomic transaction support work unchanged. The commit messages follow git revert conventions: prefixed with "Revert" and including the original commit SHA. When reverting a commit that itself starts with "Revert", the message uses "Reapply" instead. Unlike cherry-pick which preserves the original author, revert commits use the current user as the author, matching the behavior of git revert. Helped-by: Christian Couder <christian.couder@gmail.com> Helped-by: Patrick Steinhardt <ps@pks.im> Helped-by: Elijah Newren <newren@gmail.com> Helped-by: Phillip Wood <phillip.wood123@gmail.com> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Helped-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- Documentation/git-replay.adoc | 36 ++++++++- builtin/replay.c | 145 ++++++++++++++++++++++++++++------ t/t3650-replay-basics.sh | 111 ++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 25 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index dcb26e8a8e..eb297c7530 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t SYNOPSIS -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>... +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... DESCRIPTION ----------- @@ -42,6 +42,24 @@ The history is replayed on top of the <branch> and <branch> is updated to point at the tip of the resulting history. This is different from `--onto`, which uses the target only as a starting point without updating it. +--revert <branch>:: + Starting point at which to create the new revert commits; must be a + branch name. ++ +When `--revert` is specified, the commits in the revision range are reverted +(their changes are undone) and the revert commits are applied on top of <branch>. +The <branch> is then updated to point at the new commits. This is similar to +running `git revert` for each commit in the range, but works without a working tree. ++ +The commit messages follow `git revert` conventions: prefixed with "Revert" and +including the original commit SHA. When reverting a commit whose message starts +with "Revert", the new message uses "Reapply" instead. The author of the revert +commits is the current user, not the original commit author. ++ +This option is mutually exclusive with `--onto` and `--advance`. It is also +incompatible with `--contained` (which is a modifier for `--onto` only). + + --ref-action[=<mode>]:: Control how references are updated. The mode can be: + @@ -141,6 +159,22 @@ all commits they have since `base`, playing them on top of `origin/main`. These three branches may have commits on top of `base` that they have in common, but that does not need to be the case. +To revert commits from a branch: + +------------ +$ git replay --revert main feature~2..feature +------------ + +This reverts the last two commits from 'feature', creating revert commits on +top of 'main', and updates 'main' to point at the result. This is useful when +commits from 'feature' were previously merged or cherry-picked into 'main' and +need to be undone. + +NOTE: For reverting an entire merge request as a single commit (rather than +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` +which can avoid unnecessary merge conflicts. + + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/replay.c b/builtin/replay.c index 6606a2c94b..7660f7412f 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -17,6 +17,7 @@ #include "parse-options.h" #include "refs.h" #include "revision.h" +#include "sequencer.h" #include "strmap.h" #include <oidset.h> #include <tree.h> @@ -26,6 +27,11 @@ enum ref_action_mode { REF_ACTION_PRINT, }; +enum replay_action { + REPLAY_PICK, + REPLAY_REVERT, +}; + static const char *short_commit_name(struct repository *repo, struct commit *commit) { @@ -57,10 +63,32 @@ static char *get_author(const char *message) return NULL; } +static void generate_revert_message(struct strbuf *msg, + struct commit *commit, + struct repository *repo) +{ + const char *out_enc = get_commit_output_encoding(); + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); + const char *subject_start; + int subject_len; + char *subject; + + subject_len = find_commit_subject(message, &subject_start); + subject = xmemdupz(subject_start, subject_len); + + sequencer_format_revert_header(msg, subject); + strbuf_addstr(msg, oid_to_hex(&commit->object.oid)); + strbuf_addstr(msg, ".\n"); + + free(subject); + repo_unuse_commit_buffer(repo, commit, message); +} + static struct commit *create_commit(struct repository *repo, struct tree *tree, struct commit *based_on, - struct commit *parent) + struct commit *parent, + enum replay_action action) { struct object_id ret; struct object *obj = NULL; @@ -77,9 +105,14 @@ static struct commit *create_commit(struct repository *repo, commit_list_insert(parent, &parents); extra = read_commit_extra_headers(based_on, exclude_gpgsig); - find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); + if (action == REPLAY_REVERT) { + generate_revert_message(&msg, based_on, repo); + author = xstrdup(git_author_info(IDENT_STRICT)); + } else { + find_commit_subject(message, &orig_message); + strbuf_addstr(&msg, orig_message); + author = get_author(message); + } reset_ident_date(); if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, &ret, author, NULL, sign_commit, extra)) { @@ -90,7 +123,7 @@ static struct commit *create_commit(struct repository *repo, obj = parse_object(repo, &ret); out: - repo_unuse_commit_buffer(the_repository, based_on, message); + repo_unuse_commit_buffer(repo, based_on, message); free_commit_extra_headers(extra); free_commit_list(parents); strbuf_release(&msg); @@ -166,6 +199,7 @@ static void determine_replay_mode(struct repository *repo, struct rev_cmdline_info *cmd_info, const char *onto_name, char **advance_name, + char **revert_name, struct commit **onto, struct strset **update_refs) { @@ -196,6 +230,20 @@ static void determine_replay_mode(struct repository *repo, } if (rinfo.positive_refexprs > 1) die(_("cannot advance target with multiple sources because ordering would be ill-defined")); + } else if (*revert_name) { + struct object_id oid; + char *fullname = NULL; + + *onto = peel_committish(repo, *revert_name); + if (repo_dwim_ref(repo, *revert_name, strlen(*revert_name), + &oid, &fullname, 0) == 1) { + free(*revert_name); + *revert_name = fullname; + } else { + die(_("argument to --revert must be a reference")); + } + if (rinfo.positive_refexprs > 1) + die(_("cannot revert with multiple sources because ordering would be ill-defined")); } else { int positive_refs_complete = ( rinfo.positive_refexprs == @@ -261,7 +309,8 @@ static struct commit *pick_regular_commit(struct repository *repo, kh_oid_map_t *replayed_commits, struct commit *onto, struct merge_options *merge_opt, - struct merge_result *result) + struct merge_result *result, + enum replay_action action) { struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree; @@ -273,21 +322,39 @@ static struct commit *pick_regular_commit(struct repository *repo, pickme_tree = repo_get_commit_tree(repo, pickme); base_tree = repo_get_commit_tree(repo, base); - merge_opt->branch1 = short_commit_name(repo, replayed_base); - merge_opt->branch2 = short_commit_name(repo, pickme); - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + if (action == REPLAY_PICK) { + /* Cherry-pick: normal order */ + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = short_commit_name(repo, pickme); + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); - merge_incore_nonrecursive(merge_opt, - base_tree, - result->tree, - pickme_tree, - result); + merge_incore_nonrecursive(merge_opt, + base_tree, + result->tree, + pickme_tree, + result); - free((char*)merge_opt->ancestor); + free((char *)merge_opt->ancestor); + } else { + /* Revert: swap base and pickme to reverse the diff */ + const char *pickme_name = short_commit_name(repo, pickme); + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); + merge_opt->ancestor = pickme_name; + + merge_incore_nonrecursive(merge_opt, + pickme_tree, + result->tree, + base_tree, + result); + + free((char *)merge_opt->branch2); + } merge_opt->ancestor = NULL; + merge_opt->branch2 = NULL; if (!result->clean) return NULL; - return create_commit(repo, result->tree, pickme, replayed_base); + return create_commit(repo, result->tree, pickme, replayed_base, action); } static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source) @@ -345,6 +412,9 @@ int cmd_replay(int argc, { const char *advance_name_opt = NULL; char *advance_name = NULL; + const char *revert_name_opt = NULL; + char *revert_name = NULL; + enum replay_action action = REPLAY_PICK; struct commit *onto = NULL; const char *onto_name = NULL; int contained = 0; @@ -365,7 +435,7 @@ int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto <newbase> | --advance <branch>) " + "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) " "[--ref-action[=<mode>]] <revision-range>..."), NULL }; @@ -378,6 +448,9 @@ int cmd_replay(int argc, N_("replay onto given commit")), OPT_BOOL(0, "contained", &contained, N_("advance all branches contained in revision-range")), + OPT_STRING(0, "revert", &revert_name_opt, + N_("branch"), + N_("revert commits onto given branch")), OPT_STRING(0, "ref-action", &ref_action, N_("mode"), N_("control ref update behavior (update|print)")), @@ -387,18 +460,28 @@ int cmd_replay(int argc, argc = parse_options(argc, argv, prefix, replay_options, replay_usage, PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); - if (!onto_name && !advance_name_opt) { - error(_("option --onto or --advance is mandatory")); + /* Exactly one mode must be specified */ + if (!onto_name && !advance_name_opt && !revert_name_opt) { + error(_("exactly one of --onto, --advance, or --revert is required")); usage_with_options(replay_usage, replay_options); } die_for_incompatible_opt2(!!advance_name_opt, "--advance", - contained, "--contained"); + !!onto_name, "--onto"); + die_for_incompatible_opt2(!!revert_name_opt, "--revert", + !!onto_name, "--onto"); + die_for_incompatible_opt2(!!revert_name_opt, "--revert", + !!advance_name_opt, "--advance"); + die_for_incompatible_opt2(contained, "--contained", + !onto_name, "requires --onto"); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); advance_name = xstrdup_or_null(advance_name_opt); + revert_name = xstrdup_or_null(revert_name_opt); + if (revert_name) + action = REPLAY_REVERT; repo_init_revisions(repo, &revs, prefix); @@ -452,10 +535,13 @@ int cmd_replay(int argc, } determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name, + &revert_name, &onto, &update_refs); /* Build reflog message */ - if (advance_name_opt) + if (revert_name_opt) + strbuf_addf(&reflog_msg, "replay --revert %s", revert_name_opt); + else if (advance_name_opt) strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt); else strbuf_addf(&reflog_msg, "replay --onto %s", @@ -496,7 +582,7 @@ int cmd_replay(int argc, die(_("replaying merge commits is not supported yet!")); last_commit = pick_regular_commit(repo, commit, replayed_commits, - onto, &merge_opt, &result); + onto, &merge_opt, &result, action); if (!last_commit) break; @@ -508,7 +594,7 @@ int cmd_replay(int argc, kh_value(replayed_commits, pos) = last_commit; /* Update any necessary branches */ - if (advance_name) + if (advance_name || revert_name) continue; decoration = get_name_decoration(&commit->object); if (!decoration) @@ -532,7 +618,7 @@ int cmd_replay(int argc, } } - /* In --advance mode, advance the target ref */ + /* In --advance or --revert mode, update the target ref */ if (result.clean == 1 && advance_name) { if (handle_ref_update(ref_mode, transaction, advance_name, &last_commit->object.oid, @@ -544,6 +630,17 @@ int cmd_replay(int argc, goto cleanup; } } + if (result.clean == 1 && revert_name) { + if (handle_ref_update(ref_mode, transaction, revert_name, + &last_commit->object.oid, + &onto->object.oid, + reflog_msg.buf, + &transaction_err) < 0) { + ret = error(_("failed to update ref '%s': %s"), + revert_name, transaction_err.buf); + goto cleanup; + } + } /* Commit the ref transaction if we have one */ if (transaction && result.clean == 1) { diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index cf3aacf355..1c4e1cb666 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -314,4 +314,115 @@ test_expect_success 'invalid replay.refAction value' ' test_grep "invalid.*replay.refAction.*value" error ' +test_expect_success 'setup for revert tests' ' + git switch -c revert-test main && + test_commit R1 && + test_commit R2 && + test_commit R3 && + git switch main +' + +test_expect_success 'git replay --revert reverts commits' ' + # Store original state + START=$(git rev-parse revert-test) && + test_when_finished "git branch -f revert-test $START" && + + git replay --revert revert-test revert-test~2..revert-test >output && + test_must_be_empty output && + + # Verify revert-test was updated with revert commits + git log --format=%s -n 5 revert-test >actual && + cat >expect <<-\EOF && + Revert "R3" + Revert "R2" + R3 + R2 + R1 + EOF + test_cmp expect actual && + + # Verify commit message format + test_commit_message revert-test -m "Revert \"R3\" + +This reverts commit $(git rev-parse R3)." +' + +test_expect_success 'git replay --revert with --ref-action=print' ' + # Store original state + START=$(git rev-parse revert-test) && + test_when_finished "git branch -f revert-test $START" && + + git replay --ref-action=print --revert revert-test revert-test~2..revert-test >result && + test_line_count = 1 result && + + # Verify output format: update refs/heads/revert-test <new> <old> + cut -f 3 -d " " result >new-tip && + printf "update refs/heads/revert-test " >expect && + printf "%s " $(cat new-tip) >>expect && + printf "%s\n" $START >>expect && + test_cmp expect result +' + +test_expect_success 'git replay --revert reapply behavior' ' + # Store original state + START=$(git rev-parse revert-test) && + test_when_finished "git branch -f revert-test $START" && + + # First revert R3 + git replay --revert revert-test revert-test~1..revert-test && + REVERT_R3=$(git rev-parse revert-test) && + + # Now revert the revert (should create "Reapply" message) + git replay --revert revert-test revert-test~1..revert-test >output && + test_must_be_empty output && + + # Verify Reapply message + test_commit_message revert-test -m "Reapply \"R3\" + +This reverts commit $(git rev-parse $REVERT_R3)." +' + +test_expect_success 'git replay --revert with conflict' ' + # Create a conflicting scenario + git switch -c revert-conflict main && + test_commit C1 && + echo conflict >C1.t && + test_commit C2 C1.t && + git switch main && + echo different >C1.t && + test_commit C3 C1.t && + + # Try to revert C2 onto main (which has conflicting C3) + test_expect_code 1 git replay --revert main revert-conflict~1..revert-conflict +' + +test_expect_success 'git replay --revert reflog message' ' + # Store original state + START=$(git rev-parse revert-test) && + test_when_finished "git branch -f revert-test $START" && + + git replay --revert revert-test revert-test~1..revert-test >output && + test_must_be_empty output && + + # Verify reflog message includes --revert and branch name + git reflog revert-test -1 --format=%gs >reflog-msg && + echo "replay --revert revert-test" >expect-reflog && + test_cmp expect-reflog reflog-msg +' + +test_expect_success 'git replay --revert incompatible with --contained' ' + test_must_fail git replay --revert revert-test --contained revert-test~1..revert-test 2>error && + test_grep "requires --onto" error +' + +test_expect_success 'git replay --revert incompatible with --onto' ' + test_must_fail git replay --revert revert-test --onto main revert-test~1..revert-test 2>error && + test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --advance' ' + test_must_fail git replay --revert revert-test --advance main revert-test~1..revert-test 2>error && + test_grep "cannot be used together" error +' + test_done -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes 2025-12-02 20:16 ` [PATCH v2 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2025-12-05 11:33 ` Patrick Steinhardt 2025-12-07 23:03 ` Siddharth Asthana 2025-12-16 16:23 ` Phillip Wood 1 sibling, 1 reply; 92+ messages in thread From: Patrick Steinhardt @ 2025-12-05 11:33 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon On Wed, Dec 03, 2025 at 01:46:11AM +0530, Siddharth Asthana wrote: > diff --git a/builtin/replay.c b/builtin/replay.c > index 6606a2c94b..7660f7412f 100644 > --- a/builtin/replay.c > +++ b/builtin/replay.c > @@ -77,9 +105,14 @@ static struct commit *create_commit(struct repository *repo, > > commit_list_insert(parent, &parents); > extra = read_commit_extra_headers(based_on, exclude_gpgsig); > - find_commit_subject(message, &orig_message); > - strbuf_addstr(&msg, orig_message); > - author = get_author(message); > + if (action == REPLAY_REVERT) { > + generate_revert_message(&msg, based_on, repo); > + author = xstrdup(git_author_info(IDENT_STRICT)); > + } else { > + find_commit_subject(message, &orig_message); > + strbuf_addstr(&msg, orig_message); > + author = get_author(message); > + } > reset_ident_date(); > if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, > &ret, author, NULL, sign_commit, extra)) { Do we want to be defensive in those if-chains and verify that `action == REPLAY_PICK` in the other case, and `BUG()` if it's not? > @@ -273,21 +322,39 @@ static struct commit *pick_regular_commit(struct repository *repo, > pickme_tree = repo_get_commit_tree(repo, pickme); > base_tree = repo_get_commit_tree(repo, base); > > - merge_opt->branch1 = short_commit_name(repo, replayed_base); > - merge_opt->branch2 = short_commit_name(repo, pickme); > - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); > + if (action == REPLAY_PICK) { > + /* Cherry-pick: normal order */ > + merge_opt->branch1 = short_commit_name(repo, replayed_base); > + merge_opt->branch2 = short_commit_name(repo, pickme); > + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); > > - merge_incore_nonrecursive(merge_opt, > - base_tree, > - result->tree, > - pickme_tree, > - result); > + merge_incore_nonrecursive(merge_opt, > + base_tree, > + result->tree, > + pickme_tree, > + result); > > - free((char*)merge_opt->ancestor); > + free((char *)merge_opt->ancestor); > + } else { > + /* Revert: swap base and pickme to reverse the diff */ > + const char *pickme_name = short_commit_name(repo, pickme); > + merge_opt->branch1 = short_commit_name(repo, replayed_base); > + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); > + merge_opt->ancestor = pickme_name; > + > + merge_incore_nonrecursive(merge_opt, > + pickme_tree, > + result->tree, > + base_tree, > + result); > + > + free((char *)merge_opt->branch2); > + } > merge_opt->ancestor = NULL; > + merge_opt->branch2 = NULL; We can `FREE_AND_NULL()` instead of manually unsetting these. > @@ -387,18 +460,28 @@ int cmd_replay(int argc, > argc = parse_options(argc, argv, prefix, replay_options, replay_usage, > PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); > > - if (!onto_name && !advance_name_opt) { > - error(_("option --onto or --advance is mandatory")); > + /* Exactly one mode must be specified */ > + if (!onto_name && !advance_name_opt && !revert_name_opt) { > + error(_("exactly one of --onto, --advance, or --revert is required")); > usage_with_options(replay_usage, replay_options); > } > > die_for_incompatible_opt2(!!advance_name_opt, "--advance", > - contained, "--contained"); > + !!onto_name, "--onto"); > + die_for_incompatible_opt2(!!revert_name_opt, "--revert", > + !!onto_name, "--onto"); > + die_for_incompatible_opt2(!!revert_name_opt, "--revert", > + !!advance_name_opt, "--advance"); > + die_for_incompatible_opt2(contained, "--contained", > + !onto_name, "requires --onto"); We have `die_for_incompatible_opt3()` that can be used here to check for mutual exclusivity of "--revert", "--advance" and "--onto". > @@ -508,7 +594,7 @@ int cmd_replay(int argc, > kh_value(replayed_commits, pos) = last_commit; > > /* Update any necessary branches */ > - if (advance_name) > + if (advance_name || revert_name) > continue; > decoration = get_name_decoration(&commit->object); > if (!decoration) > @@ -532,7 +618,7 @@ int cmd_replay(int argc, > } > } > > - /* In --advance mode, advance the target ref */ > + /* In --advance or --revert mode, update the target ref */ > if (result.clean == 1 && advance_name) { > if (handle_ref_update(ref_mode, transaction, advance_name, > &last_commit->object.oid, > @@ -544,6 +630,17 @@ int cmd_replay(int argc, > goto cleanup; > } > } > + if (result.clean == 1 && revert_name) { > + if (handle_ref_update(ref_mode, transaction, revert_name, > + &last_commit->object.oid, > + &onto->object.oid, > + reflog_msg.buf, > + &transaction_err) < 0) { > + ret = error(_("failed to update ref '%s': %s"), > + revert_name, transaction_err.buf); > + goto cleanup; > + } > + } This conditional and the one beforehand are the exact same, except that we use either `revert_name` or `advance_name`. Let's merge them: if (result.clean == 1 && (revert_name || advance_name)) { const char *ref = revert_name ? revert_name : advance_name; if (handle_ref_update(ref_mode, transaction, ref, &last_commit->object.oid, &onto->object.oid, reflog_msg.buf, &transaction_err) < 0) { ret = error(_("failed to update ref '%s': %s"), revert_name, transaction_err.buf); goto cleanup; } } Patrick ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes 2025-12-05 11:33 ` Patrick Steinhardt @ 2025-12-07 23:03 ` Siddharth Asthana 0 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2025-12-07 23:03 UTC (permalink / raw) To: Patrick Steinhardt Cc: git, christian.couder, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon On 05/12/25 17:03, Patrick Steinhardt wrote: > On Wed, Dec 03, 2025 at 01:46:11AM +0530, Siddharth Asthana wrote: >> diff --git a/builtin/replay.c b/builtin/replay.c >> index 6606a2c94b..7660f7412f 100644 >> --- a/builtin/replay.c >> +++ b/builtin/replay.c >> @@ -77,9 +105,14 @@ static struct commit *create_commit(struct repository *repo, >> >> commit_list_insert(parent, &parents); >> extra = read_commit_extra_headers(based_on, exclude_gpgsig); >> - find_commit_subject(message, &orig_message); >> - strbuf_addstr(&msg, orig_message); >> - author = get_author(message); >> + if (action == REPLAY_REVERT) { >> + generate_revert_message(&msg, based_on, repo); >> + author = xstrdup(git_author_info(IDENT_STRICT)); >> + } else { >> + find_commit_subject(message, &orig_message); >> + strbuf_addstr(&msg, orig_message); >> + author = get_author(message); >> + } >> reset_ident_date(); >> if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, >> &ret, author, NULL, sign_commit, extra)) { > Do we want to be defensive in those if-chains and verify that `action == > REPLAY_PICK` in the other case, and `BUG()` if it's not? Good idea. I will add BUG() for unexpected action values to catch any future additions that aren't properly handled. > >> @@ -273,21 +322,39 @@ static struct commit *pick_regular_commit(struct repository *repo, >> pickme_tree = repo_get_commit_tree(repo, pickme); >> base_tree = repo_get_commit_tree(repo, base); >> >> - merge_opt->branch1 = short_commit_name(repo, replayed_base); >> - merge_opt->branch2 = short_commit_name(repo, pickme); >> - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); >> + if (action == REPLAY_PICK) { >> + /* Cherry-pick: normal order */ >> + merge_opt->branch1 = short_commit_name(repo, replayed_base); >> + merge_opt->branch2 = short_commit_name(repo, pickme); >> + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); >> >> - merge_incore_nonrecursive(merge_opt, >> - base_tree, >> - result->tree, >> - pickme_tree, >> - result); >> + merge_incore_nonrecursive(merge_opt, >> + base_tree, >> + result->tree, >> + pickme_tree, >> + result); >> >> - free((char*)merge_opt->ancestor); >> + free((char *)merge_opt->ancestor); >> + } else { >> + /* Revert: swap base and pickme to reverse the diff */ >> + const char *pickme_name = short_commit_name(repo, pickme); >> + merge_opt->branch1 = short_commit_name(repo, replayed_base); >> + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); >> + merge_opt->ancestor = pickme_name; >> + >> + merge_incore_nonrecursive(merge_opt, >> + pickme_tree, >> + result->tree, >> + base_tree, >> + result); >> + >> + free((char *)merge_opt->branch2); >> + } >> merge_opt->ancestor = NULL; >> + merge_opt->branch2 = NULL; > We can `FREE_AND_NULL()` instead of manually unsetting these. Will use FREE_AND_NULL() - cleaner and clearer intent. > >> @@ -387,18 +460,28 @@ int cmd_replay(int argc, >> argc = parse_options(argc, argv, prefix, replay_options, replay_usage, >> PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); >> >> - if (!onto_name && !advance_name_opt) { >> - error(_("option --onto or --advance is mandatory")); >> + /* Exactly one mode must be specified */ >> + if (!onto_name && !advance_name_opt && !revert_name_opt) { >> + error(_("exactly one of --onto, --advance, or --revert is required")); >> usage_with_options(replay_usage, replay_options); >> } >> >> die_for_incompatible_opt2(!!advance_name_opt, "--advance", >> - contained, "--contained"); >> + !!onto_name, "--onto"); >> + die_for_incompatible_opt2(!!revert_name_opt, "--revert", >> + !!onto_name, "--onto"); >> + die_for_incompatible_opt2(!!revert_name_opt, "--revert", >> + !!advance_name_opt, "--advance"); >> + die_for_incompatible_opt2(contained, "--contained", >> + !onto_name, "requires --onto"); > We have `die_for_incompatible_opt3()` that can be used here to check for > mutual exclusivity of "--revert", "--advance" and "--onto". Nice, I wasn't aware of this helper. Will use die_for_incompatible_opt3() for the three-way mutual exclusivity check. > >> @@ -508,7 +594,7 @@ int cmd_replay(int argc, >> kh_value(replayed_commits, pos) = last_commit; >> >> /* Update any necessary branches */ >> - if (advance_name) >> + if (advance_name || revert_name) >> continue; >> decoration = get_name_decoration(&commit->object); >> if (!decoration) >> @@ -532,7 +618,7 @@ int cmd_replay(int argc, >> } >> } >> >> - /* In --advance mode, advance the target ref */ >> + /* In --advance or --revert mode, update the target ref */ >> if (result.clean == 1 && advance_name) { >> if (handle_ref_update(ref_mode, transaction, advance_name, >> &last_commit->object.oid, >> @@ -544,6 +630,17 @@ int cmd_replay(int argc, >> goto cleanup; >> } >> } >> + if (result.clean == 1 && revert_name) { >> + if (handle_ref_update(ref_mode, transaction, revert_name, >> + &last_commit->object.oid, >> + &onto->object.oid, >> + reflog_msg.buf, >> + &transaction_err) < 0) { >> + ret = error(_("failed to update ref '%s': %s"), >> + revert_name, transaction_err.buf); >> + goto cleanup; >> + } >> + } > This conditional and the one beforehand are the exact same, except that > we use either `revert_name` or `advance_name`. Let's merge them: > > if (result.clean == 1 && (revert_name || advance_name)) { > const char *ref = revert_name ? revert_name : advance_name; > if (handle_ref_update(ref_mode, transaction, ref, > &last_commit->object.oid, > &onto->object.oid, > reflog_msg.buf, > &transaction_err) < 0) { > ret = error(_("failed to update ref '%s': %s"), > revert_name, transaction_err.buf); > goto cleanup; > } > } Agreed, this is much cleaner. Will merge the conditionals as you suggest. Will incorporate all changes in v3. Thanks for the thorough review! Siddharth > > Patrick ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v2 2/2] replay: add --revert mode to reverse commit changes 2025-12-02 20:16 ` [PATCH v2 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2025-12-05 11:33 ` Patrick Steinhardt @ 2025-12-16 16:23 ` Phillip Wood 1 sibling, 0 replies; 92+ messages in thread From: Phillip Wood @ 2025-12-16 16:23 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, phillip.wood, karthik.188, johannes.schindelin, toon Hi Siddarth I agree with Patrick's comments, I've added a few more of my own below On 02/12/2025 20:16, Siddharth Asthana wrote: > The `git replay` command performs server-side history rewriting without > requiring a working tree. While it currently supports cherry-picking > commits (--advance) and rebasing (--onto), it lacks the ability to > revert them. > > At GitLab, we use replay in Gitaly for efficient server-side operations > on bare repositories. Adding revert functionality enables us to reverse > problematic commits directly on the server, eliminating client-side > roundtrips and reducing network overhead. > > Add a `--revert <branch>` mode that reverses the changes introduced by > the specified commits. Following the architecture of --onto and --advance, > --revert is a standalone mode that takes a branch argument and updates > that branch with the revert commits. s/revert/reverted/? > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc > index dcb26e8a8e..eb297c7530 100644 > --- a/Documentation/git-replay.adoc > +++ b/Documentation/git-replay.adoc > @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t > SYNOPSIS > -------- > [verse] > -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>... > +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... > > DESCRIPTION > ----------- > @@ -42,6 +42,24 @@ The history is replayed on top of the <branch> and <branch> is updated to > point at the tip of the resulting history. This is different from `--onto`, > which uses the target only as a starting point without updating it. > > +--revert <branch>:: > + Starting point at which to create the new revert commits; must be a > + branch name. > ++ > +When `--revert` is specified, the commits in the revision range are reverted > +(their changes are undone) and the revert commits s/revert/reverted/ > are applied on top of <branch>. > +The <branch> is then updated to point at the new commits. This is similar to > +running `git revert` for each commit in the range, but works without a working tree. "git revert" takes a revision range so it is the same as running "git revert <revision-range>" but does not update the working tree. > ++ > +The commit messages follow `git revert` conventions: prefixed with "Revert" and s/conventions: prefixed/conventions: they are prefixed/ > +including the original commit SHA. s/including/include/ s/SHA/hash/ > When reverting a commit whose message starts > +with "Revert", the new message uses "Reapply" instead. The author of the revert > +commits is the current user, not the original commit author. > ++ > +This option is mutually exclusive with `--onto` and `--advance`. It is also > +incompatible with `--contained` (which is a modifier for `--onto` only). > + > + > --ref-action[=<mode>]:: > Control how references are updated. The mode can be: > + > @@ -141,6 +159,22 @@ all commits they have since `base`, playing them on top of > `origin/main`. These three branches may have commits on top of `base` > that they have in common, but that does not need to be the case. > > +To revert commits from a branch: > + > +------------ > +$ git replay --revert main feature~2..feature > +------------ > + > +This reverts the last two commits from 'feature', creating revert commits on > +top of 'main', and updates 'main' to point at the result. This is useful when > +commits from 'feature' were previously merged or cherry-picked into 'main' and > +need to be undone. > + > +NOTE: For reverting an entire merge request as a single commit (rather than > +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` > +which can avoid unnecessary merge conflicts. That's a good suggestion > + > GIT > --- > Part of the linkgit:git[1] suite > diff --git a/builtin/replay.c b/builtin/replay.c > index 6606a2c94b..7660f7412f 100644 > --- a/builtin/replay.c > +++ b/builtin/replay.c > @@ -17,6 +17,7 @@ > #include "parse-options.h" > #include "refs.h" > #include "revision.h" > +#include "sequencer.h" > #include "strmap.h" > #include <oidset.h> > #include <tree.h> > @@ -26,6 +27,11 @@ enum ref_action_mode { > REF_ACTION_PRINT, > }; > > +enum replay_action { > + REPLAY_PICK, > + REPLAY_REVERT, > +}; sequencer.h already defines enum replay_action with an extra member so this is a bit confusing, maybe we should use a different name? > +static void generate_revert_message(struct strbuf *msg, > + struct commit *commit, > + struct repository *repo) > +{ > + const char *out_enc = get_commit_output_encoding(); > + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); > + const char *subject_start; > + int subject_len; > + char *subject; > + > + subject_len = find_commit_subject(message, &subject_start); > + subject = xmemdupz(subject_start, subject_len); > + > + sequencer_format_revert_header(msg, subject); > + strbuf_addstr(msg, oid_to_hex(&commit->object.oid)); > + strbuf_addstr(msg, ".\n"); It's a bit odd that sequencer_format_revert_header() actually adds the beginning of the body but we have to add the commit oid ourselves. It would be nicer if we could pass the oid to that function and have it format the message for us. It's a bit tricky because the sequencer needs to handle merges as well but it shouldn't be too difficult. The function name is also a bit strange as header normally refers to the commit metadata not the subject line. > @@ -77,9 +105,14 @@ static struct commit *create_commit(struct repository *repo, > > commit_list_insert(parent, &parents); > extra = read_commit_extra_headers(based_on, exclude_gpgsig); > - find_commit_subject(message, &orig_message); > - strbuf_addstr(&msg, orig_message); > - author = get_author(message); > + if (action == REPLAY_REVERT) { > + generate_revert_message(&msg, based_on, repo); > + author = xstrdup(git_author_info(IDENT_STRICT)); write_commit_tree() will look up the author for us if we just pass NULL so I would just set author = NULL here or delete this line and initialize author to NULL at the beginning of this function. > + } else { > + find_commit_subject(message, &orig_message); > + strbuf_addstr(&msg, orig_message); > + author = get_author(message); > + } This matches the deleted lines - good > @@ -196,6 +230,20 @@ static void determine_replay_mode(struct repository *repo, > } > if (rinfo.positive_refexprs > 1) > die(_("cannot advance target with multiple sources because ordering would be ill-defined")); > + } else if (*revert_name) { > + struct object_id oid; > + char *fullname = NULL; > + > + *onto = peel_committish(repo, *revert_name); > + if (repo_dwim_ref(repo, *revert_name, strlen(*revert_name), > + &oid, &fullname, 0) == 1) { > + free(*revert_name); > + *revert_name = fullname; > + } else { > + die(_("argument to --revert must be a reference")); > + } > + if (rinfo.positive_refexprs > 1) > + die(_("cannot revert with multiple sources because ordering would be ill-defined")); This is a copy of what we do with --advance but with a different option name - can be factor this out into a common function that's called for both options? > @@ -452,10 +535,13 @@ int cmd_replay(int argc, > } > > determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name, > + &revert_name, > &onto, &update_refs); Let's not fold the line after "revert_name" > diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh > index cf3aacf355..1c4e1cb666 100755 > --- a/t/t3650-replay-basics.sh > +++ b/t/t3650-replay-basics.sh > @@ -314,4 +314,115 @@ test_expect_success 'invalid replay.refAction value' ' > test_grep "invalid.*replay.refAction.*value" error > ' > > +test_expect_success 'setup for revert tests' ' > + git switch -c revert-test main && > + test_commit R1 && > + test_commit R2 && > + test_commit R3 && > + git switch main > +' Why do we need a new branch for this? We already have topic3 built on top of topic1 so we can test reverting commits with git replay --revert=topic3 main..topic1 which should revert C and F > +test_expect_success 'git replay --revert reverts commits' ' > + # Store original state > + START=$(git rev-parse revert-test) && > + test_when_finished "git branch -f revert-test $START" && > + > + git replay --revert revert-test revert-test~2..revert-test >output && > + test_must_be_empty output && > + > + # Verify revert-test was updated with revert commits > + git log --format=%s -n 5 revert-test >actual && > + cat >expect <<-\EOF && > + Revert "R3" > + Revert "R2" > + R3 > + R2 > + R1 > + EOF > + test_cmp expect actual && > + > + # Verify commit message format > + test_commit_message revert-test -m "Revert \"R3\" > + > +This reverts commit $(git rev-parse R3)." test_commit_message accepts the expected message on stdin so you can write test_commit_message revert-test <<-EOF Revert "R3" This reverts commit $(git rev-parse R3) EOF which is cleaner as we don't have to escape the double quotes and the message is nicely indented. > +' > + > +test_expect_success 'git replay --revert with --ref-action=print' ' Given the ref updating code is independent of --advance, --revert etc I'm not sure what extra coverage this test adds. If the previous test passes what is a plausible scenario where this one fails? > + # Store original state > + START=$(git rev-parse revert-test) && > + test_when_finished "git branch -f revert-test $START" && > + > + git replay --ref-action=print --revert revert-test revert-test~2..revert-test >result && > + test_line_count = 1 result && > + > + # Verify output format: update refs/heads/revert-test <new> <old> > + cut -f 3 -d " " result >new-tip && > + printf "update refs/heads/revert-test " >expect && > + printf "%s " $(cat new-tip) >>expect && > + printf "%s\n" $START >>expect && > + test_cmp expect result > +' > + > +test_expect_success 'git replay --revert reapply behavior' ' Good idea > + # Store original state > + START=$(git rev-parse revert-test) && > + test_when_finished "git branch -f revert-test $START" && > + > + # First revert R3 > + git replay --revert revert-test revert-test~1..revert-test && > + REVERT_R3=$(git rev-parse revert-test) && > + > + # Now revert the revert (should create "Reapply" message) > + git replay --revert revert-test revert-test~1..revert-test >output && > + test_must_be_empty output && > + > + # Verify Reapply message > + test_commit_message revert-test -m "Reapply \"R3\" > + > +This reverts commit $(git rev-parse $REVERT_R3)." > +' > + > +test_expect_success 'git replay --revert with conflict' ' > + # Create a conflicting scenario > + git switch -c revert-conflict main && > + test_commit C1 && > + echo conflict >C1.t && > + test_commit C2 C1.t && > + git switch main && > + echo different >C1.t && > + test_commit C3 C1.t && > + > + # Try to revert C2 onto main (which has conflicting C3) > + test_expect_code 1 git replay --revert main revert-conflict~1..revert-conflict > +' > + > +test_expect_success 'git replay --revert reflog message' ' I think we should just check the reflog message in one of the earlier tests. > + # Store original state > + START=$(git rev-parse revert-test) && > + test_when_finished "git branch -f revert-test $START" && > + > + git replay --revert revert-test revert-test~1..revert-test >output && > + test_must_be_empty output && > + > + # Verify reflog message includes --revert and branch name > + git reflog revert-test -1 --format=%gs >reflog-msg && > + echo "replay --revert revert-test" >expect-reflog && > + test_cmp expect-reflog reflog-msg > +' > + > +test_expect_success 'git replay --revert incompatible with --contained' ' > + test_must_fail git replay --revert revert-test --contained revert-test~1..revert-test 2>error && > + test_grep "requires --onto" error > +' > + > +test_expect_success 'git replay --revert incompatible with --onto' ' > + test_must_fail git replay --revert revert-test --onto main revert-test~1..revert-test 2>error && > + test_grep "cannot be used together" error > +' > + > +test_expect_success 'git replay --revert incompatible with --advance' ' > + test_must_fail git replay --revert revert-test --advance main revert-test~1..revert-test 2>error && > + test_grep "cannot be used together" error > +' These last three look good. Thanks Phillip ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v3 0/2] replay: add --revert mode to reverse commit changes 2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana 2025-12-02 20:16 ` [PATCH v2 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2025-12-02 20:16 ` [PATCH v2 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2026-02-18 23:42 ` Siddharth Asthana 2026-02-18 23:42 ` [PATCH v3 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana ` (2 more replies) 2 siblings, 3 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-02-18 23:42 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon, Siddharth Asthana The `git replay` command performs server-side history rewriting without requiring a working tree. While it currently supports cherry-picking commits (--advance) and rebasing (--onto), it lacks the ability to revert them. At GitLab, we use replay in Gitaly for efficient server-side operations on bare repositories. Adding revert functionality enables us to reverse problematic commits directly on the server, eliminating client-side roundtrips and reducing network overhead. The implementation follows the same approach as sequencer.c where cherry-pick and revert are the same merge operation but with swapped arguments. For cherry-pick we merge(ancestor=parent, ours=current, theirs=commit), while for revert we merge(ancestor=commit, ours=current, theirs=parent). By swapping the base and pickme trees when calling merge_incore_nonrecursive(), we effectively reverse the diff direction. The series is structured as follows: Patch 1 extracts the revert message formatting logic into a shared sequencer_format_revert_header() function, eliminating code duplication between sequencer.c and the upcoming replay code. This follows Junio's suggestion to split the changes. Patch 2 adds the --revert <branch> mode to git replay. Following the architectural pattern suggested by Elijah and Phillip, --revert is a standalone mode (like --onto and --advance) that takes a branch argument and updates that branch with the revert commits. The series is based on top of 864f55e190 (The second batch, 2026-02-07). CI: https://gitlab.com/gitlab-org/git/-/pipelines/2329880894 The Windows CI failures (t4041, t4059, t4060, t4205, t6006) are pre-existing infrastructure issues (missing iconv, submodule pathspec errors) unrelated to this series. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- Changes in v3: - Rebased on top of the latest upstream which refactored replay into a library (replay.c / replay.h). The --revert logic now lives in replay.c alongside the existing pick logic, while builtin/replay.c is a thin CLI wrapper. - sequencer_format_revert_header() now takes an optional oid parameter so it can handle the full commit reference internally, per Patrick's suggestion about refer_to_commit() - Removed now-unused `orig_subject` variable in do_pick_commit() - Switched to die_for_incompatible_opt3() for --onto/--advance/--revert mutual exclusivity, per Patrick - --contained now just checks "die(_("--contained requires --onto"))" instead of going through die_for_incompatible_opt2, per Phillip - Added BUG() guards for unhandled replay_mode values, per Patrick - Merged the separate advance/revert ref update blocks into one - author is set to NULL for revert commits so commit_tree_extended() picks up the current user, per Phillip - Factored out common --advance/--revert branch validation into a set_up_branch_mode() helper, per Phillip - Doc wording fixes: "reverted commits", "they are prefixed", "hash" instead of "SHA", per Phillip - Tests now reuse topic4 instead of creating new branches, use heredoc for test_commit_message, and the reflog check is folded into the main revert test - Added tests for bare repo revert, error cases (argument validation, multiple sources) - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/t/#u - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/t/#u --- Documentation/git-replay.adoc | 37 +++++++- builtin/replay.c | 25 ++++-- replay.c | 162 ++++++++++++++++++++++++---------- replay.h | 11 ++- sequencer.c | 47 ++++++---- sequencer.h | 11 +++ t/t3650-replay-basics.sh | 107 ++++++++++++++++++++-- 7 files changed, 319 insertions(+), 81 deletions(-) Siddharth Asthana (2): sequencer: extract revert message formatting into shared function replay: add --revert mode to reverse commit changes Range-diff versus v2: 1: bfd75484b4 ! 1: 9d686bcdfe sequencer: extract revert message formatting into shared function @@ Commit message Extract this logic into a new sequencer_format_revert_header() function that can be shared. The function handles both regular reverts ("Revert "<subject>"") and revert-of-revert cases ("Reapply "<subject>""). + When an oid is provided, the function appends the full commit hash and + period; otherwise the caller should append the commit reference. + Update do_pick_commit() to use the new helper, eliminating code duplication while preserving the special handling for commit_use_reference. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> ## sequencer.c ## +@@ sequencer.c: static int do_pick_commit(struct repository *r, + */ + + if (command == TODO_REVERT) { +- const char *orig_subject; +- + base = commit; + base_label = msg.label; + next = parent; @@ sequencer.c: static int do_pick_commit(struct repository *r, if (opts->commit_use_reference) { strbuf_commented_addf(&ctx->message, comment_line_str, @@ sequencer.c: static int do_pick_commit(struct repository *r, - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); -+ sequencer_format_revert_header(&ctx->message, msg.subject); ++ sequencer_format_revert_header(&ctx->message, msg.subject, NULL); } - strbuf_addstr(&ctx->message, "\nThis reverts commit "); refer_to_commit(opts, &ctx->message, commit); @@ sequencer.c: int sequencer_pick_revisions(struct repository *r, return res; } -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject) ++void sequencer_format_revert_header(struct strbuf *out, ++ const char *orig_subject, ++ const struct object_id *oid) +{ + const char *revert_subject; + @@ sequencer.c: int sequencer_pick_revisions(struct repository *r, + } + + strbuf_addstr(out, "\nThis reverts commit "); ++ if (oid) { ++ strbuf_addstr(out, oid_to_hex(oid)); ++ strbuf_addstr(out, ".\n"); ++ } +} + void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +/* -+ * Formats a revert commit message header following standard Git conventions. ++ * Formats a revert commit message following standard Git conventions. + * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert -+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit " at the end. -+ * The caller should append the commit OID after calling this function. ++ * cases ("Reapply \"<subject>\""). Adds "This reverts commit <oid>." if oid ++ * is provided, otherwise just adds "This reverts commit " and the caller ++ * should append the commit reference. + */ -+void sequencer_format_revert_header(struct strbuf *out, const char *orig_subject); ++void sequencer_format_revert_header(struct strbuf *out, ++ const char *orig_subject, ++ const struct object_id *oid); + #endif /* SEQUENCER_H */ 2: a2f99bc8c2 < -: ---------- replay: add --revert mode to reverse commit changes -: ---------- > 2: a8eae7b802 replay: add --revert mode to reverse commit changes base-commit: 864f55e1906897b630333675a52874c0fec2a45c Thanks - Siddharth ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v3 1/2] sequencer: extract revert message formatting into shared function 2026-02-18 23:42 ` [PATCH v3 0/2] " Siddharth Asthana @ 2026-02-18 23:42 ` Siddharth Asthana 2026-02-20 17:01 ` Toon Claes 2026-02-26 14:27 ` Phillip Wood 2026-02-18 23:42 ` [PATCH v3 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2026-03-13 5:40 ` [PATCH v4 0/2] " Siddharth Asthana 2 siblings, 2 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-02-18 23:42 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon, Siddharth Asthana The logic for formatting revert commit messages (handling "Revert" and "Reapply" cases) is currently duplicated between sequencer.c and will be needed by builtin/replay.c. Extract this logic into a new sequencer_format_revert_header() function that can be shared. The function handles both regular reverts ("Revert "<subject>"") and revert-of-revert cases ("Reapply "<subject>""). When an oid is provided, the function appends the full commit hash and period; otherwise the caller should append the commit reference. Update do_pick_commit() to use the new helper, eliminating code duplication while preserving the special handling for commit_use_reference. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- sequencer.c | 47 +++++++++++++++++++++++++++++++---------------- sequencer.h | 11 +++++++++++ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/sequencer.c b/sequencer.c index 1f492f8460..b32347c853 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2356,8 +2356,6 @@ static int do_pick_commit(struct repository *r, */ if (command == TODO_REVERT) { - const char *orig_subject; - base = commit; base_label = msg.label; next = parent; @@ -2365,22 +2363,10 @@ static int do_pick_commit(struct repository *r, if (opts->commit_use_reference) { strbuf_commented_addf(&ctx->message, comment_line_str, "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && - /* - * We don't touch pre-existing repeated reverts, because - * theoretically these can be nested arbitrarily deeply, - * thus requiring excessive complexity to deal with. - */ - !starts_with(orig_subject, "Revert \"")) { - strbuf_addstr(&ctx->message, "Reapply \""); - strbuf_addstr(&ctx->message, orig_subject); - strbuf_addstr(&ctx->message, "\n"); + strbuf_addstr(&ctx->message, "\nThis reverts commit "); } else { - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); + sequencer_format_revert_header(&ctx->message, msg.subject, NULL); } - strbuf_addstr(&ctx->message, "\nThis reverts commit "); refer_to_commit(opts, &ctx->message, commit); if (commit->parents && commit->parents->next) { @@ -5572,6 +5558,35 @@ int sequencer_pick_revisions(struct repository *r, return res; } +void sequencer_format_revert_header(struct strbuf *out, + const char *orig_subject, + const struct object_id *oid) +{ + const char *revert_subject; + + if (skip_prefix(orig_subject, "Revert \"", &revert_subject) && + /* + * We don't touch pre-existing repeated reverts, because + * theoretically these can be nested arbitrarily deeply, + * thus requiring excessive complexity to deal with. + */ + !starts_with(revert_subject, "Revert \"")) { + strbuf_addstr(out, "Reapply \""); + strbuf_addstr(out, revert_subject); + strbuf_addch(out, '\n'); + } else { + strbuf_addstr(out, "Revert \""); + strbuf_addstr(out, orig_subject); + strbuf_addstr(out, "\"\n"); + } + + strbuf_addstr(out, "\nThis reverts commit "); + if (oid) { + strbuf_addstr(out, oid_to_hex(oid)); + strbuf_addstr(out, ".\n"); + } +} + void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) { unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP; diff --git a/sequencer.h b/sequencer.h index 719684c8a9..b7291cc52d 100644 --- a/sequencer.h +++ b/sequencer.h @@ -271,4 +271,15 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) */ int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +/* + * Formats a revert commit message following standard Git conventions. + * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert + * cases ("Reapply \"<subject>\""). Adds "This reverts commit <oid>." if oid + * is provided, otherwise just adds "This reverts commit " and the caller + * should append the commit reference. + */ +void sequencer_format_revert_header(struct strbuf *out, + const char *orig_subject, + const struct object_id *oid); + #endif /* SEQUENCER_H */ -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function 2026-02-18 23:42 ` [PATCH v3 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana @ 2026-02-20 17:01 ` Toon Claes 2026-02-25 21:53 ` Junio C Hamano 2026-03-06 4:31 ` Siddharth Asthana 2026-02-26 14:27 ` Phillip Wood 1 sibling, 2 replies; 92+ messages in thread From: Toon Claes @ 2026-02-20 17:01 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, Siddharth Asthana Siddharth Asthana <siddharthasthana31@gmail.com> writes: > The logic for formatting revert commit messages (handling "Revert" and > "Reapply" cases) is currently duplicated between sequencer.c and will be > needed by builtin/replay.c. > > Extract this logic into a new sequencer_format_revert_header() function > that can be shared. The function handles both regular reverts ("Revert > "<subject>"") and revert-of-revert cases ("Reapply "<subject>""). > When an oid is provided, the function appends the full commit hash and > period; otherwise the caller should append the commit reference. > > Update do_pick_commit() to use the new helper, eliminating code > duplication while preserving the special handling for commit_use_reference. > > Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> > --- > sequencer.c | 47 +++++++++++++++++++++++++++++++---------------- > sequencer.h | 11 +++++++++++ > 2 files changed, 42 insertions(+), 16 deletions(-) > > diff --git a/sequencer.c b/sequencer.c > index 1f492f8460..b32347c853 100644 > --- a/sequencer.c > +++ b/sequencer.c > @@ -2356,8 +2356,6 @@ static int do_pick_commit(struct repository *r, > */ > > if (command == TODO_REVERT) { > - const char *orig_subject; > - > base = commit; > base_label = msg.label; > next = parent; > @@ -2365,22 +2363,10 @@ static int do_pick_commit(struct repository *r, > if (opts->commit_use_reference) { > strbuf_commented_addf(&ctx->message, comment_line_str, > "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); > - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && > - /* > - * We don't touch pre-existing repeated reverts, because > - * theoretically these can be nested arbitrarily deeply, > - * thus requiring excessive complexity to deal with. > - */ > - !starts_with(orig_subject, "Revert \"")) { > - strbuf_addstr(&ctx->message, "Reapply \""); > - strbuf_addstr(&ctx->message, orig_subject); > - strbuf_addstr(&ctx->message, "\n"); > + strbuf_addstr(&ctx->message, "\nThis reverts commit "); > } else { > - strbuf_addstr(&ctx->message, "Revert \""); > - strbuf_addstr(&ctx->message, msg.subject); > - strbuf_addstr(&ctx->message, "\"\n"); > + sequencer_format_revert_header(&ctx->message, msg.subject, NULL); > } > - strbuf_addstr(&ctx->message, "\nThis reverts commit "); > refer_to_commit(opts, &ctx->message, commit); I still find it somewhat confusing we have some the code that deals with `opts->commit_use_reference` partly in here and partly in sequencer_format_revert_header(). Part of the confusion comes from sequencer_format_revert_header() being called with NULL for the commit OID. Was is not possible to incorporate Patrick's suggestion[1]? [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/ -- Cheers, Toon ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function 2026-02-20 17:01 ` Toon Claes @ 2026-02-25 21:53 ` Junio C Hamano 2026-03-06 4:55 ` Siddharth Asthana 2026-03-06 4:31 ` Siddharth Asthana 1 sibling, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2026-02-25 21:53 UTC (permalink / raw) To: Toon Claes Cc: Siddharth Asthana, git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin Toon Claes <toon@iotcl.com> writes: >> - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && >> - /* >> - * We don't touch pre-existing repeated reverts, because >> - * theoretically these can be nested arbitrarily deeply, >> - * thus requiring excessive complexity to deal with. >> - */ >> - !starts_with(orig_subject, "Revert \"")) { >> - strbuf_addstr(&ctx->message, "Reapply \""); >> - strbuf_addstr(&ctx->message, orig_subject); >> - strbuf_addstr(&ctx->message, "\n"); >> + strbuf_addstr(&ctx->message, "\nThis reverts commit "); >> } else { >> - strbuf_addstr(&ctx->message, "Revert \""); >> - strbuf_addstr(&ctx->message, msg.subject); >> - strbuf_addstr(&ctx->message, "\"\n"); >> + sequencer_format_revert_header(&ctx->message, msg.subject, NULL); >> } >> - strbuf_addstr(&ctx->message, "\nThis reverts commit "); >> refer_to_commit(opts, &ctx->message, commit); > > I still find it somewhat confusing we have some the code that deals with > `opts->commit_use_reference` partly in here and partly in > sequencer_format_revert_header(). True. Making sure plumbing commands are unaffected by random end-user configuration is a good thing, but I am not sure if this command is truly a plumbing. > Part of the confusion comes from sequencer_format_revert_header() being > called with NULL for the commit OID. > > Was is not possible to incorporate Patrick's suggestion[1]? > > [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/ ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function 2026-02-25 21:53 ` Junio C Hamano @ 2026-03-06 4:55 ` Siddharth Asthana 0 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-06 4:55 UTC (permalink / raw) To: Junio C Hamano, Toon Claes Cc: git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin On 26/02/26 03:23, Junio C Hamano wrote: > Toon Claes <toon@iotcl.com> writes: > >>> - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && >>> - /* >>> - * We don't touch pre-existing repeated reverts, because >>> - * theoretically these can be nested arbitrarily deeply, >>> - * thus requiring excessive complexity to deal with. >>> - */ >>> - !starts_with(orig_subject, "Revert \"")) { >>> - strbuf_addstr(&ctx->message, "Reapply \""); >>> - strbuf_addstr(&ctx->message, orig_subject); >>> - strbuf_addstr(&ctx->message, "\n"); >>> + strbuf_addstr(&ctx->message, "\nThis reverts commit "); >>> } else { >>> - strbuf_addstr(&ctx->message, "Revert \""); >>> - strbuf_addstr(&ctx->message, msg.subject); >>> - strbuf_addstr(&ctx->message, "\"\n"); >>> + sequencer_format_revert_header(&ctx->message, msg.subject, NULL); >>> } >>> - strbuf_addstr(&ctx->message, "\nThis reverts commit "); >>> refer_to_commit(opts, &ctx->message, commit); >> >> I still find it somewhat confusing we have some the code that deals with >> `opts->commit_use_reference` partly in here and partly in >> sequencer_format_revert_header(). > > True. Making sure plumbing commands are unaffected by random > end-user configuration is a good thing, but I am not sure if this > command is truly a plumbing. With Phillip's sequencer_format_revert_message() approach, replay just passes use_commit_reference=false and gets the full OID path. The split logic goes away, so this concern is resolved regardless of how we classify replay. > >> Part of the confusion comes from sequencer_format_revert_header() being >> called with NULL for the commit OID. >> >> Was is not possible to incorporate Patrick's suggestion[1]? >> >> [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/ ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function 2026-02-20 17:01 ` Toon Claes 2026-02-25 21:53 ` Junio C Hamano @ 2026-03-06 4:31 ` Siddharth Asthana 1 sibling, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-06 4:31 UTC (permalink / raw) To: Toon Claes, git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin On 20/02/26 22:31, Toon Claes wrote: > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > >> The logic for formatting revert commit messages (handling "Revert" and >> "Reapply" cases) is currently duplicated between sequencer.c and will be >> needed by builtin/replay.c. >> >> Extract this logic into a new sequencer_format_revert_header() function >> that can be shared. The function handles both regular reverts ("Revert >> "<subject>"") and revert-of-revert cases ("Reapply "<subject>""). >> When an oid is provided, the function appends the full commit hash and >> period; otherwise the caller should append the commit reference. >> >> Update do_pick_commit() to use the new helper, eliminating code >> duplication while preserving the special handling for commit_use_reference. >> >> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> >> --- >> sequencer.c | 47 +++++++++++++++++++++++++++++++---------------- >> sequencer.h | 11 +++++++++++ >> 2 files changed, 42 insertions(+), 16 deletions(-) >> >> diff --git a/sequencer.c b/sequencer.c >> index 1f492f8460..b32347c853 100644 >> --- a/sequencer.c >> +++ b/sequencer.c >> @@ -2356,8 +2356,6 @@ static int do_pick_commit(struct repository *r, >> */ >> >> if (command == TODO_REVERT) { >> - const char *orig_subject; >> - >> base = commit; >> base_label = msg.label; >> next = parent; >> @@ -2365,22 +2363,10 @@ static int do_pick_commit(struct repository *r, >> if (opts->commit_use_reference) { >> strbuf_commented_addf(&ctx->message, comment_line_str, >> "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); >> - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && >> - /* >> - * We don't touch pre-existing repeated reverts, because >> - * theoretically these can be nested arbitrarily deeply, >> - * thus requiring excessive complexity to deal with. >> - */ >> - !starts_with(orig_subject, "Revert \"")) { >> - strbuf_addstr(&ctx->message, "Reapply \""); >> - strbuf_addstr(&ctx->message, orig_subject); >> - strbuf_addstr(&ctx->message, "\n"); >> + strbuf_addstr(&ctx->message, "\nThis reverts commit "); >> } else { >> - strbuf_addstr(&ctx->message, "Revert \""); >> - strbuf_addstr(&ctx->message, msg.subject); >> - strbuf_addstr(&ctx->message, "\"\n"); >> + sequencer_format_revert_header(&ctx->message, msg.subject, NULL); >> } >> - strbuf_addstr(&ctx->message, "\nThis reverts commit "); >> refer_to_commit(opts, &ctx->message, commit); > > I still find it somewhat confusing we have some the code that deals with > `opts->commit_use_reference` partly in here and partly in > sequencer_format_revert_header(). > > Part of the confusion comes from sequencer_format_revert_header() being > called with NULL for the commit OID. > > Was is not possible to incorporate Patrick's suggestion[1]? You're right, the split is awkward. I tried to keep sequencer_format_revert_header() minimal so it didn't pull in replay_opts or refer_to_commit(), but the NULL oid path is confusing. Phillip posted a cleaner approach in his reply to this patch -- he moves everything (title, body, refer_to_commit, merge-parent handling) into one sequencer_format_revert_message() with a bool use_commit_reference. That eliminates the NULL oid entirely and addresses Patrick's suggestion at the same time. I will go with that for v4. > > [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/ > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function 2026-02-18 23:42 ` [PATCH v3 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-02-20 17:01 ` Toon Claes @ 2026-02-26 14:27 ` Phillip Wood 2026-03-06 5:00 ` Siddharth Asthana 1 sibling, 1 reply; 92+ messages in thread From: Phillip Wood @ 2026-02-26 14:27 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, phillip.wood, karthik.188, johannes.schindelin, toon Hi Siddharth On 18/02/2026 23:42, Siddharth Asthana wrote: > The logic for formatting revert commit messages (handling "Revert" and > "Reapply" cases) is currently duplicated between sequencer.c and will be > needed by builtin/replay.c. > > Extract this logic into a new sequencer_format_revert_header() function > that can be shared. The function handles both regular reverts ("Revert > "<subject>"") and revert-of-revert cases ("Reapply "<subject>""). > When an oid is provided, the function appends the full commit hash and > period; otherwise the caller should append the commit reference. > > Update do_pick_commit() to use the new helper, eliminating code > duplication while preserving the special handling for commit_use_reference. I agree with the other comments that this ends up being a bit awkward, I think something like the diff below which moves all of the revert message formatting into a helper function would be a better approach. Note that I've also added a repository argument to refer_to_commit(). You might want to do that in a separate commit, but I think it is worth doing if we're adding more callers. I've also just used a bool for the use_commit_reference flag, if we want to add more flags in the future we can convert it to an unsigned int when we do that. Thanks Phillip ---- 8< ---- diff --git a/sequencer.c b/sequencer.c index a3eb39bb252..30f6da6f959 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2198,21 +2198,55 @@ static int should_edit(struct replay_opts *opts) { return opts->edit; } -static void refer_to_commit(struct replay_opts *opts, - struct strbuf *msgbuf, struct commit *commit) +static void refer_to_commit(struct repository*r, struct strbuf *msgbuf, + const struct commit *commit, bool use_commit_reference) { - if (opts->commit_use_reference) { + if (use_commit_reference) { struct pretty_print_context ctx = { .abbrev = DEFAULT_ABBREV, .date_mode.type = DATE_SHORT, }; - repo_format_commit_message(the_repository, commit, + repo_format_commit_message(r, commit, "%h (%s, %ad)", msgbuf, &ctx); } else { strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid)); } } +void sequencer_format_revert_message(struct repository *r, const char *subject, + const struct commit *commit, const struct commit *parent, + bool use_commit_reference, struct strbuf *message) +{ + const char *orig_subject; + + if (use_commit_reference) { + strbuf_commented_addf(message, comment_line_str, + "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); + } else if (skip_prefix(subject, "Revert \"", &orig_subject) && + /* + * We don't touch pre-existing repeated reverts, because + * theoretically these can be nested arbitrarily deeply, + * thus requiring excessive complexity to deal with. + */ + !starts_with(orig_subject, "Revert \"")) { + strbuf_addstr(message, "Reapply \""); + strbuf_addstr(message, orig_subject); + strbuf_addstr(message, "\n"); + } else { + strbuf_addstr(message, "Revert \""); + strbuf_addstr(message, subject); + strbuf_addstr(message, "\"\n"); + } + strbuf_addstr(message, "\nThis reverts commit "); + refer_to_commit(r, message, commit, use_commit_reference); + + if (commit->parents && commit->parents->next) { + strbuf_addstr(message, ", reversing\nchanges made to "); + refer_to_commit(r, message, parent, use_commit_reference); + } + strbuf_addstr(message, ".\n"); +} + static const char *sequencer_reflog_action(struct replay_opts *opts) { if (!opts->reflog_action) { @@ -2356,38 +2390,13 @@ static int do_pick_commit(struct repository *r, */ if (command == TODO_REVERT) { - const char *orig_subject; - base = commit; base_label = msg.label; next = parent; next_label = msg.parent_label; - if (opts->commit_use_reference) { - strbuf_commented_addf(&ctx->message, comment_line_str, - "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && - /* - * We don't touch pre-existing repeated reverts, because - * theoretically these can be nested arbitrarily deeply, - * thus requiring excessive complexity to deal with. - */ - !starts_with(orig_subject, "Revert \"")) { - strbuf_addstr(&ctx->message, "Reapply \""); - strbuf_addstr(&ctx->message, orig_subject); - strbuf_addstr(&ctx->message, "\n"); - } else { - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); - } - strbuf_addstr(&ctx->message, "\nThis reverts commit "); - refer_to_commit(opts, &ctx->message, commit); - - if (commit->parents && commit->parents->next) { - strbuf_addstr(&ctx->message, ", reversing\nchanges made to "); - refer_to_commit(opts, &ctx->message, parent); - } - strbuf_addstr(&ctx->message, ".\n"); + sequencer_format_revert_message(r,msg.subject, commit, parent, + opts->commit_use_reference, + &ctx->message); } else { const char *p; diff --git a/sequencer.h b/sequencer.h index 719684c8a9f..a61ec6d81d4 100644 --- a/sequencer.h +++ b/sequencer.h @@ -271,4 +271,8 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) */ int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +void sequencer_format_revert_message(struct repository *r, const char *subject, + const struct commit *commit, const struct commit *parent, + bool use_commit_reference, struct strbuf *message); + #endif /* SEQUENCER_H */ ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH v3 1/2] sequencer: extract revert message formatting into shared function 2026-02-26 14:27 ` Phillip Wood @ 2026-03-06 5:00 ` Siddharth Asthana 0 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-06 5:00 UTC (permalink / raw) To: phillip.wood, git Cc: christian.couder, ps, newren, gitster, karthik.188, johannes.schindelin, toon On 26/02/26 19:57, Phillip Wood wrote: > Hi Siddharth > > On 18/02/2026 23:42, Siddharth Asthana wrote: >> The logic for formatting revert commit messages (handling "Revert" and >> "Reapply" cases) is currently duplicated between sequencer.c and will be >> needed by builtin/replay.c. >> >> Extract this logic into a new sequencer_format_revert_header() function >> that can be shared. The function handles both regular reverts ("Revert >> "<subject>"") and revert-of-revert cases ("Reapply "<subject>""). >> When an oid is provided, the function appends the full commit hash and >> period; otherwise the caller should append the commit reference. >> >> Update do_pick_commit() to use the new helper, eliminating code >> duplication while preserving the special handling for >> commit_use_reference. > > I agree with the other comments that this ends up being a bit awkward, I > think > something like the diff below which moves all of the revert message > formatting > into a helper function would be a better approach. Note that I've also > added a > repository argument to refer_to_commit(). You might want to do that in a > separate commit, but I think it is worth doing if we're adding more > callers. > I've also just used a bool for the use_commit_reference flag, if we want > to add > more flags in the future we can convert it to an unsigned int when we do > that. Thanks, this is much cleaner. Moving refer_to_commit() and the merge-parent handling into the same function gets rid of the awkward NULL oid path that Toon and Junio pointed out. I will split the refer_to_commit() signature change (adding struct repository *r) into a preparatory commit as you suggested, then have the second commit introduce the full sequencer_format_revert_message() helper. For replay, I will call it with use_commit_reference=false -- that gives the full OID through refer_to_commit() directly, no special causing needed. > > Thanks > > Phillip > > > ---- 8< ---- > diff --git a/sequencer.c b/sequencer.c > index a3eb39bb252..30f6da6f959 100644 > --- a/sequencer.c > +++ b/sequencer.c > @@ -2198,21 +2198,55 @@ static int should_edit(struct replay_opts *opts) { > return opts->edit; > } > > -static void refer_to_commit(struct replay_opts *opts, > - struct strbuf *msgbuf, struct commit *commit) > +static void refer_to_commit(struct repository*r, struct strbuf *msgbuf, > + const struct commit *commit, bool use_commit_reference) > { > - if (opts->commit_use_reference) { > + if (use_commit_reference) { > struct pretty_print_context ctx = { > .abbrev = DEFAULT_ABBREV, > .date_mode.type = DATE_SHORT, > }; > - repo_format_commit_message(the_repository, commit, > + repo_format_commit_message(r, commit, > "%h (%s, %ad)", msgbuf, &ctx); > } else { > strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid)); > } > } > > +void sequencer_format_revert_message(struct repository *r, const char > *subject, > + const struct commit *commit, const struct commit *parent, > + bool use_commit_reference, struct strbuf *message) > +{ > + const char *orig_subject; > + > + if (use_commit_reference) { > + strbuf_commented_addf(message, comment_line_str, > + "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); > + } else if (skip_prefix(subject, "Revert \"", &orig_subject) && > + /* > + * We don't touch pre-existing repeated reverts, because > + * theoretically these can be nested arbitrarily deeply, > + * thus requiring excessive complexity to deal with. > + */ > + !starts_with(orig_subject, "Revert \"")) { > + strbuf_addstr(message, "Reapply \""); > + strbuf_addstr(message, orig_subject); > + strbuf_addstr(message, "\n"); > + } else { > + strbuf_addstr(message, "Revert \""); > + strbuf_addstr(message, subject); > + strbuf_addstr(message, "\"\n"); > + } > + strbuf_addstr(message, "\nThis reverts commit "); > + refer_to_commit(r, message, commit, use_commit_reference); > + > + if (commit->parents && commit->parents->next) { > + strbuf_addstr(message, ", reversing\nchanges made to "); > + refer_to_commit(r, message, parent, use_commit_reference); > + } > + strbuf_addstr(message, ".\n"); > +} > + > static const char *sequencer_reflog_action(struct replay_opts *opts) > { > if (!opts->reflog_action) { > @@ -2356,38 +2390,13 @@ static int do_pick_commit(struct repository *r, > */ > > if (command == TODO_REVERT) { > - const char *orig_subject; > - > base = commit; > base_label = msg.label; > next = parent; > next_label = msg.parent_label; > - if (opts->commit_use_reference) { > - strbuf_commented_addf(&ctx->message, comment_line_str, > - "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); > - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && > - /* > - * We don't touch pre-existing repeated reverts, because > - * theoretically these can be nested arbitrarily deeply, > - * thus requiring excessive complexity to deal with. > - */ > - !starts_with(orig_subject, "Revert \"")) { > - strbuf_addstr(&ctx->message, "Reapply \""); > - strbuf_addstr(&ctx->message, orig_subject); > - strbuf_addstr(&ctx->message, "\n"); > - } else { > - strbuf_addstr(&ctx->message, "Revert \""); > - strbuf_addstr(&ctx->message, msg.subject); > - strbuf_addstr(&ctx->message, "\"\n"); > - } > - strbuf_addstr(&ctx->message, "\nThis reverts commit "); > - refer_to_commit(opts, &ctx->message, commit); > - > - if (commit->parents && commit->parents->next) { > - strbuf_addstr(&ctx->message, ", reversing\nchanges made to "); > - refer_to_commit(opts, &ctx->message, parent); > - } > - strbuf_addstr(&ctx->message, ".\n"); > + sequencer_format_revert_message(r,msg.subject, commit, parent, > + opts->commit_use_reference, > + &ctx->message); > } else { > const char *p; > > diff --git a/sequencer.h b/sequencer.h > index 719684c8a9f..a61ec6d81d4 100644 > --- a/sequencer.h > +++ b/sequencer.h > @@ -271,4 +271,8 @@ int sequencer_determine_whence(struct repository *r, > enum commit_whence *whence) > */ > int sequencer_get_update_refs_state(const char *wt_dir, struct > string_list *refs); > > +void sequencer_format_revert_message(struct repository *r, const char > *subject, > + const struct commit *commit, const struct commit > *parent, > + bool use_commit_reference, struct strbuf *message); > + > #endif /* SEQUENCER_H */ > ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-02-18 23:42 ` [PATCH v3 0/2] " Siddharth Asthana 2026-02-18 23:42 ` [PATCH v3 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana @ 2026-02-18 23:42 ` Siddharth Asthana 2026-02-20 17:35 ` Toon Claes 2026-02-26 14:45 ` Phillip Wood 2026-03-13 5:40 ` [PATCH v4 0/2] " Siddharth Asthana 2 siblings, 2 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-02-18 23:42 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, toon, Siddharth Asthana, Johannes Schindelin Add a `--revert <branch>` mode to git replay that undoes the changes introduced by the specified commits. Like --onto and --advance, --revert is a standalone mode: it takes a branch argument and updates that branch with the newly created revert commits. At GitLab, we need this in Gitaly for reverting commits directly on bare repositories without requiring a working tree checkout. The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick and revert are just the same three-way merge with swapped arguments: - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) - Revert: merge(ancestor=commit, ours=current, theirs=parent) We swap the base and pickme trees passed to merge_incore_nonrecursive() to reverse the diff direction. Revert commit messages follow the usual git revert conventions: prefixed with "Revert" (or "Reapply" when reverting a revert), and including "This reverts commit <hash>.". The author is set to the current user rather than preserving the original author, matching git revert behavior. Helped-by: Christian Couder <christian.couder@gmail.com> Helped-by: Patrick Steinhardt <ps@pks.im> Helped-by: Elijah Newren <newren@gmail.com> Helped-by: Phillip Wood <phillip.wood123@gmail.com> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Helped-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- Documentation/git-replay.adoc | 37 +++++++- builtin/replay.c | 25 ++++-- replay.c | 162 ++++++++++++++++++++++++---------- replay.h | 11 ++- t/t3650-replay-basics.sh | 107 ++++++++++++++++++++-- 5 files changed, 277 insertions(+), 65 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index 8d696ce3ab..ffdf790278 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t SYNOPSIS -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... DESCRIPTION ----------- @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to point at the tip of the resulting history. This is different from `--onto`, which uses the target only as a starting point without updating it. +--revert <branch>:: + Starting point at which to create the reverted commits; must be a + branch name. ++ +When `--revert` is specified, the commits in the revision range are reverted +(their changes are undone) and the reverted commits are created on top of +<branch>. The <branch> is then updated to point at the new commits. This is +the same as running `git revert <revision-range>` but does not update the +working tree. ++ +The commit messages follow `git revert` conventions: they are prefixed with +"Revert" and include "This reverts commit <hash>." When reverting a commit +whose message starts with "Revert", the new message uses "Reapply" instead. +Unlike cherry-pick which preserves the original author, revert commits use +the current user as the author, matching the behavior of `git revert`. ++ +This option is mutually exclusive with `--onto` and `--advance`. It is also +incompatible with `--contained` (which is a modifier for `--onto` only). + --contained:: Update all branches that point at commits in <revision-range>. Requires `--onto`. @@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH} where the number of refs updated depends on the arguments passed and -the shape of the history being replayed. When using `--advance`, the -number of refs updated is always one, but for `--onto`, it can be one -or more (rebasing multiple branches simultaneously is supported). +the shape of the history being replayed. When using `--advance` or +`--revert`, the number of refs updated is always one, but for `--onto`, +it can be one or more (rebasing multiple branches simultaneously is +supported). There is no stderr output on conflicts; see the <<exit-status,EXIT STATUS>> section below. @@ -152,6 +172,15 @@ all commits they have since `base`, playing them on top of `origin/main`. These three branches may have commits on top of `base` that they have in common, but that does not need to be the case. +To revert commits on a branch: + +------------ +$ git replay --revert main main~2..main +------------ + +This reverts the last two commits on `main`, creating two revert commits +on top of `main`, and updates `main` to point at the result. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/replay.c b/builtin/replay.c index 2cdde830a8..28ce5196db 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -83,8 +83,8 @@ int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto <newbase> | --advance <branch>) " - "[--ref-action[=<mode>]] <revision-range>"), + "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) " + "[--ref-action[=<mode>]] <revision-range>..."), NULL }; struct option replay_options[] = { @@ -96,6 +96,9 @@ int cmd_replay(int argc, N_("replay onto given commit")), OPT_BOOL(0, "contained", &opts.contained, N_("update all branches that point at commits in <revision-range>")), + OPT_STRING(0, "revert", &opts.revert, + N_("branch"), + N_("revert commits onto given branch")), OPT_STRING(0, "ref-action", &ref_action, N_("mode"), N_("control ref update behavior (update|print)")), @@ -105,15 +108,17 @@ int cmd_replay(int argc, argc = parse_options(argc, argv, prefix, replay_options, replay_usage, PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); - if (!opts.onto && !opts.advance) { - error(_("option --onto or --advance is mandatory")); + /* Exactly one mode must be specified */ + if (!opts.onto && !opts.advance && !opts.revert) { + error(_("exactly one of --onto, --advance, or --revert is required")); usage_with_options(replay_usage, replay_options); } - die_for_incompatible_opt2(!!opts.advance, "--advance", - opts.contained, "--contained"); - die_for_incompatible_opt2(!!opts.advance, "--advance", - !!opts.onto, "--onto"); + die_for_incompatible_opt3(!!opts.onto, "--onto", + !!opts.advance, "--advance", + !!opts.revert, "--revert"); + if (opts.contained && !opts.onto) + die(_("--contained requires --onto")); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); @@ -174,7 +179,9 @@ int cmd_replay(int argc, goto cleanup; /* Build reflog message */ - if (opts.advance) { + if (opts.revert) { + strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert); + } else if (opts.advance) { strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance); } else { struct object_id oid; diff --git a/replay.c b/replay.c index f97d652f33..6f8b5720b3 100644 --- a/replay.c +++ b/replay.c @@ -8,9 +8,15 @@ #include "refs.h" #include "replay.h" #include "revision.h" +#include "sequencer.h" #include "strmap.h" #include "tree.h" +enum replay_mode { + REPLAY_MODE_PICK, + REPLAY_MODE_REVERT, +}; + static const char *short_commit_name(struct repository *repo, struct commit *commit) { @@ -44,15 +50,35 @@ static char *get_author(const char *message) return NULL; } +static void generate_revert_message(struct strbuf *msg, + struct commit *commit, + struct repository *repo) +{ + const char *out_enc = get_commit_output_encoding(); + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); + const char *subject_start; + int subject_len; + char *subject; + + subject_len = find_commit_subject(message, &subject_start); + subject = xmemdupz(subject_start, subject_len); + + sequencer_format_revert_header(msg, subject, &commit->object.oid); + + free(subject); + repo_unuse_commit_buffer(repo, commit, message); +} + static struct commit *create_commit(struct repository *repo, struct tree *tree, struct commit *based_on, - struct commit *parent) + struct commit *parent, + enum replay_mode mode) { struct object_id ret; struct object *obj = NULL; struct commit_list *parents = NULL; - char *author; + char *author = NULL; char *sign_commit = NULL; /* FIXME: cli users might want to sign again */ struct commit_extra_header *extra = NULL; struct strbuf msg = STRBUF_INIT; @@ -64,9 +90,16 @@ static struct commit *create_commit(struct repository *repo, commit_list_insert(parent, &parents); extra = read_commit_extra_headers(based_on, exclude_gpgsig); - find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); + if (mode == REPLAY_MODE_REVERT) { + generate_revert_message(&msg, based_on, repo); + /* For revert, use current user as author (NULL = use default) */ + } else if (mode == REPLAY_MODE_PICK) { + find_commit_subject(message, &orig_message); + strbuf_addstr(&msg, orig_message); + author = get_author(message); + } else { + BUG("unexpected replay mode %d", mode); + } reset_ident_date(); if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, &ret, author, NULL, sign_commit, extra)) { @@ -147,11 +180,34 @@ static void get_ref_information(struct repository *repo, } } +static void set_up_branch_mode(struct repository *repo, + char **branch_name, + const char *option_name, + struct ref_info *rinfo, + struct commit **onto) +{ + struct object_id oid; + char *fullname = NULL; + + if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name), + &oid, &fullname, 0) == 1) { + free(*branch_name); + *branch_name = fullname; + } else { + die(_("argument to %s must be a reference"), option_name); + } + *onto = peel_committish(repo, *branch_name, option_name); + if (rinfo->positive_refexprs > 1) + die(_("cannot %s target with multiple sources because ordering would be ill-defined"), + option_name + 2); /* skip "--" prefix */ +} + static void set_up_replay_mode(struct repository *repo, struct rev_cmdline_info *cmd_info, const char *onto_name, bool *detached_head, char **advance_name, + char **revert_name, struct commit **onto, struct strset **update_refs) { @@ -166,9 +222,6 @@ static void set_up_replay_mode(struct repository *repo, if (!rinfo.positive_refexprs) die(_("need some commits to replay")); - if (!onto_name == !*advance_name) - BUG("one and only one of onto_name and *advance_name must be given"); - if (onto_name) { *onto = peel_committish(repo, onto_name, "--onto"); if (rinfo.positive_refexprs < @@ -177,23 +230,12 @@ static void set_up_replay_mode(struct repository *repo, *update_refs = xcalloc(1, sizeof(**update_refs)); **update_refs = rinfo.positive_refs; memset(&rinfo.positive_refs, 0, sizeof(**update_refs)); + } else if (*advance_name) { + set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto); + } else if (*revert_name) { + set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto); } else { - struct object_id oid; - char *fullname = NULL; - - if (!*advance_name) - BUG("expected either onto_name or *advance_name in this function"); - - if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name), - &oid, &fullname, 0) == 1) { - free(*advance_name); - *advance_name = fullname; - } else { - die(_("argument to --advance must be a reference")); - } - *onto = peel_committish(repo, *advance_name, "--advance"); - if (rinfo.positive_refexprs > 1) - die(_("cannot advance target with multiple sources because ordering would be ill-defined")); + BUG("expected one of onto_name, *advance_name, or *revert_name"); } strset_clear(&rinfo.negative_refs); strset_clear(&rinfo.positive_refs); @@ -214,7 +256,8 @@ static struct commit *pick_regular_commit(struct repository *repo, kh_oid_map_t *replayed_commits, struct commit *onto, struct merge_options *merge_opt, - struct merge_result *result) + struct merge_result *result, + enum replay_mode mode) { struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree, *replayed_base_tree; @@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct repository *repo, pickme_tree = repo_get_commit_tree(repo, pickme); base_tree = repo_get_commit_tree(repo, base); - merge_opt->branch1 = short_commit_name(repo, replayed_base); - merge_opt->branch2 = short_commit_name(repo, pickme); - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); - - merge_incore_nonrecursive(merge_opt, - base_tree, - replayed_base_tree, - pickme_tree, - result); - - free((char*)merge_opt->ancestor); + if (mode == REPLAY_MODE_PICK) { + /* Cherry-pick: normal order */ + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = short_commit_name(repo, pickme); + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + + merge_incore_nonrecursive(merge_opt, + base_tree, + replayed_base_tree, + pickme_tree, + result); + + free((char *)merge_opt->ancestor); + } else if (mode == REPLAY_MODE_REVERT) { + /* Revert: swap base and pickme to reverse the diff */ + const char *pickme_name = short_commit_name(repo, pickme); + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); + merge_opt->ancestor = pickme_name; + + merge_incore_nonrecursive(merge_opt, + pickme_tree, + replayed_base_tree, + base_tree, + result); + + free((char *)merge_opt->branch2); + } else { + BUG("unexpected replay mode %d", mode); + } merge_opt->ancestor = NULL; + merge_opt->branch2 = NULL; if (!result->clean) return NULL; - /* Drop commits that become empty */ - if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && + /* Drop commits that become empty (only for picks) */ + if (mode == REPLAY_MODE_PICK && + oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) return replayed_base; - return create_commit(repo, result->tree, pickme, replayed_base); + return create_commit(repo, result->tree, pickme, replayed_base, mode); } void replay_result_release(struct replay_result *result) @@ -281,11 +345,16 @@ int replay_revisions(struct rev_info *revs, }; bool detached_head; char *advance; + char *revert; + enum replay_mode mode = REPLAY_MODE_PICK; int ret; advance = xstrdup_or_null(opts->advance); + revert = xstrdup_or_null(opts->revert); + if (revert) + mode = REPLAY_MODE_REVERT; set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, - &detached_head, &advance, &onto, &update_refs); + &detached_head, &advance, &revert, &onto, &update_refs); /* FIXME: Should allow replaying commits with the first as a root commit */ @@ -309,7 +378,7 @@ int replay_revisions(struct rev_info *revs, die(_("replaying merge commits is not supported yet!")); last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, - onto, &merge_opt, &result); + onto, &merge_opt, &result, mode); if (!last_commit) break; @@ -321,7 +390,7 @@ int replay_revisions(struct rev_info *revs, kh_value(replayed_commits, pos) = last_commit; /* Update any necessary branches */ - if (advance) + if (advance || revert) continue; for (decoration = get_name_decoration(&commit->object); @@ -355,11 +424,13 @@ int replay_revisions(struct rev_info *revs, goto out; } - /* In --advance mode, advance the target ref */ - if (advance) - replay_result_queue_update(out, advance, + /* In --advance or --revert mode, update the target ref */ + if (advance || revert) { + const char *ref = advance ? advance : revert; + replay_result_queue_update(out, ref, &onto->object.oid, &last_commit->object.oid); + } ret = 0; @@ -371,5 +442,6 @@ int replay_revisions(struct rev_info *revs, kh_destroy_oid_map(replayed_commits); merge_finalize(&merge_opt, &result); free(advance); + free(revert); return ret; } diff --git a/replay.h b/replay.h index d8407dc7f7..e916a5f975 100644 --- a/replay.h +++ b/replay.h @@ -13,7 +13,7 @@ struct replay_revisions_options { /* * Starting point at which to create the new commits; must be a branch * name. The branch will be updated to point to the rewritten commits. - * This option is mutually exclusive with `onto`. + * This option is mutually exclusive with `onto` and `revert`. */ const char *advance; @@ -22,7 +22,14 @@ struct replay_revisions_options { * committish. References pointing at decendants of `onto` will be * updated to point to the new commits. */ - const char *onto; + const char *onto; + + /* + * Starting point at which to create revert commits; must be a branch + * name. The branch will be updated to point to the revert commits. + * This option is mutually exclusive with `onto` and `advance`. + */ + const char *revert; /* * Update branches that point at commits in the given revision range. diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index a03f8f9293..ca517cf607 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' ' test_cmp expect actual ' -test_expect_success 'option --onto or --advance is mandatory' ' - echo "error: option --onto or --advance is mandatory" >expect && +test_expect_success 'exactly one of --onto, --advance, or --revert is required' ' + echo "error: exactly one of --onto, --advance, or --revert is required" >expect && test_might_fail git replay -h >>expect && test_must_fail git replay topic1..topic2 2>actual && test_cmp expect actual @@ -87,9 +87,8 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err test_cmp expect actual ' -test_expect_success 'options --advance and --contained cannot be used together' ' - printf "fatal: options ${SQ}--advance${SQ} " >expect && - printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect && +test_expect_success '--contained requires --onto' ' + echo "fatal: --contained requires --onto" >expect && test_must_fail git replay --advance=main --contained \ topic1..topic2 2>actual && test_cmp expect actual @@ -398,4 +397,102 @@ test_expect_success 'invalid replay.refAction value' ' test_grep "invalid.*replay.refAction.*value" error ' +test_expect_success 'argument to --revert must be a reference' ' + echo "fatal: argument to --revert must be a reference" >expect && + oid=$(git rev-parse main) && + test_must_fail git replay --revert=$oid topic1..topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'cannot revert with multiple sources' ' + echo "fatal: cannot revert target with multiple sources because ordering would be ill-defined" >expect && + test_must_fail git replay --revert main main topic1 topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'using replay --revert to revert commits' ' + # Reuse existing topic4 branch (has commits I and J on top of main) + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # Revert commits I and J + git replay --revert topic4 topic4~2..topic4 && + + # Verify the revert commits were created + git log --format=%s -4 topic4 >actual && + cat >expect <<-\EOF && + Revert "J" + Revert "I" + J + I + EOF + test_cmp expect actual && + + # Verify commit message format includes hash + test_commit_message topic4 <<-EOF && + Revert "J" + + This reverts commit $(git rev-parse J). + EOF + + # Verify reflog message + git reflog topic4 -1 --format=%gs >reflog-msg && + echo "replay --revert topic4" >expect-reflog && + test_cmp expect-reflog reflog-msg +' + +test_expect_success 'using replay --revert in bare repo' ' + # Reuse existing topic4 in bare repo + START=$(git -C bare rev-parse topic4) && + test_when_finished "git -C bare update-ref refs/heads/topic4 $START" && + + # Revert commit J in bare repo + git -C bare replay --revert topic4 topic4~1..topic4 && + + # Verify revert was created + git -C bare log -1 --format=%s topic4 >actual && + echo "Revert \"J\"" >expect && + test_cmp expect actual +' + +test_expect_success 'revert of revert uses Reapply' ' + # Use topic4 and first revert J, then revert the revert + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # First revert J + git replay --revert topic4 topic4~1..topic4 && + REVERT_J=$(git rev-parse topic4) && + + # Now revert the revert - should become Reapply + git replay --revert topic4 topic4~1..topic4 && + + # Verify Reapply prefix and message format + test_commit_message topic4 <<-EOF + Reapply "J" + + This reverts commit $REVERT_J. + EOF +' + +test_expect_success 'git replay --revert with conflict' ' + # conflict branch has C.conflict which conflicts with topic1s C + test_expect_code 1 git replay --revert conflict B..topic1 +' + +test_expect_success 'git replay --revert incompatible with --contained' ' + test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error && + test_grep "requires --onto" error +' + +test_expect_success 'git replay --revert incompatible with --onto' ' + test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --advance' ' + test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + test_done -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-02-18 23:42 ` [PATCH v3 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2026-02-20 17:35 ` Toon Claes 2026-02-20 20:23 ` Junio C Hamano ` (2 more replies) 2026-02-26 14:45 ` Phillip Wood 1 sibling, 3 replies; 92+ messages in thread From: Toon Claes @ 2026-02-20 17:35 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, Siddharth Asthana, Johannes Schindelin, Elijah Newren Siddharth Asthana <siddharthasthana31@gmail.com> writes: > Add a `--revert <branch>` mode to git replay that undoes the changes > introduced by the specified commits. Like --onto and --advance, --revert > is a standalone mode: it takes a branch argument and updates that branch > with the newly created revert commits. > > At GitLab, we need this in Gitaly for reverting commits directly on bare > repositories without requiring a working tree checkout. > > The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick > and revert are just the same three-way merge with swapped arguments: > > - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) > - Revert: merge(ancestor=commit, ours=current, theirs=parent) > > We swap the base and pickme trees passed to merge_incore_nonrecursive() > to reverse the diff direction. > > Revert commit messages follow the usual git revert conventions: prefixed > with "Revert" (or "Reapply" when reverting a revert), and including > "This reverts commit <hash>.". The author is set to the current user > rather than preserving the original author, matching git revert behavior. > > Helped-by: Christian Couder <christian.couder@gmail.com> > Helped-by: Patrick Steinhardt <ps@pks.im> > Helped-by: Elijah Newren <newren@gmail.com> > Helped-by: Phillip Wood <phillip.wood123@gmail.com> > Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> > Helped-by: Junio C Hamano <gitster@pobox.com> > Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> > --- > Documentation/git-replay.adoc | 37 +++++++- > builtin/replay.c | 25 ++++-- > replay.c | 162 ++++++++++++++++++++++++---------- > replay.h | 11 ++- > t/t3650-replay-basics.sh | 107 ++++++++++++++++++++-- > 5 files changed, 277 insertions(+), 65 deletions(-) > > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc > index 8d696ce3ab..ffdf790278 100644 > --- a/Documentation/git-replay.adoc > +++ b/Documentation/git-replay.adoc > @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t > SYNOPSIS > -------- > [verse] > -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> > +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... The modes `--onto`, `--advance` and `--revert` seem to be extremely different from each other. So I'm starting to wonder whether it won't make more sense to instead create subcommands instead of options for these. Maybe something like: git replay revert --base=<branch> <revision-range> git replay pick --base=<branch> <revision-range> git replay replay --base=<branch> <revision-range> -- Cheers, Toon ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-02-20 17:35 ` Toon Claes @ 2026-02-20 20:23 ` Junio C Hamano 2026-02-23 9:13 ` Christian Couder 2026-03-06 5:05 ` Siddharth Asthana 2 siblings, 0 replies; 92+ messages in thread From: Junio C Hamano @ 2026-02-20 20:23 UTC (permalink / raw) To: Toon Claes Cc: Siddharth Asthana, git, christian.couder, ps, newren, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin Toon Claes <toon@iotcl.com> writes: >> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc >> index 8d696ce3ab..ffdf790278 100644 >> --- a/Documentation/git-replay.adoc >> +++ b/Documentation/git-replay.adoc >> @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t >> SYNOPSIS >> -------- >> [verse] >> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> >> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... > > The modes `--onto`, `--advance` and `--revert` seem to be extremely > different from each other. So I'm starting to wonder whether it won't > make more sense to instead create subcommands instead of options for > these. Maybe something like: > > git replay revert --base=<branch> <revision-range> > git replay pick --base=<branch> <revision-range> > git replay replay --base=<branch> <revision-range> Given the earlier discussion on confusions (mostly by those like me who weren't closely watching the topics around this command) on what options among --onto, --advance, etc. were incompatible, etc., that certainly does sound like a good way to make these distinctive modes more clearly stand out. Thanks. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-02-20 17:35 ` Toon Claes 2026-02-20 20:23 ` Junio C Hamano @ 2026-02-23 9:13 ` Christian Couder 2026-02-23 11:23 ` Toon Claes 2026-03-06 5:05 ` Siddharth Asthana 2 siblings, 1 reply; 92+ messages in thread From: Christian Couder @ 2026-02-23 9:13 UTC (permalink / raw) To: Toon Claes Cc: Siddharth Asthana, git, ps, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, Elijah Newren On Fri, Feb 20, 2026 at 6:35 PM Toon Claes <toon@iotcl.com> wrote: > > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc > > index 8d696ce3ab..ffdf790278 100644 > > --- a/Documentation/git-replay.adoc > > +++ b/Documentation/git-replay.adoc > > @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t > > SYNOPSIS > > -------- > > [verse] > > -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> > > +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... > > The modes `--onto`, `--advance` and `--revert` seem to be extremely > different from each other. So I'm starting to wonder whether it won't > make more sense to instead create subcommands instead of options for > these. Maybe something like: > > git replay revert --base=<branch> <revision-range> > git replay pick --base=<branch> <revision-range> > git replay replay --base=<branch> <revision-range> (I think you mean `git replay rebase` in the above line, no?) I agree that we should consider this. But I think we should do it separately in another series, after this one about --revert is merged. We might even consider waiting until we have more experience using `git replay --revert` to make a more informed decision. We shouldn't wait for too long either though... Also if we nearly always need a base, then why not: git replay rebase <base> <revision-range> git replay pick <base> <revision-range> git replay revert <base> <revision-range> ? Or what was the reason for introducing --base=<branch>? ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-02-23 9:13 ` Christian Couder @ 2026-02-23 11:23 ` Toon Claes 0 siblings, 0 replies; 92+ messages in thread From: Toon Claes @ 2026-02-23 11:23 UTC (permalink / raw) To: Christian Couder Cc: Siddharth Asthana, git, ps, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin, Elijah Newren Christian Couder <christian.couder@gmail.com> writes: > On Fri, Feb 20, 2026 at 6:35 PM Toon Claes <toon@iotcl.com> wrote: >> >> The modes `--onto`, `--advance` and `--revert` seem to be extremely >> different from each other. So I'm starting to wonder whether it won't >> make more sense to instead create subcommands instead of options for >> these. Maybe something like: >> >> git replay revert --base=<branch> <revision-range> >> git replay pick --base=<branch> <revision-range> >> git replay replay --base=<branch> <revision-range> > > (I think you mean `git replay rebase` in the above line, no?) I'm fine either way, but I agree saying "replay" twice looks weird. > I agree that we should consider this. But I think we should do it > separately in another series, after this one about --revert is merged. > We might even consider waiting until we have more experience using > `git replay --revert` to make a more informed decision. We shouldn't > wait for too long either though... I can agree with that. > Also if we nearly always need a base, then why not: > > git replay rebase <base> <revision-range> > git replay pick <base> <revision-range> > git replay revert <base> <revision-range> > > ? > > Or what was the reason for introducing --base=<branch>? Well, the modes 'revert' and 'pick' work different from 'replay'. The latter looks in <revision-range> to determine which refs need updating. This can lead to multiple refs that will be updated (with option --contained). The first two only operate on one ref and ignore whatever refs are in <revision-range>. (I think, correct me if I'm wrong) That's why I suggest to take it one step further: git replay revert --ref=<branch> <revision-range> git replay pick --ref=<branch> <revision-range> git replay rebase --onto=<branch> <revision-range> That's why the first two use --ref instead of --onto. As a benefit, this also enables me to address another issue I have: git-replay(1) cannot be used on bare commit IDs. This issue was also raised by Yee Cheng Chin[1]. With options `--onto` and `--ref` we can fix this. Because you can use them together: git replay replay --ref=ref/heads/branch --onto=112233 aabbcc..ddeeff git replay pick --ref=ref/heads/branch --onto=112233 aabbcc..ddeeff git replay rebase --ref=ref/heads/branch --onto=112233 aabbcc..ddeeff The value of --onto doesn't need to be a ref, but --ref needs. For the 'rebase' subcommand option --ref is optional, for the other two --onto is optional. And when one of both is omitted, one defaults to the other. What do you think? [1]: https://lore.kernel.org/git/CAHTeOx-SMLh_idKhGczPKzZNOKy04uYXmUhL8Z79yRuNpmE4eA@mail.gmail.com/ -- Cheers, Toon ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-02-20 17:35 ` Toon Claes 2026-02-20 20:23 ` Junio C Hamano 2026-02-23 9:13 ` Christian Couder @ 2026-03-06 5:05 ` Siddharth Asthana 2 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-06 5:05 UTC (permalink / raw) To: Toon Claes, git Cc: christian.couder, ps, newren, gitster, phillip.wood123, phillip.wood, karthik.188, johannes.schindelin On 20/02/26 23:05, Toon Claes wrote: > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > >> Add a `--revert <branch>` mode to git replay that undoes the changes >> introduced by the specified commits. Like --onto and --advance, --revert >> is a standalone mode: it takes a branch argument and updates that branch >> with the newly created revert commits. >> >> At GitLab, we need this in Gitaly for reverting commits directly on bare >> repositories without requiring a working tree checkout. >> >> The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick >> and revert are just the same three-way merge with swapped arguments: >> >> - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) >> - Revert: merge(ancestor=commit, ours=current, theirs=parent) >> >> We swap the base and pickme trees passed to merge_incore_nonrecursive() >> to reverse the diff direction. >> >> Revert commit messages follow the usual git revert conventions: prefixed >> with "Revert" (or "Reapply" when reverting a revert), and including >> "This reverts commit <hash>.". The author is set to the current user >> rather than preserving the original author, matching git revert behavior. >> >> Helped-by: Christian Couder <christian.couder@gmail.com> >> Helped-by: Patrick Steinhardt <ps@pks.im> >> Helped-by: Elijah Newren <newren@gmail.com> >> Helped-by: Phillip Wood <phillip.wood123@gmail.com> >> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> >> Helped-by: Junio C Hamano <gitster@pobox.com> >> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> >> --- >> Documentation/git-replay.adoc | 37 +++++++- >> builtin/replay.c | 25 ++++-- >> replay.c | 162 ++++++++++++++++++++++++---------- >> replay.h | 11 ++- >> t/t3650-replay-basics.sh | 107 ++++++++++++++++++++-- >> 5 files changed, 277 insertions(+), 65 deletions(-) >> >> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc >> index 8d696ce3ab..ffdf790278 100644 >> --- a/Documentation/git-replay.adoc >> +++ b/Documentation/git-replay.adoc >> @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t >> SYNOPSIS >> -------- >> [verse] >> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> >> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... > > The modes `--onto`, `--advance` and `--revert` seem to be extremely > different from each other. So I'm starting to wonder whether it won't > make more sense to instead create subcommands instead of options for > these. Maybe something like: Agree the interface could be cleaner as subcommands. I think Christian's suggestion to do this separate series after --revert lands make sense -- we would get real-world usage feedback first, and it avoids scope creep here. > > git replay revert --base=<branch> <revision-range> > git replay pick --base=<branch> <revision-range> > git replay replay --base=<branch> <revision-range> > > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-02-18 23:42 ` [PATCH v3 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2026-02-20 17:35 ` Toon Claes @ 2026-02-26 14:45 ` Phillip Wood 2026-03-06 5:28 ` Siddharth Asthana 1 sibling, 1 reply; 92+ messages in thread From: Phillip Wood @ 2026-02-26 14:45 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, phillip.wood, karthik.188, johannes.schindelin, toon Hi Siddharth On 18/02/2026 23:42, Siddharth Asthana wrote: > @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to > point at the tip of the resulting history. This is different from `--onto`, > which uses the target only as a starting point without updating it. > > +--revert <branch>:: > + Starting point at which to create the reverted commits; must be a > + branch name. > ++ > +When `--revert` is specified, the commits in the revision range are reverted > +(their changes are undone) and the reverted commits are created on top of > +<branch>. The <branch> is then updated to point at the new commits. This is > +the same as running `git revert <revision-range>` but does not update the > +working tree. > ++ > +The commit messages follow `git revert` conventions: they are prefixed with > +"Revert" and include "This reverts commit <hash>." When reverting a commit > +whose message starts with "Revert", the new message uses "Reapply" instead. > +Unlike cherry-pick which preserves the original author, revert commits use > +the current user as the author, matching the behavior of `git revert`. > ++ > +This option is mutually exclusive with `--onto` and `--advance`. It is also > +incompatible with `--contained` (which is a modifier for `--onto` only). We seem to have lost NOTE: For reverting an entire merge request as a single commit (rather than commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` which can avoid unnecessary merge conflicts. from V2 which is a shame. I do think we should seriously consider reverting commits in the reverse order that they were created (i.e. do not set '--reverse' when setting up the rev-list options) to reduce the likely-hood of conflicts when reverting a sequence of commits. > @@ -152,6 +172,15 @@ all commits they have since `base`, playing them on top of > `origin/main`. These three branches may have commits on top of `base` > that they have in common, but that does not need to be the case. > > +To revert commits on a branch: > + > +------------ > +$ git replay --revert main main~2..main It might be more realistic to revert some commits from a different branch, for example git replay --revert main topic~2..topic > +static void set_up_branch_mode(struct repository *repo, > + char **branch_name, > + const char *option_name, > + struct ref_info *rinfo, > + struct commit **onto) > [...] > + if (rinfo->positive_refexprs > 1) > + die(_("cannot %s target with multiple sources because ordering would be ill-defined"), > + option_name + 2); /* skip "--" prefix */ This is a bit of a nasty hack as it stuffs an English word into the middle of a translated sentence. Using the option name as below might be nicer die(_("'%s' cannot be used with multiple revision ranges because the ordering would be ill defined", option_name); > @@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct repository *repo, > [...] > - /* Drop commits that become empty */ > - if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && > + /* Drop commits that become empty (only for picks) */ Why? What's the advantage in creating empty revert commits? > + if (mode == REPLAY_MODE_PICK && > + oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && Thanks Phillip ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-02-26 14:45 ` Phillip Wood @ 2026-03-06 5:28 ` Siddharth Asthana 2026-03-06 15:52 ` Phillip Wood 0 siblings, 1 reply; 92+ messages in thread From: Siddharth Asthana @ 2026-03-06 5:28 UTC (permalink / raw) To: phillip.wood, git Cc: christian.couder, ps, newren, gitster, karthik.188, johannes.schindelin, toon On 26/02/26 20:15, Phillip Wood wrote: > Hi Siddharth > > On 18/02/2026 23:42, Siddharth Asthana wrote: >> @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and >> <branch> is updated to >> point at the tip of the resulting history. This is different from >> `--onto`, >> which uses the target only as a starting point without updating it. >> +--revert <branch>:: >> + Starting point at which to create the reverted commits; must be a >> + branch name. >> ++ >> +When `--revert` is specified, the commits in the revision range are >> reverted >> +(their changes are undone) and the reverted commits are created on >> top of >> +<branch>. The <branch> is then updated to point at the new commits. >> This is >> +the same as running `git revert <revision-range>` but does not update >> the >> +working tree. >> ++ >> +The commit messages follow `git revert` conventions: they are >> prefixed with >> +"Revert" and include "This reverts commit <hash>." When reverting a >> commit >> +whose message starts with "Revert", the new message uses "Reapply" >> instead. >> +Unlike cherry-pick which preserves the original author, revert >> commits use >> +the current user as the author, matching the behavior of `git revert`. >> ++ >> +This option is mutually exclusive with `--onto` and `--advance`. It >> is also >> +incompatible with `--contained` (which is a modifier for `--onto` only). > > We seem to have lost > > NOTE: For reverting an entire merge request as a single commit > (rather than commit-by-commit), consider using `git merge-tree > --merge-base $TIP HEAD $BASE` which can avoid unnecessary merge > conflicts. > > from V2 which is a shame. Yeah, I dropped it during the v3 cleanup when I was trimming the example text. will add it back. > > I do think we should seriously consider reverting commits in the reverse > order that they were created (i.e. do not set '--reverse' when setting > up the rev-list options) to reduce the likely-hood of conflicts when > reverting a sequence of commits. Good catch. sequencer.c does exactly this in prepare_revs() -- it only sets reverse for REPLAY_PICK, not REPLAY_REVERT, so git revert processes newest-first. The complication in replay is that pick_regular_commit() chains commits through mapped_commit(base, onto). With oldest-first, the parent is always already in replayed_commits so the chain works. With newest-first, the parent hasn't been processed yet and mapped_commit() falls back to onto -- so each revert be independently based on the original branch tip instead of chaining. The fix is straightforward: for revert mode, pass last_commit instead of onto as the fallback in the main loop: pick_regular_commit(repo, commit, replayed_commits, mode == REPLAY_MODE_REVERT ? last_commit : onto, &merge_opt, &result, mode); That way each revert builds on the previous one regardless of walk order. I will do this in v4 together with skipping the reverse=1 override for revert mode. > >> @@ -152,6 +172,15 @@ all commits they have since `base`, playing them >> on top of >> `origin/main`. These three branches may have commits on top of `base` >> that they have in common, but that does not need to be the case. >> +To revert commits on a branch: >> + >> +------------ >> +$ git replay --revert main main~2..main > > It might be more realistic to revert some commits from a different > branch, for example > > git replay --revert main topic~2..topic Makes sense. v2 had `git replay --revert main feature~2..feature` for this reason but I simplified it in v3. I will go back to something like your example: git replay --revert main topic2..topic > >> +static void set_up_branch_mode(struct repository *repo, >> + char **branch_name, >> + const char *option_name, >> + struct ref_info *rinfo, >> + struct commit **onto) >> [...] >> + if (rinfo->positive_refexprs > 1) >> + die(_("cannot %s target with multiple sources because >> ordering would be ill-defined"), >> + option_name + 2); /* skip "--" prefix */ > > This is a bit of a nasty hack as it stuffs an English word into the > middle of a translated sentence. Using the option name as below might be > nicer Agreed, will use your suggested form: die(_("'%s' cannot be used with multiple revision ranges " "because the ordering would be ill-defined"), option_name); > > die(_("'%s' cannot be used with multiple revision ranges because > the ordering would be ill defined", option_name); >> @@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct >> repository *repo, >> [...] >> - /* Drop commits that become empty */ >> - if (oideq(&replayed_base_tree->object.oid, &result->tree- >> >object.oid) && >> + /* Drop commits that become empty (only for picks) */ > > Why? What's the advantage in creating empty revert commits? Consistency with git revert, which doesn't silently drop empty reverts either -- it stops and asks the user to deal with it. Since replay is non-interactive and can't prompt, I kept them rather than silently dropping, to avoid hiding that something unexpected happened. That being said, I don't feel strong about it. If you think dropping is the better default for replay, I am happy to change it. Or we could error out (exit code 1) like we do for conflicts? Thanks, Siddharth > >> + if (mode == REPLAY_MODE_PICK && >> + oideq(&replayed_base_tree->object.oid, &result->tree- >> >object.oid) && > > Thanks > > Phillip ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-03-06 5:28 ` Siddharth Asthana @ 2026-03-06 15:52 ` Phillip Wood 2026-03-06 16:20 ` Siddharth Asthana 0 siblings, 1 reply; 92+ messages in thread From: Phillip Wood @ 2026-03-06 15:52 UTC (permalink / raw) To: Siddharth Asthana, phillip.wood, git Cc: christian.couder, ps, newren, gitster, karthik.188, johannes.schindelin, toon On 06/03/2026 05:28, Siddharth Asthana wrote: > On 26/02/26 20:15, Phillip Wood wrote: >> On 18/02/2026 23:42, Siddharth Asthana wrote: > >> I do think we should seriously consider reverting commits in the >> reverse order that they were created (i.e. do not set '--reverse' when >> setting up the rev-list options) to reduce the likely-hood of >> conflicts when reverting a sequence of commits. > > Good catch. sequencer.c does exactly this in prepare_revs() -- it only > sets reverse for REPLAY_PICK, not REPLAY_REVERT, so git revert processes > newest-first. > > The complication in replay is that pick_regular_commit() chains commits > through mapped_commit(base, onto). With oldest-first, the parent is > always already in replayed_commits so the chain works. With newest- > first, the parent hasn't been processed yet and mapped_commit() falls > back to onto -- so each revert be independently based on the original > branch tip instead of chaining. > > The fix is straightforward: for revert mode, pass last_commit instead of > onto as the fallback in the main loop: > > pick_regular_commit(repo, commit, replayed_commits, > mode == REPLAY_MODE_REVERT ? last_commit : onto, > &merge_opt, &result, mode); As we only allow a single range of commits with --revert that should work. > That way each revert builds on the previous one regardless of walk > order. I will do this in v4 together with skipping the reverse=1 > override for revert mode. Great >>> @@ -226,25 +269,46 @@ static struct commit >>> *pick_regular_commit(struct repository *repo, >>> [...] >>> - /* Drop commits that become empty */ >>> - if (oideq(&replayed_base_tree->object.oid, &result->tree- >>> >object.oid) && >>> + /* Drop commits that become empty (only for picks) */ >> >> Why? What's the advantage in creating empty revert commits? > > > Consistency with git revert, which doesn't silently drop empty reverts > either -- it stops and asks the user to deal with it. So does "git cherry-pick" unless you pass --empty=drop or --empty=keep (I was surprised that "git revert" does not support --empty, that seems to be an oversight) > Since replay is > non-interactive and can't prompt, I kept them rather than silently > dropping, to avoid hiding that something unexpected happened. I don't think creating empty commits for revert is very helpful, when cherry-picking one could argue that the user may want to preserve the commit message (though I think that's unlikely in practice which is why we drop commits that become empty) but that does not apply to revert. > That being said, I don't feel strong about it. If you think dropping is > the better default for replay, I am happy to change it. Or we could > error out (exit code 1) like we do for conflicts? We don't error out when cherry-picking and so we shouldn't do that when reverting. Thanks Phillip > > Thanks, > Siddharth > > >> >>> + if (mode == REPLAY_MODE_PICK && >>> + oideq(&replayed_base_tree->object.oid, &result->tree- >>> >object.oid) && >> >> Thanks >> >> Phillip > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v3 2/2] replay: add --revert mode to reverse commit changes 2026-03-06 15:52 ` Phillip Wood @ 2026-03-06 16:20 ` Siddharth Asthana 0 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-06 16:20 UTC (permalink / raw) To: phillip.wood, git Cc: christian.couder, ps, newren, gitster, karthik.188, johannes.schindelin, toon On 06/03/26 21:22, Phillip Wood wrote: > On 06/03/2026 05:28, Siddharth Asthana wrote: >> On 26/02/26 20:15, Phillip Wood wrote: >>> On 18/02/2026 23:42, Siddharth Asthana wrote: >> >>> I do think we should seriously consider reverting commits in the >>> reverse order that they were created (i.e. do not set '--reverse' >>> when setting up the rev-list options) to reduce the likely-hood of >>> conflicts when reverting a sequence of commits. >> >> Good catch. sequencer.c does exactly this in prepare_revs() -- it only >> sets reverse for REPLAY_PICK, not REPLAY_REVERT, so git revert >> processes newest-first. >> >> The complication in replay is that pick_regular_commit() chains >> commits through mapped_commit(base, onto). With oldest-first, the >> parent is always already in replayed_commits so the chain works. With >> newest- first, the parent hasn't been processed yet and >> mapped_commit() falls back to onto -- so each revert be independently >> based on the original branch tip instead of chaining. >> >> The fix is straightforward: for revert mode, pass last_commit instead >> of onto as the fallback in the main loop: >> >> pick_regular_commit(repo, commit, replayed_commits, >> mode == REPLAY_MODE_REVERT ? last_commit : onto, >> &merge_opt, &result, mode); > > As we only allow a single range of commits with --revert that should work. > >> That way each revert builds on the previous one regardless of walk >> order. I will do this in v4 together with skipping the reverse=1 >> override for revert mode. > > Great > >>>> @@ -226,25 +269,46 @@ static struct commit >>>> *pick_regular_commit(struct repository *repo, >>>> [...] >>>> - /* Drop commits that become empty */ >>>> - if (oideq(&replayed_base_tree->object.oid, &result->tree- >>>> >object.oid) && >>>> + /* Drop commits that become empty (only for picks) */ >>> >>> Why? What's the advantage in creating empty revert commits? >> >> >> Consistency with git revert, which doesn't silently drop empty reverts >> either -- it stops and asks the user to deal with it. > > So does "git cherry-pick" unless you pass --empty=drop or --empty=keep > (I was surprised that "git revert" does not support --empty, that seems > to be an oversight) > >> Since replay is non-interactive and can't prompt, I kept them rather >> than silently dropping, to avoid hiding that something unexpected >> happened. > > I don't think creating empty commits for revert is very helpful, when > cherry-picking one could argue that the user may want to preserve the > commit message (though I think that's unlikely in practice which is why > we drop commits that become empty) but that does not apply to revert. > >> That being said, I don't feel strong about it. If you think dropping >> is the better default for replay, I am happy to change it. Or we >> could error out (exit code 1) like we do for conflicts? > > We don't error out when cherry-picking and so we shouldn't do that when > reverting. Right, will drop empties for revert too then. > > Thanks > > Phillip > >> >> Thanks, >> Siddharth >> >> >>> >>>> + if (mode == REPLAY_MODE_PICK && >>>> + oideq(&replayed_base_tree->object.oid, &result->tree- >>>> >object.oid) && >>> >>> Thanks >>> >>> Phillip ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v4 0/2] replay: add --revert mode to reverse commit changes 2026-02-18 23:42 ` [PATCH v3 0/2] " Siddharth Asthana 2026-02-18 23:42 ` [PATCH v3 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-02-18 23:42 ` [PATCH v3 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2026-03-13 5:40 ` Siddharth Asthana 2026-03-13 5:40 ` [PATCH v4 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana ` (4 more replies) 2 siblings, 5 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-13 5:40 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana Hi, git replay currently supports cherry-picking (--advance) and rebasing (--onto), but not reverting. We need this at GitLab for Gitaly to reverse commits directly on bare repositories without a checkout. The approach is the same as sequencer.c -- cherry-pick and revert are just the same three-way merge with swapped arguments. We swap the base and pickme trees passed to merge_incore_nonrecursive() to reverse the diff direction. Patch 1 extracts the full revert message formatting logic into a new sequencer_format_revert_message() function, following Phillip's suggestion to move everything into one shared function rather than just the header. refer_to_commit() is updated to take a struct repository and a bool instead of replay_opts so it works outside the sequencer. Patch 2 adds --revert <branch> as a standalone mode. Reverts are processed newest-first (matching git revert) to reduce conflicts by peeling off changes from the top. The series is based on top of d181b9354c (The 13th batch, 2026-03-07). CI: https://gitlab.com/gitlab-org/git/-/pipelines/2329880894 The msvc-meson / Chocolatey failures are pre-existing infrastructure issues unrelated to this series. Changes in v4: - Replaced sequencer_format_revert_header() with a more complete sequencer_format_revert_message() that handles everything: subject prefix, commit reference via refer_to_commit(), and merge-parent references -- per Phillip - Updated refer_to_commit() signature to take (struct repository *r, bool use_commit_reference) instead of (struct replay_opts *opts) - Reverts are now newest-first (revs.reverse = 0 for --revert), chaining on last_commit rather than the parent mapping - Changed doc example to cross-branch scenario and restored the merge-tree NOTE - Updated error message format to "'--revert' cannot be used with multiple revision ranges..." (and same for --advance) - Empty revert commits are now dropped, consistent with cherry-pick - Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/ - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/ - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/ Thanks, Siddharth --- Siddharth Asthana (2): sequencer: extract revert message formatting into shared function replay: add --revert mode to reverse commit changes Documentation/git-replay.adoc | 43 ++++++++- builtin/replay.c | 46 ++++++---- replay.c | 165 ++++++++++++++++++++++++---------- replay.h | 11 ++- sequencer.c | 78 +++++++++------- sequencer.h | 14 +++ t/t3650-replay-basics.sh | 114 +++++++++++++++++++++-- 7 files changed, 364 insertions(+), 107 deletions(-) Range-diff versus v3: 1: 9d686bcdfe ! 1: bdc710b265 sequencer: extract revert message formatting into shared function @@ Commit message sequencer: extract revert message formatting into shared function The logic for formatting revert commit messages (handling "Revert" and - "Reapply" cases) is currently duplicated between sequencer.c and will be - needed by builtin/replay.c. + "Reapply" cases, appending "This reverts commit <ref>.", and handling + merge-parent references) currently lives inline in do_pick_commit(). + The upcoming replay --revert mode needs to reuse this logic. - Extract this logic into a new sequencer_format_revert_header() function - that can be shared. The function handles both regular reverts ("Revert - "<subject>"") and revert-of-revert cases ("Reapply "<subject>""). - When an oid is provided, the function appends the full commit hash and - period; otherwise the caller should append the commit reference. + Extract all of this into a new sequencer_format_revert_message() + function. The function takes a repository, the subject line, commit, + parent, a use_commit_reference flag, and the output strbuf. It handles + both regular reverts ("Revert "<subject>"") and revert-of-revert cases + ("Reapply "<subject>""), and uses refer_to_commit() internally to + format the commit reference. - Update do_pick_commit() to use the new helper, eliminating code - duplication while preserving the special handling for commit_use_reference. + Update refer_to_commit() to take a struct repository parameter instead + of relying on the_repository, and a bool instead of reading from + replay_opts directly. This makes it usable from the new shared function + without pulling in sequencer-specific state. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> ## sequencer.c ## +@@ sequencer.c: static int should_edit(struct replay_opts *opts) { + return opts->edit; + } + +-static void refer_to_commit(struct replay_opts *opts, +- struct strbuf *msgbuf, struct commit *commit) ++static void refer_to_commit(struct repository *r, struct strbuf *msgbuf, ++ const struct commit *commit, ++ bool use_commit_reference) + { +- if (opts->commit_use_reference) { ++ if (use_commit_reference) { + struct pretty_print_context ctx = { + .abbrev = DEFAULT_ABBREV, + .date_mode.type = DATE_SHORT, + }; +- repo_format_commit_message(the_repository, commit, ++ repo_format_commit_message(r, commit, + "%h (%s, %ad)", msgbuf, &ctx); + } else { + strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid)); @@ sequencer.c: static int do_pick_commit(struct repository *r, */ @@ sequencer.c: static int do_pick_commit(struct repository *r, base = commit; base_label = msg.label; next = parent; -@@ sequencer.c: static int do_pick_commit(struct repository *r, - if (opts->commit_use_reference) { - strbuf_commented_addf(&ctx->message, comment_line_str, - "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); + next_label = msg.parent_label; +- if (opts->commit_use_reference) { +- strbuf_commented_addf(&ctx->message, comment_line_str, +- "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && - /* - * We don't touch pre-existing repeated reverts, because @@ sequencer.c: static int do_pick_commit(struct repository *r, - strbuf_addstr(&ctx->message, "Reapply \""); - strbuf_addstr(&ctx->message, orig_subject); - strbuf_addstr(&ctx->message, "\n"); -+ strbuf_addstr(&ctx->message, "\nThis reverts commit "); - } else { +- } else { - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); -+ sequencer_format_revert_header(&ctx->message, msg.subject, NULL); - } +- } - strbuf_addstr(&ctx->message, "\nThis reverts commit "); - refer_to_commit(opts, &ctx->message, commit); +- refer_to_commit(opts, &ctx->message, commit); +- +- if (commit->parents && commit->parents->next) { +- strbuf_addstr(&ctx->message, ", reversing\nchanges made to "); +- refer_to_commit(opts, &ctx->message, parent); +- } +- strbuf_addstr(&ctx->message, ".\n"); ++ sequencer_format_revert_message(r, msg.subject, commit, ++ parent, ++ opts->commit_use_reference, ++ &ctx->message); + } else { + const char *p; - if (commit->parents && commit->parents->next) { @@ sequencer.c: int sequencer_pick_revisions(struct repository *r, return res; } -+void sequencer_format_revert_header(struct strbuf *out, -+ const char *orig_subject, -+ const struct object_id *oid) ++void sequencer_format_revert_message(struct repository *r, ++ const char *subject, ++ const struct commit *commit, ++ const struct commit *parent, ++ bool use_commit_reference, ++ struct strbuf *message) +{ -+ const char *revert_subject; ++ const char *orig_subject; + -+ if (skip_prefix(orig_subject, "Revert \"", &revert_subject) && -+ /* -+ * We don't touch pre-existing repeated reverts, because -+ * theoretically these can be nested arbitrarily deeply, -+ * thus requiring excessive complexity to deal with. -+ */ -+ !starts_with(revert_subject, "Revert \"")) { -+ strbuf_addstr(out, "Reapply \""); -+ strbuf_addstr(out, revert_subject); -+ strbuf_addch(out, '\n'); ++ if (use_commit_reference) { ++ strbuf_commented_addf(message, comment_line_str, ++ "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); ++ } else if (skip_prefix(subject, "Revert \"", &orig_subject) && ++ /* ++ * We don't touch pre-existing repeated reverts, because ++ * theoretically these can be nested arbitrarily deeply, ++ * thus requiring excessive complexity to deal with. ++ */ ++ !starts_with(orig_subject, "Revert \"")) { ++ strbuf_addstr(message, "Reapply \""); ++ strbuf_addstr(message, orig_subject); ++ strbuf_addstr(message, "\n"); + } else { -+ strbuf_addstr(out, "Revert \""); -+ strbuf_addstr(out, orig_subject); -+ strbuf_addstr(out, "\"\n"); ++ strbuf_addstr(message, "Revert \""); ++ strbuf_addstr(message, subject); ++ strbuf_addstr(message, "\"\n"); + } ++ strbuf_addstr(message, "\nThis reverts commit "); ++ refer_to_commit(r, message, commit, use_commit_reference); + -+ strbuf_addstr(out, "\nThis reverts commit "); -+ if (oid) { -+ strbuf_addstr(out, oid_to_hex(oid)); -+ strbuf_addstr(out, ".\n"); ++ if (commit->parents && commit->parents->next) { ++ strbuf_addstr(message, ", reversing\nchanges made to "); ++ refer_to_commit(r, message, parent, use_commit_reference); + } ++ strbuf_addstr(message, ".\n"); +} + void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +/* -+ * Formats a revert commit message following standard Git conventions. -+ * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert -+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit <oid>." if oid -+ * is provided, otherwise just adds "This reverts commit " and the caller -+ * should append the commit reference. ++ * Formats a complete revert commit message following standard Git conventions. ++ * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases ++ * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts ++ * commit <ref>." using either the abbreviated or full commit reference ++ * depending on use_commit_reference. Also handles merge-parent references. + */ -+void sequencer_format_revert_header(struct strbuf *out, -+ const char *orig_subject, -+ const struct object_id *oid); ++void sequencer_format_revert_message(struct repository *r, ++ const char *subject, ++ const struct commit *commit, ++ const struct commit *parent, ++ bool use_commit_reference, ++ struct strbuf *message); + #endif /* SEQUENCER_H */ 2: 066269706e ! 2: bea6229575 replay: add --revert mode to reverse commit changes @@ Commit message We swap the base and pickme trees passed to merge_incore_nonrecursive() to reverse the diff direction. + Reverts are processed newest-first (matching git revert behavior) to + reduce conflicts by peeling off changes from the top. Each revert + builds on the result of the previous one via the last_commit fallback + in the main replay loop, rather than relying on the parent-mapping + used for cherry-pick. + Revert commit messages follow the usual git revert conventions: prefixed with "Revert" (or "Reapply" when reverting a revert), and including "This reverts commit <hash>.". The author is set to the current user @@ Commit message Helped-by: Phillip Wood <phillip.wood123@gmail.com> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Helped-by: Junio C Hamano <gitster@pobox.com> + Helped-by: Toon Claes <toon@iotcl.com> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> ## Documentation/git-replay.adoc ## @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them +To revert commits on a branch: + +------------ -+$ git replay --revert main main~2..main ++$ git replay --revert main topic~2..topic +------------ + -+This reverts the last two commits on `main`, creating two revert commits -+on top of `main`, and updates `main` to point at the result. ++This reverts the last two commits from `topic`, creating revert commits on ++top of `main`, and updates `main` to point at the result. This is useful when ++commits from `topic` were previously merged or cherry-picked into `main` and ++need to be undone. ++ ++NOTE: For reverting an entire merge request as a single commit (rather than ++commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` ++which can avoid unnecessary merge conflicts. + GIT --- @@ builtin/replay.c: int cmd_replay(int argc, /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); +@@ builtin/replay.c: int cmd_replay(int argc, + * some options changing these values if we think they could + * be useful. + */ +- revs.reverse = 1; ++ /* ++ * Cherry-pick/rebase need oldest-first ordering so that each ++ * replayed commit can build on its already-replayed parent. ++ * Revert needs newest-first ordering (like git revert) to ++ * reduce conflicts by peeling off changes from the top. ++ */ ++ revs.reverse = opts.revert ? 0 : 1; + revs.sort_order = REV_SORT_IN_GRAPH_ORDER; + revs.topo_order = 1; + revs.simplify_history = 0; +@@ builtin/replay.c: int cmd_replay(int argc, + * Detect and warn if we override some user specified rev + * walking options. + */ +- if (revs.reverse != 1) { +- warning(_("some rev walking options will be overridden as " +- "'%s' bit in 'struct rev_info' will be forced"), +- "reverse"); +- revs.reverse = 1; ++ { ++ int desired_reverse = opts.revert ? 0 : 1; ++ if (revs.reverse != desired_reverse) { ++ warning(_("some rev walking options will be overridden as " ++ "'%s' bit in 'struct rev_info' will be forced"), ++ "reverse"); ++ revs.reverse = desired_reverse; ++ } + } + if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { + warning(_("some rev walking options will be overridden as " @@ builtin/replay.c: int cmd_replay(int argc, goto cleanup; @@ replay.c #include "strmap.h" #include "tree.h" +-/* +- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but +- * do not want to use the_repository. +- */ +-#define the_repository DO_NOT_USE_THE_REPOSITORY +enum replay_mode { + REPLAY_MODE_PICK, + REPLAY_MODE_REVERT, +}; -+ + static const char *short_commit_name(struct repository *repo, struct commit *commit) - { @@ replay.c: static char *get_author(const char *message) return NULL; } @@ replay.c: static char *get_author(const char *message) + subject_len = find_commit_subject(message, &subject_start); + subject = xmemdupz(subject_start, subject_len); + -+ sequencer_format_revert_header(msg, subject, &commit->object.oid); ++ sequencer_format_revert_message(repo, subject, commit, ++ commit->parents ? commit->parents->item : NULL, ++ false, msg); + + free(subject); + repo_unuse_commit_buffer(repo, commit, message); @@ replay.c: static void get_ref_information(struct repository *repo, + } + *onto = peel_committish(repo, *branch_name, option_name); + if (rinfo->positive_refexprs > 1) -+ die(_("cannot %s target with multiple sources because ordering would be ill-defined"), -+ option_name + 2); /* skip "--" prefix */ ++ die(_("'%s' cannot be used with multiple revision ranges " ++ "because the ordering would be ill-defined"), ++ option_name); +} + static void set_up_replay_mode(struct repository *repo, @@ replay.c: static struct commit *pick_regular_commit(struct repository *repo, + merge_opt->branch2 = NULL; if (!result->clean) return NULL; -- /* Drop commits that become empty */ -- if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && -+ /* Drop commits that become empty (only for picks) */ -+ if (mode == REPLAY_MODE_PICK && -+ oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && + /* Drop commits that become empty */ + if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) return replayed_base; - return create_commit(repo, result->tree, pickme, replayed_base); @@ replay.c: int replay_revisions(struct rev_info *revs, last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, - onto, &merge_opt, &result); -+ onto, &merge_opt, &result, mode); ++ mode == REPLAY_MODE_REVERT ? last_commit : onto, ++ &merge_opt, &result, mode); if (!last_commit) break; @@ t/t3650-replay-basics.sh: test_expect_success 'no base or negative ref gives no- test_must_fail git replay --advance=main --contained \ topic1..topic2 2>actual && test_cmp expect actual + ' + + test_expect_success 'cannot advance target ... ordering would be ill-defined' ' +- echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && ++ cat >expect <<-\EOF && ++ fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined ++ EOF + test_must_fail git replay --advance=main main topic1 topic2 2>actual && + test_cmp expect actual + ' @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' ' test_grep "invalid.*replay.refAction.*value" error ' @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' ' +' + +test_expect_success 'cannot revert with multiple sources' ' -+ echo "fatal: cannot revert target with multiple sources because ordering would be ill-defined" >expect && ++ cat >expect <<-\EOF && ++ fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined ++ EOF + test_must_fail git replay --revert main main topic1 topic2 2>actual && + test_cmp expect actual +' @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' ' + # Revert commits I and J + git replay --revert topic4 topic4~2..topic4 && + -+ # Verify the revert commits were created ++ # Verify the revert commits were created (newest-first ordering ++ # means J is reverted first, then I on top) + git log --format=%s -4 topic4 >actual && + cat >expect <<-\EOF && -+ Revert "J" + Revert "I" ++ Revert "J" + J + I + EOF + test_cmp expect actual && + -+ # Verify commit message format includes hash ++ # Verify commit message format includes hash (tip is Revert "I") + test_commit_message topic4 <<-EOF && -+ Revert "J" ++ Revert "I" + -+ This reverts commit $(git rev-parse J). ++ This reverts commit $(git rev-parse I). + EOF + + # Verify reflog message base-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2 ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v4 1/2] sequencer: extract revert message formatting into shared function 2026-03-13 5:40 ` [PATCH v4 0/2] " Siddharth Asthana @ 2026-03-13 5:40 ` Siddharth Asthana 2026-03-13 15:53 ` Junio C Hamano 2026-03-16 16:57 ` Phillip Wood 2026-03-13 5:40 ` [PATCH v4 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana ` (3 subsequent siblings) 4 siblings, 2 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-13 5:40 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana The logic for formatting revert commit messages (handling "Revert" and "Reapply" cases, appending "This reverts commit <ref>.", and handling merge-parent references) currently lives inline in do_pick_commit(). The upcoming replay --revert mode needs to reuse this logic. Extract all of this into a new sequencer_format_revert_message() function. The function takes a repository, the subject line, commit, parent, a use_commit_reference flag, and the output strbuf. It handles both regular reverts ("Revert "<subject>"") and revert-of-revert cases ("Reapply "<subject>""), and uses refer_to_commit() internally to format the commit reference. Update refer_to_commit() to take a struct repository parameter instead of relying on the_repository, and a bool instead of reading from replay_opts directly. This makes it usable from the new shared function without pulling in sequencer-specific state. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- sequencer.c | 78 +++++++++++++++++++++++++++++++---------------------- sequencer.h | 14 ++++++++++ 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/sequencer.c b/sequencer.c index aafd0bc959..7bf9d6ad19 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) { return opts->edit; } -static void refer_to_commit(struct replay_opts *opts, - struct strbuf *msgbuf, struct commit *commit) +static void refer_to_commit(struct repository *r, struct strbuf *msgbuf, + const struct commit *commit, + bool use_commit_reference) { - if (opts->commit_use_reference) { + if (use_commit_reference) { struct pretty_print_context ctx = { .abbrev = DEFAULT_ABBREV, .date_mode.type = DATE_SHORT, }; - repo_format_commit_message(the_repository, commit, + repo_format_commit_message(r, commit, "%h (%s, %ad)", msgbuf, &ctx); } else { strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid)); @@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r, */ if (command == TODO_REVERT) { - const char *orig_subject; - base = commit; base_label = msg.label; next = parent; next_label = msg.parent_label; - if (opts->commit_use_reference) { - strbuf_commented_addf(&ctx->message, comment_line_str, - "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && - /* - * We don't touch pre-existing repeated reverts, because - * theoretically these can be nested arbitrarily deeply, - * thus requiring excessive complexity to deal with. - */ - !starts_with(orig_subject, "Revert \"")) { - strbuf_addstr(&ctx->message, "Reapply \""); - strbuf_addstr(&ctx->message, orig_subject); - strbuf_addstr(&ctx->message, "\n"); - } else { - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); - } - strbuf_addstr(&ctx->message, "\nThis reverts commit "); - refer_to_commit(opts, &ctx->message, commit); - - if (commit->parents && commit->parents->next) { - strbuf_addstr(&ctx->message, ", reversing\nchanges made to "); - refer_to_commit(opts, &ctx->message, parent); - } - strbuf_addstr(&ctx->message, ".\n"); + sequencer_format_revert_message(r, msg.subject, commit, + parent, + opts->commit_use_reference, + &ctx->message); } else { const char *p; @@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r, return res; } +void sequencer_format_revert_message(struct repository *r, + const char *subject, + const struct commit *commit, + const struct commit *parent, + bool use_commit_reference, + struct strbuf *message) +{ + const char *orig_subject; + + if (use_commit_reference) { + strbuf_commented_addf(message, comment_line_str, + "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); + } else if (skip_prefix(subject, "Revert \"", &orig_subject) && + /* + * We don't touch pre-existing repeated reverts, because + * theoretically these can be nested arbitrarily deeply, + * thus requiring excessive complexity to deal with. + */ + !starts_with(orig_subject, "Revert \"")) { + strbuf_addstr(message, "Reapply \""); + strbuf_addstr(message, orig_subject); + strbuf_addstr(message, "\n"); + } else { + strbuf_addstr(message, "Revert \""); + strbuf_addstr(message, subject); + strbuf_addstr(message, "\"\n"); + } + strbuf_addstr(message, "\nThis reverts commit "); + refer_to_commit(r, message, commit, use_commit_reference); + + if (commit->parents && commit->parents->next) { + strbuf_addstr(message, ", reversing\nchanges made to "); + refer_to_commit(r, message, parent, use_commit_reference); + } + strbuf_addstr(message, ".\n"); +} + void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) { unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP; diff --git a/sequencer.h b/sequencer.h index 719684c8a9..de0bd6e8a2 100644 --- a/sequencer.h +++ b/sequencer.h @@ -271,4 +271,18 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) */ int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +/* + * Formats a complete revert commit message following standard Git conventions. + * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases + * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts + * commit <ref>." using either the abbreviated or full commit reference + * depending on use_commit_reference. Also handles merge-parent references. + */ +void sequencer_format_revert_message(struct repository *r, + const char *subject, + const struct commit *commit, + const struct commit *parent, + bool use_commit_reference, + struct strbuf *message); + #endif /* SEQUENCER_H */ -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH v4 1/2] sequencer: extract revert message formatting into shared function 2026-03-13 5:40 ` [PATCH v4 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana @ 2026-03-13 15:53 ` Junio C Hamano 2026-03-16 19:12 ` Toon Claes 2026-03-16 16:57 ` Phillip Wood 1 sibling, 1 reply; 92+ messages in thread From: Junio C Hamano @ 2026-03-13 15:53 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188, johannes.schindelin, toon Siddharth Asthana <siddharthasthana31@gmail.com> writes: > The logic for formatting revert commit messages (handling "Revert" and > "Reapply" cases, appending "This reverts commit <ref>.", and handling > merge-parent references) currently lives inline in do_pick_commit(). > The upcoming replay --revert mode needs to reuse this logic. > > Extract all of this into a new sequencer_format_revert_message() > function. The function takes a repository, the subject line, commit, > parent, a use_commit_reference flag, and the output strbuf. It handles > both regular reverts ("Revert "<subject>"") and revert-of-revert cases > ("Reapply "<subject>""), and uses refer_to_commit() internally to > format the commit reference. > > Update refer_to_commit() to take a struct repository parameter instead > of relying on the_repository, and a bool instead of reading from > replay_opts directly. This makes it usable from the new shared function > without pulling in sequencer-specific state. > > Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> > --- > sequencer.c | 78 +++++++++++++++++++++++++++++++---------------------- > sequencer.h | 14 ++++++++++ > 2 files changed, 60 insertions(+), 32 deletions(-) Relative to the previous round, sequencer_format_revert_message() that does a bit more than sequencer_format_revert_header() we had makes the existing code easier to follow, even though the total codeflow amounts to the same thing. A new caller that will use the function now has to do less. Also, even though this is an internal implementation detail, changing the list of parameters refer_to_commit() takes makes it easier to understand which part of the replay_opts structure is used (i.e., we only care about "do we use the commit reference, or not?" bit, and we have no interest in any other members of the struct). > +/* > + * Formats a complete revert commit message following standard Git conventions. > + * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases > + * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts > + * commit <ref>." using either the abbreviated or full commit reference > + * depending on use_commit_reference. Also handles merge-parent references. > + */ > +void sequencer_format_revert_message(struct repository *r, > + const char *subject, > + const struct commit *commit, > + const struct commit *parent, > + bool use_commit_reference, > + struct strbuf *message); > + > #endif /* SEQUENCER_H */ OK. ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v4 1/2] sequencer: extract revert message formatting into shared function 2026-03-13 15:53 ` Junio C Hamano @ 2026-03-16 19:12 ` Toon Claes 0 siblings, 0 replies; 92+ messages in thread From: Toon Claes @ 2026-03-16 19:12 UTC (permalink / raw) To: Junio C Hamano, Siddharth Asthana Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188, johannes.schindelin Junio C Hamano <gitster@pobox.com> writes: > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > >> The logic for formatting revert commit messages (handling "Revert" and >> "Reapply" cases, appending "This reverts commit <ref>.", and handling >> merge-parent references) currently lives inline in do_pick_commit(). >> The upcoming replay --revert mode needs to reuse this logic. >> >> Extract all of this into a new sequencer_format_revert_message() >> function. The function takes a repository, the subject line, commit, >> parent, a use_commit_reference flag, and the output strbuf. It handles >> both regular reverts ("Revert "<subject>"") and revert-of-revert cases >> ("Reapply "<subject>""), and uses refer_to_commit() internally to >> format the commit reference. >> >> Update refer_to_commit() to take a struct repository parameter instead >> of relying on the_repository, and a bool instead of reading from >> replay_opts directly. This makes it usable from the new shared function >> without pulling in sequencer-specific state. I wouldn't mind if you put removing the use of `the_repository` in a separate commit. >> >> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> >> --- >> sequencer.c | 78 +++++++++++++++++++++++++++++++---------------------- >> sequencer.h | 14 ++++++++++ >> 2 files changed, 60 insertions(+), 32 deletions(-) > > Relative to the previous round, sequencer_format_revert_message() > that does a bit more than sequencer_format_revert_header() we had > makes the existing code easier to follow, even though the total > codeflow amounts to the same thing. A new caller that will use the > function now has to do less. > > Also, even though this is an internal implementation detail, > changing the list of parameters refer_to_commit() takes makes it > easier to understand which part of the replay_opts structure is used > (i.e., we only care about "do we use the commit reference, or not?" > bit, and we have no interest in any other members of the struct). Patrick suggested[1] to use flags, but it makes sense to keep it simple for now. [1]: https://lore.kernel.org/git/aTZ5RrjnwJ2ZnT7A@pks.im/ > >> +/* >> + * Formats a complete revert commit message following standard Git conventions. >> + * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases >> + * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts >> + * commit <ref>." using either the abbreviated or full commit reference >> + * depending on use_commit_reference. Also handles merge-parent references. >> + */ I think you're trying to put too much in the comments here. I would suggest to be a bit more concise. Maybe something like: /* * Format a revert commit message with appropriate "Revert" or "Reapply" * prefix and "This reverts commit <ref>." body. When use_commit_reference * is set, <ref> is an abbreviated hash with subject and date. */ >> +void sequencer_format_revert_message(struct repository *r, >> + const char *subject, >> + const struct commit *commit, >> + const struct commit *parent, >> + bool use_commit_reference, >> + struct strbuf *message); >> + >> #endif /* SEQUENCER_H */ > > OK. > -- Cheers, Toon ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v4 1/2] sequencer: extract revert message formatting into shared function 2026-03-13 5:40 ` [PATCH v4 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-03-13 15:53 ` Junio C Hamano @ 2026-03-16 16:57 ` Phillip Wood 1 sibling, 0 replies; 92+ messages in thread From: Phillip Wood @ 2026-03-16 16:57 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, karthik.188, johannes.schindelin, toon On 13/03/2026 05:40, Siddharth Asthana wrote: > The logic for formatting revert commit messages (handling "Revert" and > "Reapply" cases, appending "This reverts commit <ref>.", and handling > merge-parent references) currently lives inline in do_pick_commit(). > The upcoming replay --revert mode needs to reuse this logic. > > Extract all of this into a new sequencer_format_revert_message() > function. The function takes a repository, the subject line, commit, > parent, a use_commit_reference flag, and the output strbuf. It handles > both regular reverts ("Revert "<subject>"") and revert-of-revert cases > ("Reapply "<subject>""), and uses refer_to_commit() internally to > format the commit reference. > > Update refer_to_commit() to take a struct repository parameter instead > of relying on the_repository, and a bool instead of reading from > replay_opts directly. This makes it usable from the new shared function > without pulling in sequencer-specific state. This looks good. > +/* > + * Formats a complete revert commit message following standard Git conventions. > + * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases Not worth a re-roll on its own, but for future reference in comments you can use single quotes to avoid nesting double quotes i.e. ('Revert "<subject>"'). Thanks Phillip > + * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts > + * commit <ref>." using either the abbreviated or full commit reference > + * depending on use_commit_reference. Also handles merge-parent references. > + */ > +void sequencer_format_revert_message(struct repository *r, > + const char *subject, > + const struct commit *commit, > + const struct commit *parent, > + bool use_commit_reference, > + struct strbuf *message); > + > #endif /* SEQUENCER_H */ ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v4 2/2] replay: add --revert mode to reverse commit changes 2026-03-13 5:40 ` [PATCH v4 0/2] " Siddharth Asthana 2026-03-13 5:40 ` [PATCH v4 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana @ 2026-03-13 5:40 ` Siddharth Asthana 2026-03-16 16:57 ` Phillip Wood 2026-03-16 19:52 ` Toon Claes 2026-03-16 16:59 ` [PATCH v4 0/2] " Phillip Wood ` (2 subsequent siblings) 4 siblings, 2 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-13 5:40 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana, Johannes Schindelin Add a `--revert <branch>` mode to git replay that undoes the changes introduced by the specified commits. Like --onto and --advance, --revert is a standalone mode: it takes a branch argument and updates that branch with the newly created revert commits. At GitLab, we need this in Gitaly for reverting commits directly on bare repositories without requiring a working tree checkout. The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick and revert are just the same three-way merge with swapped arguments: - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) - Revert: merge(ancestor=commit, ours=current, theirs=parent) We swap the base and pickme trees passed to merge_incore_nonrecursive() to reverse the diff direction. Reverts are processed newest-first (matching git revert behavior) to reduce conflicts by peeling off changes from the top. Each revert builds on the result of the previous one via the last_commit fallback in the main replay loop, rather than relying on the parent-mapping used for cherry-pick. Revert commit messages follow the usual git revert conventions: prefixed with "Revert" (or "Reapply" when reverting a revert), and including "This reverts commit <hash>.". The author is set to the current user rather than preserving the original author, matching git revert behavior. Helped-by: Christian Couder <christian.couder@gmail.com> Helped-by: Patrick Steinhardt <ps@pks.im> Helped-by: Elijah Newren <newren@gmail.com> Helped-by: Phillip Wood <phillip.wood123@gmail.com> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Helped-by: Junio C Hamano <gitster@pobox.com> Helped-by: Toon Claes <toon@iotcl.com> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- Documentation/git-replay.adoc | 43 ++++++++- builtin/replay.c | 46 ++++++---- replay.c | 165 ++++++++++++++++++++++++---------- replay.h | 11 ++- t/t3650-replay-basics.sh | 114 +++++++++++++++++++++-- 5 files changed, 304 insertions(+), 75 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index 8d696ce3ab..6698cfc047 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t SYNOPSIS -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... DESCRIPTION ----------- @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to point at the tip of the resulting history. This is different from `--onto`, which uses the target only as a starting point without updating it. +--revert <branch>:: + Starting point at which to create the reverted commits; must be a + branch name. ++ +When `--revert` is specified, the commits in the revision range are reverted +(their changes are undone) and the reverted commits are created on top of +<branch>. The <branch> is then updated to point at the new commits. This is +the same as running `git revert <revision-range>` but does not update the +working tree. ++ +The commit messages follow `git revert` conventions: they are prefixed with +"Revert" and include "This reverts commit <hash>." When reverting a commit +whose message starts with "Revert", the new message uses "Reapply" instead. +Unlike cherry-pick which preserves the original author, revert commits use +the current user as the author, matching the behavior of `git revert`. ++ +This option is mutually exclusive with `--onto` and `--advance`. It is also +incompatible with `--contained` (which is a modifier for `--onto` only). + --contained:: Update all branches that point at commits in <revision-range>. Requires `--onto`. @@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH} where the number of refs updated depends on the arguments passed and -the shape of the history being replayed. When using `--advance`, the -number of refs updated is always one, but for `--onto`, it can be one -or more (rebasing multiple branches simultaneously is supported). +the shape of the history being replayed. When using `--advance` or +`--revert`, the number of refs updated is always one, but for `--onto`, +it can be one or more (rebasing multiple branches simultaneously is +supported). There is no stderr output on conflicts; see the <<exit-status,EXIT STATUS>> section below. @@ -152,6 +172,21 @@ all commits they have since `base`, playing them on top of `origin/main`. These three branches may have commits on top of `base` that they have in common, but that does not need to be the case. +To revert commits on a branch: + +------------ +$ git replay --revert main topic~2..topic +------------ + +This reverts the last two commits from `topic`, creating revert commits on +top of `main`, and updates `main` to point at the result. This is useful when +commits from `topic` were previously merged or cherry-picked into `main` and +need to be undone. + +NOTE: For reverting an entire merge request as a single commit (rather than +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` +which can avoid unnecessary merge conflicts. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/replay.c b/builtin/replay.c index 2cdde830a8..fe69f6f8ce 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -83,8 +83,8 @@ int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto <newbase> | --advance <branch>) " - "[--ref-action[=<mode>]] <revision-range>"), + "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) " + "[--ref-action[=<mode>]] <revision-range>..."), NULL }; struct option replay_options[] = { @@ -96,6 +96,9 @@ int cmd_replay(int argc, N_("replay onto given commit")), OPT_BOOL(0, "contained", &opts.contained, N_("update all branches that point at commits in <revision-range>")), + OPT_STRING(0, "revert", &opts.revert, + N_("branch"), + N_("revert commits onto given branch")), OPT_STRING(0, "ref-action", &ref_action, N_("mode"), N_("control ref update behavior (update|print)")), @@ -105,15 +108,17 @@ int cmd_replay(int argc, argc = parse_options(argc, argv, prefix, replay_options, replay_usage, PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); - if (!opts.onto && !opts.advance) { - error(_("option --onto or --advance is mandatory")); + /* Exactly one mode must be specified */ + if (!opts.onto && !opts.advance && !opts.revert) { + error(_("exactly one of --onto, --advance, or --revert is required")); usage_with_options(replay_usage, replay_options); } - die_for_incompatible_opt2(!!opts.advance, "--advance", - opts.contained, "--contained"); - die_for_incompatible_opt2(!!opts.advance, "--advance", - !!opts.onto, "--onto"); + die_for_incompatible_opt3(!!opts.onto, "--onto", + !!opts.advance, "--advance", + !!opts.revert, "--revert"); + if (opts.contained && !opts.onto) + die(_("--contained requires --onto")); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); @@ -129,7 +134,13 @@ int cmd_replay(int argc, * some options changing these values if we think they could * be useful. */ - revs.reverse = 1; + /* + * Cherry-pick/rebase need oldest-first ordering so that each + * replayed commit can build on its already-replayed parent. + * Revert needs newest-first ordering (like git revert) to + * reduce conflicts by peeling off changes from the top. + */ + revs.reverse = opts.revert ? 0 : 1; revs.sort_order = REV_SORT_IN_GRAPH_ORDER; revs.topo_order = 1; revs.simplify_history = 0; @@ -144,11 +155,14 @@ int cmd_replay(int argc, * Detect and warn if we override some user specified rev * walking options. */ - if (revs.reverse != 1) { - warning(_("some rev walking options will be overridden as " - "'%s' bit in 'struct rev_info' will be forced"), - "reverse"); - revs.reverse = 1; + { + int desired_reverse = opts.revert ? 0 : 1; + if (revs.reverse != desired_reverse) { + warning(_("some rev walking options will be overridden as " + "'%s' bit in 'struct rev_info' will be forced"), + "reverse"); + revs.reverse = desired_reverse; + } } if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { warning(_("some rev walking options will be overridden as " @@ -174,7 +188,9 @@ int cmd_replay(int argc, goto cleanup; /* Build reflog message */ - if (opts.advance) { + if (opts.revert) { + strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert); + } else if (opts.advance) { strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance); } else { struct object_id oid; diff --git a/replay.c b/replay.c index a63f6714c4..199066f6b3 100644 --- a/replay.c +++ b/replay.c @@ -8,14 +8,14 @@ #include "refs.h" #include "replay.h" #include "revision.h" +#include "sequencer.h" #include "strmap.h" #include "tree.h" -/* - * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but - * do not want to use the_repository. - */ -#define the_repository DO_NOT_USE_THE_REPOSITORY +enum replay_mode { + REPLAY_MODE_PICK, + REPLAY_MODE_REVERT, +}; static const char *short_commit_name(struct repository *repo, struct commit *commit) @@ -50,15 +50,37 @@ static char *get_author(const char *message) return NULL; } +static void generate_revert_message(struct strbuf *msg, + struct commit *commit, + struct repository *repo) +{ + const char *out_enc = get_commit_output_encoding(); + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); + const char *subject_start; + int subject_len; + char *subject; + + subject_len = find_commit_subject(message, &subject_start); + subject = xmemdupz(subject_start, subject_len); + + sequencer_format_revert_message(repo, subject, commit, + commit->parents ? commit->parents->item : NULL, + false, msg); + + free(subject); + repo_unuse_commit_buffer(repo, commit, message); +} + static struct commit *create_commit(struct repository *repo, struct tree *tree, struct commit *based_on, - struct commit *parent) + struct commit *parent, + enum replay_mode mode) { struct object_id ret; struct object *obj = NULL; struct commit_list *parents = NULL; - char *author; + char *author = NULL; char *sign_commit = NULL; /* FIXME: cli users might want to sign again */ struct commit_extra_header *extra = NULL; struct strbuf msg = STRBUF_INIT; @@ -70,9 +92,16 @@ static struct commit *create_commit(struct repository *repo, commit_list_insert(parent, &parents); extra = read_commit_extra_headers(based_on, exclude_gpgsig); - find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); + if (mode == REPLAY_MODE_REVERT) { + generate_revert_message(&msg, based_on, repo); + /* For revert, use current user as author (NULL = use default) */ + } else if (mode == REPLAY_MODE_PICK) { + find_commit_subject(message, &orig_message); + strbuf_addstr(&msg, orig_message); + author = get_author(message); + } else { + BUG("unexpected replay mode %d", mode); + } reset_ident_date(); if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, &ret, author, NULL, sign_commit, extra)) { @@ -153,11 +182,35 @@ static void get_ref_information(struct repository *repo, } } +static void set_up_branch_mode(struct repository *repo, + char **branch_name, + const char *option_name, + struct ref_info *rinfo, + struct commit **onto) +{ + struct object_id oid; + char *fullname = NULL; + + if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name), + &oid, &fullname, 0) == 1) { + free(*branch_name); + *branch_name = fullname; + } else { + die(_("argument to %s must be a reference"), option_name); + } + *onto = peel_committish(repo, *branch_name, option_name); + if (rinfo->positive_refexprs > 1) + die(_("'%s' cannot be used with multiple revision ranges " + "because the ordering would be ill-defined"), + option_name); +} + static void set_up_replay_mode(struct repository *repo, struct rev_cmdline_info *cmd_info, const char *onto_name, bool *detached_head, char **advance_name, + char **revert_name, struct commit **onto, struct strset **update_refs) { @@ -172,9 +225,6 @@ static void set_up_replay_mode(struct repository *repo, if (!rinfo.positive_refexprs) die(_("need some commits to replay")); - if (!onto_name == !*advance_name) - BUG("one and only one of onto_name and *advance_name must be given"); - if (onto_name) { *onto = peel_committish(repo, onto_name, "--onto"); if (rinfo.positive_refexprs < @@ -183,23 +233,12 @@ static void set_up_replay_mode(struct repository *repo, *update_refs = xcalloc(1, sizeof(**update_refs)); **update_refs = rinfo.positive_refs; memset(&rinfo.positive_refs, 0, sizeof(**update_refs)); + } else if (*advance_name) { + set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto); + } else if (*revert_name) { + set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto); } else { - struct object_id oid; - char *fullname = NULL; - - if (!*advance_name) - BUG("expected either onto_name or *advance_name in this function"); - - if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name), - &oid, &fullname, 0) == 1) { - free(*advance_name); - *advance_name = fullname; - } else { - die(_("argument to --advance must be a reference")); - } - *onto = peel_committish(repo, *advance_name, "--advance"); - if (rinfo.positive_refexprs > 1) - die(_("cannot advance target with multiple sources because ordering would be ill-defined")); + BUG("expected one of onto_name, *advance_name, or *revert_name"); } strset_clear(&rinfo.negative_refs); strset_clear(&rinfo.positive_refs); @@ -220,7 +259,8 @@ static struct commit *pick_regular_commit(struct repository *repo, kh_oid_map_t *replayed_commits, struct commit *onto, struct merge_options *merge_opt, - struct merge_result *result) + struct merge_result *result, + enum replay_mode mode) { struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree, *replayed_base_tree; @@ -232,25 +272,45 @@ static struct commit *pick_regular_commit(struct repository *repo, pickme_tree = repo_get_commit_tree(repo, pickme); base_tree = repo_get_commit_tree(repo, base); - merge_opt->branch1 = short_commit_name(repo, replayed_base); - merge_opt->branch2 = short_commit_name(repo, pickme); - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); - - merge_incore_nonrecursive(merge_opt, - base_tree, - replayed_base_tree, - pickme_tree, - result); - - free((char*)merge_opt->ancestor); + if (mode == REPLAY_MODE_PICK) { + /* Cherry-pick: normal order */ + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = short_commit_name(repo, pickme); + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + + merge_incore_nonrecursive(merge_opt, + base_tree, + replayed_base_tree, + pickme_tree, + result); + + free((char *)merge_opt->ancestor); + } else if (mode == REPLAY_MODE_REVERT) { + /* Revert: swap base and pickme to reverse the diff */ + const char *pickme_name = short_commit_name(repo, pickme); + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); + merge_opt->ancestor = pickme_name; + + merge_incore_nonrecursive(merge_opt, + pickme_tree, + replayed_base_tree, + base_tree, + result); + + free((char *)merge_opt->branch2); + } else { + BUG("unexpected replay mode %d", mode); + } merge_opt->ancestor = NULL; + merge_opt->branch2 = NULL; if (!result->clean) return NULL; /* Drop commits that become empty */ if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) return replayed_base; - return create_commit(repo, result->tree, pickme, replayed_base); + return create_commit(repo, result->tree, pickme, replayed_base, mode); } void replay_result_release(struct replay_result *result) @@ -287,11 +347,16 @@ int replay_revisions(struct rev_info *revs, }; bool detached_head; char *advance; + char *revert; + enum replay_mode mode = REPLAY_MODE_PICK; int ret; advance = xstrdup_or_null(opts->advance); + revert = xstrdup_or_null(opts->revert); + if (revert) + mode = REPLAY_MODE_REVERT; set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, - &detached_head, &advance, &onto, &update_refs); + &detached_head, &advance, &revert, &onto, &update_refs); /* FIXME: Should allow replaying commits with the first as a root commit */ @@ -315,7 +380,8 @@ int replay_revisions(struct rev_info *revs, die(_("replaying merge commits is not supported yet!")); last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, - onto, &merge_opt, &result); + mode == REPLAY_MODE_REVERT ? last_commit : onto, + &merge_opt, &result, mode); if (!last_commit) break; @@ -327,7 +393,7 @@ int replay_revisions(struct rev_info *revs, kh_value(replayed_commits, pos) = last_commit; /* Update any necessary branches */ - if (advance) + if (advance || revert) continue; for (decoration = get_name_decoration(&commit->object); @@ -361,11 +427,13 @@ int replay_revisions(struct rev_info *revs, goto out; } - /* In --advance mode, advance the target ref */ - if (advance) - replay_result_queue_update(out, advance, + /* In --advance or --revert mode, update the target ref */ + if (advance || revert) { + const char *ref = advance ? advance : revert; + replay_result_queue_update(out, ref, &onto->object.oid, &last_commit->object.oid); + } ret = 0; @@ -377,5 +445,6 @@ int replay_revisions(struct rev_info *revs, kh_destroy_oid_map(replayed_commits); merge_finalize(&merge_opt, &result); free(advance); + free(revert); return ret; } diff --git a/replay.h b/replay.h index d8407dc7f7..e916a5f975 100644 --- a/replay.h +++ b/replay.h @@ -13,7 +13,7 @@ struct replay_revisions_options { /* * Starting point at which to create the new commits; must be a branch * name. The branch will be updated to point to the rewritten commits. - * This option is mutually exclusive with `onto`. + * This option is mutually exclusive with `onto` and `revert`. */ const char *advance; @@ -22,7 +22,14 @@ struct replay_revisions_options { * committish. References pointing at decendants of `onto` will be * updated to point to the new commits. */ - const char *onto; + const char *onto; + + /* + * Starting point at which to create revert commits; must be a branch + * name. The branch will be updated to point to the revert commits. + * This option is mutually exclusive with `onto` and `advance`. + */ + const char *revert; /* * Update branches that point at commits in the given revision range. diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index a03f8f9293..0c1e03e0fb 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' ' test_cmp expect actual ' -test_expect_success 'option --onto or --advance is mandatory' ' - echo "error: option --onto or --advance is mandatory" >expect && +test_expect_success 'exactly one of --onto, --advance, or --revert is required' ' + echo "error: exactly one of --onto, --advance, or --revert is required" >expect && test_might_fail git replay -h >>expect && test_must_fail git replay topic1..topic2 2>actual && test_cmp expect actual @@ -87,16 +87,17 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err test_cmp expect actual ' -test_expect_success 'options --advance and --contained cannot be used together' ' - printf "fatal: options ${SQ}--advance${SQ} " >expect && - printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect && +test_expect_success '--contained requires --onto' ' + echo "fatal: --contained requires --onto" >expect && test_must_fail git replay --advance=main --contained \ topic1..topic2 2>actual && test_cmp expect actual ' test_expect_success 'cannot advance target ... ordering would be ill-defined' ' - echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && + cat >expect <<-\EOF && + fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined + EOF test_must_fail git replay --advance=main main topic1 topic2 2>actual && test_cmp expect actual ' @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' ' test_grep "invalid.*replay.refAction.*value" error ' +test_expect_success 'argument to --revert must be a reference' ' + echo "fatal: argument to --revert must be a reference" >expect && + oid=$(git rev-parse main) && + test_must_fail git replay --revert=$oid topic1..topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'cannot revert with multiple sources' ' + cat >expect <<-\EOF && + fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined + EOF + test_must_fail git replay --revert main main topic1 topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'using replay --revert to revert commits' ' + # Reuse existing topic4 branch (has commits I and J on top of main) + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # Revert commits I and J + git replay --revert topic4 topic4~2..topic4 && + + # Verify the revert commits were created (newest-first ordering + # means J is reverted first, then I on top) + git log --format=%s -4 topic4 >actual && + cat >expect <<-\EOF && + Revert "I" + Revert "J" + J + I + EOF + test_cmp expect actual && + + # Verify commit message format includes hash (tip is Revert "I") + test_commit_message topic4 <<-EOF && + Revert "I" + + This reverts commit $(git rev-parse I). + EOF + + # Verify reflog message + git reflog topic4 -1 --format=%gs >reflog-msg && + echo "replay --revert topic4" >expect-reflog && + test_cmp expect-reflog reflog-msg +' + +test_expect_success 'using replay --revert in bare repo' ' + # Reuse existing topic4 in bare repo + START=$(git -C bare rev-parse topic4) && + test_when_finished "git -C bare update-ref refs/heads/topic4 $START" && + + # Revert commit J in bare repo + git -C bare replay --revert topic4 topic4~1..topic4 && + + # Verify revert was created + git -C bare log -1 --format=%s topic4 >actual && + echo "Revert \"J\"" >expect && + test_cmp expect actual +' + +test_expect_success 'revert of revert uses Reapply' ' + # Use topic4 and first revert J, then revert the revert + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # First revert J + git replay --revert topic4 topic4~1..topic4 && + REVERT_J=$(git rev-parse topic4) && + + # Now revert the revert - should become Reapply + git replay --revert topic4 topic4~1..topic4 && + + # Verify Reapply prefix and message format + test_commit_message topic4 <<-EOF + Reapply "J" + + This reverts commit $REVERT_J. + EOF +' + +test_expect_success 'git replay --revert with conflict' ' + # conflict branch has C.conflict which conflicts with topic1s C + test_expect_code 1 git replay --revert conflict B..topic1 +' + +test_expect_success 'git replay --revert incompatible with --contained' ' + test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error && + test_grep "requires --onto" error +' + +test_expect_success 'git replay --revert incompatible with --onto' ' + test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --advance' ' + test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + test_done -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH v4 2/2] replay: add --revert mode to reverse commit changes 2026-03-13 5:40 ` [PATCH v4 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2026-03-16 16:57 ` Phillip Wood 2026-03-16 19:52 ` Toon Claes 1 sibling, 0 replies; 92+ messages in thread From: Phillip Wood @ 2026-03-16 16:57 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, karthik.188, johannes.schindelin, toon Hi Siddharth On 13/03/2026 05:40, Siddharth Asthana wrote: > Add a `--revert <branch>` mode to git replay that undoes the changes > introduced by the specified commits. Like --onto and --advance, --revert > is a standalone mode: it takes a branch argument and updates that branch > with the newly created revert commits. > > At GitLab, we need this in Gitaly for reverting commits directly on bare > repositories without requiring a working tree checkout. > > The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick > and revert are just the same three-way merge with swapped arguments: > > - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) > - Revert: merge(ancestor=commit, ours=current, theirs=parent) > > We swap the base and pickme trees passed to merge_incore_nonrecursive() > to reverse the diff direction. > > Reverts are processed newest-first (matching git revert behavior) to > reduce conflicts by peeling off changes from the top. Each revert > builds on the result of the previous one via the last_commit fallback > in the main replay loop, rather than relying on the parent-mapping > used for cherry-pick. > > Revert commit messages follow the usual git revert conventions: prefixed > with "Revert" (or "Reapply" when reverting a revert), and including > "This reverts commit <hash>.". The author is set to the current user > rather than preserving the original author, matching git revert behavior. This addresses all my comments on the previous version. I've one minor comment below but I'm not sure its worth a re-roll on its own. > > test_expect_success 'cannot advance target ... ordering would be ill-defined' ' > - echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && > + cat >expect <<-\EOF && > + fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined This quoting is a bit strange - we'd normally drop the '\' form '\EOF' above use ${SQ} instead. git grep shows there are 220 instances of ${SQ} vs 31 instances of '"'"' in the test suite. Thanks Phillip > + EOF > test_must_fail git replay --advance=main main topic1 topic2 2>actual && > test_cmp expect actual > ' > @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' ' > test_grep "invalid.*replay.refAction.*value" error > ' > > +test_expect_success 'argument to --revert must be a reference' ' > + echo "fatal: argument to --revert must be a reference" >expect && > + oid=$(git rev-parse main) && > + test_must_fail git replay --revert=$oid topic1..topic2 2>actual && > + test_cmp expect actual > +' > + > +test_expect_success 'cannot revert with multiple sources' ' > + cat >expect <<-\EOF && > + fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined > + EOF > + test_must_fail git replay --revert main main topic1 topic2 2>actual && > + test_cmp expect actual > +' > + > +test_expect_success 'using replay --revert to revert commits' ' > + # Reuse existing topic4 branch (has commits I and J on top of main) > + START=$(git rev-parse topic4) && > + test_when_finished "git branch -f topic4 $START" && > + > + # Revert commits I and J > + git replay --revert topic4 topic4~2..topic4 && > + > + # Verify the revert commits were created (newest-first ordering > + # means J is reverted first, then I on top) > + git log --format=%s -4 topic4 >actual && > + cat >expect <<-\EOF && > + Revert "I" > + Revert "J" > + J > + I > + EOF > + test_cmp expect actual && > + > + # Verify commit message format includes hash (tip is Revert "I") > + test_commit_message topic4 <<-EOF && > + Revert "I" > + > + This reverts commit $(git rev-parse I). > + EOF > + > + # Verify reflog message > + git reflog topic4 -1 --format=%gs >reflog-msg && > + echo "replay --revert topic4" >expect-reflog && > + test_cmp expect-reflog reflog-msg > +' > + > +test_expect_success 'using replay --revert in bare repo' ' > + # Reuse existing topic4 in bare repo > + START=$(git -C bare rev-parse topic4) && > + test_when_finished "git -C bare update-ref refs/heads/topic4 $START" && > + > + # Revert commit J in bare repo > + git -C bare replay --revert topic4 topic4~1..topic4 && > + > + # Verify revert was created > + git -C bare log -1 --format=%s topic4 >actual && > + echo "Revert \"J\"" >expect && > + test_cmp expect actual > +' > + > +test_expect_success 'revert of revert uses Reapply' ' > + # Use topic4 and first revert J, then revert the revert > + START=$(git rev-parse topic4) && > + test_when_finished "git branch -f topic4 $START" && > + > + # First revert J > + git replay --revert topic4 topic4~1..topic4 && > + REVERT_J=$(git rev-parse topic4) && > + > + # Now revert the revert - should become Reapply > + git replay --revert topic4 topic4~1..topic4 && > + > + # Verify Reapply prefix and message format > + test_commit_message topic4 <<-EOF > + Reapply "J" > + > + This reverts commit $REVERT_J. > + EOF > +' > + > +test_expect_success 'git replay --revert with conflict' ' > + # conflict branch has C.conflict which conflicts with topic1s C > + test_expect_code 1 git replay --revert conflict B..topic1 > +' > + > +test_expect_success 'git replay --revert incompatible with --contained' ' > + test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error && > + test_grep "requires --onto" error > +' > + > +test_expect_success 'git replay --revert incompatible with --onto' ' > + test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error && > + test_grep "cannot be used together" error > +' > + > +test_expect_success 'git replay --revert incompatible with --advance' ' > + test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error && > + test_grep "cannot be used together" error > +' > + > test_done ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v4 2/2] replay: add --revert mode to reverse commit changes 2026-03-13 5:40 ` [PATCH v4 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2026-03-16 16:57 ` Phillip Wood @ 2026-03-16 19:52 ` Toon Claes 2026-03-17 10:11 ` Phillip Wood 1 sibling, 1 reply; 92+ messages in thread From: Toon Claes @ 2026-03-16 19:52 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, Siddharth Asthana, Johannes Schindelin Siddharth Asthana <siddharthasthana31@gmail.com> writes: > Add a `--revert <branch>` mode to git replay that undoes the changes > introduced by the specified commits. Like --onto and --advance, --revert > is a standalone mode: it takes a branch argument and updates that branch > with the newly created revert commits. > > At GitLab, we need this in Gitaly for reverting commits directly on bare > repositories without requiring a working tree checkout. > > The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick > and revert are just the same three-way merge with swapped arguments: > > - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) > - Revert: merge(ancestor=commit, ours=current, theirs=parent) > > We swap the base and pickme trees passed to merge_incore_nonrecursive() > to reverse the diff direction. > > Reverts are processed newest-first (matching git revert behavior) to > reduce conflicts by peeling off changes from the top. Each revert > builds on the result of the previous one via the last_commit fallback > in the main replay loop, rather than relying on the parent-mapping > used for cherry-pick. > > Revert commit messages follow the usual git revert conventions: prefixed > with "Revert" (or "Reapply" when reverting a revert), and including > "This reverts commit <hash>.". The author is set to the current user > rather than preserving the original author, matching git revert behavior. > > Helped-by: Christian Couder <christian.couder@gmail.com> > Helped-by: Patrick Steinhardt <ps@pks.im> > Helped-by: Elijah Newren <newren@gmail.com> > Helped-by: Phillip Wood <phillip.wood123@gmail.com> > Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> > Helped-by: Junio C Hamano <gitster@pobox.com> > Helped-by: Toon Claes <toon@iotcl.com> > Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> > --- > Documentation/git-replay.adoc | 43 ++++++++- > builtin/replay.c | 46 ++++++---- > replay.c | 165 ++++++++++++++++++++++++---------- > replay.h | 11 ++- > t/t3650-replay-basics.sh | 114 +++++++++++++++++++++-- > 5 files changed, 304 insertions(+), 75 deletions(-) > > diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc > index 8d696ce3ab..6698cfc047 100644 > --- a/Documentation/git-replay.adoc > +++ b/Documentation/git-replay.adoc > @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t > SYNOPSIS > -------- > [verse] > -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> > +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... The ellipsis (...) should be dropped from revision-range. They were removed not so long ago in 136f86abc0 (Documentation/git-replay.adoc: fix errors around revision range, 2025-11-29). > > DESCRIPTION > ----------- > @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to > point at the tip of the resulting history. This is different from `--onto`, > which uses the target only as a starting point without updating it. > > +--revert <branch>:: > + Starting point at which to create the reverted commits; must be a > + branch name. > ++ > +When `--revert` is specified, the commits in the revision range are reverted > +(their changes are undone) and the reverted commits are created on top of > +<branch>. The <branch> is then updated to point at the new commits. This is > +the same as running `git revert <revision-range>` but does not update the > +working tree. > ++ > +The commit messages follow `git revert` conventions: they are prefixed with > +"Revert" and include "This reverts commit <hash>." When reverting a commit > +whose message starts with "Revert", the new message uses "Reapply" instead. > +Unlike cherry-pick which preserves the original author, revert commits use > +the current user as the author, matching the behavior of `git revert`. > ++ > +This option is mutually exclusive with `--onto` and `--advance`. It is also > +incompatible with `--contained` (which is a modifier for `--onto` only). > + > --contained:: > Update all branches that point at commits in > <revision-range>. Requires `--onto`. > @@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to > update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH} > > where the number of refs updated depends on the arguments passed and > -the shape of the history being replayed. When using `--advance`, the > -number of refs updated is always one, but for `--onto`, it can be one > -or more (rebasing multiple branches simultaneously is supported). > +the shape of the history being replayed. When using `--advance` or > +`--revert`, the number of refs updated is always one, but for `--onto`, > +it can be one or more (rebasing multiple branches simultaneously is > +supported). > > There is no stderr output on conflicts; see the <<exit-status,EXIT > STATUS>> section below. > @@ -152,6 +172,21 @@ all commits they have since `base`, playing them on top of > `origin/main`. These three branches may have commits on top of `base` > that they have in common, but that does not need to be the case. > > +To revert commits on a branch: > + > +------------ > +$ git replay --revert main topic~2..topic > +------------ > + > +This reverts the last two commits from `topic`, creating revert commits on > +top of `main`, and updates `main` to point at the result. This is useful when > +commits from `topic` were previously merged or cherry-picked into `main` and > +need to be undone. > + > +NOTE: For reverting an entire merge request as a single commit (rather than > +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` > +which can avoid unnecessary merge conflicts. > + > GIT > --- > Part of the linkgit:git[1] suite > diff --git a/builtin/replay.c b/builtin/replay.c > index 2cdde830a8..fe69f6f8ce 100644 > --- a/builtin/replay.c > +++ b/builtin/replay.c > @@ -83,8 +83,8 @@ int cmd_replay(int argc, > > const char *const replay_usage[] = { > N_("(EXPERIMENTAL!) git replay " > - "([--contained] --onto <newbase> | --advance <branch>) " > - "[--ref-action[=<mode>]] <revision-range>"), > + "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) " > + "[--ref-action[=<mode>]] <revision-range>..."), > NULL > }; > struct option replay_options[] = { > @@ -96,6 +96,9 @@ int cmd_replay(int argc, > N_("replay onto given commit")), > OPT_BOOL(0, "contained", &opts.contained, > N_("update all branches that point at commits in <revision-range>")), > + OPT_STRING(0, "revert", &opts.revert, > + N_("branch"), > + N_("revert commits onto given branch")), > OPT_STRING(0, "ref-action", &ref_action, > N_("mode"), > N_("control ref update behavior (update|print)")), > @@ -105,15 +108,17 @@ int cmd_replay(int argc, > argc = parse_options(argc, argv, prefix, replay_options, replay_usage, > PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); > > - if (!opts.onto && !opts.advance) { > - error(_("option --onto or --advance is mandatory")); > + /* Exactly one mode must be specified */ > + if (!opts.onto && !opts.advance && !opts.revert) { > + error(_("exactly one of --onto, --advance, or --revert is required")); > usage_with_options(replay_usage, replay_options); > } > > - die_for_incompatible_opt2(!!opts.advance, "--advance", > - opts.contained, "--contained"); > - die_for_incompatible_opt2(!!opts.advance, "--advance", > - !!opts.onto, "--onto"); > + die_for_incompatible_opt3(!!opts.onto, "--onto", > + !!opts.advance, "--advance", > + !!opts.revert, "--revert"); > + if (opts.contained && !opts.onto) > + die(_("--contained requires --onto")); I think it would be more clear if you say they cannot be used together: die_for_incompatible_opt2(!!opts.advance, "--advance", opts.contained, "--contained"); die_for_incompatible_opt2(!!opts.revert, "--revert", opts.contained, "--contained"); > /* Parse ref action mode from command line or config */ > ref_mode = get_ref_action_mode(repo, ref_action); > @@ -129,7 +134,13 @@ int cmd_replay(int argc, > * some options changing these values if we think they could > * be useful. > */ > - revs.reverse = 1; > + /* > + * Cherry-pick/rebase need oldest-first ordering so that each > + * replayed commit can build on its already-replayed parent. > + * Revert needs newest-first ordering (like git revert) to > + * reduce conflicts by peeling off changes from the top. > + */ > + revs.reverse = opts.revert ? 0 : 1; > revs.sort_order = REV_SORT_IN_GRAPH_ORDER; > revs.topo_order = 1; > revs.simplify_history = 0; > @@ -144,11 +155,14 @@ int cmd_replay(int argc, > * Detect and warn if we override some user specified rev > * walking options. > */ > - if (revs.reverse != 1) { > - warning(_("some rev walking options will be overridden as " > - "'%s' bit in 'struct rev_info' will be forced"), > - "reverse"); > - revs.reverse = 1; > + { Do we want to keep these braces? > + int desired_reverse = opts.revert ? 0 : 1; > + if (revs.reverse != desired_reverse) { > + warning(_("some rev walking options will be overridden as " > + "'%s' bit in 'struct rev_info' will be forced"), > + "reverse"); > + revs.reverse = desired_reverse; > + } > } > if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { > warning(_("some rev walking options will be overridden as " > @@ -174,7 +188,9 @@ int cmd_replay(int argc, > goto cleanup; > > /* Build reflog message */ > - if (opts.advance) { > + if (opts.revert) { > + strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert); > + } else if (opts.advance) { > strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance); > } else { > struct object_id oid; > diff --git a/replay.c b/replay.c > index a63f6714c4..199066f6b3 100644 > --- a/replay.c > +++ b/replay.c > @@ -8,14 +8,14 @@ > #include "refs.h" > #include "replay.h" > #include "revision.h" > +#include "sequencer.h" > #include "strmap.h" > #include "tree.h" > > -/* > - * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but > - * do not want to use the_repository. > - */ > -#define the_repository DO_NOT_USE_THE_REPOSITORY Why are you removing this? We're still setting USE_THE_REPOSITORY_VARIABLE, and not using 'the_repository' in this file? > +enum replay_mode { > + REPLAY_MODE_PICK, > + REPLAY_MODE_REVERT, > +}; > > static const char *short_commit_name(struct repository *repo, > struct commit *commit) > @@ -50,15 +50,37 @@ static char *get_author(const char *message) > return NULL; > } > > +static void generate_revert_message(struct strbuf *msg, > + struct commit *commit, > + struct repository *repo) > +{ > + const char *out_enc = get_commit_output_encoding(); > + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); > + const char *subject_start; > + int subject_len; > + char *subject; > + > + subject_len = find_commit_subject(message, &subject_start); > + subject = xmemdupz(subject_start, subject_len); > + > + sequencer_format_revert_message(repo, subject, commit, > + commit->parents ? commit->parents->item : NULL, > + false, msg); > + > + free(subject); > + repo_unuse_commit_buffer(repo, commit, message); > +} > + > static struct commit *create_commit(struct repository *repo, > struct tree *tree, > struct commit *based_on, > - struct commit *parent) > + struct commit *parent, > + enum replay_mode mode) > { > struct object_id ret; > struct object *obj = NULL; > struct commit_list *parents = NULL; > - char *author; > + char *author = NULL; > char *sign_commit = NULL; /* FIXME: cli users might want to sign again */ > struct commit_extra_header *extra = NULL; > struct strbuf msg = STRBUF_INIT; > @@ -70,9 +92,16 @@ static struct commit *create_commit(struct repository *repo, > > commit_list_insert(parent, &parents); > extra = read_commit_extra_headers(based_on, exclude_gpgsig); > - find_commit_subject(message, &orig_message); > - strbuf_addstr(&msg, orig_message); > - author = get_author(message); > + if (mode == REPLAY_MODE_REVERT) { > + generate_revert_message(&msg, based_on, repo); > + /* For revert, use current user as author (NULL = use default) */ > + } else if (mode == REPLAY_MODE_PICK) { > + find_commit_subject(message, &orig_message); > + strbuf_addstr(&msg, orig_message); > + author = get_author(message); > + } else { > + BUG("unexpected replay mode %d", mode); > + } > reset_ident_date(); > if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, > &ret, author, NULL, sign_commit, extra)) { > @@ -153,11 +182,35 @@ static void get_ref_information(struct repository *repo, > } > } > > +static void set_up_branch_mode(struct repository *repo, > + char **branch_name, > + const char *option_name, > + struct ref_info *rinfo, > + struct commit **onto) > +{ > + struct object_id oid; > + char *fullname = NULL; > + > + if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name), > + &oid, &fullname, 0) == 1) { > + free(*branch_name); > + *branch_name = fullname; > + } else { > + die(_("argument to %s must be a reference"), option_name); > + } > + *onto = peel_committish(repo, *branch_name, option_name); > + if (rinfo->positive_refexprs > 1) > + die(_("'%s' cannot be used with multiple revision ranges " > + "because the ordering would be ill-defined"), > + option_name); > +} > + > static void set_up_replay_mode(struct repository *repo, > struct rev_cmdline_info *cmd_info, > const char *onto_name, > bool *detached_head, > char **advance_name, > + char **revert_name, > struct commit **onto, > struct strset **update_refs) > { > @@ -172,9 +225,6 @@ static void set_up_replay_mode(struct repository *repo, > if (!rinfo.positive_refexprs) > die(_("need some commits to replay")); > > - if (!onto_name == !*advance_name) > - BUG("one and only one of onto_name and *advance_name must be given"); > - > if (onto_name) { > *onto = peel_committish(repo, onto_name, "--onto"); > if (rinfo.positive_refexprs < > @@ -183,23 +233,12 @@ static void set_up_replay_mode(struct repository *repo, > *update_refs = xcalloc(1, sizeof(**update_refs)); > **update_refs = rinfo.positive_refs; > memset(&rinfo.positive_refs, 0, sizeof(**update_refs)); > + } else if (*advance_name) { > + set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto); > + } else if (*revert_name) { > + set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto); > } else { > - struct object_id oid; > - char *fullname = NULL; > - > - if (!*advance_name) > - BUG("expected either onto_name or *advance_name in this function"); > - > - if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name), > - &oid, &fullname, 0) == 1) { > - free(*advance_name); > - *advance_name = fullname; > - } else { > - die(_("argument to --advance must be a reference")); > - } > - *onto = peel_committish(repo, *advance_name, "--advance"); > - if (rinfo.positive_refexprs > 1) > - die(_("cannot advance target with multiple sources because ordering would be ill-defined")); > + BUG("expected one of onto_name, *advance_name, or *revert_name"); > } > strset_clear(&rinfo.negative_refs); > strset_clear(&rinfo.positive_refs); > @@ -220,7 +259,8 @@ static struct commit *pick_regular_commit(struct repository *repo, > kh_oid_map_t *replayed_commits, > struct commit *onto, > struct merge_options *merge_opt, > - struct merge_result *result) > + struct merge_result *result, > + enum replay_mode mode) > { > struct commit *base, *replayed_base; > struct tree *pickme_tree, *base_tree, *replayed_base_tree; > @@ -232,25 +272,45 @@ static struct commit *pick_regular_commit(struct repository *repo, > pickme_tree = repo_get_commit_tree(repo, pickme); > base_tree = repo_get_commit_tree(repo, base); > > - merge_opt->branch1 = short_commit_name(repo, replayed_base); > - merge_opt->branch2 = short_commit_name(repo, pickme); > - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); > - > - merge_incore_nonrecursive(merge_opt, > - base_tree, > - replayed_base_tree, > - pickme_tree, > - result); > - > - free((char*)merge_opt->ancestor); > + if (mode == REPLAY_MODE_PICK) { > + /* Cherry-pick: normal order */ > + merge_opt->branch1 = short_commit_name(repo, replayed_base); > + merge_opt->branch2 = short_commit_name(repo, pickme); > + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); > + > + merge_incore_nonrecursive(merge_opt, > + base_tree, > + replayed_base_tree, > + pickme_tree, > + result); > + > + free((char *)merge_opt->ancestor); > + } else if (mode == REPLAY_MODE_REVERT) { > + /* Revert: swap base and pickme to reverse the diff */ > + const char *pickme_name = short_commit_name(repo, pickme); > + merge_opt->branch1 = short_commit_name(repo, replayed_base); > + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); > + merge_opt->ancestor = pickme_name; > + > + merge_incore_nonrecursive(merge_opt, > + pickme_tree, > + replayed_base_tree, > + base_tree, > + result); > + > + free((char *)merge_opt->branch2); > + } else { > + BUG("unexpected replay mode %d", mode); > + } > merge_opt->ancestor = NULL; > + merge_opt->branch2 = NULL; It took me a while to understand the ownership of these buffers, especially those returned by short_commit_name(). But it seems to call repo_find_unique_abbrev() which has 4 buffers which it cycles through. So this seems to be okay (this was also not changed in the MODE_PICK path. > if (!result->clean) > return NULL; > /* Drop commits that become empty */ > if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && > !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) > return replayed_base; > - return create_commit(repo, result->tree, pickme, replayed_base); > + return create_commit(repo, result->tree, pickme, replayed_base, mode); > } > > void replay_result_release(struct replay_result *result) > @@ -287,11 +347,16 @@ int replay_revisions(struct rev_info *revs, > }; > bool detached_head; > char *advance; > + char *revert; > + enum replay_mode mode = REPLAY_MODE_PICK; > int ret; > > advance = xstrdup_or_null(opts->advance); > + revert = xstrdup_or_null(opts->revert); > + if (revert) > + mode = REPLAY_MODE_REVERT; > set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, > - &detached_head, &advance, &onto, &update_refs); > + &detached_head, &advance, &revert, &onto, &update_refs); > > /* FIXME: Should allow replaying commits with the first as a root commit */ > > @@ -315,7 +380,8 @@ int replay_revisions(struct rev_info *revs, > die(_("replaying merge commits is not supported yet!")); > > last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, > - onto, &merge_opt, &result); > + mode == REPLAY_MODE_REVERT ? last_commit : onto, > + &merge_opt, &result, mode); > if (!last_commit) > break; > > @@ -327,7 +393,7 @@ int replay_revisions(struct rev_info *revs, > kh_value(replayed_commits, pos) = last_commit; > > /* Update any necessary branches */ > - if (advance) > + if (advance || revert) > continue; > > for (decoration = get_name_decoration(&commit->object); > @@ -361,11 +427,13 @@ int replay_revisions(struct rev_info *revs, > goto out; > } > > - /* In --advance mode, advance the target ref */ > - if (advance) > - replay_result_queue_update(out, advance, > + /* In --advance or --revert mode, update the target ref */ > + if (advance || revert) { > + const char *ref = advance ? advance : revert; > + replay_result_queue_update(out, ref, > &onto->object.oid, > &last_commit->object.oid); > + } > > ret = 0; > > @@ -377,5 +445,6 @@ int replay_revisions(struct rev_info *revs, > kh_destroy_oid_map(replayed_commits); > merge_finalize(&merge_opt, &result); > free(advance); > + free(revert); > return ret; > } > diff --git a/replay.h b/replay.h > index d8407dc7f7..e916a5f975 100644 > --- a/replay.h > +++ b/replay.h > @@ -13,7 +13,7 @@ struct replay_revisions_options { > /* > * Starting point at which to create the new commits; must be a branch > * name. The branch will be updated to point to the rewritten commits. > - * This option is mutually exclusive with `onto`. > + * This option is mutually exclusive with `onto` and `revert`. > */ > const char *advance; > > @@ -22,7 +22,14 @@ struct replay_revisions_options { > * committish. References pointing at decendants of `onto` will be > * updated to point to the new commits. > */ > - const char *onto; > + const char *onto; > + > + /* > + * Starting point at which to create revert commits; must be a branch > + * name. The branch will be updated to point to the revert commits. > + * This option is mutually exclusive with `onto` and `advance`. > + */ > + const char *revert; > > /* > * Update branches that point at commits in the given revision range. > diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh > index a03f8f9293..0c1e03e0fb 100755 > --- a/t/t3650-replay-basics.sh > +++ b/t/t3650-replay-basics.sh > @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' ' > test_cmp expect actual > ' > > -test_expect_success 'option --onto or --advance is mandatory' ' > - echo "error: option --onto or --advance is mandatory" >expect && > +test_expect_success 'exactly one of --onto, --advance, or --revert is required' ' > + echo "error: exactly one of --onto, --advance, or --revert is required" >expect && > test_might_fail git replay -h >>expect && > test_must_fail git replay topic1..topic2 2>actual && > test_cmp expect actual > @@ -87,16 +87,17 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err > test_cmp expect actual > ' > > -test_expect_success 'options --advance and --contained cannot be used together' ' > - printf "fatal: options ${SQ}--advance${SQ} " >expect && > - printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect && > +test_expect_success '--contained requires --onto' ' > + echo "fatal: --contained requires --onto" >expect && > test_must_fail git replay --advance=main --contained \ > topic1..topic2 2>actual && > test_cmp expect actual > ' > > test_expect_success 'cannot advance target ... ordering would be ill-defined' ' > - echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && > + cat >expect <<-\EOF && > + fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined > + EOF > test_must_fail git replay --advance=main main topic1 topic2 2>actual && > test_cmp expect actual > ' > @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' ' > test_grep "invalid.*replay.refAction.*value" error > ' > > +test_expect_success 'argument to --revert must be a reference' ' > + echo "fatal: argument to --revert must be a reference" >expect && > + oid=$(git rev-parse main) && > + test_must_fail git replay --revert=$oid topic1..topic2 2>actual && > + test_cmp expect actual > +' > + > +test_expect_success 'cannot revert with multiple sources' ' > + cat >expect <<-\EOF && > + fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined > + EOF > + test_must_fail git replay --revert main main topic1 topic2 2>actual && > + test_cmp expect actual > +' > + > +test_expect_success 'using replay --revert to revert commits' ' > + # Reuse existing topic4 branch (has commits I and J on top of main) > + START=$(git rev-parse topic4) && > + test_when_finished "git branch -f topic4 $START" && > + > + # Revert commits I and J > + git replay --revert topic4 topic4~2..topic4 && > + > + # Verify the revert commits were created (newest-first ordering > + # means J is reverted first, then I on top) > + git log --format=%s -4 topic4 >actual && > + cat >expect <<-\EOF && > + Revert "I" > + Revert "J" > + J > + I > + EOF > + test_cmp expect actual && > + > + # Verify commit message format includes hash (tip is Revert "I") > + test_commit_message topic4 <<-EOF && > + Revert "I" > + > + This reverts commit $(git rev-parse I). > + EOF > + > + # Verify reflog message > + git reflog topic4 -1 --format=%gs >reflog-msg && > + echo "replay --revert topic4" >expect-reflog && > + test_cmp expect-reflog reflog-msg > +' > + > +test_expect_success 'using replay --revert in bare repo' ' > + # Reuse existing topic4 in bare repo > + START=$(git -C bare rev-parse topic4) && > + test_when_finished "git -C bare update-ref refs/heads/topic4 $START" && > + > + # Revert commit J in bare repo > + git -C bare replay --revert topic4 topic4~1..topic4 && > + > + # Verify revert was created > + git -C bare log -1 --format=%s topic4 >actual && > + echo "Revert \"J\"" >expect && > + test_cmp expect actual > +' > + > +test_expect_success 'revert of revert uses Reapply' ' > + # Use topic4 and first revert J, then revert the revert > + START=$(git rev-parse topic4) && > + test_when_finished "git branch -f topic4 $START" && > + > + # First revert J > + git replay --revert topic4 topic4~1..topic4 && > + REVERT_J=$(git rev-parse topic4) && > + > + # Now revert the revert - should become Reapply > + git replay --revert topic4 topic4~1..topic4 && > + > + # Verify Reapply prefix and message format > + test_commit_message topic4 <<-EOF > + Reapply "J" > + > + This reverts commit $REVERT_J. > + EOF > +' > + > +test_expect_success 'git replay --revert with conflict' ' > + # conflict branch has C.conflict which conflicts with topic1s C > + test_expect_code 1 git replay --revert conflict B..topic1 > +' > + > +test_expect_success 'git replay --revert incompatible with --contained' ' > + test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error && > + test_grep "requires --onto" error > +' > + > +test_expect_success 'git replay --revert incompatible with --onto' ' > + test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error && > + test_grep "cannot be used together" error > +' > + > +test_expect_success 'git replay --revert incompatible with --advance' ' > + test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error && > + test_grep "cannot be used together" error > +' > + > test_done > -- > 2.51.0 > > -- Cheers, Toon ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v4 2/2] replay: add --revert mode to reverse commit changes 2026-03-16 19:52 ` Toon Claes @ 2026-03-17 10:11 ` Phillip Wood 0 siblings, 0 replies; 92+ messages in thread From: Phillip Wood @ 2026-03-17 10:11 UTC (permalink / raw) To: Toon Claes, Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, karthik.188, johannes.schindelin On 16/03/2026 19:52, Toon Claes wrote: > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > >> @@ -144,11 +155,14 @@ int cmd_replay(int argc, >> * Detect and warn if we override some user specified rev >> * walking options. >> */ >> - if (revs.reverse != 1) { >> - warning(_("some rev walking options will be overridden as " >> - "'%s' bit in 'struct rev_info' will be forced"), >> - "reverse"); >> - revs.reverse = 1; >> + { > > Do we want to keep these braces? That's a good point which I think is worth a re-roll. It would be better to move the declaration of "desired_reverse" up so that we have int desired_reverse = !opts.revert; ... revs.reverse = desired_reverse ... if (revs.reverse != desired_reverse) warning(...); That way we know we are using the same expected value for revs.reverse when we set it and when we check it hasn't changed later. Thanks Phillip >> + int desired_reverse = opts.revert ? 0 : 1; >> + if (revs.reverse != desired_reverse) { >> + warning(_("some rev walking options will be overridden as " >> + "'%s' bit in 'struct rev_info' will be forced"), >> + "reverse"); >> + revs.reverse = desired_reverse; >> + } >> } >> if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { >> warning(_("some rev walking options will be overridden as " >> @@ -174,7 +188,9 @@ int cmd_replay(int argc, >> goto cleanup; >> >> /* Build reflog message */ >> - if (opts.advance) { >> + if (opts.revert) { >> + strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert); >> + } else if (opts.advance) { >> strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance); >> } else { >> struct object_id oid; >> diff --git a/replay.c b/replay.c >> index a63f6714c4..199066f6b3 100644 >> --- a/replay.c >> +++ b/replay.c >> @@ -8,14 +8,14 @@ >> #include "refs.h" >> #include "replay.h" >> #include "revision.h" >> +#include "sequencer.h" >> #include "strmap.h" >> #include "tree.h" >> >> -/* >> - * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but >> - * do not want to use the_repository. >> - */ >> -#define the_repository DO_NOT_USE_THE_REPOSITORY > > Why are you removing this? We're still setting > USE_THE_REPOSITORY_VARIABLE, and not using 'the_repository' in this file? > >> +enum replay_mode { >> + REPLAY_MODE_PICK, >> + REPLAY_MODE_REVERT, >> +}; >> >> static const char *short_commit_name(struct repository *repo, >> struct commit *commit) >> @@ -50,15 +50,37 @@ static char *get_author(const char *message) >> return NULL; >> } >> >> +static void generate_revert_message(struct strbuf *msg, >> + struct commit *commit, >> + struct repository *repo) >> +{ >> + const char *out_enc = get_commit_output_encoding(); >> + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); >> + const char *subject_start; >> + int subject_len; >> + char *subject; >> + >> + subject_len = find_commit_subject(message, &subject_start); >> + subject = xmemdupz(subject_start, subject_len); >> + >> + sequencer_format_revert_message(repo, subject, commit, >> + commit->parents ? commit->parents->item : NULL, >> + false, msg); >> + >> + free(subject); >> + repo_unuse_commit_buffer(repo, commit, message); >> +} >> + >> static struct commit *create_commit(struct repository *repo, >> struct tree *tree, >> struct commit *based_on, >> - struct commit *parent) >> + struct commit *parent, >> + enum replay_mode mode) >> { >> struct object_id ret; >> struct object *obj = NULL; >> struct commit_list *parents = NULL; >> - char *author; >> + char *author = NULL; >> char *sign_commit = NULL; /* FIXME: cli users might want to sign again */ >> struct commit_extra_header *extra = NULL; >> struct strbuf msg = STRBUF_INIT; >> @@ -70,9 +92,16 @@ static struct commit *create_commit(struct repository *repo, >> >> commit_list_insert(parent, &parents); >> extra = read_commit_extra_headers(based_on, exclude_gpgsig); >> - find_commit_subject(message, &orig_message); >> - strbuf_addstr(&msg, orig_message); >> - author = get_author(message); >> + if (mode == REPLAY_MODE_REVERT) { >> + generate_revert_message(&msg, based_on, repo); >> + /* For revert, use current user as author (NULL = use default) */ >> + } else if (mode == REPLAY_MODE_PICK) { >> + find_commit_subject(message, &orig_message); >> + strbuf_addstr(&msg, orig_message); >> + author = get_author(message); >> + } else { >> + BUG("unexpected replay mode %d", mode); >> + } >> reset_ident_date(); >> if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, >> &ret, author, NULL, sign_commit, extra)) { >> @@ -153,11 +182,35 @@ static void get_ref_information(struct repository *repo, >> } >> } >> >> +static void set_up_branch_mode(struct repository *repo, >> + char **branch_name, >> + const char *option_name, >> + struct ref_info *rinfo, >> + struct commit **onto) >> +{ >> + struct object_id oid; >> + char *fullname = NULL; >> + >> + if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name), >> + &oid, &fullname, 0) == 1) { >> + free(*branch_name); >> + *branch_name = fullname; >> + } else { >> + die(_("argument to %s must be a reference"), option_name); >> + } >> + *onto = peel_committish(repo, *branch_name, option_name); >> + if (rinfo->positive_refexprs > 1) >> + die(_("'%s' cannot be used with multiple revision ranges " >> + "because the ordering would be ill-defined"), >> + option_name); >> +} >> + >> static void set_up_replay_mode(struct repository *repo, >> struct rev_cmdline_info *cmd_info, >> const char *onto_name, >> bool *detached_head, >> char **advance_name, >> + char **revert_name, >> struct commit **onto, >> struct strset **update_refs) >> { >> @@ -172,9 +225,6 @@ static void set_up_replay_mode(struct repository *repo, >> if (!rinfo.positive_refexprs) >> die(_("need some commits to replay")); >> >> - if (!onto_name == !*advance_name) >> - BUG("one and only one of onto_name and *advance_name must be given"); >> - >> if (onto_name) { >> *onto = peel_committish(repo, onto_name, "--onto"); >> if (rinfo.positive_refexprs < >> @@ -183,23 +233,12 @@ static void set_up_replay_mode(struct repository *repo, >> *update_refs = xcalloc(1, sizeof(**update_refs)); >> **update_refs = rinfo.positive_refs; >> memset(&rinfo.positive_refs, 0, sizeof(**update_refs)); >> + } else if (*advance_name) { >> + set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto); >> + } else if (*revert_name) { >> + set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto); >> } else { >> - struct object_id oid; >> - char *fullname = NULL; >> - >> - if (!*advance_name) >> - BUG("expected either onto_name or *advance_name in this function"); >> - >> - if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name), >> - &oid, &fullname, 0) == 1) { >> - free(*advance_name); >> - *advance_name = fullname; >> - } else { >> - die(_("argument to --advance must be a reference")); >> - } >> - *onto = peel_committish(repo, *advance_name, "--advance"); >> - if (rinfo.positive_refexprs > 1) >> - die(_("cannot advance target with multiple sources because ordering would be ill-defined")); >> + BUG("expected one of onto_name, *advance_name, or *revert_name"); >> } >> strset_clear(&rinfo.negative_refs); >> strset_clear(&rinfo.positive_refs); >> @@ -220,7 +259,8 @@ static struct commit *pick_regular_commit(struct repository *repo, >> kh_oid_map_t *replayed_commits, >> struct commit *onto, >> struct merge_options *merge_opt, >> - struct merge_result *result) >> + struct merge_result *result, >> + enum replay_mode mode) >> { >> struct commit *base, *replayed_base; >> struct tree *pickme_tree, *base_tree, *replayed_base_tree; >> @@ -232,25 +272,45 @@ static struct commit *pick_regular_commit(struct repository *repo, >> pickme_tree = repo_get_commit_tree(repo, pickme); >> base_tree = repo_get_commit_tree(repo, base); >> >> - merge_opt->branch1 = short_commit_name(repo, replayed_base); >> - merge_opt->branch2 = short_commit_name(repo, pickme); >> - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); >> - >> - merge_incore_nonrecursive(merge_opt, >> - base_tree, >> - replayed_base_tree, >> - pickme_tree, >> - result); >> - >> - free((char*)merge_opt->ancestor); >> + if (mode == REPLAY_MODE_PICK) { >> + /* Cherry-pick: normal order */ >> + merge_opt->branch1 = short_commit_name(repo, replayed_base); >> + merge_opt->branch2 = short_commit_name(repo, pickme); >> + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); >> + >> + merge_incore_nonrecursive(merge_opt, >> + base_tree, >> + replayed_base_tree, >> + pickme_tree, >> + result); >> + >> + free((char *)merge_opt->ancestor); >> + } else if (mode == REPLAY_MODE_REVERT) { >> + /* Revert: swap base and pickme to reverse the diff */ >> + const char *pickme_name = short_commit_name(repo, pickme); >> + merge_opt->branch1 = short_commit_name(repo, replayed_base); >> + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); >> + merge_opt->ancestor = pickme_name; >> + >> + merge_incore_nonrecursive(merge_opt, >> + pickme_tree, >> + replayed_base_tree, >> + base_tree, >> + result); >> + >> + free((char *)merge_opt->branch2); >> + } else { >> + BUG("unexpected replay mode %d", mode); >> + } >> merge_opt->ancestor = NULL; >> + merge_opt->branch2 = NULL; > > It took me a while to understand the ownership of these buffers, > especially those returned by short_commit_name(). But it seems to call > repo_find_unique_abbrev() which has 4 buffers which it cycles through. > > So this seems to be okay (this was also not changed in the MODE_PICK > path. > >> if (!result->clean) >> return NULL; >> /* Drop commits that become empty */ >> if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && >> !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) >> return replayed_base; >> - return create_commit(repo, result->tree, pickme, replayed_base); >> + return create_commit(repo, result->tree, pickme, replayed_base, mode); >> } >> >> void replay_result_release(struct replay_result *result) >> @@ -287,11 +347,16 @@ int replay_revisions(struct rev_info *revs, >> }; >> bool detached_head; >> char *advance; >> + char *revert; >> + enum replay_mode mode = REPLAY_MODE_PICK; >> int ret; >> >> advance = xstrdup_or_null(opts->advance); >> + revert = xstrdup_or_null(opts->revert); >> + if (revert) >> + mode = REPLAY_MODE_REVERT; >> set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, >> - &detached_head, &advance, &onto, &update_refs); >> + &detached_head, &advance, &revert, &onto, &update_refs); >> >> /* FIXME: Should allow replaying commits with the first as a root commit */ >> >> @@ -315,7 +380,8 @@ int replay_revisions(struct rev_info *revs, >> die(_("replaying merge commits is not supported yet!")); >> >> last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, >> - onto, &merge_opt, &result); >> + mode == REPLAY_MODE_REVERT ? last_commit : onto, >> + &merge_opt, &result, mode); >> if (!last_commit) >> break; >> >> @@ -327,7 +393,7 @@ int replay_revisions(struct rev_info *revs, >> kh_value(replayed_commits, pos) = last_commit; >> >> /* Update any necessary branches */ >> - if (advance) >> + if (advance || revert) >> continue; >> >> for (decoration = get_name_decoration(&commit->object); >> @@ -361,11 +427,13 @@ int replay_revisions(struct rev_info *revs, >> goto out; >> } >> >> - /* In --advance mode, advance the target ref */ >> - if (advance) >> - replay_result_queue_update(out, advance, >> + /* In --advance or --revert mode, update the target ref */ >> + if (advance || revert) { >> + const char *ref = advance ? advance : revert; >> + replay_result_queue_update(out, ref, >> &onto->object.oid, >> &last_commit->object.oid); >> + } >> >> ret = 0; >> >> @@ -377,5 +445,6 @@ int replay_revisions(struct rev_info *revs, >> kh_destroy_oid_map(replayed_commits); >> merge_finalize(&merge_opt, &result); >> free(advance); >> + free(revert); >> return ret; >> } >> diff --git a/replay.h b/replay.h >> index d8407dc7f7..e916a5f975 100644 >> --- a/replay.h >> +++ b/replay.h >> @@ -13,7 +13,7 @@ struct replay_revisions_options { >> /* >> * Starting point at which to create the new commits; must be a branch >> * name. The branch will be updated to point to the rewritten commits. >> - * This option is mutually exclusive with `onto`. >> + * This option is mutually exclusive with `onto` and `revert`. >> */ >> const char *advance; >> >> @@ -22,7 +22,14 @@ struct replay_revisions_options { >> * committish. References pointing at decendants of `onto` will be >> * updated to point to the new commits. >> */ >> - const char *onto; >> + const char *onto; >> + >> + /* >> + * Starting point at which to create revert commits; must be a branch >> + * name. The branch will be updated to point to the revert commits. >> + * This option is mutually exclusive with `onto` and `advance`. >> + */ >> + const char *revert; >> >> /* >> * Update branches that point at commits in the given revision range. >> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh >> index a03f8f9293..0c1e03e0fb 100755 >> --- a/t/t3650-replay-basics.sh >> +++ b/t/t3650-replay-basics.sh >> @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' ' >> test_cmp expect actual >> ' >> >> -test_expect_success 'option --onto or --advance is mandatory' ' >> - echo "error: option --onto or --advance is mandatory" >expect && >> +test_expect_success 'exactly one of --onto, --advance, or --revert is required' ' >> + echo "error: exactly one of --onto, --advance, or --revert is required" >expect && >> test_might_fail git replay -h >>expect && >> test_must_fail git replay topic1..topic2 2>actual && >> test_cmp expect actual >> @@ -87,16 +87,17 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err >> test_cmp expect actual >> ' >> >> -test_expect_success 'options --advance and --contained cannot be used together' ' >> - printf "fatal: options ${SQ}--advance${SQ} " >expect && >> - printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect && >> +test_expect_success '--contained requires --onto' ' >> + echo "fatal: --contained requires --onto" >expect && >> test_must_fail git replay --advance=main --contained \ >> topic1..topic2 2>actual && >> test_cmp expect actual >> ' >> >> test_expect_success 'cannot advance target ... ordering would be ill-defined' ' >> - echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && >> + cat >expect <<-\EOF && >> + fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined >> + EOF >> test_must_fail git replay --advance=main main topic1 topic2 2>actual && >> test_cmp expect actual >> ' >> @@ -398,4 +399,105 @@ test_expect_success 'invalid replay.refAction value' ' >> test_grep "invalid.*replay.refAction.*value" error >> ' >> >> +test_expect_success 'argument to --revert must be a reference' ' >> + echo "fatal: argument to --revert must be a reference" >expect && >> + oid=$(git rev-parse main) && >> + test_must_fail git replay --revert=$oid topic1..topic2 2>actual && >> + test_cmp expect actual >> +' >> + >> +test_expect_success 'cannot revert with multiple sources' ' >> + cat >expect <<-\EOF && >> + fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined >> + EOF >> + test_must_fail git replay --revert main main topic1 topic2 2>actual && >> + test_cmp expect actual >> +' >> + >> +test_expect_success 'using replay --revert to revert commits' ' >> + # Reuse existing topic4 branch (has commits I and J on top of main) >> + START=$(git rev-parse topic4) && >> + test_when_finished "git branch -f topic4 $START" && >> + >> + # Revert commits I and J >> + git replay --revert topic4 topic4~2..topic4 && >> + >> + # Verify the revert commits were created (newest-first ordering >> + # means J is reverted first, then I on top) >> + git log --format=%s -4 topic4 >actual && >> + cat >expect <<-\EOF && >> + Revert "I" >> + Revert "J" >> + J >> + I >> + EOF >> + test_cmp expect actual && >> + >> + # Verify commit message format includes hash (tip is Revert "I") >> + test_commit_message topic4 <<-EOF && >> + Revert "I" >> + >> + This reverts commit $(git rev-parse I). >> + EOF >> + >> + # Verify reflog message >> + git reflog topic4 -1 --format=%gs >reflog-msg && >> + echo "replay --revert topic4" >expect-reflog && >> + test_cmp expect-reflog reflog-msg >> +' >> + >> +test_expect_success 'using replay --revert in bare repo' ' >> + # Reuse existing topic4 in bare repo >> + START=$(git -C bare rev-parse topic4) && >> + test_when_finished "git -C bare update-ref refs/heads/topic4 $START" && >> + >> + # Revert commit J in bare repo >> + git -C bare replay --revert topic4 topic4~1..topic4 && >> + >> + # Verify revert was created >> + git -C bare log -1 --format=%s topic4 >actual && >> + echo "Revert \"J\"" >expect && >> + test_cmp expect actual >> +' >> + >> +test_expect_success 'revert of revert uses Reapply' ' >> + # Use topic4 and first revert J, then revert the revert >> + START=$(git rev-parse topic4) && >> + test_when_finished "git branch -f topic4 $START" && >> + >> + # First revert J >> + git replay --revert topic4 topic4~1..topic4 && >> + REVERT_J=$(git rev-parse topic4) && >> + >> + # Now revert the revert - should become Reapply >> + git replay --revert topic4 topic4~1..topic4 && >> + >> + # Verify Reapply prefix and message format >> + test_commit_message topic4 <<-EOF >> + Reapply "J" >> + >> + This reverts commit $REVERT_J. >> + EOF >> +' >> + >> +test_expect_success 'git replay --revert with conflict' ' >> + # conflict branch has C.conflict which conflicts with topic1s C >> + test_expect_code 1 git replay --revert conflict B..topic1 >> +' >> + >> +test_expect_success 'git replay --revert incompatible with --contained' ' >> + test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error && >> + test_grep "requires --onto" error >> +' >> + >> +test_expect_success 'git replay --revert incompatible with --onto' ' >> + test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error && >> + test_grep "cannot be used together" error >> +' >> + >> +test_expect_success 'git replay --revert incompatible with --advance' ' >> + test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error && >> + test_grep "cannot be used together" error >> +' >> + >> test_done >> -- >> 2.51.0 >> >> > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v4 0/2] replay: add --revert mode to reverse commit changes 2026-03-13 5:40 ` [PATCH v4 0/2] " Siddharth Asthana 2026-03-13 5:40 ` [PATCH v4 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-03-13 5:40 ` [PATCH v4 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2026-03-16 16:59 ` Phillip Wood 2026-03-16 19:53 ` Toon Claes 2026-03-24 22:03 ` [PATCH v5 " Siddharth Asthana 4 siblings, 0 replies; 92+ messages in thread From: Phillip Wood @ 2026-03-16 16:59 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, karthik.188, johannes.schindelin, toon On 13/03/2026 05:40, Siddharth Asthana wrote: > > Changes in v4: > - Replaced sequencer_format_revert_header() with a more complete > sequencer_format_revert_message() that handles everything: subject > prefix, commit reference via refer_to_commit(), and merge-parent > references -- per Phillip > - Updated refer_to_commit() signature to take (struct repository *r, > bool use_commit_reference) instead of (struct replay_opts *opts) > - Reverts are now newest-first (revs.reverse = 0 for --revert), > chaining on last_commit rather than the parent mapping > - Changed doc example to cross-branch scenario and restored the > merge-tree NOTE > - Updated error message format to "'--revert' cannot be used with > multiple revision ranges..." (and same for --advance) > - Empty revert commits are now dropped, consistent with cherry-pick This looks good to me. I've left a couple of nitpicking comments but they're mainly for future reference - I'd be happy to see this merged as is. Thanks Phillip > - Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/ > - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/ > - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/ > > Thanks, > Siddharth > > --- > Siddharth Asthana (2): > sequencer: extract revert message formatting into shared function > replay: add --revert mode to reverse commit changes > > Documentation/git-replay.adoc | 43 ++++++++- > builtin/replay.c | 46 ++++++---- > replay.c | 165 ++++++++++++++++++++++++---------- > replay.h | 11 ++- > sequencer.c | 78 +++++++++------- > sequencer.h | 14 +++ > t/t3650-replay-basics.sh | 114 +++++++++++++++++++++-- > 7 files changed, 364 insertions(+), 107 deletions(-) > > Range-diff versus v3: > > 1: 9d686bcdfe ! 1: bdc710b265 sequencer: extract revert message formatting into shared function > @@ Commit message > sequencer: extract revert message formatting into shared function > > The logic for formatting revert commit messages (handling "Revert" and > - "Reapply" cases) is currently duplicated between sequencer.c and will be > - needed by builtin/replay.c. > + "Reapply" cases, appending "This reverts commit <ref>.", and handling > + merge-parent references) currently lives inline in do_pick_commit(). > + The upcoming replay --revert mode needs to reuse this logic. > > - Extract this logic into a new sequencer_format_revert_header() function > - that can be shared. The function handles both regular reverts ("Revert > - "<subject>"") and revert-of-revert cases ("Reapply "<subject>""). > - When an oid is provided, the function appends the full commit hash and > - period; otherwise the caller should append the commit reference. > + Extract all of this into a new sequencer_format_revert_message() > + function. The function takes a repository, the subject line, commit, > + parent, a use_commit_reference flag, and the output strbuf. It handles > + both regular reverts ("Revert "<subject>"") and revert-of-revert cases > + ("Reapply "<subject>""), and uses refer_to_commit() internally to > + format the commit reference. > > - Update do_pick_commit() to use the new helper, eliminating code > - duplication while preserving the special handling for commit_use_reference. > + Update refer_to_commit() to take a struct repository parameter instead > + of relying on the_repository, and a bool instead of reading from > + replay_opts directly. This makes it usable from the new shared function > + without pulling in sequencer-specific state. > > Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> > > ## sequencer.c ## > +@@ sequencer.c: static int should_edit(struct replay_opts *opts) { > + return opts->edit; > + } > + > +-static void refer_to_commit(struct replay_opts *opts, > +- struct strbuf *msgbuf, struct commit *commit) > ++static void refer_to_commit(struct repository *r, struct strbuf *msgbuf, > ++ const struct commit *commit, > ++ bool use_commit_reference) > + { > +- if (opts->commit_use_reference) { > ++ if (use_commit_reference) { > + struct pretty_print_context ctx = { > + .abbrev = DEFAULT_ABBREV, > + .date_mode.type = DATE_SHORT, > + }; > +- repo_format_commit_message(the_repository, commit, > ++ repo_format_commit_message(r, commit, > + "%h (%s, %ad)", msgbuf, &ctx); > + } else { > + strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid)); > @@ sequencer.c: static int do_pick_commit(struct repository *r, > */ > > @@ sequencer.c: static int do_pick_commit(struct repository *r, > base = commit; > base_label = msg.label; > next = parent; > -@@ sequencer.c: static int do_pick_commit(struct repository *r, > - if (opts->commit_use_reference) { > - strbuf_commented_addf(&ctx->message, comment_line_str, > - "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); > + next_label = msg.parent_label; > +- if (opts->commit_use_reference) { > +- strbuf_commented_addf(&ctx->message, comment_line_str, > +- "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); > - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && > - /* > - * We don't touch pre-existing repeated reverts, because > @@ sequencer.c: static int do_pick_commit(struct repository *r, > - strbuf_addstr(&ctx->message, "Reapply \""); > - strbuf_addstr(&ctx->message, orig_subject); > - strbuf_addstr(&ctx->message, "\n"); > -+ strbuf_addstr(&ctx->message, "\nThis reverts commit "); > - } else { > +- } else { > - strbuf_addstr(&ctx->message, "Revert \""); > - strbuf_addstr(&ctx->message, msg.subject); > - strbuf_addstr(&ctx->message, "\"\n"); > -+ sequencer_format_revert_header(&ctx->message, msg.subject, NULL); > - } > +- } > - strbuf_addstr(&ctx->message, "\nThis reverts commit "); > - refer_to_commit(opts, &ctx->message, commit); > +- refer_to_commit(opts, &ctx->message, commit); > +- > +- if (commit->parents && commit->parents->next) { > +- strbuf_addstr(&ctx->message, ", reversing\nchanges made to "); > +- refer_to_commit(opts, &ctx->message, parent); > +- } > +- strbuf_addstr(&ctx->message, ".\n"); > ++ sequencer_format_revert_message(r, msg.subject, commit, > ++ parent, > ++ opts->commit_use_reference, > ++ &ctx->message); > + } else { > + const char *p; > > - if (commit->parents && commit->parents->next) { > @@ sequencer.c: int sequencer_pick_revisions(struct repository *r, > return res; > } > > -+void sequencer_format_revert_header(struct strbuf *out, > -+ const char *orig_subject, > -+ const struct object_id *oid) > ++void sequencer_format_revert_message(struct repository *r, > ++ const char *subject, > ++ const struct commit *commit, > ++ const struct commit *parent, > ++ bool use_commit_reference, > ++ struct strbuf *message) > +{ > -+ const char *revert_subject; > ++ const char *orig_subject; > + > -+ if (skip_prefix(orig_subject, "Revert \"", &revert_subject) && > -+ /* > -+ * We don't touch pre-existing repeated reverts, because > -+ * theoretically these can be nested arbitrarily deeply, > -+ * thus requiring excessive complexity to deal with. > -+ */ > -+ !starts_with(revert_subject, "Revert \"")) { > -+ strbuf_addstr(out, "Reapply \""); > -+ strbuf_addstr(out, revert_subject); > -+ strbuf_addch(out, '\n'); > ++ if (use_commit_reference) { > ++ strbuf_commented_addf(message, comment_line_str, > ++ "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); > ++ } else if (skip_prefix(subject, "Revert \"", &orig_subject) && > ++ /* > ++ * We don't touch pre-existing repeated reverts, because > ++ * theoretically these can be nested arbitrarily deeply, > ++ * thus requiring excessive complexity to deal with. > ++ */ > ++ !starts_with(orig_subject, "Revert \"")) { > ++ strbuf_addstr(message, "Reapply \""); > ++ strbuf_addstr(message, orig_subject); > ++ strbuf_addstr(message, "\n"); > + } else { > -+ strbuf_addstr(out, "Revert \""); > -+ strbuf_addstr(out, orig_subject); > -+ strbuf_addstr(out, "\"\n"); > ++ strbuf_addstr(message, "Revert \""); > ++ strbuf_addstr(message, subject); > ++ strbuf_addstr(message, "\"\n"); > + } > ++ strbuf_addstr(message, "\nThis reverts commit "); > ++ refer_to_commit(r, message, commit, use_commit_reference); > + > -+ strbuf_addstr(out, "\nThis reverts commit "); > -+ if (oid) { > -+ strbuf_addstr(out, oid_to_hex(oid)); > -+ strbuf_addstr(out, ".\n"); > ++ if (commit->parents && commit->parents->next) { > ++ strbuf_addstr(message, ", reversing\nchanges made to "); > ++ refer_to_commit(r, message, parent, use_commit_reference); > + } > ++ strbuf_addstr(message, ".\n"); > +} > + > void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) > @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh > int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); > > +/* > -+ * Formats a revert commit message following standard Git conventions. > -+ * Handles both regular reverts ("Revert \"<subject>\"") and revert of revert > -+ * cases ("Reapply \"<subject>\""). Adds "This reverts commit <oid>." if oid > -+ * is provided, otherwise just adds "This reverts commit " and the caller > -+ * should append the commit reference. > ++ * Formats a complete revert commit message following standard Git conventions. > ++ * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases > ++ * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts > ++ * commit <ref>." using either the abbreviated or full commit reference > ++ * depending on use_commit_reference. Also handles merge-parent references. > + */ > -+void sequencer_format_revert_header(struct strbuf *out, > -+ const char *orig_subject, > -+ const struct object_id *oid); > ++void sequencer_format_revert_message(struct repository *r, > ++ const char *subject, > ++ const struct commit *commit, > ++ const struct commit *parent, > ++ bool use_commit_reference, > ++ struct strbuf *message); > + > #endif /* SEQUENCER_H */ > 2: 066269706e ! 2: bea6229575 replay: add --revert mode to reverse commit changes > @@ Commit message > We swap the base and pickme trees passed to merge_incore_nonrecursive() > to reverse the diff direction. > > + Reverts are processed newest-first (matching git revert behavior) to > + reduce conflicts by peeling off changes from the top. Each revert > + builds on the result of the previous one via the last_commit fallback > + in the main replay loop, rather than relying on the parent-mapping > + used for cherry-pick. > + > Revert commit messages follow the usual git revert conventions: prefixed > with "Revert" (or "Reapply" when reverting a revert), and including > "This reverts commit <hash>.". The author is set to the current user > @@ Commit message > Helped-by: Phillip Wood <phillip.wood123@gmail.com> > Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> > Helped-by: Junio C Hamano <gitster@pobox.com> > + Helped-by: Toon Claes <toon@iotcl.com> > Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> > > ## Documentation/git-replay.adoc ## > @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them > +To revert commits on a branch: > + > +------------ > -+$ git replay --revert main main~2..main > ++$ git replay --revert main topic~2..topic > +------------ > + > -+This reverts the last two commits on `main`, creating two revert commits > -+on top of `main`, and updates `main` to point at the result. > ++This reverts the last two commits from `topic`, creating revert commits on > ++top of `main`, and updates `main` to point at the result. This is useful when > ++commits from `topic` were previously merged or cherry-picked into `main` and > ++need to be undone. > ++ > ++NOTE: For reverting an entire merge request as a single commit (rather than > ++commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` > ++which can avoid unnecessary merge conflicts. > + > GIT > --- > @@ builtin/replay.c: int cmd_replay(int argc, > > /* Parse ref action mode from command line or config */ > ref_mode = get_ref_action_mode(repo, ref_action); > +@@ builtin/replay.c: int cmd_replay(int argc, > + * some options changing these values if we think they could > + * be useful. > + */ > +- revs.reverse = 1; > ++ /* > ++ * Cherry-pick/rebase need oldest-first ordering so that each > ++ * replayed commit can build on its already-replayed parent. > ++ * Revert needs newest-first ordering (like git revert) to > ++ * reduce conflicts by peeling off changes from the top. > ++ */ > ++ revs.reverse = opts.revert ? 0 : 1; > + revs.sort_order = REV_SORT_IN_GRAPH_ORDER; > + revs.topo_order = 1; > + revs.simplify_history = 0; > +@@ builtin/replay.c: int cmd_replay(int argc, > + * Detect and warn if we override some user specified rev > + * walking options. > + */ > +- if (revs.reverse != 1) { > +- warning(_("some rev walking options will be overridden as " > +- "'%s' bit in 'struct rev_info' will be forced"), > +- "reverse"); > +- revs.reverse = 1; > ++ { > ++ int desired_reverse = opts.revert ? 0 : 1; > ++ if (revs.reverse != desired_reverse) { > ++ warning(_("some rev walking options will be overridden as " > ++ "'%s' bit in 'struct rev_info' will be forced"), > ++ "reverse"); > ++ revs.reverse = desired_reverse; > ++ } > + } > + if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { > + warning(_("some rev walking options will be overridden as " > @@ builtin/replay.c: int cmd_replay(int argc, > goto cleanup; > > @@ replay.c > #include "strmap.h" > #include "tree.h" > > +-/* > +- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but > +- * do not want to use the_repository. > +- */ > +-#define the_repository DO_NOT_USE_THE_REPOSITORY > +enum replay_mode { > + REPLAY_MODE_PICK, > + REPLAY_MODE_REVERT, > +}; > -+ > + > static const char *short_commit_name(struct repository *repo, > struct commit *commit) > - { > @@ replay.c: static char *get_author(const char *message) > return NULL; > } > @@ replay.c: static char *get_author(const char *message) > + subject_len = find_commit_subject(message, &subject_start); > + subject = xmemdupz(subject_start, subject_len); > + > -+ sequencer_format_revert_header(msg, subject, &commit->object.oid); > ++ sequencer_format_revert_message(repo, subject, commit, > ++ commit->parents ? commit->parents->item : NULL, > ++ false, msg); > + > + free(subject); > + repo_unuse_commit_buffer(repo, commit, message); > @@ replay.c: static void get_ref_information(struct repository *repo, > + } > + *onto = peel_committish(repo, *branch_name, option_name); > + if (rinfo->positive_refexprs > 1) > -+ die(_("cannot %s target with multiple sources because ordering would be ill-defined"), > -+ option_name + 2); /* skip "--" prefix */ > ++ die(_("'%s' cannot be used with multiple revision ranges " > ++ "because the ordering would be ill-defined"), > ++ option_name); > +} > + > static void set_up_replay_mode(struct repository *repo, > @@ replay.c: static struct commit *pick_regular_commit(struct repository *repo, > + merge_opt->branch2 = NULL; > if (!result->clean) > return NULL; > -- /* Drop commits that become empty */ > -- if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && > -+ /* Drop commits that become empty (only for picks) */ > -+ if (mode == REPLAY_MODE_PICK && > -+ oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && > + /* Drop commits that become empty */ > + if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && > !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) > return replayed_base; > - return create_commit(repo, result->tree, pickme, replayed_base); > @@ replay.c: int replay_revisions(struct rev_info *revs, > > last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, > - onto, &merge_opt, &result); > -+ onto, &merge_opt, &result, mode); > ++ mode == REPLAY_MODE_REVERT ? last_commit : onto, > ++ &merge_opt, &result, mode); > if (!last_commit) > break; > > @@ t/t3650-replay-basics.sh: test_expect_success 'no base or negative ref gives no- > test_must_fail git replay --advance=main --contained \ > topic1..topic2 2>actual && > test_cmp expect actual > + ' > + > + test_expect_success 'cannot advance target ... ordering would be ill-defined' ' > +- echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && > ++ cat >expect <<-\EOF && > ++ fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined > ++ EOF > + test_must_fail git replay --advance=main main topic1 topic2 2>actual && > + test_cmp expect actual > + ' > @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' ' > test_grep "invalid.*replay.refAction.*value" error > ' > @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' ' > +' > + > +test_expect_success 'cannot revert with multiple sources' ' > -+ echo "fatal: cannot revert target with multiple sources because ordering would be ill-defined" >expect && > ++ cat >expect <<-\EOF && > ++ fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined > ++ EOF > + test_must_fail git replay --revert main main topic1 topic2 2>actual && > + test_cmp expect actual > +' > @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' ' > + # Revert commits I and J > + git replay --revert topic4 topic4~2..topic4 && > + > -+ # Verify the revert commits were created > ++ # Verify the revert commits were created (newest-first ordering > ++ # means J is reverted first, then I on top) > + git log --format=%s -4 topic4 >actual && > + cat >expect <<-\EOF && > -+ Revert "J" > + Revert "I" > ++ Revert "J" > + J > + I > + EOF > + test_cmp expect actual && > + > -+ # Verify commit message format includes hash > ++ # Verify commit message format includes hash (tip is Revert "I") > + test_commit_message topic4 <<-EOF && > -+ Revert "J" > ++ Revert "I" > + > -+ This reverts commit $(git rev-parse J). > ++ This reverts commit $(git rev-parse I). > + EOF > + > + # Verify reflog message > > > base-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2 > > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v4 0/2] replay: add --revert mode to reverse commit changes 2026-03-13 5:40 ` [PATCH v4 0/2] " Siddharth Asthana ` (2 preceding siblings ...) 2026-03-16 16:59 ` [PATCH v4 0/2] " Phillip Wood @ 2026-03-16 19:53 ` Toon Claes 2026-03-24 22:03 ` [PATCH v5 " Siddharth Asthana 4 siblings, 0 replies; 92+ messages in thread From: Toon Claes @ 2026-03-16 19:53 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, Siddharth Asthana Siddharth Asthana <siddharthasthana31@gmail.com> writes: > Hi, > > git replay currently supports cherry-picking (--advance) and rebasing > (--onto), but not reverting. We need this at GitLab for Gitaly to > reverse commits directly on bare repositories without a checkout. > > The approach is the same as sequencer.c -- cherry-pick and revert are > just the same three-way merge with swapped arguments. We swap the base > and pickme trees passed to merge_incore_nonrecursive() to reverse the > diff direction. > > Patch 1 extracts the full revert message formatting logic into a new > sequencer_format_revert_message() function, following Phillip's > suggestion to move everything into one shared function rather than > just the header. refer_to_commit() is updated to take a struct > repository and a bool instead of replay_opts so it works outside the > sequencer. > > Patch 2 adds --revert <branch> as a standalone mode. Reverts are > processed newest-first (matching git revert) to reduce conflicts by > peeling off changes from the top. > > The series is based on top of d181b9354c (The 13th batch, 2026-03-07). > > CI: https://gitlab.com/gitlab-org/git/-/pipelines/2329880894 > The msvc-meson / Chocolatey failures are pre-existing infrastructure > issues unrelated to this series. > > Changes in v4: > - Replaced sequencer_format_revert_header() with a more complete > sequencer_format_revert_message() that handles everything: subject > prefix, commit reference via refer_to_commit(), and merge-parent > references -- per Phillip > - Updated refer_to_commit() signature to take (struct repository *r, > bool use_commit_reference) instead of (struct replay_opts *opts) > - Reverts are now newest-first (revs.reverse = 0 for --revert), > chaining on last_commit rather than the parent mapping > - Changed doc example to cross-branch scenario and restored the > merge-tree NOTE > - Updated error message format to "'--revert' cannot be used with > multiple revision ranges..." (and same for --advance) > - Empty revert commits are now dropped, consistent with cherry-pick > - Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/ > - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/ > - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/ > > Thanks, > Siddharth I've added various nitpicks here and there. Overall nothing really blocking for a reroll I think, but maybe the combination of them all is. -- Cheers, Toon ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v5 0/2] replay: add --revert mode to reverse commit changes 2026-03-13 5:40 ` [PATCH v4 0/2] " Siddharth Asthana ` (3 preceding siblings ...) 2026-03-16 19:53 ` Toon Claes @ 2026-03-24 22:03 ` Siddharth Asthana 2026-03-24 22:04 ` [PATCH v5 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana ` (2 more replies) 4 siblings, 3 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-24 22:03 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana Hi, git replay currently supports cherry-picking (--advance) and rebasing (--onto), but not reverting. We need this at GitLab for Gitaly to reverse commits directly on bare repositories without a checkout. The approach is the same as sequencer.c -- cherry-pick and revert are just the same three-way merge with swapped arguments. We swap the base and pickme trees passed to merge_incore_nonrecursive() to reverse the diff direction. Patch 1 extracts the full revert message formatting logic into a new sequencer_format_revert_message() function that handles everything in one shared function rather than just the header. refer_to_commit() is updated to take a struct repository and a bool instead of replay_opts so it works outside the sequencer. Patch 2 adds --revert <branch> as a standalone mode. Reverts are processed newest-first (matching git revert) to reduce conflicts by peeling off changes from the top. The series is based on top of d181b9354c (The 13th batch, 2026-03-07). Changes in v5: - Made sequencer_format_revert_message() header comment more concise, using single quotes to avoid nested escaped double quotes - Moved desired_reverse declaration up and removed the bare braces, so the same named variable is used when setting and checking revs.reverse - Used die_for_incompatible_opt2() for --advance/--contained and --revert/--contained instead of die("--contained requires --onto") - Dropped the erroneously re-added ellipsis from <revision-range>... in the SYNOPSIS - Restored #define the_repository DO_NOT_USE_THE_REPOSITORY guard in replay.c - Used ${SQ} instead of '"'"' for single-quote escaping in tests - Link to v4: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/ - Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/ - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/ - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/ Thanks, Siddharth --- Siddharth Asthana (2): sequencer: extract revert message formatting into shared function replay: add --revert mode to reverse commit changes Documentation/git-replay.adoc | 43 ++++++++- builtin/replay.c | 35 ++++++-- replay.c | 161 +++++++++++++++++++++++++--------- replay.h | 11 ++- sequencer.c | 78 +++++++++------- sequencer.h | 13 +++ t/t3650-replay-basics.sh | 111 +++++++++++++++++++++-- 7 files changed, 355 insertions(+), 97 deletions(-) Range-diff versus v4: 1: bdc710b265 ! 1: 6bd2ce4515 sequencer: extract revert message formatting into shared function @@ sequencer.h: int sequencer_determine_whence(struct repository *r, enum commit_wh int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +/* -+ * Formats a complete revert commit message following standard Git conventions. -+ * Handles regular reverts ("Revert \"<subject>\""), revert of revert cases -+ * ("Reapply \"<subject>\""), and the --reference style. Appends "This reverts -+ * commit <ref>." using either the abbreviated or full commit reference -+ * depending on use_commit_reference. Also handles merge-parent references. ++ * Format a revert commit message with appropriate 'Revert "<subject>"' or ++ * 'Reapply "<subject>"' prefix and 'This reverts commit <ref>.' body. ++ * When use_commit_reference is set, <ref> is an abbreviated hash with ++ * subject and date; otherwise the full hex hash is used. + */ +void sequencer_format_revert_message(struct repository *r, + const char *subject, 2: bea6229575 ! 2: 9fd92497b9 replay: add --revert mode to reverse commit changes @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>... ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range> DESCRIPTION ----------- @@ builtin/replay.c: int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto <newbase> | --advance <branch>) " -- "[--ref-action[=<mode>]] <revision-range>"), + "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) " -+ "[--ref-action[=<mode>]] <revision-range>..."), + "[--ref-action[=<mode>]] <revision-range>"), NULL }; - struct option replay_options[] = { @@ builtin/replay.c: int cmd_replay(int argc, N_("replay onto given commit")), OPT_BOOL(0, "contained", &opts.contained, @@ builtin/replay.c: int cmd_replay(int argc, usage_with_options(replay_usage, replay_options); } -- die_for_incompatible_opt2(!!opts.advance, "--advance", -- opts.contained, "--contained"); -- die_for_incompatible_opt2(!!opts.advance, "--advance", -- !!opts.onto, "--onto"); + die_for_incompatible_opt3(!!opts.onto, "--onto", + !!opts.advance, "--advance", + !!opts.revert, "--revert"); -+ if (opts.contained && !opts.onto) -+ die(_("--contained requires --onto")); + die_for_incompatible_opt2(!!opts.advance, "--advance", + opts.contained, "--contained"); +- die_for_incompatible_opt2(!!opts.advance, "--advance", +- !!opts.onto, "--onto"); ++ die_for_incompatible_opt2(!!opts.revert, "--revert", ++ opts.contained, "--contained"); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); -@@ builtin/replay.c: int cmd_replay(int argc, - * some options changing these values if we think they could - * be useful. - */ -- revs.reverse = 1; + + /* + * Cherry-pick/rebase need oldest-first ordering so that each + * replayed commit can build on its already-replayed parent. + * Revert needs newest-first ordering (like git revert) to + * reduce conflicts by peeling off changes from the top. + */ -+ revs.reverse = opts.revert ? 0 : 1; ++ int desired_reverse = !opts.revert; ++ + repo_init_revisions(repo, &revs, prefix); + + /* +@@ builtin/replay.c: int cmd_replay(int argc, + * some options changing these values if we think they could + * be useful. + */ +- revs.reverse = 1; ++ revs.reverse = desired_reverse; revs.sort_order = REV_SORT_IN_GRAPH_ORDER; revs.topo_order = 1; revs.simplify_history = 0; @@ builtin/replay.c: int cmd_replay(int argc, * walking options. */ - if (revs.reverse != 1) { -- warning(_("some rev walking options will be overridden as " -- "'%s' bit in 'struct rev_info' will be forced"), -- "reverse"); ++ if (revs.reverse != desired_reverse) { + warning(_("some rev walking options will be overridden as " + "'%s' bit in 'struct rev_info' will be forced"), + "reverse"); - revs.reverse = 1; -+ { -+ int desired_reverse = opts.revert ? 0 : 1; -+ if (revs.reverse != desired_reverse) { -+ warning(_("some rev walking options will be overridden as " -+ "'%s' bit in 'struct rev_info' will be forced"), -+ "reverse"); -+ revs.reverse = desired_reverse; -+ } ++ revs.reverse = desired_reverse; } if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { warning(_("some rev walking options will be overridden as " @@ replay.c #include "strmap.h" #include "tree.h" --/* -- * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but -- * do not want to use the_repository. -- */ --#define the_repository DO_NOT_USE_THE_REPOSITORY +@@ + */ + #define the_repository DO_NOT_USE_THE_REPOSITORY + +enum replay_mode { + REPLAY_MODE_PICK, + REPLAY_MODE_REVERT, +}; - ++ static const char *short_commit_name(struct repository *repo, struct commit *commit) + { @@ replay.c: static char *get_author(const char *message) return NULL; } @@ t/t3650-replay-basics.sh: test_expect_success 'no base or negative ref gives no- -test_expect_success 'options --advance and --contained cannot be used together' ' - printf "fatal: options ${SQ}--advance${SQ} " >expect && - printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect && -+test_expect_success '--contained requires --onto' ' -+ echo "fatal: --contained requires --onto" >expect && ++test_expect_success '--advance and --contained cannot be used together' ' test_must_fail git replay --advance=main --contained \ topic1..topic2 2>actual && - test_cmp expect actual +- test_cmp expect actual ++ test_grep "cannot be used together" actual ' test_expect_success 'cannot advance target ... ordering would be ill-defined' ' - echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && -+ cat >expect <<-\EOF && -+ fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined -+ EOF ++ echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect && test_must_fail git replay --advance=main main topic1 topic2 2>actual && test_cmp expect actual ' @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' ' +' + +test_expect_success 'cannot revert with multiple sources' ' -+ cat >expect <<-\EOF && -+ fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined -+ EOF ++ echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect && + test_must_fail git replay --revert main main topic1 topic2 2>actual && + test_cmp expect actual +' @@ t/t3650-replay-basics.sh: test_expect_success 'invalid replay.refAction value' ' + +test_expect_success 'git replay --revert incompatible with --contained' ' + test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error && -+ test_grep "requires --onto" error ++ test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --onto' ' base-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2 -- 2.51.0 ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v5 1/2] sequencer: extract revert message formatting into shared function 2026-03-24 22:03 ` [PATCH v5 " Siddharth Asthana @ 2026-03-24 22:04 ` Siddharth Asthana 2026-03-24 22:04 ` [PATCH v5 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 0/2] " Siddharth Asthana 2 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-24 22:04 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana The logic for formatting revert commit messages (handling "Revert" and "Reapply" cases, appending "This reverts commit <ref>.", and handling merge-parent references) currently lives inline in do_pick_commit(). The upcoming replay --revert mode needs to reuse this logic. Extract all of this into a new sequencer_format_revert_message() function. The function takes a repository, the subject line, commit, parent, a use_commit_reference flag, and the output strbuf. It handles both regular reverts ("Revert "<subject>"") and revert-of-revert cases ("Reapply "<subject>""), and uses refer_to_commit() internally to format the commit reference. Update refer_to_commit() to take a struct repository parameter instead of relying on the_repository, and a bool instead of reading from replay_opts directly. This makes it usable from the new shared function without pulling in sequencer-specific state. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- sequencer.c | 78 +++++++++++++++++++++++++++++++---------------------- sequencer.h | 13 +++++++++ 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/sequencer.c b/sequencer.c index aafd0bc959..7bf9d6ad19 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) { return opts->edit; } -static void refer_to_commit(struct replay_opts *opts, - struct strbuf *msgbuf, struct commit *commit) +static void refer_to_commit(struct repository *r, struct strbuf *msgbuf, + const struct commit *commit, + bool use_commit_reference) { - if (opts->commit_use_reference) { + if (use_commit_reference) { struct pretty_print_context ctx = { .abbrev = DEFAULT_ABBREV, .date_mode.type = DATE_SHORT, }; - repo_format_commit_message(the_repository, commit, + repo_format_commit_message(r, commit, "%h (%s, %ad)", msgbuf, &ctx); } else { strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid)); @@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r, */ if (command == TODO_REVERT) { - const char *orig_subject; - base = commit; base_label = msg.label; next = parent; next_label = msg.parent_label; - if (opts->commit_use_reference) { - strbuf_commented_addf(&ctx->message, comment_line_str, - "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && - /* - * We don't touch pre-existing repeated reverts, because - * theoretically these can be nested arbitrarily deeply, - * thus requiring excessive complexity to deal with. - */ - !starts_with(orig_subject, "Revert \"")) { - strbuf_addstr(&ctx->message, "Reapply \""); - strbuf_addstr(&ctx->message, orig_subject); - strbuf_addstr(&ctx->message, "\n"); - } else { - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); - } - strbuf_addstr(&ctx->message, "\nThis reverts commit "); - refer_to_commit(opts, &ctx->message, commit); - - if (commit->parents && commit->parents->next) { - strbuf_addstr(&ctx->message, ", reversing\nchanges made to "); - refer_to_commit(opts, &ctx->message, parent); - } - strbuf_addstr(&ctx->message, ".\n"); + sequencer_format_revert_message(r, msg.subject, commit, + parent, + opts->commit_use_reference, + &ctx->message); } else { const char *p; @@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r, return res; } +void sequencer_format_revert_message(struct repository *r, + const char *subject, + const struct commit *commit, + const struct commit *parent, + bool use_commit_reference, + struct strbuf *message) +{ + const char *orig_subject; + + if (use_commit_reference) { + strbuf_commented_addf(message, comment_line_str, + "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); + } else if (skip_prefix(subject, "Revert \"", &orig_subject) && + /* + * We don't touch pre-existing repeated reverts, because + * theoretically these can be nested arbitrarily deeply, + * thus requiring excessive complexity to deal with. + */ + !starts_with(orig_subject, "Revert \"")) { + strbuf_addstr(message, "Reapply \""); + strbuf_addstr(message, orig_subject); + strbuf_addstr(message, "\n"); + } else { + strbuf_addstr(message, "Revert \""); + strbuf_addstr(message, subject); + strbuf_addstr(message, "\"\n"); + } + strbuf_addstr(message, "\nThis reverts commit "); + refer_to_commit(r, message, commit, use_commit_reference); + + if (commit->parents && commit->parents->next) { + strbuf_addstr(message, ", reversing\nchanges made to "); + refer_to_commit(r, message, parent, use_commit_reference); + } + strbuf_addstr(message, ".\n"); +} + void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) { unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP; diff --git a/sequencer.h b/sequencer.h index 719684c8a9..56cd50233a 100644 --- a/sequencer.h +++ b/sequencer.h @@ -271,4 +271,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) */ int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +/* + * Format a revert commit message with appropriate 'Revert "<subject>"' or + * 'Reapply "<subject>"' prefix and 'This reverts commit <ref>.' body. + * When use_commit_reference is set, <ref> is an abbreviated hash with + * subject and date; otherwise the full hex hash is used. + */ +void sequencer_format_revert_message(struct repository *r, + const char *subject, + const struct commit *commit, + const struct commit *parent, + bool use_commit_reference, + struct strbuf *message); + #endif /* SEQUENCER_H */ -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* [PATCH v5 2/2] replay: add --revert mode to reverse commit changes 2026-03-24 22:03 ` [PATCH v5 " Siddharth Asthana 2026-03-24 22:04 ` [PATCH v5 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana @ 2026-03-24 22:04 ` Siddharth Asthana 2026-03-25 6:29 ` Junio C Hamano 2026-03-25 20:23 ` [PATCH v6 0/2] " Siddharth Asthana 2 siblings, 1 reply; 92+ messages in thread From: Siddharth Asthana @ 2026-03-24 22:04 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana, Johannes Schindelin Add a `--revert <branch>` mode to git replay that undoes the changes introduced by the specified commits. Like --onto and --advance, --revert is a standalone mode: it takes a branch argument and updates that branch with the newly created revert commits. At GitLab, we need this in Gitaly for reverting commits directly on bare repositories without requiring a working tree checkout. The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick and revert are just the same three-way merge with swapped arguments: - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) - Revert: merge(ancestor=commit, ours=current, theirs=parent) We swap the base and pickme trees passed to merge_incore_nonrecursive() to reverse the diff direction. Reverts are processed newest-first (matching git revert behavior) to reduce conflicts by peeling off changes from the top. Each revert builds on the result of the previous one via the last_commit fallback in the main replay loop, rather than relying on the parent-mapping used for cherry-pick. Revert commit messages follow the usual git revert conventions: prefixed with "Revert" (or "Reapply" when reverting a revert), and including "This reverts commit <hash>.". The author is set to the current user rather than preserving the original author, matching git revert behavior. Helped-by: Christian Couder <christian.couder@gmail.com> Helped-by: Patrick Steinhardt <ps@pks.im> Helped-by: Elijah Newren <newren@gmail.com> Helped-by: Phillip Wood <phillip.wood123@gmail.com> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Helped-by: Junio C Hamano <gitster@pobox.com> Helped-by: Toon Claes <toon@iotcl.com> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- Documentation/git-replay.adoc | 43 ++++++++- builtin/replay.c | 35 ++++++-- replay.c | 161 +++++++++++++++++++++++++--------- replay.h | 11 ++- t/t3650-replay-basics.sh | 111 +++++++++++++++++++++-- 5 files changed, 296 insertions(+), 65 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index 8d696ce3ab..746312e9c6 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t SYNOPSIS -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range> DESCRIPTION ----------- @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to point at the tip of the resulting history. This is different from `--onto`, which uses the target only as a starting point without updating it. +--revert <branch>:: + Starting point at which to create the reverted commits; must be a + branch name. ++ +When `--revert` is specified, the commits in the revision range are reverted +(their changes are undone) and the reverted commits are created on top of +<branch>. The <branch> is then updated to point at the new commits. This is +the same as running `git revert <revision-range>` but does not update the +working tree. ++ +The commit messages follow `git revert` conventions: they are prefixed with +"Revert" and include "This reverts commit <hash>." When reverting a commit +whose message starts with "Revert", the new message uses "Reapply" instead. +Unlike cherry-pick which preserves the original author, revert commits use +the current user as the author, matching the behavior of `git revert`. ++ +This option is mutually exclusive with `--onto` and `--advance`. It is also +incompatible with `--contained` (which is a modifier for `--onto` only). + --contained:: Update all branches that point at commits in <revision-range>. Requires `--onto`. @@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH} where the number of refs updated depends on the arguments passed and -the shape of the history being replayed. When using `--advance`, the -number of refs updated is always one, but for `--onto`, it can be one -or more (rebasing multiple branches simultaneously is supported). +the shape of the history being replayed. When using `--advance` or +`--revert`, the number of refs updated is always one, but for `--onto`, +it can be one or more (rebasing multiple branches simultaneously is +supported). There is no stderr output on conflicts; see the <<exit-status,EXIT STATUS>> section below. @@ -152,6 +172,21 @@ all commits they have since `base`, playing them on top of `origin/main`. These three branches may have commits on top of `base` that they have in common, but that does not need to be the case. +To revert commits on a branch: + +------------ +$ git replay --revert main topic~2..topic +------------ + +This reverts the last two commits from `topic`, creating revert commits on +top of `main`, and updates `main` to point at the result. This is useful when +commits from `topic` were previously merged or cherry-picked into `main` and +need to be undone. + +NOTE: For reverting an entire merge request as a single commit (rather than +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` +which can avoid unnecessary merge conflicts. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/replay.c b/builtin/replay.c index 2cdde830a8..d3c1d920f0 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -83,7 +83,7 @@ int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto <newbase> | --advance <branch>) " + "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) " "[--ref-action[=<mode>]] <revision-range>"), NULL }; @@ -96,6 +96,9 @@ int cmd_replay(int argc, N_("replay onto given commit")), OPT_BOOL(0, "contained", &opts.contained, N_("update all branches that point at commits in <revision-range>")), + OPT_STRING(0, "revert", &opts.revert, + N_("branch"), + N_("revert commits onto given branch")), OPT_STRING(0, "ref-action", &ref_action, N_("mode"), N_("control ref update behavior (update|print)")), @@ -105,19 +108,31 @@ int cmd_replay(int argc, argc = parse_options(argc, argv, prefix, replay_options, replay_usage, PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); - if (!opts.onto && !opts.advance) { - error(_("option --onto or --advance is mandatory")); + /* Exactly one mode must be specified */ + if (!opts.onto && !opts.advance && !opts.revert) { + error(_("exactly one of --onto, --advance, or --revert is required")); usage_with_options(replay_usage, replay_options); } + die_for_incompatible_opt3(!!opts.onto, "--onto", + !!opts.advance, "--advance", + !!opts.revert, "--revert"); die_for_incompatible_opt2(!!opts.advance, "--advance", opts.contained, "--contained"); - die_for_incompatible_opt2(!!opts.advance, "--advance", - !!opts.onto, "--onto"); + die_for_incompatible_opt2(!!opts.revert, "--revert", + opts.contained, "--contained"); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); + /* + * Cherry-pick/rebase need oldest-first ordering so that each + * replayed commit can build on its already-replayed parent. + * Revert needs newest-first ordering (like git revert) to + * reduce conflicts by peeling off changes from the top. + */ + int desired_reverse = !opts.revert; + repo_init_revisions(repo, &revs, prefix); /* @@ -129,7 +144,7 @@ int cmd_replay(int argc, * some options changing these values if we think they could * be useful. */ - revs.reverse = 1; + revs.reverse = desired_reverse; revs.sort_order = REV_SORT_IN_GRAPH_ORDER; revs.topo_order = 1; revs.simplify_history = 0; @@ -144,11 +159,11 @@ int cmd_replay(int argc, * Detect and warn if we override some user specified rev * walking options. */ - if (revs.reverse != 1) { + if (revs.reverse != desired_reverse) { warning(_("some rev walking options will be overridden as " "'%s' bit in 'struct rev_info' will be forced"), "reverse"); - revs.reverse = 1; + revs.reverse = desired_reverse; } if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { warning(_("some rev walking options will be overridden as " @@ -174,7 +189,9 @@ int cmd_replay(int argc, goto cleanup; /* Build reflog message */ - if (opts.advance) { + if (opts.revert) { + strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert); + } else if (opts.advance) { strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance); } else { struct object_id oid; diff --git a/replay.c b/replay.c index a63f6714c4..d7239d4c83 100644 --- a/replay.c +++ b/replay.c @@ -8,6 +8,7 @@ #include "refs.h" #include "replay.h" #include "revision.h" +#include "sequencer.h" #include "strmap.h" #include "tree.h" @@ -17,6 +18,11 @@ */ #define the_repository DO_NOT_USE_THE_REPOSITORY +enum replay_mode { + REPLAY_MODE_PICK, + REPLAY_MODE_REVERT, +}; + static const char *short_commit_name(struct repository *repo, struct commit *commit) { @@ -50,15 +56,37 @@ static char *get_author(const char *message) return NULL; } +static void generate_revert_message(struct strbuf *msg, + struct commit *commit, + struct repository *repo) +{ + const char *out_enc = get_commit_output_encoding(); + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); + const char *subject_start; + int subject_len; + char *subject; + + subject_len = find_commit_subject(message, &subject_start); + subject = xmemdupz(subject_start, subject_len); + + sequencer_format_revert_message(repo, subject, commit, + commit->parents ? commit->parents->item : NULL, + false, msg); + + free(subject); + repo_unuse_commit_buffer(repo, commit, message); +} + static struct commit *create_commit(struct repository *repo, struct tree *tree, struct commit *based_on, - struct commit *parent) + struct commit *parent, + enum replay_mode mode) { struct object_id ret; struct object *obj = NULL; struct commit_list *parents = NULL; - char *author; + char *author = NULL; char *sign_commit = NULL; /* FIXME: cli users might want to sign again */ struct commit_extra_header *extra = NULL; struct strbuf msg = STRBUF_INIT; @@ -70,9 +98,16 @@ static struct commit *create_commit(struct repository *repo, commit_list_insert(parent, &parents); extra = read_commit_extra_headers(based_on, exclude_gpgsig); - find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); + if (mode == REPLAY_MODE_REVERT) { + generate_revert_message(&msg, based_on, repo); + /* For revert, use current user as author (NULL = use default) */ + } else if (mode == REPLAY_MODE_PICK) { + find_commit_subject(message, &orig_message); + strbuf_addstr(&msg, orig_message); + author = get_author(message); + } else { + BUG("unexpected replay mode %d", mode); + } reset_ident_date(); if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, &ret, author, NULL, sign_commit, extra)) { @@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo, } } +static void set_up_branch_mode(struct repository *repo, + char **branch_name, + const char *option_name, + struct ref_info *rinfo, + struct commit **onto) +{ + struct object_id oid; + char *fullname = NULL; + + if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name), + &oid, &fullname, 0) == 1) { + free(*branch_name); + *branch_name = fullname; + } else { + die(_("argument to %s must be a reference"), option_name); + } + *onto = peel_committish(repo, *branch_name, option_name); + if (rinfo->positive_refexprs > 1) + die(_("'%s' cannot be used with multiple revision ranges " + "because the ordering would be ill-defined"), + option_name); +} + static void set_up_replay_mode(struct repository *repo, struct rev_cmdline_info *cmd_info, const char *onto_name, bool *detached_head, char **advance_name, + char **revert_name, struct commit **onto, struct strset **update_refs) { @@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo, if (!rinfo.positive_refexprs) die(_("need some commits to replay")); - if (!onto_name == !*advance_name) - BUG("one and only one of onto_name and *advance_name must be given"); - if (onto_name) { *onto = peel_committish(repo, onto_name, "--onto"); if (rinfo.positive_refexprs < @@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo, *update_refs = xcalloc(1, sizeof(**update_refs)); **update_refs = rinfo.positive_refs; memset(&rinfo.positive_refs, 0, sizeof(**update_refs)); + } else if (*advance_name) { + set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto); + } else if (*revert_name) { + set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto); } else { - struct object_id oid; - char *fullname = NULL; - - if (!*advance_name) - BUG("expected either onto_name or *advance_name in this function"); - - if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name), - &oid, &fullname, 0) == 1) { - free(*advance_name); - *advance_name = fullname; - } else { - die(_("argument to --advance must be a reference")); - } - *onto = peel_committish(repo, *advance_name, "--advance"); - if (rinfo.positive_refexprs > 1) - die(_("cannot advance target with multiple sources because ordering would be ill-defined")); + BUG("expected one of onto_name, *advance_name, or *revert_name"); } strset_clear(&rinfo.negative_refs); strset_clear(&rinfo.positive_refs); @@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo, kh_oid_map_t *replayed_commits, struct commit *onto, struct merge_options *merge_opt, - struct merge_result *result) + struct merge_result *result, + enum replay_mode mode) { struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree, *replayed_base_tree; @@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo, pickme_tree = repo_get_commit_tree(repo, pickme); base_tree = repo_get_commit_tree(repo, base); - merge_opt->branch1 = short_commit_name(repo, replayed_base); - merge_opt->branch2 = short_commit_name(repo, pickme); - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); - - merge_incore_nonrecursive(merge_opt, - base_tree, - replayed_base_tree, - pickme_tree, - result); - - free((char*)merge_opt->ancestor); + if (mode == REPLAY_MODE_PICK) { + /* Cherry-pick: normal order */ + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = short_commit_name(repo, pickme); + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + + merge_incore_nonrecursive(merge_opt, + base_tree, + replayed_base_tree, + pickme_tree, + result); + + free((char *)merge_opt->ancestor); + } else if (mode == REPLAY_MODE_REVERT) { + /* Revert: swap base and pickme to reverse the diff */ + const char *pickme_name = short_commit_name(repo, pickme); + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); + merge_opt->ancestor = pickme_name; + + merge_incore_nonrecursive(merge_opt, + pickme_tree, + replayed_base_tree, + base_tree, + result); + + free((char *)merge_opt->branch2); + } else { + BUG("unexpected replay mode %d", mode); + } merge_opt->ancestor = NULL; + merge_opt->branch2 = NULL; if (!result->clean) return NULL; /* Drop commits that become empty */ if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) return replayed_base; - return create_commit(repo, result->tree, pickme, replayed_base); + return create_commit(repo, result->tree, pickme, replayed_base, mode); } void replay_result_release(struct replay_result *result) @@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs, }; bool detached_head; char *advance; + char *revert; + enum replay_mode mode = REPLAY_MODE_PICK; int ret; advance = xstrdup_or_null(opts->advance); + revert = xstrdup_or_null(opts->revert); + if (revert) + mode = REPLAY_MODE_REVERT; set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, - &detached_head, &advance, &onto, &update_refs); + &detached_head, &advance, &revert, &onto, &update_refs); /* FIXME: Should allow replaying commits with the first as a root commit */ @@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs, die(_("replaying merge commits is not supported yet!")); last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, - onto, &merge_opt, &result); + mode == REPLAY_MODE_REVERT ? last_commit : onto, + &merge_opt, &result, mode); if (!last_commit) break; @@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs, kh_value(replayed_commits, pos) = last_commit; /* Update any necessary branches */ - if (advance) + if (advance || revert) continue; for (decoration = get_name_decoration(&commit->object); @@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs, goto out; } - /* In --advance mode, advance the target ref */ - if (advance) - replay_result_queue_update(out, advance, + /* In --advance or --revert mode, update the target ref */ + if (advance || revert) { + const char *ref = advance ? advance : revert; + replay_result_queue_update(out, ref, &onto->object.oid, &last_commit->object.oid); + } ret = 0; @@ -377,5 +451,6 @@ int replay_revisions(struct rev_info *revs, kh_destroy_oid_map(replayed_commits); merge_finalize(&merge_opt, &result); free(advance); + free(revert); return ret; } diff --git a/replay.h b/replay.h index d8407dc7f7..e916a5f975 100644 --- a/replay.h +++ b/replay.h @@ -13,7 +13,7 @@ struct replay_revisions_options { /* * Starting point at which to create the new commits; must be a branch * name. The branch will be updated to point to the rewritten commits. - * This option is mutually exclusive with `onto`. + * This option is mutually exclusive with `onto` and `revert`. */ const char *advance; @@ -22,7 +22,14 @@ struct replay_revisions_options { * committish. References pointing at decendants of `onto` will be * updated to point to the new commits. */ - const char *onto; + const char *onto; + + /* + * Starting point at which to create revert commits; must be a branch + * name. The branch will be updated to point to the revert commits. + * This option is mutually exclusive with `onto` and `advance`. + */ + const char *revert; /* * Update branches that point at commits in the given revision range. diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index a03f8f9293..217f6fb292 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' ' test_cmp expect actual ' -test_expect_success 'option --onto or --advance is mandatory' ' - echo "error: option --onto or --advance is mandatory" >expect && +test_expect_success 'exactly one of --onto, --advance, or --revert is required' ' + echo "error: exactly one of --onto, --advance, or --revert is required" >expect && test_might_fail git replay -h >>expect && test_must_fail git replay topic1..topic2 2>actual && test_cmp expect actual @@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err test_cmp expect actual ' -test_expect_success 'options --advance and --contained cannot be used together' ' - printf "fatal: options ${SQ}--advance${SQ} " >expect && - printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect && +test_expect_success '--advance and --contained cannot be used together' ' test_must_fail git replay --advance=main --contained \ topic1..topic2 2>actual && - test_cmp expect actual + test_grep "cannot be used together" actual ' test_expect_success 'cannot advance target ... ordering would be ill-defined' ' - echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && + echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect && test_must_fail git replay --advance=main main topic1 topic2 2>actual && test_cmp expect actual ' @@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' ' test_grep "invalid.*replay.refAction.*value" error ' +test_expect_success 'argument to --revert must be a reference' ' + echo "fatal: argument to --revert must be a reference" >expect && + oid=$(git rev-parse main) && + test_must_fail git replay --revert=$oid topic1..topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'cannot revert with multiple sources' ' + echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect && + test_must_fail git replay --revert main main topic1 topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'using replay --revert to revert commits' ' + # Reuse existing topic4 branch (has commits I and J on top of main) + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # Revert commits I and J + git replay --revert topic4 topic4~2..topic4 && + + # Verify the revert commits were created (newest-first ordering + # means J is reverted first, then I on top) + git log --format=%s -4 topic4 >actual && + cat >expect <<-\EOF && + Revert "I" + Revert "J" + J + I + EOF + test_cmp expect actual && + + # Verify commit message format includes hash (tip is Revert "I") + test_commit_message topic4 <<-EOF && + Revert "I" + + This reverts commit $(git rev-parse I). + EOF + + # Verify reflog message + git reflog topic4 -1 --format=%gs >reflog-msg && + echo "replay --revert topic4" >expect-reflog && + test_cmp expect-reflog reflog-msg +' + +test_expect_success 'using replay --revert in bare repo' ' + # Reuse existing topic4 in bare repo + START=$(git -C bare rev-parse topic4) && + test_when_finished "git -C bare update-ref refs/heads/topic4 $START" && + + # Revert commit J in bare repo + git -C bare replay --revert topic4 topic4~1..topic4 && + + # Verify revert was created + git -C bare log -1 --format=%s topic4 >actual && + echo "Revert \"J\"" >expect && + test_cmp expect actual +' + +test_expect_success 'revert of revert uses Reapply' ' + # Use topic4 and first revert J, then revert the revert + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # First revert J + git replay --revert topic4 topic4~1..topic4 && + REVERT_J=$(git rev-parse topic4) && + + # Now revert the revert - should become Reapply + git replay --revert topic4 topic4~1..topic4 && + + # Verify Reapply prefix and message format + test_commit_message topic4 <<-EOF + Reapply "J" + + This reverts commit $REVERT_J. + EOF +' + +test_expect_success 'git replay --revert with conflict' ' + # conflict branch has C.conflict which conflicts with topic1s C + test_expect_code 1 git replay --revert conflict B..topic1 +' + +test_expect_success 'git replay --revert incompatible with --contained' ' + test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --onto' ' + test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --advance' ' + test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + test_done -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes 2026-03-24 22:04 ` [PATCH v5 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2026-03-25 6:29 ` Junio C Hamano 2026-03-25 15:10 ` Toon Claes 2026-03-25 15:36 ` Siddharth Asthana 0 siblings, 2 replies; 92+ messages in thread From: Junio C Hamano @ 2026-03-25 6:29 UTC (permalink / raw) To: Siddharth Asthana Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188, johannes.schindelin, toon Siddharth Asthana <siddharthasthana31@gmail.com> writes: > diff --git a/builtin/replay.c b/builtin/replay.c > index 2cdde830a8..d3c1d920f0 100644 > --- a/builtin/replay.c > +++ b/builtin/replay.c > @@ -83,7 +83,7 @@ int cmd_replay(int argc, > ... > /* Parse ref action mode from command line or config */ > ref_mode = get_ref_action_mode(repo, ref_action); > > + /* > + * Cherry-pick/rebase need oldest-first ordering so that each > + * replayed commit can build on its already-replayed parent. > + * Revert needs newest-first ordering (like git revert) to > + * reduce conflicts by peeling off changes from the top. > + */ > + int desired_reverse = !opts.revert; > + Compiler notices -Werror=declaration-after-statement error here. > repo_init_revisions(repo, &revs, prefix); ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes 2026-03-25 6:29 ` Junio C Hamano @ 2026-03-25 15:10 ` Toon Claes 2026-03-25 15:38 ` Siddharth Asthana 2026-03-25 16:44 ` Phillip Wood 2026-03-25 15:36 ` Siddharth Asthana 1 sibling, 2 replies; 92+ messages in thread From: Toon Claes @ 2026-03-25 15:10 UTC (permalink / raw) To: Junio C Hamano, Siddharth Asthana Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188, johannes.schindelin Junio C Hamano <gitster@pobox.com> writes: > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > >> diff --git a/builtin/replay.c b/builtin/replay.c >> index 2cdde830a8..d3c1d920f0 100644 >> --- a/builtin/replay.c >> +++ b/builtin/replay.c >> @@ -83,7 +83,7 @@ int cmd_replay(int argc, >> ... >> /* Parse ref action mode from command line or config */ >> ref_mode = get_ref_action_mode(repo, ref_action); >> >> + /* >> + * Cherry-pick/rebase need oldest-first ordering so that each >> + * replayed commit can build on its already-replayed parent. >> + * Revert needs newest-first ordering (like git revert) to >> + * reduce conflicts by peeling off changes from the top. >> + */ >> + int desired_reverse = !opts.revert; >> + > > Compiler notices -Werror=declaration-after-statement error here. That's basically the only comment I have on this series. Except for one micro-hit on the existing docs about <revision-range>: <revision-range>:: Range of commits to replay; see "Specifying Ranges" in linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the range should have a single tip, so that it's clear to which tip the advanced <branch> should point. Any commits in the range whose changes are already present in the branch the commits are being replayed onto will be dropped. Next to --advance, we should also mention --revert. But that's totally not worth a reroll and can be addressed in any other later series. -- Cheers, Toon ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes 2026-03-25 15:10 ` Toon Claes @ 2026-03-25 15:38 ` Siddharth Asthana 2026-03-25 16:44 ` Phillip Wood 1 sibling, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-25 15:38 UTC (permalink / raw) To: Toon Claes, Junio C Hamano Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188, johannes.schindelin On 25/03/26 20:40, Toon Claes wrote: > Junio C Hamano <gitster@pobox.com> writes: > >> Siddharth Asthana <siddharthasthana31@gmail.com> writes: >> >>> diff --git a/builtin/replay.c b/builtin/replay.c >>> index 2cdde830a8..d3c1d920f0 100644 >>> --- a/builtin/replay.c >>> +++ b/builtin/replay.c >>> @@ -83,7 +83,7 @@ int cmd_replay(int argc, >>> ... >>> /* Parse ref action mode from command line or config */ >>> ref_mode = get_ref_action_mode(repo, ref_action); >>> >>> + /* >>> + * Cherry-pick/rebase need oldest-first ordering so that each >>> + * replayed commit can build on its already-replayed parent. >>> + * Revert needs newest-first ordering (like git revert) to >>> + * reduce conflicts by peeling off changes from the top. >>> + */ >>> + int desired_reverse = !opts.revert; >>> + >> >> Compiler notices -Werror=declaration-after-statement error here. > > That's basically the only comment I have on this series. > > Except for one micro-hit on the existing docs about <revision-range>: > > <revision-range>:: > Range of commits to replay; see "Specifying Ranges" in > linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the > range should have a single tip, so that it's clear to which tip the > advanced <branch> should point. Any commits in the range whose > changes are already present in the branch the commits are being > replayed onto will be dropped. > > Next to --advance, we should also mention --revert. But that's totally > not worth a reroll and can be addressed in any other later series. Yeah thanks for catching this, I will include that in v6 since I am rerolling anyway for the declaration fix. Thanks, Siddharth ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes 2026-03-25 15:10 ` Toon Claes 2026-03-25 15:38 ` Siddharth Asthana @ 2026-03-25 16:44 ` Phillip Wood 1 sibling, 0 replies; 92+ messages in thread From: Phillip Wood @ 2026-03-25 16:44 UTC (permalink / raw) To: Toon Claes, Junio C Hamano, Siddharth Asthana Cc: git, christian.couder, ps, newren, karthik.188, johannes.schindelin On 25/03/2026 15:10, Toon Claes wrote: > Junio C Hamano <gitster@pobox.com> writes: > >> Siddharth Asthana <siddharthasthana31@gmail.com> writes: >> >>> diff --git a/builtin/replay.c b/builtin/replay.c >>> index 2cdde830a8..d3c1d920f0 100644 >>> --- a/builtin/replay.c >>> +++ b/builtin/replay.c >>> @@ -83,7 +83,7 @@ int cmd_replay(int argc, >>> ... >>> /* Parse ref action mode from command line or config */ >>> ref_mode = get_ref_action_mode(repo, ref_action); >>> >>> + /* >>> + * Cherry-pick/rebase need oldest-first ordering so that each >>> + * replayed commit can build on its already-replayed parent. >>> + * Revert needs newest-first ordering (like git revert) to >>> + * reduce conflicts by peeling off changes from the top. >>> + */ >>> + int desired_reverse = !opts.revert; >>> + >> >> Compiler notices -Werror=declaration-after-statement error here. > > That's basically the only comment I have on this series. Yes, everything else in the range-diff looks good to me Thanks Phillip > Except for one micro-hit on the existing docs about <revision-range>: > > <revision-range>:: > Range of commits to replay; see "Specifying Ranges" in > linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the > range should have a single tip, so that it's clear to which tip the > advanced <branch> should point. Any commits in the range whose > changes are already present in the branch the commits are being > replayed onto will be dropped. > > Next to --advance, we should also mention --revert. But that's totally > not worth a reroll and can be addressed in any other later series. > ^ permalink raw reply [flat|nested] 92+ messages in thread
* Re: [PATCH v5 2/2] replay: add --revert mode to reverse commit changes 2026-03-25 6:29 ` Junio C Hamano 2026-03-25 15:10 ` Toon Claes @ 2026-03-25 15:36 ` Siddharth Asthana 1 sibling, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-25 15:36 UTC (permalink / raw) To: Junio C Hamano Cc: git, christian.couder, ps, newren, phillip.wood123, karthik.188, johannes.schindelin, toon On 25/03/26 11:59, Junio C Hamano wrote: > Siddharth Asthana <siddharthasthana31@gmail.com> writes: > >> diff --git a/builtin/replay.c b/builtin/replay.c >> index 2cdde830a8..d3c1d920f0 100644 >> --- a/builtin/replay.c >> +++ b/builtin/replay.c >> @@ -83,7 +83,7 @@ int cmd_replay(int argc, >> ... >> /* Parse ref action mode from command line or config */ >> ref_mode = get_ref_action_mode(repo, ref_action); >> >> + /* >> + * Cherry-pick/rebase need oldest-first ordering so that each >> + * replayed commit can build on its already-replayed parent. >> + * Revert needs newest-first ordering (like git revert) to >> + * reduce conflicts by peeling off changes from the top. >> + */ >> + int desired_reverse = !opts.revert; >> + > > Compiler notices -Werror=declaration-after-statement error here. Ah right, I should have caught that. I have moved the declaration to the top of cmd_replay() with other variables and the CI is passing now [1]. Will send a v6. [1] https://gitlab.com/gitlab-org/git/-/pipelines/2406607541 Thanks, Siddharth > >> repo_init_revisions(repo, &revs, prefix); ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v6 0/2] replay: add --revert mode to reverse commit changes 2026-03-24 22:03 ` [PATCH v5 " Siddharth Asthana 2026-03-24 22:04 ` [PATCH v5 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-03-24 22:04 ` [PATCH v5 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2026-03-25 20:23 ` Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana ` (3 more replies) 2 siblings, 4 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana Hi, git replay currently supports cherry-picking (--advance) and rebasing (--onto), but not reverting. We need this at GitLab for Gitaly to reverse commits directly on bare repositories without a checkout. The approach is the same as sequencer.c -- cherry-pick and revert are just the same three-way merge with swapped arguments. We swap the base and pickme trees passed to merge_incore_nonrecursive() to reverse the diff direction. Patch 1 extracts the full revert message formatting logic into a new sequencer_format_revert_message() function that handles everything in one shared function rather than just the header. refer_to_commit() is updated to take a struct repository and a bool instead of replay_opts so it works outside the sequencer. Patch 2 adds --revert <branch> as a standalone mode. Reverts are processed newest-first (matching git revert) to reduce conflicts by peeling off changes from the top. The series is based on top of d181b9354c (The 13th batch, 2026-03-07). CI: https://gitlab.com/gitlab-org/git/-/pipelines/2408816732 Changes in v6: - Moved desired_reverse declaration to the top of cmd_replay() with other variable declarations to fix -Wdeclaration-after-statement - Updated <revision-range> doc to mention --revert alongside --advance - Link to v5: https://lore.kernel.org/git/20260324220401.47040-1-siddharthasthana31@gmail.com/ - Link to v4: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/ - Link to v3: https://public-inbox.org/git/20260218234215.89326-1-siddharthasthana31@gmail.com/ - Link to v2: https://public-inbox.org/git/20251202201611.22137-1-siddharthasthana31@gmail.com/ - Link to v1: https://public-inbox.org/git/20251125170056.34489-1-siddharthasthana31@gmail.com/ Thanks, Siddharth --- Siddharth Asthana (2): sequencer: extract revert message formatting into shared function replay: add --revert mode to reverse commit changes Documentation/git-replay.adoc | 52 +++++++++-- builtin/replay.c | 36 ++++++-- replay.c | 161 +++++++++++++++++++++++++--------- replay.h | 11 ++- sequencer.c | 78 +++++++++------- sequencer.h | 13 +++ t/t3650-replay-basics.sh | 111 +++++++++++++++++++++-- 7 files changed, 361 insertions(+), 101 deletions(-) Range-diff versus v5: 1: 6bd2ce4515 = 1: 6bd2ce4515 sequencer: extract revert message formatting into shared function 2: 9fd92497b9 ! 2: 41fe4861a2 replay: add --revert mode to reverse commit changes @@ Documentation/git-replay.adoc: The history is replayed on top of the <branch> an --contained:: Update all branches that point at commits in <revision-range>. Requires `--onto`. +@@ Documentation/git-replay.adoc: The default mode can be configured via the `replay.refAction` configuration vari + + <revision-range>:: + Range of commits to replay; see "Specifying Ranges" in +- linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the +- range should have a single tip, so that it's clear to which tip the +- advanced <branch> should point. Any commits in the range whose +- changes are already present in the branch the commits are being ++ linkgit:git-rev-parse[1]. In `--advance <branch>` or ++ `--revert <branch>` mode, the range should have a single tip, ++ so that it's clear to which tip the advanced or reverted ++ <branch> should point. Any commits in the range whose changes ++ are already present in the branch the commits are being + replayed onto will be dropped. + + :git-replay: 1 @@ Documentation/git-replay.adoc: When using `--ref-action=print`, the output is usable as input to update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH} @@ Documentation/git-replay.adoc: all commits they have since `base`, playing them ## builtin/replay.c ## @@ builtin/replay.c: int cmd_replay(int argc, + struct ref_transaction *transaction = NULL; + struct strbuf transaction_err = STRBUF_INIT; + struct strbuf reflog_msg = STRBUF_INIT; ++ int desired_reverse; + int ret = 0; const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " @@ builtin/replay.c: int cmd_replay(int argc, + * Revert needs newest-first ordering (like git revert) to + * reduce conflicts by peeling off changes from the top. + */ -+ int desired_reverse = !opts.revert; ++ desired_reverse = !opts.revert; + repo_init_revisions(repo, &revs, prefix); base-commit: d181b9354cf85b44455ce3ca9e6af0b9559e0ae2 -- 2.51.0 ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v6 1/2] sequencer: extract revert message formatting into shared function 2026-03-25 20:23 ` [PATCH v6 0/2] " Siddharth Asthana @ 2026-03-25 20:23 ` Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana ` (2 subsequent siblings) 3 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana The logic for formatting revert commit messages (handling "Revert" and "Reapply" cases, appending "This reverts commit <ref>.", and handling merge-parent references) currently lives inline in do_pick_commit(). The upcoming replay --revert mode needs to reuse this logic. Extract all of this into a new sequencer_format_revert_message() function. The function takes a repository, the subject line, commit, parent, a use_commit_reference flag, and the output strbuf. It handles both regular reverts ("Revert "<subject>"") and revert-of-revert cases ("Reapply "<subject>""), and uses refer_to_commit() internally to format the commit reference. Update refer_to_commit() to take a struct repository parameter instead of relying on the_repository, and a bool instead of reading from replay_opts directly. This makes it usable from the new shared function without pulling in sequencer-specific state. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- sequencer.c | 78 +++++++++++++++++++++++++++++++---------------------- sequencer.h | 13 +++++++++ 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/sequencer.c b/sequencer.c index aafd0bc959..7bf9d6ad19 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) { return opts->edit; } -static void refer_to_commit(struct replay_opts *opts, - struct strbuf *msgbuf, struct commit *commit) +static void refer_to_commit(struct repository *r, struct strbuf *msgbuf, + const struct commit *commit, + bool use_commit_reference) { - if (opts->commit_use_reference) { + if (use_commit_reference) { struct pretty_print_context ctx = { .abbrev = DEFAULT_ABBREV, .date_mode.type = DATE_SHORT, }; - repo_format_commit_message(the_repository, commit, + repo_format_commit_message(r, commit, "%h (%s, %ad)", msgbuf, &ctx); } else { strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid)); @@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r, */ if (command == TODO_REVERT) { - const char *orig_subject; - base = commit; base_label = msg.label; next = parent; next_label = msg.parent_label; - if (opts->commit_use_reference) { - strbuf_commented_addf(&ctx->message, comment_line_str, - "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && - /* - * We don't touch pre-existing repeated reverts, because - * theoretically these can be nested arbitrarily deeply, - * thus requiring excessive complexity to deal with. - */ - !starts_with(orig_subject, "Revert \"")) { - strbuf_addstr(&ctx->message, "Reapply \""); - strbuf_addstr(&ctx->message, orig_subject); - strbuf_addstr(&ctx->message, "\n"); - } else { - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); - } - strbuf_addstr(&ctx->message, "\nThis reverts commit "); - refer_to_commit(opts, &ctx->message, commit); - - if (commit->parents && commit->parents->next) { - strbuf_addstr(&ctx->message, ", reversing\nchanges made to "); - refer_to_commit(opts, &ctx->message, parent); - } - strbuf_addstr(&ctx->message, ".\n"); + sequencer_format_revert_message(r, msg.subject, commit, + parent, + opts->commit_use_reference, + &ctx->message); } else { const char *p; @@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r, return res; } +void sequencer_format_revert_message(struct repository *r, + const char *subject, + const struct commit *commit, + const struct commit *parent, + bool use_commit_reference, + struct strbuf *message) +{ + const char *orig_subject; + + if (use_commit_reference) { + strbuf_commented_addf(message, comment_line_str, + "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); + } else if (skip_prefix(subject, "Revert \"", &orig_subject) && + /* + * We don't touch pre-existing repeated reverts, because + * theoretically these can be nested arbitrarily deeply, + * thus requiring excessive complexity to deal with. + */ + !starts_with(orig_subject, "Revert \"")) { + strbuf_addstr(message, "Reapply \""); + strbuf_addstr(message, orig_subject); + strbuf_addstr(message, "\n"); + } else { + strbuf_addstr(message, "Revert \""); + strbuf_addstr(message, subject); + strbuf_addstr(message, "\"\n"); + } + strbuf_addstr(message, "\nThis reverts commit "); + refer_to_commit(r, message, commit, use_commit_reference); + + if (commit->parents && commit->parents->next) { + strbuf_addstr(message, ", reversing\nchanges made to "); + refer_to_commit(r, message, parent, use_commit_reference); + } + strbuf_addstr(message, ".\n"); +} + void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) { unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP; diff --git a/sequencer.h b/sequencer.h index 719684c8a9..56cd50233a 100644 --- a/sequencer.h +++ b/sequencer.h @@ -271,4 +271,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) */ int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +/* + * Format a revert commit message with appropriate 'Revert "<subject>"' or + * 'Reapply "<subject>"' prefix and 'This reverts commit <ref>.' body. + * When use_commit_reference is set, <ref> is an abbreviated hash with + * subject and date; otherwise the full hex hash is used. + */ +void sequencer_format_revert_message(struct repository *r, + const char *subject, + const struct commit *commit, + const struct commit *parent, + bool use_commit_reference, + struct strbuf *message); + #endif /* SEQUENCER_H */ -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* [PATCH v6 2/2] replay: add --revert mode to reverse commit changes 2026-03-25 20:23 ` [PATCH v6 0/2] " Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana @ 2026-03-25 20:23 ` Siddharth Asthana 2026-03-28 4:33 ` Tian Yuchen 2026-03-25 20:23 ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 3 siblings, 1 reply; 92+ messages in thread From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana, Johannes Schindelin Add a `--revert <branch>` mode to git replay that undoes the changes introduced by the specified commits. Like --onto and --advance, --revert is a standalone mode: it takes a branch argument and updates that branch with the newly created revert commits. At GitLab, we need this in Gitaly for reverting commits directly on bare repositories without requiring a working tree checkout. The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick and revert are just the same three-way merge with swapped arguments: - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) - Revert: merge(ancestor=commit, ours=current, theirs=parent) We swap the base and pickme trees passed to merge_incore_nonrecursive() to reverse the diff direction. Reverts are processed newest-first (matching git revert behavior) to reduce conflicts by peeling off changes from the top. Each revert builds on the result of the previous one via the last_commit fallback in the main replay loop, rather than relying on the parent-mapping used for cherry-pick. Revert commit messages follow the usual git revert conventions: prefixed with "Revert" (or "Reapply" when reverting a revert), and including "This reverts commit <hash>.". The author is set to the current user rather than preserving the original author, matching git revert behavior. Helped-by: Christian Couder <christian.couder@gmail.com> Helped-by: Patrick Steinhardt <ps@pks.im> Helped-by: Elijah Newren <newren@gmail.com> Helped-by: Phillip Wood <phillip.wood123@gmail.com> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Helped-by: Junio C Hamano <gitster@pobox.com> Helped-by: Toon Claes <toon@iotcl.com> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- Documentation/git-replay.adoc | 52 +++++++++-- builtin/replay.c | 36 ++++++-- replay.c | 161 +++++++++++++++++++++++++--------- replay.h | 11 ++- t/t3650-replay-basics.sh | 111 +++++++++++++++++++++-- 5 files changed, 302 insertions(+), 69 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index 8d696ce3ab..997097e420 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t SYNOPSIS -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range> DESCRIPTION ----------- @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to point at the tip of the resulting history. This is different from `--onto`, which uses the target only as a starting point without updating it. +--revert <branch>:: + Starting point at which to create the reverted commits; must be a + branch name. ++ +When `--revert` is specified, the commits in the revision range are reverted +(their changes are undone) and the reverted commits are created on top of +<branch>. The <branch> is then updated to point at the new commits. This is +the same as running `git revert <revision-range>` but does not update the +working tree. ++ +The commit messages follow `git revert` conventions: they are prefixed with +"Revert" and include "This reverts commit <hash>." When reverting a commit +whose message starts with "Revert", the new message uses "Reapply" instead. +Unlike cherry-pick which preserves the original author, revert commits use +the current user as the author, matching the behavior of `git revert`. ++ +This option is mutually exclusive with `--onto` and `--advance`. It is also +incompatible with `--contained` (which is a modifier for `--onto` only). + --contained:: Update all branches that point at commits in <revision-range>. Requires `--onto`. @@ -60,10 +79,11 @@ The default mode can be configured via the `replay.refAction` configuration vari <revision-range>:: Range of commits to replay; see "Specifying Ranges" in - linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the - range should have a single tip, so that it's clear to which tip the - advanced <branch> should point. Any commits in the range whose - changes are already present in the branch the commits are being + linkgit:git-rev-parse[1]. In `--advance <branch>` or + `--revert <branch>` mode, the range should have a single tip, + so that it's clear to which tip the advanced or reverted + <branch> should point. Any commits in the range whose changes + are already present in the branch the commits are being replayed onto will be dropped. :git-replay: 1 @@ -84,9 +104,10 @@ When using `--ref-action=print`, the output is usable as input to update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH} where the number of refs updated depends on the arguments passed and -the shape of the history being replayed. When using `--advance`, the -number of refs updated is always one, but for `--onto`, it can be one -or more (rebasing multiple branches simultaneously is supported). +the shape of the history being replayed. When using `--advance` or +`--revert`, the number of refs updated is always one, but for `--onto`, +it can be one or more (rebasing multiple branches simultaneously is +supported). There is no stderr output on conflicts; see the <<exit-status,EXIT STATUS>> section below. @@ -152,6 +173,21 @@ all commits they have since `base`, playing them on top of `origin/main`. These three branches may have commits on top of `base` that they have in common, but that does not need to be the case. +To revert commits on a branch: + +------------ +$ git replay --revert main topic~2..topic +------------ + +This reverts the last two commits from `topic`, creating revert commits on +top of `main`, and updates `main` to point at the result. This is useful when +commits from `topic` were previously merged or cherry-picked into `main` and +need to be undone. + +NOTE: For reverting an entire merge request as a single commit (rather than +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` +which can avoid unnecessary merge conflicts. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/replay.c b/builtin/replay.c index 2cdde830a8..a0879b020f 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -79,11 +79,12 @@ int cmd_replay(int argc, struct ref_transaction *transaction = NULL; struct strbuf transaction_err = STRBUF_INIT; struct strbuf reflog_msg = STRBUF_INIT; + int desired_reverse; int ret = 0; const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto <newbase> | --advance <branch>) " + "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) " "[--ref-action[=<mode>]] <revision-range>"), NULL }; @@ -96,6 +97,9 @@ int cmd_replay(int argc, N_("replay onto given commit")), OPT_BOOL(0, "contained", &opts.contained, N_("update all branches that point at commits in <revision-range>")), + OPT_STRING(0, "revert", &opts.revert, + N_("branch"), + N_("revert commits onto given branch")), OPT_STRING(0, "ref-action", &ref_action, N_("mode"), N_("control ref update behavior (update|print)")), @@ -105,19 +109,31 @@ int cmd_replay(int argc, argc = parse_options(argc, argv, prefix, replay_options, replay_usage, PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); - if (!opts.onto && !opts.advance) { - error(_("option --onto or --advance is mandatory")); + /* Exactly one mode must be specified */ + if (!opts.onto && !opts.advance && !opts.revert) { + error(_("exactly one of --onto, --advance, or --revert is required")); usage_with_options(replay_usage, replay_options); } + die_for_incompatible_opt3(!!opts.onto, "--onto", + !!opts.advance, "--advance", + !!opts.revert, "--revert"); die_for_incompatible_opt2(!!opts.advance, "--advance", opts.contained, "--contained"); - die_for_incompatible_opt2(!!opts.advance, "--advance", - !!opts.onto, "--onto"); + die_for_incompatible_opt2(!!opts.revert, "--revert", + opts.contained, "--contained"); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); + /* + * Cherry-pick/rebase need oldest-first ordering so that each + * replayed commit can build on its already-replayed parent. + * Revert needs newest-first ordering (like git revert) to + * reduce conflicts by peeling off changes from the top. + */ + desired_reverse = !opts.revert; + repo_init_revisions(repo, &revs, prefix); /* @@ -129,7 +145,7 @@ int cmd_replay(int argc, * some options changing these values if we think they could * be useful. */ - revs.reverse = 1; + revs.reverse = desired_reverse; revs.sort_order = REV_SORT_IN_GRAPH_ORDER; revs.topo_order = 1; revs.simplify_history = 0; @@ -144,11 +160,11 @@ int cmd_replay(int argc, * Detect and warn if we override some user specified rev * walking options. */ - if (revs.reverse != 1) { + if (revs.reverse != desired_reverse) { warning(_("some rev walking options will be overridden as " "'%s' bit in 'struct rev_info' will be forced"), "reverse"); - revs.reverse = 1; + revs.reverse = desired_reverse; } if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { warning(_("some rev walking options will be overridden as " @@ -174,7 +190,9 @@ int cmd_replay(int argc, goto cleanup; /* Build reflog message */ - if (opts.advance) { + if (opts.revert) { + strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert); + } else if (opts.advance) { strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance); } else { struct object_id oid; diff --git a/replay.c b/replay.c index a63f6714c4..d7239d4c83 100644 --- a/replay.c +++ b/replay.c @@ -8,6 +8,7 @@ #include "refs.h" #include "replay.h" #include "revision.h" +#include "sequencer.h" #include "strmap.h" #include "tree.h" @@ -17,6 +18,11 @@ */ #define the_repository DO_NOT_USE_THE_REPOSITORY +enum replay_mode { + REPLAY_MODE_PICK, + REPLAY_MODE_REVERT, +}; + static const char *short_commit_name(struct repository *repo, struct commit *commit) { @@ -50,15 +56,37 @@ static char *get_author(const char *message) return NULL; } +static void generate_revert_message(struct strbuf *msg, + struct commit *commit, + struct repository *repo) +{ + const char *out_enc = get_commit_output_encoding(); + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); + const char *subject_start; + int subject_len; + char *subject; + + subject_len = find_commit_subject(message, &subject_start); + subject = xmemdupz(subject_start, subject_len); + + sequencer_format_revert_message(repo, subject, commit, + commit->parents ? commit->parents->item : NULL, + false, msg); + + free(subject); + repo_unuse_commit_buffer(repo, commit, message); +} + static struct commit *create_commit(struct repository *repo, struct tree *tree, struct commit *based_on, - struct commit *parent) + struct commit *parent, + enum replay_mode mode) { struct object_id ret; struct object *obj = NULL; struct commit_list *parents = NULL; - char *author; + char *author = NULL; char *sign_commit = NULL; /* FIXME: cli users might want to sign again */ struct commit_extra_header *extra = NULL; struct strbuf msg = STRBUF_INIT; @@ -70,9 +98,16 @@ static struct commit *create_commit(struct repository *repo, commit_list_insert(parent, &parents); extra = read_commit_extra_headers(based_on, exclude_gpgsig); - find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); + if (mode == REPLAY_MODE_REVERT) { + generate_revert_message(&msg, based_on, repo); + /* For revert, use current user as author (NULL = use default) */ + } else if (mode == REPLAY_MODE_PICK) { + find_commit_subject(message, &orig_message); + strbuf_addstr(&msg, orig_message); + author = get_author(message); + } else { + BUG("unexpected replay mode %d", mode); + } reset_ident_date(); if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, &ret, author, NULL, sign_commit, extra)) { @@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo, } } +static void set_up_branch_mode(struct repository *repo, + char **branch_name, + const char *option_name, + struct ref_info *rinfo, + struct commit **onto) +{ + struct object_id oid; + char *fullname = NULL; + + if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name), + &oid, &fullname, 0) == 1) { + free(*branch_name); + *branch_name = fullname; + } else { + die(_("argument to %s must be a reference"), option_name); + } + *onto = peel_committish(repo, *branch_name, option_name); + if (rinfo->positive_refexprs > 1) + die(_("'%s' cannot be used with multiple revision ranges " + "because the ordering would be ill-defined"), + option_name); +} + static void set_up_replay_mode(struct repository *repo, struct rev_cmdline_info *cmd_info, const char *onto_name, bool *detached_head, char **advance_name, + char **revert_name, struct commit **onto, struct strset **update_refs) { @@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo, if (!rinfo.positive_refexprs) die(_("need some commits to replay")); - if (!onto_name == !*advance_name) - BUG("one and only one of onto_name and *advance_name must be given"); - if (onto_name) { *onto = peel_committish(repo, onto_name, "--onto"); if (rinfo.positive_refexprs < @@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo, *update_refs = xcalloc(1, sizeof(**update_refs)); **update_refs = rinfo.positive_refs; memset(&rinfo.positive_refs, 0, sizeof(**update_refs)); + } else if (*advance_name) { + set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto); + } else if (*revert_name) { + set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto); } else { - struct object_id oid; - char *fullname = NULL; - - if (!*advance_name) - BUG("expected either onto_name or *advance_name in this function"); - - if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name), - &oid, &fullname, 0) == 1) { - free(*advance_name); - *advance_name = fullname; - } else { - die(_("argument to --advance must be a reference")); - } - *onto = peel_committish(repo, *advance_name, "--advance"); - if (rinfo.positive_refexprs > 1) - die(_("cannot advance target with multiple sources because ordering would be ill-defined")); + BUG("expected one of onto_name, *advance_name, or *revert_name"); } strset_clear(&rinfo.negative_refs); strset_clear(&rinfo.positive_refs); @@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo, kh_oid_map_t *replayed_commits, struct commit *onto, struct merge_options *merge_opt, - struct merge_result *result) + struct merge_result *result, + enum replay_mode mode) { struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree, *replayed_base_tree; @@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo, pickme_tree = repo_get_commit_tree(repo, pickme); base_tree = repo_get_commit_tree(repo, base); - merge_opt->branch1 = short_commit_name(repo, replayed_base); - merge_opt->branch2 = short_commit_name(repo, pickme); - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); - - merge_incore_nonrecursive(merge_opt, - base_tree, - replayed_base_tree, - pickme_tree, - result); - - free((char*)merge_opt->ancestor); + if (mode == REPLAY_MODE_PICK) { + /* Cherry-pick: normal order */ + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = short_commit_name(repo, pickme); + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + + merge_incore_nonrecursive(merge_opt, + base_tree, + replayed_base_tree, + pickme_tree, + result); + + free((char *)merge_opt->ancestor); + } else if (mode == REPLAY_MODE_REVERT) { + /* Revert: swap base and pickme to reverse the diff */ + const char *pickme_name = short_commit_name(repo, pickme); + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); + merge_opt->ancestor = pickme_name; + + merge_incore_nonrecursive(merge_opt, + pickme_tree, + replayed_base_tree, + base_tree, + result); + + free((char *)merge_opt->branch2); + } else { + BUG("unexpected replay mode %d", mode); + } merge_opt->ancestor = NULL; + merge_opt->branch2 = NULL; if (!result->clean) return NULL; /* Drop commits that become empty */ if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) return replayed_base; - return create_commit(repo, result->tree, pickme, replayed_base); + return create_commit(repo, result->tree, pickme, replayed_base, mode); } void replay_result_release(struct replay_result *result) @@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs, }; bool detached_head; char *advance; + char *revert; + enum replay_mode mode = REPLAY_MODE_PICK; int ret; advance = xstrdup_or_null(opts->advance); + revert = xstrdup_or_null(opts->revert); + if (revert) + mode = REPLAY_MODE_REVERT; set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, - &detached_head, &advance, &onto, &update_refs); + &detached_head, &advance, &revert, &onto, &update_refs); /* FIXME: Should allow replaying commits with the first as a root commit */ @@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs, die(_("replaying merge commits is not supported yet!")); last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, - onto, &merge_opt, &result); + mode == REPLAY_MODE_REVERT ? last_commit : onto, + &merge_opt, &result, mode); if (!last_commit) break; @@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs, kh_value(replayed_commits, pos) = last_commit; /* Update any necessary branches */ - if (advance) + if (advance || revert) continue; for (decoration = get_name_decoration(&commit->object); @@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs, goto out; } - /* In --advance mode, advance the target ref */ - if (advance) - replay_result_queue_update(out, advance, + /* In --advance or --revert mode, update the target ref */ + if (advance || revert) { + const char *ref = advance ? advance : revert; + replay_result_queue_update(out, ref, &onto->object.oid, &last_commit->object.oid); + } ret = 0; @@ -377,5 +451,6 @@ int replay_revisions(struct rev_info *revs, kh_destroy_oid_map(replayed_commits); merge_finalize(&merge_opt, &result); free(advance); + free(revert); return ret; } diff --git a/replay.h b/replay.h index d8407dc7f7..e916a5f975 100644 --- a/replay.h +++ b/replay.h @@ -13,7 +13,7 @@ struct replay_revisions_options { /* * Starting point at which to create the new commits; must be a branch * name. The branch will be updated to point to the rewritten commits. - * This option is mutually exclusive with `onto`. + * This option is mutually exclusive with `onto` and `revert`. */ const char *advance; @@ -22,7 +22,14 @@ struct replay_revisions_options { * committish. References pointing at decendants of `onto` will be * updated to point to the new commits. */ - const char *onto; + const char *onto; + + /* + * Starting point at which to create revert commits; must be a branch + * name. The branch will be updated to point to the revert commits. + * This option is mutually exclusive with `onto` and `advance`. + */ + const char *revert; /* * Update branches that point at commits in the given revision range. diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index a03f8f9293..217f6fb292 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' ' test_cmp expect actual ' -test_expect_success 'option --onto or --advance is mandatory' ' - echo "error: option --onto or --advance is mandatory" >expect && +test_expect_success 'exactly one of --onto, --advance, or --revert is required' ' + echo "error: exactly one of --onto, --advance, or --revert is required" >expect && test_might_fail git replay -h >>expect && test_must_fail git replay topic1..topic2 2>actual && test_cmp expect actual @@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err test_cmp expect actual ' -test_expect_success 'options --advance and --contained cannot be used together' ' - printf "fatal: options ${SQ}--advance${SQ} " >expect && - printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect && +test_expect_success '--advance and --contained cannot be used together' ' test_must_fail git replay --advance=main --contained \ topic1..topic2 2>actual && - test_cmp expect actual + test_grep "cannot be used together" actual ' test_expect_success 'cannot advance target ... ordering would be ill-defined' ' - echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && + echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect && test_must_fail git replay --advance=main main topic1 topic2 2>actual && test_cmp expect actual ' @@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' ' test_grep "invalid.*replay.refAction.*value" error ' +test_expect_success 'argument to --revert must be a reference' ' + echo "fatal: argument to --revert must be a reference" >expect && + oid=$(git rev-parse main) && + test_must_fail git replay --revert=$oid topic1..topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'cannot revert with multiple sources' ' + echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect && + test_must_fail git replay --revert main main topic1 topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'using replay --revert to revert commits' ' + # Reuse existing topic4 branch (has commits I and J on top of main) + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # Revert commits I and J + git replay --revert topic4 topic4~2..topic4 && + + # Verify the revert commits were created (newest-first ordering + # means J is reverted first, then I on top) + git log --format=%s -4 topic4 >actual && + cat >expect <<-\EOF && + Revert "I" + Revert "J" + J + I + EOF + test_cmp expect actual && + + # Verify commit message format includes hash (tip is Revert "I") + test_commit_message topic4 <<-EOF && + Revert "I" + + This reverts commit $(git rev-parse I). + EOF + + # Verify reflog message + git reflog topic4 -1 --format=%gs >reflog-msg && + echo "replay --revert topic4" >expect-reflog && + test_cmp expect-reflog reflog-msg +' + +test_expect_success 'using replay --revert in bare repo' ' + # Reuse existing topic4 in bare repo + START=$(git -C bare rev-parse topic4) && + test_when_finished "git -C bare update-ref refs/heads/topic4 $START" && + + # Revert commit J in bare repo + git -C bare replay --revert topic4 topic4~1..topic4 && + + # Verify revert was created + git -C bare log -1 --format=%s topic4 >actual && + echo "Revert \"J\"" >expect && + test_cmp expect actual +' + +test_expect_success 'revert of revert uses Reapply' ' + # Use topic4 and first revert J, then revert the revert + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # First revert J + git replay --revert topic4 topic4~1..topic4 && + REVERT_J=$(git rev-parse topic4) && + + # Now revert the revert - should become Reapply + git replay --revert topic4 topic4~1..topic4 && + + # Verify Reapply prefix and message format + test_commit_message topic4 <<-EOF + Reapply "J" + + This reverts commit $REVERT_J. + EOF +' + +test_expect_success 'git replay --revert with conflict' ' + # conflict branch has C.conflict which conflicts with topic1s C + test_expect_code 1 git replay --revert conflict B..topic1 +' + +test_expect_success 'git replay --revert incompatible with --contained' ' + test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --onto' ' + test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --advance' ' + test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + test_done -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* Re: [PATCH v6 2/2] replay: add --revert mode to reverse commit changes 2026-03-25 20:23 ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2026-03-28 4:33 ` Tian Yuchen 0 siblings, 0 replies; 92+ messages in thread From: Tian Yuchen @ 2026-03-28 4:33 UTC (permalink / raw) To: Siddharth Asthana, git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon Hi Siddharth, The patch itself looks pretty good to me, but I have some reservations about its functionality: On 3/26/26 04:23, Siddharth Asthana wrote: > static struct commit *create_commit(struct repository *repo, > struct tree *tree, > struct commit *based_on, > - struct commit *parent) > + struct commit *parent, > + enum replay_mode mode) > { ... > extra = read_commit_extra_headers(based_on, exclude_gpgsig); ... > if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, > &ret, author, NULL, sign_commit, extra)) { > @@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo, > } > } It seems there isn’t a distinction made here between how 'cherry-pick' and 'revert' handle the `extra` header. But doesn’t the 'revert' operation actually create a *new* commit with the *current time* and *current author*? Is it appropriate to inherit the 'extra' header? > +static void set_up_branch_mode(struct repository *repo, ... > + *onto = peel_committish(repo, *branch_name, option_name); > + if (rinfo->positive_refexprs > 1) > + die(_("'%s' cannot be used with multiple revision ranges " > + "because the ordering would be ill-defined"), > + option_name); > +} This is a fail-safe design intended to prevent users from entering commands like: git replay --revert main f1 f2 This operation is indeed undefined which should be intercepted. However, considering: git replay --revert main HEAD~5..HEAD~3 HEAD~1..HEAD Is this operation also intercepted? I think the reason is that the condition 'rinfo->positive_refexprs > 1' is a bit too simplistic. > + if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name), > + &oid, &fullname, 0) == 1) { > + free(*branch_name); > + *branch_name = fullname; > + } else { > + die(_("argument to %s must be a reference"), option_name); > + } I think it would be great if a low-level command supported something like: git replay --revert new-branch HEAD~3..HEAD Even if it just saves the step of creating a new branch ;) These are just my thoughts on the matter. Hope to spark discussion. Regards, Yuchen ^ permalink raw reply [flat|nested] 92+ messages in thread
* [PATCH v6 1/2] sequencer: extract revert message formatting into shared function 2026-03-25 20:23 ` [PATCH v6 0/2] " Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana @ 2026-03-25 20:23 ` Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 3 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana The logic for formatting revert commit messages (handling "Revert" and "Reapply" cases, appending "This reverts commit <ref>.", and handling merge-parent references) currently lives inline in do_pick_commit(). The upcoming replay --revert mode needs to reuse this logic. Extract all of this into a new sequencer_format_revert_message() function. The function takes a repository, the subject line, commit, parent, a use_commit_reference flag, and the output strbuf. It handles both regular reverts ("Revert "<subject>"") and revert-of-revert cases ("Reapply "<subject>""), and uses refer_to_commit() internally to format the commit reference. Update refer_to_commit() to take a struct repository parameter instead of relying on the_repository, and a bool instead of reading from replay_opts directly. This makes it usable from the new shared function without pulling in sequencer-specific state. Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- sequencer.c | 78 +++++++++++++++++++++++++++++++---------------------- sequencer.h | 13 +++++++++ 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/sequencer.c b/sequencer.c index aafd0bc959..7bf9d6ad19 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2206,15 +2206,16 @@ static int should_edit(struct replay_opts *opts) { return opts->edit; } -static void refer_to_commit(struct replay_opts *opts, - struct strbuf *msgbuf, struct commit *commit) +static void refer_to_commit(struct repository *r, struct strbuf *msgbuf, + const struct commit *commit, + bool use_commit_reference) { - if (opts->commit_use_reference) { + if (use_commit_reference) { struct pretty_print_context ctx = { .abbrev = DEFAULT_ABBREV, .date_mode.type = DATE_SHORT, }; - repo_format_commit_message(the_repository, commit, + repo_format_commit_message(r, commit, "%h (%s, %ad)", msgbuf, &ctx); } else { strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid)); @@ -2364,38 +2365,14 @@ static int do_pick_commit(struct repository *r, */ if (command == TODO_REVERT) { - const char *orig_subject; - base = commit; base_label = msg.label; next = parent; next_label = msg.parent_label; - if (opts->commit_use_reference) { - strbuf_commented_addf(&ctx->message, comment_line_str, - "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); - } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && - /* - * We don't touch pre-existing repeated reverts, because - * theoretically these can be nested arbitrarily deeply, - * thus requiring excessive complexity to deal with. - */ - !starts_with(orig_subject, "Revert \"")) { - strbuf_addstr(&ctx->message, "Reapply \""); - strbuf_addstr(&ctx->message, orig_subject); - strbuf_addstr(&ctx->message, "\n"); - } else { - strbuf_addstr(&ctx->message, "Revert \""); - strbuf_addstr(&ctx->message, msg.subject); - strbuf_addstr(&ctx->message, "\"\n"); - } - strbuf_addstr(&ctx->message, "\nThis reverts commit "); - refer_to_commit(opts, &ctx->message, commit); - - if (commit->parents && commit->parents->next) { - strbuf_addstr(&ctx->message, ", reversing\nchanges made to "); - refer_to_commit(opts, &ctx->message, parent); - } - strbuf_addstr(&ctx->message, ".\n"); + sequencer_format_revert_message(r, msg.subject, commit, + parent, + opts->commit_use_reference, + &ctx->message); } else { const char *p; @@ -5580,6 +5557,43 @@ int sequencer_pick_revisions(struct repository *r, return res; } +void sequencer_format_revert_message(struct repository *r, + const char *subject, + const struct commit *commit, + const struct commit *parent, + bool use_commit_reference, + struct strbuf *message) +{ + const char *orig_subject; + + if (use_commit_reference) { + strbuf_commented_addf(message, comment_line_str, + "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); + } else if (skip_prefix(subject, "Revert \"", &orig_subject) && + /* + * We don't touch pre-existing repeated reverts, because + * theoretically these can be nested arbitrarily deeply, + * thus requiring excessive complexity to deal with. + */ + !starts_with(orig_subject, "Revert \"")) { + strbuf_addstr(message, "Reapply \""); + strbuf_addstr(message, orig_subject); + strbuf_addstr(message, "\n"); + } else { + strbuf_addstr(message, "Revert \""); + strbuf_addstr(message, subject); + strbuf_addstr(message, "\"\n"); + } + strbuf_addstr(message, "\nThis reverts commit "); + refer_to_commit(r, message, commit, use_commit_reference); + + if (commit->parents && commit->parents->next) { + strbuf_addstr(message, ", reversing\nchanges made to "); + refer_to_commit(r, message, parent, use_commit_reference); + } + strbuf_addstr(message, ".\n"); +} + void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag) { unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP; diff --git a/sequencer.h b/sequencer.h index 719684c8a9..56cd50233a 100644 --- a/sequencer.h +++ b/sequencer.h @@ -271,4 +271,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence) */ int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); +/* + * Format a revert commit message with appropriate 'Revert "<subject>"' or + * 'Reapply "<subject>"' prefix and 'This reverts commit <ref>.' body. + * When use_commit_reference is set, <ref> is an abbreviated hash with + * subject and date; otherwise the full hex hash is used. + */ +void sequencer_format_revert_message(struct repository *r, + const char *subject, + const struct commit *commit, + const struct commit *parent, + bool use_commit_reference, + struct strbuf *message); + #endif /* SEQUENCER_H */ -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
* [PATCH v6 2/2] replay: add --revert mode to reverse commit changes 2026-03-25 20:23 ` [PATCH v6 0/2] " Siddharth Asthana ` (2 preceding siblings ...) 2026-03-25 20:23 ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana @ 2026-03-25 20:23 ` Siddharth Asthana 3 siblings, 0 replies; 92+ messages in thread From: Siddharth Asthana @ 2026-03-25 20:23 UTC (permalink / raw) To: git Cc: christian.couder, ps, newren, gitster, phillip.wood123, karthik.188, johannes.schindelin, toon, Siddharth Asthana, Johannes Schindelin Add a `--revert <branch>` mode to git replay that undoes the changes introduced by the specified commits. Like --onto and --advance, --revert is a standalone mode: it takes a branch argument and updates that branch with the newly created revert commits. At GitLab, we need this in Gitaly for reverting commits directly on bare repositories without requiring a working tree checkout. The approach is the same as sequencer.c's do_pick_commit() -- cherry-pick and revert are just the same three-way merge with swapped arguments: - Cherry-pick: merge(ancestor=parent, ours=current, theirs=commit) - Revert: merge(ancestor=commit, ours=current, theirs=parent) We swap the base and pickme trees passed to merge_incore_nonrecursive() to reverse the diff direction. Reverts are processed newest-first (matching git revert behavior) to reduce conflicts by peeling off changes from the top. Each revert builds on the result of the previous one via the last_commit fallback in the main replay loop, rather than relying on the parent-mapping used for cherry-pick. Revert commit messages follow the usual git revert conventions: prefixed with "Revert" (or "Reapply" when reverting a revert), and including "This reverts commit <hash>.". The author is set to the current user rather than preserving the original author, matching git revert behavior. Helped-by: Christian Couder <christian.couder@gmail.com> Helped-by: Patrick Steinhardt <ps@pks.im> Helped-by: Elijah Newren <newren@gmail.com> Helped-by: Phillip Wood <phillip.wood123@gmail.com> Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Helped-by: Junio C Hamano <gitster@pobox.com> Helped-by: Toon Claes <toon@iotcl.com> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- Documentation/git-replay.adoc | 52 +++++++++-- builtin/replay.c | 36 ++++++-- replay.c | 161 +++++++++++++++++++++++++--------- replay.h | 11 ++- t/t3650-replay-basics.sh | 111 +++++++++++++++++++++-- 5 files changed, 302 insertions(+), 69 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index 8d696ce3ab..997097e420 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t SYNOPSIS -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range> DESCRIPTION ----------- @@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to point at the tip of the resulting history. This is different from `--onto`, which uses the target only as a starting point without updating it. +--revert <branch>:: + Starting point at which to create the reverted commits; must be a + branch name. ++ +When `--revert` is specified, the commits in the revision range are reverted +(their changes are undone) and the reverted commits are created on top of +<branch>. The <branch> is then updated to point at the new commits. This is +the same as running `git revert <revision-range>` but does not update the +working tree. ++ +The commit messages follow `git revert` conventions: they are prefixed with +"Revert" and include "This reverts commit <hash>." When reverting a commit +whose message starts with "Revert", the new message uses "Reapply" instead. +Unlike cherry-pick which preserves the original author, revert commits use +the current user as the author, matching the behavior of `git revert`. ++ +This option is mutually exclusive with `--onto` and `--advance`. It is also +incompatible with `--contained` (which is a modifier for `--onto` only). + --contained:: Update all branches that point at commits in <revision-range>. Requires `--onto`. @@ -60,10 +79,11 @@ The default mode can be configured via the `replay.refAction` configuration vari <revision-range>:: Range of commits to replay; see "Specifying Ranges" in - linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the - range should have a single tip, so that it's clear to which tip the - advanced <branch> should point. Any commits in the range whose - changes are already present in the branch the commits are being + linkgit:git-rev-parse[1]. In `--advance <branch>` or + `--revert <branch>` mode, the range should have a single tip, + so that it's clear to which tip the advanced or reverted + <branch> should point. Any commits in the range whose changes + are already present in the branch the commits are being replayed onto will be dropped. :git-replay: 1 @@ -84,9 +104,10 @@ When using `--ref-action=print`, the output is usable as input to update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH} where the number of refs updated depends on the arguments passed and -the shape of the history being replayed. When using `--advance`, the -number of refs updated is always one, but for `--onto`, it can be one -or more (rebasing multiple branches simultaneously is supported). +the shape of the history being replayed. When using `--advance` or +`--revert`, the number of refs updated is always one, but for `--onto`, +it can be one or more (rebasing multiple branches simultaneously is +supported). There is no stderr output on conflicts; see the <<exit-status,EXIT STATUS>> section below. @@ -152,6 +173,21 @@ all commits they have since `base`, playing them on top of `origin/main`. These three branches may have commits on top of `base` that they have in common, but that does not need to be the case. +To revert commits on a branch: + +------------ +$ git replay --revert main topic~2..topic +------------ + +This reverts the last two commits from `topic`, creating revert commits on +top of `main`, and updates `main` to point at the result. This is useful when +commits from `topic` were previously merged or cherry-picked into `main` and +need to be undone. + +NOTE: For reverting an entire merge request as a single commit (rather than +commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` +which can avoid unnecessary merge conflicts. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/replay.c b/builtin/replay.c index 2cdde830a8..a0879b020f 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -79,11 +79,12 @@ int cmd_replay(int argc, struct ref_transaction *transaction = NULL; struct strbuf transaction_err = STRBUF_INIT; struct strbuf reflog_msg = STRBUF_INIT; + int desired_reverse; int ret = 0; const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto <newbase> | --advance <branch>) " + "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) " "[--ref-action[=<mode>]] <revision-range>"), NULL }; @@ -96,6 +97,9 @@ int cmd_replay(int argc, N_("replay onto given commit")), OPT_BOOL(0, "contained", &opts.contained, N_("update all branches that point at commits in <revision-range>")), + OPT_STRING(0, "revert", &opts.revert, + N_("branch"), + N_("revert commits onto given branch")), OPT_STRING(0, "ref-action", &ref_action, N_("mode"), N_("control ref update behavior (update|print)")), @@ -105,19 +109,31 @@ int cmd_replay(int argc, argc = parse_options(argc, argv, prefix, replay_options, replay_usage, PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); - if (!opts.onto && !opts.advance) { - error(_("option --onto or --advance is mandatory")); + /* Exactly one mode must be specified */ + if (!opts.onto && !opts.advance && !opts.revert) { + error(_("exactly one of --onto, --advance, or --revert is required")); usage_with_options(replay_usage, replay_options); } + die_for_incompatible_opt3(!!opts.onto, "--onto", + !!opts.advance, "--advance", + !!opts.revert, "--revert"); die_for_incompatible_opt2(!!opts.advance, "--advance", opts.contained, "--contained"); - die_for_incompatible_opt2(!!opts.advance, "--advance", - !!opts.onto, "--onto"); + die_for_incompatible_opt2(!!opts.revert, "--revert", + opts.contained, "--contained"); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); + /* + * Cherry-pick/rebase need oldest-first ordering so that each + * replayed commit can build on its already-replayed parent. + * Revert needs newest-first ordering (like git revert) to + * reduce conflicts by peeling off changes from the top. + */ + desired_reverse = !opts.revert; + repo_init_revisions(repo, &revs, prefix); /* @@ -129,7 +145,7 @@ int cmd_replay(int argc, * some options changing these values if we think they could * be useful. */ - revs.reverse = 1; + revs.reverse = desired_reverse; revs.sort_order = REV_SORT_IN_GRAPH_ORDER; revs.topo_order = 1; revs.simplify_history = 0; @@ -144,11 +160,11 @@ int cmd_replay(int argc, * Detect and warn if we override some user specified rev * walking options. */ - if (revs.reverse != 1) { + if (revs.reverse != desired_reverse) { warning(_("some rev walking options will be overridden as " "'%s' bit in 'struct rev_info' will be forced"), "reverse"); - revs.reverse = 1; + revs.reverse = desired_reverse; } if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { warning(_("some rev walking options will be overridden as " @@ -174,7 +190,9 @@ int cmd_replay(int argc, goto cleanup; /* Build reflog message */ - if (opts.advance) { + if (opts.revert) { + strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert); + } else if (opts.advance) { strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance); } else { struct object_id oid; diff --git a/replay.c b/replay.c index a63f6714c4..d7239d4c83 100644 --- a/replay.c +++ b/replay.c @@ -8,6 +8,7 @@ #include "refs.h" #include "replay.h" #include "revision.h" +#include "sequencer.h" #include "strmap.h" #include "tree.h" @@ -17,6 +18,11 @@ */ #define the_repository DO_NOT_USE_THE_REPOSITORY +enum replay_mode { + REPLAY_MODE_PICK, + REPLAY_MODE_REVERT, +}; + static const char *short_commit_name(struct repository *repo, struct commit *commit) { @@ -50,15 +56,37 @@ static char *get_author(const char *message) return NULL; } +static void generate_revert_message(struct strbuf *msg, + struct commit *commit, + struct repository *repo) +{ + const char *out_enc = get_commit_output_encoding(); + const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc); + const char *subject_start; + int subject_len; + char *subject; + + subject_len = find_commit_subject(message, &subject_start); + subject = xmemdupz(subject_start, subject_len); + + sequencer_format_revert_message(repo, subject, commit, + commit->parents ? commit->parents->item : NULL, + false, msg); + + free(subject); + repo_unuse_commit_buffer(repo, commit, message); +} + static struct commit *create_commit(struct repository *repo, struct tree *tree, struct commit *based_on, - struct commit *parent) + struct commit *parent, + enum replay_mode mode) { struct object_id ret; struct object *obj = NULL; struct commit_list *parents = NULL; - char *author; + char *author = NULL; char *sign_commit = NULL; /* FIXME: cli users might want to sign again */ struct commit_extra_header *extra = NULL; struct strbuf msg = STRBUF_INIT; @@ -70,9 +98,16 @@ static struct commit *create_commit(struct repository *repo, commit_list_insert(parent, &parents); extra = read_commit_extra_headers(based_on, exclude_gpgsig); - find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); + if (mode == REPLAY_MODE_REVERT) { + generate_revert_message(&msg, based_on, repo); + /* For revert, use current user as author (NULL = use default) */ + } else if (mode == REPLAY_MODE_PICK) { + find_commit_subject(message, &orig_message); + strbuf_addstr(&msg, orig_message); + author = get_author(message); + } else { + BUG("unexpected replay mode %d", mode); + } reset_ident_date(); if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, &ret, author, NULL, sign_commit, extra)) { @@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo, } } +static void set_up_branch_mode(struct repository *repo, + char **branch_name, + const char *option_name, + struct ref_info *rinfo, + struct commit **onto) +{ + struct object_id oid; + char *fullname = NULL; + + if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name), + &oid, &fullname, 0) == 1) { + free(*branch_name); + *branch_name = fullname; + } else { + die(_("argument to %s must be a reference"), option_name); + } + *onto = peel_committish(repo, *branch_name, option_name); + if (rinfo->positive_refexprs > 1) + die(_("'%s' cannot be used with multiple revision ranges " + "because the ordering would be ill-defined"), + option_name); +} + static void set_up_replay_mode(struct repository *repo, struct rev_cmdline_info *cmd_info, const char *onto_name, bool *detached_head, char **advance_name, + char **revert_name, struct commit **onto, struct strset **update_refs) { @@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo, if (!rinfo.positive_refexprs) die(_("need some commits to replay")); - if (!onto_name == !*advance_name) - BUG("one and only one of onto_name and *advance_name must be given"); - if (onto_name) { *onto = peel_committish(repo, onto_name, "--onto"); if (rinfo.positive_refexprs < @@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo, *update_refs = xcalloc(1, sizeof(**update_refs)); **update_refs = rinfo.positive_refs; memset(&rinfo.positive_refs, 0, sizeof(**update_refs)); + } else if (*advance_name) { + set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto); + } else if (*revert_name) { + set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto); } else { - struct object_id oid; - char *fullname = NULL; - - if (!*advance_name) - BUG("expected either onto_name or *advance_name in this function"); - - if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name), - &oid, &fullname, 0) == 1) { - free(*advance_name); - *advance_name = fullname; - } else { - die(_("argument to --advance must be a reference")); - } - *onto = peel_committish(repo, *advance_name, "--advance"); - if (rinfo.positive_refexprs > 1) - die(_("cannot advance target with multiple sources because ordering would be ill-defined")); + BUG("expected one of onto_name, *advance_name, or *revert_name"); } strset_clear(&rinfo.negative_refs); strset_clear(&rinfo.positive_refs); @@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo, kh_oid_map_t *replayed_commits, struct commit *onto, struct merge_options *merge_opt, - struct merge_result *result) + struct merge_result *result, + enum replay_mode mode) { struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree, *replayed_base_tree; @@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo, pickme_tree = repo_get_commit_tree(repo, pickme); base_tree = repo_get_commit_tree(repo, base); - merge_opt->branch1 = short_commit_name(repo, replayed_base); - merge_opt->branch2 = short_commit_name(repo, pickme); - merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); - - merge_incore_nonrecursive(merge_opt, - base_tree, - replayed_base_tree, - pickme_tree, - result); - - free((char*)merge_opt->ancestor); + if (mode == REPLAY_MODE_PICK) { + /* Cherry-pick: normal order */ + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = short_commit_name(repo, pickme); + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + + merge_incore_nonrecursive(merge_opt, + base_tree, + replayed_base_tree, + pickme_tree, + result); + + free((char *)merge_opt->ancestor); + } else if (mode == REPLAY_MODE_REVERT) { + /* Revert: swap base and pickme to reverse the diff */ + const char *pickme_name = short_commit_name(repo, pickme); + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); + merge_opt->ancestor = pickme_name; + + merge_incore_nonrecursive(merge_opt, + pickme_tree, + replayed_base_tree, + base_tree, + result); + + free((char *)merge_opt->branch2); + } else { + BUG("unexpected replay mode %d", mode); + } merge_opt->ancestor = NULL; + merge_opt->branch2 = NULL; if (!result->clean) return NULL; /* Drop commits that become empty */ if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) && !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) return replayed_base; - return create_commit(repo, result->tree, pickme, replayed_base); + return create_commit(repo, result->tree, pickme, replayed_base, mode); } void replay_result_release(struct replay_result *result) @@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs, }; bool detached_head; char *advance; + char *revert; + enum replay_mode mode = REPLAY_MODE_PICK; int ret; advance = xstrdup_or_null(opts->advance); + revert = xstrdup_or_null(opts->revert); + if (revert) + mode = REPLAY_MODE_REVERT; set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, - &detached_head, &advance, &onto, &update_refs); + &detached_head, &advance, &revert, &onto, &update_refs); /* FIXME: Should allow replaying commits with the first as a root commit */ @@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs, die(_("replaying merge commits is not supported yet!")); last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, - onto, &merge_opt, &result); + mode == REPLAY_MODE_REVERT ? last_commit : onto, + &merge_opt, &result, mode); if (!last_commit) break; @@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs, kh_value(replayed_commits, pos) = last_commit; /* Update any necessary branches */ - if (advance) + if (advance || revert) continue; for (decoration = get_name_decoration(&commit->object); @@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs, goto out; } - /* In --advance mode, advance the target ref */ - if (advance) - replay_result_queue_update(out, advance, + /* In --advance or --revert mode, update the target ref */ + if (advance || revert) { + const char *ref = advance ? advance : revert; + replay_result_queue_update(out, ref, &onto->object.oid, &last_commit->object.oid); + } ret = 0; @@ -377,5 +451,6 @@ int replay_revisions(struct rev_info *revs, kh_destroy_oid_map(replayed_commits); merge_finalize(&merge_opt, &result); free(advance); + free(revert); return ret; } diff --git a/replay.h b/replay.h index d8407dc7f7..e916a5f975 100644 --- a/replay.h +++ b/replay.h @@ -13,7 +13,7 @@ struct replay_revisions_options { /* * Starting point at which to create the new commits; must be a branch * name. The branch will be updated to point to the rewritten commits. - * This option is mutually exclusive with `onto`. + * This option is mutually exclusive with `onto` and `revert`. */ const char *advance; @@ -22,7 +22,14 @@ struct replay_revisions_options { * committish. References pointing at decendants of `onto` will be * updated to point to the new commits. */ - const char *onto; + const char *onto; + + /* + * Starting point at which to create revert commits; must be a branch + * name. The branch will be updated to point to the revert commits. + * This option is mutually exclusive with `onto` and `advance`. + */ + const char *revert; /* * Update branches that point at commits in the given revision range. diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index a03f8f9293..217f6fb292 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' ' test_cmp expect actual ' -test_expect_success 'option --onto or --advance is mandatory' ' - echo "error: option --onto or --advance is mandatory" >expect && +test_expect_success 'exactly one of --onto, --advance, or --revert is required' ' + echo "error: exactly one of --onto, --advance, or --revert is required" >expect && test_might_fail git replay -h >>expect && test_must_fail git replay topic1..topic2 2>actual && test_cmp expect actual @@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err test_cmp expect actual ' -test_expect_success 'options --advance and --contained cannot be used together' ' - printf "fatal: options ${SQ}--advance${SQ} " >expect && - printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect && +test_expect_success '--advance and --contained cannot be used together' ' test_must_fail git replay --advance=main --contained \ topic1..topic2 2>actual && - test_cmp expect actual + test_grep "cannot be used together" actual ' test_expect_success 'cannot advance target ... ordering would be ill-defined' ' - echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect && + echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect && test_must_fail git replay --advance=main main topic1 topic2 2>actual && test_cmp expect actual ' @@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' ' test_grep "invalid.*replay.refAction.*value" error ' +test_expect_success 'argument to --revert must be a reference' ' + echo "fatal: argument to --revert must be a reference" >expect && + oid=$(git rev-parse main) && + test_must_fail git replay --revert=$oid topic1..topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'cannot revert with multiple sources' ' + echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect && + test_must_fail git replay --revert main main topic1 topic2 2>actual && + test_cmp expect actual +' + +test_expect_success 'using replay --revert to revert commits' ' + # Reuse existing topic4 branch (has commits I and J on top of main) + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # Revert commits I and J + git replay --revert topic4 topic4~2..topic4 && + + # Verify the revert commits were created (newest-first ordering + # means J is reverted first, then I on top) + git log --format=%s -4 topic4 >actual && + cat >expect <<-\EOF && + Revert "I" + Revert "J" + J + I + EOF + test_cmp expect actual && + + # Verify commit message format includes hash (tip is Revert "I") + test_commit_message topic4 <<-EOF && + Revert "I" + + This reverts commit $(git rev-parse I). + EOF + + # Verify reflog message + git reflog topic4 -1 --format=%gs >reflog-msg && + echo "replay --revert topic4" >expect-reflog && + test_cmp expect-reflog reflog-msg +' + +test_expect_success 'using replay --revert in bare repo' ' + # Reuse existing topic4 in bare repo + START=$(git -C bare rev-parse topic4) && + test_when_finished "git -C bare update-ref refs/heads/topic4 $START" && + + # Revert commit J in bare repo + git -C bare replay --revert topic4 topic4~1..topic4 && + + # Verify revert was created + git -C bare log -1 --format=%s topic4 >actual && + echo "Revert \"J\"" >expect && + test_cmp expect actual +' + +test_expect_success 'revert of revert uses Reapply' ' + # Use topic4 and first revert J, then revert the revert + START=$(git rev-parse topic4) && + test_when_finished "git branch -f topic4 $START" && + + # First revert J + git replay --revert topic4 topic4~1..topic4 && + REVERT_J=$(git rev-parse topic4) && + + # Now revert the revert - should become Reapply + git replay --revert topic4 topic4~1..topic4 && + + # Verify Reapply prefix and message format + test_commit_message topic4 <<-EOF + Reapply "J" + + This reverts commit $REVERT_J. + EOF +' + +test_expect_success 'git replay --revert with conflict' ' + # conflict branch has C.conflict which conflicts with topic1s C + test_expect_code 1 git replay --revert conflict B..topic1 +' + +test_expect_success 'git replay --revert incompatible with --contained' ' + test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --onto' ' + test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + +test_expect_success 'git replay --revert incompatible with --advance' ' + test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error && + test_grep "cannot be used together" error +' + test_done -- 2.51.0 ^ permalink raw reply related [flat|nested] 92+ messages in thread
end of thread, other threads:[~2026-03-28 4:33 UTC | newest] Thread overview: 92+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2025-11-25 17:00 [PATCH 0/1] replay: add --revert option to reverse commit changes Siddharth Asthana 2025-11-25 17:00 ` [PATCH 1/1] " Siddharth Asthana 2025-11-25 19:22 ` Junio C Hamano 2025-11-25 19:30 ` Junio C Hamano 2025-11-25 19:39 ` Junio C Hamano 2025-11-25 20:06 ` Junio C Hamano 2025-11-26 19:31 ` Siddharth Asthana 2025-11-26 19:28 ` Siddharth Asthana 2025-11-26 19:26 ` Siddharth Asthana 2025-11-26 21:13 ` Junio C Hamano 2025-11-27 19:23 ` Siddharth Asthana 2025-11-26 11:10 ` Phillip Wood 2025-11-26 17:35 ` Elijah Newren 2025-11-26 18:41 ` Junio C Hamano 2025-11-26 21:17 ` Junio C Hamano 2025-11-26 23:06 ` Elijah Newren 2025-11-26 23:14 ` Junio C Hamano 2025-11-26 23:57 ` Elijah Newren 2025-11-26 19:50 ` Siddharth Asthana 2025-11-26 19:39 ` Siddharth Asthana 2025-11-27 16:21 ` Phillip Wood 2025-11-27 19:24 ` Siddharth Asthana 2025-11-25 17:25 ` [PATCH 0/1] " Johannes Schindelin 2025-11-25 18:02 ` Junio C Hamano 2025-11-26 19:18 ` Siddharth Asthana 2025-11-26 21:04 ` Junio C Hamano 2025-11-27 19:21 ` Siddharth Asthana 2025-11-27 20:17 ` Junio C Hamano 2025-11-28 8:07 ` Elijah Newren 2025-11-28 8:24 ` Siddharth Asthana 2025-11-28 16:35 ` Junio C Hamano 2025-11-28 17:07 ` Elijah Newren 2025-11-28 20:50 ` Junio C Hamano 2025-11-28 22:03 ` Elijah Newren 2025-11-29 5:59 ` Junio C Hamano 2025-12-02 20:16 ` [PATCH v2 0/2] replay: add --revert mode " Siddharth Asthana 2025-12-02 20:16 ` [PATCH v2 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2025-12-05 11:33 ` Patrick Steinhardt 2025-12-07 23:00 ` Siddharth Asthana 2025-12-08 7:07 ` Patrick Steinhardt 2026-02-11 13:03 ` Toon Claes 2026-02-11 13:40 ` Patrick Steinhardt 2026-02-11 15:23 ` Kristoffer Haugsbakk 2026-02-11 17:41 ` Junio C Hamano 2026-02-18 22:53 ` Siddharth Asthana 2025-12-02 20:16 ` [PATCH v2 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2025-12-05 11:33 ` Patrick Steinhardt 2025-12-07 23:03 ` Siddharth Asthana 2025-12-16 16:23 ` Phillip Wood 2026-02-18 23:42 ` [PATCH v3 0/2] " Siddharth Asthana 2026-02-18 23:42 ` [PATCH v3 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-02-20 17:01 ` Toon Claes 2026-02-25 21:53 ` Junio C Hamano 2026-03-06 4:55 ` Siddharth Asthana 2026-03-06 4:31 ` Siddharth Asthana 2026-02-26 14:27 ` Phillip Wood 2026-03-06 5:00 ` Siddharth Asthana 2026-02-18 23:42 ` [PATCH v3 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2026-02-20 17:35 ` Toon Claes 2026-02-20 20:23 ` Junio C Hamano 2026-02-23 9:13 ` Christian Couder 2026-02-23 11:23 ` Toon Claes 2026-03-06 5:05 ` Siddharth Asthana 2026-02-26 14:45 ` Phillip Wood 2026-03-06 5:28 ` Siddharth Asthana 2026-03-06 15:52 ` Phillip Wood 2026-03-06 16:20 ` Siddharth Asthana 2026-03-13 5:40 ` [PATCH v4 0/2] " Siddharth Asthana 2026-03-13 5:40 ` [PATCH v4 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-03-13 15:53 ` Junio C Hamano 2026-03-16 19:12 ` Toon Claes 2026-03-16 16:57 ` Phillip Wood 2026-03-13 5:40 ` [PATCH v4 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2026-03-16 16:57 ` Phillip Wood 2026-03-16 19:52 ` Toon Claes 2026-03-17 10:11 ` Phillip Wood 2026-03-16 16:59 ` [PATCH v4 0/2] " Phillip Wood 2026-03-16 19:53 ` Toon Claes 2026-03-24 22:03 ` [PATCH v5 " Siddharth Asthana 2026-03-24 22:04 ` [PATCH v5 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-03-24 22:04 ` [PATCH v5 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2026-03-25 6:29 ` Junio C Hamano 2026-03-25 15:10 ` Toon Claes 2026-03-25 15:38 ` Siddharth Asthana 2026-03-25 16:44 ` Phillip Wood 2026-03-25 15:36 ` Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 0/2] " Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana 2026-03-28 4:33 ` Tian Yuchen 2026-03-25 20:23 ` [PATCH v6 1/2] sequencer: extract revert message formatting into shared function Siddharth Asthana 2026-03-25 20:23 ` [PATCH v6 2/2] replay: add --revert mode to reverse commit changes Siddharth Asthana
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox