t5600: fix outdated comment about unborn HEAD
[gitweb.git] / sequencer.c
index e0e66b987b27072da4aea6304a565ab708be91e4..5e09b5d34fbcb0516ffdaf6a841a6a8ff47f4ea5 100644 (file)
@@ -20,6 +20,7 @@
 #include "trailer.h"
 #include "log-tree.h"
 #include "wt-status.h"
+#include "hashmap.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -203,7 +204,7 @@ int sequencer_remove_state(struct replay_opts *opts)
                free(opts->xopts[i]);
        free(opts->xopts);
 
-       strbuf_addf(&dir, "%s", get_dir(opts));
+       strbuf_addstr(&dir, get_dir(opts));
        remove_dir_recursively(&dir, 0);
        strbuf_release(&dir);
 
@@ -437,7 +438,8 @@ static int do_recursive_merge(struct commit *base, struct commit *next,
        char **xopt;
        static struct lock_file index_lock;
 
-       hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR);
+       if (hold_locked_index(&index_lock, LOCK_REPORT_ON_ERROR) < 0)
+               return -1;
 
        read_cache();
 
@@ -692,7 +694,7 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts,
 
 static int is_original_commit_empty(struct commit *commit)
 {
-       const unsigned char *ptree_sha1;
+       const struct object_id *ptree_oid;
 
        if (parse_commit(commit))
                return error(_("could not parse commit %s\n"),
@@ -702,12 +704,12 @@ static int is_original_commit_empty(struct commit *commit)
                if (parse_commit(parent))
                        return error(_("could not parse parent commit %s\n"),
                                oid_to_hex(&parent->object.oid));
-               ptree_sha1 = parent->tree->object.oid.hash;
+               ptree_oid = &parent->tree->object.oid;
        } else {
-               ptree_sha1 = EMPTY_TREE_SHA1_BIN; /* commit is root */
+               ptree_oid = &empty_tree_oid; /* commit is root */
        }
 
-       return !hashcmp(ptree_sha1, commit->tree->object.oid.hash);
+       return !oidcmp(ptree_oid, &commit->tree->object.oid);
 }
 
 /*
@@ -897,18 +899,18 @@ static int update_squash_messages(enum todo_command command,
 
 static void flush_rewritten_pending(void) {
        struct strbuf buf = STRBUF_INIT;
-       unsigned char newsha1[20];
+       struct object_id newoid;
        FILE *out;
 
-       if (strbuf_read_file(&buf, rebase_path_rewritten_pending(), 82) > 0 &&
-           !get_sha1("HEAD", newsha1) &&
+       if (strbuf_read_file(&buf, rebase_path_rewritten_pending(), (GIT_MAX_HEXSZ + 1) * 2) > 0 &&
+           !get_oid("HEAD", &newoid) &&
            (out = fopen_or_warn(rebase_path_rewritten_list(), "a"))) {
                char *bol = buf.buf, *eol;
 
                while (*bol) {
                        eol = strchrnul(bol, '\n');
                        fprintf(out, "%.*s %s\n", (int)(eol - bol),
-                                       bol, sha1_to_hex(newsha1));
+                                       bol, oid_to_hex(&newoid));
                        if (!*eol)
                                break;
                        bol = eol + 1;
@@ -1565,6 +1567,7 @@ static int save_head(const char *head)
        static struct lock_file head_lock;
        struct strbuf buf = STRBUF_INIT;
        int fd;
+       ssize_t written;
 
        fd = hold_lock_file_for_update(&head_lock, git_path_head_file(), 0);
        if (fd < 0) {
@@ -1572,7 +1575,9 @@ static int save_head(const char *head)
                return error_errno(_("could not lock HEAD"));
        }
        strbuf_addf(&buf, "%s\n", head);
-       if (write_in_full(fd, buf.buf, buf.len) < 0) {
+       written = write_in_full(fd, buf.buf, buf.len);
+       strbuf_release(&buf);
+       if (written < 0) {
                rollback_lock_file(&head_lock);
                return error_errno(_("could not write to '%s'"),
                                   git_path_head_file());
@@ -1608,36 +1613,37 @@ static int rollback_is_safe(void)
        return !oidcmp(&actual_head, &expected_head);
 }
 
-static int reset_for_rollback(const unsigned char *sha1)
+static int reset_for_rollback(const struct object_id *oid)
 {
        const char *argv[4];    /* reset --merge <arg> + NULL */
 
        argv[0] = "reset";
        argv[1] = "--merge";
-       argv[2] = sha1_to_hex(sha1);
+       argv[2] = oid_to_hex(oid);
        argv[3] = NULL;
        return run_command_v_opt(argv, RUN_GIT_CMD);
 }
 
 static int rollback_single_pick(void)
 {
-       unsigned char head_sha1[20];
+       struct object_id head_oid;
 
        if (!file_exists(git_path_cherry_pick_head()) &&
            !file_exists(git_path_revert_head()))
                return error(_("no cherry-pick or revert in progress"));
-       if (read_ref_full("HEAD", 0, head_sha1, NULL))
+       if (read_ref_full("HEAD", 0, head_oid.hash, NULL))
                return error(_("cannot resolve HEAD"));
-       if (is_null_sha1(head_sha1))
+       if (is_null_oid(&head_oid))
                return error(_("cannot abort from a branch yet to be born"));
-       return reset_for_rollback(head_sha1);
+       return reset_for_rollback(&head_oid);
 }
 
 int sequencer_rollback(struct replay_opts *opts)
 {
        FILE *f;
-       unsigned char sha1[20];
+       struct object_id oid;
        struct strbuf buf = STRBUF_INIT;
+       const char *p;
 
        f = fopen(git_path_head_file(), "r");
        if (!f && errno == ENOENT) {
@@ -1657,12 +1663,12 @@ int sequencer_rollback(struct replay_opts *opts)
                goto fail;
        }
        fclose(f);
-       if (get_sha1_hex(buf.buf, sha1) || buf.buf[40] != '\0') {
+       if (parse_oid_hex(buf.buf, &oid, &p) || *p != '\0') {
                error(_("stored pre-cherry-pick HEAD file '%s' is corrupt"),
                        git_path_head_file());
                goto fail;
        }
-       if (is_null_sha1(sha1)) {
+       if (is_null_oid(&oid)) {
                error(_("cannot abort from a branch yet to be born"));
                goto fail;
        }
@@ -1672,7 +1678,7 @@ int sequencer_rollback(struct replay_opts *opts)
                warning(_("You seem to have moved HEAD. "
                          "Not rewinding, check your HEAD!"));
        } else
-       if (reset_for_rollback(sha1))
+       if (reset_for_rollback(&oid))
                goto fail;
        strbuf_release(&buf);
        return sequencer_remove_state(opts);
@@ -1806,13 +1812,13 @@ static int make_patch(struct commit *commit, struct replay_opts *opts)
 
 static int intend_to_amend(void)
 {
-       unsigned char head[20];
+       struct object_id head;
        char *p;
 
-       if (get_sha1("HEAD", head))
+       if (get_oid("HEAD", &head))
                return error(_("cannot read HEAD"));
 
-       p = sha1_to_hex(head);
+       p = oid_to_hex(&head);
        return write_message(p, strlen(p), rebase_path_amend(), 1);
 }
 
@@ -1857,12 +1863,15 @@ static int error_failed_squash(struct commit *commit,
 
 static int do_exec(const char *command_line)
 {
+       struct argv_array child_env = ARGV_ARRAY_INIT;
        const char *child_argv[] = { NULL, NULL };
        int dirty, status;
 
        fprintf(stderr, "Executing: %s\n", command_line);
        child_argv[0] = command_line;
-       status = run_command_v_opt(child_argv, RUN_USING_SHELL);
+       argv_array_pushf(&child_env, "GIT_DIR=%s", absolute_path(get_git_dir()));
+       status = run_command_v_opt_cd_env(child_argv, RUN_USING_SHELL, NULL,
+                                         child_env.argv);
 
        /* force re-reading of the cache */
        if (discard_cache() < 0 || read_cache() < 0)
@@ -1892,6 +1901,8 @@ static int do_exec(const char *command_line)
                status = 1;
        }
 
+       argv_array_clear(&child_env);
+
        return status;
 }
 
@@ -2097,10 +2108,10 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
                if (read_oneliner(&head_ref, rebase_path_head_name(), 0) &&
                                starts_with(head_ref.buf, "refs/")) {
                        const char *msg;
-                       unsigned char head[20], orig[20];
+                       struct object_id head, orig;
                        int res;
 
-                       if (get_sha1("HEAD", head)) {
+                       if (get_oid("HEAD", &head)) {
                                res = error(_("cannot read HEAD"));
 cleanup_head_ref:
                                strbuf_release(&head_ref);
@@ -2108,7 +2119,7 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
                                return res;
                        }
                        if (!read_oneliner(&buf, rebase_path_orig_head(), 0) ||
-                                       get_sha1_hex(buf.buf, orig)) {
+                                       get_oid_hex(buf.buf, &orig)) {
                                res = error(_("could not read orig-head"));
                                goto cleanup_head_ref;
                        }
@@ -2119,7 +2130,7 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
                        }
                        msg = reflog_message(opts, "finish", "%s onto %s",
                                head_ref.buf, buf.buf);
-                       if (update_ref(msg, head_ref.buf, head, orig,
+                       if (update_ref(msg, head_ref.buf, head.hash, orig.hash,
                                        REF_NODEREF, UPDATE_REFS_MSG_ON_ERR)) {
                                res = error(_("could not update %s"),
                                        head_ref.buf);
@@ -2147,8 +2158,8 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
                        log_tree_opt.disable_stdin = 1;
 
                        if (read_oneliner(&buf, rebase_path_orig_head(), 0) &&
-                           !get_sha1(buf.buf, orig.hash) &&
-                           !get_sha1("HEAD", head.hash)) {
+                           !get_oid(buf.buf, &orig) &&
+                           !get_oid("HEAD", &head)) {
                                diff_tree_oid(&orig, &head, "",
                                              &log_tree_opt.diffopt);
                                log_tree_diff_flush(&log_tree_opt);
@@ -2223,16 +2234,16 @@ static int commit_staged_changes(struct replay_opts *opts)
 
        if (file_exists(rebase_path_amend())) {
                struct strbuf rev = STRBUF_INIT;
-               unsigned char head[20], to_amend[20];
+               struct object_id head, to_amend;
 
-               if (get_sha1("HEAD", head))
+               if (get_oid("HEAD", &head))
                        return error(_("cannot amend non-existing commit"));
                if (!read_oneliner(&rev, rebase_path_amend(), 0))
                        return error(_("invalid file: '%s'"), rebase_path_amend());
-               if (get_sha1_hex(rev.buf, to_amend))
+               if (get_oid_hex(rev.buf, &to_amend))
                        return error(_("invalid contents: '%s'"),
                                rebase_path_amend());
-               if (hashcmp(head, to_amend))
+               if (oidcmp(&head, &to_amend))
                        return error(_("\nYou have uncommitted changes in your "
                                       "working tree. Please, commit them\n"
                                       "first and then run 'git rebase "
@@ -2284,7 +2295,7 @@ int sequencer_continue(struct replay_opts *opts)
                struct object_id oid;
 
                if (read_oneliner(&buf, rebase_path_stopped_sha(), 1) &&
-                   !get_sha1_committish(buf.buf, oid.hash))
+                   !get_oid_committish(buf.buf, &oid))
                        record_in_rewritten(&oid, peek_command(&todo_list, 0));
                strbuf_release(&buf);
        }
@@ -2431,3 +2442,520 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
 
        strbuf_release(&sob);
 }
+
+int sequencer_make_script(int keep_empty, FILE *out,
+               int argc, const char **argv)
+{
+       char *format = NULL;
+       struct pretty_print_context pp = {0};
+       struct strbuf buf = STRBUF_INIT;
+       struct rev_info revs;
+       struct commit *commit;
+
+       init_revisions(&revs, NULL);
+       revs.verbose_header = 1;
+       revs.max_parents = 1;
+       revs.cherry_pick = 1;
+       revs.limited = 1;
+       revs.reverse = 1;
+       revs.right_only = 1;
+       revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+       revs.topo_order = 1;
+
+       revs.pretty_given = 1;
+       git_config_get_string("rebase.instructionFormat", &format);
+       if (!format || !*format) {
+               free(format);
+               format = xstrdup("%s");
+       }
+       get_commit_format(format, &revs);
+       free(format);
+       pp.fmt = revs.commit_format;
+       pp.output_encoding = get_log_output_encoding();
+
+       if (setup_revisions(argc, argv, &revs, NULL) > 1)
+               return error(_("make_script: unhandled options"));
+
+       if (prepare_revision_walk(&revs) < 0)
+               return error(_("make_script: error preparing revisions"));
+
+       while ((commit = get_revision(&revs))) {
+               strbuf_reset(&buf);
+               if (!keep_empty && is_original_commit_empty(commit))
+                       strbuf_addf(&buf, "%c ", comment_line_char);
+               strbuf_addf(&buf, "pick %s ", oid_to_hex(&commit->object.oid));
+               pretty_print_commit(&pp, commit, &buf);
+               strbuf_addch(&buf, '\n');
+               fputs(buf.buf, out);
+       }
+       strbuf_release(&buf);
+       return 0;
+}
+
+
+int transform_todo_ids(int shorten_ids)
+{
+       const char *todo_file = rebase_path_todo();
+       struct todo_list todo_list = TODO_LIST_INIT;
+       int fd, res, i;
+       FILE *out;
+
+       strbuf_reset(&todo_list.buf);
+       fd = open(todo_file, O_RDONLY);
+       if (fd < 0)
+               return error_errno(_("could not open '%s'"), todo_file);
+       if (strbuf_read(&todo_list.buf, fd, 0) < 0) {
+               close(fd);
+               return error(_("could not read '%s'."), todo_file);
+       }
+       close(fd);
+
+       res = parse_insn_buffer(todo_list.buf.buf, &todo_list);
+       if (res) {
+               todo_list_release(&todo_list);
+               return error(_("unusable todo list: '%s'"), todo_file);
+       }
+
+       out = fopen(todo_file, "w");
+       if (!out) {
+               todo_list_release(&todo_list);
+               return error(_("unable to open '%s' for writing"), todo_file);
+       }
+       for (i = 0; i < todo_list.nr; i++) {
+               struct todo_item *item = todo_list.items + i;
+               int bol = item->offset_in_buf;
+               const char *p = todo_list.buf.buf + bol;
+               int eol = i + 1 < todo_list.nr ?
+                       todo_list.items[i + 1].offset_in_buf :
+                       todo_list.buf.len;
+
+               if (item->command >= TODO_EXEC && item->command != TODO_DROP)
+                       fwrite(p, eol - bol, 1, out);
+               else {
+                       const char *id = shorten_ids ?
+                               short_commit_name(item->commit) :
+                               oid_to_hex(&item->commit->object.oid);
+                       int len;
+
+                       p += strspn(p, " \t"); /* left-trim command */
+                       len = strcspn(p, " \t"); /* length of command */
+
+                       fprintf(out, "%.*s %s %.*s\n",
+                               len, p, id, item->arg_len, item->arg);
+               }
+       }
+       fclose(out);
+       todo_list_release(&todo_list);
+       return 0;
+}
+
+enum check_level {
+       CHECK_IGNORE = 0, CHECK_WARN, CHECK_ERROR
+};
+
+static enum check_level get_missing_commit_check_level(void)
+{
+       const char *value;
+
+       if (git_config_get_value("rebase.missingcommitscheck", &value) ||
+                       !strcasecmp("ignore", value))
+               return CHECK_IGNORE;
+       if (!strcasecmp("warn", value))
+               return CHECK_WARN;
+       if (!strcasecmp("error", value))
+               return CHECK_ERROR;
+       warning(_("unrecognized setting %s for option "
+                 "rebase.missingCommitsCheck. Ignoring."), value);
+       return CHECK_IGNORE;
+}
+
+/*
+ * Check if the user dropped some commits by mistake
+ * Behaviour determined by rebase.missingCommitsCheck.
+ * Check if there is an unrecognized command or a
+ * bad SHA-1 in a command.
+ */
+int check_todo_list(void)
+{
+       enum check_level check_level = get_missing_commit_check_level();
+       struct strbuf todo_file = STRBUF_INIT;
+       struct todo_list todo_list = TODO_LIST_INIT;
+       struct strbuf missing = STRBUF_INIT;
+       int advise_to_edit_todo = 0, res = 0, fd, i;
+
+       strbuf_addstr(&todo_file, rebase_path_todo());
+       fd = open(todo_file.buf, O_RDONLY);
+       if (fd < 0) {
+               res = error_errno(_("could not open '%s'"), todo_file.buf);
+               goto leave_check;
+       }
+       if (strbuf_read(&todo_list.buf, fd, 0) < 0) {
+               close(fd);
+               res = error(_("could not read '%s'."), todo_file.buf);
+               goto leave_check;
+       }
+       close(fd);
+       advise_to_edit_todo = res =
+               parse_insn_buffer(todo_list.buf.buf, &todo_list);
+
+       if (res || check_level == CHECK_IGNORE)
+               goto leave_check;
+
+       /* Mark the commits in git-rebase-todo as seen */
+       for (i = 0; i < todo_list.nr; i++) {
+               struct commit *commit = todo_list.items[i].commit;
+               if (commit)
+                       commit->util = (void *)1;
+       }
+
+       todo_list_release(&todo_list);
+       strbuf_addstr(&todo_file, ".backup");
+       fd = open(todo_file.buf, O_RDONLY);
+       if (fd < 0) {
+               res = error_errno(_("could not open '%s'"), todo_file.buf);
+               goto leave_check;
+       }
+       if (strbuf_read(&todo_list.buf, fd, 0) < 0) {
+               close(fd);
+               res = error(_("could not read '%s'."), todo_file.buf);
+               goto leave_check;
+       }
+       close(fd);
+       strbuf_release(&todo_file);
+       res = !!parse_insn_buffer(todo_list.buf.buf, &todo_list);
+
+       /* Find commits in git-rebase-todo.backup yet unseen */
+       for (i = todo_list.nr - 1; i >= 0; i--) {
+               struct todo_item *item = todo_list.items + i;
+               struct commit *commit = item->commit;
+               if (commit && !commit->util) {
+                       strbuf_addf(&missing, " - %s %.*s\n",
+                                   short_commit_name(commit),
+                                   item->arg_len, item->arg);
+                       commit->util = (void *)1;
+               }
+       }
+
+       /* Warn about missing commits */
+       if (!missing.len)
+               goto leave_check;
+
+       if (check_level == CHECK_ERROR)
+               advise_to_edit_todo = res = 1;
+
+       fprintf(stderr,
+               _("Warning: some commits may have been dropped accidentally.\n"
+               "Dropped commits (newer to older):\n"));
+
+       /* Make the list user-friendly and display */
+       fputs(missing.buf, stderr);
+       strbuf_release(&missing);
+
+       fprintf(stderr, _("To avoid this message, use \"drop\" to "
+               "explicitly remove a commit.\n\n"
+               "Use 'git config rebase.missingCommitsCheck' to change "
+               "the level of warnings.\n"
+               "The possible behaviours are: ignore, warn, error.\n\n"));
+
+leave_check:
+       strbuf_release(&todo_file);
+       todo_list_release(&todo_list);
+
+       if (advise_to_edit_todo)
+               fprintf(stderr,
+                       _("You can fix this with 'git rebase --edit-todo' "
+                         "and then run 'git rebase --continue'.\n"
+                         "Or you can abort the rebase with 'git rebase"
+                         " --abort'.\n"));
+
+       return res;
+}
+
+static int rewrite_file(const char *path, const char *buf, size_t len)
+{
+       int rc = 0;
+       int fd = open(path, O_WRONLY | O_TRUNC);
+       if (fd < 0)
+               return error_errno(_("could not open '%s' for writing"), path);
+       if (write_in_full(fd, buf, len) < 0)
+               rc = error_errno(_("could not write to '%s'"), path);
+       if (close(fd) && !rc)
+               rc = error_errno(_("could not close '%s'"), path);
+       return rc;
+}
+
+/* skip picking commits whose parents are unchanged */
+int skip_unnecessary_picks(void)
+{
+       const char *todo_file = rebase_path_todo();
+       struct strbuf buf = STRBUF_INIT;
+       struct todo_list todo_list = TODO_LIST_INIT;
+       struct object_id onto_oid, *oid = &onto_oid, *parent_oid;
+       int fd, i;
+
+       if (!read_oneliner(&buf, rebase_path_onto(), 0))
+               return error(_("could not read 'onto'"));
+       if (get_oid(buf.buf, &onto_oid)) {
+               strbuf_release(&buf);
+               return error(_("need a HEAD to fixup"));
+       }
+       strbuf_release(&buf);
+
+       fd = open(todo_file, O_RDONLY);
+       if (fd < 0) {
+               return error_errno(_("could not open '%s'"), todo_file);
+       }
+       if (strbuf_read(&todo_list.buf, fd, 0) < 0) {
+               close(fd);
+               return error(_("could not read '%s'."), todo_file);
+       }
+       close(fd);
+       if (parse_insn_buffer(todo_list.buf.buf, &todo_list) < 0) {
+               todo_list_release(&todo_list);
+               return -1;
+       }
+
+       for (i = 0; i < todo_list.nr; i++) {
+               struct todo_item *item = todo_list.items + i;
+
+               if (item->command >= TODO_NOOP)
+                       continue;
+               if (item->command != TODO_PICK)
+                       break;
+               if (parse_commit(item->commit)) {
+                       todo_list_release(&todo_list);
+                       return error(_("could not parse commit '%s'"),
+                               oid_to_hex(&item->commit->object.oid));
+               }
+               if (!item->commit->parents)
+                       break; /* root commit */
+               if (item->commit->parents->next)
+                       break; /* merge commit */
+               parent_oid = &item->commit->parents->item->object.oid;
+               if (hashcmp(parent_oid->hash, oid->hash))
+                       break;
+               oid = &item->commit->object.oid;
+       }
+       if (i > 0) {
+               int offset = i < todo_list.nr ?
+                       todo_list.items[i].offset_in_buf : todo_list.buf.len;
+               const char *done_path = rebase_path_done();
+
+               fd = open(done_path, O_CREAT | O_WRONLY | O_APPEND, 0666);
+               if (fd < 0) {
+                       error_errno(_("could not open '%s' for writing"),
+                                   done_path);
+                       todo_list_release(&todo_list);
+                       return -1;
+               }
+               if (write_in_full(fd, todo_list.buf.buf, offset) < 0) {
+                       error_errno(_("could not write to '%s'"), done_path);
+                       todo_list_release(&todo_list);
+                       close(fd);
+                       return -1;
+               }
+               close(fd);
+
+               if (rewrite_file(rebase_path_todo(), todo_list.buf.buf + offset,
+                                todo_list.buf.len - offset) < 0) {
+                       todo_list_release(&todo_list);
+                       return -1;
+               }
+
+               todo_list.current = i;
+               if (is_fixup(peek_command(&todo_list, 0)))
+                       record_in_rewritten(oid, peek_command(&todo_list, 0));
+       }
+
+       todo_list_release(&todo_list);
+       printf("%s\n", oid_to_hex(oid));
+
+       return 0;
+}
+
+struct subject2item_entry {
+       struct hashmap_entry entry;
+       int i;
+       char subject[FLEX_ARRAY];
+};
+
+static int subject2item_cmp(const void *fndata,
+                           const struct subject2item_entry *a,
+                           const struct subject2item_entry *b, const void *key)
+{
+       return key ? strcmp(a->subject, key) : strcmp(a->subject, b->subject);
+}
+
+/*
+ * Rearrange the todo list that has both "pick commit-id msg" and "pick
+ * commit-id fixup!/squash! msg" in it so that the latter is put immediately
+ * after the former, and change "pick" to "fixup"/"squash".
+ *
+ * Note that if the config has specified a custom instruction format, each log
+ * message will have to be retrieved from the commit (as the oneline in the
+ * script cannot be trusted) in order to normalize the autosquash arrangement.
+ */
+int rearrange_squash(void)
+{
+       const char *todo_file = rebase_path_todo();
+       struct todo_list todo_list = TODO_LIST_INIT;
+       struct hashmap subject2item;
+       int res = 0, rearranged = 0, *next, *tail, fd, i;
+       char **subjects;
+
+       fd = open(todo_file, O_RDONLY);
+       if (fd < 0)
+               return error_errno(_("could not open '%s'"), todo_file);
+       if (strbuf_read(&todo_list.buf, fd, 0) < 0) {
+               close(fd);
+               return error(_("could not read '%s'."), todo_file);
+       }
+       close(fd);
+       if (parse_insn_buffer(todo_list.buf.buf, &todo_list) < 0) {
+               todo_list_release(&todo_list);
+               return -1;
+       }
+
+       /*
+        * The hashmap maps onelines to the respective todo list index.
+        *
+        * If any items need to be rearranged, the next[i] value will indicate
+        * which item was moved directly after the i'th.
+        *
+        * In that case, last[i] will indicate the index of the latest item to
+        * be moved to appear after the i'th.
+        */
+       hashmap_init(&subject2item, (hashmap_cmp_fn) subject2item_cmp,
+                    NULL, todo_list.nr);
+       ALLOC_ARRAY(next, todo_list.nr);
+       ALLOC_ARRAY(tail, todo_list.nr);
+       ALLOC_ARRAY(subjects, todo_list.nr);
+       for (i = 0; i < todo_list.nr; i++) {
+               struct strbuf buf = STRBUF_INIT;
+               struct todo_item *item = todo_list.items + i;
+               const char *commit_buffer, *subject, *p;
+               size_t subject_len;
+               int i2 = -1;
+               struct subject2item_entry *entry;
+
+               next[i] = tail[i] = -1;
+               if (item->command >= TODO_EXEC) {
+                       subjects[i] = NULL;
+                       continue;
+               }
+
+               if (is_fixup(item->command)) {
+                       todo_list_release(&todo_list);
+                       return error(_("the script was already rearranged."));
+               }
+
+               item->commit->util = item;
+
+               parse_commit(item->commit);
+               commit_buffer = get_commit_buffer(item->commit, NULL);
+               find_commit_subject(commit_buffer, &subject);
+               format_subject(&buf, subject, " ");
+               subject = subjects[i] = strbuf_detach(&buf, &subject_len);
+               unuse_commit_buffer(item->commit, commit_buffer);
+               if ((skip_prefix(subject, "fixup! ", &p) ||
+                    skip_prefix(subject, "squash! ", &p))) {
+                       struct commit *commit2;
+
+                       for (;;) {
+                               while (isspace(*p))
+                                       p++;
+                               if (!skip_prefix(p, "fixup! ", &p) &&
+                                   !skip_prefix(p, "squash! ", &p))
+                                       break;
+                       }
+
+                       if ((entry = hashmap_get_from_hash(&subject2item,
+                                                          strhash(p), p)))
+                               /* found by title */
+                               i2 = entry->i;
+                       else if (!strchr(p, ' ') &&
+                                (commit2 =
+                                 lookup_commit_reference_by_name(p)) &&
+                                commit2->util)
+                               /* found by commit name */
+                               i2 = (struct todo_item *)commit2->util
+                                       - todo_list.items;
+                       else {
+                               /* copy can be a prefix of the commit subject */
+                               for (i2 = 0; i2 < i; i2++)
+                                       if (subjects[i2] &&
+                                           starts_with(subjects[i2], p))
+                                               break;
+                               if (i2 == i)
+                                       i2 = -1;
+                       }
+               }
+               if (i2 >= 0) {
+                       rearranged = 1;
+                       todo_list.items[i].command =
+                               starts_with(subject, "fixup!") ?
+                               TODO_FIXUP : TODO_SQUASH;
+                       if (next[i2] < 0)
+                               next[i2] = i;
+                       else
+                               next[tail[i2]] = i;
+                       tail[i2] = i;
+               } else if (!hashmap_get_from_hash(&subject2item,
+                                               strhash(subject), subject)) {
+                       FLEX_ALLOC_MEM(entry, subject, subject, subject_len);
+                       entry->i = i;
+                       hashmap_entry_init(entry, strhash(entry->subject));
+                       hashmap_put(&subject2item, entry);
+               }
+       }
+
+       if (rearranged) {
+               struct strbuf buf = STRBUF_INIT;
+
+               for (i = 0; i < todo_list.nr; i++) {
+                       enum todo_command command = todo_list.items[i].command;
+                       int cur = i;
+
+                       /*
+                        * Initially, all commands are 'pick's. If it is a
+                        * fixup or a squash now, we have rearranged it.
+                        */
+                       if (is_fixup(command))
+                               continue;
+
+                       while (cur >= 0) {
+                               int offset = todo_list.items[cur].offset_in_buf;
+                               int end_offset = cur + 1 < todo_list.nr ?
+                                       todo_list.items[cur + 1].offset_in_buf :
+                                       todo_list.buf.len;
+                               char *bol = todo_list.buf.buf + offset;
+                               char *eol = todo_list.buf.buf + end_offset;
+
+                               /* replace 'pick', by 'fixup' or 'squash' */
+                               command = todo_list.items[cur].command;
+                               if (is_fixup(command)) {
+                                       strbuf_addstr(&buf,
+                                               todo_command_info[command].str);
+                                       bol += strcspn(bol, " \t");
+                               }
+
+                               strbuf_add(&buf, bol, eol - bol);
+
+                               cur = next[cur];
+                       }
+               }
+
+               res = rewrite_file(todo_file, buf.buf, buf.len);
+               strbuf_release(&buf);
+       }
+
+       free(next);
+       free(tail);
+       for (i = 0; i < todo_list.nr; i++)
+               free(subjects[i]);
+       free(subjects);
+       hashmap_free(&subject2item, 1);
+       todo_list_release(&todo_list);
+
+       return res;
+}