Skip to content

Commit ba5c0d0

Browse files
edith007gitster
authored andcommitted
replay: add --revert mode to reverse commit changes
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> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 8ba0f3e commit ba5c0d0

5 files changed

Lines changed: 305 additions & 70 deletions

File tree

Documentation/git-replay.adoc

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
99
SYNOPSIS
1010
--------
1111
[verse]
12-
(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>
12+
(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...
1313

1414
DESCRIPTION
1515
-----------
@@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to
4242
point at the tip of the resulting history. This is different from `--onto`,
4343
which uses the target only as a starting point without updating it.
4444

45+
--revert <branch>::
46+
Starting point at which to create the reverted commits; must be a
47+
branch name.
48+
+
49+
When `--revert` is specified, the commits in the revision range are reverted
50+
(their changes are undone) and the reverted commits are created on top of
51+
<branch>. The <branch> is then updated to point at the new commits. This is
52+
the same as running `git revert <revision-range>` but does not update the
53+
working tree.
54+
+
55+
The commit messages follow `git revert` conventions: they are prefixed with
56+
"Revert" and include "This reverts commit <hash>." When reverting a commit
57+
whose message starts with "Revert", the new message uses "Reapply" instead.
58+
Unlike cherry-pick which preserves the original author, revert commits use
59+
the current user as the author, matching the behavior of `git revert`.
60+
+
61+
This option is mutually exclusive with `--onto` and `--advance`. It is also
62+
incompatible with `--contained` (which is a modifier for `--onto` only).
63+
4564
--contained::
4665
Update all branches that point at commits in
4766
<revision-range>. Requires `--onto`.
@@ -84,9 +103,10 @@ When using `--ref-action=print`, the output is usable as input to
84103
update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
85104

86105
where the number of refs updated depends on the arguments passed and
87-
the shape of the history being replayed. When using `--advance`, the
88-
number of refs updated is always one, but for `--onto`, it can be one
89-
or more (rebasing multiple branches simultaneously is supported).
106+
the shape of the history being replayed. When using `--advance` or
107+
`--revert`, the number of refs updated is always one, but for `--onto`,
108+
it can be one or more (rebasing multiple branches simultaneously is
109+
supported).
90110

91111
There is no stderr output on conflicts; see the <<exit-status,EXIT
92112
STATUS>> section below.
@@ -152,6 +172,21 @@ all commits they have since `base`, playing them on top of
152172
`origin/main`. These three branches may have commits on top of `base`
153173
that they have in common, but that does not need to be the case.
154174

175+
To revert commits on a branch:
176+
177+
------------
178+
$ git replay --revert main topic~2..topic
179+
------------
180+
181+
This reverts the last two commits from `topic`, creating revert commits on
182+
top of `main`, and updates `main` to point at the result. This is useful when
183+
commits from `topic` were previously merged or cherry-picked into `main` and
184+
need to be undone.
185+
186+
NOTE: For reverting an entire merge request as a single commit (rather than
187+
commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
188+
which can avoid unnecessary merge conflicts.
189+
155190
GIT
156191
---
157192
Part of the linkgit:git[1] suite

builtin/replay.c

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ int cmd_replay(int argc,
8383

8484
const char *const replay_usage[] = {
8585
N_("(EXPERIMENTAL!) git replay "
86-
"([--contained] --onto <newbase> | --advance <branch>) "
87-
"[--ref-action[=<mode>]] <revision-range>"),
86+
"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
87+
"[--ref-action[=<mode>]] <revision-range>..."),
8888
NULL
8989
};
9090
struct option replay_options[] = {
@@ -96,6 +96,9 @@ int cmd_replay(int argc,
9696
N_("replay onto given commit")),
9797
OPT_BOOL(0, "contained", &opts.contained,
9898
N_("update all branches that point at commits in <revision-range>")),
99+
OPT_STRING(0, "revert", &opts.revert,
100+
N_("branch"),
101+
N_("revert commits onto given branch")),
99102
OPT_STRING(0, "ref-action", &ref_action,
100103
N_("mode"),
101104
N_("control ref update behavior (update|print)")),
@@ -105,15 +108,17 @@ int cmd_replay(int argc,
105108
argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
106109
PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
107110

108-
if (!opts.onto && !opts.advance) {
109-
error(_("option --onto or --advance is mandatory"));
111+
/* Exactly one mode must be specified */
112+
if (!opts.onto && !opts.advance && !opts.revert) {
113+
error(_("exactly one of --onto, --advance, or --revert is required"));
110114
usage_with_options(replay_usage, replay_options);
111115
}
112116

113-
die_for_incompatible_opt2(!!opts.advance, "--advance",
114-
opts.contained, "--contained");
115-
die_for_incompatible_opt2(!!opts.advance, "--advance",
116-
!!opts.onto, "--onto");
117+
die_for_incompatible_opt3(!!opts.onto, "--onto",
118+
!!opts.advance, "--advance",
119+
!!opts.revert, "--revert");
120+
if (opts.contained && !opts.onto)
121+
die(_("--contained requires --onto"));
117122

118123
/* Parse ref action mode from command line or config */
119124
ref_mode = get_ref_action_mode(repo, ref_action);
@@ -129,7 +134,13 @@ int cmd_replay(int argc,
129134
* some options changing these values if we think they could
130135
* be useful.
131136
*/
132-
revs.reverse = 1;
137+
/*
138+
* Cherry-pick/rebase need oldest-first ordering so that each
139+
* replayed commit can build on its already-replayed parent.
140+
* Revert needs newest-first ordering (like git revert) to
141+
* reduce conflicts by peeling off changes from the top.
142+
*/
143+
revs.reverse = opts.revert ? 0 : 1;
133144
revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
134145
revs.topo_order = 1;
135146
revs.simplify_history = 0;
@@ -144,11 +155,14 @@ int cmd_replay(int argc,
144155
* Detect and warn if we override some user specified rev
145156
* walking options.
146157
*/
147-
if (revs.reverse != 1) {
148-
warning(_("some rev walking options will be overridden as "
149-
"'%s' bit in 'struct rev_info' will be forced"),
150-
"reverse");
151-
revs.reverse = 1;
158+
{
159+
int desired_reverse = opts.revert ? 0 : 1;
160+
if (revs.reverse != desired_reverse) {
161+
warning(_("some rev walking options will be overridden as "
162+
"'%s' bit in 'struct rev_info' will be forced"),
163+
"reverse");
164+
revs.reverse = desired_reverse;
165+
}
152166
}
153167
if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
154168
warning(_("some rev walking options will be overridden as "
@@ -174,7 +188,9 @@ int cmd_replay(int argc,
174188
goto cleanup;
175189

176190
/* Build reflog message */
177-
if (opts.advance) {
191+
if (opts.revert) {
192+
strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
193+
} else if (opts.advance) {
178194
strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
179195
} else {
180196
struct object_id oid;

0 commit comments

Comments
 (0)