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

From: Siddharth Asthana <[email-masked]>
Date: Thu, 19 Feb 2026 05:12:13 +0530

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