merge-tree.c: add --merge-base=<commit> option

This patch will give our callers more flexibility to use `git merge-tree`,
such as:

    git merge-tree --write-tree --merge-base=branch^ HEAD branch

This does a merge of HEAD and branch, but uses branch^ as the merge-base.

And the reason why using an option flag instead of a positional argument
is to allow additional commits passed to merge-tree to be handled via an
octopus merge in the future.

Signed-off-by: Kyle Zhao <kylezhao@tencent.com>
Signed-off-by: Taylor Blau <me@ttaylorr.com>
This commit is contained in:
Kyle Zhao
2022-11-11 23:45:13 +00:00
committed by Taylor Blau
parent ec1edbcb56
commit 66265a693e
3 changed files with 66 additions and 11 deletions

View File

@ -64,6 +64,10 @@ OPTIONS
share no common history. This flag can be given to override that share no common history. This flag can be given to override that
check and make the merge proceed anyway. check and make the merge proceed anyway.
--merge-base=<commit>::
Instead of finding the merge-bases for <branch1> and <branch2>,
specify a merge-base for the merge.
[[OUTPUT]] [[OUTPUT]]
OUTPUT OUTPUT
------ ------

View File

@ -3,6 +3,7 @@
#include "tree-walk.h" #include "tree-walk.h"
#include "xdiff-interface.h" #include "xdiff-interface.h"
#include "help.h" #include "help.h"
#include "commit.h"
#include "commit-reach.h" #include "commit-reach.h"
#include "merge-ort.h" #include "merge-ort.h"
#include "object-store.h" #include "object-store.h"
@ -406,6 +407,7 @@ struct merge_tree_options {
}; };
static int real_merge(struct merge_tree_options *o, static int real_merge(struct merge_tree_options *o,
const char *merge_base,
const char *branch1, const char *branch2, const char *branch1, const char *branch2,
const char *prefix) const char *prefix)
{ {
@ -432,6 +434,20 @@ static int real_merge(struct merge_tree_options *o,
opt.branch1 = branch1; opt.branch1 = branch1;
opt.branch2 = branch2; opt.branch2 = branch2;
if (merge_base) {
struct commit *base_commit;
struct tree *base_tree, *parent1_tree, *parent2_tree;
base_commit = lookup_commit_reference_by_name(merge_base);
if (!base_commit)
die(_("could not lookup commit %s"), merge_base);
opt.ancestor = merge_base;
base_tree = get_commit_tree(base_commit);
parent1_tree = get_commit_tree(parent1);
parent2_tree = get_commit_tree(parent2);
merge_incore_nonrecursive(&opt, base_tree, parent1_tree, parent2_tree, &result);
} else {
/* /*
* Get the merge bases, in reverse order; see comment above * Get the merge bases, in reverse order; see comment above
* merge_incore_recursive in merge-ort.h * merge_incore_recursive in merge-ort.h
@ -440,8 +456,9 @@ static int real_merge(struct merge_tree_options *o,
if (!merge_bases && !o->allow_unrelated_histories) if (!merge_bases && !o->allow_unrelated_histories)
die(_("refusing to merge unrelated histories")); die(_("refusing to merge unrelated histories"));
merge_bases = reverse_commit_list(merge_bases); merge_bases = reverse_commit_list(merge_bases);
merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result); merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
}
if (result.clean < 0) if (result.clean < 0)
die(_("failure to merge")); die(_("failure to merge"));
@ -487,6 +504,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
struct merge_tree_options o = { .show_messages = -1 }; struct merge_tree_options o = { .show_messages = -1 };
int expected_remaining_argc; int expected_remaining_argc;
int original_argc; int original_argc;
const char *merge_base = NULL;
const char * const merge_tree_usage[] = { const char * const merge_tree_usage[] = {
N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"), N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
@ -515,6 +533,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
&o.use_stdin, &o.use_stdin,
N_("perform multiple merges, one per line of input"), N_("perform multiple merges, one per line of input"),
PARSE_OPT_NONEG), PARSE_OPT_NONEG),
OPT_STRING(0, "merge-base",
&merge_base,
N_("commit"),
N_("specify a merge-base for the merge")),
OPT_END() OPT_END()
}; };
@ -529,6 +551,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
if (o.mode == MODE_TRIVIAL) if (o.mode == MODE_TRIVIAL)
die(_("--trivial-merge is incompatible with all other options")); die(_("--trivial-merge is incompatible with all other options"));
if (merge_base)
die(_("--merge-base is incompatible with --stdin"));
line_termination = '\0'; line_termination = '\0';
while (strbuf_getline_lf(&buf, stdin) != EOF) { while (strbuf_getline_lf(&buf, stdin) != EOF) {
struct strbuf **split; struct strbuf **split;
@ -538,7 +562,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
if (!split[0] || !split[1] || split[2]) if (!split[0] || !split[1] || split[2])
die(_("malformed input line: '%s'."), buf.buf); die(_("malformed input line: '%s'."), buf.buf);
strbuf_rtrim(split[0]); strbuf_rtrim(split[0]);
result = real_merge(&o, split[0]->buf, split[1]->buf, prefix); result = real_merge(&o, merge_base, split[0]->buf, split[1]->buf, prefix);
if (result < 0) if (result < 0)
die(_("merging cannot continue; got unclean result of %d"), result); die(_("merging cannot continue; got unclean result of %d"), result);
strbuf_list_free(split); strbuf_list_free(split);
@ -581,7 +605,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
/* Do the relevant type of merge */ /* Do the relevant type of merge */
if (o.mode == MODE_REAL) if (o.mode == MODE_REAL)
return real_merge(&o, argv[0], argv[1], prefix); return real_merge(&o, merge_base, argv[0], argv[1], prefix);
else else
return trivial_merge(argv[0], argv[1], argv[2]); return trivial_merge(argv[0], argv[1], argv[2]);
} }

View File

@ -860,4 +860,31 @@ test_expect_success '--stdin with both a successful and a conflicted merge' '
test_cmp expect actual test_cmp expect actual
' '
# specify merge-base as parent of branch2
# git merge-tree --write-tree --merge-base=c2 c1 c3
# Commit c1: add file1
# Commit c2: add file2 after c1
# Commit c3: add file3 after c2
# Expected: add file3, and file2 does NOT appear
test_expect_success 'specify merge-base as parent of branch2' '
# Setup
test_when_finished "rm -rf base-b2-p" &&
git init base-b2-p &&
test_commit -C base-b2-p c1 file1 &&
test_commit -C base-b2-p c2 file2 &&
test_commit -C base-b2-p c3 file3 &&
# Testing
TREE_OID=$(git -C base-b2-p merge-tree --write-tree --merge-base=c2 c1 c3) &&
q_to_tab <<-EOF >expect &&
100644 blob $(git -C base-b2-p rev-parse c1:file1)Qfile1
100644 blob $(git -C base-b2-p rev-parse c3:file3)Qfile3
EOF
git -C base-b2-p ls-tree $TREE_OID >actual &&
test_cmp expect actual
'
test_done test_done