builtin rebase: support --committer-date-is-author-date
[gitweb.git] / builtin / rebase.c
index d67df28efc9a9450fca76332183386e0ae38ef97..6ce50eae0f2ea7da89348f40ed5046c18ab8ab29 100644 (file)
@@ -21,6 +21,7 @@
 #include "diff.h"
 #include "wt-status.h"
 #include "revision.h"
+#include "rerere.h"
 
 static char const * const builtin_rebase_usage[] = {
        N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] "
@@ -79,6 +80,7 @@ struct rebase_options {
        struct commit *onto;
        const char *onto_name;
        const char *revisions;
+       const char *switch_to;
        int root;
        struct commit *restrict_revision;
        int dont_finish_rebase;
@@ -86,8 +88,13 @@ struct rebase_options {
                REBASE_NO_QUIET = 1<<0,
                REBASE_VERBOSE = 1<<1,
                REBASE_DIFFSTAT = 1<<2,
+               REBASE_FORCE = 1<<3,
+               REBASE_INTERACTIVE_EXPLICIT = 1<<4,
        } flags;
        struct strbuf git_am_opt;
+       const char *action;
+       int signoff;
+       int allow_rerere_autoupdate;
 };
 
 static int is_interactive(struct rebase_options *opts)
@@ -112,6 +119,82 @@ static const char *state_dir_path(const char *filename, struct rebase_options *o
        return path.buf;
 }
 
+/* Read one file, then strip line endings */
+static int read_one(const char *path, struct strbuf *buf)
+{
+       if (strbuf_read_file(buf, path, 0) < 0)
+               return error_errno(_("could not read '%s'"), path);
+       strbuf_trim_trailing_newline(buf);
+       return 0;
+}
+
+/* Initialize the rebase options from the state directory. */
+static int read_basic_state(struct rebase_options *opts)
+{
+       struct strbuf head_name = STRBUF_INIT;
+       struct strbuf buf = STRBUF_INIT;
+       struct object_id oid;
+
+       if (read_one(state_dir_path("head-name", opts), &head_name) ||
+           read_one(state_dir_path("onto", opts), &buf))
+               return -1;
+       opts->head_name = starts_with(head_name.buf, "refs/") ?
+               xstrdup(head_name.buf) : NULL;
+       strbuf_release(&head_name);
+       if (get_oid(buf.buf, &oid))
+               return error(_("could not get 'onto': '%s'"), buf.buf);
+       opts->onto = lookup_commit_or_die(&oid, buf.buf);
+
+       /*
+        * We always write to orig-head, but interactive rebase used to write to
+        * head. Fall back to reading from head to cover for the case that the
+        * user upgraded git with an ongoing interactive rebase.
+        */
+       strbuf_reset(&buf);
+       if (file_exists(state_dir_path("orig-head", opts))) {
+               if (read_one(state_dir_path("orig-head", opts), &buf))
+                       return -1;
+       } else if (read_one(state_dir_path("head", opts), &buf))
+               return -1;
+       if (get_oid(buf.buf, &opts->orig_head))
+               return error(_("invalid orig-head: '%s'"), buf.buf);
+
+       strbuf_reset(&buf);
+       if (read_one(state_dir_path("quiet", opts), &buf))
+               return -1;
+       if (buf.len)
+               opts->flags &= ~REBASE_NO_QUIET;
+       else
+               opts->flags |= REBASE_NO_QUIET;
+
+       if (file_exists(state_dir_path("verbose", opts)))
+               opts->flags |= REBASE_VERBOSE;
+
+       if (file_exists(state_dir_path("signoff", opts))) {
+               opts->signoff = 1;
+               opts->flags |= REBASE_FORCE;
+       }
+
+       if (file_exists(state_dir_path("allow_rerere_autoupdate", opts))) {
+               strbuf_reset(&buf);
+               if (read_one(state_dir_path("allow_rerere_autoupdate", opts),
+                           &buf))
+                       return -1;
+               if (!strcmp(buf.buf, "--rerere-autoupdate"))
+                       opts->allow_rerere_autoupdate = 1;
+               else if (!strcmp(buf.buf, "--no-rerere-autoupdate"))
+                       opts->allow_rerere_autoupdate = 0;
+               else
+                       warning(_("ignoring invalid allow_rerere_autoupdate: "
+                                 "'%s'"), buf.buf);
+       } else
+               opts->allow_rerere_autoupdate = -1;
+
+       strbuf_release(&buf);
+
+       return 0;
+}
+
 static int finish_rebase(struct rebase_options *opts)
 {
        struct strbuf dir = STRBUF_INIT;
@@ -165,11 +248,13 @@ static int run_specific_rebase(struct rebase_options *opts)
        add_var(&script_snippet, "state_dir", opts->state_dir);
 
        add_var(&script_snippet, "upstream_name", opts->upstream_name);
-       add_var(&script_snippet, "upstream",
-                                oid_to_hex(&opts->upstream->object.oid));
-       add_var(&script_snippet, "head_name", opts->head_name);
+       add_var(&script_snippet, "upstream", opts->upstream ?
+               oid_to_hex(&opts->upstream->object.oid) : NULL);
+       add_var(&script_snippet, "head_name",
+               opts->head_name ? opts->head_name : "detached HEAD");
        add_var(&script_snippet, "orig_head", oid_to_hex(&opts->orig_head));
-       add_var(&script_snippet, "onto", oid_to_hex(&opts->onto->object.oid));
+       add_var(&script_snippet, "onto", opts->onto ?
+               oid_to_hex(&opts->onto->object.oid) : NULL);
        add_var(&script_snippet, "onto_name", opts->onto_name);
        add_var(&script_snippet, "revisions", opts->revisions);
        add_var(&script_snippet, "restrict_revision", opts->restrict_revision ?
@@ -181,6 +266,16 @@ static int run_specific_rebase(struct rebase_options *opts)
                opts->flags & REBASE_VERBOSE ? "t" : "");
        add_var(&script_snippet, "diffstat",
                opts->flags & REBASE_DIFFSTAT ? "t" : "");
+       add_var(&script_snippet, "force_rebase",
+               opts->flags & REBASE_FORCE ? "t" : "");
+       if (opts->switch_to)
+               add_var(&script_snippet, "switch_to", opts->switch_to);
+       add_var(&script_snippet, "action", opts->action ? opts->action : "");
+       add_var(&script_snippet, "signoff", opts->signoff ? "--signoff" : "");
+       add_var(&script_snippet, "allow_rerere_autoupdate",
+               opts->allow_rerere_autoupdate < 0 ? "" :
+               opts->allow_rerere_autoupdate ?
+               "--rerere-autoupdate" : "--no-rerere-autoupdate");
 
        switch (opts->type) {
        case REBASE_AM:
@@ -247,6 +342,9 @@ static int reset_head(struct object_id *oid, const char *action,
                *old_orig = NULL, oid_old_orig;
        int ret = 0;
 
+       if (switch_to_branch && !starts_with(switch_to_branch, "refs/"))
+               BUG("Not a fully qualified branch: '%s'", switch_to_branch);
+
        if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
                return -1;
 
@@ -381,19 +479,54 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid,
        return res && is_linear_history(onto, head);
 }
 
+/* -i followed by -m is still -i */
+static int parse_opt_merge(const struct option *opt, const char *arg, int unset)
+{
+       struct rebase_options *opts = opt->value;
+
+       if (!is_interactive(opts))
+               opts->type = REBASE_MERGE;
+
+       return 0;
+}
+
+/* -i followed by -p is still explicitly interactive, but -p alone is not */
+static int parse_opt_interactive(const struct option *opt, const char *arg,
+                                int unset)
+{
+       struct rebase_options *opts = opt->value;
+
+       opts->type = REBASE_INTERACTIVE;
+       opts->flags |= REBASE_INTERACTIVE_EXPLICIT;
+
+       return 0;
+}
+
 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,
+               .allow_rerere_autoupdate  = -1,
        };
        const char *branch_name;
-       int ret, flags;
+       int ret, flags, total_argc, in_progress = 0;
        int ok_to_skip_pre_rebase = 0;
        struct strbuf msg = STRBUF_INIT;
        struct strbuf revisions = STRBUF_INIT;
+       struct strbuf buf = STRBUF_INIT;
        struct object_id merge_base;
+       enum {
+               NO_ACTION,
+               ACTION_CONTINUE,
+               ACTION_SKIP,
+               ACTION_ABORT,
+               ACTION_QUIT,
+               ACTION_EDIT_TODO,
+               ACTION_SHOW_CURRENT_PATCH,
+       } action = NO_ACTION;
+       int committer_date_is_author_date = 0;
        struct option builtin_rebase_options[] = {
                OPT_STRING(0, "onto", &options.onto_name,
                           N_("revision"),
@@ -409,6 +542,46 @@ 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, "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_BIT('f', "force-rebase", &options.flags,
+                       N_("cherry-pick all commits, even if unchanged"),
+                       REBASE_FORCE),
+               OPT_BIT(0, "no-ff", &options.flags,
+                       N_("cherry-pick all commits, even if unchanged"),
+                       REBASE_FORCE),
+               OPT_CMDMODE(0, "continue", &action, N_("continue"),
+                           ACTION_CONTINUE),
+               OPT_CMDMODE(0, "skip", &action,
+                           N_("skip current patch and continue"), ACTION_SKIP),
+               OPT_CMDMODE(0, "abort", &action,
+                           N_("abort and check out the original branch"),
+                           ACTION_ABORT),
+               OPT_CMDMODE(0, "quit", &action,
+                           N_("abort but keep HEAD where it is"), ACTION_QUIT),
+               OPT_CMDMODE(0, "edit-todo", &action, N_("edit the todo list "
+                           "during an interactive rebase"), ACTION_EDIT_TODO),
+               OPT_CMDMODE(0, "show-current-patch", &action,
+                           N_("show the patch file being applied or merged"),
+                           ACTION_SHOW_CURRENT_PATCH),
+               { OPTION_CALLBACK, 'm', "merge", &options, NULL,
+                       N_("use merging strategies to rebase"),
+                       PARSE_OPT_NOARG | PARSE_OPT_NONEG,
+                       parse_opt_merge },
+               { OPTION_CALLBACK, 'i', "interactive", &options, NULL,
+                       N_("let the user edit the list of commits to rebase"),
+                       PARSE_OPT_NOARG | PARSE_OPT_NONEG,
+                       parse_opt_interactive },
+               OPT_SET_INT('p', "preserve-merges", &options.type,
+                           N_("try to recreate merges instead of ignoring "
+                              "them"), REBASE_PRESERVE_MERGES),
+               OPT_BOOL(0, "rerere-autoupdate",
+                        &options.allow_rerere_autoupdate,
+                        N_("allow rerere to update index  with resolved "
+                           "conflict")),
                OPT_END(),
        };
 
@@ -438,17 +611,168 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
        git_config(rebase_config, &options);
 
+       strbuf_reset(&buf);
+       strbuf_addf(&buf, "%s/applying", apply_dir());
+       if(file_exists(buf.buf))
+               die(_("It looks like 'git am' is in progress. Cannot rebase."));
+
+       if (is_directory(apply_dir())) {
+               options.type = REBASE_AM;
+               options.state_dir = apply_dir();
+       } else if (is_directory(merge_dir())) {
+               strbuf_reset(&buf);
+               strbuf_addf(&buf, "%s/rewritten", merge_dir());
+               if (is_directory(buf.buf)) {
+                       options.type = REBASE_PRESERVE_MERGES;
+                       options.flags |= REBASE_INTERACTIVE_EXPLICIT;
+               } else {
+                       strbuf_reset(&buf);
+                       strbuf_addf(&buf, "%s/interactive", merge_dir());
+                       if(file_exists(buf.buf)) {
+                               options.type = REBASE_INTERACTIVE;
+                               options.flags |= REBASE_INTERACTIVE_EXPLICIT;
+                       } else
+                               options.type = REBASE_MERGE;
+               }
+               options.state_dir = merge_dir();
+       }
+
+       if (options.type != REBASE_UNSPECIFIED)
+               in_progress = 1;
+
+       total_argc = argc;
        argc = parse_options(argc, argv, prefix,
                             builtin_rebase_options,
                             builtin_rebase_usage, 0);
 
+       if (action != NO_ACTION && total_argc != 2) {
+               usage_with_options(builtin_rebase_usage,
+                                  builtin_rebase_options);
+       }
+
        if (argc > 2)
                usage_with_options(builtin_rebase_usage,
                                   builtin_rebase_options);
 
+       if (action != NO_ACTION && !in_progress)
+               die(_("No rebase in progress?"));
+
+       if (action == ACTION_EDIT_TODO && !is_interactive(&options))
+               die(_("The --edit-todo action can only be used during "
+                     "interactive rebase."));
+
+       switch (action) {
+       case ACTION_CONTINUE: {
+               struct object_id head;
+               struct lock_file lock_file = LOCK_INIT;
+               int fd;
+
+               options.action = "continue";
+
+               /* Sanity check */
+               if (get_oid("HEAD", &head))
+                       die(_("Cannot read HEAD"));
+
+               fd = hold_locked_index(&lock_file, 0);
+               if (read_index(the_repository->index) < 0)
+                       die(_("could not read index"));
+               refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL,
+                             NULL);
+               if (0 <= fd)
+                       update_index_if_able(the_repository->index,
+                                            &lock_file);
+               rollback_lock_file(&lock_file);
+
+               if (has_unstaged_changes(1)) {
+                       puts(_("You must edit all merge conflicts and then\n"
+                              "mark them as resolved using git add"));
+                       exit(1);
+               }
+               if (read_basic_state(&options))
+                       exit(1);
+               goto run_rebase;
+       }
+       case ACTION_SKIP: {
+               struct string_list merge_rr = STRING_LIST_INIT_DUP;
+
+               options.action = "skip";
+
+               rerere_clear(&merge_rr);
+               string_list_clear(&merge_rr, 1);
+
+               if (reset_head(NULL, "reset", NULL, 0) < 0)
+                       die(_("could not discard worktree changes"));
+               if (read_basic_state(&options))
+                       exit(1);
+               goto run_rebase;
+       }
+       case ACTION_ABORT: {
+               struct string_list merge_rr = STRING_LIST_INIT_DUP;
+               options.action = "abort";
+
+               rerere_clear(&merge_rr);
+               string_list_clear(&merge_rr, 1);
+
+               if (read_basic_state(&options))
+                       exit(1);
+               if (reset_head(&options.orig_head, "reset",
+                              options.head_name, 0) < 0)
+                       die(_("could not move back to %s"),
+                           oid_to_hex(&options.orig_head));
+               ret = finish_rebase(&options);
+               goto cleanup;
+       }
+       case ACTION_QUIT: {
+               strbuf_reset(&buf);
+               strbuf_addstr(&buf, options.state_dir);
+               ret = !!remove_dir_recursively(&buf, 0);
+               if (ret)
+                       die(_("could not remove '%s'"), options.state_dir);
+               goto cleanup;
+       }
+       case ACTION_EDIT_TODO:
+               options.action = "edit-todo";
+               options.dont_finish_rebase = 1;
+               goto run_rebase;
+       case ACTION_SHOW_CURRENT_PATCH:
+               options.action = "show-current-patch";
+               options.dont_finish_rebase = 1;
+               goto run_rebase;
+       case NO_ACTION:
+               break;
+       default:
+               BUG("action: %d", action);
+       }
+
+       /* Make sure no rebase is in progress */
+       if (in_progress) {
+               const char *last_slash = strrchr(options.state_dir, '/');
+               const char *state_dir_base =
+                       last_slash ? last_slash + 1 : options.state_dir;
+               const char *cmd_live_rebase =
+                       "git rebase (--continue | --abort | --skip)";
+               strbuf_reset(&buf);
+               strbuf_addf(&buf, "rm -fr \"%s\"", options.state_dir);
+               die(_("It seems that there is already a %s directory, and\n"
+                     "I wonder if you are in the middle of another rebase.  "
+                     "If that is the\n"
+                     "case, please try\n\t%s\n"
+                     "If that is not the case, please\n\t%s\n"
+                     "and run me again.  I am stopping in case you still "
+                     "have something\n"
+                     "valuable there.\n"),
+                   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;
+       }
+
        switch (options.type) {
        case REBASE_MERGE:
        case REBASE_INTERACTIVE:
@@ -465,6 +789,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                break;
        }
 
+       if (options.signoff) {
+               if (options.type == REBASE_PRESERVE_MERGES)
+                       die("cannot combine '--signoff' with "
+                           "'--preserve-merges'");
+               strbuf_addstr(&options.git_am_opt, " --signoff");
+               options.flags |= REBASE_FORCE;
+       }
+
        if (!options.root) {
                if (argc < 1)
                        die("TODO: handle @{upstream}");
@@ -503,11 +835,25 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
         * branch_name -- branch/commit being rebased, or
         *                HEAD (already detached)
         * orig_head -- commit object name of tip of the branch before rebasing
-        * head_name -- refs/heads/<that-branch> or "detached HEAD"
+        * head_name -- refs/heads/<that-branch> or NULL (detached HEAD)
         */
-       if (argc > 0)
-                die("TODO: handle switch_to");
-       else {
+       if (argc == 1) {
+               /* Is it "rebase other branchname" or "rebase other commit"? */
+               branch_name = argv[0];
+               options.switch_to = argv[0];
+
+               /* Is it a local branch? */
+               strbuf_reset(&buf);
+               strbuf_addf(&buf, "refs/heads/%s", branch_name);
+               if (!read_ref(buf.buf, &options.orig_head))
+                       options.head_name = xstrdup(buf.buf);
+               /* If not is it a valid ref (branch or commit)? */
+               else if (!get_oid(branch_name, &options.orig_head))
+                       options.head_name = NULL;
+               else
+                       die(_("fatal: no such branch/commit '%s'"),
+                           branch_name);
+       } else if (argc == 0) {
                /* Do not need to switch branches, we are already on it. */
                options.head_name =
                        xstrdup_or_null(resolve_ref_unsafe("HEAD", 0, NULL,
@@ -520,12 +866,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                                branch_name = options.head_name;
 
                } else {
-                       options.head_name = xstrdup("detached HEAD");
+                       free(options.head_name);
+                       options.head_name = NULL;
                        branch_name = "HEAD";
                }
                if (get_oid("HEAD", &options.orig_head))
                        die(_("Could not resolve HEAD to a revision"));
-       }
+       } else
+               BUG("unexpected number of arguments left to parse");
 
        if (read_index(the_repository->index) < 0)
                die(_("could not read index"));
@@ -551,7 +899,40 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
            !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)) {
                int flag;
 
-               if (!(options.flags & REBASE_NO_QUIET))
+               if (!(options.flags & REBASE_FORCE)) {
+                       /* Lazily switch to the target branch if needed... */
+                       if (options.switch_to) {
+                               struct object_id oid;
+
+                               if (get_oid(options.switch_to, &oid) < 0) {
+                                       ret = !!error(_("could not parse '%s'"),
+                                                     options.switch_to);
+                                       goto cleanup;
+                               }
+
+                               strbuf_reset(&buf);
+                               strbuf_addf(&buf, "rebase: checkout %s",
+                                           options.switch_to);
+                               if (reset_head(&oid, "checkout",
+                                              options.head_name, 0) < 0) {
+                                       ret = !!error(_("could not switch to "
+                                                       "%s"),
+                                                     options.switch_to);
+                                       goto cleanup;
+                               }
+                       }
+
+                       if (!(options.flags & REBASE_NO_QUIET))
+                               ; /* be quiet */
+                       else if (!strcmp(branch_name, "HEAD") &&
+                                resolve_ref_unsafe("HEAD", 0, NULL, &flag))
+                               puts(_("HEAD is up to date."));
+                       else
+                               printf(_("Current branch %s is up to date.\n"),
+                                      branch_name);
+                       ret = !!finish_rebase(&options);
+                       goto cleanup;
+               } else if (!(options.flags & REBASE_NO_QUIET))
                        ; /* be quiet */
                else if (!strcmp(branch_name, "HEAD") &&
                         resolve_ref_unsafe("HEAD", 0, NULL, &flag))
@@ -589,6 +970,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                diff_flush(&opts);
        }
 
+       if (is_interactive(&options))
+               goto run_rebase;
+
        /* Detach HEAD and reset the tree */
        if (options.flags & REBASE_NO_QUIET)
                printf(_("First, rewinding head to replay your work on top of "
@@ -608,6 +992,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
        options.revisions = revisions.buf;
 
+run_rebase:
        ret = !!run_specific_rebase(&options);
 
 cleanup: