Merge branch 'js/sequencer-and-root-commits'
authorJunio C Hamano <gitster@pobox.com>
Wed, 30 May 2018 05:04:04 +0000 (14:04 +0900)
committerJunio C Hamano <gitster@pobox.com>
Wed, 30 May 2018 05:04:04 +0000 (14:04 +0900)
The implementation of "git rebase -i --root" has been updated to use
the sequencer machinery more.

* js/sequencer-and-root-commits:
rebase --rebase-merges: root commits can be cousins, too
rebase --rebase-merges: a "merge" into a new root is a fast-forward
sequencer: allow introducing new root commits
rebase -i --root: let the sequencer handle even the initial part
sequencer: learn about the special "fake root commit" handling
sequencer: extract helper to update active_cache_tree

git-rebase--interactive.sh
sequencer.c
sequencer.h
t/t3404-rebase-interactive.sh
t/t3421-rebase-topology-linear.sh
t/t3430-rebase-merges.sh
index cbf44f864828cca964693bea6a701ce882afd6eb..2f4941d0fc95270f0b723eaed8f69d506ee69a27 100644 (file)
@@ -894,6 +894,8 @@ init_revisions_and_shortrevisions () {
        else
                revisions=$onto...$orig_head
                shortrevisions=$shorthead
+               test -z "$squash_onto" ||
+               echo "$squash_onto" >"$state_dir"/squash-onto
        fi
 }
 
@@ -948,7 +950,7 @@ EOF
        die "Could not skip unnecessary pick commands"
 
        checkout_onto
-       if test -z "$rebase_root" && test ! -d "$rewritten"
+       if test ! -d "$rewritten"
        then
                require_clean_work_tree "rebase"
                exec git rebase--helper ${force_rebase:+--no-ff} $allow_empty_message \
index 1ce63261a32a2398c3916fd41f577db58478fe0f..abb6c5dc26c04e21607615326c4d101cd85d5c00 100644 (file)
@@ -123,6 +123,12 @@ 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 path of the file containig the OID of the "squash onto" commit, i.e.
+ * the dummy commit used for `reset [new root]`.
+ */
+static GIT_PATH_FUNC(rebase_path_squash_onto, "rebase-merge/squash-onto")
+
 /*
  * The path of the file listing refs that need to be deleted after the rebase
  * finishes. This is used by the `label` command to record the need for cleanup.
@@ -469,7 +475,8 @@ static int fast_forward_to(const struct object_id *to, const struct object_id *f
        transaction = ref_transaction_begin(&err);
        if (!transaction ||
            ref_transaction_update(transaction, "HEAD",
-                                  to, unborn ? &null_oid : from,
+                                  to, unborn && !is_rebase_i(opts) ?
+                                  &null_oid : from,
                                   0, sb.buf, &err) ||
            ref_transaction_commit(transaction, &err)) {
                ref_transaction_free(transaction);
@@ -561,9 +568,23 @@ static int do_recursive_merge(struct commit *base, struct commit *next,
        return !clean;
 }
 
+static struct object_id *get_cache_tree_oid(void)
+{
+       if (!active_cache_tree)
+               active_cache_tree = cache_tree();
+
+       if (!cache_tree_fully_valid(active_cache_tree))
+               if (cache_tree_update(&the_index, 0)) {
+                       error(_("unable to update cache tree"));
+                       return NULL;
+               }
+
+       return &active_cache_tree->oid;
+}
+
 static int is_index_unchanged(void)
 {
-       struct object_id head_oid;
+       struct object_id head_oid, *cache_tree_oid;
        struct commit *head_commit;
 
        if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL))
@@ -582,15 +603,10 @@ static int is_index_unchanged(void)
        if (parse_commit(head_commit))
                return -1;
 
-       if (!active_cache_tree)
-               active_cache_tree = cache_tree();
-
-       if (!cache_tree_fully_valid(active_cache_tree))
-               if (cache_tree_update(&the_index, 0))
-                       return error(_("unable to update cache tree"));
+       if (!(cache_tree_oid = get_cache_tree_oid()))
+               return -1;
 
-       return !oidcmp(&active_cache_tree->oid,
-                      get_commit_tree_oid(head_commit));
+       return !oidcmp(cache_tree_oid, get_commit_tree_oid(head_commit));
 }
 
 static int write_author_script(const char *message)
@@ -682,6 +698,52 @@ static char *get_author(const char *message)
        return NULL;
 }
 
+/* Read author-script and return an ident line (author <email> timestamp) */
+static const char *read_author_ident(struct strbuf *buf)
+{
+       const char *keys[] = {
+               "GIT_AUTHOR_NAME=", "GIT_AUTHOR_EMAIL=", "GIT_AUTHOR_DATE="
+       };
+       char *in, *out, *eol;
+       int i = 0, len;
+
+       if (strbuf_read_file(buf, rebase_path_author_script(), 256) <= 0)
+               return NULL;
+
+       /* dequote values and construct ident line in-place */
+       for (in = out = buf->buf; i < 3 && in - buf->buf < buf->len; i++) {
+               if (!skip_prefix(in, keys[i], (const char **)&in)) {
+                       warning("could not parse '%s' (looking for '%s'",
+                               rebase_path_author_script(), keys[i]);
+                       return NULL;
+               }
+
+               eol = strchrnul(in, '\n');
+               *eol = '\0';
+               sq_dequote(in);
+               len = strlen(in);
+
+               if (i > 0) /* separate values by spaces */
+                       *(out++) = ' ';
+               if (i == 1) /* email needs to be surrounded by <...> */
+                       *(out++) = '<';
+               memmove(out, in, len);
+               out += len;
+               if (i == 1) /* email needs to be surrounded by <...> */
+                       *(out++) = '>';
+               in = eol + 1;
+       }
+
+       if (i < 3) {
+               warning("could not parse '%s' (looking for '%s')",
+                       rebase_path_author_script(), keys[i]);
+               return NULL;
+       }
+
+       buf->len = out - buf->buf;
+       return buf->buf;
+}
+
 static const char staged_changes_advice[] =
 N_("you have staged changes in your working tree\n"
 "If these changes are meant to be squashed into the previous commit, run:\n"
@@ -701,6 +763,7 @@ N_("you have staged changes in your working tree\n"
 #define AMEND_MSG   (1<<2)
 #define CLEANUP_MSG (1<<3)
 #define VERIFY_MSG  (1<<4)
+#define CREATE_ROOT_COMMIT (1<<5)
 
 /*
  * If we are cherry-pick, and if the merge did not result in
@@ -720,6 +783,40 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts,
        struct child_process cmd = CHILD_PROCESS_INIT;
        const char *value;
 
+       if (flags & CREATE_ROOT_COMMIT) {
+               struct strbuf msg = STRBUF_INIT, script = STRBUF_INIT;
+               const char *author = is_rebase_i(opts) ?
+                       read_author_ident(&script) : NULL;
+               struct object_id root_commit, *cache_tree_oid;
+               int res = 0;
+
+               if (!defmsg)
+                       BUG("root commit without message");
+
+               if (!(cache_tree_oid = get_cache_tree_oid()))
+                       res = -1;
+
+               if (!res)
+                       res = strbuf_read_file(&msg, defmsg, 0);
+
+               if (res <= 0)
+                       res = error_errno(_("could not read '%s'"), defmsg);
+               else
+                       res = commit_tree(msg.buf, msg.len, cache_tree_oid,
+                                         NULL, &root_commit, author,
+                                         opts->gpg_sign);
+
+               strbuf_release(&msg);
+               strbuf_release(&script);
+               if (!res) {
+                       update_ref(NULL, "CHERRY_PICK_HEAD", &root_commit, NULL,
+                                  REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR);
+                       res = update_ref(NULL, "HEAD", &root_commit, NULL, 0,
+                                        UPDATE_REFS_MSG_ON_ERR);
+               }
+               return res < 0 ? error(_("writing root commit")) : 0;
+       }
+
        cmd.git_cmd = 1;
 
        if (is_rebase_i(opts)) {
@@ -1210,7 +1307,8 @@ static int do_commit(const char *msg_file, const char *author,
 {
        int res = 1;
 
-       if (!(flags & EDIT_MSG) && !(flags & VERIFY_MSG)) {
+       if (!(flags & EDIT_MSG) && !(flags & VERIFY_MSG) &&
+           !(flags & CREATE_ROOT_COMMIT)) {
                struct object_id oid;
                struct strbuf sb = STRBUF_INIT;
 
@@ -1363,6 +1461,22 @@ static int is_fixup(enum todo_command command)
        return command == TODO_FIXUP || command == TODO_SQUASH;
 }
 
+/* Does this command create a (non-merge) commit? */
+static int is_pick_or_similar(enum todo_command command)
+{
+       switch (command) {
+       case TODO_PICK:
+       case TODO_REVERT:
+       case TODO_EDIT:
+       case TODO_REWORD:
+       case TODO_FIXUP:
+       case TODO_SQUASH:
+               return 1;
+       default:
+               return 0;
+       }
+}
+
 static int update_squash_messages(enum todo_command command,
                struct commit *commit, struct replay_opts *opts)
 {
@@ -1516,7 +1630,14 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
                        return error(_("your index file is unmerged."));
        } else {
                unborn = get_oid("HEAD", &head);
-               if (unborn)
+               /* Do we want to generate a root commit? */
+               if (is_pick_or_similar(command) && opts->have_squash_onto &&
+                   !oidcmp(&head, &opts->squash_onto)) {
+                       if (is_fixup(command))
+                               return error(_("cannot fixup root commit"));
+                       flags |= CREATE_ROOT_COMMIT;
+                       unborn = 1;
+               } else if (unborn)
                        oidcpy(&head, the_hash_algo->empty_tree);
                if (index_differs_from(unborn ? EMPTY_TREE_SHA1_HEX : "HEAD",
                                       NULL, 0))
@@ -2142,6 +2263,12 @@ static int read_populate_opts(struct replay_opts *opts)
                        }
                }
 
+               if (read_oneliner(&buf, rebase_path_squash_onto(), 0)) {
+                       if (get_oid_hex(buf.buf, &opts->squash_onto) < 0)
+                               return error(_("unusable squash-onto"));
+                       opts->have_squash_onto = 1;
+               }
+
                return 0;
        }
 
@@ -2634,18 +2761,34 @@ static int do_reset(const char *name, int len, struct replay_opts *opts)
        if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
                return -1;
 
-       /* Determine the length of the label */
-       for (i = 0; i < len; i++)
-               if (isspace(name[i]))
-                       len = i;
-
-       strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
-       if (get_oid(ref_name.buf, &oid) &&
-           get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
-               error(_("could not read '%s'"), ref_name.buf);
-               rollback_lock_file(&lock);
-               strbuf_release(&ref_name);
-               return -1;
+       if (len == 10 && !strncmp("[new root]", name, len)) {
+               if (!opts->have_squash_onto) {
+                       const char *hex;
+                       if (commit_tree("", 0, the_hash_algo->empty_tree,
+                                       NULL, &opts->squash_onto,
+                                       NULL, NULL))
+                               return error(_("writing fake root commit"));
+                       opts->have_squash_onto = 1;
+                       hex = oid_to_hex(&opts->squash_onto);
+                       if (write_message(hex, strlen(hex),
+                                         rebase_path_squash_onto(), 0))
+                               return error(_("writing squash-onto"));
+               }
+               oidcpy(&oid, &opts->squash_onto);
+       } else {
+               /* Determine the length of the label */
+               for (i = 0; i < len; i++)
+                       if (isspace(name[i]))
+                               len = i;
+
+               strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+               if (get_oid(ref_name.buf, &oid) &&
+                   get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
+                       error(_("could not read '%s'"), ref_name.buf);
+                       rollback_lock_file(&lock);
+                       strbuf_release(&ref_name);
+                       return -1;
+               }
        }
 
        memset(&unpack_tree_opts, 0, sizeof(unpack_tree_opts));
@@ -2741,6 +2884,18 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
                goto leave_merge;
        }
 
+       if (opts->have_squash_onto &&
+           !oidcmp(&head_commit->object.oid, &opts->squash_onto)) {
+               /*
+                * When the user tells us to "merge" something into a
+                * "[new root]", let's simply fast-forward to the merge head.
+                */
+               rollback_lock_file(&lock);
+               ret = fast_forward_to(&merge_commit->object.oid,
+                                      &head_commit->object.oid, 0, opts);
+               goto leave_merge;
+       }
+
        if (commit) {
                const char *message = get_commit_buffer(commit, NULL);
                const char *body;
@@ -3850,7 +4005,8 @@ static int make_script_with_merges(struct pretty_print_context *pp,
                }
 
                if (!commit)
-                       fprintf(out, "%s onto\n", cmd_reset);
+                       fprintf(out, "%s %s\n", cmd_reset,
+                               rebase_cousins ? "onto" : "[new root]");
                else {
                        const char *to = NULL;
 
index a800cb57558fab14d43cda47c9a69b3c5015e1a8..c5787c6b566bbc89caad1a099f4281fecba01766 100644 (file)
@@ -48,6 +48,10 @@ struct replay_opts {
        struct strbuf current_fixups;
        int current_fixup_count;
 
+       /* placeholder commit for -i --root */
+       struct object_id squash_onto;
+       int have_squash_onto;
+
        /* Only used by REPLAY_NONE */
        struct rev_info *revs;
 };
index 59c766540e5361af0eab19e33d1d61ef650bc72d..c65826ddaced2f4bafeea0e4570ef9018218692c 100755 (executable)
@@ -1204,10 +1204,6 @@ test_expect_success 'drop' '
        test A = $(git cat-file commit HEAD^^ | sed -ne \$p)
 '
 
-cat >expect <<EOF
-Successfully rebased and updated refs/heads/missing-commit.
-EOF
-
 test_expect_success 'rebase -i respects rebase.missingCommitsCheck = ignore' '
        test_config rebase.missingCommitsCheck ignore &&
        rebase_setup_and_clean missing-commit &&
@@ -1215,7 +1211,9 @@ test_expect_success 'rebase -i respects rebase.missingCommitsCheck = ignore' '
        FAKE_LINES="1 2 3 4" \
                git rebase -i --root 2>actual &&
        test D = $(git cat-file commit HEAD | sed -ne \$p) &&
-       test_i18ncmp expect actual
+       test_i18ngrep \
+               "Successfully rebased and updated refs/heads/missing-commit" \
+               actual
 '
 
 cat >expect <<EOF
@@ -1227,15 +1225,24 @@ 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.
 
+Rebasing (1/4)
+Rebasing (2/4)
+Rebasing (3/4)
+Rebasing (4/4)
 Successfully rebased and updated refs/heads/missing-commit.
 EOF
 
+cr_to_nl () {
+       tr '\015' '\012'
+}
+
 test_expect_success 'rebase -i respects rebase.missingCommitsCheck = warn' '
        test_config rebase.missingCommitsCheck warn &&
        rebase_setup_and_clean missing-commit &&
        set_fake_editor &&
        FAKE_LINES="1 2 3 4" \
-               git rebase -i --root 2>actual &&
+               git rebase -i --root 2>actual.2 &&
+       cr_to_nl <actual.2 >actual &&
        test_i18ncmp expect actual &&
        test D = $(git cat-file commit HEAD | sed -ne \$p)
 '
index e7438ad06acedee06ced9995a47e454a2eef3e2e..99b2aac9219da0e9a77e845509ca7b84c65ab88b 100755 (executable)
@@ -328,9 +328,9 @@ test_run_rebase () {
                test_cmp_rev c HEAD
        "
 }
-test_run_rebase failure ''
-test_run_rebase failure -m
-test_run_rebase failure -i
+test_run_rebase success ''
+test_run_rebase success -m
+test_run_rebase success -i
 test_run_rebase failure -p
 
 test_run_rebase () {
index 3d4dfdf7bec7db74eb41bd7606c13629f6abc370..ce6de6f491e5a6bf5f1d767e14af2b19c467330b 100755 (executable)
@@ -241,4 +241,76 @@ test_expect_success 'refuse to merge ancestors of HEAD' '
        test_cmp_rev HEAD $before
 '
 
+test_expect_success 'root commits' '
+       git checkout --orphan unrelated &&
+       (GIT_AUTHOR_NAME="Parsnip" GIT_AUTHOR_EMAIL="root@example.com" \
+        test_commit second-root) &&
+       test_commit third-root &&
+       cat >script-from-scratch <<-\EOF &&
+       pick third-root
+       label first-branch
+       reset [new root]
+       pick second-root
+       merge first-branch # Merge the 3rd root
+       EOF
+       test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+       test_tick &&
+       git rebase -i --force --root -r &&
+       test "Parsnip" = "$(git show -s --format=%an HEAD^)" &&
+       test $(git rev-parse second-root^0) != $(git rev-parse HEAD^) &&
+       test $(git rev-parse second-root:second-root.t) = \
+               $(git rev-parse HEAD^:second-root.t) &&
+       test_cmp_graph HEAD <<-\EOF &&
+       *   Merge the 3rd root
+       |\
+       | * third-root
+       * second-root
+       EOF
+
+       : fast forward if possible &&
+       before="$(git rev-parse --verify HEAD)" &&
+       test_might_fail git config --unset sequence.editor &&
+       test_tick &&
+       git rebase -i --root -r &&
+       test_cmp_rev HEAD $before
+'
+
+test_expect_success 'a "merge" into a root commit is a fast-forward' '
+       head=$(git rev-parse HEAD) &&
+       cat >script-from-scratch <<-EOF &&
+       reset [new root]
+       merge $head
+       EOF
+       test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+       test_tick &&
+       git rebase -i -r HEAD^ &&
+       test_cmp_rev HEAD $head
+'
+
+test_expect_success 'A root commit can be a cousin, treat it that way' '
+       git checkout --orphan khnum &&
+       test_commit yama &&
+       git checkout -b asherah master &&
+       test_commit shamkat &&
+       git merge --allow-unrelated-histories khnum &&
+       test_tick &&
+       git rebase -f -r HEAD^ &&
+       ! test_cmp_rev HEAD^2 khnum &&
+       test_cmp_graph HEAD^.. <<-\EOF &&
+       *   Merge branch '\''khnum'\'' into asherah
+       |\
+       | * yama
+       o shamkat
+       EOF
+       test_tick &&
+       git rebase --rebase-merges=rebase-cousins HEAD^ &&
+       test_cmp_graph HEAD^.. <<-\EOF
+       *   Merge branch '\''khnum'\'' into asherah
+       |\
+       | * yama
+       |/
+       o shamkat
+       EOF
+'
+
 test_done