Merge branch 'pw/sequencer-in-process-commit'
[gitweb.git] / sequencer.c
index b005e412a5b04ba6bf2df31157d01bbbe779fd27..5bfdc4044233d5f809f9f1fbc55ebe3da477e3f0 100644 (file)
@@ -1,10 +1,10 @@
 #include "cache.h"
 #include "config.h"
 #include "lockfile.h"
-#include "sequencer.h"
 #include "dir.h"
 #include "object.h"
 #include "commit.h"
+#include "sequencer.h"
 #include "tag.h"
 #include "run-command.h"
 #include "exec_cmd.h"
 #include "log-tree.h"
 #include "wt-status.h"
 #include "hashmap.h"
+#include "notes-utils.h"
+#include "sigchain.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
 const char sign_off_header[] = "Signed-off-by: ";
 static const char cherry_picked_prefix[] = "(cherry picked from commit ";
 
+GIT_PATH_FUNC(git_path_commit_editmsg, "COMMIT_EDITMSG")
+
 GIT_PATH_FUNC(git_path_seq_dir, "sequencer")
 
 static GIT_PATH_FUNC(git_path_todo_file, "sequencer/todo")
@@ -130,6 +134,51 @@ static GIT_PATH_FUNC(rebase_path_strategy, "rebase-merge/strategy")
 static GIT_PATH_FUNC(rebase_path_strategy_opts, "rebase-merge/strategy_opts")
 static GIT_PATH_FUNC(rebase_path_allow_rerere_autoupdate, "rebase-merge/allow_rerere_autoupdate")
 
+static int git_sequencer_config(const char *k, const char *v, void *cb)
+{
+       struct replay_opts *opts = cb;
+       int status;
+
+       if (!strcmp(k, "commit.cleanup")) {
+               const char *s;
+
+               status = git_config_string(&s, k, v);
+               if (status)
+                       return status;
+
+               if (!strcmp(s, "verbatim"))
+                       opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_NONE;
+               else if (!strcmp(s, "whitespace"))
+                       opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_SPACE;
+               else if (!strcmp(s, "strip"))
+                       opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_ALL;
+               else if (!strcmp(s, "scissors"))
+                       opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_SPACE;
+               else
+                       warning(_("invalid commit message cleanup mode '%s'"),
+                                 s);
+
+               return status;
+       }
+
+       if (!strcmp(k, "commit.gpgsign")) {
+               opts->gpg_sign = git_config_bool(k, v) ? xstrdup("") : NULL;
+               return 0;
+       }
+
+       status = git_gpg_config(k, v, NULL);
+       if (status)
+               return status;
+
+       return git_diff_basic_config(k, v, NULL);
+}
+
+void sequencer_init_config(struct replay_opts *opts)
+{
+       opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_NONE;
+       git_config(git_sequencer_config, opts);
+}
+
 static inline int is_rebase_i(const struct replay_opts *opts)
 {
        return opts->action == REPLAY_INTERACTIVE_REBASE;
@@ -478,9 +527,6 @@ static int do_recursive_merge(struct commit *base, struct commit *next,
                        _(action_name(opts)));
        rollback_lock_file(&index_lock);
 
-       if (opts->signoff)
-               append_signoff(msgbuf, 0, 0);
-
        if (!clean)
                append_conflicts_hint(msgbuf);
 
@@ -596,6 +642,18 @@ static int read_env_script(struct argv_array *env)
        return 0;
 }
 
+static char *get_author(const char *message)
+{
+       size_t len;
+       const char *a;
+
+       a = find_commit_header(message, "author", &len);
+       if (a)
+               return xmemdupz(a, len);
+
+       return NULL;
+}
+
 static const char staged_changes_advice[] =
 N_("you have staged changes in your working tree\n"
 "If these changes are meant to be squashed into the previous commit, run:\n"
@@ -658,8 +716,6 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts,
                argv_array_push(&cmd.args, "--amend");
        if (opts->gpg_sign)
                argv_array_pushf(&cmd.args, "-S%s", opts->gpg_sign);
-       if (opts->signoff)
-               argv_array_push(&cmd.args, "-s");
        if (defmsg)
                argv_array_pushl(&cmd.args, "-F", defmsg, NULL);
        if ((flags & CLEANUP_MSG))
@@ -694,6 +750,461 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts,
        return run_command(&cmd);
 }
 
+static int rest_is_empty(const struct strbuf *sb, int start)
+{
+       int i, eol;
+       const char *nl;
+
+       /* Check if the rest is just whitespace and Signed-off-by's. */
+       for (i = start; i < sb->len; i++) {
+               nl = memchr(sb->buf + i, '\n', sb->len - i);
+               if (nl)
+                       eol = nl - sb->buf;
+               else
+                       eol = sb->len;
+
+               if (strlen(sign_off_header) <= eol - i &&
+                   starts_with(sb->buf + i, sign_off_header)) {
+                       i = eol;
+                       continue;
+               }
+               while (i < eol)
+                       if (!isspace(sb->buf[i++]))
+                               return 0;
+       }
+
+       return 1;
+}
+
+/*
+ * Find out if the message in the strbuf contains only whitespace and
+ * Signed-off-by lines.
+ */
+int message_is_empty(const struct strbuf *sb,
+                    enum commit_msg_cleanup_mode cleanup_mode)
+{
+       if (cleanup_mode == COMMIT_MSG_CLEANUP_NONE && sb->len)
+               return 0;
+       return rest_is_empty(sb, 0);
+}
+
+/*
+ * See if the user edited the message in the editor or left what
+ * was in the template intact
+ */
+int template_untouched(const struct strbuf *sb, const char *template_file,
+                      enum commit_msg_cleanup_mode cleanup_mode)
+{
+       struct strbuf tmpl = STRBUF_INIT;
+       const char *start;
+
+       if (cleanup_mode == COMMIT_MSG_CLEANUP_NONE && sb->len)
+               return 0;
+
+       if (!template_file || strbuf_read_file(&tmpl, template_file, 0) <= 0)
+               return 0;
+
+       strbuf_stripspace(&tmpl, cleanup_mode == COMMIT_MSG_CLEANUP_ALL);
+       if (!skip_prefix(sb->buf, tmpl.buf, &start))
+               start = sb->buf;
+       strbuf_release(&tmpl);
+       return rest_is_empty(sb, start - sb->buf);
+}
+
+int update_head_with_reflog(const struct commit *old_head,
+                           const struct object_id *new_head,
+                           const char *action, const struct strbuf *msg,
+                           struct strbuf *err)
+{
+       struct ref_transaction *transaction;
+       struct strbuf sb = STRBUF_INIT;
+       const char *nl;
+       int ret = 0;
+
+       if (action) {
+               strbuf_addstr(&sb, action);
+               strbuf_addstr(&sb, ": ");
+       }
+
+       nl = strchr(msg->buf, '\n');
+       if (nl) {
+               strbuf_add(&sb, msg->buf, nl + 1 - msg->buf);
+       } else {
+               strbuf_addbuf(&sb, msg);
+               strbuf_addch(&sb, '\n');
+       }
+
+       transaction = ref_transaction_begin(err);
+       if (!transaction ||
+           ref_transaction_update(transaction, "HEAD", new_head,
+                                  old_head ? &old_head->object.oid : &null_oid,
+                                  0, sb.buf, err) ||
+           ref_transaction_commit(transaction, err)) {
+               ret = -1;
+       }
+       ref_transaction_free(transaction);
+       strbuf_release(&sb);
+
+       return ret;
+}
+
+static int run_rewrite_hook(const struct object_id *oldoid,
+                           const struct object_id *newoid)
+{
+       struct child_process proc = CHILD_PROCESS_INIT;
+       const char *argv[3];
+       int code;
+       struct strbuf sb = STRBUF_INIT;
+
+       argv[0] = find_hook("post-rewrite");
+       if (!argv[0])
+               return 0;
+
+       argv[1] = "amend";
+       argv[2] = NULL;
+
+       proc.argv = argv;
+       proc.in = -1;
+       proc.stdout_to_stderr = 1;
+
+       code = start_command(&proc);
+       if (code)
+               return code;
+       strbuf_addf(&sb, "%s %s\n", oid_to_hex(oldoid), oid_to_hex(newoid));
+       sigchain_push(SIGPIPE, SIG_IGN);
+       write_in_full(proc.in, sb.buf, sb.len);
+       close(proc.in);
+       strbuf_release(&sb);
+       sigchain_pop(SIGPIPE);
+       return finish_command(&proc);
+}
+
+void commit_post_rewrite(const struct commit *old_head,
+                        const struct object_id *new_head)
+{
+       struct notes_rewrite_cfg *cfg;
+
+       cfg = init_copy_notes_for_rewrite("amend");
+       if (cfg) {
+               /* we are amending, so old_head is not NULL */
+               copy_note_for_rewrite(cfg, &old_head->object.oid, new_head);
+               finish_copy_notes_for_rewrite(cfg, "Notes added by 'git commit --amend'");
+       }
+       run_rewrite_hook(&old_head->object.oid, new_head);
+}
+
+static int run_prepare_commit_msg_hook(struct strbuf *msg, const char *commit)
+{
+       struct argv_array hook_env = ARGV_ARRAY_INIT;
+       int ret;
+       const char *name;
+
+       name = git_path_commit_editmsg();
+       if (write_message(msg->buf, msg->len, name, 0))
+               return -1;
+
+       argv_array_pushf(&hook_env, "GIT_INDEX_FILE=%s", get_index_file());
+       argv_array_push(&hook_env, "GIT_EDITOR=:");
+       if (commit)
+               ret = run_hook_le(hook_env.argv, "prepare-commit-msg", name,
+                                 "commit", commit, NULL);
+       else
+               ret = run_hook_le(hook_env.argv, "prepare-commit-msg", name,
+                                 "message", NULL);
+       if (ret)
+               ret = error(_("'prepare-commit-msg' hook failed"));
+       argv_array_clear(&hook_env);
+
+       return ret;
+}
+
+static const char implicit_ident_advice_noconfig[] =
+N_("Your name and email address were configured automatically based\n"
+"on your username and hostname. Please check that they are accurate.\n"
+"You can suppress this message by setting them explicitly. Run the\n"
+"following command and follow the instructions in your editor to edit\n"
+"your configuration file:\n"
+"\n"
+"    git config --global --edit\n"
+"\n"
+"After doing this, you may fix the identity used for this commit with:\n"
+"\n"
+"    git commit --amend --reset-author\n");
+
+static const char implicit_ident_advice_config[] =
+N_("Your name and email address were configured automatically based\n"
+"on your username and hostname. Please check that they are accurate.\n"
+"You can suppress this message by setting them explicitly:\n"
+"\n"
+"    git config --global user.name \"Your Name\"\n"
+"    git config --global user.email you@example.com\n"
+"\n"
+"After doing this, you may fix the identity used for this commit with:\n"
+"\n"
+"    git commit --amend --reset-author\n");
+
+static const char *implicit_ident_advice(void)
+{
+       char *user_config = expand_user_path("~/.gitconfig", 0);
+       char *xdg_config = xdg_config_home("config");
+       int config_exists = file_exists(user_config) || file_exists(xdg_config);
+
+       free(user_config);
+       free(xdg_config);
+
+       if (config_exists)
+               return _(implicit_ident_advice_config);
+       else
+               return _(implicit_ident_advice_noconfig);
+
+}
+
+void print_commit_summary(const char *prefix, const struct object_id *oid,
+                         unsigned int flags)
+{
+       struct rev_info rev;
+       struct commit *commit;
+       struct strbuf format = STRBUF_INIT;
+       const char *head;
+       struct pretty_print_context pctx = {0};
+       struct strbuf author_ident = STRBUF_INIT;
+       struct strbuf committer_ident = STRBUF_INIT;
+
+       commit = lookup_commit(oid);
+       if (!commit)
+               die(_("couldn't look up newly created commit"));
+       if (parse_commit(commit))
+               die(_("could not parse newly created commit"));
+
+       strbuf_addstr(&format, "format:%h] %s");
+
+       format_commit_message(commit, "%an <%ae>", &author_ident, &pctx);
+       format_commit_message(commit, "%cn <%ce>", &committer_ident, &pctx);
+       if (strbuf_cmp(&author_ident, &committer_ident)) {
+               strbuf_addstr(&format, "\n Author: ");
+               strbuf_addbuf_percentquote(&format, &author_ident);
+       }
+       if (flags & SUMMARY_SHOW_AUTHOR_DATE) {
+               struct strbuf date = STRBUF_INIT;
+
+               format_commit_message(commit, "%ad", &date, &pctx);
+               strbuf_addstr(&format, "\n Date: ");
+               strbuf_addbuf_percentquote(&format, &date);
+               strbuf_release(&date);
+       }
+       if (!committer_ident_sufficiently_given()) {
+               strbuf_addstr(&format, "\n Committer: ");
+               strbuf_addbuf_percentquote(&format, &committer_ident);
+               if (advice_implicit_identity) {
+                       strbuf_addch(&format, '\n');
+                       strbuf_addstr(&format, implicit_ident_advice());
+               }
+       }
+       strbuf_release(&author_ident);
+       strbuf_release(&committer_ident);
+
+       init_revisions(&rev, prefix);
+       setup_revisions(0, NULL, &rev, NULL);
+
+       rev.diff = 1;
+       rev.diffopt.output_format =
+               DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_SUMMARY;
+
+       rev.verbose_header = 1;
+       rev.show_root_diff = 1;
+       get_commit_format(format.buf, &rev);
+       rev.always_show_header = 0;
+       rev.diffopt.detect_rename = DIFF_DETECT_RENAME;
+       rev.diffopt.break_opt = 0;
+       diff_setup_done(&rev.diffopt);
+
+       head = resolve_ref_unsafe("HEAD", 0, NULL, NULL);
+       if (!head)
+               die_errno(_("unable to resolve HEAD after creating commit"));
+       if (!strcmp(head, "HEAD"))
+               head = _("detached HEAD");
+       else
+               skip_prefix(head, "refs/heads/", &head);
+       printf("[%s%s ", head, (flags & SUMMARY_INITIAL_COMMIT) ?
+                                               _(" (root-commit)") : "");
+
+       if (!log_tree_commit(&rev, commit)) {
+               rev.always_show_header = 1;
+               rev.use_terminator = 1;
+               log_tree_commit(&rev, commit);
+       }
+
+       strbuf_release(&format);
+}
+
+static int parse_head(struct commit **head)
+{
+       struct commit *current_head;
+       struct object_id oid;
+
+       if (get_oid("HEAD", &oid)) {
+               current_head = NULL;
+       } else {
+               current_head = lookup_commit_reference(&oid);
+               if (!current_head)
+                       return error(_("could not parse HEAD"));
+               if (oidcmp(&oid, &current_head->object.oid)) {
+                       warning(_("HEAD %s is not a commit!"),
+                               oid_to_hex(&oid));
+               }
+               if (parse_commit(current_head))
+                       return error(_("could not parse HEAD commit"));
+       }
+       *head = current_head;
+
+       return 0;
+}
+
+/*
+ * Try to commit without forking 'git commit'. In some cases we need
+ * to run 'git commit' to display an error message
+ *
+ * Returns:
+ *  -1 - error unable to commit
+ *   0 - success
+ *   1 - run 'git commit'
+ */
+static int try_to_commit(struct strbuf *msg, const char *author,
+                        struct replay_opts *opts, unsigned int flags,
+                        struct object_id *oid)
+{
+       struct object_id tree;
+       struct commit *current_head;
+       struct commit_list *parents = NULL;
+       struct commit_extra_header *extra = NULL;
+       struct strbuf err = STRBUF_INIT;
+       struct strbuf commit_msg = STRBUF_INIT;
+       char *amend_author = NULL;
+       const char *hook_commit = NULL;
+       enum commit_msg_cleanup_mode cleanup;
+       int res = 0;
+
+       if (parse_head(&current_head))
+               return -1;
+
+       if (flags & AMEND_MSG) {
+               const char *exclude_gpgsig[] = { "gpgsig", NULL };
+               const char *out_enc = get_commit_output_encoding();
+               const char *message = logmsg_reencode(current_head, NULL,
+                                                     out_enc);
+
+               if (!msg) {
+                       const char *orig_message = NULL;
+
+                       find_commit_subject(message, &orig_message);
+                       msg = &commit_msg;
+                       strbuf_addstr(msg, orig_message);
+                       hook_commit = "HEAD";
+               }
+               author = amend_author = get_author(message);
+               unuse_commit_buffer(current_head, message);
+               if (!author) {
+                       res = error(_("unable to parse commit author"));
+                       goto out;
+               }
+               parents = copy_commit_list(current_head->parents);
+               extra = read_commit_extra_headers(current_head, exclude_gpgsig);
+       } else if (current_head) {
+               commit_list_insert(current_head, &parents);
+       }
+
+       if (write_cache_as_tree(tree.hash, 0, NULL)) {
+               res = error(_("git write-tree failed to write a tree"));
+               goto out;
+       }
+
+       if (!(flags & ALLOW_EMPTY) && !oidcmp(current_head ?
+                                             &current_head->tree->object.oid :
+                                             &empty_tree_oid, &tree)) {
+               res = 1; /* run 'git commit' to display error message */
+               goto out;
+       }
+
+       if (find_hook("prepare-commit-msg")) {
+               res = run_prepare_commit_msg_hook(msg, hook_commit);
+               if (res)
+                       goto out;
+               if (strbuf_read_file(&commit_msg, git_path_commit_editmsg(),
+                                    2048) < 0) {
+                       res = error_errno(_("unable to read commit message "
+                                             "from '%s'"),
+                                           git_path_commit_editmsg());
+                       goto out;
+               }
+               msg = &commit_msg;
+       }
+
+       cleanup = (flags & CLEANUP_MSG) ? COMMIT_MSG_CLEANUP_ALL :
+                                         opts->default_msg_cleanup;
+
+       if (cleanup != COMMIT_MSG_CLEANUP_NONE)
+               strbuf_stripspace(msg, cleanup == COMMIT_MSG_CLEANUP_ALL);
+       if (!opts->allow_empty_message && message_is_empty(msg, cleanup)) {
+               res = 1; /* run 'git commit' to display error message */
+               goto out;
+       }
+
+       if (commit_tree_extended(msg->buf, msg->len, tree.hash, parents,
+                                oid->hash, author, opts->gpg_sign, extra)) {
+               res = error(_("failed to write commit object"));
+               goto out;
+       }
+
+       if (update_head_with_reflog(current_head, oid,
+                                   getenv("GIT_REFLOG_ACTION"), msg, &err)) {
+               res = error("%s", err.buf);
+               goto out;
+       }
+
+       if (flags & AMEND_MSG)
+               commit_post_rewrite(current_head, oid);
+
+out:
+       free_commit_extra_headers(extra);
+       strbuf_release(&err);
+       strbuf_release(&commit_msg);
+       free(amend_author);
+
+       return res;
+}
+
+static int do_commit(const char *msg_file, const char *author,
+                    struct replay_opts *opts, unsigned int flags)
+{
+       int res = 1;
+
+       if (!(flags & EDIT_MSG) && !(flags & VERIFY_MSG)) {
+               struct object_id oid;
+               struct strbuf sb = STRBUF_INIT;
+
+               if (msg_file && strbuf_read_file(&sb, msg_file, 2048) < 0)
+                       return error_errno(_("unable to read commit message "
+                                            "from '%s'"),
+                                          msg_file);
+
+               res = try_to_commit(msg_file ? &sb : NULL, author, opts, flags,
+                                   &oid);
+               strbuf_release(&sb);
+               if (!res) {
+                       unlink(git_path_cherry_pick_head());
+                       unlink(git_path_merge_msg());
+                       if (!is_rebase_i(opts))
+                               print_commit_summary(NULL, &oid,
+                                               SUMMARY_SHOW_AUTHOR_DATE);
+                       return res;
+               }
+       }
+       if (res == 1)
+               return run_git_commit(msg_file, opts, flags);
+
+       return res;
+}
+
 static int is_original_commit_empty(struct commit *commit)
 {
        const struct object_id *ptree_oid;
@@ -952,6 +1463,7 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
        struct object_id head;
        struct commit *base, *next, *parent;
        const char *base_label, *next_label;
+       char *author = NULL;
        struct commit_message msg = { NULL, NULL, NULL, NULL };
        struct strbuf msgbuf = STRBUF_INIT;
        int res, unborn = 0, allow;
@@ -1066,6 +1578,8 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
                        strbuf_addstr(&msgbuf, oid_to_hex(&commit->object.oid));
                        strbuf_addstr(&msgbuf, ")\n");
                }
+               if (!is_fixup(command))
+                       author = get_author(msg.message);
        }
 
        if (command == TODO_REWORD)
@@ -1091,6 +1605,9 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
                }
        }
 
+       if (opts->signoff)
+               append_signoff(&msgbuf, 0, 0);
+
        if (is_rebase_i(opts) && write_author_script(msg.message) < 0)
                res = -1;
        else if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) {
@@ -1148,9 +1665,13 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
                goto leave;
        } else if (allow)
                flags |= ALLOW_EMPTY;
-       if (!opts->no_commit)
+       if (!opts->no_commit) {
 fast_forward_edit:
-               res = run_git_commit(msg_file, opts, flags);
+               if (author || command == TODO_REVERT || (flags & AMEND_MSG))
+                       res = do_commit(msg_file, author, opts, flags);
+               else
+                       res = error(_("unable to parse commit author"));
+       }
 
        if (!res && final_fixup) {
                unlink(rebase_path_fixup_msg());
@@ -1159,6 +1680,7 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
 
 leave:
        free_message(commit, &msg);
+       free(author);
        update_abort_safety_file();
 
        return res;
@@ -2586,7 +3108,10 @@ int transform_todos(unsigned flags)
                        strbuf_addf(&buf, " %s", oid);
                }
                /* add all the rest */
-               strbuf_addf(&buf, " %.*s\n", item->arg_len, item->arg);
+               if (!item->arg_len)
+                       strbuf_addch(&buf, '\n');
+               else
+                       strbuf_addf(&buf, " %.*s\n", item->arg_len, item->arg);
        }
 
        i = write_message(buf.buf, buf.len, todo_file, 0);