From 6a1f9046a40ad69eb00e5a83c6825972113f6e0f Mon Sep 17 00:00:00 2001 From: Rohit Ashiwal Date: Tue, 2 Jul 2019 14:41:25 +0530 Subject: sequencer: add advice for revert In the case of merge conflicts, while performing a revert, we are currently advised to use `git cherry-pick --`. Introduce a separate advice message for `git revert`. Also change the signature of `create_seq_dir` to handle which advice to display selectively. Signed-off-by: Rohit Ashiwal Signed-off-by: Junio C Hamano diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt index ec4f6ae..1cd9096 100644 --- a/Documentation/config/advice.txt +++ b/Documentation/config/advice.txt @@ -57,6 +57,8 @@ advice.*:: resolveConflict:: Advice shown by various commands when conflicts prevent the operation from being performed. + sequencerInUse:: + Advice shown when a sequencer command is already in progress. implicitIdentity:: Advice on how to set your identity configuration when your information is guessed from the system username and diff --git a/advice.c b/advice.c index ce5f374..b101f0c 100644 --- a/advice.c +++ b/advice.c @@ -15,6 +15,7 @@ int advice_status_u_option = 1; int advice_commit_before_merge = 1; int advice_reset_quiet_warning = 1; int advice_resolve_conflict = 1; +int advice_sequencer_in_use = 1; int advice_implicit_identity = 1; int advice_detached_head = 1; int advice_set_upstream_failure = 1; @@ -71,6 +72,7 @@ static struct { { "commitBeforeMerge", &advice_commit_before_merge }, { "resetQuiet", &advice_reset_quiet_warning }, { "resolveConflict", &advice_resolve_conflict }, + { "sequencerInUse", &advice_sequencer_in_use }, { "implicitIdentity", &advice_implicit_identity }, { "detachedHead", &advice_detached_head }, { "setupStreamFailure", &advice_set_upstream_failure }, diff --git a/advice.h b/advice.h index e50f02c..ebc838d 100644 --- a/advice.h +++ b/advice.h @@ -15,6 +15,7 @@ extern int advice_status_u_option; extern int advice_commit_before_merge; extern int advice_reset_quiet_warning; extern int advice_resolve_conflict; +extern int advice_sequencer_in_use; extern int advice_implicit_identity; extern int advice_detached_head; extern int advice_set_upstream_failure; diff --git a/sequencer.c b/sequencer.c index ab74b6b..acf4aa7 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2650,15 +2650,38 @@ static int walk_revs_populate_todo(struct todo_list *todo_list, return 0; } -static int create_seq_dir(void) +static int create_seq_dir(struct repository *r) { - if (file_exists(git_path_seq_dir())) { - error(_("a cherry-pick or revert is already in progress")); - advise(_("try \"git cherry-pick (--continue | --quit | --abort)\"")); + enum replay_action action; + const char *in_progress_error = NULL; + const char *in_progress_advice = NULL; + + if (!sequencer_get_last_command(r, &action)) { + switch (action) { + case REPLAY_REVERT: + in_progress_error = _("revert is already in progress"); + in_progress_advice = + _("try \"git revert (--continue | --abort | --quit)\""); + break; + case REPLAY_PICK: + in_progress_error = _("cherry-pick is already in progress"); + in_progress_advice = + _("try \"git cherry-pick (--continue | --abort | --quit)\""); + break; + default: + BUG("unexpected action in create_seq_dir"); + } + } + if (in_progress_error) { + error("%s", in_progress_error); + if (advice_sequencer_in_use) + advise("%s", in_progress_advice); return -1; - } else if (mkdir(git_path_seq_dir(), 0777) < 0) + } + if (mkdir(git_path_seq_dir(), 0777) < 0) return error_errno(_("could not create sequencer directory '%s'"), git_path_seq_dir()); + return 0; } @@ -4242,7 +4265,7 @@ int sequencer_pick_revisions(struct repository *r, */ if (walk_revs_populate_todo(&todo_list, opts) || - create_seq_dir() < 0) + create_seq_dir(r) < 0) return -1; if (get_oid("HEAD", &oid) && (opts->action == REPLAY_REVERT)) return error(_("can't revert as initial commit")); -- cgit v0.10.2-6-g49f6 From 918d1e6ed89738afad63a5cfc92668ac77a7be95 Mon Sep 17 00:00:00 2001 From: Rohit Ashiwal Date: Tue, 2 Jul 2019 14:41:26 +0530 Subject: sequencer: rename reset_for_rollback to reset_merge We are on a path to teach cherry-pick/revert how to skip commits. To achieve this, we could really make use of existing functions. reset_for_rollback is one such function, but the name does not intuitively suggest to use it to reset a merge, which it was born to perform, see 539047c ("revert: introduce --abort to cancel a failed cherry-pick", 2011-11-23). Change the name to reset_merge to make it more intuitive. Signed-off-by: Rohit Ashiwal Signed-off-by: Junio C Hamano diff --git a/sequencer.c b/sequencer.c index acf4aa7..e8779c7 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2732,7 +2732,7 @@ static int rollback_is_safe(void) return oideq(&actual_head, &expected_head); } -static int reset_for_rollback(const struct object_id *oid) +static int reset_merge(const struct object_id *oid) { const char *argv[4]; /* reset --merge + NULL */ @@ -2754,7 +2754,7 @@ static int rollback_single_pick(struct repository *r) return error(_("cannot resolve HEAD")); if (is_null_oid(&head_oid)) return error(_("cannot abort from a branch yet to be born")); - return reset_for_rollback(&head_oid); + return reset_merge(&head_oid); } int sequencer_rollback(struct repository *r, struct replay_opts *opts) @@ -2797,7 +2797,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts) warning(_("You seem to have moved HEAD. " "Not rewinding, check your HEAD!")); } else - if (reset_for_rollback(&oid)) + if (reset_merge(&oid)) goto fail; strbuf_release(&buf); return sequencer_remove_state(opts); -- cgit v0.10.2-6-g49f6 From 265ab48f2646605b27e1343ac50efa9763690f04 Mon Sep 17 00:00:00 2001 From: Rohit Ashiwal Date: Tue, 2 Jul 2019 14:41:27 +0530 Subject: sequencer: use argv_array in reset_merge Avoid using magic numbers for array size and index under `reset_merge` function. Use `argv_array` instead. This will make code shorter and easier to extend. Signed-off-by: Rohit Ashiwal Signed-off-by: Junio C Hamano diff --git a/sequencer.c b/sequencer.c index e8779c7..0b858ca 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2734,13 +2734,18 @@ static int rollback_is_safe(void) static int reset_merge(const struct object_id *oid) { - const char *argv[4]; /* reset --merge + NULL */ + int ret; + struct argv_array argv = ARGV_ARRAY_INIT; - argv[0] = "reset"; - argv[1] = "--merge"; - argv[2] = oid_to_hex(oid); - argv[3] = NULL; - return run_command_v_opt(argv, RUN_GIT_CMD); + argv_array_pushl(&argv, "reset", "--merge", NULL); + + if (!is_null_oid(oid)) + argv_array_push(&argv, oid_to_hex(oid)); + + ret = run_command_v_opt(argv.argv, RUN_GIT_CMD); + argv_array_clear(&argv); + + return ret; } static int rollback_single_pick(struct repository *r) -- cgit v0.10.2-6-g49f6 From de81ca3f36b0f1db8bd615c98f2a5c819092c275 Mon Sep 17 00:00:00 2001 From: Rohit Ashiwal Date: Tue, 2 Jul 2019 14:41:28 +0530 Subject: 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 Signed-off-by: Junio C Hamano diff --git a/Documentation/git-cherry-pick.txt b/Documentation/git-cherry-pick.txt index 754b16c..83ce51a 100644 --- a/Documentation/git-cherry-pick.txt +++ b/Documentation/git-cherry-pick.txt @@ -10,9 +10,7 @@ SYNOPSIS [verse] 'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff] [-S[]] ... -'git cherry-pick' --continue -'git cherry-pick' --quit -'git cherry-pick' --abort +'git cherry-pick' (--continue | --skip | --abort | --quit) DESCRIPTION ----------- diff --git a/Documentation/git-revert.txt b/Documentation/git-revert.txt index 0c82ca5..665e065 100644 --- a/Documentation/git-revert.txt +++ b/Documentation/git-revert.txt @@ -9,9 +9,7 @@ SYNOPSIS -------- [verse] 'git revert' [--[no-]edit] [-n] [-m parent-number] [-s] [-S[]] ... -'git revert' --continue -'git revert' --quit -'git revert' --abort +'git revert' (--continue | --skip | --abort | --quit) DESCRIPTION ----------- diff --git a/Documentation/sequencer.txt b/Documentation/sequencer.txt index 5a57c4a..3bceb56 100644 --- a/Documentation/sequencer.txt +++ b/Documentation/sequencer.txt @@ -3,6 +3,10 @@ `.git/sequencer`. Can be used to continue after resolving conflicts in a failed cherry-pick or revert. +--skip:: + Skip the current commit and continue with the rest of the + sequence. + --quit:: Forget about the current operation in progress. Can be used to clear the sequencer state after a failed cherry-pick or diff --git a/builtin/revert.c b/builtin/revert.c index d4dcedb..5dc5891 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -102,6 +102,7 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts) OPT_CMDMODE(0, "quit", &cmd, N_("end revert or cherry-pick sequence"), 'q'), OPT_CMDMODE(0, "continue", &cmd, N_("resume revert or cherry-pick sequence"), 'c'), OPT_CMDMODE(0, "abort", &cmd, N_("cancel revert or cherry-pick sequence"), 'a'), + OPT_CMDMODE(0, "skip", &cmd, N_("skip current commit and continue"), 's'), OPT_CLEANUP(&cleanup_arg), OPT_BOOL('n', "no-commit", &opts->no_commit, N_("don't automatically commit")), 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) this_operation = "--quit"; else if (cmd == 'c') this_operation = "--continue"; + else if (cmd == 's') + this_operation = "--skip"; else { assert(cmd == 'a'); this_operation = "--abort"; @@ -210,6 +213,8 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts) return sequencer_continue(the_repository, opts); if (cmd == 'a') return sequencer_rollback(the_repository, opts); + if (cmd == 's') + return sequencer_skip(the_repository, opts); return sequencer_pick_revisions(the_repository, opts); } diff --git a/sequencer.c b/sequencer.c index 0b858ca..dede47a 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2762,6 +2762,15 @@ static int rollback_single_pick(struct repository *r) return reset_merge(&head_oid); } +static int skip_single_pick(void) +{ + struct object_id head; + + if (read_ref_full("HEAD", 0, &head, NULL)) + return error(_("cannot resolve HEAD")); + return reset_merge(&head); +} + int sequencer_rollback(struct repository *r, struct replay_opts *opts) { FILE *f; @@ -2811,6 +2820,70 @@ fail: return -1; } +int sequencer_skip(struct repository *r, struct replay_opts *opts) +{ + enum replay_action action = -1; + sequencer_get_last_command(r, &action); + + /* + * Check whether the subcommand requested to skip the commit is actually + * in progress and that it's safe to skip the commit. + * + * opts->action tells us which subcommand requested to skip the commit. + * If the corresponding .git/_HEAD exists, we know that the + * action is in progress and we can skip the commit. + * + * Otherwise we check that the last instruction was related to the + * particular subcommand we're trying to execute and barf if that's not + * the case. + * + * Finally we check that the rollback is "safe", i.e., has the HEAD + * moved? In this case, it doesn't make sense to "reset the merge" and + * "skip the commit" as the user already handled this by committing. But + * we'd not want to barf here, instead give advice on how to proceed. We + * only need to check that when .git/_HEAD doesn't exist because + * it gets removed when the user commits, so if it still exists we're + * sure the user can't have committed before. + */ + switch (opts->action) { + case REPLAY_REVERT: + if (!file_exists(git_path_revert_head(r))) { + if (action != REPLAY_REVERT) + return error(_("no revert in progress")); + if (!rollback_is_safe()) + goto give_advice; + } + break; + case REPLAY_PICK: + if (!file_exists(git_path_cherry_pick_head(r))) { + if (action != REPLAY_PICK) + return error(_("no cherry-pick in progress")); + if (!rollback_is_safe()) + goto give_advice; + } + break; + default: + BUG("unexpected action in sequencer_skip"); + } + + if (skip_single_pick()) + return error(_("failed to skip the commit")); + if (!is_directory(git_path_seq_dir())) + return 0; + + return sequencer_continue(r, opts); + +give_advice: + error(_("there is nothing to skip")); + + if (advice_resolve_conflict) { + advise(_("have you committed already?\n" + "try \"git %s --continue\""), + action == REPLAY_REVERT ? "revert" : "cherry-pick"); + } + return -1; +} + static int save_todo(struct todo_list *todo_list, struct replay_opts *opts) { struct lock_file todo_lock = LOCK_INIT; diff --git a/sequencer.h b/sequencer.h index 0c494b8..731b985 100644 --- a/sequencer.h +++ b/sequencer.h @@ -129,6 +129,7 @@ int sequencer_pick_revisions(struct repository *repo, struct replay_opts *opts); int sequencer_continue(struct repository *repo, struct replay_opts *opts); int sequencer_rollback(struct repository *repo, struct replay_opts *opts); +int sequencer_skip(struct repository *repo, struct replay_opts *opts); int sequencer_remove_state(struct replay_opts *opts); #define TODO_LIST_KEEP_EMPTY (1U << 0) diff --git a/t/t3510-cherry-pick-sequence.sh b/t/t3510-cherry-pick-sequence.sh index 941d502..20515ea 100755 --- a/t/t3510-cherry-pick-sequence.sh +++ b/t/t3510-cherry-pick-sequence.sh @@ -93,6 +93,108 @@ test_expect_success 'cherry-pick cleans up sequencer state upon success' ' test_path_is_missing .git/sequencer ' +test_expect_success 'cherry-pick --skip requires cherry-pick in progress' ' + pristine_detach initial && + test_must_fail git cherry-pick --skip +' + +test_expect_success 'revert --skip requires revert in progress' ' + pristine_detach initial && + test_must_fail git revert --skip +' + +test_expect_success 'cherry-pick --skip to skip commit' ' + pristine_detach initial && + test_must_fail git cherry-pick anotherpick && + test_must_fail git revert --skip && + git cherry-pick --skip && + test_cmp_rev initial HEAD && + test_path_is_missing .git/CHERRY_PICK_HEAD +' + +test_expect_success 'revert --skip to skip commit' ' + pristine_detach anotherpick && + test_must_fail git revert anotherpick~1 && + test_must_fail git cherry-pick --skip && + git revert --skip && + test_cmp_rev anotherpick HEAD +' + +test_expect_success 'skip "empty" commit' ' + pristine_detach picked && + test_commit dummy foo d && + test_must_fail git cherry-pick anotherpick && + git cherry-pick --skip && + test_cmp_rev dummy HEAD +' + +test_expect_success 'skip a commit and check if rest of sequence is correct' ' + pristine_detach initial && + echo e >expect && + cat >expect.log <<-EOF && + OBJID + :100644 100644 OBJID OBJID M foo + OBJID + :100644 100644 OBJID OBJID M foo + OBJID + :100644 100644 OBJID OBJID M unrelated + OBJID + :000000 100644 OBJID OBJID A foo + :000000 100644 OBJID OBJID A unrelated + EOF + test_must_fail git cherry-pick base..yetanotherpick && + test_must_fail git cherry-pick --skip && + echo d >foo && + git add foo && + git cherry-pick --continue && + { + git rev-list HEAD | + git diff-tree --root --stdin | + sed "s/$OID_REGEX/OBJID/g" + } >actual.log && + test_cmp expect foo && + test_cmp expect.log actual.log +' + +test_expect_success 'check advice when we move HEAD by committing' ' + pristine_detach initial && + cat >expect <<-EOF && + error: there is nothing to skip + hint: have you committed already? + hint: try "git cherry-pick --continue" + fatal: cherry-pick failed + EOF + test_must_fail git cherry-pick base..yetanotherpick && + echo c >foo && + git commit -a && + test_path_is_missing .git/CHERRY_PICK_HEAD && + test_must_fail git cherry-pick --skip 2>advice && + test_i18ncmp expect advice +' + +test_expect_success 'allow skipping commit but not abort for a new history' ' + pristine_detach initial && + cat >expect <<-EOF && + error: cannot abort from a branch yet to be born + fatal: cherry-pick failed + EOF + git checkout --orphan new_disconnected && + git reset --hard && + test_must_fail git cherry-pick anotherpick && + test_must_fail git cherry-pick --abort 2>advice && + git cherry-pick --skip && + test_i18ncmp expect advice +' + +test_expect_success 'allow skipping stopped cherry-pick because of untracked file modifications' ' + pristine_detach initial && + git rm --cached unrelated && + git commit -m "untrack unrelated" && + test_must_fail git cherry-pick initial base && + test_path_is_missing .git/CHERRY_PICK_HEAD && + git cherry-pick --skip +' + test_expect_success '--quit does not complain when no cherry-pick is in progress' ' pristine_detach initial && git cherry-pick --quit -- cgit v0.10.2-6-g49f6 From dcb500dc16ce8db556b51e9a5b3fa977111d0443 Mon Sep 17 00:00:00 2001 From: Rohit Ashiwal Date: Tue, 2 Jul 2019 14:41:29 +0530 Subject: cherry-pick/revert: advise using --skip The previous commit introduced a --skip flag for cherry-pick and revert. Update the advice messages, to tell users about this less cumbersome way of skipping commits. Also add tests to ensure everything is working fine. Signed-off-by: Rohit Ashiwal Signed-off-by: Junio C Hamano diff --git a/builtin/commit.c b/builtin/commit.c index 1c9e8e2..1f47c51 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -60,15 +60,18 @@ N_("The previous cherry-pick is now empty, possibly due to conflict resolution.\ "\n"); static const char empty_cherry_pick_advice_single[] = -N_("Otherwise, please use 'git reset'\n"); +N_("Otherwise, please use 'git cherry-pick --skip'\n"); static const char empty_cherry_pick_advice_multi[] = -N_("If you wish to skip this commit, use:\n" +N_("and then use:\n" "\n" -" git reset\n" +" git cherry-pick --continue\n" "\n" -"Then \"git cherry-pick --continue\" will resume cherry-picking\n" -"the remaining commits.\n"); +"to resume cherry-picking the remaining commits.\n" +"If you wish to skip this commit, use:\n" +"\n" +" git cherry-pick --skip\n" +"\n"); static const char *color_status_slots[] = { [WT_STATUS_HEADER] = "header", diff --git a/sequencer.c b/sequencer.c index dede47a..7d0e5f9 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2655,18 +2655,20 @@ static int create_seq_dir(struct repository *r) enum replay_action action; const char *in_progress_error = NULL; const char *in_progress_advice = NULL; + unsigned int advise_skip = file_exists(git_path_revert_head(r)) || + file_exists(git_path_cherry_pick_head(r)); if (!sequencer_get_last_command(r, &action)) { switch (action) { case REPLAY_REVERT: in_progress_error = _("revert is already in progress"); in_progress_advice = - _("try \"git revert (--continue | --abort | --quit)\""); + _("try \"git revert (--continue | %s--abort | --quit)\""); break; case REPLAY_PICK: in_progress_error = _("cherry-pick is already in progress"); in_progress_advice = - _("try \"git cherry-pick (--continue | --abort | --quit)\""); + _("try \"git cherry-pick (--continue | %s--abort | --quit)\""); break; default: BUG("unexpected action in create_seq_dir"); @@ -2675,7 +2677,8 @@ static int create_seq_dir(struct repository *r) if (in_progress_error) { error("%s", in_progress_error); if (advice_sequencer_in_use) - advise("%s", in_progress_advice); + advise(in_progress_advice, + advise_skip ? "--skip | " : ""); return -1; } if (mkdir(git_path_seq_dir(), 0777) < 0) diff --git a/t/t3510-cherry-pick-sequence.sh b/t/t3510-cherry-pick-sequence.sh index 20515ea..793bcc7 100755 --- a/t/t3510-cherry-pick-sequence.sh +++ b/t/t3510-cherry-pick-sequence.sh @@ -172,6 +172,26 @@ test_expect_success 'check advice when we move HEAD by committing' ' test_i18ncmp expect advice ' +test_expect_success 'selectively advise --skip while launching another sequence' ' + pristine_detach initial && + cat >expect <<-EOF && + error: cherry-pick is already in progress + hint: try "git cherry-pick (--continue | --skip | --abort | --quit)" + fatal: cherry-pick failed + EOF + test_must_fail git cherry-pick picked..yetanotherpick && + test_must_fail git cherry-pick picked..yetanotherpick 2>advice && + test_i18ncmp expect advice && + cat >expect <<-EOF && + error: cherry-pick is already in progress + hint: try "git cherry-pick (--continue | --abort | --quit)" + fatal: cherry-pick failed + EOF + git reset --merge && + test_must_fail git cherry-pick picked..yetanotherpick 2>advice && + test_i18ncmp expect advice +' + test_expect_success 'allow skipping commit but not abort for a new history' ' pristine_detach initial && cat >expect <<-EOF && -- cgit v0.10.2-6-g49f6