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 &&