Merge branch 'kz/merge-tree-merge-base'

"merge-tree" learns a new `--merge-base` option.

* kz/merge-tree-merge-base:
  docs: fix description of the `--merge-base` option
  merge-tree.c: allow specifying the merge-base when --stdin is passed
  merge-tree.c: add --merge-base=<commit> option
This commit is contained in:
Junio C Hamano 2022-12-14 15:55:45 +09:00
commit 7576e512ce
3 changed files with 131 additions and 12 deletions

View file

@ -64,6 +64,11 @@ 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, and specifying multiple bases is
currently not supported. This option is incompatible with `--stdin`.
[[OUTPUT]] [[OUTPUT]]
OUTPUT OUTPUT
------ ------
@ -216,6 +221,17 @@ with linkgit:git-merge[1]:
* any messages that would have been printed to stdout (the * any messages that would have been printed to stdout (the
<<IM,Informational messages>>) <<IM,Informational messages>>)
INPUT FORMAT
------------
'git merge-tree --stdin' input format is fully text based. Each line
has this format:
[<base-commit> -- ]<branch1> <branch2>
If one line is separated by `--`, the string before the separator is
used for specifying a merge-base for the merge and the string after
the separator describes the branches to be merged.
MISTAKES TO AVOID MISTAKES TO AVOID
----------------- -----------------

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,16 +434,31 @@ static int real_merge(struct merge_tree_options *o,
opt.branch1 = branch1; opt.branch1 = branch1;
opt.branch2 = branch2; opt.branch2 = branch2;
/* if (merge_base) {
* Get the merge bases, in reverse order; see comment above struct commit *base_commit;
* merge_incore_recursive in merge-ort.h struct tree *base_tree, *parent1_tree, *parent2_tree;
*/
merge_bases = get_merge_bases(parent1, parent2); base_commit = lookup_commit_reference_by_name(merge_base);
if (!merge_bases && !o->allow_unrelated_histories) if (!base_commit)
die(_("refusing to merge unrelated histories")); die(_("could not lookup commit %s"), merge_base);
merge_bases = reverse_commit_list(merge_bases);
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
* merge_incore_recursive in merge-ort.h
*/
merge_bases = get_merge_bases(parent1, parent2);
if (!merge_bases && !o->allow_unrelated_histories)
die(_("refusing to merge unrelated histories"));
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,16 +551,35 @@ 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;
int result; int result;
const char *input_merge_base = NULL;
split = strbuf_split(&buf, ' '); split = strbuf_split(&buf, ' ');
if (!split[0] || !split[1] || split[2]) if (!split[0] || !split[1])
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); strbuf_rtrim(split[1]);
/* parse the merge-base */
if (!strcmp(split[1]->buf, "--")) {
input_merge_base = split[0]->buf;
}
if (input_merge_base && split[2] && split[3] && !split[4]) {
strbuf_rtrim(split[2]);
strbuf_rtrim(split[3]);
result = real_merge(&o, input_merge_base, split[2]->buf, split[3]->buf, prefix);
} else if (!input_merge_base && !split[2]) {
result = real_merge(&o, NULL, split[0]->buf, split[1]->buf, prefix);
} else {
die(_("malformed input line: '%s'."), buf.buf);
}
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 +622,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,66 @@ test_expect_success '--stdin with both a successful and a conflicted merge' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success '--merge-base is incompatible with --stdin' '
test_must_fail git merge-tree --merge-base=side1 --stdin 2>expect &&
grep "^fatal: --merge-base is incompatible with --stdin" expect
'
# 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
'
# Since the earlier tests have verified that individual merge-tree calls
# are doing the right thing, this test case is only used to verify that
# we can also trigger merges via --stdin, and that when we do we get
# the same answer as running a bunch of separate merges.
test_expect_success 'check the input format when --stdin is passed' '
test_when_finished "rm -rf repo" &&
git init repo &&
test_commit -C repo c1 &&
test_commit -C repo c2 &&
test_commit -C repo c3 &&
printf "c1 c3\nc2 -- c1 c3\nc2 c3" | git -C repo merge-tree --stdin >actual &&
printf "1\0" >expect &&
git -C repo merge-tree --write-tree -z c1 c3 >>expect &&
printf "\0" >>expect &&
printf "1\0" >>expect &&
git -C repo merge-tree --write-tree -z --merge-base=c2 c1 c3 >>expect &&
printf "\0" >>expect &&
printf "1\0" >>expect &&
git -C repo merge-tree --write-tree -z c2 c3 >>expect &&
printf "\0" >>expect &&
test_cmp expect actual
'
test_done test_done