Merge branch 'jc/merge-reduce-parents-early'

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
This commit is contained in:
Junio C Hamano 2012-04-27 13:59:20 -07:00
Родитель 7b864abe0b 5802f81b14
Коммит 283097e9ed
5 изменённых файлов: 275 добавлений и 70 удалений

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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; "

Просмотреть файл

@ -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

Просмотреть файл

@ -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
'

Просмотреть файл

@ -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