Skip to content

Commit 539047c

Browse files
jrngitster
authored andcommitted
revert: introduce --abort to cancel a failed cherry-pick
After running some ill-advised command like "git cherry-pick HEAD..linux-next", the bewildered novice may want to return to more familiar territory. Introduce a "git cherry-pick --abort" command that rolls back the entire cherry-pick sequence and places the repository back on solid ground. Just like "git merge --abort", this internally uses "git reset --merge", so local changes not involved in the conflict resolution are preserved. Signed-off-by: Jonathan Nieder <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 82433cd commit 539047c

File tree

5 files changed

+185
-3
lines changed

5 files changed

+185
-3
lines changed

Documentation/git-cherry-pick.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ SYNOPSIS
1111
'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff] <commit>...
1212
'git cherry-pick' --continue
1313
'git cherry-pick' --quit
14+
'git cherry-pick' --abort
1415

1516
DESCRIPTION
1617
-----------

Documentation/git-revert.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ SYNOPSIS
1111
'git revert' [--edit | --no-edit] [-n] [-m parent-number] [-s] <commit>...
1212
'git revert' --continue
1313
'git revert' --quit
14+
'git revert' --abort
1415

1516
DESCRIPTION
1617
-----------

Documentation/sequencer.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77
Forget about the current operation in progress. Can be used
88
to clear the sequencer state after a failed cherry-pick or
99
revert.
10+
11+
--abort::
12+
Cancel the operation and return to the pre-sequence state.

builtin/revert.c

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ static const char * const cherry_pick_usage[] = {
4040
};
4141

4242
enum replay_action { REVERT, CHERRY_PICK };
43-
enum replay_subcommand { REPLAY_NONE, REPLAY_REMOVE_STATE, REPLAY_CONTINUE };
43+
enum replay_subcommand {
44+
REPLAY_NONE,
45+
REPLAY_REMOVE_STATE,
46+
REPLAY_CONTINUE,
47+
REPLAY_ROLLBACK
48+
};
4449

4550
struct replay_opts {
4651
enum replay_action action;
@@ -135,9 +140,11 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
135140
const char *me = action_name(opts);
136141
int remove_state = 0;
137142
int contin = 0;
143+
int rollback = 0;
138144
struct option options[] = {
139145
OPT_BOOLEAN(0, "quit", &remove_state, "end revert or cherry-pick sequence"),
140146
OPT_BOOLEAN(0, "continue", &contin, "resume revert or cherry-pick sequence"),
147+
OPT_BOOLEAN(0, "abort", &rollback, "cancel revert or cherry-pick sequence"),
141148
OPT_BOOLEAN('n', "no-commit", &opts->no_commit, "don't automatically commit"),
142149
OPT_BOOLEAN('e', "edit", &opts->edit, "edit the commit message"),
143150
OPT_NOOP_NOARG('r', NULL),
@@ -173,13 +180,16 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
173180
verify_opt_mutually_compatible(me,
174181
"--quit", remove_state,
175182
"--continue", contin,
183+
"--abort", rollback,
176184
NULL);
177185

178186
/* Set the subcommand */
179187
if (remove_state)
180188
opts->subcommand = REPLAY_REMOVE_STATE;
181189
else if (contin)
182190
opts->subcommand = REPLAY_CONTINUE;
191+
else if (rollback)
192+
opts->subcommand = REPLAY_ROLLBACK;
183193
else
184194
opts->subcommand = REPLAY_NONE;
185195

@@ -188,8 +198,12 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
188198
char *this_operation;
189199
if (opts->subcommand == REPLAY_REMOVE_STATE)
190200
this_operation = "--quit";
191-
else
201+
else if (opts->subcommand == REPLAY_CONTINUE)
192202
this_operation = "--continue";
203+
else {
204+
assert(opts->subcommand == REPLAY_ROLLBACK);
205+
this_operation = "--abort";
206+
}
193207

194208
verify_opt_compatible(me, this_operation,
195209
"--no-commit", opts->no_commit,
@@ -850,7 +864,7 @@ static int create_seq_dir(void)
850864

851865
if (file_exists(seq_dir)) {
852866
error(_("a cherry-pick or revert is already in progress"));
853-
advise(_("try \"git cherry-pick (--continue | --quit)\""));
867+
advise(_("try \"git cherry-pick (--continue | --quit | --abort)\""));
854868
return -1;
855869
}
856870
else if (mkdir(seq_dir, 0777) < 0)
@@ -873,6 +887,71 @@ static void save_head(const char *head)
873887
die(_("Error wrapping up %s."), head_file);
874888
}
875889

890+
static int reset_for_rollback(const unsigned char *sha1)
891+
{
892+
const char *argv[4]; /* reset --merge <arg> + NULL */
893+
argv[0] = "reset";
894+
argv[1] = "--merge";
895+
argv[2] = sha1_to_hex(sha1);
896+
argv[3] = NULL;
897+
return run_command_v_opt(argv, RUN_GIT_CMD);
898+
}
899+
900+
static int rollback_single_pick(void)
901+
{
902+
unsigned char head_sha1[20];
903+
904+
if (!file_exists(git_path("CHERRY_PICK_HEAD")) &&
905+
!file_exists(git_path("REVERT_HEAD")))
906+
return error(_("no cherry-pick or revert in progress"));
907+
if (!resolve_ref("HEAD", head_sha1, 0, NULL))
908+
return error(_("cannot resolve HEAD"));
909+
if (is_null_sha1(head_sha1))
910+
return error(_("cannot abort from a branch yet to be born"));
911+
return reset_for_rollback(head_sha1);
912+
}
913+
914+
static int sequencer_rollback(struct replay_opts *opts)
915+
{
916+
const char *filename;
917+
FILE *f;
918+
unsigned char sha1[20];
919+
struct strbuf buf = STRBUF_INIT;
920+
921+
filename = git_path(SEQ_HEAD_FILE);
922+
f = fopen(filename, "r");
923+
if (!f && errno == ENOENT) {
924+
/*
925+
* There is no multiple-cherry-pick in progress.
926+
* If CHERRY_PICK_HEAD or REVERT_HEAD indicates
927+
* a single-cherry-pick in progress, abort that.
928+
*/
929+
return rollback_single_pick();
930+
}
931+
if (!f)
932+
return error(_("cannot open %s: %s"), filename,
933+
strerror(errno));
934+
if (strbuf_getline(&buf, f, '\n')) {
935+
error(_("cannot read %s: %s"), filename, ferror(f) ?
936+
strerror(errno) : _("unexpected end of file"));
937+
goto fail;
938+
}
939+
if (get_sha1_hex(buf.buf, sha1) || buf.buf[40] != '\0') {
940+
error(_("stored pre-cherry-pick HEAD file '%s' is corrupt"),
941+
filename);
942+
goto fail;
943+
}
944+
if (reset_for_rollback(sha1))
945+
goto fail;
946+
strbuf_release(&buf);
947+
fclose(f);
948+
return 0;
949+
fail:
950+
strbuf_release(&buf);
951+
fclose(f);
952+
return -1;
953+
}
954+
876955
static void save_todo(struct commit_list *todo_list, struct replay_opts *opts)
877956
{
878957
const char *todo_file = git_path(SEQ_TODO_FILE);
@@ -977,6 +1056,8 @@ static int pick_revisions(struct replay_opts *opts)
9771056
remove_sequencer_state(1);
9781057
return 0;
9791058
}
1059+
if (opts->subcommand == REPLAY_ROLLBACK)
1060+
return sequencer_rollback(opts);
9801061
if (opts->subcommand == REPLAY_CONTINUE) {
9811062
if (!file_exists(git_path(SEQ_TODO_FILE)))
9821063
return error(_("No %s in progress"), action_name(opts));

t/t3510-cherry-pick-sequence.sh

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
test_description='Test cherry-pick continuation features
44
5+
+ yetanotherpick: rewrites foo to e
56
+ anotherpick: rewrites foo to d
67
+ picked: rewrites foo to c
78
+ unrelatedpick: rewrites unrelated to reallyunrelated
@@ -19,6 +20,12 @@ pristine_detach () {
1920
git clean -d -f -f -q -x
2021
}
2122

23+
test_cmp_rev () {
24+
git rev-parse --verify "$1" >expect.rev &&
25+
git rev-parse --verify "$2" >actual.rev &&
26+
test_cmp expect.rev actual.rev
27+
}
28+
2229
test_expect_success setup '
2330
echo unrelated >unrelated &&
2431
git add unrelated &&
@@ -27,6 +34,7 @@ test_expect_success setup '
2734
test_commit unrelatedpick unrelated reallyunrelated &&
2835
test_commit picked foo c &&
2936
test_commit anotherpick foo d &&
37+
test_commit yetanotherpick foo e &&
3038
git config advice.detachedhead false
3139
3240
'
@@ -75,6 +83,11 @@ test_expect_success '--quit does not complain when no cherry-pick is in progress
7583
git cherry-pick --quit
7684
'
7785

86+
test_expect_success '--abort requires cherry-pick in progress' '
87+
pristine_detach initial &&
88+
test_must_fail git cherry-pick --abort
89+
'
90+
7891
test_expect_success '--quit cleans up sequencer state' '
7992
pristine_detach initial &&
8093
test_must_fail git cherry-pick base..picked &&
@@ -103,6 +116,79 @@ test_expect_success 'cherry-pick --reset (another name for --quit)' '
103116
test_cmp expect actual
104117
'
105118

119+
test_expect_success '--abort to cancel multiple cherry-pick' '
120+
pristine_detach initial &&
121+
test_must_fail git cherry-pick base..anotherpick &&
122+
git cherry-pick --abort &&
123+
test_path_is_missing .git/sequencer &&
124+
test_cmp_rev initial HEAD &&
125+
git update-index --refresh &&
126+
git diff-index --exit-code HEAD
127+
'
128+
129+
test_expect_success '--abort to cancel single cherry-pick' '
130+
pristine_detach initial &&
131+
test_must_fail git cherry-pick picked &&
132+
git cherry-pick --abort &&
133+
test_path_is_missing .git/sequencer &&
134+
test_cmp_rev initial HEAD &&
135+
git update-index --refresh &&
136+
git diff-index --exit-code HEAD
137+
'
138+
139+
test_expect_success 'cherry-pick --abort to cancel multiple revert' '
140+
pristine_detach anotherpick &&
141+
test_must_fail git revert base..picked &&
142+
git cherry-pick --abort &&
143+
test_path_is_missing .git/sequencer &&
144+
test_cmp_rev anotherpick HEAD &&
145+
git update-index --refresh &&
146+
git diff-index --exit-code HEAD
147+
'
148+
149+
test_expect_success 'revert --abort works, too' '
150+
pristine_detach anotherpick &&
151+
test_must_fail git revert base..picked &&
152+
git revert --abort &&
153+
test_path_is_missing .git/sequencer &&
154+
test_cmp_rev anotherpick HEAD
155+
'
156+
157+
test_expect_success '--abort to cancel single revert' '
158+
pristine_detach anotherpick &&
159+
test_must_fail git revert picked &&
160+
git revert --abort &&
161+
test_path_is_missing .git/sequencer &&
162+
test_cmp_rev anotherpick HEAD &&
163+
git update-index --refresh &&
164+
git diff-index --exit-code HEAD
165+
'
166+
167+
test_expect_success '--abort keeps unrelated change, easy case' '
168+
pristine_detach unrelatedpick &&
169+
echo changed >expect &&
170+
test_must_fail git cherry-pick picked..yetanotherpick &&
171+
echo changed >unrelated &&
172+
git cherry-pick --abort &&
173+
test_cmp expect unrelated
174+
'
175+
176+
test_expect_success '--abort refuses to clobber unrelated change, harder case' '
177+
pristine_detach initial &&
178+
echo changed >expect &&
179+
test_must_fail git cherry-pick base..anotherpick &&
180+
echo changed >unrelated &&
181+
test_must_fail git cherry-pick --abort &&
182+
test_cmp expect unrelated &&
183+
git rev-list HEAD >log &&
184+
test_line_count = 2 log &&
185+
test_must_fail git update-index --refresh &&
186+
187+
git checkout unrelated &&
188+
git cherry-pick --abort &&
189+
test_cmp_rev initial HEAD
190+
'
191+
106192
test_expect_success 'cherry-pick cleans up sequencer state when one commit is left' '
107193
pristine_detach initial &&
108194
test_must_fail git cherry-pick base..picked &&
@@ -127,6 +213,16 @@ test_expect_success 'cherry-pick cleans up sequencer state when one commit is le
127213
test_cmp expect actual
128214
'
129215

216+
test_expect_failure '--abort after last commit in sequence' '
217+
pristine_detach initial &&
218+
test_must_fail git cherry-pick base..picked &&
219+
git cherry-pick --abort &&
220+
test_path_is_missing .git/sequencer &&
221+
test_cmp_rev initial HEAD &&
222+
git update-index --refresh &&
223+
git diff-index --exit-code HEAD
224+
'
225+
130226
test_expect_success 'cherry-pick does not implicitly stomp an existing operation' '
131227
pristine_detach initial &&
132228
test_must_fail git cherry-pick base..anotherpick &&

0 commit comments

Comments
 (0)