mirror of
https://github.com/git/git.git
synced 2024-11-15 14:14:08 +01:00
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>
This commit is contained in:
parent
82433cdf4d
commit
539047c19e
@ -11,6 +11,7 @@ SYNOPSIS
|
|||||||
'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff] <commit>...
|
'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff] <commit>...
|
||||||
'git cherry-pick' --continue
|
'git cherry-pick' --continue
|
||||||
'git cherry-pick' --quit
|
'git cherry-pick' --quit
|
||||||
|
'git cherry-pick' --abort
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
-----------
|
-----------
|
||||||
|
@ -11,6 +11,7 @@ SYNOPSIS
|
|||||||
'git revert' [--edit | --no-edit] [-n] [-m parent-number] [-s] <commit>...
|
'git revert' [--edit | --no-edit] [-n] [-m parent-number] [-s] <commit>...
|
||||||
'git revert' --continue
|
'git revert' --continue
|
||||||
'git revert' --quit
|
'git revert' --quit
|
||||||
|
'git revert' --abort
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
-----------
|
-----------
|
||||||
|
@ -7,3 +7,6 @@
|
|||||||
Forget about the current operation in progress. Can be used
|
Forget about the current operation in progress. Can be used
|
||||||
to clear the sequencer state after a failed cherry-pick or
|
to clear the sequencer state after a failed cherry-pick or
|
||||||
revert.
|
revert.
|
||||||
|
|
||||||
|
--abort::
|
||||||
|
Cancel the operation and return to the pre-sequence state.
|
||||||
|
@ -40,7 +40,12 @@ static const char * const cherry_pick_usage[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum replay_action { REVERT, CHERRY_PICK };
|
enum replay_action { REVERT, CHERRY_PICK };
|
||||||
enum replay_subcommand { REPLAY_NONE, REPLAY_REMOVE_STATE, REPLAY_CONTINUE };
|
enum replay_subcommand {
|
||||||
|
REPLAY_NONE,
|
||||||
|
REPLAY_REMOVE_STATE,
|
||||||
|
REPLAY_CONTINUE,
|
||||||
|
REPLAY_ROLLBACK
|
||||||
|
};
|
||||||
|
|
||||||
struct replay_opts {
|
struct replay_opts {
|
||||||
enum replay_action action;
|
enum replay_action action;
|
||||||
@ -135,9 +140,11 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
|
|||||||
const char *me = action_name(opts);
|
const char *me = action_name(opts);
|
||||||
int remove_state = 0;
|
int remove_state = 0;
|
||||||
int contin = 0;
|
int contin = 0;
|
||||||
|
int rollback = 0;
|
||||||
struct option options[] = {
|
struct option options[] = {
|
||||||
OPT_BOOLEAN(0, "quit", &remove_state, "end revert or cherry-pick sequence"),
|
OPT_BOOLEAN(0, "quit", &remove_state, "end revert or cherry-pick sequence"),
|
||||||
OPT_BOOLEAN(0, "continue", &contin, "resume revert or cherry-pick sequence"),
|
OPT_BOOLEAN(0, "continue", &contin, "resume revert or cherry-pick sequence"),
|
||||||
|
OPT_BOOLEAN(0, "abort", &rollback, "cancel revert or cherry-pick sequence"),
|
||||||
OPT_BOOLEAN('n', "no-commit", &opts->no_commit, "don't automatically commit"),
|
OPT_BOOLEAN('n', "no-commit", &opts->no_commit, "don't automatically commit"),
|
||||||
OPT_BOOLEAN('e', "edit", &opts->edit, "edit the commit message"),
|
OPT_BOOLEAN('e', "edit", &opts->edit, "edit the commit message"),
|
||||||
OPT_NOOP_NOARG('r', NULL),
|
OPT_NOOP_NOARG('r', NULL),
|
||||||
@ -173,6 +180,7 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
|
|||||||
verify_opt_mutually_compatible(me,
|
verify_opt_mutually_compatible(me,
|
||||||
"--quit", remove_state,
|
"--quit", remove_state,
|
||||||
"--continue", contin,
|
"--continue", contin,
|
||||||
|
"--abort", rollback,
|
||||||
NULL);
|
NULL);
|
||||||
|
|
||||||
/* Set the subcommand */
|
/* Set the subcommand */
|
||||||
@ -180,6 +188,8 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
|
|||||||
opts->subcommand = REPLAY_REMOVE_STATE;
|
opts->subcommand = REPLAY_REMOVE_STATE;
|
||||||
else if (contin)
|
else if (contin)
|
||||||
opts->subcommand = REPLAY_CONTINUE;
|
opts->subcommand = REPLAY_CONTINUE;
|
||||||
|
else if (rollback)
|
||||||
|
opts->subcommand = REPLAY_ROLLBACK;
|
||||||
else
|
else
|
||||||
opts->subcommand = REPLAY_NONE;
|
opts->subcommand = REPLAY_NONE;
|
||||||
|
|
||||||
@ -188,8 +198,12 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
|
|||||||
char *this_operation;
|
char *this_operation;
|
||||||
if (opts->subcommand == REPLAY_REMOVE_STATE)
|
if (opts->subcommand == REPLAY_REMOVE_STATE)
|
||||||
this_operation = "--quit";
|
this_operation = "--quit";
|
||||||
else
|
else if (opts->subcommand == REPLAY_CONTINUE)
|
||||||
this_operation = "--continue";
|
this_operation = "--continue";
|
||||||
|
else {
|
||||||
|
assert(opts->subcommand == REPLAY_ROLLBACK);
|
||||||
|
this_operation = "--abort";
|
||||||
|
}
|
||||||
|
|
||||||
verify_opt_compatible(me, this_operation,
|
verify_opt_compatible(me, this_operation,
|
||||||
"--no-commit", opts->no_commit,
|
"--no-commit", opts->no_commit,
|
||||||
@ -850,7 +864,7 @@ static int create_seq_dir(void)
|
|||||||
|
|
||||||
if (file_exists(seq_dir)) {
|
if (file_exists(seq_dir)) {
|
||||||
error(_("a cherry-pick or revert is already in progress"));
|
error(_("a cherry-pick or revert is already in progress"));
|
||||||
advise(_("try \"git cherry-pick (--continue | --quit)\""));
|
advise(_("try \"git cherry-pick (--continue | --quit | --abort)\""));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
else if (mkdir(seq_dir, 0777) < 0)
|
else if (mkdir(seq_dir, 0777) < 0)
|
||||||
@ -873,6 +887,71 @@ static void save_head(const char *head)
|
|||||||
die(_("Error wrapping up %s."), head_file);
|
die(_("Error wrapping up %s."), head_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int reset_for_rollback(const unsigned char *sha1)
|
||||||
|
{
|
||||||
|
const char *argv[4]; /* reset --merge <arg> + NULL */
|
||||||
|
argv[0] = "reset";
|
||||||
|
argv[1] = "--merge";
|
||||||
|
argv[2] = sha1_to_hex(sha1);
|
||||||
|
argv[3] = NULL;
|
||||||
|
return run_command_v_opt(argv, RUN_GIT_CMD);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int rollback_single_pick(void)
|
||||||
|
{
|
||||||
|
unsigned char head_sha1[20];
|
||||||
|
|
||||||
|
if (!file_exists(git_path("CHERRY_PICK_HEAD")) &&
|
||||||
|
!file_exists(git_path("REVERT_HEAD")))
|
||||||
|
return error(_("no cherry-pick or revert in progress"));
|
||||||
|
if (!resolve_ref("HEAD", head_sha1, 0, NULL))
|
||||||
|
return error(_("cannot resolve HEAD"));
|
||||||
|
if (is_null_sha1(head_sha1))
|
||||||
|
return error(_("cannot abort from a branch yet to be born"));
|
||||||
|
return reset_for_rollback(head_sha1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sequencer_rollback(struct replay_opts *opts)
|
||||||
|
{
|
||||||
|
const char *filename;
|
||||||
|
FILE *f;
|
||||||
|
unsigned char sha1[20];
|
||||||
|
struct strbuf buf = STRBUF_INIT;
|
||||||
|
|
||||||
|
filename = git_path(SEQ_HEAD_FILE);
|
||||||
|
f = fopen(filename, "r");
|
||||||
|
if (!f && errno == ENOENT) {
|
||||||
|
/*
|
||||||
|
* There is no multiple-cherry-pick in progress.
|
||||||
|
* If CHERRY_PICK_HEAD or REVERT_HEAD indicates
|
||||||
|
* a single-cherry-pick in progress, abort that.
|
||||||
|
*/
|
||||||
|
return rollback_single_pick();
|
||||||
|
}
|
||||||
|
if (!f)
|
||||||
|
return error(_("cannot open %s: %s"), filename,
|
||||||
|
strerror(errno));
|
||||||
|
if (strbuf_getline(&buf, f, '\n')) {
|
||||||
|
error(_("cannot read %s: %s"), filename, ferror(f) ?
|
||||||
|
strerror(errno) : _("unexpected end of file"));
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
if (get_sha1_hex(buf.buf, sha1) || buf.buf[40] != '\0') {
|
||||||
|
error(_("stored pre-cherry-pick HEAD file '%s' is corrupt"),
|
||||||
|
filename);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
if (reset_for_rollback(sha1))
|
||||||
|
goto fail;
|
||||||
|
strbuf_release(&buf);
|
||||||
|
fclose(f);
|
||||||
|
return 0;
|
||||||
|
fail:
|
||||||
|
strbuf_release(&buf);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
static void save_todo(struct commit_list *todo_list, struct replay_opts *opts)
|
static void save_todo(struct commit_list *todo_list, struct replay_opts *opts)
|
||||||
{
|
{
|
||||||
const char *todo_file = git_path(SEQ_TODO_FILE);
|
const char *todo_file = git_path(SEQ_TODO_FILE);
|
||||||
@ -977,6 +1056,8 @@ static int pick_revisions(struct replay_opts *opts)
|
|||||||
remove_sequencer_state(1);
|
remove_sequencer_state(1);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (opts->subcommand == REPLAY_ROLLBACK)
|
||||||
|
return sequencer_rollback(opts);
|
||||||
if (opts->subcommand == REPLAY_CONTINUE) {
|
if (opts->subcommand == REPLAY_CONTINUE) {
|
||||||
if (!file_exists(git_path(SEQ_TODO_FILE)))
|
if (!file_exists(git_path(SEQ_TODO_FILE)))
|
||||||
return error(_("No %s in progress"), action_name(opts));
|
return error(_("No %s in progress"), action_name(opts));
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
test_description='Test cherry-pick continuation features
|
test_description='Test cherry-pick continuation features
|
||||||
|
|
||||||
|
+ yetanotherpick: rewrites foo to e
|
||||||
+ anotherpick: rewrites foo to d
|
+ anotherpick: rewrites foo to d
|
||||||
+ picked: rewrites foo to c
|
+ picked: rewrites foo to c
|
||||||
+ unrelatedpick: rewrites unrelated to reallyunrelated
|
+ unrelatedpick: rewrites unrelated to reallyunrelated
|
||||||
@ -19,6 +20,12 @@ pristine_detach () {
|
|||||||
git clean -d -f -f -q -x
|
git clean -d -f -f -q -x
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test_cmp_rev () {
|
||||||
|
git rev-parse --verify "$1" >expect.rev &&
|
||||||
|
git rev-parse --verify "$2" >actual.rev &&
|
||||||
|
test_cmp expect.rev actual.rev
|
||||||
|
}
|
||||||
|
|
||||||
test_expect_success setup '
|
test_expect_success setup '
|
||||||
echo unrelated >unrelated &&
|
echo unrelated >unrelated &&
|
||||||
git add unrelated &&
|
git add unrelated &&
|
||||||
@ -27,6 +34,7 @@ test_expect_success setup '
|
|||||||
test_commit unrelatedpick unrelated reallyunrelated &&
|
test_commit unrelatedpick unrelated reallyunrelated &&
|
||||||
test_commit picked foo c &&
|
test_commit picked foo c &&
|
||||||
test_commit anotherpick foo d &&
|
test_commit anotherpick foo d &&
|
||||||
|
test_commit yetanotherpick foo e &&
|
||||||
git config advice.detachedhead false
|
git config advice.detachedhead false
|
||||||
|
|
||||||
'
|
'
|
||||||
@ -75,6 +83,11 @@ test_expect_success '--quit does not complain when no cherry-pick is in progress
|
|||||||
git cherry-pick --quit
|
git cherry-pick --quit
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success '--abort requires cherry-pick in progress' '
|
||||||
|
pristine_detach initial &&
|
||||||
|
test_must_fail git cherry-pick --abort
|
||||||
|
'
|
||||||
|
|
||||||
test_expect_success '--quit cleans up sequencer state' '
|
test_expect_success '--quit cleans up sequencer state' '
|
||||||
pristine_detach initial &&
|
pristine_detach initial &&
|
||||||
test_must_fail git cherry-pick base..picked &&
|
test_must_fail git cherry-pick base..picked &&
|
||||||
@ -103,6 +116,79 @@ test_expect_success 'cherry-pick --reset (another name for --quit)' '
|
|||||||
test_cmp expect actual
|
test_cmp expect actual
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success '--abort to cancel multiple cherry-pick' '
|
||||||
|
pristine_detach initial &&
|
||||||
|
test_must_fail git cherry-pick base..anotherpick &&
|
||||||
|
git cherry-pick --abort &&
|
||||||
|
test_path_is_missing .git/sequencer &&
|
||||||
|
test_cmp_rev initial HEAD &&
|
||||||
|
git update-index --refresh &&
|
||||||
|
git diff-index --exit-code HEAD
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success '--abort to cancel single cherry-pick' '
|
||||||
|
pristine_detach initial &&
|
||||||
|
test_must_fail git cherry-pick picked &&
|
||||||
|
git cherry-pick --abort &&
|
||||||
|
test_path_is_missing .git/sequencer &&
|
||||||
|
test_cmp_rev initial HEAD &&
|
||||||
|
git update-index --refresh &&
|
||||||
|
git diff-index --exit-code HEAD
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'cherry-pick --abort to cancel multiple revert' '
|
||||||
|
pristine_detach anotherpick &&
|
||||||
|
test_must_fail git revert base..picked &&
|
||||||
|
git cherry-pick --abort &&
|
||||||
|
test_path_is_missing .git/sequencer &&
|
||||||
|
test_cmp_rev anotherpick HEAD &&
|
||||||
|
git update-index --refresh &&
|
||||||
|
git diff-index --exit-code HEAD
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'revert --abort works, too' '
|
||||||
|
pristine_detach anotherpick &&
|
||||||
|
test_must_fail git revert base..picked &&
|
||||||
|
git revert --abort &&
|
||||||
|
test_path_is_missing .git/sequencer &&
|
||||||
|
test_cmp_rev anotherpick HEAD
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success '--abort to cancel single revert' '
|
||||||
|
pristine_detach anotherpick &&
|
||||||
|
test_must_fail git revert picked &&
|
||||||
|
git revert --abort &&
|
||||||
|
test_path_is_missing .git/sequencer &&
|
||||||
|
test_cmp_rev anotherpick HEAD &&
|
||||||
|
git update-index --refresh &&
|
||||||
|
git diff-index --exit-code HEAD
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success '--abort keeps unrelated change, easy case' '
|
||||||
|
pristine_detach unrelatedpick &&
|
||||||
|
echo changed >expect &&
|
||||||
|
test_must_fail git cherry-pick picked..yetanotherpick &&
|
||||||
|
echo changed >unrelated &&
|
||||||
|
git cherry-pick --abort &&
|
||||||
|
test_cmp expect unrelated
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success '--abort refuses to clobber unrelated change, harder case' '
|
||||||
|
pristine_detach initial &&
|
||||||
|
echo changed >expect &&
|
||||||
|
test_must_fail git cherry-pick base..anotherpick &&
|
||||||
|
echo changed >unrelated &&
|
||||||
|
test_must_fail git cherry-pick --abort &&
|
||||||
|
test_cmp expect unrelated &&
|
||||||
|
git rev-list HEAD >log &&
|
||||||
|
test_line_count = 2 log &&
|
||||||
|
test_must_fail git update-index --refresh &&
|
||||||
|
|
||||||
|
git checkout unrelated &&
|
||||||
|
git cherry-pick --abort &&
|
||||||
|
test_cmp_rev initial HEAD
|
||||||
|
'
|
||||||
|
|
||||||
test_expect_success 'cherry-pick cleans up sequencer state when one commit is left' '
|
test_expect_success 'cherry-pick cleans up sequencer state when one commit is left' '
|
||||||
pristine_detach initial &&
|
pristine_detach initial &&
|
||||||
test_must_fail git cherry-pick base..picked &&
|
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
|
|||||||
test_cmp expect actual
|
test_cmp expect actual
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_failure '--abort after last commit in sequence' '
|
||||||
|
pristine_detach initial &&
|
||||||
|
test_must_fail git cherry-pick base..picked &&
|
||||||
|
git cherry-pick --abort &&
|
||||||
|
test_path_is_missing .git/sequencer &&
|
||||||
|
test_cmp_rev initial HEAD &&
|
||||||
|
git update-index --refresh &&
|
||||||
|
git diff-index --exit-code HEAD
|
||||||
|
'
|
||||||
|
|
||||||
test_expect_success 'cherry-pick does not implicitly stomp an existing operation' '
|
test_expect_success 'cherry-pick does not implicitly stomp an existing operation' '
|
||||||
pristine_detach initial &&
|
pristine_detach initial &&
|
||||||
test_must_fail git cherry-pick base..anotherpick &&
|
test_must_fail git cherry-pick base..anotherpick &&
|
||||||
|
Loading…
Reference in New Issue
Block a user