Skip to content

Commit f79189a

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. 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> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent c573851 commit f79189a

5 files changed

Lines changed: 277 additions & 65 deletions

File tree

Documentation/git-replay.adoc

Lines changed: 33 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,15 @@ 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 main~2..main
179+
------------
180+
181+
This reverts the last two commits on `main`, creating two revert commits
182+
on top of `main`, and updates `main` to point at the result.
183+
155184
GIT
156185
---
157186
Part of the linkgit:git[1] suite

builtin/replay.c

Lines changed: 16 additions & 9 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);
@@ -174,7 +179,9 @@ int cmd_replay(int argc,
174179
goto cleanup;
175180

176181
/* Build reflog message */
177-
if (opts.advance) {
182+
if (opts.revert) {
183+
strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
184+
} else if (opts.advance) {
178185
strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
179186
} else {
180187
struct object_id oid;

replay.c

Lines changed: 117 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@
88
#include "refs.h"
99
#include "replay.h"
1010
#include "revision.h"
11+
#include "sequencer.h"
1112
#include "strmap.h"
1213
#include "tree.h"
1314

15+
enum replay_mode {
16+
REPLAY_MODE_PICK,
17+
REPLAY_MODE_REVERT,
18+
};
19+
1420
static const char *short_commit_name(struct repository *repo,
1521
struct commit *commit)
1622
{
@@ -44,15 +50,35 @@ static char *get_author(const char *message)
4450
return NULL;
4551
}
4652

53+
static void generate_revert_message(struct strbuf *msg,
54+
struct commit *commit,
55+
struct repository *repo)
56+
{
57+
const char *out_enc = get_commit_output_encoding();
58+
const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);
59+
const char *subject_start;
60+
int subject_len;
61+
char *subject;
62+
63+
subject_len = find_commit_subject(message, &subject_start);
64+
subject = xmemdupz(subject_start, subject_len);
65+
66+
sequencer_format_revert_header(msg, subject, &commit->object.oid);
67+
68+
free(subject);
69+
repo_unuse_commit_buffer(repo, commit, message);
70+
}
71+
4772
static struct commit *create_commit(struct repository *repo,
4873
struct tree *tree,
4974
struct commit *based_on,
50-
struct commit *parent)
75+
struct commit *parent,
76+
enum replay_mode mode)
5177
{
5278
struct object_id ret;
5379
struct object *obj = NULL;
5480
struct commit_list *parents = NULL;
55-
char *author;
81+
char *author = NULL;
5682
char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
5783
struct commit_extra_header *extra = NULL;
5884
struct strbuf msg = STRBUF_INIT;
@@ -64,9 +90,16 @@ static struct commit *create_commit(struct repository *repo,
6490

6591
commit_list_insert(parent, &parents);
6692
extra = read_commit_extra_headers(based_on, exclude_gpgsig);
67-
find_commit_subject(message, &orig_message);
68-
strbuf_addstr(&msg, orig_message);
69-
author = get_author(message);
93+
if (mode == REPLAY_MODE_REVERT) {
94+
generate_revert_message(&msg, based_on, repo);
95+
/* For revert, use current user as author (NULL = use default) */
96+
} else if (mode == REPLAY_MODE_PICK) {
97+
find_commit_subject(message, &orig_message);
98+
strbuf_addstr(&msg, orig_message);
99+
author = get_author(message);
100+
} else {
101+
BUG("unexpected replay mode %d", mode);
102+
}
70103
reset_ident_date();
71104
if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
72105
&ret, author, NULL, sign_commit, extra)) {
@@ -147,11 +180,34 @@ static void get_ref_information(struct repository *repo,
147180
}
148181
}
149182

183+
static void set_up_branch_mode(struct repository *repo,
184+
char **branch_name,
185+
const char *option_name,
186+
struct ref_info *rinfo,
187+
struct commit **onto)
188+
{
189+
struct object_id oid;
190+
char *fullname = NULL;
191+
192+
if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
193+
&oid, &fullname, 0) == 1) {
194+
free(*branch_name);
195+
*branch_name = fullname;
196+
} else {
197+
die(_("argument to %s must be a reference"), option_name);
198+
}
199+
*onto = peel_committish(repo, *branch_name, option_name);
200+
if (rinfo->positive_refexprs > 1)
201+
die(_("cannot %s target with multiple sources because ordering would be ill-defined"),
202+
option_name + 2); /* skip "--" prefix */
203+
}
204+
150205
static void set_up_replay_mode(struct repository *repo,
151206
struct rev_cmdline_info *cmd_info,
152207
const char *onto_name,
153208
bool *detached_head,
154209
char **advance_name,
210+
char **revert_name,
155211
struct commit **onto,
156212
struct strset **update_refs)
157213
{
@@ -166,9 +222,6 @@ static void set_up_replay_mode(struct repository *repo,
166222
if (!rinfo.positive_refexprs)
167223
die(_("need some commits to replay"));
168224

169-
if (!onto_name == !*advance_name)
170-
BUG("one and only one of onto_name and *advance_name must be given");
171-
172225
if (onto_name) {
173226
*onto = peel_committish(repo, onto_name, "--onto");
174227
if (rinfo.positive_refexprs <
@@ -177,23 +230,12 @@ static void set_up_replay_mode(struct repository *repo,
177230
*update_refs = xcalloc(1, sizeof(**update_refs));
178231
**update_refs = rinfo.positive_refs;
179232
memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
233+
} else if (*advance_name) {
234+
set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
235+
} else if (*revert_name) {
236+
set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
180237
} else {
181-
struct object_id oid;
182-
char *fullname = NULL;
183-
184-
if (!*advance_name)
185-
BUG("expected either onto_name or *advance_name in this function");
186-
187-
if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
188-
&oid, &fullname, 0) == 1) {
189-
free(*advance_name);
190-
*advance_name = fullname;
191-
} else {
192-
die(_("argument to --advance must be a reference"));
193-
}
194-
*onto = peel_committish(repo, *advance_name, "--advance");
195-
if (rinfo.positive_refexprs > 1)
196-
die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
238+
BUG("expected one of onto_name, *advance_name, or *revert_name");
197239
}
198240
strset_clear(&rinfo.negative_refs);
199241
strset_clear(&rinfo.positive_refs);
@@ -214,7 +256,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
214256
kh_oid_map_t *replayed_commits,
215257
struct commit *onto,
216258
struct merge_options *merge_opt,
217-
struct merge_result *result)
259+
struct merge_result *result,
260+
enum replay_mode mode)
218261
{
219262
struct commit *base, *replayed_base;
220263
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -226,25 +269,46 @@ static struct commit *pick_regular_commit(struct repository *repo,
226269
pickme_tree = repo_get_commit_tree(repo, pickme);
227270
base_tree = repo_get_commit_tree(repo, base);
228271

229-
merge_opt->branch1 = short_commit_name(repo, replayed_base);
230-
merge_opt->branch2 = short_commit_name(repo, pickme);
231-
merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
232-
233-
merge_incore_nonrecursive(merge_opt,
234-
base_tree,
235-
replayed_base_tree,
236-
pickme_tree,
237-
result);
238-
239-
free((char*)merge_opt->ancestor);
272+
if (mode == REPLAY_MODE_PICK) {
273+
/* Cherry-pick: normal order */
274+
merge_opt->branch1 = short_commit_name(repo, replayed_base);
275+
merge_opt->branch2 = short_commit_name(repo, pickme);
276+
merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
277+
278+
merge_incore_nonrecursive(merge_opt,
279+
base_tree,
280+
replayed_base_tree,
281+
pickme_tree,
282+
result);
283+
284+
free((char *)merge_opt->ancestor);
285+
} else if (mode == REPLAY_MODE_REVERT) {
286+
/* Revert: swap base and pickme to reverse the diff */
287+
const char *pickme_name = short_commit_name(repo, pickme);
288+
merge_opt->branch1 = short_commit_name(repo, replayed_base);
289+
merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
290+
merge_opt->ancestor = pickme_name;
291+
292+
merge_incore_nonrecursive(merge_opt,
293+
pickme_tree,
294+
replayed_base_tree,
295+
base_tree,
296+
result);
297+
298+
free((char *)merge_opt->branch2);
299+
} else {
300+
BUG("unexpected replay mode %d", mode);
301+
}
240302
merge_opt->ancestor = NULL;
303+
merge_opt->branch2 = NULL;
241304
if (!result->clean)
242305
return NULL;
243-
/* Drop commits that become empty */
244-
if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
306+
/* Drop commits that become empty (only for picks) */
307+
if (mode == REPLAY_MODE_PICK &&
308+
oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
245309
!oideq(&pickme_tree->object.oid, &base_tree->object.oid))
246310
return replayed_base;
247-
return create_commit(repo, result->tree, pickme, replayed_base);
311+
return create_commit(repo, result->tree, pickme, replayed_base, mode);
248312
}
249313

250314
void replay_result_release(struct replay_result *result)
@@ -281,11 +345,16 @@ int replay_revisions(struct rev_info *revs,
281345
};
282346
bool detached_head;
283347
char *advance;
348+
char *revert;
349+
enum replay_mode mode = REPLAY_MODE_PICK;
284350
int ret;
285351

286352
advance = xstrdup_or_null(opts->advance);
353+
revert = xstrdup_or_null(opts->revert);
354+
if (revert)
355+
mode = REPLAY_MODE_REVERT;
287356
set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
288-
&detached_head, &advance, &onto, &update_refs);
357+
&detached_head, &advance, &revert, &onto, &update_refs);
289358

290359
/* FIXME: Should allow replaying commits with the first as a root commit */
291360

@@ -309,7 +378,7 @@ int replay_revisions(struct rev_info *revs,
309378
die(_("replaying merge commits is not supported yet!"));
310379

311380
last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
312-
onto, &merge_opt, &result);
381+
onto, &merge_opt, &result, mode);
313382
if (!last_commit)
314383
break;
315384

@@ -321,7 +390,7 @@ int replay_revisions(struct rev_info *revs,
321390
kh_value(replayed_commits, pos) = last_commit;
322391

323392
/* Update any necessary branches */
324-
if (advance)
393+
if (advance || revert)
325394
continue;
326395

327396
for (decoration = get_name_decoration(&commit->object);
@@ -355,11 +424,13 @@ int replay_revisions(struct rev_info *revs,
355424
goto out;
356425
}
357426

358-
/* In --advance mode, advance the target ref */
359-
if (advance)
360-
replay_result_queue_update(out, advance,
427+
/* In --advance or --revert mode, update the target ref */
428+
if (advance || revert) {
429+
const char *ref = advance ? advance : revert;
430+
replay_result_queue_update(out, ref,
361431
&onto->object.oid,
362432
&last_commit->object.oid);
433+
}
363434

364435
ret = 0;
365436

@@ -371,5 +442,6 @@ int replay_revisions(struct rev_info *revs,
371442
kh_destroy_oid_map(replayed_commits);
372443
merge_finalize(&merge_opt, &result);
373444
free(advance);
445+
free(revert);
374446
return ret;
375447
}

0 commit comments

Comments
 (0)