From: Siddharth Asthana Date: Tue, 24 Mar 2026 22:03:59 GMT Subject: [PATCH v5 0/2] replay: add --revert mode to reverse commit changes Message-ID: <20260324220401.47040-1-siddharthasthana31@gmail.com> In-Reply-To: <20260313054035.26605-1-siddharthasthana31@gmail.com> 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 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 ... 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 \"\""), revert of revert cases -+ * ("Reapply \"\""), and the --reference style. Appends "This reverts -+ * commit ." 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 ""' or ++ * 'Reapply ""' prefix and 'This reverts commit .' body. ++ * When use_commit_reference is set, 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 | --advance ) [--ref-action[=]] -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto | --advance | --revert ) [--ref-action[=]] ... ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto | --advance | --revert ) [--ref-action[=]] DESCRIPTION ----------- @@ builtin/replay.c: int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto | --advance ) " -- "[--ref-action[=]] "), + "([--contained] --onto | --advance | --revert ) " -+ "[--ref-action[=]] ..."), + "[--ref-action[=]] "), 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