Skip to content

Commit 5a5d80f

Browse files
artagnongitster
authored andcommitted
revert: Introduce --continue to continue the operation
Introduce a new "git cherry-pick --continue" command which uses the information in ".git/sequencer" to continue a cherry-pick that stopped because of a conflict or other error. It works by dropping the first instruction from .git/sequencer/todo and performing the remaining cherry-picks listed there, with options (think "-s" and "-X") from the initial command listed in ".git/sequencer/opts". So now you can do: $ git cherry-pick -Xpatience foo..bar ... description conflict in commit moo ... $ git cherry-pick --continue error: 'cherry-pick' is not possible because you have unmerged files. fatal: failed to resume cherry-pick $ echo resolved >conflictingfile $ git add conflictingfile && git commit $ git cherry-pick --continue; # resumes with the commit after "moo" During the "git commit" stage, CHERRY_PICK_HEAD will aid by providing the commit message from the conflicting "moo" commit. Note that the cherry-pick mechanism has no control at this stage, so the user is free to violate anything that was specified during the first cherry-pick invocation. For example, if "-x" was specified during the first cherry-pick invocation, the user is free to edit out the message during commit time. Note that the "--signoff" option specified at cherry-pick invocation time is not reflected in the commit message provided by CHERRY_PICK_HEAD; the user must take care to add "--signoff" during the "git commit" invocation. Helped-by: Christian Couder <[email protected]> Signed-off-by: Ramkumar Ramachandra <[email protected]> Signed-off-by: Jonathan Nieder <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 21afd08 commit 5a5d80f

File tree

5 files changed

+287
-4
lines changed

5 files changed

+287
-4
lines changed

Documentation/git-cherry-pick.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ SYNOPSIS
99
--------
1010
'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff] <commit>...
1111
'git cherry-pick' --reset
12+
'git cherry-pick' --continue
1213

1314
DESCRIPTION
1415
-----------

Documentation/git-revert.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ SYNOPSIS
99
--------
1010
'git revert' [--edit | --no-edit] [-n] [-m parent-number] [-s] <commit>...
1111
'git revert' --reset
12+
'git revert' --continue
1213

1314
DESCRIPTION
1415
-----------

Documentation/sequencer.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
Forget about the current operation in progress. Can be used
33
to clear the sequencer state after a failed cherry-pick or
44
revert.
5+
6+
--continue::
7+
Continue the operation in progress using the information in
8+
'.git/sequencer'. Can be used to continue after resolving
9+
conflicts in a failed cherry-pick or revert.

builtin/revert.c

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

4242
enum replay_action { REVERT, CHERRY_PICK };
43-
enum replay_subcommand { REPLAY_NONE, REPLAY_RESET };
43+
enum replay_subcommand { REPLAY_NONE, REPLAY_RESET, REPLAY_CONTINUE };
4444

4545
struct replay_opts {
4646
enum replay_action action;
@@ -117,14 +117,37 @@ static void verify_opt_compatible(const char *me, const char *base_opt, ...)
117117
die(_("%s: %s cannot be used with %s"), me, this_opt, base_opt);
118118
}
119119

120+
static void verify_opt_mutually_compatible(const char *me, ...)
121+
{
122+
const char *opt1, *opt2;
123+
va_list ap;
124+
125+
va_start(ap, me);
126+
while ((opt1 = va_arg(ap, const char *))) {
127+
if (va_arg(ap, int))
128+
break;
129+
}
130+
if (opt1) {
131+
while ((opt2 = va_arg(ap, const char *))) {
132+
if (va_arg(ap, int))
133+
break;
134+
}
135+
}
136+
137+
if (opt1 && opt2)
138+
die(_("%s: %s cannot be used with %s"), me, opt1, opt2);
139+
}
140+
120141
static void parse_args(int argc, const char **argv, struct replay_opts *opts)
121142
{
122143
const char * const * usage_str = revert_or_cherry_pick_usage(opts);
123144
const char *me = action_name(opts);
124145
int noop;
125146
int reset = 0;
147+
int contin = 0;
126148
struct option options[] = {
127149
OPT_BOOLEAN(0, "reset", &reset, "forget the current operation"),
150+
OPT_BOOLEAN(0, "continue", &contin, "continue the current operation"),
128151
OPT_BOOLEAN('n', "no-commit", &opts->no_commit, "don't automatically commit"),
129152
OPT_BOOLEAN('e', "edit", &opts->edit, "edit the commit message"),
130153
{ OPTION_BOOLEAN, 'r', NULL, &noop, NULL, "no-op (backward compatibility)",
@@ -154,15 +177,29 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
154177
PARSE_OPT_KEEP_ARGV0 |
155178
PARSE_OPT_KEEP_UNKNOWN);
156179

180+
/* Check for incompatible subcommands */
181+
verify_opt_mutually_compatible(me,
182+
"--reset", reset,
183+
"--continue", contin,
184+
NULL);
185+
157186
/* Set the subcommand */
158187
if (reset)
159188
opts->subcommand = REPLAY_RESET;
189+
else if (contin)
190+
opts->subcommand = REPLAY_CONTINUE;
160191
else
161192
opts->subcommand = REPLAY_NONE;
162193

163194
/* Check for incompatible command line arguments */
164-
if (opts->subcommand == REPLAY_RESET) {
165-
verify_opt_compatible(me, "--reset",
195+
if (opts->subcommand != REPLAY_NONE) {
196+
char *this_operation;
197+
if (opts->subcommand == REPLAY_RESET)
198+
this_operation = "--reset";
199+
else
200+
this_operation = "--continue";
201+
202+
verify_opt_compatible(me, this_operation,
166203
"--no-commit", opts->no_commit,
167204
"--signoff", opts->signoff,
168205
"--mainline", opts->mainline,
@@ -668,6 +705,137 @@ static int format_todo(struct strbuf *buf, struct commit_list *todo_list,
668705
return 0;
669706
}
670707

708+
static struct commit *parse_insn_line(char *start, struct replay_opts *opts)
709+
{
710+
unsigned char commit_sha1[20];
711+
char sha1_abbrev[40];
712+
enum replay_action action;
713+
int insn_len = 0;
714+
char *p, *q;
715+
716+
if (!prefixcmp(start, "pick ")) {
717+
action = CHERRY_PICK;
718+
insn_len = strlen("pick");
719+
p = start + insn_len + 1;
720+
} else if (!prefixcmp(start, "revert ")) {
721+
action = REVERT;
722+
insn_len = strlen("revert");
723+
p = start + insn_len + 1;
724+
} else
725+
return NULL;
726+
727+
q = strchr(p, ' ');
728+
if (!q)
729+
return NULL;
730+
q++;
731+
732+
strlcpy(sha1_abbrev, p, q - p);
733+
734+
/*
735+
* Verify that the action matches up with the one in
736+
* opts; we don't support arbitrary instructions
737+
*/
738+
if (action != opts->action) {
739+
const char *action_str;
740+
action_str = action == REVERT ? "revert" : "cherry-pick";
741+
error(_("Cannot %s during a %s"), action_str, action_name(opts));
742+
return NULL;
743+
}
744+
745+
if (get_sha1(sha1_abbrev, commit_sha1) < 0)
746+
return NULL;
747+
748+
return lookup_commit_reference(commit_sha1);
749+
}
750+
751+
static int parse_insn_buffer(char *buf, struct commit_list **todo_list,
752+
struct replay_opts *opts)
753+
{
754+
struct commit_list **next = todo_list;
755+
struct commit *commit;
756+
char *p = buf;
757+
int i;
758+
759+
for (i = 1; *p; i++) {
760+
commit = parse_insn_line(p, opts);
761+
if (!commit)
762+
return error(_("Could not parse line %d."), i);
763+
next = commit_list_append(commit, next);
764+
p = strchrnul(p, '\n');
765+
if (*p)
766+
p++;
767+
}
768+
if (!*todo_list)
769+
return error(_("No commits parsed."));
770+
return 0;
771+
}
772+
773+
static void read_populate_todo(struct commit_list **todo_list,
774+
struct replay_opts *opts)
775+
{
776+
const char *todo_file = git_path(SEQ_TODO_FILE);
777+
struct strbuf buf = STRBUF_INIT;
778+
int fd, res;
779+
780+
fd = open(todo_file, O_RDONLY);
781+
if (fd < 0)
782+
die_errno(_("Could not open %s."), todo_file);
783+
if (strbuf_read(&buf, fd, 0) < 0) {
784+
close(fd);
785+
strbuf_release(&buf);
786+
die(_("Could not read %s."), todo_file);
787+
}
788+
close(fd);
789+
790+
res = parse_insn_buffer(buf.buf, todo_list, opts);
791+
strbuf_release(&buf);
792+
if (res)
793+
die(_("Unusable instruction sheet: %s"), todo_file);
794+
}
795+
796+
static int populate_opts_cb(const char *key, const char *value, void *data)
797+
{
798+
struct replay_opts *opts = data;
799+
int error_flag = 1;
800+
801+
if (!value)
802+
error_flag = 0;
803+
else if (!strcmp(key, "options.no-commit"))
804+
opts->no_commit = git_config_bool_or_int(key, value, &error_flag);
805+
else if (!strcmp(key, "options.edit"))
806+
opts->edit = git_config_bool_or_int(key, value, &error_flag);
807+
else if (!strcmp(key, "options.signoff"))
808+
opts->signoff = git_config_bool_or_int(key, value, &error_flag);
809+
else if (!strcmp(key, "options.record-origin"))
810+
opts->record_origin = git_config_bool_or_int(key, value, &error_flag);
811+
else if (!strcmp(key, "options.allow-ff"))
812+
opts->allow_ff = git_config_bool_or_int(key, value, &error_flag);
813+
else if (!strcmp(key, "options.mainline"))
814+
opts->mainline = git_config_int(key, value);
815+
else if (!strcmp(key, "options.strategy"))
816+
git_config_string(&opts->strategy, key, value);
817+
else if (!strcmp(key, "options.strategy-option")) {
818+
ALLOC_GROW(opts->xopts, opts->xopts_nr + 1, opts->xopts_alloc);
819+
opts->xopts[opts->xopts_nr++] = xstrdup(value);
820+
} else
821+
return error(_("Invalid key: %s"), key);
822+
823+
if (!error_flag)
824+
return error(_("Invalid value for %s: %s"), key, value);
825+
826+
return 0;
827+
}
828+
829+
static void read_populate_opts(struct replay_opts **opts_ptr)
830+
{
831+
const char *opts_file = git_path(SEQ_OPTS_FILE);
832+
833+
if (!file_exists(opts_file))
834+
return;
835+
if (git_config_from_file(populate_opts_cb, opts_file, *opts_ptr) < 0)
836+
die(_("Malformed options sheet: %s"), opts_file);
837+
}
838+
671839
static void walk_revs_populate_todo(struct commit_list **todo_list,
672840
struct replay_opts *opts)
673841
{
@@ -811,6 +979,15 @@ static int pick_revisions(struct replay_opts *opts)
811979
if (opts->subcommand == REPLAY_RESET) {
812980
remove_sequencer_state(1);
813981
return 0;
982+
} else if (opts->subcommand == REPLAY_CONTINUE) {
983+
if (!file_exists(git_path(SEQ_TODO_FILE)))
984+
goto error;
985+
read_populate_opts(&opts);
986+
read_populate_todo(&todo_list, opts);
987+
988+
/* Verify that the conflict has been resolved */
989+
if (!index_differs_from("HEAD", 0))
990+
todo_list = todo_list->next;
814991
} else {
815992
/*
816993
* Start a new cherry-pick/ revert sequence; but
@@ -821,7 +998,8 @@ static int pick_revisions(struct replay_opts *opts)
821998
walk_revs_populate_todo(&todo_list, opts);
822999
if (create_seq_dir() < 0) {
8231000
fatal(_("A cherry-pick or revert is in progress."));
824-
advise(_("Use --reset to forget about it"));
1001+
advise(_("Use --continue to continue the operation"));
1002+
advise(_("or --reset to forget about it"));
8251003
exit(128);
8261004
}
8271005
if (get_sha1("HEAD", sha1)) {
@@ -833,6 +1011,8 @@ static int pick_revisions(struct replay_opts *opts)
8331011
save_opts(opts);
8341012
}
8351013
return pick_commits(todo_list, opts);
1014+
error:
1015+
die(_("No %s in progress"), action_name(opts));
8361016
}
8371017

8381018
int cmd_revert(int argc, const char **argv, const char *prefix)

t/t3510-cherry-pick-sequence.sh

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,100 @@ test_expect_success 'cherry-pick does not implicitly stomp an existing operation
115115
test_cmp expect actual
116116
'
117117

118+
test_expect_success '--continue complains when no cherry-pick is in progress' '
119+
pristine_detach initial &&
120+
test_must_fail git cherry-pick --continue
121+
'
122+
123+
test_expect_success '--continue complains when there are unresolved conflicts' '
124+
pristine_detach initial &&
125+
test_must_fail git cherry-pick base..anotherpick &&
126+
test_must_fail git cherry-pick --continue
127+
'
128+
129+
test_expect_success '--continue continues after conflicts are resolved' '
130+
pristine_detach initial &&
131+
test_must_fail git cherry-pick base..anotherpick &&
132+
echo "c" >foo &&
133+
git add foo &&
134+
git commit &&
135+
git cherry-pick --continue &&
136+
test_path_is_missing .git/sequencer &&
137+
{
138+
git rev-list HEAD |
139+
git diff-tree --root --stdin |
140+
sed "s/$_x40/OBJID/g"
141+
} >actual &&
142+
cat >expect <<-\EOF &&
143+
OBJID
144+
:100644 100644 OBJID OBJID M foo
145+
OBJID
146+
:100644 100644 OBJID OBJID M foo
147+
OBJID
148+
:100644 100644 OBJID OBJID M unrelated
149+
OBJID
150+
:000000 100644 OBJID OBJID A foo
151+
:000000 100644 OBJID OBJID A unrelated
152+
EOF
153+
test_cmp expect actual
154+
'
155+
156+
test_expect_success '--continue respects opts' '
157+
pristine_detach initial &&
158+
test_must_fail git cherry-pick -x base..anotherpick &&
159+
echo "c" >foo &&
160+
git add foo &&
161+
git commit &&
162+
git cherry-pick --continue &&
163+
test_path_is_missing .git/sequencer &&
164+
git cat-file commit HEAD >anotherpick_msg &&
165+
git cat-file commit HEAD~1 >picked_msg &&
166+
git cat-file commit HEAD~2 >unrelatedpick_msg &&
167+
git cat-file commit HEAD~3 >initial_msg &&
168+
test_must_fail grep "cherry picked from" initial_msg &&
169+
grep "cherry picked from" unrelatedpick_msg &&
170+
grep "cherry picked from" picked_msg &&
171+
grep "cherry picked from" anotherpick_msg
172+
'
173+
174+
test_expect_success '--signoff is not automatically propagated to resolved conflict' '
175+
pristine_detach initial &&
176+
test_must_fail git cherry-pick --signoff base..anotherpick &&
177+
echo "c" >foo &&
178+
git add foo &&
179+
git commit &&
180+
git cherry-pick --continue &&
181+
test_path_is_missing .git/sequencer &&
182+
git cat-file commit HEAD >anotherpick_msg &&
183+
git cat-file commit HEAD~1 >picked_msg &&
184+
git cat-file commit HEAD~2 >unrelatedpick_msg &&
185+
git cat-file commit HEAD~3 >initial_msg &&
186+
test_must_fail grep "Signed-off-by:" initial_msg &&
187+
grep "Signed-off-by:" unrelatedpick_msg &&
188+
test_must_fail grep "Signed-off-by:" picked_msg &&
189+
grep "Signed-off-by:" anotherpick_msg
190+
'
191+
192+
test_expect_success 'malformed instruction sheet 1' '
193+
pristine_detach initial &&
194+
test_must_fail git cherry-pick base..anotherpick &&
195+
echo "resolved" >foo &&
196+
git add foo &&
197+
git commit &&
198+
sed "s/pick /pick/" .git/sequencer/todo >new_sheet &&
199+
cp new_sheet .git/sequencer/todo &&
200+
test_must_fail git cherry-pick --continue
201+
'
202+
203+
test_expect_success 'malformed instruction sheet 2' '
204+
pristine_detach initial &&
205+
test_must_fail git cherry-pick base..anotherpick &&
206+
echo "resolved" >foo &&
207+
git add foo &&
208+
git commit &&
209+
sed "s/pick/revert/" .git/sequencer/todo >new_sheet &&
210+
cp new_sheet .git/sequencer/todo &&
211+
test_must_fail git cherry-pick --continue
212+
'
213+
118214
test_done

0 commit comments

Comments
 (0)