rebase-helper --make-script: introduce a flag to rebase merges

The sequencer just learned new commands intended to recreate branch structure (similar in spirit to --preserve-merges, but with a substantially less-broken design). Let's allow the rebase--helper to generate todo lists making use of these commands, triggered by the new --rebase-merges option. For a commit topology like this (where the HEAD points to C): - A - B - C \ / D the generated todo list would look like this: # branch D pick 0123 A label branch-point pick 1234 D label D reset branch-point pick 2345 B merge -C 3456 D # C To keep things simple, we first only implement support for merge commits with exactly two parents, leaving support for octopus merges to a later patch series. All merge-rebasing todo lists start with a hard-coded `label onto` line. This makes it convenient to refer later on to the revision onto which everything is rebased, e.g. as starting point for branches other than the very first one. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed Apr 25, 2018 at 14:29 UTC 1644c73c6d4fe35d4d3304b9c03c9586de51626f
3 files changed +349 -2
builtin/rebase--helper.c
+3 -1
@@ -12,7 +12,7 @@ static const char * const builtin_rebase_helper_usage[] = {
12 int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
13 {
14 struct replay_opts opts = REPLAY_OPTS_INIT;
15 - unsigned flags = 0, keep_empty = 0;
15 + unsigned flags = 0, keep_empty = 0, rebase_merges = 0;
16 int abbreviate_commands = 0;
17 enum {
18 CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
@@ -24,6 +24,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
24 OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
25 OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message,
26 N_("allow commits with empty messages")),
27 + OPT_BOOL(0, "rebase-merges", &rebase_merges, N_("rebase merge commits")),
28 OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
29 CONTINUE),
30 OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -57,6 +58,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
58
59 flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
60 flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
61 + flags |= rebase_merges ? TODO_LIST_REBASE_MERGES : 0;
62 flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
63
64 if (command == CONTINUE && argc == 1)
sequencer.c
+345 -1
@@ -25,6 +25,8 @@
25 #include "sigchain.h"
26 #include "unpack-trees.h"
27 #include "worktree.h"
28 +#include "oidmap.h"
29 +#include "oidset.h"
30
31 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
32
@@ -3448,6 +3450,343 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
3450 strbuf_release(&sob);
3451 }
3452
3453 +struct labels_entry {
3454 + struct hashmap_entry entry;
3455 + char label[FLEX_ARRAY];
3456 +};
3457 +
3458 +static int labels_cmp(const void *fndata, const struct labels_entry *a,
3459 + const struct labels_entry *b, const void *key)
3460 +{
3461 + return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
3462 +}
3463 +
3464 +struct string_entry {
3465 + struct oidmap_entry entry;
3466 + char string[FLEX_ARRAY];
3467 +};
3468 +
3469 +struct label_state {
3470 + struct oidmap commit2label;
3471 + struct hashmap labels;
3472 + struct strbuf buf;
3473 +};
3474 +
3475 +static const char *label_oid(struct object_id *oid, const char *label,
3476 + struct label_state *state)
3477 +{
3478 + struct labels_entry *labels_entry;
3479 + struct string_entry *string_entry;
3480 + struct object_id dummy;
3481 + size_t len;
3482 + int i;
3483 +
3484 + string_entry = oidmap_get(&state->commit2label, oid);
3485 + if (string_entry)
3486 + return string_entry->string;
3487 +
3488 + /*
3489 + * For "uninteresting" commits, i.e. commits that are not to be
3490 + * rebased, and which can therefore not be labeled, we use a unique
3491 + * abbreviation of the commit name. This is slightly more complicated
3492 + * than calling find_unique_abbrev() because we also need to make
3493 + * sure that the abbreviation does not conflict with any other
3494 + * label.
3495 + *
3496 + * We disallow "interesting" commits to be labeled by a string that
3497 + * is a valid full-length hash, to ensure that we always can find an
3498 + * abbreviation for any uninteresting commit's names that does not
3499 + * clash with any other label.
3500 + */
3501 + if (!label) {
3502 + char *p;
3503 +
3504 + strbuf_reset(&state->buf);
3505 + strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
3506 + label = p = state->buf.buf;
3507 +
3508 + find_unique_abbrev_r(p, oid, default_abbrev);
3509 +
3510 + /*
3511 + * We may need to extend the abbreviated hash so that there is
3512 + * no conflicting label.
3513 + */
3514 + if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
3515 + size_t i = strlen(p) + 1;
3516 +
3517 + oid_to_hex_r(p, oid);
3518 + for (; i < GIT_SHA1_HEXSZ; i++) {
3519 + char save = p[i];
3520 + p[i] = '\0';
3521 + if (!hashmap_get_from_hash(&state->labels,
3522 + strihash(p), p))
3523 + break;
3524 + p[i] = save;
3525 + }
3526 + }
3527 + } else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
3528 + !get_oid_hex(label, &dummy)) ||
3529 + (len == 1 && *label == '#') ||
3530 + hashmap_get_from_hash(&state->labels,
3531 + strihash(label), label)) {
3532 + /*
3533 + * If the label already exists, or if the label is a valid full
3534 + * OID, or the label is a '#' (which we use as a separator
3535 + * between merge heads and oneline), we append a dash and a
3536 + * number to make it unique.
3537 + */
3538 + struct strbuf *buf = &state->buf;
3539 +
3540 + strbuf_reset(buf);
3541 + strbuf_add(buf, label, len);
3542 +
3543 + for (i = 2; ; i++) {
3544 + strbuf_setlen(buf, len);
3545 + strbuf_addf(buf, "-%d", i);
3546 + if (!hashmap_get_from_hash(&state->labels,
3547 + strihash(buf->buf),
3548 + buf->buf))
3549 + break;
3550 + }
3551 +
3552 + label = buf->buf;
3553 + }
3554 +
3555 + FLEX_ALLOC_STR(labels_entry, label, label);
3556 + hashmap_entry_init(labels_entry, strihash(label));
3557 + hashmap_add(&state->labels, labels_entry);
3558 +
3559 + FLEX_ALLOC_STR(string_entry, string, label);
3560 + oidcpy(&string_entry->entry.oid, oid);
3561 + oidmap_put(&state->commit2label, string_entry);
3562 +
3563 + return string_entry->string;
3564 +}
3565 +
3566 +static int make_script_with_merges(struct pretty_print_context *pp,
3567 + struct rev_info *revs, FILE *out,
3568 + unsigned flags)
3569 +{
3570 + int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
3571 + struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
3572 + struct strbuf label = STRBUF_INIT;
3573 + struct commit_list *commits = NULL, **tail = &commits, *iter;
3574 + struct commit_list *tips = NULL, **tips_tail = &tips;
3575 + struct commit *commit;
3576 + struct oidmap commit2todo = OIDMAP_INIT;
3577 + struct string_entry *entry;
3578 + struct oidset interesting = OIDSET_INIT, child_seen = OIDSET_INIT,
3579 + shown = OIDSET_INIT;
3580 + struct label_state state = { OIDMAP_INIT, { NULL }, STRBUF_INIT };
3581 +
3582 + int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
3583 + const char *cmd_pick = abbr ? "p" : "pick",
3584 + *cmd_label = abbr ? "l" : "label",
3585 + *cmd_reset = abbr ? "t" : "reset",
3586 + *cmd_merge = abbr ? "m" : "merge";
3587 +
3588 + oidmap_init(&commit2todo, 0);
3589 + oidmap_init(&state.commit2label, 0);
3590 + hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
3591 + strbuf_init(&state.buf, 32);
3592 +
3593 + if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
3594 + struct object_id *oid = &revs->cmdline.rev[0].item->oid;
3595 + FLEX_ALLOC_STR(entry, string, "onto");
3596 + oidcpy(&entry->entry.oid, oid);
3597 + oidmap_put(&state.commit2label, entry);
3598 + }
3599 +
3600 + /*
3601 + * First phase:
3602 + * - get onelines for all commits
3603 + * - gather all branch tips (i.e. 2nd or later parents of merges)
3604 + * - label all branch tips
3605 + */
3606 + while ((commit = get_revision(revs))) {
3607 + struct commit_list *to_merge;
3608 + int is_octopus;
3609 + const char *p1, *p2;
3610 + struct object_id *oid;
3611 + int is_empty;
3612 +
3613 + tail = &commit_list_insert(commit, tail)->next;
3614 + oidset_insert(&interesting, &commit->object.oid);
3615 +
3616 + is_empty = is_original_commit_empty(commit);
3617 + if (!is_empty && (commit->object.flags & PATCHSAME))
3618 + continue;
3619 +
3620 + strbuf_reset(&oneline);
3621 + pretty_print_commit(pp, commit, &oneline);
3622 +
3623 + to_merge = commit->parents ? commit->parents->next : NULL;
3624 + if (!to_merge) {
3625 + /* non-merge commit: easy case */
3626 + strbuf_reset(&buf);
3627 + if (!keep_empty && is_empty)
3628 + strbuf_addf(&buf, "%c ", comment_line_char);
3629 + strbuf_addf(&buf, "%s %s %s", cmd_pick,
3630 + oid_to_hex(&commit->object.oid),
3631 + oneline.buf);
3632 +
3633 + FLEX_ALLOC_STR(entry, string, buf.buf);
3634 + oidcpy(&entry->entry.oid, &commit->object.oid);
3635 + oidmap_put(&commit2todo, entry);
3636 +
3637 + continue;
3638 + }
3639 +
3640 + is_octopus = to_merge && to_merge->next;
3641 +
3642 + if (is_octopus)
3643 + BUG("Octopus merges not yet supported");
3644 +
3645 + /* Create a label */
3646 + strbuf_reset(&label);
3647 + if (skip_prefix(oneline.buf, "Merge ", &p1) &&
3648 + (p1 = strchr(p1, '\'')) &&
3649 + (p2 = strchr(++p1, '\'')))
3650 + strbuf_add(&label, p1, p2 - p1);
3651 + else if (skip_prefix(oneline.buf, "Merge pull request ",
3652 + &p1) &&
3653 + (p1 = strstr(p1, " from ")))
3654 + strbuf_addstr(&label, p1 + strlen(" from "));
3655 + else
3656 + strbuf_addbuf(&label, &oneline);
3657 +
3658 + for (p1 = label.buf; *p1; p1++)
3659 + if (isspace(*p1))
3660 + *(char *)p1 = '-';
3661 +
3662 + strbuf_reset(&buf);
3663 + strbuf_addf(&buf, "%s -C %s",
3664 + cmd_merge, oid_to_hex(&commit->object.oid));
3665 +
3666 + /* label the tip of merged branch */
3667 + oid = &to_merge->item->object.oid;
3668 + strbuf_addch(&buf, ' ');
3669 +
3670 + if (!oidset_contains(&interesting, oid))
3671 + strbuf_addstr(&buf, label_oid(oid, NULL, &state));
3672 + else {
3673 + tips_tail = &commit_list_insert(to_merge->item,
3674 + tips_tail)->next;
3675 +
3676 + strbuf_addstr(&buf, label_oid(oid, label.buf, &state));
3677 + }
3678 + strbuf_addf(&buf, " # %s", oneline.buf);
3679 +
3680 + FLEX_ALLOC_STR(entry, string, buf.buf);
3681 + oidcpy(&entry->entry.oid, &commit->object.oid);
3682 + oidmap_put(&commit2todo, entry);
3683 + }
3684 +
3685 + /*
3686 + * Second phase:
3687 + * - label branch points
3688 + * - add HEAD to the branch tips
3689 + */
3690 + for (iter = commits; iter; iter = iter->next) {
3691 + struct commit_list *parent = iter->item->parents;
3692 + for (; parent; parent = parent->next) {
3693 + struct object_id *oid = &parent->item->object.oid;
3694 + if (!oidset_contains(&interesting, oid))
3695 + continue;
3696 + if (!oidset_contains(&child_seen, oid))
3697 + oidset_insert(&child_seen, oid);
3698 + else
3699 + label_oid(oid, "branch-point", &state);
3700 + }
3701 +
3702 + /* Add HEAD as implict "tip of branch" */
3703 + if (!iter->next)
3704 + tips_tail = &commit_list_insert(iter->item,
3705 + tips_tail)->next;
3706 + }
3707 +
3708 + /*
3709 + * Third phase: output the todo list. This is a bit tricky, as we
3710 + * want to avoid jumping back and forth between revisions. To
3711 + * accomplish that goal, we walk backwards from the branch tips,
3712 + * gathering commits not yet shown, reversing the list on the fly,
3713 + * then outputting that list (labeling revisions as needed).
3714 + */
3715 + fprintf(out, "%s onto\n", cmd_label);
3716 + for (iter = tips; iter; iter = iter->next) {
3717 + struct commit_list *list = NULL, *iter2;
3718 +
3719 + commit = iter->item;
3720 + if (oidset_contains(&shown, &commit->object.oid))
3721 + continue;
3722 + entry = oidmap_get(&state.commit2label, &commit->object.oid);
3723 +
3724 + if (entry)
3725 + fprintf(out, "\n# Branch %s\n", entry->string);
3726 + else
3727 + fprintf(out, "\n");
3728 +
3729 + while (oidset_contains(&interesting, &commit->object.oid) &&
3730 + !oidset_contains(&shown, &commit->object.oid)) {
3731 + commit_list_insert(commit, &list);
3732 + if (!commit->parents) {
3733 + commit = NULL;
3734 + break;
3735 + }
3736 + commit = commit->parents->item;
3737 + }
3738 +
3739 + if (!commit)
3740 + fprintf(out, "%s onto\n", cmd_reset);
3741 + else {
3742 + const char *to = NULL;
3743 +
3744 + entry = oidmap_get(&state.commit2label,
3745 + &commit->object.oid);
3746 + if (entry)
3747 + to = entry->string;
3748 +
3749 + if (!to || !strcmp(to, "onto"))
3750 + fprintf(out, "%s onto\n", cmd_reset);
3751 + else {
3752 + strbuf_reset(&oneline);
3753 + pretty_print_commit(pp, commit, &oneline);
3754 + fprintf(out, "%s %s # %s\n",
3755 + cmd_reset, to, oneline.buf);
3756 + }
3757 + }
3758 +
3759 + for (iter2 = list; iter2; iter2 = iter2->next) {
3760 + struct object_id *oid = &iter2->item->object.oid;
3761 + entry = oidmap_get(&commit2todo, oid);
3762 + /* only show if not already upstream */
3763 + if (entry)
3764 + fprintf(out, "%s\n", entry->string);
3765 + entry = oidmap_get(&state.commit2label, oid);
3766 + if (entry)
3767 + fprintf(out, "%s %s\n",
3768 + cmd_label, entry->string);
3769 + oidset_insert(&shown, oid);
3770 + }
3771 +
3772 + free_commit_list(list);
3773 + }
3774 +
3775 + free_commit_list(commits);
3776 + free_commit_list(tips);
3777 +
3778 + strbuf_release(&label);
3779 + strbuf_release(&oneline);
3780 + strbuf_release(&buf);
3781 +
3782 + oidmap_free(&commit2todo, 1);
3783 + oidmap_free(&state.commit2label, 1);
3784 + hashmap_free(&state.labels, 1);
3785 + strbuf_release(&state.buf);
3786 +
3787 + return 0;
3788 +}
3789 +
3790 int sequencer_make_script(FILE *out, int argc, const char **argv,
3791 unsigned flags)
3792 {
@@ -3458,10 +3797,12 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
3797 struct commit *commit;
3798 int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
3799 const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
3800 + int rebase_merges = flags & TODO_LIST_REBASE_MERGES;
3801
3802 init_revisions(&revs, NULL);
3803 revs.verbose_header = 1;
3464 - revs.max_parents = 1;
3804 + if (!rebase_merges)
3805 + revs.max_parents = 1;
3806 revs.cherry_mark = 1;
3807 revs.limited = 1;
3808 revs.reverse = 1;
@@ -3486,6 +3827,9 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
3827 if (prepare_revision_walk(&revs) < 0)
3828 return error(_("make_script: error preparing revisions"));
3829
3830 + if (rebase_merges)
3831 + return make_script_with_merges(&pp, &revs, out, flags);
3832 +
3833 while ((commit = get_revision(&revs))) {
3834 int is_empty = is_original_commit_empty(commit);
3835
sequencer.h
+1
@@ -59,6 +59,7 @@ int sequencer_remove_state(struct replay_opts *opts);
59 #define TODO_LIST_KEEP_EMPTY (1U << 0)
60 #define TODO_LIST_SHORTEN_IDS (1U << 1)
61 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
62 +#define TODO_LIST_REBASE_MERGES (1U << 3)
63 int sequencer_make_script(FILE *out, int argc, const char **argv,
64 unsigned flags);
65