merge-tree: support including merge messages in output

When running `git merge-tree --write-tree`, we previously would only return an exit status reflecting the cleanness of a merge, and print out the toplevel tree of the resulting merge. Merges also have informational messages, such as: * "Auto-merging <PATH>" * "CONFLICT (content): ..." * "CONFLICT (file/directory)" * etc. In fact, when non-content conflicts occur (such as file/directory, modify/delete, add/add with differing modes, rename/rename (1to2), etc.), these informational messages may be the only notification the user gets since these conflicts are not representable in the contents of the file. Add a --[no-]messages option so that callers can request these messages be included at the end of the output. Include such messages by default when there are conflicts, and omit them by default when the merge is clean. Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Jun 18, 2022 at 00:20 UTC a1a7811975e4e3d872379ab03acab9506933de9c
3 files changed +97 -8
Documentation/git-merge-tree.txt
+41 -6
@@ -9,7 +9,7 @@ git-merge-tree - Perform merge without touching index or working tree
9 SYNOPSIS
10 --------
11 [verse]
12 -'git merge-tree' [--write-tree] <branch1> <branch2>
12 +'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
13 'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
14
15 [[NEWMERGE]]
@@ -37,18 +37,50 @@ linkgit:git-merge[1], including:
37 After the merge completes, a new toplevel tree object is created. See
38 `OUTPUT` below for details.
39
40 +OPTIONS
41 +-------
42 +
43 +--[no-]messages::
44 + Write any informational messages such as "Auto-merging <path>"
45 + or CONFLICT notices to the end of stdout. If unspecified, the
46 + default is to include these messages if there are merge
47 + conflicts, and to omit them otherwise.
48 +
49 [[OUTPUT]]
50 OUTPUT
51 ------
52
44 -For either a successful or conflicted merge, the output from
45 -git-merge-tree is simply one line:
53 +For a successful merge, the output from git-merge-tree is simply one
54 +line:
55 +
56 + <OID of toplevel tree>
57 +
58 +Whereas for a conflicted merge, the output is by default of the form:
59
60 <OID of toplevel tree>
61 + <Informational messages>
62 +
63 +These are discussed individually below.
64
49 -The printed tree object corresponds to what would be checked out in
50 -the working tree at the end of `git merge`, and thus may have files
51 -with conflict markers in them.
65 +[[OIDTLT]]
66 +OID of toplevel tree
67 +~~~~~~~~~~~~~~~~~~~~
68 +
69 +This is a tree object that represents what would be checked out in the
70 +working tree at the end of `git merge`. If there were conflicts, then
71 +files within this tree may have embedded conflict markers.
72 +
73 +[[IM]]
74 +Informational messages
75 +~~~~~~~~~~~~~~~~~~~~~~
76 +
77 +This always starts with a blank line to separate it from the previous
78 +section, and then has free-form messages about the merge, such as:
79 +
80 + * "Auto-merging <file>"
81 + * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
82 + * "Failed to merge submodule <submodule> (<reason>)"
83 + * "Warning: cannot merge binary files: <filename>"
84
85 EXIT STATUS
86 -----------
@@ -72,6 +104,9 @@ used as a part of a series of steps such as:
104 NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
105 git update-ref $BRANCH1 $NEWCOMMIT
106
107 +Note that when the exit status is non-zero, `NEWTREE` in this sequence
108 +will contain a lot more output than just a tree.
109 +
110 [[DEPMERGE]]
111 DEPRECATED DESCRIPTION
112 ----------------------
builtin/merge-tree.c
+19 -2
@@ -396,6 +396,7 @@ enum mode {
396
397 struct merge_tree_options {
398 int mode;
399 + int show_messages;
400 };
401
402 static int real_merge(struct merge_tree_options *o,
@@ -435,18 +436,27 @@ static int real_merge(struct merge_tree_options *o,
436 merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
437 if (result.clean < 0)
438 die(_("failure to merge"));
439 +
440 + if (o->show_messages == -1)
441 + o->show_messages = !result.clean;
442 +
443 puts(oid_to_hex(&result.tree->object.oid));
444 + if (o->show_messages) {
445 + printf("\n");
446 + merge_display_update_messages(&opt, &result);
447 + }
448 merge_finalize(&opt, &result);
449 return !result.clean; /* result.clean < 0 handled above */
450 }
451
452 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
453 {
445 - struct merge_tree_options o = { 0 };
454 + struct merge_tree_options o = { .show_messages = -1 };
455 int expected_remaining_argc;
456 + int original_argc;
457
458 const char * const merge_tree_usage[] = {
449 - N_("git merge-tree [--write-tree] <branch1> <branch2>"),
459 + N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
460 N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
461 NULL
462 };
@@ -456,10 +466,13 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
466 MODE_REAL),
467 OPT_CMDMODE(0, "trivial-merge", &o.mode,
468 N_("do a trivial merge only"), MODE_TRIVIAL),
469 + OPT_BOOL(0, "messages", &o.show_messages,
470 + N_("also show informational/conflict messages")),
471 OPT_END()
472 };
473
474 /* Parse arguments */
475 + original_argc = argc - 1; /* ignoring argv[0] */
476 argc = parse_options(argc, argv, prefix, mt_options,
477 merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
478 switch (o.mode) {
@@ -483,8 +496,12 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
496 break;
497 case MODE_TRIVIAL:
498 expected_remaining_argc = 3;
499 + /* Removal of `--trivial-merge` is expected */
500 + original_argc--;
501 break;
502 }
503 + if (o.mode == MODE_TRIVIAL && argc < original_argc)
504 + die(_("--trivial-merge is incompatible with all other options"));
505
506 if (argc != expected_remaining_argc)
507 usage_with_options(merge_tree_usage, mt_options);
t/t4301-merge-tree-write-tree.sh
+37
@@ -103,4 +103,41 @@ test_expect_success 'Barf on too many arguments' '
103 grep "^usage: git merge-tree" expect
104 '
105
106 +anonymize_hash() {
107 + sed -e "s/[0-9a-f]\{40,\}/HASH/g" "$@"
108 +}
109 +
110 +test_expect_success 'test conflict notices and such' '
111 + test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
112 + anonymize_hash out >actual &&
113 +
114 + # Expected results:
115 + # "greeting" should merge with conflicts
116 + # "numbers" should merge cleanly
117 + # "whatever" has *both* a modify/delete and a file/directory conflict
118 + cat <<-EOF >expect &&
119 + HASH
120 +
121 + Auto-merging greeting
122 + CONFLICT (content): Merge conflict in greeting
123 + Auto-merging numbers
124 + CONFLICT (file/directory): directory in the way of whatever from side1; moving it to whatever~side1 instead.
125 + CONFLICT (modify/delete): whatever~side1 deleted in side2 and modified in side1. Version side1 of whatever~side1 left in tree.
126 + EOF
127 +
128 + test_cmp expect actual
129 +'
130 +
131 +for opt in $(git merge-tree --git-completion-helper-all)
132 +do
133 + if test $opt = "--trivial-merge" || test $opt = "--write-tree"
134 + then
135 + continue
136 + fi
137 +
138 + test_expect_success "usage: --trivial-merge is incompatible with $opt" '
139 + test_expect_code 128 git merge-tree --trivial-merge $opt side1 side2 side3
140 + '
141 +done
142 +
143 test_done