Merge branch 'jc/merge-reduce-parents-early'
authorJunio C Hamano <gitster@pobox.com>
Fri, 27 Apr 2012 20:59:20 +0000 (13:59 -0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 27 Apr 2012 20:59:20 +0000 (13:59 -0700)
Octopus merge strategy did not reduce heads that are recorded in the
final commit correctly.

By Junio C Hamano (4) and MichaƂ Kiedrowicz (1)
* jc/merge-reduce-parents-early:
fmt-merge-msg: discard needless merge parents
builtin/merge.c: reduce parents early
builtin/merge.c: collect other parents early
builtin/merge.c: remove "remoteheads" global variable
merge tests: octopus with redundant parents

builtin/fmt-merge-msg.c
builtin/merge.c
t/t6028-merge-up-to-date.sh
t/t7602-merge-octopus-many.sh
t/t7603-merge-reduce-heads.sh
index 1bc6b8b8c3a4f4ab174841f3c8507d784f237a1f..a517f1794a1c1bcc0939ad8b81d482356c20d2ba 100644 (file)
@@ -55,7 +55,48 @@ static void init_src_data(struct src_data *data)
 static struct string_list srcs = STRING_LIST_INIT_DUP;
 static struct string_list origins = STRING_LIST_INIT_DUP;
 
-static int handle_line(char *line)
+struct merge_parents {
+       int alloc, nr;
+       struct merge_parent {
+               unsigned char given[20];
+               unsigned char commit[20];
+               unsigned char used;
+       } *item;
+};
+
+/*
+ * I know, I know, this is inefficient, but you won't be pulling and merging
+ * hundreds of heads at a time anyway.
+ */
+static struct merge_parent *find_merge_parent(struct merge_parents *table,
+                                             unsigned char *given,
+                                             unsigned char *commit)
+{
+       int i;
+       for (i = 0; i < table->nr; i++) {
+               if (given && hashcmp(table->item[i].given, given))
+                       continue;
+               if (commit && hashcmp(table->item[i].commit, commit))
+                       continue;
+               return &table->item[i];
+       }
+       return NULL;
+}
+
+static void add_merge_parent(struct merge_parents *table,
+                            unsigned char *given,
+                            unsigned char *commit)
+{
+       if (table->nr && find_merge_parent(table, given, commit))
+               return;
+       ALLOC_GROW(table->item, table->nr + 1, table->alloc);
+       hashcpy(table->item[table->nr].given, given);
+       hashcpy(table->item[table->nr].commit, commit);
+       table->item[table->nr].used = 0;
+       table->nr++;
+}
+
+static int handle_line(char *line, struct merge_parents *merge_parents)
 {
        int i, len = strlen(line);
        struct origin_data *origin_data;
@@ -63,6 +104,7 @@ static int handle_line(char *line)
        struct src_data *src_data;
        struct string_list_item *item;
        int pulling_head = 0;
+       unsigned char sha1[20];
 
        if (len < 43 || line[40] != '\t')
                return 1;
@@ -73,14 +115,15 @@ static int handle_line(char *line)
        if (line[41] != '\t')
                return 2;
 
-       line[40] = 0;
-       origin_data = xcalloc(1, sizeof(struct origin_data));
-       i = get_sha1(line, origin_data->sha1);
-       line[40] = '\t';
-       if (i) {
-               free(origin_data);
+       i = get_sha1_hex(line, sha1);
+       if (i)
                return 3;
-       }
+
+       if (!find_merge_parent(merge_parents, sha1, NULL))
+               return 0; /* subsumed by other parents */
+
+       origin_data = xcalloc(1, sizeof(struct origin_data));
+       hashcpy(origin_data->sha1, sha1);
 
        if (line[len - 1] == '\n')
                line[len - 1] = 0;
@@ -472,6 +515,67 @@ static void fmt_merge_msg_sigs(struct strbuf *out)
        strbuf_release(&tagbuf);
 }
 
+static void find_merge_parents(struct merge_parents *result,
+                              struct strbuf *in, unsigned char *head)
+{
+       struct commit_list *parents, *next;
+       struct commit *head_commit;
+       int pos = 0, i, j;
+
+       parents = NULL;
+       while (pos < in->len) {
+               int len;
+               char *p = in->buf + pos;
+               char *newline = strchr(p, '\n');
+               unsigned char sha1[20];
+               struct commit *parent;
+               struct object *obj;
+
+               len = newline ? newline - p : strlen(p);
+               pos += len + !!newline;
+
+               if (len < 43 ||
+                   get_sha1_hex(p, sha1) ||
+                   p[40] != '\t' ||
+                   p[41] != '\t')
+                       continue; /* skip not-for-merge */
+               /*
+                * Do not use get_merge_parent() here; we do not have
+                * "name" here and we do not want to contaminate its
+                * util field yet.
+                */
+               obj = parse_object(sha1);
+               parent = (struct commit *)peel_to_type(NULL, 0, obj, OBJ_COMMIT);
+               if (!parent)
+                       continue;
+               commit_list_insert(parent, &parents);
+               add_merge_parent(result, obj->sha1, parent->object.sha1);
+       }
+       head_commit = lookup_commit(head);
+       if (head_commit)
+               commit_list_insert(head_commit, &parents);
+       parents = reduce_heads(parents);
+
+       while (parents) {
+               for (i = 0; i < result->nr; i++)
+                       if (!hashcmp(result->item[i].commit,
+                                    parents->item->object.sha1))
+                               result->item[i].used = 1;
+               next = parents->next;
+               free(parents);
+               parents = next;
+       }
+
+       for (i = j = 0; i < result->nr; i++) {
+               if (result->item[i].used) {
+                       if (i != j)
+                               result->item[j] = result->item[i];
+                       j++;
+               }
+       }
+       result->nr = j;
+}
+
 int fmt_merge_msg(struct strbuf *in, struct strbuf *out,
                  struct fmt_merge_msg_opts *opts)
 {
@@ -479,6 +583,9 @@ int fmt_merge_msg(struct strbuf *in, struct strbuf *out,
        unsigned char head_sha1[20];
        const char *current_branch;
        void *current_branch_to_free;
+       struct merge_parents merge_parents;
+
+       memset(&merge_parents, 0, sizeof(merge_parents));
 
        /* get current branch */
        current_branch = current_branch_to_free =
@@ -488,6 +595,8 @@ int fmt_merge_msg(struct strbuf *in, struct strbuf *out,
        if (!prefixcmp(current_branch, "refs/heads/"))
                current_branch += 11;
 
+       find_merge_parents(&merge_parents, in, head_sha1);
+
        /* get a line */
        while (pos < in->len) {
                int len;
@@ -498,7 +607,7 @@ int fmt_merge_msg(struct strbuf *in, struct strbuf *out,
                pos += len + !!newline;
                i++;
                p[len] = 0;
-               if (handle_line(p))
+               if (handle_line(p, &merge_parents))
                        die ("Error in line %d: %.*s", i, len, p);
        }
 
@@ -529,6 +638,7 @@ int fmt_merge_msg(struct strbuf *in, struct strbuf *out,
 
        strbuf_complete_line(out);
        free(current_branch_to_free);
+       free(merge_parents.item);
        return 0;
 }
 
index 08e01e8a60d7d7f9c60e9e7a3a2f8b4d66c12ce3..470fc57c5d6ceabedbd866a5db35d3123a764af0 100644 (file)
@@ -52,7 +52,6 @@ static int fast_forward_only, option_edit = -1;
 static int allow_trivial = 1, have_message;
 static int overwrite_ignore = 1;
 static struct strbuf merge_msg = STRBUF_INIT;
-static struct commit_list *remoteheads;
 static struct strategy **use_strategies;
 static size_t use_strategies_nr, use_strategies_alloc;
 static const char **xopts;
@@ -318,7 +317,7 @@ static void finish_up_to_date(const char *msg)
        drop_save();
 }
 
-static void squash_message(struct commit *commit)
+static void squash_message(struct commit *commit, struct commit_list *remoteheads)
 {
        struct rev_info rev;
        struct strbuf out = STRBUF_INIT;
@@ -366,6 +365,7 @@ static void squash_message(struct commit *commit)
 }
 
 static void finish(struct commit *head_commit,
+                  struct commit_list *remoteheads,
                   const unsigned char *new_head, const char *msg)
 {
        struct strbuf reflog_message = STRBUF_INIT;
@@ -380,7 +380,7 @@ static void finish(struct commit *head_commit,
                        getenv("GIT_REFLOG_ACTION"), msg);
        }
        if (squash) {
-               squash_message(head_commit);
+               squash_message(head_commit, remoteheads);
        } else {
                if (verbosity >= 0 && !merge_msg.len)
                        printf(_("No merge message -- not updating HEAD\n"));
@@ -683,6 +683,7 @@ int try_merge_command(const char *strategy, size_t xopts_nr,
 }
 
 static int try_merge_strategy(const char *strategy, struct commit_list *common,
+                             struct commit_list *remoteheads,
                              struct commit *head, const char *head_arg)
 {
        int index_fd;
@@ -876,14 +877,14 @@ static void read_merge_msg(struct strbuf *msg)
                die_errno(_("Could not read from '%s'"), filename);
 }
 
-static void write_merge_state(void);
-static void abort_commit(const char *err_msg)
+static void write_merge_state(struct commit_list *);
+static void abort_commit(struct commit_list *remoteheads, const char *err_msg)
 {
        if (err_msg)
                error("%s", err_msg);
        fprintf(stderr,
                _("Not committing merge; use 'git commit' to complete the merge.\n"));
-       write_merge_state();
+       write_merge_state(remoteheads);
        exit(1);
 }
 
@@ -894,7 +895,7 @@ N_("Please enter a commit message to explain why this merge is necessary,\n"
    "Lines starting with '#' will be ignored, and an empty message aborts\n"
    "the commit.\n");
 
-static void prepare_to_commit(void)
+static void prepare_to_commit(struct commit_list *remoteheads)
 {
        struct strbuf msg = STRBUF_INIT;
        const char *comment = _(merge_editor_comment);
@@ -907,18 +908,18 @@ static void prepare_to_commit(void)
                 git_path("MERGE_MSG"), "merge", NULL, NULL);
        if (0 < option_edit) {
                if (launch_editor(git_path("MERGE_MSG"), NULL, NULL))
-                       abort_commit(NULL);
+                       abort_commit(remoteheads, NULL);
        }
        read_merge_msg(&msg);
        stripspace(&msg, 0 < option_edit);
        if (!msg.len)
-               abort_commit(_("Empty commit message."));
+               abort_commit(remoteheads, _("Empty commit message."));
        strbuf_release(&merge_msg);
        strbuf_addbuf(&merge_msg, &msg);
        strbuf_release(&msg);
 }
 
-static int merge_trivial(struct commit *head)
+static int merge_trivial(struct commit *head, struct commit_list *remoteheads)
 {
        unsigned char result_tree[20], result_commit[20];
        struct commit_list *parent = xmalloc(sizeof(*parent));
@@ -929,45 +930,37 @@ static int merge_trivial(struct commit *head)
        parent->next = xmalloc(sizeof(*parent->next));
        parent->next->item = remoteheads->item;
        parent->next->next = NULL;
-       prepare_to_commit();
+       prepare_to_commit(remoteheads);
        if (commit_tree(&merge_msg, result_tree, parent, result_commit, NULL,
                        sign_commit))
                die(_("failed to write commit object"));
-       finish(head, result_commit, "In-index merge");
+       finish(head, remoteheads, result_commit, "In-index merge");
        drop_save();
        return 0;
 }
 
 static int finish_automerge(struct commit *head,
+                           int head_subsumed,
                            struct commit_list *common,
+                           struct commit_list *remoteheads,
                            unsigned char *result_tree,
                            const char *wt_strategy)
 {
-       struct commit_list *parents = NULL, *j;
+       struct commit_list *parents = NULL;
        struct strbuf buf = STRBUF_INIT;
        unsigned char result_commit[20];
 
        free_commit_list(common);
-       if (allow_fast_forward) {
-               parents = remoteheads;
+       parents = remoteheads;
+       if (!head_subsumed || !allow_fast_forward)
                commit_list_insert(head, &parents);
-               parents = reduce_heads(parents);
-       } else {
-               struct commit_list **pptr = &parents;
-
-               pptr = &commit_list_insert(head,
-                               pptr)->next;
-               for (j = remoteheads; j; j = j->next)
-                       pptr = &commit_list_insert(j->item, pptr)->next;
-       }
        strbuf_addch(&merge_msg, '\n');
-       prepare_to_commit();
-       free_commit_list(remoteheads);
+       prepare_to_commit(remoteheads);
        if (commit_tree(&merge_msg, result_tree, parents, result_commit,
                        NULL, sign_commit))
                die(_("failed to write commit object"));
        strbuf_addf(&buf, "Merge made by the '%s' strategy.", wt_strategy);
-       finish(head, result_commit, buf.buf);
+       finish(head, remoteheads, result_commit, buf.buf);
        strbuf_release(&buf);
        drop_save();
        return 0;
@@ -1072,7 +1065,7 @@ static int setup_with_upstream(const char ***argv)
        return i;
 }
 
-static void write_merge_state(void)
+static void write_merge_state(struct commit_list *remoteheads)
 {
        const char *filename;
        int fd;
@@ -1137,6 +1130,39 @@ static int default_edit_option(void)
                st_stdin.st_mode == st_stdout.st_mode);
 }
 
+static struct commit_list *collect_parents(struct commit *head_commit,
+                                          int *head_subsumed,
+                                          int argc, const char **argv)
+{
+       int i;
+       struct commit_list *remoteheads = NULL, *parents, *next;
+       struct commit_list **remotes = &remoteheads;
+
+       if (head_commit)
+               remotes = &commit_list_insert(head_commit, remotes)->next;
+       for (i = 0; i < argc; i++) {
+               struct commit *commit = get_merge_parent(argv[i]);
+               if (!commit)
+                       die(_("%s - not something we can merge"), argv[i]);
+               remotes = &commit_list_insert(commit, remotes)->next;
+       }
+       *remotes = NULL;
+
+       parents = reduce_heads(remoteheads);
+
+       *head_subsumed = 1; /* we will flip this to 0 when we find it */
+       for (remoteheads = NULL, remotes = &remoteheads;
+            parents;
+            parents = next) {
+               struct commit *commit = parents->item;
+               next = parents->next;
+               if (commit == head_commit)
+                       *head_subsumed = 0;
+               else
+                       remotes = &commit_list_insert(commit, remotes)->next;
+       }
+       return remoteheads;
+}
 
 int cmd_merge(int argc, const char **argv, const char *prefix)
 {
@@ -1146,11 +1172,11 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
        struct commit *head_commit;
        struct strbuf buf = STRBUF_INIT;
        const char *head_arg;
-       int flag, i, ret = 0;
+       int flag, i, ret = 0, head_subsumed;
        int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0;
        struct commit_list *common = NULL;
        const char *best_strategy = NULL, *wt_strategy = NULL;
-       struct commit_list **remotes = &remoteheads;
+       struct commit_list *remoteheads, *p;
        void *branch_to_free;
 
        if (argc == 2 && !strcmp(argv[1], "-h"))
@@ -1255,6 +1281,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                head_arg = argv[1];
                argv += 2;
                argc -= 2;
+               remoteheads = collect_parents(head_commit, &head_subsumed, argc, argv);
        } else if (!head_commit) {
                struct commit *remote_head;
                /*
@@ -1270,7 +1297,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                if (!allow_fast_forward)
                        die(_("Non-fast-forward commit does not make sense into "
                            "an empty head"));
-               remote_head = get_merge_parent(argv[0]);
+               remoteheads = collect_parents(head_commit, &head_subsumed, argc, argv);
+               remote_head = remoteheads->item;
                if (!remote_head)
                        die(_("%s - not something we can merge"), argv[0]);
                read_empty(remote_head->object.sha1, 0);
@@ -1288,8 +1316,9 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                 * the standard merge summary message to be appended
                 * to the given message.
                 */
-               for (i = 0; i < argc; i++)
-                       merge_name(argv[i], &merge_names);
+               remoteheads = collect_parents(head_commit, &head_subsumed, argc, argv);
+               for (p = remoteheads; p; p = p->next)
+                       merge_name(merge_remote_util(p->item)->name, &merge_names);
 
                if (!have_message || shortlog_len) {
                        struct fmt_merge_msg_opts opts;
@@ -1308,19 +1337,16 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                        builtin_merge_options);
 
        strbuf_addstr(&buf, "merge");
-       for (i = 0; i < argc; i++)
-               strbuf_addf(&buf, " %s", argv[i]);
+       for (p = remoteheads; p; p = p->next)
+               strbuf_addf(&buf, " %s", merge_remote_util(p->item)->name);
        setenv("GIT_REFLOG_ACTION", buf.buf, 0);
        strbuf_reset(&buf);
 
-       for (i = 0; i < argc; i++) {
-               struct commit *commit = get_merge_parent(argv[i]);
-               if (!commit)
-                       die(_("%s - not something we can merge"), argv[i]);
-               remotes = &commit_list_insert(commit, remotes)->next;
+       for (p = remoteheads; p; p = p->next) {
+               struct commit *commit = p->item;
                strbuf_addf(&buf, "GITHEAD_%s",
                            sha1_to_hex(commit->object.sha1));
-               setenv(buf.buf, argv[i], 1);
+               setenv(buf.buf, merge_remote_util(commit)->name, 1);
                strbuf_reset(&buf);
                if (!fast_forward_only &&
                    merge_remote_util(commit) &&
@@ -1333,7 +1359,9 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                option_edit = default_edit_option();
 
        if (!use_strategies) {
-               if (!remoteheads->next)
+               if (!remoteheads)
+                       ; /* already up-to-date */
+               else if (!remoteheads->next)
                        add_strategies(pull_twohead, DEFAULT_TWOHEAD);
                else
                        add_strategies(pull_octopus, DEFAULT_OCTOPUS);
@@ -1346,7 +1374,9 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                        allow_trivial = 0;
        }
 
-       if (!remoteheads->next)
+       if (!remoteheads)
+               ; /* already up-to-date */
+       else if (!remoteheads->next)
                common = get_merge_bases(head_commit, remoteheads->item, 1);
        else {
                struct commit_list *list = remoteheads;
@@ -1358,10 +1388,11 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
        update_ref("updating ORIG_HEAD", "ORIG_HEAD", head_commit->object.sha1,
                   NULL, 0, DIE_ON_ERR);
 
-       if (!common)
+       if (remoteheads && !common)
                ; /* No common ancestors found. We need a real merge. */
-       else if (!remoteheads->next && !common->next &&
-                       common->item == remoteheads->item) {
+       else if (!remoteheads ||
+                (!remoteheads->next && !common->next &&
+                 common->item == remoteheads->item)) {
                /*
                 * If head can reach all the merge then we are up to date.
                 * but first the most common case of merging one remote.
@@ -1399,7 +1430,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                        goto done;
                }
 
-               finish(head_commit, commit->object.sha1, msg.buf);
+               finish(head_commit, remoteheads, commit->object.sha1, msg.buf);
                drop_save();
                goto done;
        } else if (!remoteheads->next && common->next)
@@ -1421,7 +1452,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                        if (!read_tree_trivial(common->item->object.sha1,
                                               head_commit->object.sha1,
                                               remoteheads->item->object.sha1)) {
-                               ret = merge_trivial(head_commit);
+                               ret = merge_trivial(head_commit, remoteheads);
                                goto done;
                        }
                        printf(_("Nope.\n"));
@@ -1492,7 +1523,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                wt_strategy = use_strategies[i]->name;
 
                ret = try_merge_strategy(use_strategies[i]->name,
-                                        common, head_commit, head_arg);
+                                        common, remoteheads,
+                                        head_commit, head_arg);
                if (!option_commit && !ret) {
                        merge_was_ok = 1;
                        /*
@@ -1534,8 +1566,9 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
         * auto resolved the merge cleanly.
         */
        if (automerge_was_ok) {
-               ret = finish_automerge(head_commit, common, result_tree,
-                                      wt_strategy);
+               ret = finish_automerge(head_commit, head_subsumed,
+                                      common, remoteheads,
+                                      result_tree, wt_strategy);
                goto done;
        }
 
@@ -1560,13 +1593,14 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                restore_state(head_commit->object.sha1, stash);
                printf(_("Using the %s to prepare resolving by hand.\n"),
                        best_strategy);
-               try_merge_strategy(best_strategy, common, head_commit, head_arg);
+               try_merge_strategy(best_strategy, common, remoteheads,
+                                  head_commit, head_arg);
        }
 
        if (squash)
-               finish(head_commit, NULL, NULL);
+               finish(head_commit, remoteheads, NULL, NULL);
        else
-               write_merge_state();
+               write_merge_state(remoteheads);
 
        if (merge_was_ok)
                fprintf(stderr, _("Automatic merge went well; "
index a91644e3b2ac3490cfe49d5e67c9736197cd56a1..c518e9c30ccbb12c504d25aa965f8941a3989d8d 100755 (executable)
@@ -16,7 +16,12 @@ test_expect_success setup '
        test_tick &&
        git commit -m second &&
        git tag c1 &&
-       git branch test
+       git branch test &&
+       echo third >file &&
+       git add file &&
+       test_tick &&
+       git commit -m third &&
+       git tag c2
 '
 
 test_expect_success 'merge -s recursive up-to-date' '
@@ -74,4 +79,14 @@ test_expect_success 'merge -s subtree up-to-date' '
 
 '
 
+test_expect_success 'merge fast-forward octopus' '
+
+       git reset --hard c0 &&
+       test_tick &&
+       git merge c1 c2
+       expect=$(git rev-parse c2) &&
+       current=$(git rev-parse HEAD) &&
+       test "$expect" = "$current"
+'
+
 test_done
index bce0bd37cb6006f29b9c8c532863947c44509486..3b72c097ee0c9538d4e8003639ab7c44126c49d6 100755 (executable)
@@ -70,16 +70,14 @@ test_expect_success 'merge output uses pretty names' '
 '
 
 cat >expected <<\EOF
-Already up-to-date with c4
-Trying simple merge with c5
-Merge made by the 'octopus' strategy.
+Merge made by the 'recursive' strategy.
  c5.c |    1 +
  1 file changed, 1 insertion(+)
  create mode 100644 c5.c
 EOF
 
-test_expect_success 'merge up-to-date output uses pretty names' '
-       git merge c4 c5 >actual &&
+test_expect_success 'merge reduces irrelevant remote heads' '
+       GIT_MERGE_VERBOSITY=0 git merge c4 c5 >actual &&
        test_i18ncmp expected actual
 '
 
index 7e17eb490d4b1f4ac275a7033562aaa6693181ab..98948955ae507ef007dc541f7e981d06bf7b49ab 100755 (executable)
@@ -57,7 +57,36 @@ test_expect_success 'merge c1 with c2, c3, c4, c5' '
        test -f c2.c &&
        test -f c3.c &&
        test -f c4.c &&
-       test -f c5.c
+       test -f c5.c &&
+       git show --format=%s -s >actual &&
+       ! grep c1 actual &&
+       grep c2 actual &&
+       grep c3 actual &&
+       ! grep c4 actual &&
+       grep c5 actual
+'
+
+test_expect_success 'pull c2, c3, c4, c5 into c1' '
+       git reset --hard c1 &&
+       git pull . c2 c3 c4 c5 &&
+       test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" &&
+       test "$(git rev-parse c1)" = "$(git rev-parse HEAD^1)" &&
+       test "$(git rev-parse c2)" = "$(git rev-parse HEAD^2)" &&
+       test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" &&
+       test "$(git rev-parse c5)" = "$(git rev-parse HEAD^4)" &&
+       git diff --exit-code &&
+       test -f c0.c &&
+       test -f c1.c &&
+       test -f c2.c &&
+       test -f c3.c &&
+       test -f c4.c &&
+       test -f c5.c &&
+       git show --format=%s -s >actual &&
+       ! grep c1 actual &&
+       grep c2 actual &&
+       grep c3 actual &&
+       ! grep c4 actual &&
+       grep c5 actual
 '
 
 test_expect_success 'setup' '
@@ -113,4 +142,23 @@ test_expect_success 'verify merge result' '
        test $(git rev-parse HEAD^1) = $(git rev-parse E2) &&
        test $(git rev-parse HEAD^2) = $(git rev-parse I2)
 '
+
+test_expect_success 'fast-forward to redundant refs' '
+       git reset --hard c0 &&
+       git merge c4 c5
+'
+
+test_expect_success 'verify merge result' '
+       test $(git rev-parse HEAD) = $(git rev-parse c5)
+'
+
+test_expect_success 'merge up-to-date redundant refs' '
+       git reset --hard c5 &&
+       git merge c0 c4
+'
+
+test_expect_success 'verify merge result' '
+       test $(git rev-parse HEAD) = $(git rev-parse c5)
+'
+
 test_done