rebase: introduce a shortcut for --reschedule-failed-exec
[gitweb.git] / builtin / rebase.c
index 59e6f7852f5cd2ca77e0aef721f2875edd7a6794..1963076f6064456f1e89b286458bd78de8934ddf 100644 (file)
@@ -49,7 +49,10 @@ static int use_builtin_rebase(void)
 {
        struct child_process cp = CHILD_PROCESS_INIT;
        struct strbuf out = STRBUF_INIT;
-       int ret;
+       int ret, env = git_env_bool("GIT_TEST_REBASE_USE_BUILTIN", -1);
+
+       if (env != -1)
+               return env;
 
        argv_array_pushl(&cp.args,
                         "config", "--bool", "rebase.usebuiltin", NULL);
@@ -88,7 +91,7 @@ struct rebase_options {
                REBASE_FORCE = 1<<3,
                REBASE_INTERACTIVE_EXPLICIT = 1<<4,
        } flags;
-       struct strbuf git_am_opt;
+       struct argv_array git_am_opts;
        const char *action;
        int signoff;
        int allow_rerere_autoupdate;
@@ -101,6 +104,7 @@ struct rebase_options {
        int rebase_merges, rebase_cousins;
        char *strategy, *strategy_opts;
        struct strbuf git_format_patch_opt;
+       int reschedule_failed_exec;
 };
 
 static int is_interactive(struct rebase_options *opts)
@@ -340,7 +344,7 @@ N_("Resolve all conflicts manually, mark them as resolved with\n"
 static int run_specific_rebase(struct rebase_options *opts)
 {
        const char *argv[] = { NULL, NULL };
-       struct strbuf script_snippet = STRBUF_INIT;
+       struct strbuf script_snippet = STRBUF_INIT, buf = STRBUF_INIT;
        int status;
        const char *backend, *backend_func;
 
@@ -412,6 +416,8 @@ static int run_specific_rebase(struct rebase_options *opts)
                        argv_array_push(&child.args, opts->gpg_sign_opt);
                if (opts->signoff)
                        argv_array_push(&child.args, "--signoff");
+               if (opts->reschedule_failed_exec)
+                       argv_array_push(&child.args, "--reschedule-failed-exec");
 
                status = run_command(&child);
                goto finished_rebase;
@@ -434,7 +440,9 @@ static int run_specific_rebase(struct rebase_options *opts)
                oid_to_hex(&opts->restrict_revision->object.oid) : NULL);
        add_var(&script_snippet, "GIT_QUIET",
                opts->flags & REBASE_NO_QUIET ? "" : "t");
-       add_var(&script_snippet, "git_am_opt", opts->git_am_opt.buf);
+       sq_quote_argv_pretty(&buf, opts->git_am_opts.argv);
+       add_var(&script_snippet, "git_am_opt", buf.buf);
+       strbuf_release(&buf);
        add_var(&script_snippet, "verbose",
                opts->flags & REBASE_VERBOSE ? "t" : "");
        add_var(&script_snippet, "diffstat",
@@ -577,7 +585,8 @@ static int reset_head(struct object_id *oid, const char *action,
        }
 
        if (!reset_hard && !fill_tree_descriptor(&desc[nr++], &head_oid)) {
-               ret = error(_("failed to find tree of %s"), oid_to_hex(oid));
+               ret = error(_("failed to find tree of %s"),
+                           oid_to_hex(&head_oid));
                goto leave_reset_head;
        }
 
@@ -668,6 +677,11 @@ static int rebase_config(const char *var, const char *value, void *data)
                return 0;
        }
 
+       if (!strcmp(var, "rebase.reschedulefailedexec")) {
+               opts->reschedule_failed_exec = git_config_bool(var, value);
+               return 0;
+       }
+
        return git_default_config(var, value, data);
 }
 
@@ -740,6 +754,23 @@ static int parse_opt_interactive(const struct option *opt, const char *arg,
        return 0;
 }
 
+struct opt_y {
+       struct string_list *list;
+       struct rebase_options *options;
+};
+
+static int parse_opt_y(const struct option *opt, const char *arg, int unset)
+{
+       struct opt_y *o = opt->value;
+
+       if (unset || !arg)
+               return -1;
+
+       o->options->reschedule_failed_exec = 1;
+       string_list_append(o->list, arg);
+       return 0;
+}
+
 static void NORETURN error_on_missing_default_upstream(void)
 {
        struct branch *current_branch = branch_get(NULL);
@@ -770,12 +801,29 @@ static void NORETURN error_on_missing_default_upstream(void)
        exit(1);
 }
 
+static void set_reflog_action(struct rebase_options *options)
+{
+       const char *env;
+       struct strbuf buf = STRBUF_INIT;
+
+       if (!is_interactive(options))
+               return;
+
+       env = getenv(GIT_REFLOG_ACTION_ENVIRONMENT);
+       if (env && strcmp("rebase", env))
+               return; /* only override it if it is "rebase" */
+
+       strbuf_addf(&buf, "rebase -i (%s)", options->action);
+       setenv(GIT_REFLOG_ACTION_ENVIRONMENT, buf.buf, 1);
+       strbuf_release(&buf);
+}
+
 int cmd_rebase(int argc, const char **argv, const char *prefix)
 {
        struct rebase_options options = {
                .type = REBASE_UNSPECIFIED,
                .flags = REBASE_NO_QUIET,
-               .git_am_opt = STRBUF_INIT,
+               .git_am_opts = ARGV_ARRAY_INIT,
                .allow_rerere_autoupdate  = -1,
                .allow_empty_message = 1,
                .git_format_patch_opt = STRBUF_INIT,
@@ -796,18 +844,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                ACTION_EDIT_TODO,
                ACTION_SHOW_CURRENT_PATCH,
        } action = NO_ACTION;
-       int committer_date_is_author_date = 0;
-       int ignore_date = 0;
-       int ignore_whitespace = 0;
        const char *gpg_sign = NULL;
-       int opt_c = -1;
-       struct string_list whitespace = STRING_LIST_INIT_NODUP;
        struct string_list exec = STRING_LIST_INIT_NODUP;
        const char *rebase_merges = NULL;
        int fork_point = -1;
        struct string_list strategy_options = STRING_LIST_INIT_NODUP;
        struct object_id squash_onto;
        char *squash_onto_name = NULL;
+       struct opt_y opt_y = { .list = &exec, .options = &options };
        struct option builtin_rebase_options[] = {
                OPT_STRING(0, "onto", &options.onto_name,
                           N_("revision"),
@@ -823,15 +867,20 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                {OPTION_NEGBIT, 'n', "no-stat", &options.flags, NULL,
                        N_("do not show diffstat of what changed upstream"),
                        PARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT },
-               OPT_BOOL(0, "ignore-whitespace", &ignore_whitespace,
-                        N_("passed to 'git apply'")),
                OPT_BOOL(0, "signoff", &options.signoff,
                         N_("add a Signed-off-by: line to each commit")),
-               OPT_BOOL(0, "committer-date-is-author-date",
-                        &committer_date_is_author_date,
-                        N_("passed to 'git am'")),
-               OPT_BOOL(0, "ignore-date", &ignore_date,
-                        N_("passed to 'git am'")),
+               OPT_PASSTHRU_ARGV(0, "ignore-whitespace", &options.git_am_opts,
+                                 NULL, N_("passed to 'git am'"),
+                                 PARSE_OPT_NOARG),
+               OPT_PASSTHRU_ARGV(0, "committer-date-is-author-date",
+                                 &options.git_am_opts, NULL,
+                                 N_("passed to 'git am'"), PARSE_OPT_NOARG),
+               OPT_PASSTHRU_ARGV(0, "ignore-date", &options.git_am_opts, NULL,
+                                 N_("passed to 'git am'"), PARSE_OPT_NOARG),
+               OPT_PASSTHRU_ARGV('C', NULL, &options.git_am_opts, N_("n"),
+                                 N_("passed to 'git apply'"), 0),
+               OPT_PASSTHRU_ARGV(0, "whitespace", &options.git_am_opts,
+                                 N_("action"), N_("passed to 'git apply'"), 0),
                OPT_BIT('f', "force-rebase", &options.flags,
                        N_("cherry-pick all commits, even if unchanged"),
                        REBASE_FORCE),
@@ -865,7 +914,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                               "them"), REBASE_PRESERVE_MERGES),
                OPT_BOOL(0, "rerere-autoupdate",
                         &options.allow_rerere_autoupdate,
-                        N_("allow rerere to update index  with resolved "
+                        N_("allow rerere to update index with resolved "
                            "conflict")),
                OPT_BOOL('k', "keep-empty", &options.keep_empty,
                         N_("preserve empty commits during rebase")),
@@ -875,15 +924,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"),
                        N_("GPG-sign commits"),
                        PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
-               OPT_STRING_LIST(0, "whitespace", &whitespace,
-                               N_("whitespace"), N_("passed to 'git apply'")),
-               OPT_SET_INT('C', NULL, &opt_c, N_("passed to 'git apply'"),
-                           REBASE_AM),
                OPT_BOOL(0, "autostash", &options.autostash,
                         N_("automatically stash/stash pop before and after")),
                OPT_STRING_LIST('x', "exec", &exec, N_("exec"),
                                N_("add exec lines after each commit of the "
                                   "editable list")),
+               { OPTION_CALLBACK, 'y', NULL, &opt_y, N_("<cmd>"),
+                       N_("same as --reschedule-failed-exec -x <cmd>"),
+                       PARSE_OPT_NONEG, parse_opt_y },
                OPT_BOOL(0, "allow-empty-message",
                         &options.allow_empty_message,
                         N_("allow rebasing commits with empty messages")),
@@ -901,8 +949,12 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                                   "strategy")),
                OPT_BOOL(0, "root", &options.root,
                         N_("rebase all reachable commits up to the root(s)")),
+               OPT_BOOL(0, "reschedule-failed-exec",
+                        &options.reschedule_failed_exec,
+                        N_("automatically re-schedule any `exec` that fails")),
                OPT_END(),
        };
+       int i;
 
        /*
         * NEEDSWORK: Once the builtin rebase has been tested enough
@@ -975,6 +1027,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
        if (action != NO_ACTION && !in_progress)
                die(_("No rebase in progress?"));
+       setenv(GIT_REFLOG_ACTION_ENVIRONMENT, "rebase", 0);
 
        if (action == ACTION_EDIT_TODO && !is_interactive(&options))
                die(_("The --edit-todo action can only be used during "
@@ -987,6 +1040,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                int fd;
 
                options.action = "continue";
+               set_reflog_action(&options);
 
                /* Sanity check */
                if (get_oid("HEAD", &head))
@@ -1015,6 +1069,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                struct string_list merge_rr = STRING_LIST_INIT_DUP;
 
                options.action = "skip";
+               set_reflog_action(&options);
 
                rerere_clear(&merge_rr);
                string_list_clear(&merge_rr, 1);
@@ -1030,6 +1085,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
        case ACTION_ABORT: {
                struct string_list merge_rr = STRING_LIST_INIT_DUP;
                options.action = "abort";
+               set_reflog_action(&options);
 
                rerere_clear(&merge_rr);
                string_list_clear(&merge_rr, 1);
@@ -1087,22 +1143,27 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                    state_dir_base, cmd_live_rebase, buf.buf);
        }
 
-       if (!(options.flags & REBASE_NO_QUIET))
-               strbuf_addstr(&options.git_am_opt, " -q");
-
-       if (committer_date_is_author_date) {
-               strbuf_addstr(&options.git_am_opt,
-                             " --committer-date-is-author-date");
-               options.flags |= REBASE_FORCE;
+       for (i = 0; i < options.git_am_opts.argc; i++) {
+               const char *option = options.git_am_opts.argv[i], *p;
+               if (!strcmp(option, "--committer-date-is-author-date") ||
+                   !strcmp(option, "--ignore-date") ||
+                   !strcmp(option, "--whitespace=fix") ||
+                   !strcmp(option, "--whitespace=strip"))
+                       options.flags |= REBASE_FORCE;
+               else if (skip_prefix(option, "-C", &p)) {
+                       while (*p)
+                               if (!isdigit(*(p++)))
+                                       die(_("switch `C' expects a "
+                                             "numerical value"));
+               } else if (skip_prefix(option, "--whitespace=", &p)) {
+                       if (*p && strcmp(p, "warn") && strcmp(p, "nowarn") &&
+                           strcmp(p, "error") && strcmp(p, "error-all"))
+                               die("Invalid whitespace option: '%s'", p);
+               }
        }
 
-       if (ignore_whitespace)
-               strbuf_addstr(&options.git_am_opt, " --ignore-whitespace");
-
-       if (ignore_date) {
-               strbuf_addstr(&options.git_am_opt, " --ignore-date");
-               options.flags |= REBASE_FORCE;
-       }
+       if (!(options.flags & REBASE_NO_QUIET))
+               argv_array_push(&options.git_am_opts, "-q");
 
        if (options.keep_empty)
                imply_interactive(&options, "--keep-empty");
@@ -1112,23 +1173,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                options.gpg_sign_opt = xstrfmt("-S%s", gpg_sign);
        }
 
-       if (opt_c >= 0)
-               strbuf_addf(&options.git_am_opt, " -C%d", opt_c);
-
-       if (whitespace.nr) {
-               int i;
-
-               for (i = 0; i < whitespace.nr; i++) {
-                       const char *item = whitespace.items[i].string;
-
-                       strbuf_addf(&options.git_am_opt, " --whitespace=%s",
-                                   item);
-
-                       if ((!strcmp(item, "fix")) || (!strcmp(item, "strip")))
-                               options.flags |= REBASE_FORCE;
-               }
-       }
-
        if (exec.nr) {
                int i;
 
@@ -1204,23 +1248,21 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                break;
        }
 
-       if (options.git_am_opt.len) {
-               const char *p;
+       if (options.reschedule_failed_exec && !is_interactive(&options))
+               die(_("--reschedule-failed-exec requires an interactive rebase"));
 
+       if (options.git_am_opts.argc) {
                /* all am options except -q are compatible only with --am */
-               strbuf_reset(&buf);
-               strbuf_addbuf(&buf, &options.git_am_opt);
-               strbuf_addch(&buf, ' ');
-               while ((p = strstr(buf.buf, " -q ")))
-                       strbuf_splice(&buf, p - buf.buf, 4, " ", 1);
-               strbuf_trim(&buf);
+               for (i = options.git_am_opts.argc - 1; i >= 0; i--)
+                       if (strcmp(options.git_am_opts.argv[i], "-q"))
+                               break;
 
-               if (is_interactive(&options) && buf.len)
+               if (is_interactive(&options) && i >= 0)
                        die(_("error: cannot combine interactive options "
                              "(--interactive, --exec, --rebase-merges, "
                              "--preserve-merges, --keep-empty, --root + "
                              "--onto) with am options (%s)"), buf.buf);
-               if (options.type == REBASE_MERGE && buf.len)
+               if (options.type == REBASE_MERGE && i >= 0)
                        die(_("error: cannot combine merge options (--merge, "
                              "--strategy, --strategy-option) with am options "
                              "(%s)"), buf.buf);
@@ -1230,11 +1272,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                if (options.type == REBASE_PRESERVE_MERGES)
                        die("cannot combine '--signoff' with "
                            "'--preserve-merges'");
-               strbuf_addstr(&options.git_am_opt, " --signoff");
+               argv_array_push(&options.git_am_opts, "--signoff");
                options.flags |= REBASE_FORCE;
        }
 
-       if (options.type == REBASE_PRESERVE_MERGES)
+       if (options.type == REBASE_PRESERVE_MERGES) {
                /*
                 * Note: incompatibility with --signoff handled in signoff block above
                 * Note: incompatibility with --interactive is just a strong warning;
@@ -1244,6 +1286,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                        die(_("error: cannot combine '--preserve-merges' with "
                              "'--rebase-merges'"));
 
+               if (options.reschedule_failed_exec)
+                       die(_("error: cannot combine '--preserve-merges' with "
+                             "'--reschedule-failed-exec'"));
+       }
+
        if (options.rebase_merges) {
                if (strategy_options.nr)
                        die(_("error: cannot combine '--rebase-merges' with "
@@ -1454,11 +1501,12 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                                }
 
                                strbuf_reset(&buf);
-                               strbuf_addf(&buf, "rebase: checkout %s",
+                               strbuf_addf(&buf, "%s: checkout %s",
+                                           getenv(GIT_REFLOG_ACTION_ENVIRONMENT),
                                            options.switch_to);
                                if (reset_head(&oid, "checkout",
                                               options.head_name, 0,
-                                              NULL, NULL) < 0) {
+                                              NULL, buf.buf) < 0) {
                                        ret = !!error(_("could not switch to "
                                                        "%s"),
                                                      options.switch_to);
@@ -1495,10 +1543,15 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
        if (options.flags & REBASE_DIFFSTAT) {
                struct diff_options opts;
 
-               if (options.flags & REBASE_VERBOSE)
-                       printf(_("Changes from %s to %s:\n"),
-                               oid_to_hex(&merge_base),
-                               oid_to_hex(&options.onto->object.oid));
+               if (options.flags & REBASE_VERBOSE) {
+                       if (is_null_oid(&merge_base))
+                               printf(_("Changes to %s:\n"),
+                                      oid_to_hex(&options.onto->object.oid));
+                       else
+                               printf(_("Changes from %s to %s:\n"),
+                                      oid_to_hex(&merge_base),
+                                      oid_to_hex(&options.onto->object.oid));
+               }
 
                /* We want color (if set), but no pager */
                diff_setup(&opts);
@@ -1508,8 +1561,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                        DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT;
                opts.detect_rename = DIFF_DETECT_RENAME;
                diff_setup_done(&opts);
-               diff_tree_oid(&merge_base, &options.onto->object.oid,
-                             "", &opts);
+               diff_tree_oid(is_null_oid(&merge_base) ?
+                             the_hash_algo->empty_tree : &merge_base,
+                             &options.onto->object.oid, "", &opts);
                diffcore_std(&opts);
                diff_flush(&opts);
        }
@@ -1522,7 +1576,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                printf(_("First, rewinding head to replay your work on top of "
                         "it...\n"));
 
-       strbuf_addf(&msg, "rebase: checkout %s", options.onto_name);
+       strbuf_addf(&msg, "%s: checkout %s",
+                   getenv(GIT_REFLOG_ACTION_ENVIRONMENT), options.onto_name);
        if (reset_head(&options.onto->object.oid, "checkout", NULL,
                       RESET_HEAD_DETACH, NULL, msg.buf))
                die(_("Could not detach HEAD"));
@@ -1534,7 +1589,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
         */
        strbuf_reset(&msg);
        if (!oidcmp(&merge_base, &options.orig_head)) {
-               printf(_("Fast-forwarded %s to %s. \n"),
+               printf(_("Fast-forwarded %s to %s.\n"),
                        branch_name, options.onto_name);
                strbuf_addf(&msg, "rebase finished: %s onto %s",
                        options.head_name ? options.head_name : "detached HEAD",