diff options
-rw-r--r-- | builtin/fmt-merge-msg.c | 128 | ||||
-rw-r--r-- | builtin/merge.c | 142 | ||||
-rwxr-xr-x | t/t6028-merge-up-to-date.sh | 17 | ||||
-rwxr-xr-x | t/t7602-merge-octopus-many.sh | 10 | ||||
-rwxr-xr-x | t/t7603-merge-reduce-heads.sh | 50 |
5 files changed, 276 insertions, 71 deletions
diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index c81a7fef26..fc083e3c5c 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -53,7 +53,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; @@ -61,6 +102,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; @@ -71,14 +113,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; @@ -366,6 +409,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) { @@ -373,6 +477,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 = @@ -382,6 +489,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; @@ -392,7 +501,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); } @@ -423,6 +532,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; } diff --git a/builtin/merge.c b/builtin/merge.c index 08e01e8a60..470fc57c5d 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -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; " diff --git a/t/t6028-merge-up-to-date.sh b/t/t6028-merge-up-to-date.sh index a91644e3b2..c518e9c30c 100755 --- a/t/t6028-merge-up-to-date.sh +++ b/t/t6028-merge-up-to-date.sh @@ -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 diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh index 5783ebf3ab..7117b57ccc 100755 --- a/t/t7602-merge-octopus-many.sh +++ b/t/t7602-merge-octopus-many.sh @@ -70,17 +70,15 @@ 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_cmp actual expected +test_expect_success 'merge reduces irrelevant remote heads' ' + GIT_MERGE_VERBOSITY=0 git merge c4 c5 >actual && + test_cmp expected actual ' cat >expected <<\EOF diff --git a/t/t7603-merge-reduce-heads.sh b/t/t7603-merge-reduce-heads.sh index 7e17eb490d..98948955ae 100755 --- a/t/t7603-merge-reduce-heads.sh +++ b/t/t7603-merge-reduce-heads.sh @@ -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 |