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>

Kyle Zhao committed Nov 11, 2022 at 23:45 UTC 66265a693e8deb3ab86577eb7f69940410044081
3 files changed +66 -11
Documentation/git-merge-tree.txt
+4
@@ -64,6 +64,10 @@ OPTIONS
64 share no common history. This flag can be given to override that
65 check and make the merge proceed anyway.
66
67 +--merge-base=<commit>::
68 + Instead of finding the merge-bases for <branch1> and <branch2>,
69 + specify a merge-base for the merge.
70 +
71 [[OUTPUT]]
72 OUTPUT
73 ------
builtin/merge-tree.c
+35 -11
@@ -3,6 +3,7 @@
3 #include "tree-walk.h"
4 #include "xdiff-interface.h"
5 #include "help.h"
6 +#include "commit.h"
7 #include "commit-reach.h"
8 #include "merge-ort.h"
9 #include "object-store.h"
@@ -406,6 +407,7 @@ struct merge_tree_options {
407 };
408
409 static int real_merge(struct merge_tree_options *o,
410 + const char *merge_base,
411 const char *branch1, const char *branch2,
412 const char *prefix)
413 {
@@ -432,16 +434,31 @@ static int real_merge(struct merge_tree_options *o,
434 opt.branch1 = branch1;
435 opt.branch2 = branch2;
436
435 - /*
436 - * Get the merge bases, in reverse order; see comment above
437 - * merge_incore_recursive in merge-ort.h
438 - */
439 - merge_bases = get_merge_bases(parent1, parent2);
440 - if (!merge_bases && !o->allow_unrelated_histories)
441 - die(_("refusing to merge unrelated histories"));
442 - merge_bases = reverse_commit_list(merge_bases);
437 + if (merge_base) {
438 + struct commit *base_commit;
439 + struct tree *base_tree, *parent1_tree, *parent2_tree;
440 +
441 + base_commit = lookup_commit_reference_by_name(merge_base);
442 + if (!base_commit)
443 + die(_("could not lookup commit %s"), merge_base);
444 +
445 + opt.ancestor = merge_base;
446 + base_tree = get_commit_tree(base_commit);
447 + parent1_tree = get_commit_tree(parent1);
448 + parent2_tree = get_commit_tree(parent2);
449 + merge_incore_nonrecursive(&opt, base_tree, parent1_tree, parent2_tree, &result);
450 + } else {
451 + /*
452 + * Get the merge bases, in reverse order; see comment above
453 + * merge_incore_recursive in merge-ort.h
454 + */
455 + merge_bases = get_merge_bases(parent1, parent2);
456 + if (!merge_bases && !o->allow_unrelated_histories)
457 + die(_("refusing to merge unrelated histories"));
458 + merge_bases = reverse_commit_list(merge_bases);
459 + merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
460 + }
461
444 - merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
462 if (result.clean < 0)
463 die(_("failure to merge"));
464
@@ -487,6 +504,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
504 struct merge_tree_options o = { .show_messages = -1 };
505 int expected_remaining_argc;
506 int original_argc;
507 + const char *merge_base = NULL;
508
509 const char * const merge_tree_usage[] = {
510 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)
533 &o.use_stdin,
534 N_("perform multiple merges, one per line of input"),
535 PARSE_OPT_NONEG),
536 + OPT_STRING(0, "merge-base",
537 + &merge_base,
538 + N_("commit"),
539 + N_("specify a merge-base for the merge")),
540 OPT_END()
541 };
542
@@ -529,6 +551,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
551
552 if (o.mode == MODE_TRIVIAL)
553 die(_("--trivial-merge is incompatible with all other options"));
554 + if (merge_base)
555 + die(_("--merge-base is incompatible with --stdin"));
556 line_termination = '\0';
557 while (strbuf_getline_lf(&buf, stdin) != EOF) {
558 struct strbuf **split;
@@ -538,7 +562,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
562 if (!split[0] || !split[1] || split[2])
563 die(_("malformed input line: '%s'."), buf.buf);
564 strbuf_rtrim(split[0]);
541 - result = real_merge(&o, split[0]->buf, split[1]->buf, prefix);
565 + result = real_merge(&o, merge_base, split[0]->buf, split[1]->buf, prefix);
566 if (result < 0)
567 die(_("merging cannot continue; got unclean result of %d"), result);
568 strbuf_list_free(split);
@@ -581,7 +605,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
605
606 /* Do the relevant type of merge */
607 if (o.mode == MODE_REAL)
584 - return real_merge(&o, argv[0], argv[1], prefix);
608 + return real_merge(&o, merge_base, argv[0], argv[1], prefix);
609 else
610 return trivial_merge(argv[0], argv[1], argv[2]);
611 }
t/t4301-merge-tree-write-tree.sh
+27
@@ -860,4 +860,31 @@ test_expect_success '--stdin with both a successful and a conflicted merge' '
860 test_cmp expect actual
861 '
862
863 +# specify merge-base as parent of branch2
864 +# git merge-tree --write-tree --merge-base=c2 c1 c3
865 +# Commit c1: add file1
866 +# Commit c2: add file2 after c1
867 +# Commit c3: add file3 after c2
868 +# Expected: add file3, and file2 does NOT appear
869 +
870 +test_expect_success 'specify merge-base as parent of branch2' '
871 + # Setup
872 + test_when_finished "rm -rf base-b2-p" &&
873 + git init base-b2-p &&
874 + test_commit -C base-b2-p c1 file1 &&
875 + test_commit -C base-b2-p c2 file2 &&
876 + test_commit -C base-b2-p c3 file3 &&
877 +
878 + # Testing
879 + TREE_OID=$(git -C base-b2-p merge-tree --write-tree --merge-base=c2 c1 c3) &&
880 +
881 + q_to_tab <<-EOF >expect &&
882 + 100644 blob $(git -C base-b2-p rev-parse c1:file1)Qfile1
883 + 100644 blob $(git -C base-b2-p rev-parse c3:file3)Qfile3
884 + EOF
885 +
886 + git -C base-b2-p ls-tree $TREE_OID >actual &&
887 + test_cmp expect actual
888 +'
889 +
890 test_done