Merge branch 'js/rebase-i-shortopt'
authorJunio C Hamano <gitster@pobox.com>
Fri, 2 Nov 2018 02:04:59 +0000 (11:04 +0900)
committerJunio C Hamano <gitster@pobox.com>
Fri, 2 Nov 2018 02:04:59 +0000 (11:04 +0900)
"git rebase -i" learned to take 'b' as the short form of 'break'
option in the todo list.

* js/rebase-i-shortopt:
rebase -i: recognize short commands without arguments

1  2 
sequencer.c
t/lib-rebase.sh
t/t3418-rebase-continue.sh
diff --combined sequencer.c
index 73efa92da837c895980ce2d101c190218b6a7204,3107f59ea700bdb37e4d32bb035bf959aa6b3497..9e1ab3a2a7e3f65cc444cded7f3330bb27be2db8
@@@ -30,7 -30,6 +30,7 @@@
  #include "oidset.h"
  #include "commit-slab.h"
  #include "alias.h"
 +#include "commit-reach.h"
  #include "rebase-interactive.h"
  
  #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@@@ -68,12 -67,12 +68,12 @@@ static GIT_PATH_FUNC(rebase_path_done, 
   * The file to keep track of how many commands were already processed (e.g.
   * for the prompt).
   */
 -static GIT_PATH_FUNC(rebase_path_msgnum, "rebase-merge/msgnum");
 +static GIT_PATH_FUNC(rebase_path_msgnum, "rebase-merge/msgnum")
  /*
   * The file to keep track of how many commands are to be processed in total
   * (e.g. for the prompt).
   */
 -static GIT_PATH_FUNC(rebase_path_msgtotal, "rebase-merge/end");
 +static GIT_PATH_FUNC(rebase_path_msgtotal, "rebase-merge/end")
  /*
   * The commit message that is planned to be used for any changes that
   * need to be committed following a user interaction.
@@@ -231,16 -230,13 +231,16 @@@ static const char *get_todo_path(const 
   * Returns 3 when sob exists within conforming footer as last entry
   */
  static int has_conforming_footer(struct strbuf *sb, struct strbuf *sob,
 -      int ignore_footer)
 +      size_t ignore_footer)
  {
 +      struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
        struct trailer_info info;
 -      int i;
 +      size_t i;
        int found_sob = 0, found_sob_last = 0;
  
 -      trailer_info_get(&info, sb->buf);
 +      opts.no_divider = 1;
 +
 +      trailer_info_get(&info, sb->buf, &opts);
  
        if (info.trailer_start == info.trailer_end)
                return 0;
@@@ -316,7 -312,7 +316,7 @@@ static const char *action_name(const st
        case REPLAY_INTERACTIVE_REBASE:
                return N_("rebase -i");
        }
 -      die(_("Unknown action: %d"), opts->action);
 +      die(_("unknown action: %d"), opts->action);
  }
  
  struct commit_message {
@@@ -442,7 -438,7 +442,7 @@@ static int read_oneliner(struct strbuf 
  
  static struct tree *empty_tree(void)
  {
 -      return lookup_tree(the_hash_algo->empty_tree);
 +      return lookup_tree(the_repository, the_repository->hash_algo->empty_tree);
  }
  
  static int error_dirty_index(struct replay_opts *opts)
@@@ -479,8 -475,8 +479,8 @@@ static int fast_forward_to(const struc
        struct strbuf sb = STRBUF_INIT;
        struct strbuf err = STRBUF_INIT;
  
 -      read_cache();
 -      if (checkout_fast_forward(from, to, 1))
 +      read_index(&the_index);
 +      if (checkout_fast_forward(the_repository, from, to, 1))
                return -1; /* the callee should have complained already */
  
        strbuf_addf(&sb, _("%s: fast-forward"), _(action_name(opts)));
@@@ -603,7 -599,7 +603,7 @@@ static int is_index_unchanged(void
        if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL))
                return error(_("could not resolve HEAD commit"));
  
 -      head_commit = lookup_commit(&head_oid);
 +      head_commit = lookup_commit(the_repository, &head_oid);
  
        /*
         * If head_commit is NULL, check_commit, called from
        if (!(cache_tree_oid = get_cache_tree_oid()))
                return -1;
  
 -      return !oidcmp(cache_tree_oid, get_commit_tree_oid(head_commit));
 +      return oideq(cache_tree_oid, get_commit_tree_oid(head_commit));
  }
  
  static int write_author_script(const char *message)
@@@ -648,7 -644,7 +648,7 @@@ missing_author
                else if (*message != '\'')
                        strbuf_addch(&buf, *(message++));
                else
 -                      strbuf_addf(&buf, "'\\\\%c'", *(message++));
 +                      strbuf_addf(&buf, "'\\%c'", *(message++));
        strbuf_addstr(&buf, "'\nGIT_AUTHOR_EMAIL='");
        while (*message && *message != '\n' && *message != '\r')
                if (skip_prefix(message, "> ", &message))
                else if (*message != '\'')
                        strbuf_addch(&buf, *(message++));
                else
 -                      strbuf_addf(&buf, "'\\\\%c'", *(message++));
 +                      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++));
 +                      strbuf_addf(&buf, "'\\%c'", *(message++));
 +      strbuf_addch(&buf, '\'');
        res = write_message(buf.buf, buf.len, rebase_path_author_script(), 1);
        strbuf_release(&buf);
        return res;
  }
  
 +
 +/*
 + * write_author_script() used to fail to terminate the last line with a "'" and
 + * also escaped "'" incorrectly as "'\\\\''" rather than "'\\''". We check for
 + * the terminating "'" on the last line to see how "'" has been escaped in case
 + * git was upgraded while rebase was stopped.
 + */
 +static int quoting_is_broken(const char *s, size_t n)
 +{
 +      /* Skip any empty lines in case the file was hand edited */
 +      while (n > 0 && s[--n] == '\n')
 +              ; /* empty */
 +      if (n > 0 && s[n] != '\'')
 +              return 1;
 +
 +      return 0;
 +}
 +
  /*
   * Read a list of environment variable assignments (such as the author-script
   * file) into an environment block. Returns -1 on error, 0 otherwise.
  static int read_env_script(struct argv_array *env)
  {
        struct strbuf script = STRBUF_INIT;
 -      int i, count = 0;
 -      char *p, *p2;
 +      int i, count = 0, sq_bug;
 +      const char *p2;
 +      char *p;
  
        if (strbuf_read_file(&script, rebase_path_author_script(), 256) <= 0)
                return -1;
 -
 +      /* write_author_script() used to quote incorrectly */
 +      sq_bug = quoting_is_broken(script.buf, script.len);
        for (p = script.buf; *p; p++)
 -              if (skip_prefix(p, "'\\\\''", (const char **)&p2))
 +              if (sq_bug && skip_prefix(p, "'\\\\''", &p2))
 +                      strbuf_splice(&script, p - script.buf, p2 - p, "'", 1);
 +              else if (skip_prefix(p, "'\\''", &p2))
                        strbuf_splice(&script, p - script.buf, p2 - p, "'", 1);
                else if (*p == '\'')
                        strbuf_splice(&script, p-- - script.buf, 1, "", 0);
@@@ -740,51 -713,43 +740,51 @@@ static const char *read_author_ident(st
        const char *keys[] = {
                "GIT_AUTHOR_NAME=", "GIT_AUTHOR_EMAIL=", "GIT_AUTHOR_DATE="
        };
 -      char *in, *out, *eol;
 -      int i = 0, len;
 +      struct strbuf out = STRBUF_INIT;
 +      char *in, *eol;
 +      const char *val[3];
 +      int i = 0;
  
        if (strbuf_read_file(buf, rebase_path_author_script(), 256) <= 0)
                return NULL;
  
        /* dequote values and construct ident line in-place */
 -      for (in = out = buf->buf; i < 3 && in - buf->buf < buf->len; i++) {
 +      for (in = buf->buf; i < 3 && in - buf->buf < buf->len; i++) {
                if (!skip_prefix(in, keys[i], (const char **)&in)) {
 -                      warning("could not parse '%s' (looking for '%s'",
 +                      warning(_("could not parse '%s' (looking for '%s')"),
                                rebase_path_author_script(), keys[i]);
                        return NULL;
                }
  
                eol = strchrnul(in, '\n');
                *eol = '\0';
 -              sq_dequote(in);
 -              len = strlen(in);
 -
 -              if (i > 0) /* separate values by spaces */
 -                      *(out++) = ' ';
 -              if (i == 1) /* email needs to be surrounded by <...> */
 -                      *(out++) = '<';
 -              memmove(out, in, len);
 -              out += len;
 -              if (i == 1) /* email needs to be surrounded by <...> */
 -                      *(out++) = '>';
 +              if (!sq_dequote(in)) {
 +                      warning(_("bad quoting on %s value in '%s'"),
 +                              keys[i], rebase_path_author_script());
 +                      return NULL;
 +              }
 +              val[i] = in;
                in = eol + 1;
        }
  
        if (i < 3) {
 -              warning("could not parse '%s' (looking for '%s')",
 +              warning(_("could not parse '%s' (looking for '%s')"),
                        rebase_path_author_script(), keys[i]);
                return NULL;
        }
  
 -      buf->len = out - buf->buf;
 +      /* validate date since fmt_ident() will die() on bad value */
 +      if (parse_date(val[2], &out)){
 +              warning(_("invalid date format '%s' in '%s'"),
 +                      val[2], rebase_path_author_script());
 +              strbuf_release(&out);
 +              return NULL;
 +      }
 +
 +      strbuf_reset(&out);
 +      strbuf_addstr(&out, fmt_ident(val[0], val[1], val[2], 0));
 +      strbuf_swap(buf, &out);
 +      strbuf_release(&out);
        return buf->buf;
  }
  
@@@ -846,18 -811,11 +846,18 @@@ static int run_git_commit(const char *d
  
        if ((flags & CREATE_ROOT_COMMIT) && !(flags & AMEND_MSG)) {
                struct strbuf msg = STRBUF_INIT, script = STRBUF_INIT;
 -              const char *author = is_rebase_i(opts) ?
 -                      read_author_ident(&script) : NULL;
 +              const char *author = NULL;
                struct object_id root_commit, *cache_tree_oid;
                int res = 0;
  
 +              if (is_rebase_i(opts)) {
 +                      author = read_author_ident(&script);
 +                      if (!author) {
 +                              strbuf_release(&script);
 +                              return -1;
 +                      }
 +              }
 +
                if (!defmsg)
                        BUG("root commit without message");
  
        if ((flags & ALLOW_EMPTY))
                argv_array_push(&cmd.args, "--allow-empty");
  
 -      if (opts->allow_empty_message)
 +      if (!(flags & EDIT_MSG))
                argv_array_push(&cmd.args, "--allow-empty-message");
  
        if (is_rebase_i(opts) && !(flags & EDIT_MSG))
@@@ -1147,7 -1105,7 +1147,7 @@@ void print_commit_summary(const char *p
        struct strbuf author_ident = STRBUF_INIT;
        struct strbuf committer_ident = STRBUF_INIT;
  
 -      commit = lookup_commit(oid);
 +      commit = lookup_commit(the_repository, oid);
        if (!commit)
                die(_("couldn't look up newly created commit"));
        if (parse_commit(commit))
        strbuf_release(&author_ident);
        strbuf_release(&committer_ident);
  
 -      init_revisions(&rev, prefix);
 +      repo_init_revisions(the_repository, &rev, prefix);
        setup_revisions(0, NULL, &rev, NULL);
  
        rev.diff = 1;
@@@ -1222,10 -1180,10 +1222,10 @@@ static int parse_head(struct commit **h
        if (get_oid("HEAD", &oid)) {
                current_head = NULL;
        } else {
 -              current_head = lookup_commit_reference(&oid);
 +              current_head = lookup_commit_reference(the_repository, &oid);
                if (!current_head)
                        return error(_("could not parse HEAD"));
 -              if (oidcmp(&oid, &current_head->object.oid)) {
 +              if (!oideq(&oid, &current_head->object.oid)) {
                        warning(_("HEAD %s is not a commit!"),
                                oid_to_hex(&oid));
                }
@@@ -1290,14 -1248,14 +1290,14 @@@ static int try_to_commit(struct strbuf 
                commit_list_insert(current_head, &parents);
        }
  
 -      if (write_cache_as_tree(&tree, 0, NULL)) {
 +      if (write_index_as_tree(&tree, &the_index, get_index_file(), 0, NULL)) {
                res = error(_("git write-tree failed to write a tree"));
                goto out;
        }
  
 -      if (!(flags & ALLOW_EMPTY) && !oidcmp(current_head ?
 -                                            get_commit_tree_oid(current_head) :
 -                                            the_hash_algo->empty_tree, &tree)) {
 +      if (!(flags & ALLOW_EMPTY) && oideq(current_head ?
 +                                          get_commit_tree_oid(current_head) :
 +                                          the_hash_algo->empty_tree, &tree)) {
                res = 1; /* run 'git commit' to display error message */
                goto out;
        }
  
        if (cleanup != COMMIT_MSG_CLEANUP_NONE)
                strbuf_stripspace(msg, cleanup == COMMIT_MSG_CLEANUP_ALL);
 -      if (!opts->allow_empty_message && message_is_empty(msg, cleanup)) {
 +      if ((flags & EDIT_MSG) && message_is_empty(msg, cleanup)) {
                res = 1; /* run 'git commit' to display error message */
                goto out;
        }
@@@ -1402,7 -1360,7 +1402,7 @@@ static int is_original_commit_empty(str
                ptree_oid = the_hash_algo->empty_tree; /* commit is root */
        }
  
 -      return !oidcmp(ptree_oid, get_commit_tree_oid(commit));
 +      return oideq(ptree_oid, get_commit_tree_oid(commit));
  }
  
  /*
@@@ -1493,7 -1451,7 +1493,7 @@@ static const char *command_to_string(co
  {
        if (command < TODO_COMMENT)
                return todo_command_info[command].str;
 -      die("Unknown command: %d", command);
 +      die(_("unknown command: %d"), command);
  }
  
  static char command_to_char(const enum todo_command command)
@@@ -1559,7 -1517,7 +1559,7 @@@ static int update_squash_messages(enum 
  
                if (get_oid("HEAD", &head))
                        return error(_("need a HEAD to fixup"));
 -              if (!(head_commit = lookup_commit_reference(&head)))
 +              if (!(head_commit = lookup_commit_reference(the_repository, &head)))
                        return error(_("could not read HEAD"));
                if (!(head_message = get_commit_buffer(head_commit, NULL)))
                        return error(_("could not read HEAD's commit message"));
                unlink(rebase_path_fixup_msg());
                strbuf_addf(&buf, "\n%c ", comment_line_char);
                strbuf_addf(&buf, _("This is the commit message #%d:"),
 -                          ++opts->current_fixup_count);
 +                          ++opts->current_fixup_count + 1);
                strbuf_addstr(&buf, "\n\n");
                strbuf_addstr(&buf, body);
        } else if (command == TODO_FIXUP) {
                strbuf_addf(&buf, "\n%c ", comment_line_char);
                strbuf_addf(&buf, _("The commit message #%d will be skipped:"),
 -                          ++opts->current_fixup_count);
 +                          ++opts->current_fixup_count + 1);
                strbuf_addstr(&buf, "\n\n");
                strbuf_add_commented_lines(&buf, body, strlen(body));
        } else
@@@ -1678,13 -1636,13 +1678,13 @@@ static int do_pick_commit(enum todo_com
                 * that represents the "current" state for merge-recursive
                 * to work on.
                 */
 -              if (write_cache_as_tree(&head, 0, NULL))
 +              if (write_index_as_tree(&head, &the_index, get_index_file(), 0, NULL))
                        return error(_("your index file is unmerged."));
        } else {
                unborn = get_oid("HEAD", &head);
                /* Do we want to generate a root commit? */
                if (is_pick_or_similar(command) && opts->have_squash_onto &&
 -                  !oidcmp(&head, &opts->squash_onto)) {
 +                  oideq(&head, &opts->squash_onto)) {
                        if (is_fixup(command))
                                return error(_("cannot fixup root commit"));
                        flags |= CREATE_ROOT_COMMIT;
                        oid_to_hex(&commit->object.oid));
  
        if (opts->allow_ff && !is_fixup(command) &&
 -          ((parent && !oidcmp(&parent->object.oid, &head)) ||
 +          ((parent && oideq(&parent->object.oid, &head)) ||
             (!parent && unborn))) {
                if (is_rebase_i(opts))
                        write_author_script(msg.message);
  
                commit_list_insert(base, &common);
                commit_list_insert(next, &remotes);
 -              res |= try_merge_command(opts->strategy,
 +              res |= try_merge_command(the_repository, opts->strategy,
                                         opts->xopts_nr, (const char **)opts->xopts,
                                        common, oid_to_hex(&head), remotes);
                free_commit_list(common);
                      : _("could not apply %s... %s"),
                      short_commit_name(commit), msg.subject);
                print_advice(res == 1, opts);
 -              rerere(opts->allow_rerere_auto);
 +              repo_rerere(the_repository, opts->allow_rerere_auto);
                goto leave;
        }
  
@@@ -1912,6 -1870,8 +1912,6 @@@ static int prepare_revs(struct replay_o
        if (prepare_revision_walk(opts->revs))
                return error(_("revision walk setup failed"));
  
 -      if (!opts->revs->commits)
 -              return error(_("empty commit set passed"));
        return 0;
  }
  
@@@ -1919,7 -1879,7 +1919,7 @@@ static int read_and_refresh_cache(struc
  {
        struct lock_file index_lock = LOCK_INIT;
        int index_fd = hold_locked_index(&index_lock, 0);
 -      if (read_index_preload(&the_index, NULL) < 0) {
 +      if (read_index_preload(&the_index, NULL, 0) < 0) {
                rollback_lock_file(&index_lock);
                return error(_("git %s: failed to read the index"),
                        _(action_name(opts)));
@@@ -1994,7 -1954,8 +1994,8 @@@ static int parse_insn_line(struct todo_
                if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
                        item->command = i;
                        break;
-               } else if (bol[1] == ' ' && *bol == todo_command_info[i].c) {
+               } else if ((bol + 1 == eol || bol[1] == ' ') &&
+                          *bol == todo_command_info[i].c) {
                        bol++;
                        item->command = i;
                        break;
        if (status < 0)
                return -1;
  
 -      item->commit = lookup_commit_reference(&commit_oid);
 +      item->commit = lookup_commit_reference(the_repository, &commit_oid);
        return !item->commit;
  }
  
@@@ -2377,7 -2338,7 +2378,7 @@@ int write_basic_state(struct replay_opt
                write_file(rebase_path_quiet(), "\n");
  
        if (opts->verbose)
 -              write_file(rebase_path_verbose(), "");
 +              write_file(rebase_path_verbose(), "%s", "");
        if (opts->strategy)
                write_file(rebase_path_strategy(), "%s\n", opts->strategy);
        if (opts->xopts_nr > 0)
@@@ -2423,10 -2384,6 +2424,10 @@@ static int walk_revs_populate_todo(stru
                        short_commit_name(commit), subject_len, subject);
                unuse_commit_buffer(commit, commit_buffer);
        }
 +
 +      if (!todo_list->nr)
 +              return error(_("empty commit set passed"));
 +
        return 0;
  }
  
@@@ -2486,7 -2443,7 +2487,7 @@@ static int rollback_is_safe(void
        if (get_oid("HEAD", &actual_head))
                oidclr(&actual_head);
  
 -      return !oidcmp(&actual_head, &expected_head);
 +      return oideq(&actual_head, &expected_head);
  }
  
  static int reset_for_rollback(const struct object_id *oid)
@@@ -2659,7 -2616,7 +2660,7 @@@ static int make_patch(struct commit *co
  
        strbuf_addf(&buf, "%s/patch", get_dir(opts));
        memset(&log_tree_opt, 0, sizeof(log_tree_opt));
 -      init_revisions(&log_tree_opt, NULL);
 +      repo_init_revisions(the_repository, &log_tree_opt, NULL);
        log_tree_opt.abbrev = 0;
        log_tree_opt.diff = 1;
        log_tree_opt.diffopt.output_format = DIFF_FORMAT_PATCH;
@@@ -2703,39 -2660,23 +2704,39 @@@ static int error_with_patch(struct comm
        const char *subject, int subject_len,
        struct replay_opts *opts, int exit_code, int to_amend)
  {
 -      if (make_patch(commit, opts))
 -              return -1;
 +      if (commit) {
 +              if (make_patch(commit, opts))
 +                      return -1;
 +      } else if (copy_file(rebase_path_message(),
 +                           git_path_merge_msg(the_repository), 0666))
 +              return error(_("unable to copy '%s' to '%s'"),
 +                           git_path_merge_msg(the_repository), rebase_path_message());
  
        if (to_amend) {
                if (intend_to_amend())
                        return -1;
  
 -              fprintf(stderr, "You can amend the commit now, with\n"
 -                      "\n"
 -                      "  git commit --amend %s\n"
 -                      "\n"
 -                      "Once you are satisfied with your changes, run\n"
 -                      "\n"
 -                      "  git rebase --continue\n", gpg_sign_opt_quoted(opts));
 -      } else if (exit_code)
 -              fprintf(stderr, "Could not apply %s... %.*s\n",
 -                      short_commit_name(commit), subject_len, subject);
 +              fprintf(stderr,
 +                      _("You can amend the commit now, with\n"
 +                        "\n"
 +                        "  git commit --amend %s\n"
 +                        "\n"
 +                        "Once you are satisfied with your changes, run\n"
 +                        "\n"
 +                        "  git rebase --continue\n"),
 +                      gpg_sign_opt_quoted(opts));
 +      } else if (exit_code) {
 +              if (commit)
 +                      fprintf_ln(stderr, _("Could not apply %s... %.*s"),
 +                                 short_commit_name(commit), subject_len, subject);
 +              else
 +                      /*
 +                       * We don't have the hash of the parent so
 +                       * just print the line from the todo file.
 +                       */
 +                      fprintf_ln(stderr, _("Could not merge %.*s"),
 +                                 subject_len, subject);
 +      }
  
        return exit_code;
  }
@@@ -2763,8 -2704,6 +2764,8 @@@ static int do_exec(const char *command_
        fprintf(stderr, "Executing: %s\n", command_line);
        child_argv[0] = command_line;
        argv_array_pushf(&child_env, "GIT_DIR=%s", absolute_path(get_git_dir()));
 +      argv_array_pushf(&child_env, "GIT_WORK_TREE=%s",
 +                       absolute_path(get_git_work_tree()));
        status = run_command_v_opt_cd_env(child_argv, RUN_USING_SHELL, NULL,
                                          child_env.argv);
  
@@@ -2848,7 -2787,7 +2849,7 @@@ static int do_label(const char *name, i
        struct object_id head_oid;
  
        if (len == 1 && *name == '#')
 -              return error("Illegal label name: '%.*s'", len, name);
 +              return error(_("illegal label name: '%.*s'"), len, name);
  
        strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
        strbuf_addf(&msg, "rebase -i (label) '%.*s'", len, name);
@@@ -2970,26 -2909,6 +2971,26 @@@ static int do_reset(const char *name, i
        return ret;
  }
  
 +static struct commit *lookup_label(const char *label, int len,
 +                                 struct strbuf *buf)
 +{
 +      struct commit *commit;
 +
 +      strbuf_reset(buf);
 +      strbuf_addf(buf, "refs/rewritten/%.*s", len, label);
 +      commit = lookup_commit_reference_by_name(buf->buf);
 +      if (!commit) {
 +              /* fall back to non-rewritten ref or commit */
 +              strbuf_splice(buf, 0, strlen("refs/rewritten/"), "", 0);
 +              commit = lookup_commit_reference_by_name(buf->buf);
 +      }
 +
 +      if (!commit)
 +              error(_("could not resolve '%s'"), buf->buf);
 +
 +      return commit;
 +}
 +
  static int do_merge(struct commit *commit, const char *arg, int arg_len,
                    int flags, struct replay_opts *opts)
  {
        struct strbuf ref_name = STRBUF_INIT;
        struct commit *head_commit, *merge_commit, *i;
        struct commit_list *bases, *j, *reversed = NULL;
 +      struct commit_list *to_merge = NULL, **tail = &to_merge;
        struct merge_options o;
 -      int merge_arg_len, oneline_offset, can_fast_forward, ret;
 +      int merge_arg_len, oneline_offset, can_fast_forward, ret, k;
        static struct lock_file lock;
        const char *p;
  
                goto leave_merge;
        }
  
 -      oneline_offset = arg_len;
 -      merge_arg_len = strcspn(arg, " \t\n");
 -      p = arg + merge_arg_len;
 -      p += strspn(p, " \t\n");
 -      if (*p == '#' && (!p[1] || isspace(p[1]))) {
 -              p += 1 + strspn(p + 1, " \t\n");
 -              oneline_offset = p - arg;
 -      } else if (p - arg < arg_len)
 -              BUG("octopus merges are not supported yet: '%s'", p);
 -
 -      strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
 -      merge_commit = lookup_commit_reference_by_name(ref_name.buf);
 -      if (!merge_commit) {
 -              /* fall back to non-rewritten ref or commit */
 -              strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
 -              merge_commit = lookup_commit_reference_by_name(ref_name.buf);
 +      /*
 +       * For octopus merges, the arg starts with the list of revisions to be
 +       * merged. The list is optionally followed by '#' and the oneline.
 +       */
 +      merge_arg_len = oneline_offset = arg_len;
 +      for (p = arg; p - arg < arg_len; p += strspn(p, " \t\n")) {
 +              if (!*p)
 +                      break;
 +              if (*p == '#' && (!p[1] || isspace(p[1]))) {
 +                      p += 1 + strspn(p + 1, " \t\n");
 +                      oneline_offset = p - arg;
 +                      break;
 +              }
 +              k = strcspn(p, " \t\n");
 +              if (!k)
 +                      continue;
 +              merge_commit = lookup_label(p, k, &ref_name);
 +              if (!merge_commit) {
 +                      ret = error(_("unable to parse '%.*s'"), k, p);
 +                      goto leave_merge;
 +              }
 +              tail = &commit_list_insert(merge_commit, tail)->next;
 +              p += k;
 +              merge_arg_len = p - arg;
        }
  
 -      if (!merge_commit) {
 -              ret = error(_("could not resolve '%s'"), ref_name.buf);
 +      if (!to_merge) {
 +              ret = error(_("nothing to merge: '%.*s'"), arg_len, arg);
                goto leave_merge;
        }
  
        if (opts->have_squash_onto &&
 -          !oidcmp(&head_commit->object.oid, &opts->squash_onto)) {
 +          oideq(&head_commit->object.oid, &opts->squash_onto)) {
                /*
                 * When the user tells us to "merge" something into a
                 * "[new root]", let's simply fast-forward to the merge head.
                 */
                rollback_lock_file(&lock);
 -              ret = fast_forward_to(&merge_commit->object.oid,
 -                                     &head_commit->object.oid, 0, opts);
 +              if (to_merge->next)
 +                      ret = error(_("octopus merge cannot be executed on "
 +                                    "top of a [new root]"));
 +              else
 +                      ret = fast_forward_to(&to_merge->item->object.oid,
 +                                            &head_commit->object.oid, 0,
 +                                            opts);
                goto leave_merge;
        }
  
                        p = arg + oneline_offset;
                        len = arg_len - oneline_offset;
                } else {
 -                      strbuf_addf(&buf, "Merge branch '%.*s'",
 +                      strbuf_addf(&buf, "Merge %s '%.*s'",
 +                                  to_merge->next ? "branches" : "branch",
                                    merge_arg_len, arg);
                        p = buf.buf;
                        len = buf.len;
         * commit, we cannot fast-forward.
         */
        can_fast_forward = opts->allow_ff && commit && commit->parents &&
 -              !oidcmp(&commit->parents->item->object.oid,
 -                      &head_commit->object.oid);
 +              oideq(&commit->parents->item->object.oid,
 +                    &head_commit->object.oid);
  
        /*
 -       * If the merge head is different from the original one, we cannot
 +       * If any merge head is different from the original one, we cannot
         * fast-forward.
         */
        if (can_fast_forward) {
 -              struct commit_list *second_parent = commit->parents->next;
 +              struct commit_list *p = commit->parents->next;
  
 -              if (second_parent && !second_parent->next &&
 -                  oidcmp(&merge_commit->object.oid,
 -                         &second_parent->item->object.oid))
 +              for (j = to_merge; j && p; j = j->next, p = p->next)
 +                      if (!oideq(&j->item->object.oid,
 +                                 &p->item->object.oid)) {
 +                              can_fast_forward = 0;
 +                              break;
 +                      }
 +              /*
 +               * If the number of merge heads differs from the original merge
 +               * commit, we cannot fast-forward.
 +               */
 +              if (j || p)
                        can_fast_forward = 0;
        }
  
 -      if (can_fast_forward && commit->parents->next &&
 -          !commit->parents->next->next &&
 -          !oidcmp(&commit->parents->next->item->object.oid,
 -                  &merge_commit->object.oid)) {
 +      if (can_fast_forward) {
                rollback_lock_file(&lock);
                ret = fast_forward_to(&commit->object.oid,
                                      &head_commit->object.oid, 0, opts);
                goto leave_merge;
        }
  
 +      if (to_merge->next) {
 +              /* Octopus merge */
 +              struct child_process cmd = CHILD_PROCESS_INIT;
 +
 +              if (read_env_script(&cmd.env_array)) {
 +                      const char *gpg_opt = gpg_sign_opt_quoted(opts);
 +
 +                      ret = error(_(staged_changes_advice), gpg_opt, gpg_opt);
 +                      goto leave_merge;
 +              }
 +
 +              cmd.git_cmd = 1;
 +              argv_array_push(&cmd.args, "merge");
 +              argv_array_push(&cmd.args, "-s");
 +              argv_array_push(&cmd.args, "octopus");
 +              argv_array_push(&cmd.args, "--no-edit");
 +              argv_array_push(&cmd.args, "--no-ff");
 +              argv_array_push(&cmd.args, "--no-log");
 +              argv_array_push(&cmd.args, "--no-stat");
 +              argv_array_push(&cmd.args, "-F");
 +              argv_array_push(&cmd.args, git_path_merge_msg(the_repository));
 +              if (opts->gpg_sign)
 +                      argv_array_push(&cmd.args, opts->gpg_sign);
 +
 +              /* Add the tips to be merged */
 +              for (j = to_merge; j; j = j->next)
 +                      argv_array_push(&cmd.args,
 +                                      oid_to_hex(&j->item->object.oid));
 +
 +              strbuf_release(&ref_name);
 +              unlink(git_path_cherry_pick_head(the_repository));
 +              rollback_lock_file(&lock);
 +
 +              rollback_lock_file(&lock);
 +              ret = run_command(&cmd);
 +
 +              /* force re-reading of the cache */
 +              if (!ret && (discard_cache() < 0 || read_cache() < 0))
 +                      ret = error(_("could not read index"));
 +              goto leave_merge;
 +      }
 +
 +      merge_commit = to_merge->item;
        write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
                      git_path_merge_head(the_repository), 0);
        write_message("no-ff", 5, git_path_merge_mode(the_repository), 0);
  
        bases = get_merge_bases(head_commit, merge_commit);
 -      if (bases && !oidcmp(&merge_commit->object.oid,
 -                           &bases->item->object.oid)) {
 +      if (bases && oideq(&merge_commit->object.oid,
 +                         &bases->item->object.oid)) {
                ret = 0;
                /* skip merging an ancestor of HEAD */
                goto leave_merge;
  
        rollback_lock_file(&lock);
        if (ret)
 -              rerere(opts->allow_rerere_auto);
 +              repo_rerere(the_repository, opts->allow_rerere_auto);
        else
                /*
                 * In case of problems, we now want to return a positive
  leave_merge:
        strbuf_release(&ref_name);
        rollback_lock_file(&lock);
 +      free_commit_list(to_merge);
        return ret;
  }
  
@@@ -3401,8 -3256,7 +3402,8 @@@ static int stopped_at_head(void
        struct commit *commit;
        struct commit_message message;
  
 -      if (get_oid("HEAD", &head) || !(commit = lookup_commit(&head)) ||
 +      if (get_oid("HEAD", &head) ||
 +          !(commit = lookup_commit(the_repository, &head)) ||
            parse_commit(commit) || get_message(commit, &message))
                fprintf(stderr, _("Stopped at HEAD\n"));
        else {
@@@ -3512,9 -3366,9 +3513,9 @@@ static int pick_commits(struct todo_lis
                                 */
                                if (item->command == TODO_REWORD &&
                                    !get_oid("HEAD", &oid) &&
 -                                  (!oidcmp(&item->commit->object.oid, &oid) ||
 +                                  (oideq(&item->commit->object.oid, &oid) ||
                                     (opts->have_squash_onto &&
 -                                    !oidcmp(&opts->squash_onto, &oid))))
 +                                    oideq(&opts->squash_onto, &oid))))
                                        to_amend = 1;
  
                                return res | error_with_patch(item->commit,
@@@ -3640,7 -3494,7 +3641,7 @@@ cleanup_head_ref
                        struct object_id orig, head;
  
                        memset(&log_tree_opt, 0, sizeof(log_tree_opt));
 -                      init_revisions(&log_tree_opt, NULL);
 +                      repo_init_revisions(the_repository, &log_tree_opt, NULL);
                        log_tree_opt.diff = 1;
                        log_tree_opt.diffopt.output_format =
                                DIFF_FORMAT_DIFFSTAT;
@@@ -3729,7 -3583,7 +3730,7 @@@ static int commit_staged_changes(struc
                if (get_oid_hex(rev.buf, &to_amend))
                        return error(_("invalid contents: '%s'"),
                                rebase_path_amend());
 -              if (!is_clean && oidcmp(&head, &to_amend))
 +              if (!is_clean && !oideq(&head, &to_amend))
                        return error(_("\nYou have uncommitted changes in your "
                                       "working tree. Please, commit them\n"
                                       "first and then run 'git rebase "
                 * the commit message and if there was a squash, let the user
                 * edit it.
                 */
 -              if (is_clean && !oidcmp(&head, &to_amend) &&
 -                  opts->current_fixup_count > 0 &&
 -                  file_exists(rebase_path_stopped_sha())) {
 +              if (!is_clean || !opts->current_fixup_count)
 +                      ; /* this is not the final fixup */
 +              else if (!oideq(&head, &to_amend) ||
 +                       !file_exists(rebase_path_stopped_sha())) {
 +                      /* was a final fixup or squash done manually? */
 +                      if (!is_fixup(peek_command(todo_list, 0))) {
 +                              unlink(rebase_path_fixup_msg());
 +                              unlink(rebase_path_squash_msg());
 +                              unlink(rebase_path_current_fixups());
 +                              strbuf_reset(&opts->current_fixups);
 +                              opts->current_fixup_count = 0;
 +                      }
 +              } else {
 +                      /* we are in a fixup/squash chain */
                        const char *p = opts->current_fixups.buf;
                        int len = opts->current_fixups.len;
  
@@@ -3919,7 -3762,7 +3920,7 @@@ int sequencer_pick_revisions(struct rep
                        continue;
  
                if (!get_oid(name, &oid)) {
 -                      if (!lookup_commit_reference_gently(&oid, 1)) {
 +                      if (!lookup_commit_reference_gently(the_repository, &oid, 1)) {
                                enum object_type type = oid_object_info(the_repository,
                                                                        &oid,
                                                                        NULL);
                if (prepare_revision_walk(opts->revs))
                        return error(_("revision walk setup failed"));
                cmit = get_revision(opts->revs);
 -              if (!cmit || get_revision(opts->revs))
 -                      return error("BUG: expected exactly one commit from walk");
 +              if (!cmit)
 +                      return error(_("empty commit set passed"));
 +              if (get_revision(opts->revs))
 +                      BUG("unexpected extra commit from walk");
                return single_pick(cmit, opts);
        }
  
        return res;
  }
  
 -void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
 +void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
  {
        unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
        struct strbuf sob = STRBUF_INIT;
@@@ -4190,6 -4031,7 +4191,6 @@@ static int make_script_with_merges(stru
         */
        while ((commit = get_revision(revs))) {
                struct commit_list *to_merge;
 -              int is_octopus;
                const char *p1, *p2;
                struct object_id *oid;
                int is_empty;
                        continue;
                }
  
 -              is_octopus = to_merge && to_merge->next;
 -
 -              if (is_octopus)
 -                      BUG("Octopus merges not yet supported");
 -
                /* Create a label */
                strbuf_reset(&label);
                if (skip_prefix(oneline.buf, "Merge ", &p1) &&
                strbuf_addf(&buf, "%s -C %s",
                            cmd_merge, oid_to_hex(&commit->object.oid));
  
 -              /* label the tip of merged branch */
 -              oid = &to_merge->item->object.oid;
 -              strbuf_addch(&buf, ' ');
 +              /* label the tips of merged branches */
 +              for (; to_merge; to_merge = to_merge->next) {
 +                      oid = &to_merge->item->object.oid;
 +                      strbuf_addch(&buf, ' ');
 +
 +                      if (!oidset_contains(&interesting, oid)) {
 +                              strbuf_addstr(&buf, label_oid(oid, NULL,
 +                                                            &state));
 +                              continue;
 +                      }
  
 -              if (!oidset_contains(&interesting, oid))
 -                      strbuf_addstr(&buf, label_oid(oid, NULL, &state));
 -              else {
                        tips_tail = &commit_list_insert(to_merge->item,
                                                        tips_tail)->next;
  
                        struct object_id *oid = &parent->item->object.oid;
                        if (!oidset_contains(&interesting, oid))
                                continue;
 -                      if (!oidset_contains(&child_seen, oid))
 -                              oidset_insert(&child_seen, oid);
 -                      else
 +                      if (oidset_insert(&child_seen, oid))
                                label_oid(oid, "branch-point", &state);
                }
  
                entry = oidmap_get(&state.commit2label, &commit->object.oid);
  
                if (entry)
 -                      fprintf(out, "\n# Branch %s\n", entry->string);
 +                      fprintf(out, "\n%c Branch %s\n", comment_line_char, entry->string);
                else
                        fprintf(out, "\n");
  
@@@ -4384,7 -4229,7 +4385,7 @@@ int sequencer_make_script(FILE *out, in
        const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
        int rebase_merges = flags & TODO_LIST_REBASE_MERGES;
  
 -      init_revisions(&revs, NULL);
 +      repo_init_revisions(the_repository, &revs, NULL);
        revs.verbose_header = 1;
        if (!rebase_merges)
                revs.max_parents = 1;
@@@ -4441,9 -4286,10 +4442,9 @@@ int sequencer_add_exec_commands(const c
  {
        const char *todo_file = rebase_path_todo();
        struct todo_list todo_list = TODO_LIST_INIT;
 -      struct todo_item *item;
        struct strbuf *buf = &todo_list.buf;
        size_t offset = 0, commands_len = strlen(commands);
 -      int i, first;
 +      int i, insert;
  
        if (strbuf_read_file(&todo_list.buf, todo_file, 0) < 0)
                return error(_("could not read '%s'."), todo_file);
                return error(_("unusable todo list: '%s'"), todo_file);
        }
  
 -      first = 1;
 -      /* insert <commands> before every pick except the first one */
 -      for (item = todo_list.items, i = 0; i < todo_list.nr; i++, item++) {
 -              if (item->command == TODO_PICK && !first) {
 -                      strbuf_insert(buf, item->offset_in_buf + offset,
 -                                    commands, commands_len);
 +      /*
 +       * Insert <commands> after every pick. Here, fixup/squash chains
 +       * are considered part of the pick, so we insert the commands *after*
 +       * those chains if there are any.
 +       */
 +      insert = -1;
 +      for (i = 0; i < todo_list.nr; i++) {
 +              enum todo_command command = todo_list.items[i].command;
 +
 +              if (insert >= 0) {
 +                      /* skip fixup/squash chains */
 +                      if (command == TODO_COMMENT)
 +                              continue;
 +                      else if (is_fixup(command)) {
 +                              insert = i + 1;
 +                              continue;
 +                      }
 +                      strbuf_insert(buf,
 +                                    todo_list.items[insert].offset_in_buf +
 +                                    offset, commands, commands_len);
                        offset += commands_len;
 +                      insert = -1;
                }
 -              first = 0;
 +
 +              if (command == TODO_PICK || command == TODO_MERGE)
 +                      insert = i + 1;
        }
  
 -      /* append final <commands> */
 -      strbuf_add(buf, commands, commands_len);
 +      /* insert or append final <commands> */
 +      if (insert >= 0 && insert < todo_list.nr)
 +              strbuf_insert(buf, todo_list.items[insert].offset_in_buf +
 +                            offset, commands, commands_len);
 +      else if (insert >= 0 || !offset)
 +              strbuf_add(buf, commands, commands_len);
  
        i = write_message(buf->buf, buf->len, todo_file, 0);
        todo_list_release(&todo_list);
@@@ -4713,7 -4538,7 +4714,7 @@@ static int skip_unnecessary_picks(struc
                if (item->commit->parents->next)
                        break; /* merge commit */
                parent_oid = &item->commit->parents->item->object.oid;
 -              if (hashcmp(parent_oid->hash, output_oid->hash))
 +              if (!oideq(parent_oid, output_oid))
                        break;
                oidcpy(output_oid, &item->commit->object.oid);
        }
diff --combined t/lib-rebase.sh
index 241f64b09b3773f5a6b35992a40097a298093771,86572438ec7832d96c24012dba7842bceb5a85cf..f6c45ee08f6e348bc7af2939d2bfa65f5c057ae0
@@@ -14,8 -14,8 +14,8 @@@
  #       specified line.
  #
  #   "<cmd> <lineno>" -- add a line with the specified command
 -#       ("squash", "fixup", "edit", "reword" or "drop") and the SHA1 taken
 -#       from the specified line.
 +#       ("pick", "squash", "fixup", "edit", "reword" or "drop") and the
 +#       SHA1 taken from the specified line.
  #
  #   "exec_cmd_with_args" -- add an "exec cmd with args" line.
  #
@@@ -47,9 -47,9 +47,9 @@@ set_fake_editor () 
        action=pick
        for line in $FAKE_LINES; do
                case $line in
 -              squash|fixup|edit|reword|drop)
 +              pick|squash|fixup|edit|reword|drop)
                        action="$line";;
-               exec*|break)
+               exec*|break|b)
                        echo "$line" | sed 's/_/ /g' >> "$1";;
                "#")
                        echo '# comment' >> "$1";;
index 75cd2d80df0f3a180129c315d427ae2222662429,b282505aaceb3dbe11f8348b7c9b41dcdab27a8f..4c3f7d8dfea1fb087c839c3a039e728642869975
@@@ -60,7 -60,7 +60,7 @@@ test_expect_success 'rebase --continue 
        EOF
        chmod +x test-bin/git-merge-funny &&
        (
 -              PATH=./test-bin:$PATH
 +              PATH=./test-bin:$PATH &&
                test_must_fail git rebase -s funny -Xopt master topic
        ) &&
        test -f funny.was.run &&
@@@ -68,7 -68,7 +68,7 @@@
        echo "Resolved" >F2 &&
        git add F2 &&
        (
 -              PATH=./test-bin:$PATH
 +              PATH=./test-bin:$PATH &&
                git rebase --continue
        ) &&
        test -f funny.was.run
@@@ -160,15 -160,13 +160,15 @@@ test_expect_success '--skip after faile
        : The first squash was skipped, therefore: &&
        git show HEAD >out &&
        test_i18ngrep "# This is a combination of 2 commits" out &&
 +      test_i18ngrep "# This is the commit message #2:" out &&
  
        (test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) &&
        git show HEAD >out &&
        test_i18ngrep ! "# This is a combination" out &&
  
        : Final squash failed, but there was still a squash &&
 -      test_i18ngrep "# This is a combination of 2 commits" .git/copy.txt
 +      test_i18ngrep "# This is a combination of 2 commits" .git/copy.txt &&
 +      test_i18ngrep "# This is the commit message #2:" .git/copy.txt
  '
  
  test_expect_success 'setup rerere database' '
@@@ -245,7 -243,9 +245,9 @@@ unset GIT_SEQUENCE_EDITO
  
  test_expect_success 'the todo command "break" works' '
        rm -f execed &&
-       FAKE_LINES="break exec_>execed" git rebase -i HEAD &&
+       FAKE_LINES="break b exec_>execed" git rebase -i HEAD &&
+       test_path_is_missing execed &&
+       git rebase --continue &&
        test_path_is_missing execed &&
        git rebase --continue &&
        test_path_is_file execed