{"thread":{"id":"58676","subject":"[PATCH 2/2] merge-tree: support multiple batched merges with --stdin","startedAt":"2022-10-23T01:28:15Z","lastAt":"2022-10-23T01:28:22Z","messageCount":3,"participants":["Elijah Newren via GitGitGadget"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"465591","messageId":"60421a477f39dcf6ede7d87444b0c2378fe1dc6c.1666488485.git.gitgitgadget@gmail.com","threadId":"58676","inReplyTo":"pull.1361.git.1666488485.gitgitgadget@gmail.com","subject":"[PATCH 2/2] merge-tree: support multiple batched merges with --stdin","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-10-23T01:28:05Z","receivedAt":"2022-10-23T01:28:15Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nAdd an option, --stdin, to merge-tree which will accept lines of input\nwith two branches to merge per line, and which will perform all the\nmerges and give output for each in turn.  This option implies -z, and\nmodifies the output to also include a merge status since the exit code\nof the program can no longer convey that information now that multiple\nmerges are involved.\n\nThis could be useful, for example, by Git hosting providers.  When one\nbranch is updated, one may want to check whether all code reviews\ntargetting that branch can still cleanly merge.  Avoiding the overhead\nof starting up a separate process for each of those code reviews might\nprovide significant savings in a repository with many code reviews.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n Documentation/git-merge-tree.txt | 30 ++++++++++++++++++++++-\n builtin/merge-tree.c             | 42 +++++++++++++++++++++++++++++---\n t/t4301-merge-tree-write-tree.sh | 41 +++++++++++++++++++++++++++++++\n 3 files changed, 109 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-merge-tree.txt b/Documentation/git-merge-tree.txt\nindex 9914b1bc0cd..04bcc416e6e 100644\n--- a/Documentation/git-merge-tree.txt\n+++ b/Documentation/git-merge-tree.txt\n@@ -81,6 +81,31 @@ Whereas for a conflicted merge, the output is by default of the form:\n \n These are discussed individually below.\n \n+However, there is an exception.  If `--stdin` is passed, then there is\n+an extra section at the beginning, a NUL character at the end, and then\n+all the sections repeat for each line of input.  Thus, if the first merge\n+is conflicted and the second is clean, the output would be of the form:\n+\n+\t<Merge status>\n+\t<OID of toplevel tree>\n+\t<Conflicted file info>\n+\t<Informational messages>\n+\tNUL\n+\t<Merge status>\n+\t<OID of toplevel tree>\n+\tNUL\n+\n+[[MS]]\n+Merge status\n+~~~~~~~~~~~~\n+\n+This is an integer status followed by a NUL character.  The integer status is:\n+\n+     0: merge had conflicts\n+     1: merge was clean\n+     &lt;0: something prevented the merge from running (e.g. access to repository\n+\t objects denied by filesystem)\n+\n [[OIDTLT]]\n OID of toplevel tree\n ~~~~~~~~~~~~~~~~~~~~\n@@ -159,7 +184,10 @@ EXIT STATUS\n For a successful, non-conflicted merge, the exit status is 0.  When the\n merge has conflicts, the exit status is 1.  If the merge is not able to\n complete (or start) due to some kind of error, the exit status is\n-something other than 0 or 1 (and the output is unspecified).\n+something other than 0 or 1 (and the output is unspecified).  When\n+--stdin is passed, the return status is 0 for both successful and\n+conflicted merges, and something other than 0 or 1 if it cannot complete\n+all the requested merges.\n \n USAGE NOTES\n -----------\ndiff --git a/builtin/merge-tree.c b/builtin/merge-tree.c\nindex ae5782917b9..fe853aa8f91 100644\n--- a/builtin/merge-tree.c\n+++ b/builtin/merge-tree.c\n@@ -402,6 +402,7 @@ struct merge_tree_options {\n \tint allow_unrelated_histories;\n \tint show_messages;\n \tint name_only;\n+\tint use_stdin;\n };\n \n static int real_merge(struct merge_tree_options *o,\n@@ -412,6 +413,7 @@ static int real_merge(struct merge_tree_options *o,\n \tstruct commit_list *merge_bases = NULL;\n \tstruct merge_options opt;\n \tstruct merge_result result = { 0 };\n+\tint show_messages = o->show_messages;\n \n \tparent1 = get_merge_parent(branch1);\n \tif (!parent1)\n@@ -443,9 +445,11 @@ static int real_merge(struct merge_tree_options *o,\n \tif (result.clean < 0)\n \t\tdie(_(\"failure to merge\"));\n \n-\tif (o->show_messages == -1)\n-\t\to->show_messages = !result.clean;\n+\tif (show_messages == -1)\n+\t\tshow_messages = !result.clean;\n \n+\tif (o->use_stdin)\n+\t\tprintf(\"%d%c\", result.clean, line_termination);\n \tprintf(\"%s%c\", oid_to_hex(&result.tree->object.oid), line_termination);\n \tif (!result.clean) {\n \t\tstruct string_list conflicted_files = STRING_LIST_INIT_NODUP;\n@@ -467,11 +471,13 @@ static int real_merge(struct merge_tree_options *o,\n \t\t}\n \t\tstring_list_clear(&conflicted_files, 1);\n \t}\n-\tif (o->show_messages) {\n+\tif (show_messages) {\n \t\tputchar(line_termination);\n \t\tmerge_display_update_messages(&opt, line_termination == '\\0',\n \t\t\t\t\t      &result);\n \t}\n+\tif (o->use_stdin)\n+\t\tputchar(line_termination);\n \tmerge_finalize(&opt, &result);\n \treturn !result.clean; /* result.clean < 0 handled above */\n }\n@@ -505,6 +511,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)\n \t\t\t   &o.allow_unrelated_histories,\n \t\t\t   N_(\"allow merging unrelated histories\"),\n \t\t\t   PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"stdin\",\n+\t\t\t   &o.use_stdin,\n+\t\t\t   N_(\"perform multiple merges, one per line of input\"),\n+\t\t\t   PARSE_OPT_NONEG),\n \t\tOPT_END()\n \t};\n \n@@ -512,6 +522,32 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)\n \toriginal_argc = argc - 1; /* ignoring argv[0] */\n \targc = parse_options(argc, argv, prefix, mt_options,\n \t\t\t     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\t/* Handle --stdin */\n+\tif (o.use_stdin) {\n+\t\tstruct strbuf buf = STRBUF_INIT;\n+\n+\t\tif (o.mode == MODE_TRIVIAL)\n+\t\t\tdie(_(\"--trivial-merge is incompatible with all other options\"));\n+\t\tline_termination = '\\0';\n+\t\twhile (strbuf_getline_lf(&buf, stdin) != EOF) {\n+\t\t\tstruct strbuf **split;\n+\t\t\tint result;\n+\n+\t\t\tsplit = strbuf_split(&buf, ' ');\n+\t\t\tif (!split[0] || !split[1] || split[2])\n+\t\t\t\tdie(_(\"malformed input line: '%s'.\"), buf.buf);\n+\t\t\tstrbuf_rtrim(split[0]);\n+\t\t\tresult = real_merge(&o, split[0]->buf, split[1]->buf, prefix);\n+\t\t\tif (result < 0)\n+\t\t\t\tdie(_(\"merging cannot continue; got unclean result of %d\"), result);\n+\t\t\tstrbuf_list_free(split);\n+\t\t}\n+\t\tstrbuf_release(&buf);\n+\t\treturn 0;\n+\t}\n+\n+\t/* Figure out which mode to use */\n \tswitch (o.mode) {\n \tdefault:\n \t\tBUG(\"unexpected command mode %d\", o.mode);\ndiff --git a/t/t4301-merge-tree-write-tree.sh b/t/t4301-merge-tree-write-tree.sh\nindex 013b77144bd..cac85591b52 100755\n--- a/t/t4301-merge-tree-write-tree.sh\n+++ b/t/t4301-merge-tree-write-tree.sh\n@@ -819,4 +819,45 @@ test_expect_success SANITY 'merge-ort fails gracefully in a read-only repository\n \ttest_must_fail git -C read-only merge-tree side1 side2\n '\n \n+test_expect_success '--stdin with both a successful and a conflicted merge' '\n+\tprintf \"side1 side3\\nside1 side2\" | git merge-tree --stdin >actual &&\n+\n+\tgit checkout side1^0 &&\n+\tgit merge side3 &&\n+\n+\tprintf \"1\\0\" >expect &&\n+\tgit rev-parse HEAD^{tree} | lf_to_nul >>expect &&\n+\tprintf \"\\0\" >>expect &&\n+\n+\tgit checkout side1^0 &&\n+\ttest_must_fail git merge side2 &&\n+\tsed s/HEAD/side1/ greeting >tmp &&\n+\tmv tmp greeting &&\n+\tgit add -u &&\n+\tgit mv whatever~HEAD whatever~side1 &&\n+\n+\tprintf \"0\\0\" >>expect &&\n+\tgit write-tree | lf_to_nul >>expect &&\n+\n+\tcat <<-EOF | q_to_tab | lf_to_nul >>expect &&\n+\t100644 $(git rev-parse side1~1:greeting) 1Qgreeting\n+\t100644 $(git rev-parse side1:greeting) 2Qgreeting\n+\t100644 $(git rev-parse side2:greeting) 3Qgreeting\n+\t100644 $(git rev-parse side1~1:whatever) 1Qwhatever~side1\n+\t100644 $(git rev-parse side1:whatever) 2Qwhatever~side1\n+\tEOF\n+\n+\tq_to_nul <<-EOF >>expect &&\n+\tQ1QgreetingQAuto-mergingQAuto-merging greeting\n+\tQ1QgreetingQCONFLICT (contents)QCONFLICT (content): Merge conflict in greeting\n+\tQ1QnumbersQAuto-mergingQAuto-merging numbers\n+\tQ2Qwhatever~side1QwhateverQCONFLICT (file/directory)QCONFLICT (file/directory): directory in the way of whatever from side1; moving it to whatever~side1 instead.\n+\tQ1Qwhatever~side1QCONFLICT (modify/delete)QCONFLICT (modify/delete): whatever~side1 deleted in side2 and modified in side1.  Version side1 of whatever~side1 left in tree.\n+\tEOF\n+\n+\tprintf \"\\0\\0\" >>expect &&\n+\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"465592","messageId":"980cf9df9a9c1cd0a538ff00c6ea2ce81b483aa4.1666488485.git.gitgitgadget@gmail.com","threadId":"58676","inReplyTo":"pull.1361.git.1666488485.gitgitgadget@gmail.com","subject":"[PATCH 1/2] merge-tree: update documentation for differences in -z output","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-10-23T01:28:04Z","receivedAt":"2022-10-23T01:28:22Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nThe Informational Messages was updated in de90581141 (\"merge-ort:\noptionally produce machine-readable output\", 2022-06-18) to provide more\ndetailed and machine parseable output when `-z` is passed, but the\nDocumentation was not updated to reflect these changes.  Update it now.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n Documentation/git-merge-tree.txt | 46 +++++++++++++++++++++++++++-----\n 1 file changed, 39 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-merge-tree.txt b/Documentation/git-merge-tree.txt\nindex d6c356740ef..9914b1bc0cd 100644\n--- a/Documentation/git-merge-tree.txt\n+++ b/Documentation/git-merge-tree.txt\n@@ -108,18 +108,50 @@ character instead of a newline character.\n Informational messages\n ~~~~~~~~~~~~~~~~~~~~~~\n \n-This always starts with a blank line (or NUL if `-z` is passed) to\n-separate it from the previous sections, and then has free-form\n-messages about the merge, such as:\n+This section provides informational messages, typically about\n+conflicts.  The format of the section varies significantly depending\n+on whether `-z` is passed.\n+\n+If `-z` is passed:\n+\n+The output format is zero or more conflict informational records, each\n+of the form:\n+\n+\t<list-of-paths><conflict-type>NUL<conflict-message>NUL\n+\n+where <list-of-paths> is of the form\n+\n+\t<number-of-paths>NUL<path1>NUL<path2>NUL...<pathN>NUL\n+\n+and includes paths (or branch names) affected by the conflict or\n+informational message in <conflict-message>.  Also, <conflict-type> is a\n+stable string explaining the type of conflict, such as\n+\n+  * \"Auto-merging\"\n+  * \"CONFLICT (rename/delete)\"\n+  * \"CONFLICT (submodule lacks merge base)\"\n+  * \"CONFLICT (binary)\"\n+\n+and <conflict-message> is a more detailed message about the conflict which often\n+(but not always) embeds the <stable-short-type-description> within it.  These\n+strings may change in future Git versions.  Some examples:\n \n   * \"Auto-merging <file>\"\n   * \"CONFLICT (rename/delete): <oldfile> renamed...but deleted in...\"\n-  * \"Failed to merge submodule <submodule> (<reason>)\"\n+  * \"Failed to merge submodule <submodule> (no merge base)\"\n   * \"Warning: cannot merge binary files: <filename>\"\n \n-Note that these free-form messages will never have a NUL character\n-in or between them, even if -z is passed.  It is simply a large block\n-of text taking up the remainder of the output.\n+If `-z` is NOT passed:\n+\n+This section starts with a blank line to separate it from the previous\n+sections, and then only contains the <conflict-message> information\n+from the previous section (separated by newlines).  These are\n+non-stable strings that should not be parsed by scripts, and are just\n+meant for human consumption.  Also, note that while <conflict-message>\n+strings usually do not contain embedded newlines, they sometimes do.\n+(However, the free-form messages will never have an embedded NUL\n+character).  So, the entire block of information is meant for human\n+readers as an agglomeration of all conflict messages.\n \n EXIT STATUS\n -----------\n-- \ngitgitgadget\n\n"},{"id":"465593","messageId":"pull.1361.git.1666488485.gitgitgadget@gmail.com","threadId":"58676","inReplyTo":null,"subject":"[PATCH 0/2] merge-tree: batched merges","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-10-23T01:28:03Z","receivedAt":"2022-10-23T01:28:22Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"When a branch is updated, a Git hosting provider may want to check all open\ncode reviews targetting the given branch to determine if those code reviews\ncan still merge cleanly into the target branch. Provide a new --stdin option\nto git-merge-tree to enable this capability, with each line of input\nproviding the branches for the next merge.\n\nNote: this still requires all the merges to be in the same repository. There\nis some code I noticed a while back in merge-ort that implicitly uses\nthe_repository (indirectly), and there may be more uses that I am unaware\nof.\n\nElijah Newren (2):\n  merge-tree: update documentation for differences in -z output\n  merge-tree: support multiple batched merges with --stdin\n\n Documentation/git-merge-tree.txt | 76 ++++++++++++++++++++++++++++----\n builtin/merge-tree.c             | 42 ++++++++++++++++--\n t/t4301-merge-tree-write-tree.sh | 41 +++++++++++++++++\n 3 files changed, 148 insertions(+), 11 deletions(-)\n\n\nbase-commit: 45c9f05c44b1cb6bd2d6cb95a22cf5e3d21d5b63\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1361%2Fnewren%2Fbatched-merges-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1361/newren/batched-merges-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/1361\n-- \ngitgitgadget\n"}]}