sequencer (rebase -i): implement the 'reword' command
[gitweb.git] / sequencer.c
index 29b944d724a4f91ce694e65499e98c8f8544f230..50e998acc4c5749fbd37b1dfbcf73dc495a7f233 100644 (file)
@@ -102,6 +102,8 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
 static GIT_PATH_FUNC(rebase_path_gpg_sign_opt, "rebase-merge/gpg_sign_opt")
 static GIT_PATH_FUNC(rebase_path_orig_head, "rebase-merge/orig-head")
 static GIT_PATH_FUNC(rebase_path_verbose, "rebase-merge/verbose")
+static GIT_PATH_FUNC(rebase_path_head_name, "rebase-merge/head-name")
+static GIT_PATH_FUNC(rebase_path_onto, "rebase-merge/onto")
 
 static inline int is_rebase_i(const struct replay_opts *opts)
 {
@@ -483,6 +485,52 @@ static int is_index_unchanged(void)
        return !hashcmp(active_cache_tree->sha1, head_commit->tree->object.oid.hash);
 }
 
+static int write_author_script(const char *message)
+{
+       struct strbuf buf = STRBUF_INIT;
+       const char *eol;
+       int res;
+
+       for (;;)
+               if (!*message || starts_with(message, "\n")) {
+missing_author:
+                       /* Missing 'author' line? */
+                       unlink(rebase_path_author_script());
+                       return 0;
+               } else if (skip_prefix(message, "author ", &message))
+                       break;
+               else if ((eol = strchr(message, '\n')))
+                       message = eol + 1;
+               else
+                       goto missing_author;
+
+       strbuf_addstr(&buf, "GIT_AUTHOR_NAME='");
+       while (*message && *message != '\n' && *message != '\r')
+               if (skip_prefix(message, " <", &message))
+                       break;
+               else if (*message != '\'')
+                       strbuf_addch(&buf, *(message++));
+               else
+                       strbuf_addf(&buf, "'\\\\%c'", *(message++));
+       strbuf_addstr(&buf, "'\nGIT_AUTHOR_EMAIL='");
+       while (*message && *message != '\n' && *message != '\r')
+               if (skip_prefix(message, "> ", &message))
+                       break;
+               else if (*message != '\'')
+                       strbuf_addch(&buf, *(message++));
+               else
+                       strbuf_addf(&buf, "'\\\\%c'", *(message++));
+       strbuf_addstr(&buf, "'\nGIT_AUTHOR_DATE='@");
+       while (*message && *message != '\n' && *message != '\r')
+               if (*message != '\'')
+                       strbuf_addch(&buf, *(message++));
+               else
+                       strbuf_addf(&buf, "'\\\\%c'", *(message++));
+       res = write_message(buf.buf, buf.len, rebase_path_author_script(), 1);
+       strbuf_release(&buf);
+       return res;
+}
+
 /*
  * Read the author-script file into an environment block, ready for use in
  * run_command(), that can be free()d afterwards.
@@ -670,6 +718,7 @@ enum todo_command {
        TODO_PICK = 0,
        TODO_REVERT,
        TODO_EDIT,
+       TODO_REWORD,
        TODO_FIXUP,
        TODO_SQUASH,
        /* commands that do something else than handling a single commit */
@@ -685,6 +734,7 @@ static struct {
        { 'p', "pick" },
        { 0,   "revert" },
        { 'e', "edit" },
+       { 'r', "reword" },
        { 'f', "fixup" },
        { 's', "squash" },
        { 'x', "exec" },
@@ -914,7 +964,9 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
                }
        }
 
-       if (is_fixup(command)) {
+       if (command == TODO_REWORD)
+               edit = 1;
+       else if (is_fixup(command)) {
                if (update_squash_messages(command, commit, opts))
                        return -1;
                amend = 1;
@@ -935,7 +987,9 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
                }
        }
 
-       if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) {
+       if (is_rebase_i(opts) && write_author_script(msg.message) < 0)
+               res = -1;
+       else if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) {
                res = do_recursive_merge(base, next, base_label, next_label,
                                         head, &msgbuf, opts);
                if (res < 0)
@@ -1167,8 +1221,7 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list)
                else if (!is_noop(item->command))
                        fixup_okay = 1;
        }
-       if (!todo_list->nr)
-               return error(_("no commits parsed."));
+
        return res;
 }
 
@@ -1192,6 +1245,10 @@ static int read_populate_todo(struct todo_list *todo_list,
        if (res)
                return error(_("unusable instruction sheet: '%s'"), todo_file);
 
+       if (!todo_list->nr &&
+           (!is_rebase_i(opts) || !file_exists(rebase_path_done())))
+               return error(_("no commits parsed."));
+
        if (!is_rebase_i(opts)) {
                enum todo_command valid =
                        opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;
@@ -1716,7 +1773,10 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
                                        intend_to_amend();
                                return error_failed_squash(item->commit, opts,
                                        item->arg_len, item->arg);
-                       }
+                       } else if (res && is_rebase_i(opts))
+                               return res | error_with_patch(item->commit,
+                                       item->arg, item->arg_len, opts, res,
+                                       item->command == TODO_REWORD);
                } else if (item->command == TODO_EXEC) {
                        char *end_of_arg = (char *)(item->arg + item->arg_len);
                        int saved = *end_of_arg;
@@ -1733,12 +1793,53 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
        }
 
        if (is_rebase_i(opts)) {
-               struct strbuf buf = STRBUF_INIT;
+               struct strbuf head_ref = STRBUF_INIT, buf = STRBUF_INIT;
 
                /* Stopped in the middle, as planned? */
                if (todo_list->current < todo_list->nr)
                        return 0;
 
+               if (read_oneliner(&head_ref, rebase_path_head_name(), 0) &&
+                               starts_with(head_ref.buf, "refs/")) {
+                       unsigned char head[20], orig[20];
+                       int res;
+
+                       if (get_sha1("HEAD", head)) {
+                               res = error(_("cannot read HEAD"));
+cleanup_head_ref:
+                               strbuf_release(&head_ref);
+                               strbuf_release(&buf);
+                               return res;
+                       }
+                       if (!read_oneliner(&buf, rebase_path_orig_head(), 0) ||
+                                       get_sha1_hex(buf.buf, orig)) {
+                               res = error(_("could not read orig-head"));
+                               goto cleanup_head_ref;
+                       }
+                       strbuf_addf(&buf, "rebase -i (finish): %s onto ",
+                               head_ref.buf);
+                       if (!read_oneliner(&buf, rebase_path_onto(), 0)) {
+                               res = error(_("could not read 'onto'"));
+                               goto cleanup_head_ref;
+                       }
+                       if (update_ref(buf.buf, head_ref.buf, head, orig,
+                                       REF_NODEREF, UPDATE_REFS_MSG_ON_ERR)) {
+                               res = error(_("could not update %s"),
+                                       head_ref.buf);
+                               goto cleanup_head_ref;
+                       }
+                       strbuf_reset(&buf);
+                       strbuf_addf(&buf,
+                               "rebase -i (finish): returning to %s",
+                               head_ref.buf);
+                       if (create_symref("HEAD", head_ref.buf, buf.buf)) {
+                               res = error(_("could not update HEAD to %s"),
+                                       head_ref.buf);
+                               goto cleanup_head_ref;
+                       }
+                       strbuf_reset(&buf);
+               }
+
                if (opts->verbose) {
                        struct rev_info log_tree_opt;
                        struct object_id orig, head;
@@ -1759,6 +1860,7 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
                        }
                }
                strbuf_release(&buf);
+               strbuf_release(&head_ref);
        }
 
        /*
@@ -1778,6 +1880,47 @@ static int continue_single_pick(void)
        return run_command_v_opt(argv, RUN_GIT_CMD);
 }
 
+static int commit_staged_changes(struct replay_opts *opts)
+{
+       int amend = 0;
+
+       if (has_unstaged_changes(1))
+               return error(_("cannot rebase: You have unstaged changes."));
+       if (!has_uncommitted_changes(0)) {
+               const char *cherry_pick_head = git_path("CHERRY_PICK_HEAD");
+
+               if (file_exists(cherry_pick_head) && unlink(cherry_pick_head))
+                       return error(_("could not remove CHERRY_PICK_HEAD"));
+               return 0;
+       }
+
+       if (file_exists(rebase_path_amend())) {
+               struct strbuf rev = STRBUF_INIT;
+               unsigned char head[20], to_amend[20];
+
+               if (get_sha1("HEAD", head))
+                       return error(_("cannot amend non-existing commit"));
+               if (!read_oneliner(&rev, rebase_path_amend(), 0))
+                       return error(_("invalid file: '%s'"), rebase_path_amend());
+               if (get_sha1_hex(rev.buf, to_amend))
+                       return error(_("invalid contents: '%s'"),
+                               rebase_path_amend());
+               if (hashcmp(head, to_amend))
+                       return error(_("\nYou have uncommitted changes in your "
+                                      "working tree. Please, commit them\n"
+                                      "first and then run 'git rebase "
+                                      "--continue' again."));
+
+               strbuf_release(&rev);
+               amend = 1;
+       }
+
+       if (run_git_commit(rebase_path_message(), opts, 1, 1, amend, 0))
+               return error(_("could not commit staged changes."));
+       unlink(rebase_path_amend());
+       return 0;
+}
+
 int sequencer_continue(struct replay_opts *opts)
 {
        struct todo_list todo_list = TODO_LIST_INIT;
@@ -1786,25 +1929,31 @@ int sequencer_continue(struct replay_opts *opts)
        if (read_and_refresh_cache(opts))
                return -1;
 
-       if (!file_exists(get_todo_path(opts)))
+       if (is_rebase_i(opts)) {
+               if (commit_staged_changes(opts))
+                       return -1;
+       } else if (!file_exists(get_todo_path(opts)))
                return continue_single_pick();
        if (read_populate_opts(opts))
                return -1;
        if ((res = read_populate_todo(&todo_list, opts)))
                goto release_todo_list;
 
-       /* Verify that the conflict has been resolved */
-       if (file_exists(git_path_cherry_pick_head()) ||
-           file_exists(git_path_revert_head())) {
-               res = continue_single_pick();
-               if (res)
+       if (!is_rebase_i(opts)) {
+               /* Verify that the conflict has been resolved */
+               if (file_exists(git_path_cherry_pick_head()) ||
+                   file_exists(git_path_revert_head())) {
+                       res = continue_single_pick();
+                       if (res)
+                               goto release_todo_list;
+               }
+               if (index_differs_from("HEAD", 0, 0)) {
+                       res = error_dirty_index(opts);
                        goto release_todo_list;
+               }
+               todo_list.current++;
        }
-       if (index_differs_from("HEAD", 0, 0)) {
-               res = error_dirty_index(opts);
-               goto release_todo_list;
-       }
-       todo_list.current++;
+
        res = pick_commits(&todo_list, opts);
 release_todo_list:
        todo_list_release(&todo_list);