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`, `bud` and `reset` commands
to label commits and to reset to a labeled commits. This patch adds the
`merge` command, with the following syntax:

        merge <commit> <rev> <oneline>

The <commit> parameter in this instance is the *original* merge commit,
whose author and message will be used for the to-be-created merge
commit.

The <rev> parameter refers to the (possibly rewritten) revision to
merge. Let's see an example of a todo list:

        label onto

        # Branch abc
        bud
        pick deadbeef Hello, world!
        label abc

        bud
        pick cafecafe And now for something completely different
        merge baaabaaa abc Merge the branch 'abc' into master

To support creating *new* merges, i.e. without copying the commit
message from an existing commit, use the special value `-` as <commit>
parameter (in which case the text after the <rev> parameter is used as
commit message):

        merge - abc This will be the actual commit message of the merge

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 this patch
series, support for merges using strategies other than the recursive
merge is left for future contributions.

The design of the `merge` command as introduced by this patch only
supports creating new merge commits with exactly two parents, i.e. it
adds no support for octopus merges.

We will introduce support for octopus merges in a later commit.

Signed-off-by: Johannes Schindelin <johannes.schinde...@gmx.de>
---
 git-rebase--interactive.sh |   1 +
 sequencer.c                | 146 +++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 143 insertions(+), 4 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 3d2cd19d65a..5bf1ea3781f 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -165,6 +165,7 @@ d, drop = remove commit
 l, label = label current HEAD with a name
 t, reset = reset HEAD to a label
 b, bud = reset HEAD to the revision labeled 'onto'
+m, merge = create a merge commit using a given commit's message
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index 91cc55a002f..567cfcbbe8b 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -779,6 +779,7 @@ enum todo_command {
        TODO_LABEL,
        TODO_RESET,
        TODO_BUD,
+       TODO_MERGE,
        /* commands that do nothing but are counted for reporting progress */
        TODO_NOOP,
        TODO_DROP,
@@ -800,6 +801,7 @@ static struct {
        { 'l', "label" },
        { 't', "reset" },
        { 'b', "bud" },
+       { 'm', "merge" },
        { 0,   "noop" },
        { 'd', "drop" },
        { 0,   NULL }
@@ -1304,14 +1306,20 @@ static int parse_insn_line(struct todo_item *item, 
const char *bol, char *eol)
        }
 
        end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
+       item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
+       item->arg_len = (int)(eol - item->arg);
+
        saved = *end_of_object_name;
+       if (item->command == TODO_MERGE && *bol == '-' &&
+           bol + 1 == end_of_object_name) {
+               item->commit = NULL;
+               return 0;
+       }
+
        *end_of_object_name = '\0';
        status = get_oid(bol, &commit_oid);
        *end_of_object_name = saved;
 
-       item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
-       item->arg_len = (int)(eol - item->arg);
-
        if (status < 0)
                return -1;
 
@@ -2069,6 +2077,132 @@ static int do_reset(const char *name, int len)
        return ret;
 }
 
+static int do_merge(struct commit *commit, const char *arg, int arg_len,
+                   struct replay_opts *opts)
+{
+       int merge_arg_len;
+       struct strbuf ref_name = STRBUF_INIT;
+       struct commit *head_commit, *merge_commit, *i;
+       struct commit_list *common, *j, *reversed = NULL;
+       struct merge_options o;
+       int ret;
+       static struct lock_file lock;
+
+       for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
+               if (isspace(arg[merge_arg_len]))
+                       break;
+
+       if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+               return -1;
+
+       if (commit) {
+               const char *message = get_commit_buffer(commit, NULL);
+               const char *body;
+               int len;
+
+               if (!message) {
+                       rollback_lock_file(&lock);
+                       return error(_("could not get commit message of '%s'"),
+                                    oid_to_hex(&commit->object.oid));
+               }
+               write_author_script(message);
+               find_commit_subject(message, &body);
+               len = strlen(body);
+               if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
+                       error_errno(_("Could not write '%s'"),
+                                   git_path_merge_msg());
+                       unuse_commit_buffer(commit, message);
+                       rollback_lock_file(&lock);
+                       return -1;
+               }
+               unuse_commit_buffer(commit, message);
+       } else {
+               const char *p = arg + merge_arg_len;
+               struct strbuf buf = STRBUF_INIT;
+               int len;
+
+               strbuf_addf(&buf, "author %s", git_author_info(0));
+               write_author_script(buf.buf);
+               strbuf_reset(&buf);
+
+               p += strspn(p, " \t");
+               if (*p)
+                       len = strlen(p);
+               else {
+                       strbuf_addf(&buf, "Merge branch '%.*s'",
+                                   merge_arg_len, arg);
+                       p = buf.buf;
+                       len = buf.len;
+               }
+
+               if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
+                       error_errno(_("Could not write '%s'"),
+                                   git_path_merge_msg());
+                       strbuf_release(&buf);
+                       rollback_lock_file(&lock);
+                       return -1;
+               }
+               strbuf_release(&buf);
+       }
+
+       head_commit = lookup_commit_reference_by_name("HEAD");
+       if (!head_commit) {
+               rollback_lock_file(&lock);
+               return error(_("Cannot merge without a current revision"));
+       }
+
+       strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
+       merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+       if (!merge_commit) {
+               /* fall back to non-rewritten ref or commit */
+               strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
+               merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+       }
+       if (!merge_commit) {
+               error(_("could not resolve '%s'"), ref_name.buf);
+               strbuf_release(&ref_name);
+               rollback_lock_file(&lock);
+               return -1;
+       }
+       write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
+                     git_path_merge_head(), 0);
+       write_message("no-ff", 5, git_path_merge_mode(), 0);
+
+       common = get_merge_bases(head_commit, merge_commit);
+       for (j = common; j; j = j->next)
+               commit_list_insert(j->item, &reversed);
+       free_commit_list(common);
+
+       read_cache();
+       init_merge_options(&o);
+       o.branch1 = "HEAD";
+       o.branch2 = ref_name.buf;
+       o.buffer_output = 2;
+
+       ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
+       if (ret <= 0)
+               fputs(o.obuf.buf, stdout);
+       strbuf_release(&o.obuf);
+       if (ret < 0) {
+               strbuf_release(&ref_name);
+               rollback_lock_file(&lock);
+               return error(_("conflicts while merging '%.*s'"),
+                            merge_arg_len, arg);
+       }
+
+       if (active_cache_changed &&
+           write_locked_index(&the_index, &lock, COMMIT_LOCK)) {
+               strbuf_release(&ref_name);
+               return error(_("merge: Unable to write new index file"));
+       }
+       rollback_lock_file(&lock);
+
+       ret = run_git_commit(git_path_merge_msg(), opts, 0);
+       strbuf_release(&ref_name);
+
+       return ret;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
        int i = todo_list->current;
@@ -2258,6 +2392,9 @@ static int pick_commits(struct todo_list *todo_list, 
struct replay_opts *opts)
                        res = do_reset(item->arg, item->arg_len);
                else if (item->command == TODO_BUD)
                        res = do_reset("onto", 4);
+               else if (item->command == TODO_MERGE)
+                       res = do_merge(item->commit,
+                                      item->arg, item->arg_len, opts);
                else if (!is_noop(item->command))
                        return error(_("unknown command %d"), item->command);
 
@@ -2757,7 +2894,8 @@ int transform_todos(unsigned flags)
                                          oid_to_hex(&item->commit->object.oid);
 
                        strbuf_addf(&buf, " %s", oid);
-               }
+               } else if (item->command == TODO_MERGE)
+                       strbuf_addstr(&buf, " -");
                /* add all the rest */
                if (!item->arg_len)
                        strbuf_addch(&buf, '\n');
-- 
2.15.1.windows.2.1430.ga56c4f9e2a9


Reply via email to