Subject: [PATCH v5 0/2] replay: add --revert mode to reverse commit changes

From: Siddharth Asthana <[email-masked]>
Date: Wed, 25 Mar 2026 03:33:59 +0530

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/[email-masked]/
- Link to v3: https://public-inbox.org/git/[email-masked]/
- Link to v2: https://public-inbox.org/git/[email-masked]/
- Link to v1: https://public-inbox.org/git/[email-masked]/

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