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 <jrnieder@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Jonathan Nieder committed
Nov 22, 2011 at 19:27 UTC
539047c19ec040819b6f6af2d55714195b812abb
5 files changed
+185
-3
Documentation/git-cherry-pick.txt
+1
@@ -11,6 +11,7 @@ SYNOPSIS
11
'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff] <commit>...
12
'git cherry-pick' --continue
13
'git cherry-pick' --quit
14
+'git cherry-pick' --abort
15
16
DESCRIPTION
17
-----------
Documentation/git-revert.txt
+1
@@ -11,6 +11,7 @@ SYNOPSIS
11
'git revert' [--edit | --no-edit] [-n] [-m parent-number] [-s] <commit>...
12
'git revert' --continue
13
'git revert' --quit
14
+'git revert' --abort
15
16
DESCRIPTION
17
-----------
Documentation/sequencer.txt
+3
@@ -7,3 +7,6 @@
7
Forget about the current operation in progress. Can be used
8
to clear the sequencer state after a failed cherry-pick or
9
revert.
10
+
11
+--abort::
12
+ Cancel the operation and return to the pre-sequence state.
builtin/revert.c
+84
-3
@@ -40,7 +40,12 @@ static const char * const cherry_pick_usage[] = {
40
};
41
42
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
+};
49
50
struct replay_opts {
51
enum replay_action action;
@@ -135,9 +140,11 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
140
const char *me = action_name(opts);
141
int remove_state = 0;
142
int contin = 0;
143
+ int rollback = 0;
144
struct option options[] = {
145
OPT_BOOLEAN(0, "quit", &remove_state, "end revert or cherry-pick sequence"),
146
OPT_BOOLEAN(0, "continue", &contin, "resume revert or cherry-pick sequence"),
147
+ OPT_BOOLEAN(0, "abort", &rollback, "cancel revert or cherry-pick sequence"),
148
OPT_BOOLEAN('n', "no-commit", &opts->no_commit, "don't automatically commit"),
149
OPT_BOOLEAN('e', "edit", &opts->edit, "edit the commit message"),
150
OPT_NOOP_NOARG('r', NULL),
@@ -173,6 +180,7 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
180
verify_opt_mutually_compatible(me,
181
"--quit", remove_state,
182
"--continue", contin,
183
+ "--abort", rollback,
184
NULL);
185
186
/* Set the subcommand */
@@ -180,6 +188,8 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
188
opts->subcommand = REPLAY_REMOVE_STATE;
189
else if (contin)
190
opts->subcommand = REPLAY_CONTINUE;
191
+ else if (rollback)
192
+ opts->subcommand = REPLAY_ROLLBACK;
193
else
194
opts->subcommand = REPLAY_NONE;
195
@@ -188,8 +198,12 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
198
char *this_operation;
199
if (opts->subcommand == REPLAY_REMOVE_STATE)
200
this_operation = "--quit";
191
- else
201
+ else if (opts->subcommand == REPLAY_CONTINUE)
202
this_operation = "--continue";
203
+ else {
204
+ assert(opts->subcommand == REPLAY_ROLLBACK);
205
+ this_operation = "--abort";
206
+ }
207
208
verify_opt_compatible(me, this_operation,
209
"--no-commit", opts->no_commit,
@@ -850,7 +864,7 @@ static int create_seq_dir(void)
864
865
if (file_exists(seq_dir)) {
866
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)\""));
868
return -1;
869
}
870
else if (mkdir(seq_dir, 0777) < 0)
@@ -873,6 +887,71 @@ static void save_head(const char *head)
887
die(_("Error wrapping up %s."), head_file);
888
}
889
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
+
955
static void save_todo(struct commit_list *todo_list, struct replay_opts *opts)
956
{
957
const char *todo_file = git_path(SEQ_TODO_FILE);
@@ -977,6 +1056,8 @@ static int pick_revisions(struct replay_opts *opts)
1056
remove_sequencer_state(1);
1057
return 0;
1058
}
1059
+ if (opts->subcommand == REPLAY_ROLLBACK)
1060
+ return sequencer_rollback(opts);
1061
if (opts->subcommand == REPLAY_CONTINUE) {
1062
if (!file_exists(git_path(SEQ_TODO_FILE)))
1063
return error(_("No %s in progress"), action_name(opts));
t/t3510-cherry-pick-sequence.sh
+96
@@ -2,6 +2,7 @@
2
3
test_description='Test cherry-pick continuation features
4
5
+ + yetanotherpick: rewrites foo to e
6
+ anotherpick: rewrites foo to d
7
+ picked: rewrites foo to c
8
+ unrelatedpick: rewrites unrelated to reallyunrelated
@@ -19,6 +20,12 @@ pristine_detach () {
20
git clean -d -f -f -q -x
21
}
22
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
+
29
test_expect_success setup '
30
echo unrelated >unrelated &&
31
git add unrelated &&
@@ -27,6 +34,7 @@ test_expect_success setup '
34
test_commit unrelatedpick unrelated reallyunrelated &&
35
test_commit picked foo c &&
36
test_commit anotherpick foo d &&
37
+ test_commit yetanotherpick foo e &&
38
git config advice.detachedhead false
39
40
'
@@ -75,6 +83,11 @@ test_expect_success '--quit does not complain when no cherry-pick is in progress
83
git cherry-pick --quit
84
'
85
86
+test_expect_success '--abort requires cherry-pick in progress' '
87
+ pristine_detach initial &&
88
+ test_must_fail git cherry-pick --abort
89
+'
90
+
91
test_expect_success '--quit cleans up sequencer state' '
92
pristine_detach initial &&
93
test_must_fail git cherry-pick base..picked &&
@@ -103,6 +116,79 @@ test_expect_success 'cherry-pick --reset (another name for --quit)' '
116
test_cmp expect actual
117
'
118
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
+
192
test_expect_success 'cherry-pick cleans up sequencer state when one commit is left' '
193
pristine_detach initial &&
194
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
213
test_cmp expect actual
214
'
215
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
+
226
test_expect_success 'cherry-pick does not implicitly stomp an existing operation' '
227
pristine_detach initial &&
228
test_must_fail git cherry-pick base..anotherpick &&