t7503: verify proper hook execution
[gitweb.git] / sequencer.c
index f88a97fb10a322c21062ec8c102482677cfd0feb..34ebf8ed94ad7d8df6773337d31ff7d9c2c84c4a 100644 (file)
@@ -279,7 +279,7 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts)
 int sequencer_remove_state(struct replay_opts *opts)
 {
        struct strbuf buf = STRBUF_INIT;
-       int i;
+       int i, ret = 0;
 
        if (is_rebase_i(opts) &&
            strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0) > 0) {
@@ -288,8 +288,10 @@ int sequencer_remove_state(struct replay_opts *opts)
                        char *eol = strchr(p, '\n');
                        if (eol)
                                *eol = '\0';
-                       if (delete_ref("(rebase -i) cleanup", p, NULL, 0) < 0)
+                       if (delete_ref("(rebase -i) cleanup", p, NULL, 0) < 0) {
                                warning(_("could not delete '%s'"), p);
+                               ret = -1;
+                       }
                        if (!eol)
                                break;
                        p = eol + 1;
@@ -305,10 +307,11 @@ int sequencer_remove_state(struct replay_opts *opts)
 
        strbuf_reset(&buf);
        strbuf_addstr(&buf, get_dir(opts));
-       remove_dir_recursively(&buf, 0);
+       if (remove_dir_recursively(&buf, 0))
+               ret = error(_("could not remove '%s'"), buf.buf);
        strbuf_release(&buf);
 
-       return 0;
+       return ret;
 }
 
 static const char *action_name(const struct replay_opts *opts)
@@ -767,7 +770,7 @@ static int parse_key_value_squoted(char *buf, struct string_list *list)
  *     GIT_AUTHOR_DATE='$author_date'
  *
  * where $author_name, $author_email and $author_date are quoted. We are strict
- * with our parsing, as the file was meant to be eval'd in the old
+ * with our parsing, as the file was meant to be eval'd in the now-removed
  * git-am.sh/git-rebase--interactive.sh scripts, and thus if the file differs
  * from what this function expects, it is better to bail out than to do
  * something that the user does not expect.
@@ -2076,6 +2079,18 @@ const char *todo_item_get_arg(struct todo_list *todo_list,
        return todo_list->buf.buf + item->arg_offset;
 }
 
+static int is_command(enum todo_command command, const char **bol)
+{
+       const char *str = todo_command_info[command].str;
+       const char nick = todo_command_info[command].c;
+       const char *p = *bol + 1;
+
+       return skip_prefix(*bol, str, bol) ||
+               ((nick && **bol == nick) &&
+                (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || !*p) &&
+                (*bol = p));
+}
+
 static int parse_insn_line(struct repository *r, struct todo_item *item,
                           const char *buf, const char *bol, char *eol)
 {
@@ -2097,12 +2112,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,
        }
 
        for (i = 0; i < TODO_COMMENT; i++)
-               if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
-                       item->command = i;
-                       break;
-               } else if ((bol + 1 == eol || bol[1] == ' ') &&
-                          *bol == todo_command_info[i].c) {
-                       bol++;
+               if (is_command(i, &bol)) {
                        item->command = i;
                        break;
                }
@@ -2170,34 +2180,26 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,
 
 int sequencer_get_last_command(struct repository *r, enum replay_action *action)
 {
-       struct todo_item item;
-       char *eol;
-       const char *todo_file;
+       const char *todo_file, *bol;
        struct strbuf buf = STRBUF_INIT;
-       int ret = -1;
+       int ret = 0;
 
        todo_file = git_path_todo_file();
        if (strbuf_read_file(&buf, todo_file, 0) < 0) {
-               if (errno == ENOENT)
+               if (errno == ENOENT || errno == ENOTDIR)
                        return -1;
                else
                        return error_errno("unable to open '%s'", todo_file);
        }
-       eol = strchrnul(buf.buf, '\n');
-       if (buf.buf != eol && eol[-1] == '\r')
-               eol--; /* strip Carriage Return */
-       if (parse_insn_line(r, &item, buf.buf, buf.buf, eol))
-               goto fail;
-       if (item.command == TODO_PICK)
+       bol = buf.buf + strspn(buf.buf, " \t\r\n");
+       if (is_command(TODO_PICK, &bol) && (*bol == ' ' || *bol == '\t'))
                *action = REPLAY_PICK;
-       else if (item.command == TODO_REVERT)
+       else if (is_command(TODO_REVERT, &bol) &&
+                (*bol == ' ' || *bol == '\t'))
                *action = REPLAY_REVERT;
        else
-               goto fail;
-
-       ret = 0;
+               ret = -1;
 
- fail:
        strbuf_release(&buf);
 
        return ret;
@@ -2311,19 +2313,21 @@ static int have_finished_the_last_pick(void)
        return ret;
 }
 
-void sequencer_post_commit_cleanup(struct repository *r)
+void sequencer_post_commit_cleanup(struct repository *r, int verbose)
 {
        struct replay_opts opts = REPLAY_OPTS_INIT;
        int need_cleanup = 0;
 
        if (file_exists(git_path_cherry_pick_head(r))) {
-               unlink(git_path_cherry_pick_head(r));
+               if (!unlink(git_path_cherry_pick_head(r)) && verbose)
+                       warning(_("cancelling a cherry picking in progress"));
                opts.action = REPLAY_PICK;
                need_cleanup = 1;
        }
 
        if (file_exists(git_path_revert_head(r))) {
-               unlink(git_path_revert_head(r));
+               if (!unlink(git_path_revert_head(r)) && verbose)
+                       warning(_("cancelling a revert in progress"));
                opts.action = REPLAY_REVERT;
                need_cleanup = 1;
        }
@@ -2650,15 +2654,41 @@ 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;
+       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 | %s--abort | --quit)\"");
+                       break;
+               case REPLAY_PICK:
+                       in_progress_error = _("cherry-pick is already in progress");
+                       in_progress_advice =
+                       _("try \"git cherry-pick (--continue | %s--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(in_progress_advice,
+                               advise_skip ? "--skip | " : "");
                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;
 }
 
@@ -2709,15 +2739,20 @@ 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 <arg> + 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)
@@ -2731,7 +2766,16 @@ 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);
+}
+
+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)
@@ -2774,7 +2818,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);
@@ -2783,6 +2827,70 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)
        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/<ACTION>_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/<ACTION>_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;
@@ -3194,7 +3302,7 @@ static int do_reset(struct repository *r,
                return error_resolve_conflict(_(action_name(opts)));
        }
 
-       if (!fill_tree_descriptor(&desc, &oid)) {
+       if (!fill_tree_descriptor(r, &desc, &oid)) {
                error(_("failed to find tree of %s"), oid_to_hex(&oid));
                rollback_lock_file(&lock);
                free((void *)desc.buffer);
@@ -3401,6 +3509,10 @@ static int do_merge(struct repository *r,
                rollback_lock_file(&lock);
                ret = fast_forward_to(r, &commit->object.oid,
                                      &head_commit->object.oid, 0, opts);
+               if (flags & TODO_EDIT_MERGE_MSG) {
+                       run_commit_flags |= AMEND_MSG;
+                       goto fast_forward_edit;
+               }
                goto leave_merge;
        }
 
@@ -3504,6 +3616,7 @@ static int do_merge(struct repository *r,
                 * value (a negative one would indicate that the `merge`
                 * command needs to be rescheduled).
                 */
+       fast_forward_edit:
                ret = !!run_git_commit(r, git_path_merge_msg(r), opts,
                                       run_commit_flags);
 
@@ -3728,11 +3841,14 @@ static int pick_commits(struct repository *r,
                        unlink(rebase_path_author_script());
                        unlink(rebase_path_stopped_sha());
                        unlink(rebase_path_amend());
-                       unlink(git_path_merge_head(the_repository));
+                       unlink(git_path_merge_head(r));
                        delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
 
-                       if (item->command == TODO_BREAK)
+                       if (item->command == TODO_BREAK) {
+                               if (!opts->verbose)
+                                       term_clear_line();
                                return stopped_at_head(r);
+                       }
                }
                if (item->command <= TODO_SQUASH) {
                        if (is_rebase_i(opts))
@@ -3754,11 +3870,14 @@ static int pick_commits(struct repository *r,
                        }
                        if (item->command == TODO_EDIT) {
                                struct commit *commit = item->commit;
-                               if (!res)
+                               if (!res) {
+                                       if (!opts->verbose)
+                                               term_clear_line();
                                        fprintf(stderr,
                                                _("Stopped at %s...  %.*s\n"),
                                                short_commit_name(commit),
                                                item->arg_len, arg);
+                               }
                                return error_with_patch(r, commit,
                                        arg, item->arg_len, opts, res, !res);
                        }
@@ -3796,6 +3915,8 @@ static int pick_commits(struct repository *r,
                        int saved = *end_of_arg;
                        struct stat st;
 
+                       if (!opts->verbose)
+                               term_clear_line();
                        *end_of_arg = '\0';
                        res = do_exec(r, arg);
                        *end_of_arg = saved;
@@ -3954,10 +4075,13 @@ static int pick_commits(struct repository *r,
                }
                apply_autostash(opts);
 
-               if (!opts->quiet)
+               if (!opts->quiet) {
+                       if (!opts->verbose)
+                               term_clear_line();
                        fprintf(stderr,
                                "Successfully rebased and updated %s.\n",
                                head_ref.buf);
+               }
 
                strbuf_release(&buf);
                strbuf_release(&head_ref);
@@ -4102,7 +4226,7 @@ static int commit_staged_changes(struct repository *r,
                           opts, flags))
                return error(_("could not commit staged changes."));
        unlink(rebase_path_amend());
-       unlink(git_path_merge_head(the_repository));
+       unlink(git_path_merge_head(r));
        if (final_fixup) {
                unlink(rebase_path_fixup_msg());
                unlink(rebase_path_squash_msg());
@@ -4237,7 +4361,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"));