Merge branch 'pw/rebase-keep-empty-fixes' into pw/rebase-signoff
authorJunio C Hamano <gitster@pobox.com>
Thu, 29 Mar 2018 18:08:09 +0000 (11:08 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 29 Mar 2018 18:08:09 +0000 (11:08 -0700)
* pw/rebase-keep-empty-fixes:
rebase: respect --no-keep-empty
rebase -i --keep-empty: don't prune empty commits
rebase --root: stop assuming squash_onto is unset
Git 2.16.2

1  2 
sequencer.c
diff --combined sequencer.c
index 5bfdc4044233d5f809f9f1fbc55ebe3da477e3f0,1a10766492e8772e59b8fd569e4d36bfafeee03d..45aa7e307276a61172676e9e5dcf771b97d8de5d
@@@ -1,10 -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")
@@@ -134,51 -130,6 +134,51 @@@ static GIT_PATH_FUNC(rebase_path_strate
  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;
@@@ -527,6 -478,9 +527,6 @@@ static int do_recursive_merge(struct co
                        _(action_name(opts)));
        rollback_lock_file(&index_lock);
  
 -      if (opts->signoff)
 -              append_signoff(msgbuf, 0, 0);
 -
        if (!clean)
                append_conflicts_hint(msgbuf);
  
@@@ -642,18 -596,6 +642,18 @@@ static int read_env_script(struct argv_
        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"
@@@ -716,6 -658,8 +716,6 @@@ static int run_git_commit(const char *d
                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))
        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;
@@@ -1463,7 -952,6 +1463,7 @@@ static int do_pick_commit(enum todo_com
        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;
                        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)
                }
        }
  
 +      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) {
                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());
  
  leave:
        free_message(commit, &msg);
 +      free(author);
        update_abort_safety_file();
  
        return res;
@@@ -2992,7 -2470,7 +2992,7 @@@ int sequencer_make_script(FILE *out, in
        init_revisions(&revs, NULL);
        revs.verbose_header = 1;
        revs.max_parents = 1;
-       revs.cherry_pick = 1;
+       revs.cherry_mark = 1;
        revs.limited = 1;
        revs.reverse = 1;
        revs.right_only = 1;
                return error(_("make_script: error preparing revisions"));
  
        while ((commit = get_revision(&revs))) {
+               int is_empty  = is_original_commit_empty(commit);
+               if (!is_empty && (commit->object.flags & PATCHSAME))
+                       continue;
                strbuf_reset(&buf);
-               if (!keep_empty && is_original_commit_empty(commit))
+               if (!keep_empty && is_empty)
                        strbuf_addf(&buf, "%c ", comment_line_char);
                strbuf_addf(&buf, "%s %s ", insn,
                            oid_to_hex(&commit->object.oid));