From ba97aea1659e249a3a58ecc5f583ee2056a90ad8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 May 2018 01:01:15 +0200 Subject: [PATCH 1/6] sequencer: extract helper to update active_cache_tree This patch extracts the code from is_index_unchanged() to initialize or update the index' cache tree (i.e. a tree object reflecting the current index' top-level tree). The new helper will be used in the upcoming code to support `git rebase -i --root` via the sequencer. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- sequencer.c | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/sequencer.c b/sequencer.c index e2f8394284..90c8218aa9 100644 --- a/sequencer.c +++ b/sequencer.c @@ -562,9 +562,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)) @@ -583,15 +597,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_oid = get_cache_tree_oid())) + return -1; - if (!cache_tree_fully_valid(active_cache_tree)) - if (cache_tree_update(&the_index, 0)) - return error(_("unable to update cache tree")); - - return !oidcmp(&active_cache_tree->oid, - &head_commit->tree->object.oid); + return !oidcmp(cache_tree_oid, &head_commit->tree->object.oid); } static int write_author_script(const char *message) From d87d48b2e033e8a8d39afbe7cb5eea331bbe47a6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 May 2018 01:01:17 +0200 Subject: [PATCH 2/6] 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 /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 Signed-off-by: Junio C Hamano --- sequencer.c | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++-- sequencer.h | 4 ++ 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/sequencer.c b/sequencer.c index 90c8218aa9..8eddda681d 100644 --- a/sequencer.c +++ b/sequencer.c @@ -125,6 +125,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. @@ -470,7 +476,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); @@ -692,6 +699,52 @@ static char *get_author(const char *message) return NULL; } +/* Read author-script and return an ident line (author 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" @@ -711,6 +764,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 @@ -730,6 +784,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)) { @@ -1216,7 +1304,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; @@ -1369,6 +1458,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) { @@ -1523,7 +1628,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)) @@ -2136,6 +2248,12 @@ static int read_populate_opts(struct replay_opts *opts) read_strategy_opts(opts, &buf); strbuf_release(&buf); + 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; } diff --git a/sequencer.h b/sequencer.h index d9570d92b1..4b2717881f 100644 --- a/sequencer.h +++ b/sequencer.h @@ -44,6 +44,10 @@ struct replay_opts { char **xopts; size_t xopts_nr, xopts_alloc; + /* placeholder commit for -i --root */ + struct object_id squash_onto; + int have_squash_onto; + /* Only used by REPLAY_NONE */ struct rev_info *revs; }; From 21d0764c823bdca78b99dae562ca9e5c2a121738 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 May 2018 01:01:18 +0200 Subject: [PATCH 3/6] rebase -i --root: let the sequencer handle even the initial part In this developer's earlier attempt to accelerate interactive rebases by converting large parts from Unix shell script into portable, performant C, the --root handling was specifically excluded (to simplify the task a little bit; it still took over a year to get that reduced set of patches into Git proper). This patch ties up that loose end: now only --preserve-merges uses the slow Unix shell script implementation to perform the interactive rebase. As the rebase--helper reports progress to stderr (unlike the scripted interactive rebase, which reports it to stdout, of all places), we have to adjust a couple of tests that did not expect that for `git rebase -i --root`. This patch fixes -- at long last! -- the really old bug reported in 6a6bc5bdc4d (add tests for rebasing root, 2013-06-06) that rebasing with --root *always* rewrote the root commit, even if there were no changes. The bug still persists in --preserve-merges mode, of course, but that mode will be deprecated as soon as the new --rebase-merges mode stabilizes, anyway. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- git-rebase--interactive.sh | 4 +++- t/t3404-rebase-interactive.sh | 19 +++++++++++++------ t/t3421-rebase-topology-linear.sh | 6 +++--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index cbf44f8648..2f4941d0fc 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -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 \ diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 59c766540e..c65826ddac 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1204,10 +1204,6 @@ test_expect_success 'drop' ' test A = $(git cat-file commit HEAD^^ | sed -ne \$p) ' -cat >expect <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 <actual && + git rebase -i --root 2>actual.2 && + cr_to_nl actual && test_i18ncmp expect actual && test D = $(git cat-file commit HEAD | sed -ne \$p) ' diff --git a/t/t3421-rebase-topology-linear.sh b/t/t3421-rebase-topology-linear.sh index e7438ad06a..99b2aac921 100755 --- a/t/t3421-rebase-topology-linear.sh +++ b/t/t3421-rebase-topology-linear.sh @@ -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 () { From ebddf393964120628f60f6f5e435cde87cc209c6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 May 2018 01:01:23 +0200 Subject: [PATCH 4/6] sequencer: allow introducing new root commits In the context of the new --rebase-merges mode, which was designed specifically to allow for changing the existing branch topology liberally, a user may want to extract commits into a completely fresh branch that starts with a newly-created root commit. This is now possible by inserting the command `reset [new root]` before `pick`ing the commit that wants to become a root commit. Example: reset [new root] pick 012345 a commit that is about to become a root commit pick 234567 this commit will have the previous one as parent This does not conflict with other uses of the `reset` command because `[new root]` is not (part of) a valid ref name: both the opening bracket as well as the space are illegal in ref names. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- sequencer.c | 38 +++++++++++++++++++++++++++----------- t/t3430-rebase-merges.sh | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/sequencer.c b/sequencer.c index 8eddda681d..a7832399b1 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2747,18 +2747,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; + 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; + 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)); diff --git a/t/t3430-rebase-merges.sh b/t/t3430-rebase-merges.sh index 3d4dfdf7be..35260862fc 100755 --- a/t/t3430-rebase-merges.sh +++ b/t/t3430-rebase-merges.sh @@ -241,4 +241,38 @@ 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_done From 9c85a1c29c5ac26c94aa6eda8da9bbfbc6751785 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 May 2018 01:01:28 +0200 Subject: [PATCH 5/6] rebase --rebase-merges: a "merge" into a new root is a fast-forward When a user provides a todo list containing something like reset [new root] merge my-branch let's do the same as if pulling into an orphan branch: simply fast-forward. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- sequencer.c | 12 ++++++++++++ t/t3430-rebase-merges.sh | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/sequencer.c b/sequencer.c index a7832399b1..65a8c49378 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2870,6 +2870,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; diff --git a/t/t3430-rebase-merges.sh b/t/t3430-rebase-merges.sh index 35260862fc..5543f1d5a3 100755 --- a/t/t3430-rebase-merges.sh +++ b/t/t3430-rebase-merges.sh @@ -275,4 +275,17 @@ test_expect_success 'root commits' ' 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_done From 8fa6eea0ffd271f0564048dc939987e9640efca5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 4 May 2018 01:01:29 +0200 Subject: [PATCH 6/6] rebase --rebase-merges: root commits can be cousins, too Reported by Wink Saville: when rebasing with no-rebase-cousins, we will want to refrain from rebasing all of them, even when they are root commits. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- sequencer.c | 3 ++- t/t3430-rebase-merges.sh | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index 65a8c49378..01e561bc20 100644 --- a/sequencer.c +++ b/sequencer.c @@ -3903,7 +3903,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; diff --git a/t/t3430-rebase-merges.sh b/t/t3430-rebase-merges.sh index 5543f1d5a3..ce6de6f491 100755 --- a/t/t3430-rebase-merges.sh +++ b/t/t3430-rebase-merges.sh @@ -287,5 +287,30 @@ test_expect_success 'a "merge" into a root commit is a fast-forward' ' 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