sequencer: introduce the `merge` command

This patch is part of the effort to reimplement `--preserve-merges` with a substantially improved design, a design that has been developed in the Git for Windows project to maintain the dozens of Windows-specific patch series on top of upstream Git. The previous patch implemented the `label` and `reset` commands to label commits and to reset to labeled commits. This patch adds the `merge` command, with the following syntax: merge [-C <commit>] <rev> # <oneline> The <commit> parameter in this instance is the *original* merge commit, whose author and message will be used for the merge commit that is about to be created. The <rev> parameter refers to the (possibly rewritten) revision to merge. Let's see an example of a todo list (the initial `label onto` command is an auto-generated convenience so that the label `onto` can be used to refer to the revision onto which we rebase): label onto # Branch abc reset onto pick deadbeef Hello, world! label abc reset onto pick cafecafe And now for something completely different merge -C baaabaaa abc # Merge the branch 'abc' into master To edit the merge commit's message (a "reword" for merges, if you will), use `-c` (lower-case) instead of `-C`; this convention was borrowed from `git commit` that also supports `-c` and `-C` with similar meanings. To create *new* merges, i.e. without copying the commit message from an existing commit, simply omit the `-C <commit>` parameter (which will open an editor for the merge message): merge abc This comes in handy when splitting a branch into two or more branches. Note: this patch only adds support for recursive merges, to keep things simple. Support for octopus merges will be added later in a separate patch series, support for merges using strategies other than the recursive merge is left for the future. 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:28 UTC 4c68e7ddb59457efe9d3a141dc600feda86cbe67
2 files changed +204
git-rebase--interactive.sh
+4
@@ -164,6 +164,10 @@ x, exec <commit> = run command (the rest of the line) using shell
164 d, drop <commit> = remove commit
165 l, label <label> = label current HEAD with a name
166 t, reset <label> = reset HEAD to a label
167 +m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
168 +. create a merge commit using the original merge commit's
169 +. message (or the oneline, if no original merge commit was
170 +. specified). Use -c <commit> to reword the commit message.
171
172 These lines can be re-ordered; they are executed from top to bottom.
173 " | git stripspace --comment-lines >>"$todo"
sequencer.c
+200
@@ -1307,6 +1307,7 @@ enum todo_command {
1307 TODO_EXEC,
1308 TODO_LABEL,
1309 TODO_RESET,
1310 + TODO_MERGE,
1311 /* commands that do nothing but are counted for reporting progress */
1312 TODO_NOOP,
1313 TODO_DROP,
@@ -1327,6 +1328,7 @@ static struct {
1328 { 'x', "exec" },
1329 { 'l', "label" },
1330 { 't', "reset" },
1331 + { 'm', "merge" },
1332 { 0, "noop" },
1333 { 'd', "drop" },
1334 { 0, NULL }
@@ -1754,9 +1756,14 @@ static int read_and_refresh_cache(struct replay_opts *opts)
1756 return 0;
1757 }
1758
1759 +enum todo_item_flags {
1760 + TODO_EDIT_MERGE_MSG = 1
1761 +};
1762 +
1763 struct todo_item {
1764 enum todo_command command;
1765 struct commit *commit;
1766 + unsigned int flags;
1767 const char *arg;
1768 int arg_len;
1769 size_t offset_in_buf;
@@ -1791,6 +1798,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
1798 char *end_of_object_name;
1799 int i, saved, status, padding;
1800
1801 + item->flags = 0;
1802 +
1803 /* left-trim */
1804 bol += strspn(bol, " \t");
1805
@@ -1840,6 +1849,21 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
1849 return 0;
1850 }
1851
1852 + if (item->command == TODO_MERGE) {
1853 + if (skip_prefix(bol, "-C", &bol))
1854 + bol += strspn(bol, " \t");
1855 + else if (skip_prefix(bol, "-c", &bol)) {
1856 + bol += strspn(bol, " \t");
1857 + item->flags |= TODO_EDIT_MERGE_MSG;
1858 + } else {
1859 + item->flags |= TODO_EDIT_MERGE_MSG;
1860 + item->commit = NULL;
1861 + item->arg = bol;
1862 + item->arg_len = (int)(eol - bol);
1863 + return 0;
1864 + }
1865 + }
1866 +
1867 end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
1868 saved = *end_of_object_name;
1869 *end_of_object_name = '\0';
@@ -2654,6 +2678,158 @@ static int do_reset(const char *name, int len, struct replay_opts *opts)
2678 return ret;
2679 }
2680
2681 +static int do_merge(struct commit *commit, const char *arg, int arg_len,
2682 + int flags, struct replay_opts *opts)
2683 +{
2684 + int run_commit_flags = (flags & TODO_EDIT_MERGE_MSG) ?
2685 + EDIT_MSG | VERIFY_MSG : 0;
2686 + struct strbuf ref_name = STRBUF_INIT;
2687 + struct commit *head_commit, *merge_commit, *i;
2688 + struct commit_list *bases, *j, *reversed = NULL;
2689 + struct merge_options o;
2690 + int merge_arg_len, oneline_offset, ret;
2691 + static struct lock_file lock;
2692 + const char *p;
2693 +
2694 + if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0) {
2695 + ret = -1;
2696 + goto leave_merge;
2697 + }
2698 +
2699 + head_commit = lookup_commit_reference_by_name("HEAD");
2700 + if (!head_commit) {
2701 + ret = error(_("cannot merge without a current revision"));
2702 + goto leave_merge;
2703 + }
2704 +
2705 + oneline_offset = arg_len;
2706 + merge_arg_len = strcspn(arg, " \t\n");
2707 + p = arg + merge_arg_len;
2708 + p += strspn(p, " \t\n");
2709 + if (*p == '#' && (!p[1] || isspace(p[1]))) {
2710 + p += 1 + strspn(p + 1, " \t\n");
2711 + oneline_offset = p - arg;
2712 + } else if (p - arg < arg_len)
2713 + BUG("octopus merges are not supported yet: '%s'", p);
2714 +
2715 + strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
2716 + merge_commit = lookup_commit_reference_by_name(ref_name.buf);
2717 + if (!merge_commit) {
2718 + /* fall back to non-rewritten ref or commit */
2719 + strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
2720 + merge_commit = lookup_commit_reference_by_name(ref_name.buf);
2721 + }
2722 +
2723 + if (!merge_commit) {
2724 + ret = error(_("could not resolve '%s'"), ref_name.buf);
2725 + goto leave_merge;
2726 + }
2727 +
2728 + if (commit) {
2729 + const char *message = get_commit_buffer(commit, NULL);
2730 + const char *body;
2731 + int len;
2732 +
2733 + if (!message) {
2734 + ret = error(_("could not get commit message of '%s'"),
2735 + oid_to_hex(&commit->object.oid));
2736 + goto leave_merge;
2737 + }
2738 + write_author_script(message);
2739 + find_commit_subject(message, &body);
2740 + len = strlen(body);
2741 + ret = write_message(body, len, git_path_merge_msg(), 0);
2742 + unuse_commit_buffer(commit, message);
2743 + if (ret) {
2744 + error_errno(_("could not write '%s'"),
2745 + git_path_merge_msg());
2746 + goto leave_merge;
2747 + }
2748 + } else {
2749 + struct strbuf buf = STRBUF_INIT;
2750 + int len;
2751 +
2752 + strbuf_addf(&buf, "author %s", git_author_info(0));
2753 + write_author_script(buf.buf);
2754 + strbuf_reset(&buf);
2755 +
2756 + if (oneline_offset < arg_len) {
2757 + p = arg + oneline_offset;
2758 + len = arg_len - oneline_offset;
2759 + } else {
2760 + strbuf_addf(&buf, "Merge branch '%.*s'",
2761 + merge_arg_len, arg);
2762 + p = buf.buf;
2763 + len = buf.len;
2764 + }
2765 +
2766 + ret = write_message(p, len, git_path_merge_msg(), 0);
2767 + strbuf_release(&buf);
2768 + if (ret) {
2769 + error_errno(_("could not write '%s'"),
2770 + git_path_merge_msg());
2771 + goto leave_merge;
2772 + }
2773 + }
2774 +
2775 + write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
2776 + git_path_merge_head(), 0);
2777 + write_message("no-ff", 5, git_path_merge_mode(), 0);
2778 +
2779 + bases = get_merge_bases(head_commit, merge_commit);
2780 + for (j = bases; j; j = j->next)
2781 + commit_list_insert(j->item, &reversed);
2782 + free_commit_list(bases);
2783 +
2784 + read_cache();
2785 + init_merge_options(&o);
2786 + o.branch1 = "HEAD";
2787 + o.branch2 = ref_name.buf;
2788 + o.buffer_output = 2;
2789 +
2790 + ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
2791 + if (ret <= 0)
2792 + fputs(o.obuf.buf, stdout);
2793 + strbuf_release(&o.obuf);
2794 + if (ret < 0) {
2795 + error(_("could not even attempt to merge '%.*s'"),
2796 + merge_arg_len, arg);
2797 + goto leave_merge;
2798 + }
2799 + /*
2800 + * The return value of merge_recursive() is 1 on clean, and 0 on
2801 + * unclean merge.
2802 + *
2803 + * Let's reverse that, so that do_merge() returns 0 upon success and
2804 + * 1 upon failed merge (keeping the return value -1 for the cases where
2805 + * we will want to reschedule the `merge` command).
2806 + */
2807 + ret = !ret;
2808 +
2809 + if (active_cache_changed &&
2810 + write_locked_index(&the_index, &lock, COMMIT_LOCK)) {
2811 + ret = error(_("merge: Unable to write new index file"));
2812 + goto leave_merge;
2813 + }
2814 +
2815 + rollback_lock_file(&lock);
2816 + if (ret)
2817 + rerere(opts->allow_rerere_auto);
2818 + else
2819 + /*
2820 + * In case of problems, we now want to return a positive
2821 + * value (a negative one would indicate that the `merge`
2822 + * command needs to be rescheduled).
2823 + */
2824 + ret = !!run_git_commit(git_path_merge_msg(), opts,
2825 + run_commit_flags);
2826 +
2827 +leave_merge:
2828 + strbuf_release(&ref_name);
2829 + rollback_lock_file(&lock);
2830 + return ret;
2831 +}
2832 +
2833 static int is_final_fixup(struct todo_list *todo_list)
2834 {
2835 int i = todo_list->current;
@@ -2860,6 +3036,17 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
3036 } else if (item->command == TODO_RESET) {
3037 if ((res = do_reset(item->arg, item->arg_len, opts)))
3038 reschedule = 1;
3039 + } else if (item->command == TODO_MERGE) {
3040 + if ((res = do_merge(item->commit,
3041 + item->arg, item->arg_len,
3042 + item->flags, opts)) < 0)
3043 + reschedule = 1;
3044 + else if (res > 0)
3045 + /* failed with merge conflicts */
3046 + return error_with_patch(item->commit,
3047 + item->arg,
3048 + item->arg_len, opts,
3049 + res, 0);
3050 } else if (!is_noop(item->command))
3051 return error(_("unknown command %d"), item->command);
3052
@@ -2871,6 +3058,11 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
3058 todo_list->current--;
3059 if (save_todo(todo_list, opts))
3060 return -1;
3061 + if (item->commit)
3062 + return error_with_patch(item->commit,
3063 + item->arg,
3064 + item->arg_len, opts,
3065 + res, 0);
3066 }
3067
3068 todo_list->current++;
@@ -3356,8 +3548,16 @@ int transform_todos(unsigned flags)
3548 short_commit_name(item->commit) :
3549 oid_to_hex(&item->commit->object.oid);
3550
3551 + if (item->command == TODO_MERGE) {
3552 + if (item->flags & TODO_EDIT_MERGE_MSG)
3553 + strbuf_addstr(&buf, " -c");
3554 + else
3555 + strbuf_addstr(&buf, " -C");
3556 + }
3557 +
3558 strbuf_addf(&buf, " %s", oid);
3559 }
3560 +
3561 /* add all the rest */
3562 if (!item->arg_len)
3563 strbuf_addch(&buf, '\n');