Merge branch 'en/merge-tree-sequence'

"git merge-tree --stdin" is a new way to request a series of merges
and report the merge results.

* en/merge-tree-sequence:
  merge-tree: support multiple batched merges with --stdin
  merge-tree: update documentation for differences in -z output
This commit is contained in:
Taylor Blau 2022-10-30 21:04:44 -04:00
commit b1e3dd68ee
3 changed files with 148 additions and 11 deletions

View File

@ -81,6 +81,31 @@ Whereas for a conflicted merge, the output is by default of the form:
These are discussed individually below. These are discussed individually below.
However, there is an exception. If `--stdin` is passed, then there is
an extra section at the beginning, a NUL character at the end, and then
all the sections repeat for each line of input. Thus, if the first merge
is conflicted and the second is clean, the output would be of the form:
<Merge status>
<OID of toplevel tree>
<Conflicted file info>
<Informational messages>
NUL
<Merge status>
<OID of toplevel tree>
NUL
[[MS]]
Merge status
~~~~~~~~~~~~
This is an integer status followed by a NUL character. The integer status is:
0: merge had conflicts
1: merge was clean
&lt;0: something prevented the merge from running (e.g. access to repository
objects denied by filesystem)
[[OIDTLT]] [[OIDTLT]]
OID of toplevel tree OID of toplevel tree
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
@ -108,18 +133,50 @@ character instead of a newline character.
Informational messages Informational messages
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
This always starts with a blank line (or NUL if `-z` is passed) to This section provides informational messages, typically about
separate it from the previous sections, and then has free-form conflicts. The format of the section varies significantly depending
messages about the merge, such as: on whether `-z` is passed.
If `-z` is passed:
The output format is zero or more conflict informational records, each
of the form:
<list-of-paths><conflict-type>NUL<conflict-message>NUL
where <list-of-paths> is of the form
<number-of-paths>NUL<path1>NUL<path2>NUL...<pathN>NUL
and includes paths (or branch names) affected by the conflict or
informational message in <conflict-message>. Also, <conflict-type> is a
stable string explaining the type of conflict, such as
* "Auto-merging"
* "CONFLICT (rename/delete)"
* "CONFLICT (submodule lacks merge base)"
* "CONFLICT (binary)"
and <conflict-message> is a more detailed message about the conflict which often
(but not always) embeds the <stable-short-type-description> within it. These
strings may change in future Git versions. Some examples:
* "Auto-merging <file>" * "Auto-merging <file>"
* "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..." * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
* "Failed to merge submodule <submodule> (<reason>)" * "Failed to merge submodule <submodule> (no merge base)"
* "Warning: cannot merge binary files: <filename>" * "Warning: cannot merge binary files: <filename>"
Note that these free-form messages will never have a NUL character If `-z` is NOT passed:
in or between them, even if -z is passed. It is simply a large block
of text taking up the remainder of the output. This section starts with a blank line to separate it from the previous
sections, and then only contains the <conflict-message> information
from the previous section (separated by newlines). These are
non-stable strings that should not be parsed by scripts, and are just
meant for human consumption. Also, note that while <conflict-message>
strings usually do not contain embedded newlines, they sometimes do.
(However, the free-form messages will never have an embedded NUL
character). So, the entire block of information is meant for human
readers as an agglomeration of all conflict messages.
EXIT STATUS EXIT STATUS
----------- -----------
@ -127,7 +184,10 @@ EXIT STATUS
For a successful, non-conflicted merge, the exit status is 0. When the For a successful, non-conflicted merge, the exit status is 0. When the
merge has conflicts, the exit status is 1. If the merge is not able to merge has conflicts, the exit status is 1. If the merge is not able to
complete (or start) due to some kind of error, the exit status is complete (or start) due to some kind of error, the exit status is
something other than 0 or 1 (and the output is unspecified). something other than 0 or 1 (and the output is unspecified). When
--stdin is passed, the return status is 0 for both successful and
conflicted merges, and something other than 0 or 1 if it cannot complete
all the requested merges.
USAGE NOTES USAGE NOTES
----------- -----------

View File

@ -402,6 +402,7 @@ struct merge_tree_options {
int allow_unrelated_histories; int allow_unrelated_histories;
int show_messages; int show_messages;
int name_only; int name_only;
int use_stdin;
}; };
static int real_merge(struct merge_tree_options *o, static int real_merge(struct merge_tree_options *o,
@ -412,6 +413,7 @@ static int real_merge(struct merge_tree_options *o,
struct commit_list *merge_bases = NULL; struct commit_list *merge_bases = NULL;
struct merge_options opt; struct merge_options opt;
struct merge_result result = { 0 }; struct merge_result result = { 0 };
int show_messages = o->show_messages;
parent1 = get_merge_parent(branch1); parent1 = get_merge_parent(branch1);
if (!parent1) if (!parent1)
@ -443,9 +445,11 @@ static int real_merge(struct merge_tree_options *o,
if (result.clean < 0) if (result.clean < 0)
die(_("failure to merge")); die(_("failure to merge"));
if (o->show_messages == -1) if (show_messages == -1)
o->show_messages = !result.clean; show_messages = !result.clean;
if (o->use_stdin)
printf("%d%c", result.clean, line_termination);
printf("%s%c", oid_to_hex(&result.tree->object.oid), line_termination); printf("%s%c", oid_to_hex(&result.tree->object.oid), line_termination);
if (!result.clean) { if (!result.clean) {
struct string_list conflicted_files = STRING_LIST_INIT_NODUP; struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
@ -467,11 +471,13 @@ static int real_merge(struct merge_tree_options *o,
} }
string_list_clear(&conflicted_files, 1); string_list_clear(&conflicted_files, 1);
} }
if (o->show_messages) { if (show_messages) {
putchar(line_termination); putchar(line_termination);
merge_display_update_messages(&opt, line_termination == '\0', merge_display_update_messages(&opt, line_termination == '\0',
&result); &result);
} }
if (o->use_stdin)
putchar(line_termination);
merge_finalize(&opt, &result); merge_finalize(&opt, &result);
return !result.clean; /* result.clean < 0 handled above */ return !result.clean; /* result.clean < 0 handled above */
} }
@ -505,6 +511,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
&o.allow_unrelated_histories, &o.allow_unrelated_histories,
N_("allow merging unrelated histories"), N_("allow merging unrelated histories"),
PARSE_OPT_NONEG), PARSE_OPT_NONEG),
OPT_BOOL_F(0, "stdin",
&o.use_stdin,
N_("perform multiple merges, one per line of input"),
PARSE_OPT_NONEG),
OPT_END() OPT_END()
}; };
@ -512,6 +522,32 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
original_argc = argc - 1; /* ignoring argv[0] */ original_argc = argc - 1; /* ignoring argv[0] */
argc = parse_options(argc, argv, prefix, mt_options, argc = parse_options(argc, argv, prefix, mt_options,
merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION); merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
/* Handle --stdin */
if (o.use_stdin) {
struct strbuf buf = STRBUF_INIT;
if (o.mode == MODE_TRIVIAL)
die(_("--trivial-merge is incompatible with all other options"));
line_termination = '\0';
while (strbuf_getline_lf(&buf, stdin) != EOF) {
struct strbuf **split;
int result;
split = strbuf_split(&buf, ' ');
if (!split[0] || !split[1] || split[2])
die(_("malformed input line: '%s'."), buf.buf);
strbuf_rtrim(split[0]);
result = real_merge(&o, split[0]->buf, split[1]->buf, prefix);
if (result < 0)
die(_("merging cannot continue; got unclean result of %d"), result);
strbuf_list_free(split);
}
strbuf_release(&buf);
return 0;
}
/* Figure out which mode to use */
switch (o.mode) { switch (o.mode) {
default: default:
BUG("unexpected command mode %d", o.mode); BUG("unexpected command mode %d", o.mode);

View File

@ -819,4 +819,45 @@ test_expect_success SANITY 'merge-ort fails gracefully in a read-only repository
test_must_fail git -C read-only merge-tree side1 side2 test_must_fail git -C read-only merge-tree side1 side2
' '
test_expect_success '--stdin with both a successful and a conflicted merge' '
printf "side1 side3\nside1 side2" | git merge-tree --stdin >actual &&
git checkout side1^0 &&
git merge side3 &&
printf "1\0" >expect &&
git rev-parse HEAD^{tree} | lf_to_nul >>expect &&
printf "\0" >>expect &&
git checkout side1^0 &&
test_must_fail git merge side2 &&
sed s/HEAD/side1/ greeting >tmp &&
mv tmp greeting &&
git add -u &&
git mv whatever~HEAD whatever~side1 &&
printf "0\0" >>expect &&
git write-tree | lf_to_nul >>expect &&
cat <<-EOF | q_to_tab | lf_to_nul >>expect &&
100644 $(git rev-parse side1~1:greeting) 1Qgreeting
100644 $(git rev-parse side1:greeting) 2Qgreeting
100644 $(git rev-parse side2:greeting) 3Qgreeting
100644 $(git rev-parse side1~1:whatever) 1Qwhatever~side1
100644 $(git rev-parse side1:whatever) 2Qwhatever~side1
EOF
q_to_nul <<-EOF >>expect &&
Q1QgreetingQAuto-mergingQAuto-merging greeting
Q1QgreetingQCONFLICT (contents)QCONFLICT (content): Merge conflict in greeting
Q1QnumbersQAuto-mergingQAuto-merging numbers
Q2Qwhatever~side1QwhateverQCONFLICT (file/directory)QCONFLICT (file/directory): directory in the way of whatever from side1; moving it to whatever~side1 instead.
Q1Qwhatever~side1QCONFLICT (modify/delete)QCONFLICT (modify/delete): whatever~side1 deleted in side2 and modified in side1. Version side1 of whatever~side1 left in tree.
EOF
printf "\0\0" >>expect &&
test_cmp expect actual
'
test_done test_done