cherry-pick/revert: add --skip option
git am or rebase have a --skip flag to skip the current commit if the user wishes to do so. During a cherry-pick or revert a user could likewise skip a commit, but needs to use 'git reset' (or in the case of conflicts 'git reset --merge'), followed by 'git (cherry-pick | revert) --continue' to skip the commit. This is more annoying and sometimes confusing on the users' part. Add a `--skip` option to make skipping commits easier for the user and to make the commands more consistent. In the next commit, we will change the advice messages hence finishing the process of teaching revert and cherry-pick "how to skip commits". Signed-off-by: Rohit Ashiwal <rohit.ashiwal265@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Rohit Ashiwal committed
Jul 2, 2019 at 14:41 UTC
de81ca3f36b0f1db8bd615c98f2a5c819092c275
7 files changed
+187
-6
Documentation/git-cherry-pick.txt
+1
-3
@@ -10,9 +10,7 @@ SYNOPSIS
10
[verse]
11
'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff]
12
[-S[<keyid>]] <commit>...
13
-'git cherry-pick' --continue
14
-'git cherry-pick' --quit
15
-'git cherry-pick' --abort
13
+'git cherry-pick' (--continue | --skip | --abort | --quit)
14
15
DESCRIPTION
16
-----------
Documentation/git-revert.txt
+1
-3
@@ -9,9 +9,7 @@ SYNOPSIS
9
--------
10
[verse]
11
'git revert' [--[no-]edit] [-n] [-m parent-number] [-s] [-S[<keyid>]] <commit>...
12
-'git revert' --continue
13
-'git revert' --quit
14
-'git revert' --abort
12
+'git revert' (--continue | --skip | --abort | --quit)
13
14
DESCRIPTION
15
-----------
Documentation/sequencer.txt
+4
@@ -3,6 +3,10 @@
3
`.git/sequencer`. Can be used to continue after resolving
4
conflicts in a failed cherry-pick or revert.
5
6
+--skip::
7
+ Skip the current commit and continue with the rest of the
8
+ sequence.
9
+
10
--quit::
11
Forget about the current operation in progress. Can be used
12
to clear the sequencer state after a failed cherry-pick or
builtin/revert.c
+5
@@ -102,6 +102,7 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)
102
OPT_CMDMODE(0, "quit", &cmd, N_("end revert or cherry-pick sequence"), 'q'),
103
OPT_CMDMODE(0, "continue", &cmd, N_("resume revert or cherry-pick sequence"), 'c'),
104
OPT_CMDMODE(0, "abort", &cmd, N_("cancel revert or cherry-pick sequence"), 'a'),
105
+ OPT_CMDMODE(0, "skip", &cmd, N_("skip current commit and continue"), 's'),
106
OPT_CLEANUP(&cleanup_arg),
107
OPT_BOOL('n', "no-commit", &opts->no_commit, N_("don't automatically commit")),
108
OPT_BOOL('e', "edit", &opts->edit, N_("edit the commit message")),
@@ -151,6 +152,8 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)
152
this_operation = "--quit";
153
else if (cmd == 'c')
154
this_operation = "--continue";
155
+ else if (cmd == 's')
156
+ this_operation = "--skip";
157
else {
158
assert(cmd == 'a');
159
this_operation = "--abort";
@@ -210,6 +213,8 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)
213
return sequencer_continue(the_repository, opts);
214
if (cmd == 'a')
215
return sequencer_rollback(the_repository, opts);
216
+ if (cmd == 's')
217
+ return sequencer_skip(the_repository, opts);
218
return sequencer_pick_revisions(the_repository, opts);
219
}
220
sequencer.c
+73
@@ -2762,6 +2762,15 @@ static int rollback_single_pick(struct repository *r)
2762
return reset_merge(&head_oid);
2763
}
2764
2765
+static int skip_single_pick(void)
2766
+{
2767
+ struct object_id head;
2768
+
2769
+ if (read_ref_full("HEAD", 0, &head, NULL))
2770
+ return error(_("cannot resolve HEAD"));
2771
+ return reset_merge(&head);
2772
+}
2773
+
2774
int sequencer_rollback(struct repository *r, struct replay_opts *opts)
2775
{
2776
FILE *f;
@@ -2811,6 +2820,70 @@ fail:
2820
return -1;
2821
}
2822
2823
+int sequencer_skip(struct repository *r, struct replay_opts *opts)
2824
+{
2825
+ enum replay_action action = -1;
2826
+ sequencer_get_last_command(r, &action);
2827
+
2828
+ /*
2829
+ * Check whether the subcommand requested to skip the commit is actually
2830
+ * in progress and that it's safe to skip the commit.
2831
+ *
2832
+ * opts->action tells us which subcommand requested to skip the commit.
2833
+ * If the corresponding .git/<ACTION>_HEAD exists, we know that the
2834
+ * action is in progress and we can skip the commit.
2835
+ *
2836
+ * Otherwise we check that the last instruction was related to the
2837
+ * particular subcommand we're trying to execute and barf if that's not
2838
+ * the case.
2839
+ *
2840
+ * Finally we check that the rollback is "safe", i.e., has the HEAD
2841
+ * moved? In this case, it doesn't make sense to "reset the merge" and
2842
+ * "skip the commit" as the user already handled this by committing. But
2843
+ * we'd not want to barf here, instead give advice on how to proceed. We
2844
+ * only need to check that when .git/<ACTION>_HEAD doesn't exist because
2845
+ * it gets removed when the user commits, so if it still exists we're
2846
+ * sure the user can't have committed before.
2847
+ */
2848
+ switch (opts->action) {
2849
+ case REPLAY_REVERT:
2850
+ if (!file_exists(git_path_revert_head(r))) {
2851
+ if (action != REPLAY_REVERT)
2852
+ return error(_("no revert in progress"));
2853
+ if (!rollback_is_safe())
2854
+ goto give_advice;
2855
+ }
2856
+ break;
2857
+ case REPLAY_PICK:
2858
+ if (!file_exists(git_path_cherry_pick_head(r))) {
2859
+ if (action != REPLAY_PICK)
2860
+ return error(_("no cherry-pick in progress"));
2861
+ if (!rollback_is_safe())
2862
+ goto give_advice;
2863
+ }
2864
+ break;
2865
+ default:
2866
+ BUG("unexpected action in sequencer_skip");
2867
+ }
2868
+
2869
+ if (skip_single_pick())
2870
+ return error(_("failed to skip the commit"));
2871
+ if (!is_directory(git_path_seq_dir()))
2872
+ return 0;
2873
+
2874
+ return sequencer_continue(r, opts);
2875
+
2876
+give_advice:
2877
+ error(_("there is nothing to skip"));
2878
+
2879
+ if (advice_resolve_conflict) {
2880
+ advise(_("have you committed already?\n"
2881
+ "try \"git %s --continue\""),
2882
+ action == REPLAY_REVERT ? "revert" : "cherry-pick");
2883
+ }
2884
+ return -1;
2885
+}
2886
+
2887
static int save_todo(struct todo_list *todo_list, struct replay_opts *opts)
2888
{
2889
struct lock_file todo_lock = LOCK_INIT;
sequencer.h
+1
@@ -129,6 +129,7 @@ int sequencer_pick_revisions(struct repository *repo,
129
struct replay_opts *opts);
130
int sequencer_continue(struct repository *repo, struct replay_opts *opts);
131
int sequencer_rollback(struct repository *repo, struct replay_opts *opts);
132
+int sequencer_skip(struct repository *repo, struct replay_opts *opts);
133
int sequencer_remove_state(struct replay_opts *opts);
134
135
#define TODO_LIST_KEEP_EMPTY (1U << 0)
t/t3510-cherry-pick-sequence.sh
+102
@@ -93,6 +93,108 @@ test_expect_success 'cherry-pick cleans up sequencer state upon success' '
93
test_path_is_missing .git/sequencer
94
'
95
96
+test_expect_success 'cherry-pick --skip requires cherry-pick in progress' '
97
+ pristine_detach initial &&
98
+ test_must_fail git cherry-pick --skip
99
+'
100
+
101
+test_expect_success 'revert --skip requires revert in progress' '
102
+ pristine_detach initial &&
103
+ test_must_fail git revert --skip
104
+'
105
+
106
+test_expect_success 'cherry-pick --skip to skip commit' '
107
+ pristine_detach initial &&
108
+ test_must_fail git cherry-pick anotherpick &&
109
+ test_must_fail git revert --skip &&
110
+ git cherry-pick --skip &&
111
+ test_cmp_rev initial HEAD &&
112
+ test_path_is_missing .git/CHERRY_PICK_HEAD
113
+'
114
+
115
+test_expect_success 'revert --skip to skip commit' '
116
+ pristine_detach anotherpick &&
117
+ test_must_fail git revert anotherpick~1 &&
118
+ test_must_fail git cherry-pick --skip &&
119
+ git revert --skip &&
120
+ test_cmp_rev anotherpick HEAD
121
+'
122
+
123
+test_expect_success 'skip "empty" commit' '
124
+ pristine_detach picked &&
125
+ test_commit dummy foo d &&
126
+ test_must_fail git cherry-pick anotherpick &&
127
+ git cherry-pick --skip &&
128
+ test_cmp_rev dummy HEAD
129
+'
130
+
131
+test_expect_success 'skip a commit and check if rest of sequence is correct' '
132
+ pristine_detach initial &&
133
+ echo e >expect &&
134
+ cat >expect.log <<-EOF &&
135
+ OBJID
136
+ :100644 100644 OBJID OBJID M foo
137
+ OBJID
138
+ :100644 100644 OBJID OBJID M foo
139
+ OBJID
140
+ :100644 100644 OBJID OBJID M unrelated
141
+ OBJID
142
+ :000000 100644 OBJID OBJID A foo
143
+ :000000 100644 OBJID OBJID A unrelated
144
+ EOF
145
+ test_must_fail git cherry-pick base..yetanotherpick &&
146
+ test_must_fail git cherry-pick --skip &&
147
+ echo d >foo &&
148
+ git add foo &&
149
+ git cherry-pick --continue &&
150
+ {
151
+ git rev-list HEAD |
152
+ git diff-tree --root --stdin |
153
+ sed "s/$OID_REGEX/OBJID/g"
154
+ } >actual.log &&
155
+ test_cmp expect foo &&
156
+ test_cmp expect.log actual.log
157
+'
158
+
159
+test_expect_success 'check advice when we move HEAD by committing' '
160
+ pristine_detach initial &&
161
+ cat >expect <<-EOF &&
162
+ error: there is nothing to skip
163
+ hint: have you committed already?
164
+ hint: try "git cherry-pick --continue"
165
+ fatal: cherry-pick failed
166
+ EOF
167
+ test_must_fail git cherry-pick base..yetanotherpick &&
168
+ echo c >foo &&
169
+ git commit -a &&
170
+ test_path_is_missing .git/CHERRY_PICK_HEAD &&
171
+ test_must_fail git cherry-pick --skip 2>advice &&
172
+ test_i18ncmp expect advice
173
+'
174
+
175
+test_expect_success 'allow skipping commit but not abort for a new history' '
176
+ pristine_detach initial &&
177
+ cat >expect <<-EOF &&
178
+ error: cannot abort from a branch yet to be born
179
+ fatal: cherry-pick failed
180
+ EOF
181
+ git checkout --orphan new_disconnected &&
182
+ git reset --hard &&
183
+ test_must_fail git cherry-pick anotherpick &&
184
+ test_must_fail git cherry-pick --abort 2>advice &&
185
+ git cherry-pick --skip &&
186
+ test_i18ncmp expect advice
187
+'
188
+
189
+test_expect_success 'allow skipping stopped cherry-pick because of untracked file modifications' '
190
+ pristine_detach initial &&
191
+ git rm --cached unrelated &&
192
+ git commit -m "untrack unrelated" &&
193
+ test_must_fail git cherry-pick initial base &&
194
+ test_path_is_missing .git/CHERRY_PICK_HEAD &&
195
+ git cherry-pick --skip
196
+'
197
+
198
test_expect_success '--quit does not complain when no cherry-pick is in progress' '
199
pristine_detach initial &&
200
git cherry-pick --quit