Merge branch 'pk/rebase-in-c-3-acts'
authorJunio C Hamano <gitster@pobox.com>
Fri, 2 Nov 2018 02:04:54 +0000 (11:04 +0900)
committerJunio C Hamano <gitster@pobox.com>
Fri, 2 Nov 2018 02:04:54 +0000 (11:04 +0900)
Rewrite "git rebase" in C.

* pk/rebase-in-c-3-acts:
builtin rebase: stop if `git am` is in progress
builtin rebase: actions require a rebase in progress
builtin rebase: support --edit-todo and --show-current-patch
builtin rebase: support --quit
builtin rebase: support --abort
builtin rebase: support --skip
builtin rebase: support --continue

builtin/rebase.c
strbuf.c
strbuf.h
index 71367c8530e77035fe89ddfee8fcef0d77eaced0..13084082776d6a72da0d1fe44cd2ad8163e07cb4 100644 (file)
@@ -22,6 +22,7 @@
 #include "wt-status.h"
 #include "revision.h"
 #include "commit-reach.h"
+#include "rerere.h"
 
 static char const * const builtin_rebase_usage[] = {
        N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] "
@@ -92,6 +93,7 @@ struct rebase_options {
                REBASE_INTERACTIVE_EXPLICIT = 1<<4,
        } flags;
        struct strbuf git_am_opt;
+       const char *action;
 };
 
 static int is_interactive(struct rebase_options *opts)
@@ -116,6 +118,62 @@ 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;
+
+       strbuf_release(&buf);
+
+       return 0;
+}
+
 static int finish_rebase(struct rebase_options *opts)
 {
        struct strbuf dir = STRBUF_INIT;
@@ -169,12 +227,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, "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 ?
@@ -190,6 +249,7 @@ static int run_specific_rebase(struct rebase_options *opts)
                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 : "");
 
        switch (opts->type) {
        case REBASE_AM:
@@ -401,12 +461,21 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                .git_am_opt = STRBUF_INIT,
        };
        const char *branch_name;
-       int ret, flags, in_progress = 0;
+       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;
        struct option builtin_rebase_options[] = {
                OPT_STRING(0, "onto", &options.onto_name,
                           N_("revision"),
@@ -428,6 +497,20 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                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),
                OPT_END(),
        };
 
@@ -457,6 +540,11 @@ 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();
@@ -481,14 +569,110 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
        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, '/');
@@ -720,6 +904,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
        options.revisions = revisions.buf;
 
+run_rebase:
        ret = !!run_specific_rebase(&options);
 
 cleanup:
index 64041c3c249b158478ecc6db7257519bc3fc5c19..f6a6cf78b9426abfa73a2053c17326fd645645e7 100644 (file)
--- a/strbuf.c
+++ b/strbuf.c
@@ -120,6 +120,15 @@ void strbuf_trim_trailing_dir_sep(struct strbuf *sb)
        sb->buf[sb->len] = '\0';
 }
 
+void strbuf_trim_trailing_newline(struct strbuf *sb)
+{
+       if (sb->len > 0 && sb->buf[sb->len - 1] == '\n') {
+               if (--sb->len > 0 && sb->buf[sb->len - 1] == '\r')
+                       --sb->len;
+               sb->buf[sb->len] = '\0';
+       }
+}
+
 void strbuf_ltrim(struct strbuf *sb)
 {
        char *b = sb->buf;
index 9981f782b2573c1a4360879e03884ec753999aec..fc40873b65124fc98a2e06150b7c8043f05ed4ab 100644 (file)
--- a/strbuf.h
+++ b/strbuf.h
@@ -190,6 +190,9 @@ void strbuf_ltrim(struct strbuf *sb);
 /* Strip trailing directory separators */
 void strbuf_trim_trailing_dir_sep(struct strbuf *sb);
 
+/* Strip trailing LF or CR/LF */
+void strbuf_trim_trailing_newline(struct strbuf *sb);
+
 /**
  * Replace the contents of the strbuf with a reencoded form.  Returns -1
  * on error, 0 on success.