]> git.ipfire.org Git - thirdparty/git.git/blobdiff - sequencer.c
cherry-pick/revert: advise using --skip
[thirdparty/git.git] / sequencer.c
index 4407a3f97858acdf47add8e8848cba12daf1de6d..7d0e5f93663e8c4d5737e203b5e27840dc0870e0 100644 (file)
@@ -767,7 +767,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.
@@ -2168,6 +2168,41 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,
        return !item->commit;
 }
 
+int sequencer_get_last_command(struct repository *r, enum replay_action *action)
+{
+       struct todo_item item;
+       char *eol;
+       const char *todo_file;
+       struct strbuf buf = STRBUF_INIT;
+       int ret = -1;
+
+       todo_file = git_path_todo_file();
+       if (strbuf_read_file(&buf, todo_file, 0) < 0) {
+               if (errno == ENOENT)
+                       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)
+               *action = REPLAY_PICK;
+       else if (item.command == TODO_REVERT)
+               *action = REPLAY_REVERT;
+       else
+               goto fail;
+
+       ret = 0;
+
+ fail:
+       strbuf_release(&buf);
+
+       return ret;
+}
+
 int todo_list_parse_insn_buffer(struct repository *r, char *buf,
                                struct todo_list *todo_list)
 {
@@ -2251,6 +2286,57 @@ static ssize_t strbuf_read_file_or_whine(struct strbuf *sb, const char *path)
        return len;
 }
 
+static int have_finished_the_last_pick(void)
+{
+       struct strbuf buf = STRBUF_INIT;
+       const char *eol;
+       const char *todo_path = git_path_todo_file();
+       int ret = 0;
+
+       if (strbuf_read_file(&buf, todo_path, 0) < 0) {
+               if (errno == ENOENT) {
+                       return 0;
+               } else {
+                       error_errno("unable to open '%s'", todo_path);
+                       return 0;
+               }
+       }
+       /* If there is only one line then we are done */
+       eol = strchr(buf.buf, '\n');
+       if (!eol || !eol[1])
+               ret = 1;
+
+       strbuf_release(&buf);
+
+       return ret;
+}
+
+void sequencer_post_commit_cleanup(struct repository *r)
+{
+       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));
+               opts.action = REPLAY_PICK;
+               need_cleanup = 1;
+       }
+
+       if (file_exists(git_path_revert_head(r))) {
+               unlink(git_path_revert_head(r));
+               opts.action = REPLAY_REVERT;
+               need_cleanup = 1;
+       }
+
+       if (!need_cleanup)
+               return;
+
+       if (!have_finished_the_last_pick())
+               return;
+
+       sequencer_remove_state(&opts);
+}
+
 static int read_populate_todo(struct repository *r,
                              struct todo_list *todo_list,
                              struct replay_opts *opts)
@@ -2494,14 +2580,15 @@ static void write_strategy_opts(struct replay_opts *opts)
 }
 
 int write_basic_state(struct replay_opts *opts, const char *head_name,
-                     const char *onto, const char *orig_head)
+                     struct commit *onto, const char *orig_head)
 {
        const char *quiet = getenv("GIT_QUIET");
 
        if (head_name)
                write_file(rebase_path_head_name(), "%s\n", head_name);
        if (onto)
-               write_file(rebase_path_onto(), "%s\n", onto);
+               write_file(rebase_path_onto(), "%s\n",
+                          oid_to_hex(&onto->object.oid));
        if (orig_head)
                write_file(rebase_path_orig_head(), "%s\n", orig_head);
 
@@ -2563,15 +2650,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;
 }
 
@@ -2622,15 +2735,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)
@@ -2644,7 +2762,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)
@@ -2687,7 +2814,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);
@@ -2696,6 +2823,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/<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;
@@ -3314,6 +3505,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;
        }
 
@@ -3417,6 +3612,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);
 
@@ -3517,10 +3713,11 @@ static const char *reflog_message(struct replay_opts *opts,
        return buf.buf;
 }
 
-static int run_git_checkout(struct replay_opts *opts, const char *commit,
-                           const char *action)
+static int run_git_checkout(struct repository *r, struct replay_opts *opts,
+                           const char *commit, const char *action)
 {
        struct child_process cmd = CHILD_PROCESS_INIT;
+       int ret;
 
        cmd.git_cmd = 1;
 
@@ -3529,26 +3726,32 @@ static int run_git_checkout(struct replay_opts *opts, const char *commit,
        argv_array_pushf(&cmd.env_array, GIT_REFLOG_ACTION "=%s", action);
 
        if (opts->verbose)
-               return run_command(&cmd);
+               ret = run_command(&cmd);
        else
-               return run_command_silent_on_success(&cmd);
+               ret = run_command_silent_on_success(&cmd);
+
+       if (!ret)
+               discard_index(r->index);
+
+       return ret;
 }
 
-int prepare_branch_to_be_rebased(struct replay_opts *opts, const char *commit)
+int prepare_branch_to_be_rebased(struct repository *r, struct replay_opts *opts,
+                                const char *commit)
 {
        const char *action;
 
        if (commit && *commit) {
                action = reflog_message(opts, "start", "checkout %s", commit);
-               if (run_git_checkout(opts, commit, action))
+               if (run_git_checkout(r, opts, commit, action))
                        return error(_("could not checkout %s"), commit);
        }
 
        return 0;
 }
 
-static int checkout_onto(struct replay_opts *opts,
-                        const char *onto_name, const char *onto,
+static int checkout_onto(struct repository *r, struct replay_opts *opts,
+                        const char *onto_name, const struct object_id *onto,
                         const char *orig_head)
 {
        struct object_id oid;
@@ -3557,7 +3760,7 @@ static int checkout_onto(struct replay_opts *opts,
        if (get_oid(orig_head, &oid))
                return error(_("%s: not a valid OID"), orig_head);
 
-       if (run_git_checkout(opts, onto, action)) {
+       if (run_git_checkout(r, opts, oid_to_hex(onto), action)) {
                apply_autostash(opts);
                sequencer_remove_state(opts);
                return error(_("could not detach HEAD"));
@@ -4143,7 +4346,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"));
@@ -4833,16 +5036,16 @@ static int skip_unnecessary_picks(struct repository *r,
 
 int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags,
                    const char *shortrevisions, const char *onto_name,
-                   const char *onto, const char *orig_head, struct string_list *commands,
-                   unsigned autosquash, struct todo_list *todo_list)
+                   struct commit *onto, const char *orig_head,
+                   struct string_list *commands, unsigned autosquash,
+                   struct todo_list *todo_list)
 {
        const char *shortonto, *todo_file = rebase_path_todo();
        struct todo_list new_todo = TODO_LIST_INIT;
        struct strbuf *buf = &todo_list->buf;
-       struct object_id oid;
+       struct object_id oid = onto->object.oid;
        int res;
 
-       get_oid(onto, &oid);
        shortonto = find_unique_abbrev(&oid, DEFAULT_ABBREV);
 
        if (buf->len == 0) {
@@ -4885,7 +5088,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
        if (todo_list_parse_insn_buffer(r, new_todo.buf.buf, &new_todo) ||
            todo_list_check(todo_list, &new_todo)) {
                fprintf(stderr, _(edit_todo_list_advice));
-               checkout_onto(opts, onto_name, onto, orig_head);
+               checkout_onto(r, opts, onto_name, &onto->object.oid, orig_head);
                todo_list_release(&new_todo);
 
                return -1;
@@ -4904,7 +5107,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
 
        todo_list_release(&new_todo);
 
-       if (checkout_onto(opts, onto_name, oid_to_hex(&oid), orig_head))
+       if (checkout_onto(r, opts, onto_name, &oid, orig_head))
                return -1;
 
        if (require_clean_work_tree(r, "rebase", "", 1, 1))