sequencer: learn about the special "fake root commit" handling

When an interactive rebase wants to recreate a root commit, it - first creates a new, empty root commit, - checks it out, - converts the next `pick` command so that it amends the empty root commit Introduce support in the sequencer to handle such an empty root commit, by looking for the file <GIT_DIR>/rebase-merge/squash-onto; if it exists and contains a commit name, the sequencer will compare the HEAD to said root commit, and if identical, a new root commit will be created. While converting scripted code into proper, portable C, we also do away with the old "amend with an empty commit message, then cherry-pick without committing, then amend again" dance and replace it with code that uses the internal API properly to do exactly what we want: create a new root commit. To keep the implementation simple, we always spawn `git commit` to create new root commits. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed May 4, 2018 at 01:01 UTC d87d48b2e033e8a8d39afbe7cb5eea331bbe47a6
2 files changed +125 -3
sequencer.c
+121 -3
@@ -125,6 +125,12 @@ static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
125 static GIT_PATH_FUNC(rebase_path_rewritten_pending,
126 "rebase-merge/rewritten-pending")
127
128 +/*
129 + * The path of the file containig the OID of the "squash onto" commit, i.e.
130 + * the dummy commit used for `reset [new root]`.
131 + */
132 +static GIT_PATH_FUNC(rebase_path_squash_onto, "rebase-merge/squash-onto")
133 +
134 /*
135 * The path of the file listing refs that need to be deleted after the rebase
136 * finishes. This is used by the `label` command to record the need for cleanup.
@@ -470,7 +476,8 @@ static int fast_forward_to(const struct object_id *to, const struct object_id *f
476 transaction = ref_transaction_begin(&err);
477 if (!transaction ||
478 ref_transaction_update(transaction, "HEAD",
473 - to, unborn ? &null_oid : from,
479 + to, unborn && !is_rebase_i(opts) ?
480 + &null_oid : from,
481 0, sb.buf, &err) ||
482 ref_transaction_commit(transaction, &err)) {
483 ref_transaction_free(transaction);
@@ -692,6 +699,52 @@ static char *get_author(const char *message)
699 return NULL;
700 }
701
702 +/* Read author-script and return an ident line (author <email> timestamp) */
703 +static const char *read_author_ident(struct strbuf *buf)
704 +{
705 + const char *keys[] = {
706 + "GIT_AUTHOR_NAME=", "GIT_AUTHOR_EMAIL=", "GIT_AUTHOR_DATE="
707 + };
708 + char *in, *out, *eol;
709 + int i = 0, len;
710 +
711 + if (strbuf_read_file(buf, rebase_path_author_script(), 256) <= 0)
712 + return NULL;
713 +
714 + /* dequote values and construct ident line in-place */
715 + for (in = out = buf->buf; i < 3 && in - buf->buf < buf->len; i++) {
716 + if (!skip_prefix(in, keys[i], (const char **)&in)) {
717 + warning("could not parse '%s' (looking for '%s'",
718 + rebase_path_author_script(), keys[i]);
719 + return NULL;
720 + }
721 +
722 + eol = strchrnul(in, '\n');
723 + *eol = '\0';
724 + sq_dequote(in);
725 + len = strlen(in);
726 +
727 + if (i > 0) /* separate values by spaces */
728 + *(out++) = ' ';
729 + if (i == 1) /* email needs to be surrounded by <...> */
730 + *(out++) = '<';
731 + memmove(out, in, len);
732 + out += len;
733 + if (i == 1) /* email needs to be surrounded by <...> */
734 + *(out++) = '>';
735 + in = eol + 1;
736 + }
737 +
738 + if (i < 3) {
739 + warning("could not parse '%s' (looking for '%s')",
740 + rebase_path_author_script(), keys[i]);
741 + return NULL;
742 + }
743 +
744 + buf->len = out - buf->buf;
745 + return buf->buf;
746 +}
747 +
748 static const char staged_changes_advice[] =
749 N_("you have staged changes in your working tree\n"
750 "If these changes are meant to be squashed into the previous commit, run:\n"
@@ -711,6 +764,7 @@ N_("you have staged changes in your working tree\n"
764 #define AMEND_MSG (1<<2)
765 #define CLEANUP_MSG (1<<3)
766 #define VERIFY_MSG (1<<4)
767 +#define CREATE_ROOT_COMMIT (1<<5)
768
769 /*
770 * If we are cherry-pick, and if the merge did not result in
@@ -730,6 +784,40 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts,
784 struct child_process cmd = CHILD_PROCESS_INIT;
785 const char *value;
786
787 + if (flags & CREATE_ROOT_COMMIT) {
788 + struct strbuf msg = STRBUF_INIT, script = STRBUF_INIT;
789 + const char *author = is_rebase_i(opts) ?
790 + read_author_ident(&script) : NULL;
791 + struct object_id root_commit, *cache_tree_oid;
792 + int res = 0;
793 +
794 + if (!defmsg)
795 + BUG("root commit without message");
796 +
797 + if (!(cache_tree_oid = get_cache_tree_oid()))
798 + res = -1;
799 +
800 + if (!res)
801 + res = strbuf_read_file(&msg, defmsg, 0);
802 +
803 + if (res <= 0)
804 + res = error_errno(_("could not read '%s'"), defmsg);
805 + else
806 + res = commit_tree(msg.buf, msg.len, cache_tree_oid,
807 + NULL, &root_commit, author,
808 + opts->gpg_sign);
809 +
810 + strbuf_release(&msg);
811 + strbuf_release(&script);
812 + if (!res) {
813 + update_ref(NULL, "CHERRY_PICK_HEAD", &root_commit, NULL,
814 + REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR);
815 + res = update_ref(NULL, "HEAD", &root_commit, NULL, 0,
816 + UPDATE_REFS_MSG_ON_ERR);
817 + }
818 + return res < 0 ? error(_("writing root commit")) : 0;
819 + }
820 +
821 cmd.git_cmd = 1;
822
823 if (is_rebase_i(opts)) {
@@ -1216,7 +1304,8 @@ static int do_commit(const char *msg_file, const char *author,
1304 {
1305 int res = 1;
1306
1219 - if (!(flags & EDIT_MSG) && !(flags & VERIFY_MSG)) {
1307 + if (!(flags & EDIT_MSG) && !(flags & VERIFY_MSG) &&
1308 + !(flags & CREATE_ROOT_COMMIT)) {
1309 struct object_id oid;
1310 struct strbuf sb = STRBUF_INIT;
1311
@@ -1369,6 +1458,22 @@ static int is_fixup(enum todo_command command)
1458 return command == TODO_FIXUP || command == TODO_SQUASH;
1459 }
1460
1461 +/* Does this command create a (non-merge) commit? */
1462 +static int is_pick_or_similar(enum todo_command command)
1463 +{
1464 + switch (command) {
1465 + case TODO_PICK:
1466 + case TODO_REVERT:
1467 + case TODO_EDIT:
1468 + case TODO_REWORD:
1469 + case TODO_FIXUP:
1470 + case TODO_SQUASH:
1471 + return 1;
1472 + default:
1473 + return 0;
1474 + }
1475 +}
1476 +
1477 static int update_squash_messages(enum todo_command command,
1478 struct commit *commit, struct replay_opts *opts)
1479 {
@@ -1523,7 +1628,14 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
1628 return error(_("your index file is unmerged."));
1629 } else {
1630 unborn = get_oid("HEAD", &head);
1526 - if (unborn)
1631 + /* Do we want to generate a root commit? */
1632 + if (is_pick_or_similar(command) && opts->have_squash_onto &&
1633 + !oidcmp(&head, &opts->squash_onto)) {
1634 + if (is_fixup(command))
1635 + return error(_("cannot fixup root commit"));
1636 + flags |= CREATE_ROOT_COMMIT;
1637 + unborn = 1;
1638 + } else if (unborn)
1639 oidcpy(&head, the_hash_algo->empty_tree);
1640 if (index_differs_from(unborn ? EMPTY_TREE_SHA1_HEX : "HEAD",
1641 NULL, 0))
@@ -2136,6 +2248,12 @@ static int read_populate_opts(struct replay_opts *opts)
2248 read_strategy_opts(opts, &buf);
2249 strbuf_release(&buf);
2250
2251 + if (read_oneliner(&buf, rebase_path_squash_onto(), 0)) {
2252 + if (get_oid_hex(buf.buf, &opts->squash_onto) < 0)
2253 + return error(_("unusable squash-onto"));
2254 + opts->have_squash_onto = 1;
2255 + }
2256 +
2257 return 0;
2258 }
2259
sequencer.h
+4
@@ -44,6 +44,10 @@ struct replay_opts {
44 char **xopts;
45 size_t xopts_nr, xopts_alloc;
46
47 + /* placeholder commit for -i --root */
48 + struct object_id squash_onto;
49 + int have_squash_onto;
50 +
51 /* Only used by REPLAY_NONE */
52 struct rev_info *revs;
53 };