From e4b1b233574ca62dfdd4403c6232ddae905692b0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 9 Feb 2016 14:10:43 +0100 Subject: [PATCH 01/45] sequencer: support a new action: 'interactive rebase' This patch introduces a new action for the sequencer. It really does not do a whole lot of its own right now, but lays the ground work for patches to come. The intention, of course, is to finally make the sequencer the work horse of the interactive rebase (the original idea behind the "sequencer" concept). Signed-off-by: Johannes Schindelin --- sequencer.c | 33 +++++++++++++++++++++++++++++---- sequencer.h | 3 ++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/sequencer.c b/sequencer.c index 30b10ba143..21cfdacd06 100644 --- a/sequencer.c +++ b/sequencer.c @@ -28,6 +28,14 @@ static GIT_PATH_FUNC(git_path_todo_file, "sequencer/todo") static GIT_PATH_FUNC(git_path_opts_file, "sequencer/opts") static GIT_PATH_FUNC(git_path_head_file, "sequencer/head") +static GIT_PATH_FUNC(rebase_path, "rebase-merge") +/* + * The file containing rebase commands, comments, and empty lines. + * This file is created by "git rebase -i" then edited by the user. As + * the lines are processed, they are removed from the front of this + * file and written to the tail of 'done'. + */ +static GIT_PATH_FUNC(rebase_path_todo, "rebase-merge/git-rebase-todo") /* * A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and * GIT_AUTHOR_DATE that will be used for the commit that is currently @@ -40,19 +48,22 @@ static GIT_PATH_FUNC(rebase_path_author_script, "rebase-merge/author-script") */ static GIT_PATH_FUNC(rebase_path_gpg_sign_opt, "rebase-merge/gpg_sign_opt") -/* We will introduce the 'interactive rebase' mode later */ static inline int is_rebase_i(const struct replay_opts *opts) { - return 0; + return opts->action == REPLAY_INTERACTIVE_REBASE; } static const char *get_dir(const struct replay_opts *opts) { + if (is_rebase_i(opts)) + return rebase_path(); return git_path_seq_dir(); } static const char *get_todo_path(const struct replay_opts *opts) { + if (is_rebase_i(opts)) + return rebase_path_todo(); return git_path_todo_file(); } @@ -168,7 +179,15 @@ int sequencer_remove_state(struct replay_opts *opts) static const char *action_name(const struct replay_opts *opts) { - return opts->action == REPLAY_REVERT ? N_("revert") : N_("cherry-pick"); + switch (opts->action) { + case REPLAY_REVERT: + return N_("revert"); + case REPLAY_PICK: + return N_("cherry-pick"); + case REPLAY_INTERACTIVE_REBASE: + return N_("rebase -i"); + } + die(_("Unknown action: %d"), opts->action); } struct commit_message { @@ -395,7 +414,10 @@ static int do_recursive_merge(struct commit *base, struct commit *next, if (active_cache_changed && write_locked_index(&the_index, &index_lock, COMMIT_LOCK)) - /* TRANSLATORS: %s will be "revert" or "cherry-pick" */ + /* + * TRANSLATORS: %s will be "revert", "cherry-pick" or + * "rebase -i". + */ return error(_("%s: Unable to write new index file"), _(action_name(opts))); rollback_lock_file(&index_lock); @@ -1204,6 +1226,9 @@ static int save_todo(struct todo_list *todo_list, struct replay_opts *opts) const char *todo_path = get_todo_path(opts); int next = todo_list->current, offset, fd; + if (is_rebase_i(opts)) + next++; + fd = hold_lock_file_for_update(&todo_lock, todo_path, 0); if (fd < 0) return error_errno(_("could not lock '%s'"), todo_path); diff --git a/sequencer.h b/sequencer.h index 7a513c576b..cb21cfddee 100644 --- a/sequencer.h +++ b/sequencer.h @@ -7,7 +7,8 @@ const char *git_path_seq_dir(void); enum replay_action { REPLAY_REVERT, - REPLAY_PICK + REPLAY_PICK, + REPLAY_INTERACTIVE_REBASE }; struct replay_opts { From f9ca921573794a10c3edc776b66ae0493591bc94 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 14:07:35 +0200 Subject: [PATCH 02/45] sequencer (rebase -i): implement the 'noop' command The 'noop' command is probably the most boring of all rebase -i commands to support in the sequencer. Which makes it an excellent candidate for this first stab to add support for rebase -i's commands to the sequencer. For the moment, let's also treat empty lines and commented-out lines as 'noop'; We will refine that handling later in this patch series. To make it easier to identify "classes" of todo_commands (such as: determine whether a command is pick-like, i.e. handles a single commit), let's enforce a certain order of said commands. Signed-off-by: Johannes Schindelin --- sequencer.c | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/sequencer.c b/sequencer.c index 21cfdacd06..1224799286 100644 --- a/sequencer.c +++ b/sequencer.c @@ -639,14 +639,23 @@ static int allow_empty(struct replay_opts *opts, struct commit *commit) return 1; } +/* + * Note that ordering matters in this enum. Not only must it match the mapping + * below, it is also divided into several sections that matter. When adding + * new commands, make sure you add it in the right section. + */ enum todo_command { + /* commands that handle commits */ TODO_PICK = 0, - TODO_REVERT + TODO_REVERT, + /* commands that do nothing but are counted for reporting progress */ + TODO_NOOP }; static const char *todo_command_strings[] = { "pick", - "revert" + "revert", + "noop" }; static const char *command_to_string(const enum todo_command command) @@ -916,6 +925,14 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol) /* left-trim */ bol += strspn(bol, " \t"); + if (bol == eol || *bol == '\r' || *bol == comment_line_char) { + item->command = TODO_NOOP; + item->commit = NULL; + item->arg = bol; + item->arg_len = eol - bol; + return 0; + } + for (i = 0; i < ARRAY_SIZE(todo_command_strings); i++) if (skip_prefix(bol, todo_command_strings[i], &bol)) { item->command = i; @@ -924,6 +941,13 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol) if (i >= ARRAY_SIZE(todo_command_strings)) return -1; + if (item->command == TODO_NOOP) { + item->commit = NULL; + item->arg = bol; + item->arg_len = eol - bol; + return 0; + } + /* Eat up extra spaces/ tabs before object name */ padding = strspn(bol, " \t"); if (!padding) @@ -1292,7 +1316,12 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) struct todo_item *item = todo_list->items + todo_list->current; if (save_todo(todo_list, opts)) return -1; - res = do_pick_commit(item->command, item->commit, opts); + if (item->command <= TODO_REVERT) + res = do_pick_commit(item->command, item->commit, + opts); + else if (item->command != TODO_NOOP) + return error(_("unknown command %d"), item->command); + todo_list->current++; if (res) return res; From c06adfab827c91cd66b166d689c6d4bc1c206d35 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 14:07:47 +0200 Subject: [PATCH 03/45] sequencer (rebase -i): implement the 'edit' command This patch is a straight-forward reimplementation of the `edit` operation of the interactive rebase command. Well, not *quite* straight-forward: when stopping, the `edit` command wants to write the `patch` file (which is not only the patch, but includes the commit message and author information). To that end, this patch requires the earlier work that taught the log-tree machinery to respect the `file` setting of rev_info->diffopt to write to a file stream different than stdout. Signed-off-by: Johannes Schindelin --- sequencer.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/sequencer.c b/sequencer.c index 1224799286..68e2c84803 100644 --- a/sequencer.c +++ b/sequencer.c @@ -16,6 +16,7 @@ #include "refs.h" #include "argv-array.h" #include "quote.h" +#include "log-tree.h" #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION" @@ -42,6 +43,20 @@ static GIT_PATH_FUNC(rebase_path_todo, "rebase-merge/git-rebase-todo") * being rebased. */ static GIT_PATH_FUNC(rebase_path_author_script, "rebase-merge/author-script") +/* + * When an "edit" rebase command is being processed, the SHA1 of the + * commit to be edited is recorded in this file. When "git rebase + * --continue" is executed, if there are any staged changes then they + * will be amended to the HEAD commit, but only provided the HEAD + * commit is still the commit to be edited. When any other rebase + * command is processed, this file is deleted. + */ +static GIT_PATH_FUNC(rebase_path_amend, "rebase-merge/amend") +/* + * When we stop at a given patch via the "edit" command, this file contains + * the long commit name of the corresponding patch. + */ +static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha") /* * The following files are written by git-rebase just after parsing the * command-line (and are only consumed, not modified, by the sequencer). @@ -648,6 +663,7 @@ enum todo_command { /* commands that handle commits */ TODO_PICK = 0, TODO_REVERT, + TODO_EDIT, /* commands that do nothing but are counted for reporting progress */ TODO_NOOP }; @@ -655,6 +671,7 @@ enum todo_command { static const char *todo_command_strings[] = { "pick", "revert", + "edit", "noop" }; @@ -1301,9 +1318,87 @@ static int save_opts(struct replay_opts *opts) return res; } +static int make_patch(struct commit *commit, struct replay_opts *opts) +{ + struct strbuf buf = STRBUF_INIT; + struct rev_info log_tree_opt; + const char *commit_buffer = get_commit_buffer(commit, NULL), *subject, *p; + int res = 0; + + p = short_commit_name(commit); + if (write_message(p, strlen(p), rebase_path_stopped_sha(), 1) < 0) + return -1; + + strbuf_addf(&buf, "%s/patch", get_dir(opts)); + memset(&log_tree_opt, 0, sizeof(log_tree_opt)); + init_revisions(&log_tree_opt, NULL); + log_tree_opt.abbrev = 0; + log_tree_opt.diff = 1; + log_tree_opt.diffopt.output_format = DIFF_FORMAT_PATCH; + log_tree_opt.disable_stdin = 1; + log_tree_opt.no_commit_id = 1; + log_tree_opt.diffopt.file = fopen(buf.buf, "w"); + log_tree_opt.diffopt.use_color = GIT_COLOR_NEVER; + if (!log_tree_opt.diffopt.file) + res |= error_errno(_("could not open '%s'"), buf.buf); + else { + res |= log_tree_commit(&log_tree_opt, commit); + fclose(log_tree_opt.diffopt.file); + } + strbuf_reset(&buf); + + strbuf_addf(&buf, "%s/message", get_dir(opts)); + if (!file_exists(buf.buf)) { + find_commit_subject(commit_buffer, &subject); + res |= write_message(subject, strlen(subject), buf.buf, 1); + unuse_commit_buffer(commit, commit_buffer); + } + strbuf_release(&buf); + + return res; +} + +static int intend_to_amend(void) +{ + unsigned char head[20]; + char *p; + + if (get_sha1("HEAD", head)) + return error(_("cannot read HEAD")); + + p = sha1_to_hex(head); + return write_message(p, strlen(p), rebase_path_amend(), 1); +} + +static int error_with_patch(struct commit *commit, + const char *subject, int subject_len, + struct replay_opts *opts, int exit_code, int to_amend) +{ + if (make_patch(commit, opts)) + return -1; + + 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); + + return exit_code; +} + static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) { - int res; + int res = 0; setenv(GIT_REFLOG_ACTION, action_name(opts), 0); if (opts->allow_ff) @@ -1316,9 +1411,20 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) struct todo_item *item = todo_list->items + todo_list->current; if (save_todo(todo_list, opts)) return -1; - if (item->command <= TODO_REVERT) + if (item->command <= TODO_EDIT) { res = do_pick_commit(item->command, item->commit, opts); + if (item->command == TODO_EDIT) { + struct commit *commit = item->commit; + if (!res) + warning(_("stopped at %s... %.*s"), + short_commit_name(commit), + item->arg_len, item->arg); + return error_with_patch(commit, + item->arg, item->arg_len, opts, res, + !res); + } + } else if (item->command != TODO_NOOP) return error(_("unknown command %d"), item->command); @@ -1327,6 +1433,12 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) return res; } + if (is_rebase_i(opts)) { + /* Stopped in the middle, as planned? */ + if (todo_list->current < todo_list->nr) + return 0; + } + /* * Sequence of picks finished successfully; cleanup by * removing the .git/sequencer directory From c9e912505275f97acdc8f2b2c370f733089f4679 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 14:08:01 +0200 Subject: [PATCH 04/45] sequencer (rebase -i): implement the 'exec' command The 'exec' command is a little special among rebase -i's commands, as it does *not* have a SHA-1 as first parameter. Instead, everything after the `exec` command is treated as command-line to execute. Let's reuse the arg/arg_len fields of the todo_item structure (which hold the oneline for pick/edit commands) to point to the command-line. Signed-off-by: Johannes Schindelin --- sequencer.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/sequencer.c b/sequencer.c index 68e2c84803..700b7575ed 100644 --- a/sequencer.c +++ b/sequencer.c @@ -17,6 +17,7 @@ #include "argv-array.h" #include "quote.h" #include "log-tree.h" +#include "wt-status.h" #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION" @@ -664,6 +665,8 @@ enum todo_command { TODO_PICK = 0, TODO_REVERT, TODO_EDIT, + /* commands that do something else than handling a single commit */ + TODO_EXEC, /* commands that do nothing but are counted for reporting progress */ TODO_NOOP }; @@ -672,6 +675,7 @@ static const char *todo_command_strings[] = { "pick", "revert", "edit", + "exec", "noop" }; @@ -971,6 +975,12 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol) return -1; bol += padding; + if (item->command == TODO_EXEC) { + item->arg = bol; + item->arg_len = (int)(eol - bol); + return 0; + } + end_of_object_name = (char *) bol + strcspn(bol, " \t\n"); saved = *end_of_object_name; *end_of_object_name = '\0'; @@ -1396,6 +1406,47 @@ static int error_with_patch(struct commit *commit, return exit_code; } +static int do_exec(const char *command_line) +{ + 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); + + /* force re-reading of the cache */ + if (discard_cache() < 0 || read_cache() < 0) + return error(_("could not read index")); + + dirty = require_clean_work_tree("rebase", NULL, 1, 1); + + if (status) { + warning(_("execution failed: %s\n%s" + "You can fix the problem, and then run\n" + "\n" + " git rebase --continue\n" + "\n"), + command_line, + dirty ? N_("and made changes to the index and/or the " + "working tree\n") : ""); + if (status == 127) + /* command not found */ + status = 1; + } + else if (dirty) { + warning(_("execution succeeded: %s\nbut " + "left changes to the index and/or the working tree\n" + "Commit or stash your changes, and then run\n" + "\n" + " git rebase --continue\n" + "\n"), command_line); + status = 1; + } + + return status; +} + static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) { int res = 0; @@ -1425,6 +1476,14 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) !res); } } + else if (item->command == TODO_EXEC) { + char *end_of_arg = (char *)(item->arg + item->arg_len); + int saved = *end_of_arg; + + *end_of_arg = '\0'; + res = do_exec(item->arg); + *end_of_arg = saved; + } else if (item->command != TODO_NOOP) return error(_("unknown command %d"), item->command); From 207631e26c22d653f746712e7691e8c8a80b9425 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Apr 2016 15:30:26 +0200 Subject: [PATCH 05/45] sequencer (rebase -i): learn about the 'verbose' mode When calling `git rebase -i -v`, the user wants to see some statistics after the commits were rebased. Let's show some. The strbuf we use to perform that task will be used for other things in subsequent commits, hence it is declared and initialized in a wider scope than strictly needed here. Signed-off-by: Johannes Schindelin --- sequencer.c | 22 ++++++++++++++++++++++ sequencer.h | 1 + 2 files changed, 23 insertions(+) diff --git a/sequencer.c b/sequencer.c index 700b7575ed..1ab50884bd 100644 --- a/sequencer.c +++ b/sequencer.c @@ -63,6 +63,8 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha") * command-line (and are only consumed, not modified, by the sequencer). */ static GIT_PATH_FUNC(rebase_path_gpg_sign_opt, "rebase-merge/gpg_sign_opt") +static GIT_PATH_FUNC(rebase_path_orig_head, "rebase-merge/orig-head") +static GIT_PATH_FUNC(rebase_path_verbose, "rebase-merge/verbose") static inline int is_rebase_i(const struct replay_opts *opts) { @@ -1121,6 +1123,9 @@ static int read_populate_opts(struct replay_opts *opts) } strbuf_release(&buf); + if (file_exists(rebase_path_verbose())) + opts->verbose = 1; + return 0; } @@ -1493,9 +1498,26 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) } if (is_rebase_i(opts)) { + struct strbuf buf = STRBUF_INIT; + /* Stopped in the middle, as planned? */ if (todo_list->current < todo_list->nr) return 0; + + if (opts->verbose) { + const char *argv[] = { + "diff-tree", "--stat", NULL, NULL + }; + + if (!read_oneliner(&buf, rebase_path_orig_head(), 0)) + return error(_("could not read '%s'"), + rebase_path_orig_head()); + strbuf_addstr(&buf, "..HEAD"); + argv[2] = buf.buf; + run_command_v_opt(argv, RUN_GIT_CMD); + strbuf_reset(&buf); + } + strbuf_release(&buf); } /* diff --git a/sequencer.h b/sequencer.h index cb21cfddee..f885b68395 100644 --- a/sequencer.h +++ b/sequencer.h @@ -24,6 +24,7 @@ struct replay_opts { int allow_empty; int allow_empty_message; int keep_redundant_commits; + int verbose; int mainline; From 302e02038ed508e6e4cbaa554149c530e9a8407f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 Mar 2016 16:18:50 +0100 Subject: [PATCH 06/45] sequencer (rebase -i): write the 'done' file In the interactive rebase, commands that were successfully processed are not simply discarded, but appended to the 'done' file instead. This is used e.g. to display the current state to the user in the output of `git status` or the progress. Signed-off-by: Johannes Schindelin --- sequencer.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/sequencer.c b/sequencer.c index 1ab50884bd..a400c620f6 100644 --- a/sequencer.c +++ b/sequencer.c @@ -38,6 +38,12 @@ static GIT_PATH_FUNC(rebase_path, "rebase-merge") * file and written to the tail of 'done'. */ static GIT_PATH_FUNC(rebase_path_todo, "rebase-merge/git-rebase-todo") +/* + * The rebase command lines that have already been processed. A line + * is moved here when it is first handled, before any associated user + * actions. + */ +static GIT_PATH_FUNC(rebase_path_done, "rebase-merge/done") /* * A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and * GIT_AUTHOR_DATE that will be used for the commit that is currently @@ -1295,6 +1301,20 @@ static int save_todo(struct todo_list *todo_list, struct replay_opts *opts) return error_errno(_("could not write to '%s'"), todo_path); if (commit_lock_file(&todo_lock) < 0) return error(_("failed to finalize '%s'."), todo_path); + + if (is_rebase_i(opts)) { + const char *done_path = rebase_path_done(); + int fd = open(done_path, O_CREAT | O_WRONLY | O_APPEND, 0666); + int prev_offset = !next ? 0 : + todo_list->items[next - 1].offset_in_buf; + + if (offset > prev_offset && write_in_full(fd, + todo_list->buf.buf + prev_offset, + offset - prev_offset) < 0) + return error_errno(_("could not write to '%s'"), + done_path); + close(fd); + } return 0; } From 829dfa1964a4113056f6f8bb02a791c3f0dc612d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 23 May 2016 16:49:51 +0200 Subject: [PATCH 07/45] sequencer (rebase -i): add support for the 'fixup' and 'squash' commands This is a huge patch, and at the same time a huge step forward to execute the performance-critical parts of the interactive rebase in a builtin command. Since 'fixup' and 'squash' are not only similar, but also need to know about each other (we want to reduce a series of fixups/squashes into a single, final commit message edit, from the user's point of view), we really have to implement them both at the same time. Most of the actual work is done by the existing code path that already handles the "pick" and the "edit" commands; We added support for other features (e.g. to amend the commit message) in the patches leading up to this one, yet there are still quite a few bits in this patch that simply would not make sense as individual patches (such as: determining whether there was anything to "fix up" in the "todo" script, etc). In theory, it would be possible to reuse the fast-forward code path also for the fixup and the squash code paths, but in practice this would make the code less readable. The end result cannot be fast-forwarded anyway, therefore let's just extend the cherry-picking code path for now. Since the sequencer parses the entire `git-rebase-todo` script in one go, fixup or squash commands without a preceding pick can be reported early (in git-rebase--interactive, we could only report such errors just before executing the fixup/squash). Signed-off-by: Johannes Schindelin --- sequencer.c | 232 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 222 insertions(+), 10 deletions(-) diff --git a/sequencer.c b/sequencer.c index a400c620f6..12645b8a0b 100644 --- a/sequencer.c +++ b/sequencer.c @@ -44,6 +44,35 @@ static GIT_PATH_FUNC(rebase_path_todo, "rebase-merge/git-rebase-todo") * actions. */ static GIT_PATH_FUNC(rebase_path_done, "rebase-merge/done") +/* + * The commit message that is planned to be used for any changes that + * need to be committed following a user interaction. + */ +static GIT_PATH_FUNC(rebase_path_message, "rebase-merge/message") +/* + * The file into which is accumulated the suggested commit message for + * squash/fixup commands. When the first of a series of squash/fixups + * is seen, the file is created and the commit message from the + * previous commit and from the first squash/fixup commit are written + * to it. The commit message for each subsequent squash/fixup commit + * is appended to the file as it is processed. + * + * The first line of the file is of the form + * # This is a combination of $count commits. + * where $count is the number of commits whose messages have been + * written to the file so far (including the initial "pick" commit). + * Each time that a commit message is processed, this line is read and + * updated. It is deleted just before the combined commit is made. + */ +static GIT_PATH_FUNC(rebase_path_squash_msg, "rebase-merge/message-squash") +/* + * If the current series of squash/fixups has not yet included a squash + * command, then this file exists and holds the commit message of the + * original "pick" commit. (If the series ends without a "squash" + * command, then this can be used as the commit message of the combined + * commit without opening the editor.) + */ +static GIT_PATH_FUNC(rebase_path_fixup_msg, "rebase-merge/message-fixup") /* * A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and * GIT_AUTHOR_DATE that will be used for the commit that is currently @@ -673,6 +702,8 @@ enum todo_command { TODO_PICK = 0, TODO_REVERT, TODO_EDIT, + TODO_FIXUP, + TODO_SQUASH, /* commands that do something else than handling a single commit */ TODO_EXEC, /* commands that do nothing but are counted for reporting progress */ @@ -683,6 +714,8 @@ static const char *todo_command_strings[] = { "pick", "revert", "edit", + "fixup", + "squash", "exec", "noop" }; @@ -694,16 +727,119 @@ static const char *command_to_string(const enum todo_command command) die("Unknown command: %d", command); } +static int is_fixup(enum todo_command command) +{ + return command == TODO_FIXUP || command == TODO_SQUASH; +} + +static int update_squash_messages(enum todo_command command, + struct commit *commit, struct replay_opts *opts) +{ + struct strbuf buf = STRBUF_INIT; + int count, res; + const char *message, *body; + + if (file_exists(rebase_path_squash_msg())) { + char *p, *p2; + + if (strbuf_read_file(&buf, rebase_path_squash_msg(), 2048) <= 0) + return error(_("could not read '%s'"), + rebase_path_squash_msg()); + + if (buf.buf[0] == '\n' || !skip_prefix(buf.buf + 1, + " This is a combination of ", + (const char **)&p)) + return error(_("unexpected 1st line of squash message:" + "\n\n\t%.*s"), + (int)(strchrnul(buf.buf, '\n') - buf.buf), + buf.buf); + count = strtol(p, &p2, 10); + + if (count < 1 || *p2 != ' ') + return error(_("invalid 1st line of squash message:\n" + "\n\t%.*s"), + (int)(strchrnul(buf.buf, '\n') - buf.buf), + buf.buf); + + sprintf((char *)p, "%d", ++count); + if (!*p2) + *p2 = ' '; + else { + *(++p2) = 'c'; + strbuf_insert(&buf, p2 - buf.buf, " ", 1); + } + } + else { + unsigned char head[20]; + struct commit *head_commit; + const char *head_message, *body; + + if (get_sha1("HEAD", head)) + return error(_("need a HEAD to fixup")); + if (!(head_commit = lookup_commit_reference(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")); + + body = strstr(head_message, "\n\n"); + if (!body) + body = ""; + else + body = skip_blank_lines(body + 2); + if (write_message(body, strlen(body), + rebase_path_fixup_msg(), 0)) + return error(_("cannot write '%s'"), + rebase_path_fixup_msg()); + + count = 2; + strbuf_addf(&buf, _("%c This is a combination of 2 commits.\n" + "%c The first commit's message is:\n\n%s"), + comment_line_char, comment_line_char, body); + + unuse_commit_buffer(head_commit, head_message); + } + + if (!(message = get_commit_buffer(commit, NULL))) + return error(_("could not read commit message of %s"), + oid_to_hex(&commit->object.oid)); + body = strstr(message, "\n\n"); + if (!body) + body = ""; + else + body = skip_blank_lines(body + 2); + + if (command == TODO_SQUASH) { + unlink(rebase_path_fixup_msg()); + strbuf_addf(&buf, _("\n%c This is the commit message #%d:\n" + "\n%s"), + comment_line_char, count, body); + } + else if (command == TODO_FIXUP) { + strbuf_addf(&buf, _("\n%c The commit message #%d " + "will be skipped:\n\n"), + comment_line_char, count); + strbuf_add_commented_lines(&buf, body, strlen(body)); + } + else + return error(_("unknown command: %d"), command); + unuse_commit_buffer(commit, message); + + res = write_message(buf.buf, buf.len, rebase_path_squash_msg(), 0); + strbuf_release(&buf); + return res; +} static int do_pick_commit(enum todo_command command, struct commit *commit, - struct replay_opts *opts) + struct replay_opts *opts, int final_fixup) { + int edit = opts->edit, cleanup_commit_message = 0; + const char *msg_file = edit ? NULL : git_path_merge_msg(); unsigned char head[20]; struct commit *base, *next, *parent; const char *base_label, *next_label; struct commit_message msg = { NULL, NULL, NULL, NULL }; struct strbuf msgbuf = STRBUF_INIT; - int res, unborn = 0, allow; + int res, unborn = 0, amend = 0, allow; if (opts->no_commit) { /* @@ -749,7 +885,7 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, else parent = commit->parents->item; - if (opts->allow_ff && + if (opts->allow_ff && !is_fixup(command) && ((parent && !hashcmp(parent->object.oid.hash, head)) || (!parent && unborn))) return fast_forward_to(commit->object.oid.hash, head, unborn, opts); @@ -813,6 +949,28 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, } } + if (is_fixup(command)) { + if (update_squash_messages(command, commit, opts)) + return -1; + amend = 1; + if (!final_fixup) + msg_file = rebase_path_squash_msg(); + else if (file_exists(rebase_path_fixup_msg())) { + cleanup_commit_message = 1; + msg_file = rebase_path_fixup_msg(); + } + else { + const char *dest = git_path("SQUASH_MSG"); + unlink(dest); + if (copy_file(dest, rebase_path_squash_msg(), 0666)) + return error(_("could not rename '%s' to '%s'"), + rebase_path_squash_msg(), dest); + unlink(git_path("MERGE_MSG")); + msg_file = dest; + edit = 1; + } + } + if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) { res = do_recursive_merge(base, next, base_label, next_label, head, &msgbuf, opts); @@ -868,8 +1026,13 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, goto leave; } if (!opts->no_commit) - res = run_git_commit(opts->edit ? NULL : git_path_merge_msg(), - opts, allow, opts->edit, 0, 0); + res = run_git_commit(msg_file, opts, allow, edit, amend, + cleanup_commit_message); + + if (!res && final_fixup) { + unlink(rebase_path_fixup_msg()); + unlink(rebase_path_squash_msg()); + } leave: free_message(commit, &msg); @@ -1009,7 +1172,7 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list) { struct todo_item *item; char *p = buf, *next_p; - int i, res = 0; + int i, res = 0, fixup_okay = file_exists(rebase_path_done()); for (i = 1; *p; i++, p = next_p) { char *eol = strchrnul(p, '\n'); @@ -1024,8 +1187,16 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list) if (parse_insn_line(item, p, eol)) { res = error(_("invalid line %d: %.*s"), i, (int)(eol - p), p); - item->command = -1; + item->command = TODO_NOOP; } + + if (fixup_okay) + ; /* do nothing */ + else if (is_fixup(item->command)) + return error(_("cannot '%s' without a previous commit"), + command_to_string(item->command)); + else if (item->command != TODO_NOOP) + fixup_okay = 1; } if (!todo_list->nr) return error(_("no commits parsed.")); @@ -1431,6 +1602,20 @@ static int error_with_patch(struct commit *commit, return exit_code; } +static int error_failed_squash(struct commit *commit, + struct replay_opts *opts, int subject_len, const char *subject) +{ + if (rename(rebase_path_squash_msg(), rebase_path_message())) + return error(_("could not rename '%s' to '%s'"), + rebase_path_squash_msg(), rebase_path_message()); + unlink(rebase_path_fixup_msg()); + unlink(git_path("MERGE_MSG")); + if (copy_file(git_path("MERGE_MSG"), rebase_path_message(), 0666)) + return error(_("could not copy '%s' to '%s'"), + rebase_path_message(), git_path("MERGE_MSG")); + return error_with_patch(commit, subject, subject_len, opts, 1, 0); +} + static int do_exec(const char *command_line) { const char *child_argv[] = { NULL, NULL }; @@ -1472,6 +1657,21 @@ static int do_exec(const char *command_line) return status; } +static int is_final_fixup(struct todo_list *todo_list) +{ + int i = todo_list->current; + + if (!is_fixup(todo_list->items[i].command)) + return 0; + + while (++i < todo_list->nr) + if (is_fixup(todo_list->items[i].command)) + return 0; + else if (todo_list->items[i].command < TODO_NOOP) + break; + return 1; +} + static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) { int res = 0; @@ -1487,9 +1687,15 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) struct todo_item *item = todo_list->items + todo_list->current; if (save_todo(todo_list, opts)) return -1; - if (item->command <= TODO_EDIT) { + if (is_rebase_i(opts)) { + unlink(rebase_path_message()); + unlink(rebase_path_author_script()); + unlink(rebase_path_stopped_sha()); + unlink(rebase_path_amend()); + } + if (item->command <= TODO_SQUASH) { res = do_pick_commit(item->command, item->commit, - opts); + opts, is_final_fixup(todo_list)); if (item->command == TODO_EDIT) { struct commit *commit = item->commit; if (!res) @@ -1500,6 +1706,12 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) item->arg, item->arg_len, opts, res, !res); } + if (res && is_fixup(item->command)) { + if (res == 1) + intend_to_amend(); + return error_failed_squash(item->commit, opts, + item->arg_len, item->arg); + } } else if (item->command == TODO_EXEC) { char *end_of_arg = (char *)(item->arg + item->arg_len); @@ -1594,7 +1806,7 @@ static int single_pick(struct commit *cmit, struct replay_opts *opts) { setenv(GIT_REFLOG_ACTION, action_name(opts), 0); return do_pick_commit(opts->action == REPLAY_PICK ? - TODO_PICK : TODO_REVERT, cmit, opts); + TODO_PICK : TODO_REVERT, cmit, opts, 0); } int sequencer_pick_revisions(struct replay_opts *opts) From 811752ef361ece40da70bcaf0d832450170c2aeb Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 14:08:09 +0200 Subject: [PATCH 08/45] sequencer (rebase -i): implement the short commands For users' convenience, most rebase commands can be abbreviated, e.g. 'p' instead of 'pick' and 'x' instead of 'exec'. Let's teach the sequencer to handle those abbreviated commands just fine. Signed-off-by: Johannes Schindelin --- sequencer.c | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/sequencer.c b/sequencer.c index 12645b8a0b..2d94092e72 100644 --- a/sequencer.c +++ b/sequencer.c @@ -710,20 +710,23 @@ enum todo_command { TODO_NOOP }; -static const char *todo_command_strings[] = { - "pick", - "revert", - "edit", - "fixup", - "squash", - "exec", - "noop" +static struct { + char c; + const char *str; +} todo_command_info[] = { + { 'p', "pick" }, + { 0, "revert" }, + { 'e', "edit" }, + { 'f', "fixup" }, + { 's', "squash" }, + { 'x', "exec" }, + { 0, "noop" } }; static const char *command_to_string(const enum todo_command command) { - if ((size_t)command < ARRAY_SIZE(todo_command_strings)) - return todo_command_strings[command]; + if ((size_t)command < ARRAY_SIZE(todo_command_info)) + return todo_command_info[command].str; die("Unknown command: %d", command); } @@ -1125,12 +1128,17 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol) return 0; } - for (i = 0; i < ARRAY_SIZE(todo_command_strings); i++) - if (skip_prefix(bol, todo_command_strings[i], &bol)) { + for (i = 0; i < ARRAY_SIZE(todo_command_info); i++) + if (skip_prefix(bol, todo_command_info[i].str, &bol)) { item->command = i; break; } - if (i >= ARRAY_SIZE(todo_command_strings)) + else if (bol[1] == ' ' && *bol == todo_command_info[i].c) { + bol++; + item->command = i; + break; + } + if (i >= ARRAY_SIZE(todo_command_info)) return -1; if (item->command == TODO_NOOP) { @@ -1325,7 +1333,7 @@ static int walk_revs_populate_todo(struct todo_list *todo_list, { enum todo_command command = opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT; - const char *command_string = todo_command_strings[command]; + const char *command_string = todo_command_info[command].str; struct commit *commit; if (prepare_revs(opts)) From c2c27356c03c0b67689e8e0abb176b12e8f4ef3f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 14:11:12 +0200 Subject: [PATCH 09/45] sequencer (rebase -i): write an author-script file When the interactive rebase aborts, it writes out an author-script file to record the author information for the current commit. As we are about to teach the sequencer how to perform the actions behind an interactive rebase, it needs to write those author-script files, too. Signed-off-by: Johannes Schindelin --- sequencer.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index 2d94092e72..734ce2aabd 100644 --- a/sequencer.c +++ b/sequencer.c @@ -515,6 +515,53 @@ static int is_index_unchanged(void) return !hashcmp(active_cache_tree->sha1, head_commit->tree->object.oid.hash); } +static int write_author_script(const char *message) +{ + struct strbuf buf = STRBUF_INIT; + const char *eol; + int res; + + for (;;) + if (!*message || starts_with(message, "\n")) { +missing_author: + /* Missing 'author' line? */ + unlink(rebase_path_author_script()); + return 0; + } + else if (skip_prefix(message, "author ", &message)) + break; + else if ((eol = strchr(message, '\n'))) + message = eol + 1; + else + goto missing_author; + + strbuf_addstr(&buf, "GIT_AUTHOR_NAME='"); + while (*message && *message != '\n' && *message != '\r') + if (skip_prefix(message, " <", &message)) + break; + else if (*message != '\'') + strbuf_addch(&buf, *(message++)); + else + strbuf_addf(&buf, "'\\\\%c'", *(message++)); + strbuf_addstr(&buf, "'\nGIT_AUTHOR_EMAIL='"); + while (*message && *message != '\n' && *message != '\r') + if (skip_prefix(message, "> ", &message)) + break; + else if (*message != '\'') + strbuf_addch(&buf, *(message++)); + else + strbuf_addf(&buf, "'\\\\%c'", *(message++)); + strbuf_addstr(&buf, "'\nGIT_AUTHOR_DATE='@"); + while (*message && *message != '\n' && *message != '\r') + if (*message != '\'') + strbuf_addch(&buf, *(message++)); + else + strbuf_addf(&buf, "'\\\\%c'", *(message++)); + res = write_message(buf.buf, buf.len, rebase_path_author_script(), 1); + strbuf_release(&buf); + return res; +} + /* * Read the author-script file into an environment block, ready for use in * run_command(), that can be free()d afterwards. @@ -974,7 +1021,9 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, } } - if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) { + if (is_rebase_i(opts) && write_author_script(msg.message) < 0) + res = -1; + else if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) { res = do_recursive_merge(base, next, base_label, next_label, head, &msgbuf, opts); if (res < 0) From 50c4d3cfe3176dabea7040402066a25c92669804 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 9 Mar 2016 17:12:20 +0100 Subject: [PATCH 10/45] sequencer (rebase -i): allow continuing with staged changes When an interactive rebase is interrupted, the user may stage changes before continuing, and we need to commit those changes in that case. Signed-off-by: Johannes Schindelin --- sequencer.c | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/sequencer.c b/sequencer.c index 734ce2aabd..3cc36d690a 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1826,6 +1826,42 @@ static int continue_single_pick(void) return run_command_v_opt(argv, RUN_GIT_CMD); } +static int commit_staged_changes(struct replay_opts *opts) +{ + int amend = 0; + + if (has_unstaged_changes(1)) + return error(_("cannot rebase: You have unstaged changes.")); + if (!has_uncommitted_changes(0)) + return 0; + + if (file_exists(rebase_path_amend())) { + struct strbuf rev = STRBUF_INIT; + unsigned char head[20], to_amend[20]; + + if (get_sha1("HEAD", head)) + return error(_("cannot amend non-existing commit")); + if (!read_oneliner(&rev, rebase_path_amend(), 0)) + return error(_("invalid file: '%s'"), rebase_path_amend()); + if (get_sha1_hex(rev.buf, to_amend)) + return error(_("invalid contents: '%s'"), + rebase_path_amend()); + if (hashcmp(head, to_amend)) + return error(_("\nYou have uncommitted changes in your " + "working tree. Please, commit them\n" + "first and then run 'git rebase " + "--continue' again.")); + + strbuf_release(&rev); + amend = 1; + } + + if (run_git_commit(rebase_path_message(), opts, 1, 1, amend, 0)) + return error(_("could not commit staged changes.")); + unlink(rebase_path_amend()); + return 0; +} + int sequencer_continue(struct replay_opts *opts) { struct todo_list todo_list = TODO_LIST_INIT; @@ -1834,6 +1870,10 @@ int sequencer_continue(struct replay_opts *opts) if (read_and_refresh_cache(opts)) return -1; + if (is_rebase_i(opts)) { + if (commit_staged_changes(opts)) + return -1; + } if (!file_exists(get_todo_path(opts))) return continue_single_pick(); if (read_populate_opts(opts)) From bc486cd5f9e6661ffe0439a85dd925a598701db7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 9 Apr 2016 16:29:21 +0200 Subject: [PATCH 11/45] sequencer (rebase -i): remove CHERRY_PICK_HEAD when no longer needed The scripted version of the interactive rebase already does that. Signed-off-by: Johannes Schindelin --- sequencer.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index 3cc36d690a..09d160d4a7 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1832,8 +1832,13 @@ static int commit_staged_changes(struct replay_opts *opts) if (has_unstaged_changes(1)) return error(_("cannot rebase: You have unstaged changes.")); - if (!has_uncommitted_changes(0)) + if (!has_uncommitted_changes(0)) { + const char *cherry_pick_head = git_path("CHERRY_PICK_HEAD"); + + if (file_exists(cherry_pick_head) && unlink(cherry_pick_head)) + return error(_("could not remove CHERRY_PICK_HEAD")); return 0; + } if (file_exists(rebase_path_amend())) { struct strbuf rev = STRBUF_INIT; From 8c8c8750ad36b327b359c5bf52d52db036dde11b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 31 Aug 2016 08:43:51 +0200 Subject: [PATCH 12/45] sequencer (rebase -i): skip some revert/cherry-pick specific code path When a cherry-pick continues without a "todo script", the intention is simply to pick a single commit. However, when an interactive rebase is continued without a "todo script", it means that the last command has been completed and that we now need to clean up. This commit guards the revert/cherry-pick specific steps so that they are not executed in rebase -i mode. Signed-off-by: Johannes Schindelin --- sequencer.c | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/sequencer.c b/sequencer.c index 09d160d4a7..b3c77ab569 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1879,25 +1879,28 @@ int sequencer_continue(struct replay_opts *opts) if (commit_staged_changes(opts)) return -1; } - if (!file_exists(get_todo_path(opts))) + else if (!file_exists(get_todo_path(opts))) return continue_single_pick(); if (read_populate_opts(opts)) return -1; if ((res = read_populate_todo(&todo_list, opts))) goto release_todo_list; - /* Verify that the conflict has been resolved */ - if (file_exists(git_path_cherry_pick_head()) || - file_exists(git_path_revert_head())) { - res = continue_single_pick(); - if (res) + if (!is_rebase_i(opts)) { + /* Verify that the conflict has been resolved */ + if (file_exists(git_path_cherry_pick_head()) || + file_exists(git_path_revert_head())) { + res = continue_single_pick(); + if (res) + goto release_todo_list; + } + if (index_differs_from("HEAD", 0, 0)) { + res = error_dirty_index(opts); goto release_todo_list; + } + todo_list.current++; } - if (index_differs_from("HEAD", 0, 0)) { - res = error_dirty_index(opts); - goto release_todo_list; - } - todo_list.current++; + res = pick_commits(&todo_list, opts); release_todo_list: todo_list_release(&todo_list); From 13ae05b1207d124f968346bf4ae2152299b884b2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 14:11:54 +0200 Subject: [PATCH 13/45] sequencer (rebase -i): the todo can be empty when continuing When the last command of an interactive rebase fails, the user needs to resolve the problem and then continue the interactive rebase. Naturally, the todo script is empty by then. So let's not complain about that! To that end, let's move that test out of the function that parses the todo script, and into the more high-level function read_populate_todo(). This is also necessary by now because the lower-level parse_insn_buffer() has no idea whether we are performing an interactive rebase or not. Signed-off-by: Johannes Schindelin --- sequencer.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sequencer.c b/sequencer.c index b3c77ab569..fd9e2d7005 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1255,8 +1255,7 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list) else if (item->command != TODO_NOOP) fixup_okay = 1; } - if (!todo_list->nr) - return error(_("no commits parsed.")); + return res; } @@ -1280,6 +1279,10 @@ static int read_populate_todo(struct todo_list *todo_list, if (res) return error(_("unusable instruction sheet: '%s'"), todo_file); + if (!todo_list->nr && + (!is_rebase_i(opts) || !file_exists(rebase_path_done()))) + return error(_("no commits parsed.")); + if (!is_rebase_i(opts)) { enum todo_command valid = opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT; From 9fcd5645e056f2e5572d0ecc125ffbf73ee922d4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 14:12:54 +0200 Subject: [PATCH 14/45] sequencer (rebase -i): update refs after a successful rebase An interactive rebase operates on a detached HEAD (to keep the reflog of the original branch relatively clean), and updates the branch only at the end. Now that the sequencer learns to perform interactive rebases, it also needs to learn the trick to update the branch before removing the directory containing the state of the interactive rebase. Signed-off-by: Johannes Schindelin --- sequencer.c | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index fd9e2d7005..e5880ada70 100644 --- a/sequencer.c +++ b/sequencer.c @@ -100,6 +100,8 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha") static GIT_PATH_FUNC(rebase_path_gpg_sign_opt, "rebase-merge/gpg_sign_opt") static GIT_PATH_FUNC(rebase_path_orig_head, "rebase-merge/orig-head") static GIT_PATH_FUNC(rebase_path_verbose, "rebase-merge/verbose") +static GIT_PATH_FUNC(rebase_path_head_name, "rebase-merge/head-name") +static GIT_PATH_FUNC(rebase_path_onto, "rebase-merge/onto") static inline int is_rebase_i(const struct replay_opts *opts) { @@ -1790,12 +1792,39 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) } if (is_rebase_i(opts)) { - struct strbuf buf = STRBUF_INIT; + struct strbuf head_ref = STRBUF_INIT, buf = STRBUF_INIT; /* Stopped in the middle, as planned? */ if (todo_list->current < todo_list->nr) return 0; + if (read_oneliner(&head_ref, rebase_path_head_name(), 0) && + starts_with(head_ref.buf, "refs/")) { + unsigned char head[20], orig[20]; + + if (get_sha1("HEAD", head)) + return error(_("cannot read HEAD")); + if (!read_oneliner(&buf, rebase_path_orig_head(), 0) || + get_sha1_hex(buf.buf, orig)) + return error(_("could not read orig-head")); + strbuf_addf(&buf, "rebase -i (finish): %s onto ", + head_ref.buf); + if (!read_oneliner(&buf, rebase_path_onto(), 0)) + return error(_("could not read 'onto'")); + if (update_ref(buf.buf, head_ref.buf, head, orig, + REF_NODEREF, UPDATE_REFS_MSG_ON_ERR)) + return error(_("could not update %s"), + head_ref.buf); + strbuf_reset(&buf); + strbuf_addf(&buf, + "rebase -i (finish): returning to %s", + head_ref.buf); + if (create_symref("HEAD", head_ref.buf, buf.buf)) + return error(_("could not update HEAD to %s"), + head_ref.buf); + strbuf_reset(&buf); + } + if (opts->verbose) { const char *argv[] = { "diff-tree", "--stat", NULL, NULL @@ -1810,6 +1839,7 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) strbuf_reset(&buf); } strbuf_release(&buf); + strbuf_release(&head_ref); } /* From 620f196652023806ab712969c338606911a25f21 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 10 Mar 2016 21:47:32 +0100 Subject: [PATCH 15/45] sequencer (rebase -i): leave a patch upon error When doing an interactive rebase, we want to leave a 'patch' file for further inspection by the user (even if we never tried to actually apply that patch, since we're cherry-picking instead). Signed-off-by: Johannes Schindelin --- sequencer.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sequencer.c b/sequencer.c index e5880ada70..c588809f26 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1774,6 +1774,9 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) return error_failed_squash(item->commit, opts, item->arg_len, item->arg); } + else if (res && is_rebase_i(opts)) + return res | error_with_patch(item->commit, + item->arg, item->arg_len, opts, res, 0); } else if (item->command == TODO_EXEC) { char *end_of_arg = (char *)(item->arg + item->arg_len); From 1e4c5fd5f75117162c545a3b6b4f0f92e0c63636 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 14:13:31 +0200 Subject: [PATCH 16/45] sequencer (rebase -i): implement the 'reword' command This is now trivial, as all the building blocks are in place: all we need to do is to flip the "edit" switch when committing. Signed-off-by: Johannes Schindelin --- sequencer.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sequencer.c b/sequencer.c index c588809f26..393d0089b7 100644 --- a/sequencer.c +++ b/sequencer.c @@ -751,6 +751,7 @@ enum todo_command { TODO_PICK = 0, TODO_REVERT, TODO_EDIT, + TODO_REWORD, TODO_FIXUP, TODO_SQUASH, /* commands that do something else than handling a single commit */ @@ -766,6 +767,7 @@ static struct { { 'p', "pick" }, { 0, "revert" }, { 'e', "edit" }, + { 'r', "reword" }, { 'f', "fixup" }, { 's', "squash" }, { 'x', "exec" }, @@ -1001,7 +1003,9 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, } } - if (is_fixup(command)) { + if (command == TODO_REWORD) + edit = 1; + else if (is_fixup(command)) { if (update_squash_messages(command, commit, opts)) return -1; amend = 1; @@ -1776,7 +1780,8 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) } else if (res && is_rebase_i(opts)) return res | error_with_patch(item->commit, - item->arg, item->arg_len, opts, res, 0); + item->arg, item->arg_len, opts, res, + item->command == TODO_REWORD); } else if (item->command == TODO_EXEC) { char *end_of_arg = (char *)(item->arg + item->arg_len); From b029eaf4d0997de72b5d892bc916b729fbb1235e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 23 May 2016 16:56:21 +0200 Subject: [PATCH 17/45] sequencer (rebase -i): allow fast-forwarding for edit/reword The sequencer already knew how to fast-forward instead of cherry-picking, if possible. We want to continue to do this, of course, but in case of the 'reword' command, we will need to call `git commit` after fast-forwarding. Signed-off-by: Johannes Schindelin --- sequencer.c | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/sequencer.c b/sequencer.c index 393d0089b7..c240b7ec58 100644 --- a/sequencer.c +++ b/sequencer.c @@ -893,7 +893,7 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, const char *base_label, *next_label; struct commit_message msg = { NULL, NULL, NULL, NULL }; struct strbuf msgbuf = STRBUF_INIT; - int res, unborn = 0, amend = 0, allow; + int res, unborn = 0, amend = 0, allow = 0; if (opts->no_commit) { /* @@ -939,11 +939,23 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, else parent = commit->parents->item; + if (get_message(commit, &msg) != 0) + return error(_("cannot get commit message for %s"), + oid_to_hex(&commit->object.oid)); + if (opts->allow_ff && !is_fixup(command) && ((parent && !hashcmp(parent->object.oid.hash, head)) || - (!parent && unborn))) - return fast_forward_to(commit->object.oid.hash, head, unborn, opts); - + (!parent && unborn))) { + if (is_rebase_i(opts)) + write_author_script(msg.message); + res = fast_forward_to(commit->object.oid.hash, head, unborn, + opts); + if (res || command != TODO_REWORD) + goto leave; + edit = amend = 1; + msg_file = NULL; + goto fast_forward_edit; + } if (parent && parse_commit(parent) < 0) /* TRANSLATORS: The first %s will be a "todo" command like "revert" or "pick", the second %s a SHA1. */ @@ -951,10 +963,6 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, command_to_string(command), oid_to_hex(&parent->object.oid)); - if (get_message(commit, &msg) != 0) - return error(_("cannot get commit message for %s"), - oid_to_hex(&commit->object.oid)); - /* * "commit" is an existing commit. We would want to apply * the difference it introduces since its first parent "prev" @@ -1084,6 +1092,7 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, goto leave; } if (!opts->no_commit) +fast_forward_edit: res = run_git_commit(msg_file, opts, allow, edit, amend, cleanup_commit_message); From 503cec52e9fc8dc46c07f6c5e9a55a9ef29bc644 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 9 Apr 2016 16:27:51 +0200 Subject: [PATCH 18/45] sequencer (rebase -i): refactor setting the reflog message This makes the code DRYer, with the obvious benefit that we can enhance the code further in a single place. We can also reuse the functionality elsewhere by calling this new function. Signed-off-by: Johannes Schindelin --- sequencer.c | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/sequencer.c b/sequencer.c index c240b7ec58..a1c86740af 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1747,6 +1747,26 @@ static int is_final_fixup(struct todo_list *todo_list) return 1; } +static const char *reflog_message(struct replay_opts *opts, + const char *sub_action, const char *fmt, ...) +{ + va_list ap; + static struct strbuf buf = STRBUF_INIT; + + va_start(ap, fmt); + strbuf_reset(&buf); + strbuf_addstr(&buf, action_name(opts)); + if (sub_action) + strbuf_addf(&buf, " (%s)", sub_action); + if (fmt) { + strbuf_addstr(&buf, ": "); + strbuf_vaddf(&buf, fmt, ap); + } + va_end(ap); + + return buf.buf; +} + static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) { int res = 0; @@ -1817,6 +1837,7 @@ 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]; if (get_sha1("HEAD", head)) @@ -1824,19 +1845,17 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) if (!read_oneliner(&buf, rebase_path_orig_head(), 0) || get_sha1_hex(buf.buf, orig)) return error(_("could not read orig-head")); - strbuf_addf(&buf, "rebase -i (finish): %s onto ", - head_ref.buf); if (!read_oneliner(&buf, rebase_path_onto(), 0)) return error(_("could not read 'onto'")); - if (update_ref(buf.buf, head_ref.buf, head, orig, + msg = reflog_message(opts, "finish", "%s onto %s", + head_ref.buf, buf.buf); + if (update_ref(msg, head_ref.buf, head, orig, REF_NODEREF, UPDATE_REFS_MSG_ON_ERR)) return error(_("could not update %s"), head_ref.buf); - strbuf_reset(&buf); - strbuf_addf(&buf, - "rebase -i (finish): returning to %s", + msg = reflog_message(opts, "finish", "returning to %s", head_ref.buf); - if (create_symref("HEAD", head_ref.buf, buf.buf)) + if (create_symref("HEAD", head_ref.buf, msg)) return error(_("could not update HEAD to %s"), head_ref.buf); strbuf_reset(&buf); From f26844580b30e19d817c58596db07b6f07e8be51 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 9 Apr 2016 16:28:07 +0200 Subject: [PATCH 19/45] sequencer (rebase -i): set the reflog message consistently We already used the same reflog message as the scripted version of rebase -i when finishing. With this commit, we do that also for all the commands before that. Signed-off-by: Johannes Schindelin --- sequencer.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sequencer.c b/sequencer.c index a1c86740af..303de0aac9 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1789,6 +1789,10 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) unlink(rebase_path_amend()); } if (item->command <= TODO_SQUASH) { + if (is_rebase_i(opts)) + setenv("GIT_REFLOG_ACTION", reflog_message(opts, + command_to_string(item->command), NULL), + 1); res = do_pick_commit(item->command, item->commit, opts, is_final_fixup(todo_list)); if (item->command == TODO_EDIT) { From ba685024e27e9b50e996a6cee0a60d148b874a2d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Apr 2016 17:26:31 +0200 Subject: [PATCH 20/45] sequencer (rebase -i): copy commit notes at end When rebasing commits that have commit notes attached, the interactive rebase rewrites those notes faithfully at the end. The sequencer must do this, too, if it wishes to do interactive rebase's job. Signed-off-by: Johannes Schindelin --- sequencer.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/sequencer.c b/sequencer.c index 303de0aac9..79c5efecf9 100644 --- a/sequencer.c +++ b/sequencer.c @@ -93,6 +93,15 @@ static GIT_PATH_FUNC(rebase_path_amend, "rebase-merge/amend") * the long commit name of the corresponding patch. */ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha") +/* + * For the post-rewrite hook, we make a list of rewritten commits and + * their new sha1s. The rewritten-pending list keeps the sha1s of + * commits that have been processed, but not committed yet, + * e.g. because they are waiting for a 'squash' command. + */ +static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list") +static GIT_PATH_FUNC(rebase_path_rewritten_pending, + "rebase-merge/rewritten-pending") /* * The following files are written by git-rebase just after parsing the * command-line (and are only consumed, not modified, by the sequencer). @@ -883,6 +892,43 @@ static int update_squash_messages(enum todo_command command, return res; } +static void flush_rewritten_pending(void) { + struct strbuf buf = STRBUF_INIT; + unsigned char newsha1[20]; + FILE *out; + + if (strbuf_read_file(&buf, rebase_path_rewritten_pending(), 82) > 0 && + !get_sha1("HEAD", newsha1) && + (out = fopen(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)); + if (!*eol) + break; + bol = eol + 1; + } + fclose(out); + unlink(rebase_path_rewritten_pending()); + } +} + +static void record_in_rewritten(struct object_id *oid, + enum todo_command next_command) { + FILE *out = fopen(rebase_path_rewritten_pending(), "a"); + + if (!out) + return; + + fprintf(out, "%s\n", oid_to_hex(oid)); + fclose(out); + + if (!is_fixup(next_command)) + flush_rewritten_pending(); +} + static int do_pick_commit(enum todo_command command, struct commit *commit, struct replay_opts *opts, int final_fixup) { @@ -1747,6 +1793,17 @@ static int is_final_fixup(struct todo_list *todo_list) return 1; } +static enum todo_command peek_command(struct todo_list *todo_list, int offset) +{ + int i; + + for (i = todo_list->current + offset; i < todo_list->nr; i++) + if (todo_list->items[i].command != TODO_NOOP) + return todo_list->items[i].command; + + return -1; +} + static const char *reflog_message(struct replay_opts *opts, const char *sub_action, const char *fmt, ...) { @@ -1805,6 +1862,9 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) item->arg, item->arg_len, opts, res, !res); } + if (is_rebase_i(opts) && !res) + record_in_rewritten(&item->commit->object.oid, + peek_command(todo_list, 1)); if (res && is_fixup(item->command)) { if (res == 1) intend_to_amend(); @@ -1834,6 +1894,7 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) if (is_rebase_i(opts)) { struct strbuf head_ref = STRBUF_INIT, buf = STRBUF_INIT; + struct stat st; /* Stopped in the middle, as planned? */ if (todo_list->current < todo_list->nr) @@ -1878,6 +1939,20 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) run_command_v_opt(argv, RUN_GIT_CMD); strbuf_reset(&buf); } + flush_rewritten_pending(); + if (!stat(rebase_path_rewritten_list(), &st) && + st.st_size > 0) { + struct child_process child = CHILD_PROCESS_INIT; + + child.in = open(rebase_path_rewritten_list(), O_RDONLY); + child.git_cmd = 1; + argv_array_push(&child.args, "notes"); + argv_array_push(&child.args, "copy"); + argv_array_push(&child.args, "--for-rewrite=rebase"); + /* we don't care if this copying failed */ + run_command(&child); + } + strbuf_release(&buf); strbuf_release(&head_ref); } From fb4d72f3119efa5d28516cf1ecf7a3615c946656 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 Apr 2016 17:30:25 +0200 Subject: [PATCH 21/45] sequencer (rebase -i): record interrupted commits in rewritten, too When continuing after a `pick` command failed, we want that commit to show up in the rewritten-list (and its notes to be rewritten), too. Signed-off-by: Johannes Schindelin --- sequencer.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sequencer.c b/sequencer.c index 79c5efecf9..e896a92346 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2048,6 +2048,17 @@ int sequencer_continue(struct replay_opts *opts) } todo_list.current++; } + else if (file_exists(rebase_path_stopped_sha())) { + struct strbuf buf = STRBUF_INIT; + struct object_id oid; + + if (read_oneliner(&buf, rebase_path_stopped_sha(), 1)) { + if (!get_sha1_committish(buf.buf, oid.hash)) + record_in_rewritten(&oid, + peek_command(&todo_list, 0)); + } + strbuf_release(&buf); + } res = pick_commits(&todo_list, opts); release_todo_list: From ec2b1862d3f3d9df4c635d0bf9adcd0777a48d3a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 Apr 2016 17:27:03 +0200 Subject: [PATCH 22/45] sequencer (rebase -i): run the post-rewrite hook, if needed Signed-off-by: Johannes Schindelin --- sequencer.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sequencer.c b/sequencer.c index e896a92346..62d9751878 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1943,6 +1943,8 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) if (!stat(rebase_path_rewritten_list(), &st) && st.st_size > 0) { struct child_process child = CHILD_PROCESS_INIT; + const char *post_rewrite_hook = + find_hook("post-rewrite"); child.in = open(rebase_path_rewritten_list(), O_RDONLY); child.git_cmd = 1; @@ -1951,6 +1953,17 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) argv_array_push(&child.args, "--for-rewrite=rebase"); /* we don't care if this copying failed */ run_command(&child); + + if (post_rewrite_hook) { + struct child_process hook = CHILD_PROCESS_INIT; + + hook.in = open(rebase_path_rewritten_list(), + O_RDONLY); + argv_array_push(&hook.args, post_rewrite_hook); + argv_array_push(&hook.args, "rebase"); + /* we don't care if this hook failed */ + run_command(&hook); + } } strbuf_release(&buf); From eac92fafd270e5814b147a0d418869ba867180a0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 Apr 2016 16:05:06 +0200 Subject: [PATCH 23/45] sequencer (rebase -i): respect the rebase.autostash setting Git's `rebase` command inspects the `rebase.autostash` config setting to determine whether it should stash any uncommitted changes before rebasing and re-apply them afterwards. As we introduce more bits and pieces to let the sequencer act as interactive rebase's backend, here is the part that adds support for the autostash feature. Signed-off-by: Johannes Schindelin --- sequencer.c | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/sequencer.c b/sequencer.c index 62d9751878..073ff3ed75 100644 --- a/sequencer.c +++ b/sequencer.c @@ -111,6 +111,7 @@ static GIT_PATH_FUNC(rebase_path_orig_head, "rebase-merge/orig-head") static GIT_PATH_FUNC(rebase_path_verbose, "rebase-merge/verbose") static GIT_PATH_FUNC(rebase_path_head_name, "rebase-merge/head-name") static GIT_PATH_FUNC(rebase_path_onto, "rebase-merge/onto") +static GIT_PATH_FUNC(rebase_path_autostash, "rebase-merge/autostash") static inline int is_rebase_i(const struct replay_opts *opts) { @@ -1804,6 +1805,47 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset) return -1; } +static int apply_autostash(struct replay_opts *opts) +{ + struct strbuf stash_sha1 = STRBUF_INIT; + struct child_process child = CHILD_PROCESS_INIT; + int ret = 0; + + if (!read_oneliner(&stash_sha1, rebase_path_autostash(), 1)) { + strbuf_release(&stash_sha1); + return 0; + } + strbuf_trim(&stash_sha1); + + child.git_cmd = 1; + argv_array_push(&child.args, "stash"); + argv_array_push(&child.args, "apply"); + argv_array_push(&child.args, stash_sha1.buf); + if (!run_command(&child)) + printf(_("Applied autostash.")); + else { + struct child_process store = CHILD_PROCESS_INIT; + + store.git_cmd = 1; + argv_array_push(&store.args, "stash"); + argv_array_push(&store.args, "store"); + argv_array_push(&store.args, "-m"); + argv_array_push(&store.args, "autostash"); + argv_array_push(&store.args, "-q"); + argv_array_push(&store.args, stash_sha1.buf); + if (run_command(&store)) + ret = error(_("cannot store %s"), stash_sha1.buf); + else + printf(_("Applying autostash resulted in conflicts.\n" + "Your changes are safe in the stash.\n" + "You can run \"git stash pop\" or" + " \"git stash drop\" at any time.\n")); + } + + strbuf_release(&stash_sha1); + return ret; +} + static const char *reflog_message(struct replay_opts *opts, const char *sub_action, const char *fmt, ...) { @@ -1965,6 +2007,7 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) run_command(&hook); } } + apply_autostash(opts); strbuf_release(&buf); strbuf_release(&head_ref); From 2eadbeaa7d10979ac7e87576b927cc9b6225bb3c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 30 Mar 2016 17:58:55 +0200 Subject: [PATCH 24/45] sequencer (rebase -i): respect strategy/strategy_opts settings The sequencer already has an idea about using different merge strategies. We just piggy-back on top of that, using rebase -i's own settings, when running the sequencer in interactive rebase mode. Signed-off-by: Johannes Schindelin --- sequencer.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index 073ff3ed75..587bd6c316 100644 --- a/sequencer.c +++ b/sequencer.c @@ -112,6 +112,8 @@ static GIT_PATH_FUNC(rebase_path_verbose, "rebase-merge/verbose") static GIT_PATH_FUNC(rebase_path_head_name, "rebase-merge/head-name") static GIT_PATH_FUNC(rebase_path_onto, "rebase-merge/onto") static GIT_PATH_FUNC(rebase_path_autostash, "rebase-merge/autostash") +static GIT_PATH_FUNC(rebase_path_strategy, "rebase-merge/strategy") +static GIT_PATH_FUNC(rebase_path_strategy_opts, "rebase-merge/strategy_opts") static inline int is_rebase_i(const struct replay_opts *opts) { @@ -1407,6 +1409,26 @@ static int populate_opts_cb(const char *key, const char *value, void *data) return 0; } +static void read_strategy_opts(struct replay_opts *opts, struct strbuf *buf) +{ + int i; + + strbuf_reset(buf); + if (!read_oneliner(buf, rebase_path_strategy(), 0)) + return; + opts->strategy = strbuf_detach(buf, NULL); + if (!read_oneliner(buf, rebase_path_strategy_opts(), 0)) + return; + + opts->xopts_nr = split_cmdline(buf->buf, (const char ***)&opts->xopts); + for (i = 0; i < opts->xopts_nr; i++) { + const char *arg = opts->xopts[i]; + + skip_prefix(arg, "--", &arg); + opts->xopts[i] = xstrdup(arg); + } +} + static int read_populate_opts(struct replay_opts *opts) { if (is_rebase_i(opts)) { @@ -1420,11 +1442,13 @@ static int read_populate_opts(struct replay_opts *opts) opts->gpg_sign = xstrdup(buf.buf + 2); } } - strbuf_release(&buf); if (file_exists(rebase_path_verbose())) opts->verbose = 1; + read_strategy_opts(opts, &buf); + strbuf_release(&buf); + return 0; } From d6ff320e486178c6e99725abf3a3c85075e85be1 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 7 Apr 2016 16:31:59 +0200 Subject: [PATCH 25/45] sequencer (rebase -i): allow rescheduling commands The interactive rebase has the very special magic that a cherry-pick that exits with a status different from 0 and 1 signifies a failure to even record that a cherry-pick was started. This can happen e.g. when a fast-forward fails because it would overwrite untracked files. In that case, we must reschedule the command that we thought we already had at least started successfully. Signed-off-by: Johannes Schindelin --- sequencer.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sequencer.c b/sequencer.c index 587bd6c316..b2f1a8eab6 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1918,6 +1918,12 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) 1); res = do_pick_commit(item->command, item->commit, opts, is_final_fixup(todo_list)); + if (is_rebase_i(opts) && res < 0) { + /* Reschedule */ + todo_list->current--; + if (save_todo(todo_list, opts)) + return -1; + } if (item->command == TODO_EDIT) { struct commit *commit = item->commit; if (!res) From 7d92436b5fe9999107e92f5b86456870162baf3e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 15:18:29 +0200 Subject: [PATCH 26/45] sequencer (rebase -i): implement the 'drop' command The parsing part of a 'drop' command is almost identical to parsing a 'pick', while the operation is the same as that of a 'noop'. Signed-off-by: Johannes Schindelin --- sequencer.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sequencer.c b/sequencer.c index b2f1a8eab6..770b12bbc7 100644 --- a/sequencer.c +++ b/sequencer.c @@ -769,7 +769,8 @@ enum todo_command { /* commands that do something else than handling a single commit */ TODO_EXEC, /* commands that do nothing but are counted for reporting progress */ - TODO_NOOP + TODO_NOOP, + TODO_DROP }; static struct { @@ -783,7 +784,8 @@ static struct { { 'f', "fixup" }, { 's', "squash" }, { 'x', "exec" }, - { 0, "noop" } + { 0, "noop" }, + { 'd', "drop" } }; static const char *command_to_string(const enum todo_command command) @@ -1316,7 +1318,7 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list) else if (is_fixup(item->command)) return error(_("cannot '%s' without a previous commit"), command_to_string(item->command)); - else if (item->command != TODO_NOOP) + else if (item->command < TODO_NOOP) fixup_okay = 1; } @@ -1823,7 +1825,7 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset) int i; for (i = todo_list->current + offset; i < todo_list->nr; i++) - if (todo_list->items[i].command != TODO_NOOP) + if (todo_list->items[i].command < TODO_NOOP) return todo_list->items[i].command; return -1; @@ -1956,7 +1958,7 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) res = do_exec(item->arg); *end_of_arg = saved; } - else if (item->command != TODO_NOOP) + else if (item->command < TODO_NOOP) return error(_("unknown command %d"), item->command); todo_list->current++; From 5019ce10dd74cdc6eae6e8554bd3495f1b028323 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 24 Nov 2016 21:18:37 +0100 Subject: [PATCH 27/45] sequencer (rebase -i): differentiate between comments and 'noop' In the upcoming patch, we will support rebase -i's progress reporting. The progress skips comments but counts 'noop's. Signed-off-by: Johannes Schindelin --- sequencer.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/sequencer.c b/sequencer.c index 770b12bbc7..e3b912fe41 100644 --- a/sequencer.c +++ b/sequencer.c @@ -770,7 +770,9 @@ enum todo_command { TODO_EXEC, /* commands that do nothing but are counted for reporting progress */ TODO_NOOP, - TODO_DROP + TODO_DROP, + /* comments (not counted for reporting progress) */ + TODO_COMMENT }; static struct { @@ -785,12 +787,13 @@ static struct { { 's', "squash" }, { 'x', "exec" }, { 0, "noop" }, - { 'd', "drop" } + { 'd', "drop" }, + { 0, NULL } }; static const char *command_to_string(const enum todo_command command) { - if ((size_t)command < ARRAY_SIZE(todo_command_info)) + if (command < TODO_COMMENT) return todo_command_info[command].str; die("Unknown command: %d", command); } @@ -1236,14 +1239,14 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol) bol += strspn(bol, " \t"); if (bol == eol || *bol == '\r' || *bol == comment_line_char) { - item->command = TODO_NOOP; + item->command = TODO_COMMENT; item->commit = NULL; item->arg = bol; item->arg_len = eol - bol; return 0; } - for (i = 0; i < ARRAY_SIZE(todo_command_info); i++) + for (i = 0; i < TODO_COMMENT; i++) if (skip_prefix(bol, todo_command_info[i].str, &bol)) { item->command = i; break; @@ -1253,7 +1256,7 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol) item->command = i; break; } - if (i >= ARRAY_SIZE(todo_command_info)) + if (i >= TODO_COMMENT) return -1; if (item->command == TODO_NOOP) { From 161d7c052122f39824b6d87f0063cc30db0389cc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Apr 2016 14:54:20 +0200 Subject: [PATCH 28/45] run_command_opt(): optionally hide stderr when the command succeeds This will be needed to hide the output of `git commit` when the sequencer handles an interactive rebase's script. Signed-off-by: Johannes Schindelin --- run-command.c | 23 +++++++++++++++++++++++ run-command.h | 1 + 2 files changed, 24 insertions(+) diff --git a/run-command.c b/run-command.c index ca905a9e80..5bb957afdd 100644 --- a/run-command.c +++ b/run-command.c @@ -589,6 +589,29 @@ int run_command_v_opt_cd_env(const char **argv, int opt, const char *dir, const cmd.clean_on_exit = opt & RUN_CLEAN_ON_EXIT ? 1 : 0; cmd.dir = dir; cmd.env = env; + + if (opt & RUN_HIDE_STDERR_ON_SUCCESS) { + struct strbuf buf = STRBUF_INIT; + int res; + + cmd.err = -1; + if (start_command(&cmd) < 0) + return -1; + + if (strbuf_read(&buf, cmd.err, 0) < 0) { + close(cmd.err); + finish_command(&cmd); /* throw away exit code */ + return -1; + } + + close(cmd.err); + res = finish_command(&cmd); + if (res) + fputs(buf.buf, stderr); + strbuf_release(&buf); + return res; + } + return run_command(&cmd); } diff --git a/run-command.h b/run-command.h index dd1c78c28d..65a21ddd4e 100644 --- a/run-command.h +++ b/run-command.h @@ -72,6 +72,7 @@ extern int run_hook_ve(const char *const *env, const char *name, va_list args); #define RUN_SILENT_EXEC_FAILURE 8 #define RUN_USING_SHELL 16 #define RUN_CLEAN_ON_EXIT 32 +#define RUN_HIDE_STDERR_ON_SUCCESS 64 int run_command_v_opt(const char **argv, int opt); /* From 28c5e3bca08591dc3b776c270c6cbe7bfc5c8ec7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Apr 2016 14:56:52 +0200 Subject: [PATCH 29/45] sequencer (rebase -i): show only failed `git commit`'s output This is the behavior of the shell script version of the interactive rebase, by using the `output` function defined in `git-rebase.sh`. Signed-off-by: Johannes Schindelin --- sequencer.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sequencer.c b/sequencer.c index e3b912fe41..ad33163d71 100644 --- a/sequencer.c +++ b/sequencer.c @@ -647,10 +647,15 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts, { char **env = NULL; struct argv_array array; - int rc; + int opt = RUN_GIT_CMD, rc; const char *value; if (is_rebase_i(opts)) { + if (!edit) { + opt |= RUN_COMMAND_STDOUT_TO_STDERR; + opt |= RUN_HIDE_STDERR_ON_SUCCESS; + } + env = read_author_script(); if (!env) { const char *gpg_opt = gpg_sign_opt_quoted(opts); @@ -687,7 +692,7 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts, if (opts->allow_empty_message) argv_array_push(&array, "--allow-empty-message"); - rc = run_command_v_opt_cd_env(array.argv, RUN_GIT_CMD, NULL, + rc = run_command_v_opt_cd_env(array.argv, opt, NULL, (const char *const *)env); argv_array_clear(&array); free(env); From 5f952b3b51e3155be512bb5179447ea99a381fc7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Apr 2016 14:56:52 +0200 Subject: [PATCH 30/45] sequencer (rebase -i): show only failed cherry-picks' output This is the behavior of the shell script version of the interactive rebase, by using the `output` function defined in `git-rebase.sh`. Signed-off-by: Johannes Schindelin --- sequencer.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sequencer.c b/sequencer.c index ad33163d71..62e8236a56 100644 --- a/sequencer.c +++ b/sequencer.c @@ -464,6 +464,8 @@ static int do_recursive_merge(struct commit *base, struct commit *next, o.ancestor = base ? base_label : "(empty tree)"; o.branch1 = "HEAD"; o.branch2 = next ? next_label : "(empty tree)"; + if (is_rebase_i(opts)) + o.buffer_output = 2; head_tree = parse_tree_indirect(head); next_tree = next ? next->tree : empty_tree(); @@ -475,6 +477,8 @@ static int do_recursive_merge(struct commit *base, struct commit *next, clean = merge_trees(&o, head_tree, next_tree, base_tree, &result); + if (is_rebase_i(opts) && clean <= 0) + fputs(o.obuf.buf, stdout); strbuf_release(&o.obuf); if (clean < 0) return clean; From 22cfbad76657163c039d0765a05145133fecab1f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 21 Apr 2016 12:51:50 +0200 Subject: [PATCH 31/45] sequencer (rebase -i): suggest --edit-todo upon unknown command This is the same behavior as known from `git rebase -i`. Signed-off-by: Johannes Schindelin --- sequencer.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index 62e8236a56..b6b54ecc15 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1354,8 +1354,12 @@ static int read_populate_todo(struct todo_list *todo_list, close(fd); res = parse_insn_buffer(todo_list->buf.buf, todo_list); - if (res) + if (res) { + if (is_rebase_i(opts)) + return error(_("please fix this using " + "'git rebase --edit-todo'.")); return error(_("unusable instruction sheet: '%s'"), todo_file); + } if (!todo_list->nr && (!is_rebase_i(opts) || !file_exists(rebase_path_done()))) From 8513b2daa0c65cb1373668dcd013cd51e3e2da38 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 Apr 2016 15:51:19 +0200 Subject: [PATCH 32/45] sequencer (rebase -i): show the progress The interactive rebase keeps the user informed about its progress. If the sequencer wants to do the grunt work of the interactive rebase, it also needs to show that progress. Signed-off-by: Johannes Schindelin --- sequencer.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/sequencer.c b/sequencer.c index b6b54ecc15..570d6bf35e 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1220,6 +1220,7 @@ struct todo_list { struct strbuf buf; struct todo_item *items; int nr, alloc, current; + int done_nr, total_nr; }; #define TODO_LIST_INIT { STRBUF_INIT } @@ -1337,6 +1338,17 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list) return res; } +static int count_commands(struct todo_list *todo_list) +{ + int count = 0, i; + + for (i = 0; i < todo_list->nr; i++) + if (todo_list->items[i].command != TODO_COMMENT) + count++; + + return count; +} + static int read_populate_todo(struct todo_list *todo_list, struct replay_opts *opts) { @@ -1379,6 +1391,21 @@ static int read_populate_todo(struct todo_list *todo_list, return error(_("cannot revert during a cherry-pick.")); } + if (is_rebase_i(opts)) { + struct todo_list done = TODO_LIST_INIT; + + if (strbuf_read_file(&done.buf, rebase_path_done(), 0) > 0 && + !parse_insn_buffer(done.buf.buf, &done)) + todo_list->done_nr = count_commands(&done); + else + todo_list->done_nr = 0; + + todo_list->total_nr = todo_list->done_nr + + count_commands(todo_list); + + todo_list_release(&done); + } + return 0; } @@ -1924,6 +1951,11 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) if (save_todo(todo_list, opts)) return -1; if (is_rebase_i(opts)) { + if (item->command != TODO_COMMENT) + fprintf(stderr, "Rebasing (%d/%d)%s", + ++(todo_list->done_nr), + todo_list->total_nr, + opts->verbose ? "\n" : "\r"); unlink(rebase_path_message()); unlink(rebase_path_author_script()); unlink(rebase_path_stopped_sha()); From f703457526be3faf343e5a98dd9e5d66841d8e24 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 Apr 2016 16:40:13 +0200 Subject: [PATCH 33/45] sequencer (rebase -i): write the progress into files For the benefit of e.g. the shell prompt, the interactive rebase not only displays the progress for the user to see, but also writes it into the msgnum/end files in the state directory. Teach the sequencer this new trick. Signed-off-by: Johannes Schindelin --- sequencer.c | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/sequencer.c b/sequencer.c index 570d6bf35e..40228b7495 100644 --- a/sequencer.c +++ b/sequencer.c @@ -44,6 +44,16 @@ static GIT_PATH_FUNC(rebase_path_todo, "rebase-merge/git-rebase-todo") * actions. */ static GIT_PATH_FUNC(rebase_path_done, "rebase-merge/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"); +/* + * 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"); /* * The commit message that is planned to be used for any changes that * need to be committed following a user interaction. @@ -1393,17 +1403,22 @@ static int read_populate_todo(struct todo_list *todo_list, if (is_rebase_i(opts)) { struct todo_list done = TODO_LIST_INIT; + FILE *f = fopen(rebase_path_msgtotal(), "w"); if (strbuf_read_file(&done.buf, rebase_path_done(), 0) > 0 && !parse_insn_buffer(done.buf.buf, &done)) todo_list->done_nr = count_commands(&done); else todo_list->done_nr = 0; + todo_list_release(&done); todo_list->total_nr = todo_list->done_nr + count_commands(todo_list); - todo_list_release(&done); + if (f) { + fprintf(f, "%d\n", todo_list->total_nr); + fclose(f); + } } return 0; @@ -1951,11 +1966,20 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) if (save_todo(todo_list, opts)) return -1; if (is_rebase_i(opts)) { - if (item->command != TODO_COMMENT) + if (item->command != TODO_COMMENT) { + FILE *f = fopen(rebase_path_msgnum(), "w"); + + todo_list->done_nr++; + + if (f) { + fprintf(f, "%d\n", todo_list->done_nr); + fclose(f); + } fprintf(stderr, "Rebasing (%d/%d)%s", - ++(todo_list->done_nr), + todo_list->done_nr, todo_list->total_nr, opts->verbose ? "\n" : "\r"); + } unlink(rebase_path_message()); unlink(rebase_path_author_script()); unlink(rebase_path_stopped_sha()); From 1e8863326f756ddb01002c611e93621ff98e5876 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 Apr 2016 17:28:04 +0200 Subject: [PATCH 34/45] sequencer (rebase -i): write out the final message The shell script version of the interactive rebase has a very specific final message. Teach the sequencer to print the same. Signed-off-by: Johannes Schindelin --- sequencer.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sequencer.c b/sequencer.c index 40228b7495..fb32e8cf42 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2113,6 +2113,9 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) } apply_autostash(opts); + fprintf(stderr, "Successfully rebased and updated %s.\n", + head_ref.buf); + strbuf_release(&buf); strbuf_release(&head_ref); } From f8a2ffd5dbdabe5a6a19be7e757bcb5a41155218 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 17:43:05 +0200 Subject: [PATCH 35/45] rebase -i: generate the script via rebase--helper The first step of an interactive rebase is to generate the so-called "todo script", to be stored in the state directory as "git-rebase-todo" and to be edited by the user. Originally, we adjusted the output of `git log ` using a simple sed script. Over the course of the years, the code became more complicated. We now use shell scripting to edit the output of `git log` conditionally, depending whether to keep "empty" commits (i.e. commits that do not change any files). On platforms where shell scripting is not native, this can be a serious drag. And it opens the door for incompatibilities between platforms when it comes to shell scripting or to Unix-y commands. Let's just re-implement the todo script generation in plain C, using the revision machinery directly. This is substantially faster, improving the speed relative to the shell script version of the interactive rebase from 2x to 3x on Windows. Note that the rearrange_squash() function in git-rebase--interactive relied on the fact that we set the "format" variable to the config setting rebase.instructionFormat. Relying on a side effect like this is no good, hence we explicitly perform that assignment (possibly again) in rearrange_squash(). Signed-off-by: Johannes Schindelin --- builtin/rebase--helper.c | 8 ++++++- git-rebase--interactive.sh | 42 +++++++++++++++++++----------------- sequencer.c | 44 ++++++++++++++++++++++++++++++++++++++ sequencer.h | 3 +++ 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c index ca1ebb2fa1..821058d452 100644 --- a/builtin/rebase--helper.c +++ b/builtin/rebase--helper.c @@ -11,15 +11,19 @@ static const char * const builtin_rebase_helper_usage[] = { int cmd_rebase__helper(int argc, const char **argv, const char *prefix) { struct replay_opts opts = REPLAY_OPTS_INIT; + int keep_empty = 0; enum { - CONTINUE = 1, ABORT + CONTINUE = 1, ABORT, MAKE_SCRIPT } command = 0; struct option options[] = { OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")), + OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")), OPT_CMDMODE(0, "continue", &command, N_("continue rebase"), CONTINUE), OPT_CMDMODE(0, "abort", &command, N_("abort rebase"), ABORT), + OPT_CMDMODE(0, "make-script", &command, + N_("make rebase script"), MAKE_SCRIPT), OPT_END() }; @@ -36,5 +40,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) return !!sequencer_continue(&opts); if (command == ABORT && argc == 1) return !!sequencer_remove_state(&opts); + if (command == MAKE_SCRIPT && argc > 1) + return !!sequencer_make_script(keep_empty, stdout, argc, argv); usage_with_options(builtin_rebase_helper_usage, options); } diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 301926804f..971ffab6f9 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -784,6 +784,7 @@ collapse_todo_ids() { # each log message will be re-retrieved in order to normalize the # autosquash arrangement rearrange_squash () { + format=$(git config --get rebase.instructionFormat) # extract fixup!/squash! lines and resolve any referenced sha1's while read -r pick sha1 message do @@ -1209,26 +1210,27 @@ else revisions=$onto...$orig_head shortrevisions=$shorthead fi -format=$(git config --get rebase.instructionFormat) -# the 'rev-list .. | sed' requires %m to parse; the instruction requires %H to parse -git rev-list $merges_option --format="%m%H ${format:-%s}" \ - --reverse --left-right --topo-order \ - $revisions ${restrict_revision+^$restrict_revision} | \ - sed -n "s/^>//p" | -while read -r sha1 rest -do +if test t != "$preserve_merges" +then + git rebase--helper --make-script ${keep_empty:+--keep-empty} \ + $revisions ${restrict_revision+^$restrict_revision} >"$todo" +else + format=$(git config --get rebase.instructionFormat) + # the 'rev-list .. | sed' requires %m to parse; the instruction requires %H to parse + git rev-list $merges_option --format="%m%H ${format:-%s}" \ + --reverse --left-right --topo-order \ + $revisions ${restrict_revision+^$restrict_revision} | \ + sed -n "s/^>//p" | + while read -r sha1 rest + do - if test -z "$keep_empty" && is_empty_commit $sha1 && ! is_merge_commit $sha1 - then - comment_out="$comment_char " - else - comment_out= - fi + if test -z "$keep_empty" && is_empty_commit $sha1 && ! is_merge_commit $sha1 + then + comment_out="$comment_char " + else + comment_out= + fi - if test t != "$preserve_merges" - then - printf '%s\n' "${comment_out}pick $sha1 $rest" >>"$todo" - else if test -z "$rebase_root" then preserve=t @@ -1247,8 +1249,8 @@ do touch "$rewritten"/$sha1 printf '%s\n' "${comment_out}pick $sha1 $rest" >>"$todo" fi - fi -done + done +fi # Watch for commits that been dropped by --cherry-pick if test t = "$preserve_merges" diff --git a/sequencer.c b/sequencer.c index fb32e8cf42..bc4f5bf040 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2368,3 +2368,47 @@ 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 = "%s"; + 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); + get_commit_format(format, &revs); + 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; +} diff --git a/sequencer.h b/sequencer.h index f885b68395..83f2943b7a 100644 --- a/sequencer.h +++ b/sequencer.h @@ -45,6 +45,9 @@ int sequencer_continue(struct replay_opts *opts); int sequencer_rollback(struct replay_opts *opts); int sequencer_remove_state(struct replay_opts *opts); +int sequencer_make_script(int keep_empty, FILE *out, + int argc, const char **argv); + extern const char sign_off_header[]; void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag); From 9ab3fc063e248a1c078f5cc1a013c7077744f065 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 15 Apr 2016 10:00:39 +0200 Subject: [PATCH 36/45] rebase -i: remove useless indentation The commands used to be indented, and it is nice to look at, but when we transform the SHA-1s, the indentation is removed. So let's do away with it. For the moment, at least: when we will use the upcoming rebase--helper to transform the SHA-1s, we *will* keep the indentation and can reintroduce it. Yet, to be able to validate the rebase--helper against the output of the current shell script version, we need to remove the extra indentation. Signed-off-by: Johannes Schindelin --- git-rebase--interactive.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 971ffab6f9..2b11559c8a 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -155,13 +155,13 @@ reschedule_last_action () { append_todo_help () { gettext " Commands: - p, pick = use commit - r, reword = use commit, but edit the commit message - e, edit = use commit, but stop for amending - s, squash = use commit, but meld into previous commit - f, fixup = like \"squash\", but discard this commit's log message - x, exec = run command (the rest of the line) using shell - d, drop = remove commit +p, pick = use commit +r, reword = use commit, but edit the commit message +e, edit = use commit, but stop for amending +s, squash = use commit, but meld into previous commit +f, fixup = like \"squash\", but discard this commit's log message +x, exec = run command (the rest of the line) using shell +d, drop = remove commit These lines can be re-ordered; they are executed from top to bottom. " | git stripspace --comment-lines >>"$todo" From c03d74ecabcfcb31a8df48a9de4e1defb8e8b8c4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 17 Apr 2016 08:48:44 +0200 Subject: [PATCH 37/45] rebase -i: do not invent onelines when expanding/collapsing SHA-1s To avoid problems with short SHA-1s that become non-unique during the rebase, we rewrite the todo script with short/long SHA-1s before and after letting the user edit the script. Since SHA-1s are not intuitive for humans, rebase -i also provides the onelines (commit message subjects) in the script, purely for the user's convenience. It is very possible to generate a todo script via different means than rebase -i and then to let rebase -i run with it; In this case, these onelines are not required. And this is where the expand/collapse machinery has a bug: it *expects* that oneline, and failing to find one reuses the previous SHA-1 as "oneline". It was most likely an oversight, and made implementation in the (quite limiting) shell script language less convoluted. However, we are about to reimplement performance-critical parts in C (and due to spawning a git.exe process for every single line of the todo script, the expansion/collapsing of the SHA-1s *is* performance-hampering on Windows), therefore let's fix this bug to make cross-validation with the C version of that functionality possible. Signed-off-by: Johannes Schindelin --- git-rebase--interactive.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 2b11559c8a..daf07ad87e 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -759,7 +759,12 @@ transform_todo_ids () { ;; *) sha1=$(git rev-parse --verify --quiet "$@" ${rest%%[ ]*}) && - rest="$sha1 ${rest#*[ ]}" + if test "a$rest" = "a${rest#*[ ]}" + then + rest=$sha1 + else + rest="$sha1 ${rest#*[ ]}" + fi ;; esac printf '%s\n' "$command${rest:+ }$rest" From 5174dbde5d27fade3356150d408af9eabd3320cc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Apr 2016 18:17:19 +0200 Subject: [PATCH 38/45] rebase -i: also expand/collapse the SHA-1s via the rebase--helper This is crucial to improve performance on Windows, as the speed is now mostly dominated by the SHA-1 transformation (because it spawns a new rev-parse process for *every* line, and spawning processes is pretty slow from Git for Windows' MSYS2 Bash). Signed-off-by: Johannes Schindelin --- builtin/rebase--helper.c | 10 ++++++- git-rebase--interactive.sh | 4 +-- sequencer.c | 59 ++++++++++++++++++++++++++++++++++++++ sequencer.h | 2 ++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c index 821058d452..9444c8d6c6 100644 --- a/builtin/rebase--helper.c +++ b/builtin/rebase--helper.c @@ -13,7 +13,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) struct replay_opts opts = REPLAY_OPTS_INIT; int keep_empty = 0; enum { - CONTINUE = 1, ABORT, MAKE_SCRIPT + CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_SHA1S, EXPAND_SHA1S } command = 0; struct option options[] = { OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")), @@ -24,6 +24,10 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) ABORT), OPT_CMDMODE(0, "make-script", &command, N_("make rebase script"), MAKE_SCRIPT), + OPT_CMDMODE(0, "shorten-sha1s", &command, + N_("shorten SHA-1s in the todo list"), SHORTEN_SHA1S), + OPT_CMDMODE(0, "expand-sha1s", &command, + N_("expand SHA-1s in the todo list"), EXPAND_SHA1S), OPT_END() }; @@ -42,5 +46,9 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) return !!sequencer_remove_state(&opts); if (command == MAKE_SCRIPT && argc > 1) return !!sequencer_make_script(keep_empty, stdout, argc, argv); + if (command == SHORTEN_SHA1S && argc == 1) + return !!transform_todo_ids(1); + if (command == EXPAND_SHA1S && argc == 1) + return !!transform_todo_ids(0); usage_with_options(builtin_rebase_helper_usage, options); } diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index daf07ad87e..cc104eaff6 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -773,11 +773,11 @@ transform_todo_ids () { } expand_todo_ids() { - transform_todo_ids + git rebase--helper --expand-sha1s } collapse_todo_ids() { - transform_todo_ids --short + git rebase--helper --shorten-sha1s } # Rearrange the todo list that has both "pick sha1 msg" and diff --git a/sequencer.c b/sequencer.c index bc4f5bf040..4bb966e8d9 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2412,3 +2412,62 @@ int sequencer_make_script(int keep_empty, FILE *out, strbuf_release(&buf); return 0; } + + +int transform_todo_ids(int shorten_sha1s) +{ + 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 instruction sheet: '%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 { + int eoc = strcspn(p, " \t"); + const char *sha1 = shorten_sha1s ? + short_commit_name(item->commit) : + oid_to_hex(&item->commit->object.oid); + + if (!eoc) { + p += strspn(p, " \t"); + eoc = strcspn(p, " \t"); + } + + fprintf(out, "%.*s %s %.*s\n", + eoc, p, sha1, item->arg_len, item->arg); + } + } + fclose(out); + todo_list_release(&todo_list); + return 0; +} diff --git a/sequencer.h b/sequencer.h index 83f2943b7a..47a81034e7 100644 --- a/sequencer.h +++ b/sequencer.h @@ -48,6 +48,8 @@ int sequencer_remove_state(struct replay_opts *opts); int sequencer_make_script(int keep_empty, FILE *out, int argc, const char **argv); +int transform_todo_ids(int shorten_sha1s); + extern const char sign_off_header[]; void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag); From 76eb1f5b7c9bf415ef7aed28f2ca860a09deb5a8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jun 2016 16:54:43 +0200 Subject: [PATCH 39/45] t3404: relax rebase.missingCommitsCheck tests These tests were a bit anal about the *exact* warning/error message printed by git rebase. But those messages are intended for the *end user*, therefore it does not make sense to test so rigidly for the *exact* wording. In the following, we will reimplement the missing commits check in the sequencer, with slightly different words. So let's just test for the parts in the warning/error message that we *really* care about, nothing more, nothing less. Signed-off-by: Johannes Schindelin --- t/t3404-rebase-interactive.sh | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 5e0b56ffeb..71b9c8ef8b 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1226,20 +1226,13 @@ test_expect_success 'rebase -i respects rebase.missingCommitsCheck = error' ' test B = $(git cat-file commit HEAD^ | sed -ne \$p) ' -cat >expect <actual && - test_i18ncmp expect actual && + test_i18ngrep "badcmd $(git rev-list --oneline -1 master~1)" actual && + test_i18ngrep "You can fix this with .git rebase --edit-todo.." actual && FAKE_LINES="1 2 3 drop 4 5" git rebase --edit-todo && git rebase --continue && test E = $(git cat-file commit HEAD | sed -ne \$p) && @@ -1261,20 +1254,13 @@ test_expect_success 'tabs and spaces are accepted in the todolist' ' test E = $(git cat-file commit HEAD | sed -ne \$p) ' -cat >expect <actual && - test_i18ncmp expect actual && + test_i18ngrep "edit XXXXXXX False commit" actual && + test_i18ngrep "You can fix this with .git rebase --edit-todo.." actual && FAKE_LINES="1 2 4 5 6" git rebase --edit-todo && git rebase --continue && test E = $(git cat-file commit HEAD | sed -ne \$p) From 14e4885908bb1fb46d639005ee4e5da96cd59528 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jun 2016 16:58:59 +0200 Subject: [PATCH 40/45] rebase -i: check for missing commits in the rebase--helper In particular on Windows, where shell scripts are even more expensive than on MacOSX or Linux, it makes sense to move a loop that forks Git at least once for every line in the todo list into a builtin. Signed-off-by: Johannes Schindelin --- builtin/rebase--helper.c | 7 +- git-rebase--interactive.sh | 164 ++----------------------------------- sequencer.c | 125 ++++++++++++++++++++++++++++ sequencer.h | 1 + 4 files changed, 137 insertions(+), 160 deletions(-) diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c index 9444c8d6c6..e706eac710 100644 --- a/builtin/rebase--helper.c +++ b/builtin/rebase--helper.c @@ -13,7 +13,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) struct replay_opts opts = REPLAY_OPTS_INIT; int keep_empty = 0; enum { - CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_SHA1S, EXPAND_SHA1S + CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_SHA1S, EXPAND_SHA1S, + CHECK_TODO_LIST } command = 0; struct option options[] = { OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")), @@ -28,6 +29,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) N_("shorten SHA-1s in the todo list"), SHORTEN_SHA1S), OPT_CMDMODE(0, "expand-sha1s", &command, N_("expand SHA-1s in the todo list"), EXPAND_SHA1S), + OPT_CMDMODE(0, "check-todo-list", &command, + N_("check the todo list"), CHECK_TODO_LIST), OPT_END() }; @@ -50,5 +53,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) return !!transform_todo_ids(1); if (command == EXPAND_SHA1S && argc == 1) return !!transform_todo_ids(0); + if (command == CHECK_TODO_LIST && argc == 1) + return !!check_todo_list(); usage_with_options(builtin_rebase_helper_usage, options); } diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index cc104eaff6..78e78b6a7f 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -889,96 +889,6 @@ add_exec_commands () { mv "$1.new" "$1" } -# Check if the SHA-1 passed as an argument is a -# correct one, if not then print $2 in "$todo".badsha -# $1: the SHA-1 to test -# $2: the line number of the input -# $3: the input filename -check_commit_sha () { - badsha=0 - if test -z "$1" - then - badsha=1 - else - sha1_verif="$(git rev-parse --verify --quiet $1^{commit})" - if test -z "$sha1_verif" - then - badsha=1 - fi - fi - - if test $badsha -ne 0 - then - line="$(sed -n -e "${2}p" "$3")" - warn "$(eval_gettext "\ -Warning: the SHA-1 is missing or isn't a commit in the following line: - - \$line")" - warn - fi - - return $badsha -} - -# prints the bad commits and bad commands -# from the todolist in stdin -check_bad_cmd_and_sha () { - retval=0 - lineno=0 - while read -r command rest - do - lineno=$(( $lineno + 1 )) - case $command in - "$comment_char"*|''|noop|x|exec) - # Doesn't expect a SHA-1 - ;; - "$cr") - # Work around CR left by "read" (e.g. with Git for - # Windows' Bash). - ;; - pick|p|drop|d|reword|r|edit|e|squash|s|fixup|f) - if ! check_commit_sha "${rest%%[ ]*}" "$lineno" "$1" - then - retval=1 - fi - ;; - *) - line="$(sed -n -e "${lineno}p" "$1")" - warn "$(eval_gettext "\ -Warning: the command isn't recognized in the following line: - - \$line")" - warn - retval=1 - ;; - esac - done <"$1" - return $retval -} - -# Print the list of the SHA-1 of the commits -# from stdin to stdout -todo_list_to_sha_list () { - git stripspace --strip-comments | - while read -r command sha1 rest - do - case $command in - "$comment_char"*|''|noop|x|"exec") - ;; - *) - long_sha=$(git rev-list --no-walk "$sha1" 2>/dev/null) - printf "%s\n" "$long_sha" - ;; - esac - done -} - -# Use warn for each line in stdin -warn_lines () { - while read -r line - do - warn " - $line" - done -} - # Switch to the branch in $into and notify it in the reflog checkout_onto () { GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION: checkout $onto_name" @@ -993,74 +903,6 @@ get_missing_commit_check_level () { printf '%s' "$check_level" | tr 'A-Z' 'a-z' } -# 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. -check_todo_list () { - raise_error=f - - check_level=$(get_missing_commit_check_level) - - case "$check_level" in - warn|error) - # Get the SHA-1 of the commits - todo_list_to_sha_list <"$todo".backup >"$todo".oldsha1 - todo_list_to_sha_list <"$todo" >"$todo".newsha1 - - # Sort the SHA-1 and compare them - sort -u "$todo".oldsha1 >"$todo".oldsha1+ - mv "$todo".oldsha1+ "$todo".oldsha1 - sort -u "$todo".newsha1 >"$todo".newsha1+ - mv "$todo".newsha1+ "$todo".newsha1 - comm -2 -3 "$todo".oldsha1 "$todo".newsha1 >"$todo".miss - - # Warn about missing commits - if test -s "$todo".miss - then - test "$check_level" = error && raise_error=t - - warn "$(gettext "\ -Warning: some commits may have been dropped accidentally. -Dropped commits (newer to older):")" - - # Make the list user-friendly and display - opt="--no-walk=sorted --format=oneline --abbrev-commit --stdin" - git rev-list $opt <"$todo".miss | warn_lines - - warn "$(gettext "\ -To avoid this message, use \"drop\" to explicitly remove a commit. - -Use 'git config rebase.missingCommitsCheck' to change the level of warnings. -The possible behaviours are: ignore, warn, error.")" - warn - fi - ;; - ignore) - ;; - *) - warn "$(eval_gettext "Unrecognized setting \$check_level for option rebase.missingCommitsCheck. Ignoring.")" - ;; - esac - - if ! check_bad_cmd_and_sha "$todo" - then - raise_error=t - fi - - if test $raise_error = t - then - # Checkout before the first commit of the - # rebase: this way git rebase --continue - # will work correctly as it expects HEAD to be - # placed before the commit of the next action - checkout_onto - - warn "$(gettext "You can fix this with 'git rebase --edit-todo' and then run 'git rebase --continue'.")" - die "$(gettext "Or you can abort the rebase with 'git rebase --abort'.")" - fi -} - # The whole contents of this file is run by dot-sourcing it from # inside a shell function. It used to be that "return"s we see # below were not inside any function, and expected to return @@ -1321,7 +1163,11 @@ git_sequence_editor "$todo" || has_action "$todo" || return 2 -check_todo_list +git rebase--helper --check-todo-list || { + ret=$? + checkout_onto + exit $ret +} expand_todo_ids diff --git a/sequencer.c b/sequencer.c index 4bb966e8d9..08717a1bd3 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2471,3 +2471,128 @@ int transform_todo_ids(int shorten_sha1s) 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 commit_list *missing = NULL; + int raise_error = 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); + raise_error = res = + parse_insn_buffer(todo_list.buf.buf, &todo_list); + + if (check_level == CHECK_IGNORE) + goto leave_check; + + /* Get the SHA-1 of the commits */ + for (i = 0; i < todo_list.nr; i++) { + struct commit *commit = todo_list.items[i].commit; + if (commit) + commit->util = todo_list.items + i; + } + + 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 that are missing after editing */ + for (i = 0; i < todo_list.nr; i++) { + struct commit *commit = todo_list.items[i].commit; + if (commit && !commit->util) { + commit_list_insert(commit, &missing); + commit->util = todo_list.items + i; + } + } + + /* Warn about missing commits */ + if (!missing) + goto leave_check; + + if (check_level == CHECK_ERROR) + raise_error = 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 */ + while (missing) { + struct commit *commit = pop_commit(&missing); + struct todo_item *item = commit->util; + + fprintf(stderr, " - %s %.*s\n", short_commit_name(commit), + item->arg_len, item->arg); + } + free_commit_list(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 (raise_error) + 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; +} diff --git a/sequencer.h b/sequencer.h index 47a81034e7..4978a61b83 100644 --- a/sequencer.h +++ b/sequencer.h @@ -49,6 +49,7 @@ int sequencer_make_script(int keep_empty, FILE *out, int argc, const char **argv); int transform_todo_ids(int shorten_sha1s); +int check_todo_list(void); extern const char sign_off_header[]; From 084dca28c6774f1926ad640659828e34fdd88c93 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 15 Jun 2016 09:09:09 +0200 Subject: [PATCH 41/45] rebase -i: skip unnecessary picks using the rebase--helper In particular on Windows, where shell scripts are even more expensive than on MacOSX or Linux, it makes sense to move a loop that forks Git at least once for every line in the todo list into a builtin. Note: The original code did not try to skip unnecessary picks of root commits but punts instead (probably --root was not considered common enough of a use case to bother optimizing). We do the same, for now. Signed-off-by: Johannes Schindelin --- builtin/rebase--helper.c | 6 ++- git-rebase--interactive.sh | 41 ++--------------- sequencer.c | 90 ++++++++++++++++++++++++++++++++++++++ sequencer.h | 1 + 4 files changed, 99 insertions(+), 39 deletions(-) diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c index e706eac710..de3ccd9bfb 100644 --- a/builtin/rebase--helper.c +++ b/builtin/rebase--helper.c @@ -14,7 +14,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) int keep_empty = 0; enum { CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_SHA1S, EXPAND_SHA1S, - CHECK_TODO_LIST + CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS } command = 0; struct option options[] = { OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")), @@ -31,6 +31,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) N_("expand SHA-1s in the todo list"), EXPAND_SHA1S), OPT_CMDMODE(0, "check-todo-list", &command, N_("check the todo list"), CHECK_TODO_LIST), + OPT_CMDMODE(0, "skip-unnecessary-picks", &command, + N_("skip unnecessary picks"), SKIP_UNNECESSARY_PICKS), OPT_END() }; @@ -55,5 +57,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) return !!transform_todo_ids(0); if (command == CHECK_TODO_LIST && argc == 1) return !!check_todo_list(); + if (command == SKIP_UNNECESSARY_PICKS && argc == 1) + return !!skip_unnecessary_picks(); usage_with_options(builtin_rebase_helper_usage, options); } diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 78e78b6a7f..a1eae57bf2 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -712,43 +712,6 @@ do_rest () { done } -# skip picking commits whose parents are unchanged -skip_unnecessary_picks () { - fd=3 - while read -r command rest - do - # fd=3 means we skip the command - case "$fd,$command" in - 3,pick|3,p) - # pick a commit whose parent is current $onto -> skip - sha1=${rest%% *} - case "$(git rev-parse --verify --quiet "$sha1"^)" in - "$onto"*) - onto=$sha1 - ;; - *) - fd=1 - ;; - esac - ;; - 3,"$comment_char"*|3,) - # copy comments - ;; - *) - fd=1 - ;; - esac - printf '%s\n' "$command${rest:+ }$rest" >&$fd - done <"$todo" >"$todo.new" 3>>"$done" && - mv -f "$todo".new "$todo" && - case "$(peek_next_command)" in - squash|s|fixup|f) - record_in_rewritten "$onto" - ;; - esac || - die "$(gettext "Could not skip unnecessary pick commands")" -} - transform_todo_ids () { while read -r command rest do @@ -1171,7 +1134,9 @@ git rebase--helper --check-todo-list || { expand_todo_ids -test -d "$rewritten" || test -n "$force_rebase" || skip_unnecessary_picks +test -d "$rewritten" || test -n "$force_rebase" || +onto="$(git rebase--helper --skip-unnecessary-picks)" || +die "Could not skip unnecessary pick commands" checkout_onto if test -z "$rebase_root" && test ! -d "$rewritten" diff --git a/sequencer.c b/sequencer.c index 08717a1bd3..092a3def5e 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2596,3 +2596,93 @@ leave_check: return res; } + +/* 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_sha1(buf.buf, onto_oid.hash)) { + 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 (write_in_full(fd, todo_list.buf.buf, offset) < 0) { + todo_list_release(&todo_list); + return error_errno(_("could not write to '%s'"), + done_path); + } + close(fd); + + fd = open(rebase_path_todo(), O_WRONLY, 0666); + if (write_in_full(fd, todo_list.buf.buf + offset, + todo_list.buf.len - offset) < 0) { + todo_list_release(&todo_list); + return error_errno(_("could not write to '%s'"), + rebase_path_todo()); + } + if (ftruncate(fd, todo_list.buf.len - offset) < 0) { + todo_list_release(&todo_list); + return error_errno(_("could not truncate '%s'"), + rebase_path_todo()); + } + close(fd); + + 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; +} diff --git a/sequencer.h b/sequencer.h index 4978a61b83..28e1fc1e9b 100644 --- a/sequencer.h +++ b/sequencer.h @@ -50,6 +50,7 @@ int sequencer_make_script(int keep_empty, FILE *out, int transform_todo_ids(int shorten_sha1s); int check_todo_list(void); +int skip_unnecessary_picks(void); extern const char sign_off_header[]; From 234cc8656590e37a08b7da28019ac7f44746fd3f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Apr 2016 16:02:27 +0200 Subject: [PATCH 42/45] Add a builtin helper for interactive rebases Git's interactive rebase is still implemented as a shell script, despite its complexity. This implies that it suffers from the portability point of view, from lack of expressibility, and of course also from performance. The latter issue is particularly serious on Windows, where we pay a hefty price for relying so much on POSIX. Unfortunately, being such a huge shell script also means that we missed the train when it would have been relatively easy to port it to C, and instead piled feature upon feature onto that poor script that originally never intended to be more than a slightly pimped cherry-pick in a loop. To open the road toward better performance (in addition to all the other benefits of C over shell scripts), let's just start *somewhere*. The approach taken here is to add a builtin helper that at first intends to take care of the parts of the interactive rebase that are most affected by the performance penalties mentioned above. In particular, after we spent all those efforts on preparing the sequencer to process rebase -i's git-rebase-todo scripts, we implement the `git rebase -i --continue` functionality as a new builtin, git-rebase--helper. Once that is in place, we can work gradually on tackling the rest of the technical debt. Note that the rebase--helper needs to learn about the transient --ff/--no-ff options of git-rebase, as the corresponding flag is not persisted to, and re-read from, the state directory. Signed-off-by: Johannes Schindelin --- .gitignore | 1 + Makefile | 1 + builtin.h | 1 + builtin/rebase--helper.c | 40 ++++++++++++++++++++++++++++++++++++++++ git.c | 1 + 5 files changed, 44 insertions(+) create mode 100644 builtin/rebase--helper.c diff --git a/.gitignore b/.gitignore index 05cb58a3d4..a9b8c968c9 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,7 @@ /git-read-tree /git-rebase /git-rebase--am +/git-rebase--helper /git-rebase--interactive /git-rebase--merge /git-receive-pack diff --git a/Makefile b/Makefile index f53fcc90d7..70617d4639 100644 --- a/Makefile +++ b/Makefile @@ -929,6 +929,7 @@ BUILTIN_OBJS += builtin/prune.o BUILTIN_OBJS += builtin/pull.o BUILTIN_OBJS += builtin/push.o BUILTIN_OBJS += builtin/read-tree.o +BUILTIN_OBJS += builtin/rebase--helper.o BUILTIN_OBJS += builtin/receive-pack.o BUILTIN_OBJS += builtin/reflog.o BUILTIN_OBJS += builtin/remote.o diff --git a/builtin.h b/builtin.h index b9122bc5f4..6442c9bd6e 100644 --- a/builtin.h +++ b/builtin.h @@ -102,6 +102,7 @@ extern int cmd_prune_packed(int argc, const char **argv, const char *prefix); extern int cmd_pull(int argc, const char **argv, const char *prefix); extern int cmd_push(int argc, const char **argv, const char *prefix); extern int cmd_read_tree(int argc, const char **argv, const char *prefix); +extern int cmd_rebase__helper(int argc, const char **argv, const char *prefix); extern int cmd_receive_pack(int argc, const char **argv, const char *prefix); extern int cmd_reflog(int argc, const char **argv, const char *prefix); extern int cmd_remote(int argc, const char **argv, const char *prefix); diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c new file mode 100644 index 0000000000..ca1ebb2fa1 --- /dev/null +++ b/builtin/rebase--helper.c @@ -0,0 +1,40 @@ +#include "builtin.h" +#include "cache.h" +#include "parse-options.h" +#include "sequencer.h" + +static const char * const builtin_rebase_helper_usage[] = { + N_("git rebase--helper []"), + NULL +}; + +int cmd_rebase__helper(int argc, const char **argv, const char *prefix) +{ + struct replay_opts opts = REPLAY_OPTS_INIT; + enum { + CONTINUE = 1, ABORT + } command = 0; + struct option options[] = { + OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")), + OPT_CMDMODE(0, "continue", &command, N_("continue rebase"), + CONTINUE), + OPT_CMDMODE(0, "abort", &command, N_("abort rebase"), + ABORT), + OPT_END() + }; + + git_config(git_default_config, NULL); + + opts.action = REPLAY_INTERACTIVE_REBASE; + opts.allow_ff = 1; + opts.allow_empty = 1; + + argc = parse_options(argc, argv, NULL, options, + builtin_rebase_helper_usage, PARSE_OPT_KEEP_ARGV0); + + if (command == CONTINUE && argc == 1) + return !!sequencer_continue(&opts); + if (command == ABORT && argc == 1) + return !!sequencer_remove_state(&opts); + usage_with_options(builtin_rebase_helper_usage, options); +} diff --git a/git.c b/git.c index dce529fcbf..254e4e127e 100644 --- a/git.c +++ b/git.c @@ -472,6 +472,7 @@ static struct cmd_struct commands[] = { { "pull", cmd_pull, RUN_SETUP | NEED_WORK_TREE }, { "push", cmd_push, RUN_SETUP }, { "read-tree", cmd_read_tree, RUN_SETUP }, + { "rebase--helper", cmd_rebase__helper, RUN_SETUP | NEED_WORK_TREE }, { "receive-pack", cmd_receive_pack }, { "reflog", cmd_reflog, RUN_SETUP }, { "remote", cmd_remote, RUN_SETUP }, From 838389009f1f42e2324cee763dae14c993aa0d02 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 24 Jul 2016 10:22:13 +0200 Subject: [PATCH 43/45] t3415: test fixup with wrapped oneline The `git commit --fixup` command unwraps wrapped onelines when constructing the commit message, without wrapping the result. We need to make sure that `git rebase --autosquash` keeps handling such cases correctly, in particular since we are about to move the autosquash handling into the rebase--helper. Signed-off-by: Johannes Schindelin --- t/t3415-rebase-autosquash.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index 48346f1cc0..9fd629a6e2 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -304,4 +304,18 @@ test_expect_success 'extra spaces after fixup!' ' test $base = $parent ' +test_expect_success 'wrapped original subject' ' + if test -d .git/rebase-merge; then git rebase --abort; fi && + base=$(git rev-parse HEAD) && + echo "wrapped subject" >wrapped && + git add wrapped && + test_tick && + git commit --allow-empty -m "$(printf "To\nfixup")" && + test_tick && + git commit --allow-empty -m "fixup! To fixup" && + git rebase -i --autosquash --keep-empty HEAD~2 && + parent=$(git rev-parse HEAD^) && + test $base = $parent +' + test_done From 7876d7fc497e8dda790ee33002929d9fb7e86bf2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 28 Mar 2016 13:34:50 +0200 Subject: [PATCH 44/45] rebase -i: use the rebase--helper builtin Now that the sequencer learned to process a "normal" interactive rebase, we use it. The original shell script is still used for "non-normal" interactive rebases, i.e. when --root or --preserve-merges was passed. Please note that the --root option (via the $squash_onto variable) needs special handling only for the very first command, hence it is still okay to use the helper upon continue/skip. Also please note that the --no-ff setting is volatile, i.e. when the interactive rebase is interrupted at any stage, there is no record of it. Therefore, we have to pass it from the shell script to the rebase--helper. Note: the test t3404 had to be adjusted because the the error messages produced by the sequencer comply with our current convention to start with a lower-case letter. Signed-off-by: Johannes Schindelin --- git-rebase--interactive.sh | 13 +++++++++++++ t/t3404-rebase-interactive.sh | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 41fd374c72..301926804f 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -1068,6 +1068,10 @@ git_rebase__interactive () { case "$action" in continue) + if test ! -d "$rewritten" + then + exec git rebase--helper ${force_rebase:+--no-ff} --continue + fi # do we have anything to commit? if git diff-index --cached --quiet HEAD -- then @@ -1127,6 +1131,10 @@ first and then run 'git rebase --continue' again.")" skip) git rerere clear + if test ! -d "$rewritten" + then + exec git rebase--helper ${force_rebase:+--no-ff} --continue + fi do_rest return 0 ;; @@ -1313,6 +1321,11 @@ expand_todo_ids test -d "$rewritten" || test -n "$force_rebase" || skip_unnecessary_picks checkout_onto +if test -z "$rebase_root" && test ! -d "$rewritten" +then + require_clean_work_tree "rebase" + exec git rebase--helper ${force_rebase:+--no-ff} --continue +fi do_rest } diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index c896a4c106..5e0b56ffeb 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -540,7 +540,7 @@ test_expect_success 'clean error after failed "exec"' ' echo "edited again" > file7 && git add file7 && test_must_fail git rebase --continue 2>error && - test_i18ngrep "You have staged changes in your working tree." error + test_i18ngrep "you have staged changes in your working tree" error ' test_expect_success 'rebase a detached HEAD' ' From 13b660918d2fa3bb6f0e2aaf7946fe9147000829 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 16 Jun 2016 15:37:45 +0200 Subject: [PATCH 45/45] rebase -i: rearrange fixup/squash lines using the rebase--helper This operation has quadratic complexity, which is especially painful on Windows, where shell scripts are *already* slow (mainly due to the overhead of the POSIX emulation layer). Let's reimplement this with linear complexity (using a hash map to match the commits' subject lines) for the common case; Sadly, the fixup/squash feature's design neglected performance considerations, allowing arbitrary prefixes (read: `fixup! hell` will match the commit subject `hello world`), which means that we are stuck with quadratic performance in the worst case. The reimplemented logic also happens to fix a bug where commented-out lines (representing empty patches) were dropped by the previous code. Signed-off-by: Johannes Schindelin --- builtin/rebase--helper.c | 6 +- git-rebase--interactive.sh | 90 +--------------- sequencer.c | 198 +++++++++++++++++++++++++++++++++++ sequencer.h | 1 + t/t3415-rebase-autosquash.sh | 2 +- 5 files changed, 206 insertions(+), 91 deletions(-) diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c index de3ccd9bfb..e6591f0111 100644 --- a/builtin/rebase--helper.c +++ b/builtin/rebase--helper.c @@ -14,7 +14,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) int keep_empty = 0; enum { CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_SHA1S, EXPAND_SHA1S, - CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS + CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH } command = 0; struct option options[] = { OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")), @@ -33,6 +33,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) N_("check the todo list"), CHECK_TODO_LIST), OPT_CMDMODE(0, "skip-unnecessary-picks", &command, N_("skip unnecessary picks"), SKIP_UNNECESSARY_PICKS), + OPT_CMDMODE(0, "rearrange-squash", &command, + N_("rearrange fixup/squash lines"), REARRANGE_SQUASH), OPT_END() }; @@ -59,5 +61,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix) return !!check_todo_list(); if (command == SKIP_UNNECESSARY_PICKS && argc == 1) return !!skip_unnecessary_picks(); + if (command == REARRANGE_SQUASH && argc == 1) + return !!rearrange_squash(); usage_with_options(builtin_rebase_helper_usage, options); } diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index a1eae57bf2..8790d8ed8b 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -743,94 +743,6 @@ collapse_todo_ids() { git rebase--helper --shorten-sha1s } -# Rearrange the todo list that has both "pick sha1 msg" and -# "pick sha1 fixup!/squash! msg" appears in it so that the latter -# comes 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 be re-retrieved in order to normalize the -# autosquash arrangement -rearrange_squash () { - format=$(git config --get rebase.instructionFormat) - # extract fixup!/squash! lines and resolve any referenced sha1's - while read -r pick sha1 message - do - test -z "${format}" || message=$(git log -n 1 --format="%s" ${sha1}) - case "$message" in - "squash! "*|"fixup! "*) - action="${message%%!*}" - rest=$message - prefix= - # skip all squash! or fixup! (but save for later) - while : - do - case "$rest" in - "squash! "*|"fixup! "*) - prefix="$prefix${rest%%!*}," - rest="${rest#*! }" - ;; - *) - break - ;; - esac - done - printf '%s %s %s %s\n' "$sha1" "$action" "$prefix" "$rest" - # if it's a single word, try to resolve to a full sha1 and - # emit a second copy. This allows us to match on both message - # and on sha1 prefix - if test "${rest#* }" = "$rest"; then - fullsha="$(git rev-parse -q --verify "$rest" 2>/dev/null)" - if test -n "$fullsha"; then - # prefix the action to uniquely identify this line as - # intended for full sha1 match - echo "$sha1 +$action $prefix $fullsha" - fi - fi - esac - done >"$1.sq" <"$1" - test -s "$1.sq" || return - - used= - while read -r pick sha1 message - do - case " $used" in - *" $sha1 "*) continue ;; - esac - printf '%s\n' "$pick $sha1 $message" - test -z "${format}" || message=$(git log -n 1 --format="%s" ${sha1}) - used="$used$sha1 " - while read -r squash action msg_prefix msg_content - do - case " $used" in - *" $squash "*) continue ;; - esac - emit=0 - case "$action" in - +*) - action="${action#+}" - # full sha1 prefix test - case "$msg_content" in "$sha1"*) emit=1;; esac ;; - *) - # message prefix test - case "$message" in "$msg_content"*) emit=1;; esac ;; - esac - if test $emit = 1; then - if test -n "${format}" - then - msg_content=$(git log -n 1 --format="${format}" ${squash}) - else - msg_content="$(echo "$msg_prefix" | sed "s/,/! /g")$msg_content" - fi - printf '%s\n' "$action $squash $msg_content" - used="$used$squash " - fi - done <"$1.sq" - done >"$1.rearranged" <"$1" - cat "$1.rearranged" >"$1" - rm -f "$1.sq" "$1.rearranged" -} - # Add commands after a pick or after a squash/fixup serie # in the todo list. add_exec_commands () { @@ -1090,7 +1002,7 @@ then fi test -s "$todo" || echo noop >> "$todo" -test -n "$autosquash" && rearrange_squash "$todo" +test -z "$autosquash" || git rebase--helper --rearrange-squash || exit test -n "$cmd" && add_exec_commands "$todo" todocount=$(git stripspace --strip-comments <"$todo" | wc -l) diff --git a/sequencer.c b/sequencer.c index 092a3def5e..984c0fc2aa 100644 --- a/sequencer.c +++ b/sequencer.c @@ -18,6 +18,7 @@ #include "quote.h" #include "log-tree.h" #include "wt-status.h" +#include "hashmap.h" #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION" @@ -2686,3 +2687,200 @@ int skip_unnecessary_picks(void) return 0; } + +struct subject2item_entry { + struct hashmap_entry entry; + int i; + char subject[FLEX_ARRAY]; +}; + +static int subject2item_cmp(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 sha1 msg" and "pick sha1 + * 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, + 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; + 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] = buf.buf; + 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, buf.buf, buf.len); + entry->i = i; + hashmap_entry_init(entry, strhash(entry->subject)); + hashmap_put(&subject2item, entry); + } + strbuf_detach(&buf, NULL); + } + + if (rearranged) { + struct strbuf buf = STRBUF_INIT; + char *format = NULL; + + git_config_get_string("rebase.instructionFormat", &format); + 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]; + } + } + + fd = open(todo_file, O_WRONLY); + if (fd < 0) + res = error_errno(_("could not open '%s'"), todo_file); + else if (write(fd, buf.buf, buf.len) < 0) + res = error_errno(_("could not read '%s'."), todo_file); + else if (ftruncate(fd, buf.len) < 0) + res = error_errno(_("could not finish '%s'"), + todo_file); + close(fd); + 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; +} diff --git a/sequencer.h b/sequencer.h index 28e1fc1e9b..1c94bec762 100644 --- a/sequencer.h +++ b/sequencer.h @@ -51,6 +51,7 @@ int sequencer_make_script(int keep_empty, FILE *out, int transform_todo_ids(int shorten_sha1s); int check_todo_list(void); int skip_unnecessary_picks(void); +int rearrange_squash(void); extern const char sign_off_header[]; diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index 9fd629a6e2..b9e26008a7 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -278,7 +278,7 @@ set_backup_editor () { test_set_editor "$PWD/backup-editor.sh" } -test_expect_failure 'autosquash with multiple empty patches' ' +test_expect_success 'autosquash with multiple empty patches' ' test_tick && git commit --allow-empty -m "empty" && test_tick &&