{"thread":{"id":"57133","subject":"[PATCH 0/9] Add a new --remerge-diff capability to show & log","startedAt":"2021-12-21T18:05:53Z","lastAt":"2022-02-02T11:18:29Z","messageCount":113,"participants":["Elijah Newren via GitGitGadget","Ævar Arnfjörð Bjarmason","Elijah Newren","Junio C Hamano","Johannes Altmanninger","brian m. carlson"],"isPatch":true,"patchVersion":1,"patchTotal":9},"messages":[{"id":"444636","messageId":"pull.1103.git.1640109948.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":null,"subject":"[PATCH 0/9] Add a new --remerge-diff capability to show & log","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:39Z","receivedAt":"2021-12-21T18:05:53Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Here are some patches to add a --remerge-diff capability to show & log,\nwhich works by comparing merge commits to an automatic remerge (note that\nthe automatic remerge tree can contain files with conflict markers).\n\nChanges since original submission[1]:\n\n * Rebased on top of the version of ns/tmp-objdir that Neeraj submitted\n   (Neeraj's patches were based on v2.34, but ns/tmp-objdir got applied on\n   an old commit and does not even build because of that).\n * Modify ll-merge API to return a status, instead of printing \"Cannot merge\n   binary files\" on stdout[2] (as suggested by Peff)\n * Make conflict messages and other such warnings into diff headers of the\n   subsequent remerge-diff rather than appearing in the diff as file content\n   of some funny looking filenames (as suggested by Peff[3] and Junio[4])\n * Sergey ack'ed the diff-merges.c portion of the patches, but that wasn't\n   limited to one patch so not sure where to record that ack.\n\n[1]\nhttps://lore.kernel.org/git/pull.1080.git.git.1630376800.gitgitgadget@gmail.com/;\nGitHub wouldn't let me change the target branch for the PR, so I had to\ncreate a new one with the new base and thus the reason for not sending this\nas v2 even though it is. [2]\nhttps://lore.kernel.org/git/YVOZRhWttzF18Xql@coredump.intra.peff.net/,\nhttps://lore.kernel.org/git/YVOZty9D7NRbzhE5@coredump.intra.peff.net/ [3]\nhttps://lore.kernel.org/git/YVOXPTjsp9lrxmS6@coredump.intra.peff.net/ [4]\nhttps://lore.kernel.org/git/xmqqr1d7e4ug.fsf@gitster.g/\n\n=== FURTHER BACKGROUND (original cover letter material) ==\n\nHere are some example commits you can try this out on (with git show\n--remerge-diff $COMMIT):\n\n * git.git conflicted merge: 07601b5b36\n * git.git non-conflicted change: bf04590ecd\n * linux.git conflicted merge: eab3540562fb\n * linux.git non-conflicted change: 223cea6a4f05\n\nMany more can be found by just running git log --merges --remerge-diff in\nyour repository of choice and searching for diffs (most merges tend to be\nclean and unmodified and thus produce no diff but a search of '^diff' in the\nlog output tends to find the examples nicely).\n\nSome basic high level details about this new option:\n\n * This option is most naturally compared to --cc, though the output seems\n   to be much more understandable to most users than --cc output.\n * Since merges are often clean and unmodified, this new option results in\n   an empty diff for most merges.\n * This new option shows things like the removal of conflict markers, which\n   hunks users picked from the various conflicted sides to keep or remove,\n   and shows changes made outside of conflict markers (which might reflect\n   changes needed to resolve semantic conflicts or cleanups of e.g.\n   compilation warnings or other additional changes an integrator felt\n   belonged in the merged result).\n * This new option does not (currently) work for octopus merges, since\n   merge-ort is specific to two-parent merges[1].\n * This option will not work on a read-only or full filesystem[2].\n * We discussed this capability at Git Merge 2020, and one of the\n   suggestions was doing a periodic git gc --auto during the operation (due\n   to potential new blobs and trees created during the operation). I found a\n   way to avoid that; see [2].\n * This option is faster than you'd probably expect; it handles 33.5 merge\n   commits per second in linux.git on my computer; see below.\n\nIn regards to the performance point above, the timing for running the\nfollowing command:\n\ntime git log --min-parents=2 --max-parents=2 $DIFF_FLAG | wc -l\n\n\nin linux.git (with v5.4 checked out, since my copy of linux is very out of\ndate) is as follows:\n\nDIFF_FLAG=--cc:            71m 31.536s\nDIFF_FLAG=--remerge-diff:  31m  3.170s\n\n\nNote that there are 62476 merges in this history. Also, output size is:\n\nDIFF_FLAG=--cc:            2169111 lines\nDIFF_FLAG=--remerge-diff:  2458020 lines\n\n\nSo roughly the same amount of output as --cc, as you'd expect.\n\nAs a side note: git log --remerge-diff, when run in various repositories and\nallowed to run all the way back to the beginning(s) of history, is a nice\nstress test of sorts for merge-ort. Especially when users run it for you on\ntheir repositories they are working on, whether intentionally or via a bug\nin a tool triggering that command to be run unexpectedly. Long story short,\nsuch a bug in an internal tool existed last December and this command was\nrun on an internal repository and found a platform-specific bug in merge-ort\non some really old merge commit from that repo. I fixed that bug (a\nSTABLE_QSORT thing) while upstreaming all the merge-ort patches in the mean\ntime, but it was nice getting extra testing. Having more folks run this on\ntheir repositories might be useful extra testing of the new merge strategy.\n\nAlso, I previously mentioned --remerge-diff-only (a flag to show how\ncherry-picks or reverts differ from an automatic cherry-pick or revert, in\naddition to showing how merges differ from an automatic merge). This series\ndoes not include the patches to introduce that option; I'll submit them\nlater.\n\nTwo other things that might be interesting but are not included and which I\nhaven't investigated:\n\n * some mechanism for passing extra merge options through (e.g.\n   -Xignore-space-change)\n * a capability to compare the automatic merge to a second automatic merge\n   done with different merge options. (Not sure if this would be of interest\n   to end users, but might be interesting while developing new a\n   --strategy-option, or maybe checking how changing some default in the\n   merge algorithm would affect historical merges in various repositories).\n\n[1] I have nebulous ideas of how an Octopus-centric ORT strategy could be\nwritten -- basically, just repeatedly invoking ort and trying to make sure\nnested conflicts can be differentiated. For now, though, a simple warning is\nprinted that octopus merges are not handled and no diff will be shown. [2]\nNew blobs/trees can be written by the three-way merging step. These are\nwritten to a temporary area (via tmp-objdir.c) under the git object store\nthat is cleaned up at the end of the operation, with the new loose objects\nfrom the remerge being cleaned up after each individual merge.\n\nElijah Newren (9):\n  tmp_objdir: add a helper function for discarding all contained objects\n  ll-merge: make callers responsible for showing warnings\n  merge-ort: capture and print ll-merge warnings in our preferred\n    fashion\n  merge-ort: mark a few more conflict messages as omittable\n  merge-ort: make path_messages available to external callers\n  diff: add ability to insert additional headers for paths\n  merge-ort: format messages slightly different for use in headers\n  show, log: provide a --remerge-diff capability\n  doc/diff-options: explain the new --remerge-diff option\n\n Documentation/diff-options.txt |  8 ++++\n apply.c                        |  5 ++-\n builtin/checkout.c             | 12 ++++--\n builtin/log.c                  | 16 ++++++++\n diff-merges.c                  | 12 ++++++\n diff.c                         | 34 ++++++++++++++++-\n diff.h                         |  1 +\n ll-merge.c                     | 40 ++++++++++---------\n ll-merge.h                     |  9 ++++-\n log-tree.c                     | 70 ++++++++++++++++++++++++++++++++++\n merge-blobs.c                  |  5 ++-\n merge-ort.c                    | 49 +++++++++++++++++++++---\n merge-ort.h                    | 10 +++++\n merge-recursive.c              |  8 +++-\n merge-recursive.h              |  1 +\n notes-merge.c                  |  5 ++-\n rerere.c                       | 10 +++--\n revision.h                     |  6 ++-\n t/t6404-recursive-merge.sh     |  9 ++++-\n t/t6406-merge-attr.sh          |  9 ++++-\n tmp-objdir.c                   |  5 +++\n tmp-objdir.h                   |  6 +++\n 22 files changed, 288 insertions(+), 42 deletions(-)\n\n\nbase-commit: 4e44121c2d7bced65e25eb7ec5156290132bec94\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1103%2Fnewren%2Fremerge-diff-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1103/newren/remerge-diff-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/1103\n-- \ngitgitgadget\n"},{"id":"444637","messageId":"fab1b2c69eafbd3f211745886786c1d0ebdc05c2.1640109948.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH 1/9] tmp_objdir: add a helper function for discarding all contained objects","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:40Z","receivedAt":"2021-12-21T18:05:54Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n tmp-objdir.c | 5 +++++\n tmp-objdir.h | 6 ++++++\n 2 files changed, 11 insertions(+)\n\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex 3d38eeab66b..adf6033549e 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -79,6 +79,11 @@ static void remove_tmp_objdir_on_signal(int signo)\n \traise(signo);\n }\n \n+void tmp_objdir_discard_objects(struct tmp_objdir *t)\n+{\n+\tremove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n+}\n+\n /*\n  * These env_* functions are for setting up the child environment; the\n  * \"replace\" variant overrides the value of any existing variable with that\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex cda5ec76778..76efc7edee5 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -46,6 +46,12 @@ int tmp_objdir_migrate(struct tmp_objdir *);\n  */\n int tmp_objdir_destroy(struct tmp_objdir *);\n \n+/*\n+ * Remove all objects from the temporary object directory, while leaving it\n+ * around so more objects can be added.\n+ */\n+void tmp_objdir_discard_objects(struct tmp_objdir *);\n+\n /*\n  * Add the temporary object directory as an alternate object store in the\n  * current process.\n-- \ngitgitgadget\n\n"},{"id":"444638","messageId":"d022176618d76943743940da0787291d51c9b4f0.1640109948.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH 2/9] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:41Z","receivedAt":"2021-12-21T18:05:56Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince some callers may want to send warning messages to somewhere other\nthan stdout/stderr, stop printing \"warning: Cannot merge binary files\"\nfrom ll-merge and instead modify the return status of ll_merge() to\nindicate when a merge of binary files has occurred.\n\nNote that my methodology included first modifying ll_merge() to return\na struct, so that the compiler would catch all the callers for me and\nensure I had modified all of them.  After modifying all of them, I then\nchanged the struct to an enum.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n apply.c            |  5 ++++-\n builtin/checkout.c | 12 ++++++++----\n ll-merge.c         | 40 ++++++++++++++++++++++------------------\n ll-merge.h         |  9 ++++++++-\n merge-blobs.c      |  5 ++++-\n merge-ort.c        |  5 ++++-\n merge-recursive.c  |  5 ++++-\n notes-merge.c      |  5 ++++-\n rerere.c           | 10 +++++++---\n 9 files changed, 65 insertions(+), 31 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex 43a0aebf4ee..12ea9c72a6b 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -3492,7 +3492,7 @@ static int three_way_merge(struct apply_state *state,\n {\n \tmmfile_t base_file, our_file, their_file;\n \tmmbuffer_t result = { NULL };\n-\tint status;\n+\tenum ll_merge_result status;\n \n \t/* resolve trivial cases first */\n \tif (oideq(base, ours))\n@@ -3509,6 +3509,9 @@ static int three_way_merge(struct apply_state *state,\n \t\t\t  &their_file, \"theirs\",\n \t\t\t  state->repo->index,\n \t\t\t  NULL);\n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\t\"base\", \"ours\", \"theirs\");\n \tfree(base_file.ptr);\n \tfree(our_file.ptr);\n \tfree(their_file.ptr);\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex cbf73b8c9f6..3a559d69303 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -237,6 +237,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tstruct cache_entry *ce = active_cache[pos];\n \tconst char *path = ce->name;\n \tmmfile_t ancestor, ours, theirs;\n+\tenum ll_merge_result merge_status;\n \tint status;\n \tstruct object_id oid;\n \tmmbuffer_t result_buf;\n@@ -267,13 +268,16 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tmemset(&ll_opts, 0, sizeof(ll_opts));\n \tgit_config_get_bool(\"merge.renormalize\", &renormalize);\n \tll_opts.renormalize = renormalize;\n-\tstatus = ll_merge(&result_buf, path, &ancestor, \"base\",\n-\t\t\t  &ours, \"ours\", &theirs, \"theirs\",\n-\t\t\t  state->istate, &ll_opts);\n+\tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n+\t\t\t\t&ours, \"ours\", &theirs, \"theirs\",\n+\t\t\t\tstate->istate, &ll_opts);\n \tfree(ancestor.ptr);\n \tfree(ours.ptr);\n \tfree(theirs.ptr);\n-\tif (status < 0 || !result_buf.ptr) {\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \"ours\", \"theirs\");\n+\tif (merge_status < 0 || !result_buf.ptr) {\n \t\tfree(result_buf.ptr);\n \t\treturn error(_(\"path '%s': cannot merge\"), path);\n \t}\ndiff --git a/ll-merge.c b/ll-merge.c\nindex 261657578c7..669c09eed6c 100644\n--- a/ll-merge.c\n+++ b/ll-merge.c\n@@ -14,7 +14,7 @@\n \n struct ll_merge_driver;\n \n-typedef int (*ll_merge_fn)(const struct ll_merge_driver *,\n+typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -49,7 +49,7 @@ void reset_merge_attributes(void)\n /*\n  * Built-in low-levels\n  */\n-static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -58,6 +58,7 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   const struct ll_merge_options *opts,\n \t\t\t   int marker_size)\n {\n+\tenum ll_merge_result ret;\n \tmmfile_t *stolen;\n \tassert(opts);\n \n@@ -68,16 +69,19 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t */\n \tif (opts->virtual_ancestor) {\n \t\tstolen = orig;\n+\t\tret = LL_MERGE_OK;\n \t} else {\n \t\tswitch (opts->variant) {\n \t\tdefault:\n-\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\t\tpath, name1, name2);\n-\t\t\t/* fallthru */\n+\t\t\tret = LL_MERGE_BINARY_CONFLICT;\n+\t\t\tstolen = src1;\n+\t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_OURS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src1;\n \t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_THEIRS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src2;\n \t\t\tbreak;\n \t\t}\n@@ -87,16 +91,10 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \tresult->size = stolen->size;\n \tstolen->ptr = NULL;\n \n-\t/*\n-\t * With -Xtheirs or -Xours, we have cleanly merged;\n-\t * otherwise we got a conflict.\n-\t */\n-\treturn opts->variant == XDL_MERGE_FAVOR_OURS ||\n-\t       opts->variant == XDL_MERGE_FAVOR_THEIRS ?\n-\t       0 : 1;\n+\treturn ret;\n }\n \n-static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -105,7 +103,9 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tconst struct ll_merge_options *opts,\n \t\t\tint marker_size)\n {\n+\tenum ll_merge_result ret;\n \txmparam_t xmp;\n+\tint status;\n \tassert(opts);\n \n \tif (orig->size > MAX_XDIFF_SIZE ||\n@@ -133,10 +133,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \txmp.ancestor = orig_name;\n \txmp.file1 = name1;\n \txmp.file2 = name2;\n-\treturn xdl_merge(orig, src1, src2, &xmp, result);\n+\tstatus = xdl_merge(orig, src1, src2, &xmp, result);\n+\tret = (status > 1 ) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n-static int ll_union_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_union_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t  mmbuffer_t *result,\n \t\t\t  const char *path,\n \t\t\t  mmfile_t *orig, const char *orig_name,\n@@ -178,7 +180,7 @@ static void create_temp(mmfile_t *src, char *path, size_t len)\n /*\n  * User defined low-level merge driver support.\n  */\n-static int ll_ext_merge(const struct ll_merge_driver *fn,\n+static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -194,6 +196,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \tconst char *args[] = { NULL, NULL };\n \tint status, fd, i;\n \tstruct stat st;\n+\tenum ll_merge_result ret;\n \tassert(opts);\n \n \tsq_quote_buf(&path_sq, path);\n@@ -236,7 +239,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \t\tunlink_or_warn(temp[i]);\n \tstrbuf_release(&cmd);\n \tstrbuf_release(&path_sq);\n-\treturn status;\n+\tret = (status > 1) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n /*\n@@ -362,7 +366,7 @@ static void normalize_file(mmfile_t *mm, const char *path, struct index_state *i\n \t}\n }\n \n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/ll-merge.h b/ll-merge.h\nindex aceb1b24132..e4a20e81a3a 100644\n--- a/ll-merge.h\n+++ b/ll-merge.h\n@@ -82,13 +82,20 @@ struct ll_merge_options {\n \tlong xdl_opts;\n };\n \n+enum ll_merge_result {\n+\tLL_MERGE_ERROR = -1,\n+\tLL_MERGE_OK = 0,\n+\tLL_MERGE_CONFLICT,\n+\tLL_MERGE_BINARY_CONFLICT,\n+};\n+\n /**\n  * Perform a three-way single-file merge in core.  This is a thin wrapper\n  * around `xdl_merge` that takes the path and any merge backend specified in\n  * `.gitattributes` or `.git/info/attributes` into account.\n  * Returns 0 for a clean merge.\n  */\n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/merge-blobs.c b/merge-blobs.c\nindex ee0a0e90c94..8138090f81c 100644\n--- a/merge-blobs.c\n+++ b/merge-blobs.c\n@@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\t mmfile_t *their,\n \t\t\t\t unsigned long *size)\n {\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \tmmbuffer_t res;\n \n \t/*\n@@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\tistate, NULL);\n \tif (merge_status < 0)\n \t\treturn NULL;\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \".our\", \".their\");\n \n \t*size = res.size;\n \treturn res.ptr;\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 0342f104836..c24da2ba3cb 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1743,7 +1743,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tif (!opt->priv->attr_index.initialized)\n \t\tinitialize_attr_index(opt);\n@@ -1787,6 +1787,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex d9457797dbb..bc73c52dd84 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tll_opts.renormalize = opt->renormalize;\n \tll_opts.extra_marker_size = extra_marker_size;\n@@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, a->path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\topt->repo->index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\ta->path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/notes-merge.c b/notes-merge.c\nindex b4a3a903e86..01d596920ea 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n {\n \tmmbuffer_t result_buf;\n \tmmfile_t base, local, remote;\n-\tint status;\n+\tenum ll_merge_result status;\n \n \tread_mmblob(&base, &p->base);\n \tread_mmblob(&local, &p->local);\n@@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n \tfree(local.ptr);\n \tfree(remote.ptr);\n \n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\toid_to_hex(&p->obj), o->local_ref, o->remote_ref);\n \tif ((status < 0) || !result_buf.ptr)\n \t\tdie(\"Failed to execute internal merge\");\n \ndiff --git a/rerere.c b/rerere.c\nindex d83d58df4fb..46fd01819b8 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -609,19 +609,23 @@ static int try_merge(struct index_state *istate,\n \t\t     const struct rerere_id *id, const char *path,\n \t\t     mmfile_t *cur, mmbuffer_t *result)\n {\n-\tint ret;\n+\tenum ll_merge_result ret;\n \tmmfile_t base = {NULL, 0}, other = {NULL, 0};\n \n \tif (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n \t    read_mmfile(&other, rerere_path(id, \"postimage\")))\n-\t\tret = 1;\n-\telse\n+\t\tret = LL_MERGE_CONFLICT;\n+\telse {\n \t\t/*\n \t\t * A three-way merge. Note that this honors user-customizable\n \t\t * low-level merge driver settings.\n \t\t */\n \t\tret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n \t\t\t       istate, NULL);\n+\t\tif (ret == LL_MERGE_BINARY_CONFLICT)\n+\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\t\tpath, \"\", \"\");\n+\t}\n \n \tfree(base.ptr);\n \tfree(other.ptr);\n-- \ngitgitgadget\n\n"},{"id":"444639","messageId":"f36395fdee044a077a640bd3031a7a8da8a39657.1640109948.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH 3/9] merge-ort: capture and print ll-merge warnings in our preferred fashion","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:42Z","receivedAt":"2021-12-21T18:05:57Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nInstead of immediately printing ll-merge warnings to stderr, we save\nthem in our output strbuf.  Besides allowing us to move these warnings\nto a special file for --remerge-diff, this has two other benefits for\nregular merges done by merge-ort:\n\n  * The deferral of messages ensures we can print all messages about\n    any given path together (merge-recursive was known to sometimes\n    intersperse messages about other paths, particularly when renames\n    were involved).\n\n  * The deferral of messages means we can avoid printing spurious\n    conflict messages when we just end up aborting due to local user\n    modifications in the way.  (In contrast to merge-recursive.c which\n    prematurely checks for local modifications in the way via\n    unpack_trees() and gets the check wrong both in terms of false\n    positives and false negatives relative to renames, merge-ort does\n    not perform the local modifications in the way check until the\n    checkout() step after the full merge has been computed.)\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c                | 5 +++--\n t/t6404-recursive-merge.sh | 9 +++++++--\n t/t6406-merge-attr.sh      | 9 +++++++--\n 3 files changed, 17 insertions(+), 6 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex c24da2ba3cb..a18f47e23c5 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1788,8 +1788,9 @@ static int merge_3way(struct merge_options *opt,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n \tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n-\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\tpath, name1, name2);\n+\t\tpath_msg(opt, path, 0,\n+\t\t\t \"warning: Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\t path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/t/t6404-recursive-merge.sh b/t/t6404-recursive-merge.sh\nindex eaf48e941e2..b8735c6db4d 100755\n--- a/t/t6404-recursive-merge.sh\n+++ b/t/t6404-recursive-merge.sh\n@@ -108,8 +108,13 @@ test_expect_success 'refuse to merge binary files' '\n \tprintf \"\\0\\0\" >binary-file &&\n \tgit add binary-file &&\n \tgit commit -m binary2 &&\n-\ttest_must_fail git merge F >merge.out 2>merge.err &&\n-\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge.err\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge F >merge_output\n+\telse\n+\t\ttest_must_fail git merge F 2>merge_output\n+\tfi &&\n+\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge_output\n '\n \n test_expect_success 'mark rename/delete as unmerged' '\ndiff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh\nindex 84946458371..c41584eb33e 100755\n--- a/t/t6406-merge-attr.sh\n+++ b/t/t6406-merge-attr.sh\n@@ -221,8 +221,13 @@ test_expect_success 'binary files with union attribute' '\n \tprintf \"two\\0\" >bin.txt &&\n \tgit commit -am two &&\n \n-\ttest_must_fail git merge bin-main 2>stderr &&\n-\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" stderr\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge bin-main >output\n+\telse\n+\t\ttest_must_fail git merge bin-main 2>output\n+\tfi &&\n+\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" output\n '\n \n test_done\n-- \ngitgitgadget\n\n"},{"id":"444640","messageId":"1e7eef7705e2f7b4a456056e4335d82267680214.1640109948.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH 4/9] merge-ort: mark a few more conflict messages as omittable","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:43Z","receivedAt":"2021-12-21T18:05:59Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\npath_msg() has the ability to mark messages as omittable, designed for\nremerge-diff where we'll instead be showing conflict messages as diff\nheaders for a subsequent diff.  While all these messages are very useful\nwhen trying to create a merge initially, early use with the\n--remerge-diff feature (the only user of this omittable conflict message\ncapability), suggests that the particular messages marked in this commit\nare just noise when trying to see what changes users made to create a\nmerge commit.  Mark them as omittable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex a18f47e23c5..fe27870e73e 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -2420,7 +2420,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t */\n \t\tci->path_conflict = 1;\n \t\tif (pair->status == 'A')\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s added in %s \"\n \t\t\t\t   \"inside a directory that was renamed in %s, \"\n \t\t\t\t   \"suggesting it should perhaps be moved to \"\n@@ -2428,7 +2428,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t\t\t old_path, branch_with_new_path,\n \t\t\t\t branch_with_dir_rename, new_path);\n \t\telse\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s renamed to %s \"\n \t\t\t\t   \"in %s, inside a directory that was renamed \"\n \t\t\t\t   \"in %s, suggesting it should perhaps be \"\n@@ -3825,7 +3825,7 @@ static void process_entry(struct merge_options *opt,\n \t\t\t\treason = _(\"add/add\");\n \t\t\tif (S_ISGITLINK(merged_file.mode))\n \t\t\t\treason = _(\"submodule\");\n-\t\t\tpath_msg(opt, path, 0,\n+\t\t\tpath_msg(opt, path, 1,\n \t\t\t\t _(\"CONFLICT (%s): Merge conflict in %s\"),\n \t\t\t\t reason, path);\n \t\t}\n-- \ngitgitgadget\n\n"},{"id":"444641","messageId":"dd5461d45de99ef50a82297142332c25a54aff82.1640109948.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH 5/9] merge-ort: make path_messages available to external callers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:44Z","receivedAt":"2021-12-21T18:06:00Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nmerge-ort is designed to be more flexible so that it could be called as\nmore of a library function.  Part of that design is not writing to the\nworking tree or index unless and until requested.  Part of it is\nreturning tree objects (rather than creating commits and making them\npart of HEAD), and allowing callers to do their own special thing with\nthat merged tree.  Along the same lines, we want to enable callers to do\nsomething special with output messages (conflicts and other warnings)\nbesides just automatically displaying on stdout/stderr.  Do so by making\nthe output path messages accessible via a new member of struct\nmerge_result named path_messages.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c |  1 +\n merge-ort.h | 10 ++++++++++\n 2 files changed, 11 insertions(+)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex fe27870e73e..c4d6c5c81cc 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -4547,6 +4547,7 @@ redo:\n \ttrace2_region_leave(\"merge\", \"process_entries\", opt->repo);\n \n \t/* Set return values */\n+\tresult->path_messages = &opt->priv->output;\n \tresult->tree = parse_tree_indirect(&working_tree_oid);\n \t/* existence of conflicted entries implies unclean */\n \tresult->clean &= strmap_empty(&opt->priv->conflicted);\ndiff --git a/merge-ort.h b/merge-ort.h\nindex c011864ffeb..fe599b87868 100644\n--- a/merge-ort.h\n+++ b/merge-ort.h\n@@ -5,6 +5,7 @@\n \n struct commit;\n struct tree;\n+struct strmap;\n \n struct merge_result {\n \t/*\n@@ -23,6 +24,15 @@ struct merge_result {\n \t */\n \tstruct tree *tree;\n \n+\t/*\n+\t * Special messages and conflict notices for various paths\n+\t *\n+\t * This is a map of pathnames to strbufs.  It contains various\n+\t * warning/conflict/notice messages (possibly multiple per path)\n+\t * that callers may want to use.\n+\t */\n+\tstruct strmap *path_messages;\n+\n \t/*\n \t * Additional metadata used by merge_switch_to_result() or future calls\n \t * to merge_incore_*().  Includes data needed to update the index (if\n-- \ngitgitgadget\n\n"},{"id":"444642","messageId":"15600df925fb06ecf2c12afecd514f551a1bf7c2.1640109948.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH 6/9] diff: add ability to insert additional headers for paths","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:45Z","receivedAt":"2021-12-21T18:06:01Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nIn support of a remerge-diff ability we will add in a few commits, we\nwant to be able to provide additional headers to show along with a diff.\nAdd the plumbing necessary to enable this.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n diff.c | 34 +++++++++++++++++++++++++++++++++-\n diff.h |  1 +\n 2 files changed, 34 insertions(+), 1 deletion(-)\n\ndiff --git a/diff.c b/diff.c\nindex 861282db1c3..a9490b9b2ba 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -27,6 +27,7 @@\n #include \"help.h\"\n #include \"promisor-remote.h\"\n #include \"dir.h\"\n+#include \"strmap.h\"\n \n #ifdef NO_FAST_WORKING_DIRECTORY\n #define FAST_WORKING_DIRECTORY 0\n@@ -3406,6 +3407,33 @@ struct userdiff_driver *get_textconv(struct repository *r,\n \treturn userdiff_get_textconv(r, one->driver);\n }\n \n+static struct strbuf* additional_headers(struct diff_options *o,\n+\t\t\t\t\t const char *path)\n+{\n+\tif (!o->additional_path_headers)\n+\t\treturn NULL;\n+\treturn strmap_get(o->additional_path_headers, path);\n+}\n+\n+static void add_formatted_headers(struct strbuf *msg,\n+\t\t\t\t  struct strbuf *more_headers,\n+\t\t\t\t  const char *line_prefix,\n+\t\t\t\t  const char *meta,\n+\t\t\t\t  const char *reset)\n+{\n+\tchar *next, *newline;\n+\n+\tnext = more_headers->buf;\n+\twhile ((newline = strchr(next, '\\n'))) {\n+\t\t*newline = '\\0';\n+\t\tstrbuf_addf(msg, \"%s%s%s%s\\n\", line_prefix, meta, next, reset);\n+\t\t*newline = '\\n';\n+\t\tnext = newline + 1;\n+\t}\n+\tif (*next)\n+\t\tstrbuf_addf(msg, \"%s%s%s%s\\n\", line_prefix, meta, next, reset);\n+}\n+\n static void builtin_diff(const char *name_a,\n \t\t\t const char *name_b,\n \t\t\t struct diff_filespec *one,\n@@ -4328,9 +4356,13 @@ static void fill_metainfo(struct strbuf *msg,\n \tconst char *set = diff_get_color(use_color, DIFF_METAINFO);\n \tconst char *reset = diff_get_color(use_color, DIFF_RESET);\n \tconst char *line_prefix = diff_line_prefix(o);\n+\tstruct strbuf *more_headers = NULL;\n \n \t*must_show_header = 1;\n \tstrbuf_init(msg, PATH_MAX * 2 + 300);\n+\tif ((more_headers = additional_headers(o, name)))\n+\t\tadd_formatted_headers(msg, more_headers,\n+\t\t\t\t      line_prefix, set, reset);\n \tswitch (p->status) {\n \tcase DIFF_STATUS_COPIED:\n \t\tstrbuf_addf(msg, \"%s%ssimilarity index %d%%\",\n@@ -5852,7 +5884,7 @@ int diff_unmodified_pair(struct diff_filepair *p)\n \n static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n {\n-\tif (diff_unmodified_pair(p))\n+\tif (diff_unmodified_pair(p) && !additional_headers(o, p->one->path))\n \t\treturn;\n \n \tif ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||\ndiff --git a/diff.h b/diff.h\nindex 8ba85c5e605..289badf5643 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -395,6 +395,7 @@ struct diff_options {\n \n \tstruct repository *repo;\n \tstruct option *parseopts;\n+\tstruct strmap *additional_path_headers;\n \n \tint no_free;\n };\n-- \ngitgitgadget\n\n"},{"id":"444643","messageId":"b307f63569fdd8a546012aef8dd34ed0e5a5fb38.1640109948.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH 7/9] merge-ort: format messages slightly different for use in headers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:46Z","receivedAt":"2021-12-21T18:06:03Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWe want to add an ability for users to run\n    git show --remerge-diff $MERGE_COMMIT\nor even\n    git log -p --remerge-diff ...\nand have git show the differences between where the merge machinery\nwould stop and what is recorded in merge commits.  However, in such\ncases, stdout is not an appropriate location to dump conflict messages.\nWe instead want these messages to appear as headers in the subsequent\ndiff.  For them to work as headers, though, we need for any multiline\nmessages to replace newlines with both a newline and a space.  Add a new\nflag to signal when we want these messages modified in such a fashion,\nand use it in path_msg() to modify these messages this way.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c       | 36 ++++++++++++++++++++++++++++++++++--\n merge-recursive.c |  3 +++\n merge-recursive.h |  1 +\n 3 files changed, 38 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex c4d6c5c81cc..0ae3e4ffa75 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -634,17 +634,46 @@ static void path_msg(struct merge_options *opt,\n \t\t     const char *fmt, ...)\n {\n \tva_list ap;\n-\tstruct strbuf *sb = strmap_get(&opt->priv->output, path);\n+\tstruct strbuf *sb, *dest;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\n+\tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n+\t\treturn; /* Do not record mere hints in tree */\n+\tsb = strmap_get(&opt->priv->output, path);\n \tif (!sb) {\n \t\tsb = xmalloc(sizeof(*sb));\n \t\tstrbuf_init(sb, 0);\n \t\tstrmap_put(&opt->priv->output, path, sb);\n \t}\n \n+\tdest = (opt->record_conflict_msgs_as_headers ? &tmp : sb);\n+\n \tva_start(ap, fmt);\n-\tstrbuf_vaddf(sb, fmt, ap);\n+\tstrbuf_vaddf(dest, fmt, ap);\n \tva_end(ap);\n \n+\tif (opt->record_conflict_msgs_as_headers) {\n+\t\tint i_sb = 0, i_tmp = 0;\n+\n+\t\t/* Copy tmp to sb, adding spaces after newlines */\n+\t\tstrbuf_grow(sb, 2*tmp.len); /* more than sufficient */\n+\t\tfor (; i_tmp < tmp.len; i_tmp++, i_sb++) {\n+\t\t\t/* Copy next character from tmp to sb */\n+\t\t\tsb->buf[sb->len + i_sb] = tmp.buf[i_tmp];\n+\n+\t\t\t/* If we copied a newline, add a space */\n+\t\t\tif (tmp.buf[i_tmp] == '\\n')\n+\t\t\t\tsb->buf[++i_sb] = ' ';\n+\t\t}\n+\t\t/* Update length and ensure it's NUL-terminated */\n+\t\tsb->len += i_sb;\n+\t\tsb->buf[sb->len] = '\\0';\n+\n+\t\t/* Clean up tmp */\n+\t\tstrbuf_release(&tmp);\n+\t}\n+\n+\t/* Add final newline character to sb */\n \tstrbuf_addch(sb, '\\n');\n }\n \n@@ -4246,6 +4275,9 @@ void merge_switch_to_result(struct merge_options *opt,\n \t\tstruct string_list olist = STRING_LIST_INIT_NODUP;\n \t\tint i;\n \n+\t\tif (opt->record_conflict_msgs_as_headers)\n+\t\t\tBUG(\"Either display conflict messages or record them as headers, not both\");\n+\n \t\ttrace2_region_enter(\"merge\", \"display messages\", opt->repo);\n \n \t\t/* Hack to pre-allocate olist to the desired size */\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex bc73c52dd84..c9ba7e904a6 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -3714,6 +3714,9 @@ static int merge_start(struct merge_options *opt, struct tree *head)\n \n \tassert(opt->priv == NULL);\n \n+\t/* Not supported; option specific to merge-ort */\n+\tassert(!opt->record_conflict_msgs_as_headers);\n+\n \t/* Sanity check on repo state; index must match head */\n \tif (repo_index_has_changes(opt->repo, head, &sb)) {\n \t\terr(opt, _(\"Your local changes to the following files would be overwritten by merge:\\n  %s\"),\ndiff --git a/merge-recursive.h b/merge-recursive.h\nindex 0795a1d3ec1..ebfdb7f994e 100644\n--- a/merge-recursive.h\n+++ b/merge-recursive.h\n@@ -46,6 +46,7 @@ struct merge_options {\n \t/* miscellaneous control options */\n \tconst char *subtree_shift;\n \tunsigned renormalize : 1;\n+\tunsigned record_conflict_msgs_as_headers : 1;\n \n \t/* internal fields used by the implementation */\n \tstruct merge_options_internal *priv;\n-- \ngitgitgadget\n\n"},{"id":"444644","messageId":"5d5846be0bd67b8fd850ef2583beef064481a67d.1640109948.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH 8/9] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:47Z","receivedAt":"2021-12-21T18:06:03Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen this option is specified, we remerge all (two parent) merge commits\nand diff the actual merge commit to the automatically created version,\nin order to show how users removed conflict markers, resolved the\ndifferent conflict versions, and potentially added new changes outside\nof conflict regions in order to resolve semantic merge problems (or,\npossibly, just to hide other random changes).\n\nThis capability works by creating a temporary object directory and\nmarking it as the primary object store, so that any blobs or trees\ncreated during the automatic merge, can be easily removed afterwards by\njust deleting all objects from the temporary object directory.  We can\ndo this after handling each merge commit, in order to avoid the need to\nworry about doing `git gc --auto` runs while running `git log\n--remerge-diff`.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/log.c | 16 ++++++++++++\n diff-merges.c | 12 +++++++++\n log-tree.c    | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++\n revision.h    |  6 ++++-\n 4 files changed, 103 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex f75d87e8d7f..2b51d8b6aae 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -35,6 +35,8 @@\n #include \"repository.h\"\n #include \"commit-reach.h\"\n #include \"range-diff.h\"\n+#include \"dir.h\"\n+#include \"tmp-objdir.h\"\n \n #define MAIL_DEFAULT_WRAP 72\n #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n@@ -407,6 +409,13 @@ static int cmd_log_walk(struct rev_info *rev)\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n \n+\tif (rev->remerge_diff) {\n+\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\tif (!rev->remerge_objdir)\n+\t\t\tdie(_(\"unable to create temporary object directory\"));\n+\t\ttmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n+\t}\n+\n \tif (rev->early_output)\n \t\tsetup_early_output();\n \n@@ -449,6 +458,11 @@ static int cmd_log_walk(struct rev_info *rev)\n \trev->diffopt.no_free = 0;\n \tdiff_free(&rev->diffopt);\n \n+\tif (rev->remerge_diff) {\n+\t\ttmp_objdir_destroy(rev->remerge_objdir);\n+\t\trev->remerge_objdir = NULL;\n+\t}\n+\n \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n \t    rev->diffopt.flags.check_failed) {\n \t\treturn 02;\n@@ -1943,6 +1957,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tdie(_(\"--name-status does not make sense\"));\n \tif (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF)\n \t\tdie(_(\"--check does not make sense\"));\n+\tif (rev.remerge_diff)\n+\t\tdie(_(\"--remerge_diff does not make sense\"));\n \n \tif (!use_patch_format &&\n \t\t(!rev.diffopt.output_format ||\ndiff --git a/diff-merges.c b/diff-merges.c\nindex 5060ccd890b..0af4b3f9191 100644\n--- a/diff-merges.c\n+++ b/diff-merges.c\n@@ -17,6 +17,7 @@ static void suppress(struct rev_info *revs)\n \trevs->combined_all_paths = 0;\n \trevs->merges_imply_patch = 0;\n \trevs->merges_need_diff = 0;\n+\trevs->remerge_diff = 0;\n }\n \n static void set_separate(struct rev_info *revs)\n@@ -45,6 +46,12 @@ static void set_dense_combined(struct rev_info *revs)\n \trevs->dense_combined_merges = 1;\n }\n \n+static void set_remerge_diff(struct rev_info *revs)\n+{\n+\tsuppress(revs);\n+\trevs->remerge_diff = 1;\n+}\n+\n static diff_merges_setup_func_t func_by_opt(const char *optarg)\n {\n \tif (!strcmp(optarg, \"off\") || !strcmp(optarg, \"none\"))\n@@ -57,6 +64,8 @@ static diff_merges_setup_func_t func_by_opt(const char *optarg)\n \t\treturn set_combined;\n \telse if (!strcmp(optarg, \"cc\") || !strcmp(optarg, \"dense-combined\"))\n \t\treturn set_dense_combined;\n+\telse if (!strcmp(optarg, \"r\") || !strcmp(optarg, \"remerge\"))\n+\t\treturn set_remerge_diff;\n \telse if (!strcmp(optarg, \"m\") || !strcmp(optarg, \"on\"))\n \t\treturn set_to_default;\n \treturn NULL;\n@@ -110,6 +119,9 @@ int diff_merges_parse_opts(struct rev_info *revs, const char **argv)\n \t} else if (!strcmp(arg, \"--cc\")) {\n \t\tset_dense_combined(revs);\n \t\trevs->merges_imply_patch = 1;\n+\t} else if (!strcmp(arg, \"--remerge-diff\")) {\n+\t\tset_remerge_diff(revs);\n+\t\trevs->merges_imply_patch = 1;\n \t} else if (!strcmp(arg, \"--no-diff-merges\")) {\n \t\tsuppress(revs);\n \t} else if (!strcmp(arg, \"--combined-all-paths\")) {\ndiff --git a/log-tree.c b/log-tree.c\nindex 644893fd8cf..8fef9822a1e 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -1,12 +1,15 @@\n #include \"cache.h\"\n+#include \"commit-reach.h\"\n #include \"config.h\"\n #include \"diff.h\"\n #include \"object-store.h\"\n #include \"repository.h\"\n+#include \"tmp-objdir.h\"\n #include \"commit.h\"\n #include \"tag.h\"\n #include \"graph.h\"\n #include \"log-tree.h\"\n+#include \"merge-ort.h\"\n #include \"reflog-walk.h\"\n #include \"refs.h\"\n #include \"string-list.h\"\n@@ -16,6 +19,7 @@\n #include \"line-log.h\"\n #include \"help.h\"\n #include \"range-diff.h\"\n+#include \"dir.h\"\n \n static struct decoration name_decoration = { \"object names\" };\n static int decoration_loaded;\n@@ -902,6 +906,60 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n \treturn !opt->loginfo;\n }\n \n+static int do_remerge_diff(struct rev_info *opt,\n+\t\t\t   struct commit_list *parents,\n+\t\t\t   struct object_id *oid,\n+\t\t\t   struct commit *commit)\n+{\n+\tstruct merge_options o;\n+\tstruct commit_list *bases;\n+\tstruct merge_result res;\n+\tstruct pretty_print_context ctx = {0};\n+\tstruct strbuf commit1 = STRBUF_INIT;\n+\tstruct strbuf commit2 = STRBUF_INIT;\n+\n+\t/* Setup merge options */\n+\tinit_merge_options(&o, the_repository);\n+\tmemset(&res, 0, sizeof(res));\n+\to.show_rename_progress = 0;\n+\n+\tctx.abbrev = DEFAULT_ABBREV;\n+\tformat_commit_message(parents->item,       \"%h (%s)\", &commit1, &ctx);\n+\tformat_commit_message(parents->next->item, \"%h (%s)\", &commit2, &ctx);\n+\to.branch1 = commit1.buf;\n+\to.branch2 = commit2.buf;\n+\to.record_conflict_msgs_as_headers = 1;\n+\n+\t/* Parse the relevant commits and get the merge bases */\n+\tparse_commit_or_die(parents->item);\n+\tparse_commit_or_die(parents->next->item);\n+\tbases = get_merge_bases(parents->item, parents->next->item);\n+\n+\t/* Re-merge the parents */\n+\tmerge_incore_recursive(&o,\n+\t\t\t       bases, parents->item, parents->next->item,\n+\t\t\t       &res);\n+\n+\t/* Show the diff */\n+\topt->diffopt.additional_path_headers = res.path_messages;\n+\tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n+\tlog_tree_diff_flush(opt);\n+\n+\t/* Cleanup */\n+\topt->diffopt.additional_path_headers = NULL;\n+\tstrbuf_release(&commit1);\n+\tstrbuf_release(&commit2);\n+\tmerge_finalize(&o, &res);\n+\n+\t/* Clean up the temporary object directory */\n+\tif (opt->remerge_objdir != NULL)\n+\t\ttmp_objdir_discard_objects(opt->remerge_objdir);\n+\telse\n+\t\tBUG(\"unable to remove temporary object directory\");\n+\n+\treturn !opt->loginfo;\n+}\n+\n /*\n  * Show the diff of a commit.\n  *\n@@ -936,6 +994,18 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log\n \t}\n \n \tif (is_merge) {\n+\t\tint octopus = (parents->next->next != NULL);\n+\n+\t\tif (opt->remerge_diff) {\n+\t\t\tif (octopus) {\n+\t\t\t\tshow_log(opt);\n+\t\t\t\tfprintf(opt->diffopt.file,\n+\t\t\t\t\t\"diff: warning: Skipping remerge-diff \"\n+\t\t\t\t\t\"for octopus merges.\\n\");\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\treturn do_remerge_diff(opt, parents, oid, commit);\n+\t\t}\n \t\tif (opt->combine_merges)\n \t\t\treturn do_diff_combined(opt, commit);\n \t\tif (opt->separate_merges) {\ndiff --git a/revision.h b/revision.h\nindex 5578bb4720a..44efce3f410 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -195,7 +195,8 @@ struct rev_info {\n \t\t\tcombine_merges:1,\n \t\t\tcombined_all_paths:1,\n \t\t\tdense_combined_merges:1,\n-\t\t\tfirst_parent_merges:1;\n+\t\t\tfirst_parent_merges:1,\n+\t\t\tremerge_diff:1;\n \n \t/* Format info */\n \tint\t\tshow_notes;\n@@ -317,6 +318,9 @@ struct rev_info {\n \n \t/* misc. flags related to '--no-kept-objects' */\n \tunsigned keep_pack_cache_flags;\n+\n+\t/* Location where temporary objects for remerge-diff are written. */\n+\tstruct tmp_objdir *remerge_objdir;\n };\n \n int ref_excluded(struct string_list *, const char *path);\n-- \ngitgitgadget\n\n"},{"id":"444645","messageId":"4f21969e35724ffac6938beb9e089a970f90c41a.1640109949.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH 9/9] doc/diff-options: explain the new --remerge-diff option","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-21T18:05:48Z","receivedAt":"2021-12-21T18:06:04Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n Documentation/diff-options.txt | 8 ++++++++\n 1 file changed, 8 insertions(+)\n\ndiff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\nindex c89d530d3d1..b05f1c9f1c9 100644\n--- a/Documentation/diff-options.txt\n+++ b/Documentation/diff-options.txt\n@@ -64,6 +64,14 @@ ifdef::git-log[]\n \teach of the parents. Separate log entry and diff is generated\n \tfor each parent.\n +\n+--diff-merges=remerge:::\n+--diff-merges=r:::\n+--remerge-diff:::\n+\tWith this option, two-parent merge commits are remerged to\n+\tcreate a temporary tree object -- potentially containing files\n+\twith conflict markers and such.  A diff is then shown between\n+\tthat temporary tree and the actual merge commit.\n++\n --diff-merges=combined:::\n --diff-merges=c:::\n -c:::\n-- \ngitgitgadget\n"},{"id":"444662","messageId":"211221.868rwdr6wc.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"d022176618d76943743940da0787291d51c9b4f0.1640109948.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/9] ll-merge: make callers responsible for showing warnings","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-21T21:19:48Z","receivedAt":"2021-12-21T21:21:28Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n\n> +\tif (status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\t\"base\", \"ours\", \"theirs\");\n\nThis & other messages in the series have warning/BUG etc. starting with\nupper-case.\n"},{"id":"444663","messageId":"211221.864k71r6kz.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"5d5846be0bd67b8fd850ef2583beef064481a67d.1640109948.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 8/9] show, log: provide a --remerge-diff capability","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-21T21:23:41Z","receivedAt":"2021-12-21T21:28:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n\n> +\tif (rev->remerge_diff) {\n> +\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n> +\t\tif (!rev->remerge_objdir)\n> +\t\t\tdie(_(\"unable to create temporary object directory\"));\n\nIt looks like the tmp_objdir_create() API is rather bad about mixing\nerrors that would come with an errno with others, but shouldn't this be\ndie_errno() in the case where it would fail due to a syscall? Even\nbetter would be passing a \"gentle\" to it and have it emit the\nappropriate errors.\n\n> +\tif (rev.remerge_diff)\n> +\t\tdie(_(\"--remerge_diff does not make sense\"));\n\ns/_/-/\n\n\n> +\tstruct merge_options o;\n> +\tstruct commit_list *bases;\n> +\tstruct merge_result res;\n\nnit: could use \"= { 0 }\" instead of memset below.\n\n> +\t/* Re-merge the parents */\n> +\tmerge_incore_recursive(&o,\n> +\t\t\t       bases, parents->item, parents->next->item,\n> +\t\t\t       &res);\n\nstyle: odd not to have arguments that fit on the line on\nthe line, i.e. \"&o, bases, ...\".\n\n> +\t/* Clean up the temporary object directory */\n> +\tif (opt->remerge_objdir != NULL)\n\nstyle: if (!x) not if (x != NULL)\n"},{"id":"444664","messageId":"211221.86zgotprz3.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"4f21969e35724ffac6938beb9e089a970f90c41a.1640109949.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 9/9] doc/diff-options: explain the new --remerge-diff option","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-21T21:28:48Z","receivedAt":"2021-12-21T21:29:09Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  Documentation/diff-options.txt | 8 ++++++++\n>  1 file changed, 8 insertions(+)\n>\n> diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\n> index c89d530d3d1..b05f1c9f1c9 100644\n> --- a/Documentation/diff-options.txt\n> +++ b/Documentation/diff-options.txt\n> @@ -64,6 +64,14 @@ ifdef::git-log[]\n>  \teach of the parents. Separate log entry and diff is generated\n>  \tfor each parent.\n>  +\n> +--diff-merges=remerge:::\n> +--diff-merges=r:::\n> +--remerge-diff:::\n> +\tWith this option, two-parent merge commits are remerged to\n> +\tcreate a temporary tree object -- potentially containing files\n> +\twith conflict markers and such.  A diff is then shown between\n> +\tthat temporary tree and the actual merge commit.\n> ++\n>  --diff-merges=combined:::\n>  --diff-merges=c:::\n>  -c:::\n\nThis & 5/9 would I think be better squashed into their respective \"main\"\npatches.\n"},{"id":"444666","messageId":"CABPp-BHZH=hdEZ=iYQK_Gg+3aXmJj4uv4SZcJB=J4_K=TTKwHQ@mail.gmail.com","threadId":"57133","inReplyTo":"211221.868rwdr6wc.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 2/9] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-21T21:57:30Z","receivedAt":"2021-12-21T21:57:44Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 1:21 PM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n>\n> > +     if (status == LL_MERGE_BINARY_CONFLICT)\n> > +             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > +                     \"base\", \"ours\", \"theirs\");\n>\n> This & other messages in the series have warning/BUG etc. starting with\n> upper-case.\n\nYes, but I'm not introducing a new message here; I'm merely moving an\nexisting one.  It's important to me that readers of this patch be able\nto verify that I have made no functional changes in this patch, so\nfixing the case should definitely be a different patch from this one.\nI kind of think that fixing the case distracts a bit from the point of\nthe series, and the series is already kind of long, but do you feel\nstrongly that I should fix the case with a new patch inserted into the\nseries?\n"},{"id":"444667","messageId":"CABPp-BFuZbLKLf8VnNxLAuF0NaSg=2Nut++YxhHsCxr3n1jwJg@mail.gmail.com","threadId":"57133","inReplyTo":"211221.864k71r6kz.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 8/9] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-21T22:18:26Z","receivedAt":"2021-12-21T22:18:40Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 1:28 PM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n>\n> On Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n>\n> > +     if (rev->remerge_diff) {\n> > +             rev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n> > +             if (!rev->remerge_objdir)\n> > +                     die(_(\"unable to create temporary object directory\"));\n>\n> It looks like the tmp_objdir_create() API is rather bad about mixing\n> errors that would come with an errno with others, but shouldn't this be\n> die_errno() in the case where it would fail due to a syscall? Even\n> better would be passing a \"gentle\" to it and have it emit the\n> appropriate errors.\n\nI can switch to die_errno().\n\n>\n> > +     if (rev.remerge_diff)\n> > +             die(_(\"--remerge_diff does not make sense\"));\n>\n> s/_/-/\n\nIndeed, thanks.\n\n> > +     struct merge_options o;\n> > +     struct commit_list *bases;\n> > +     struct merge_result res;\n>\n> nit: could use \"= { 0 }\" instead of memset below.\n\nSure, I can make that change.\n\n> > +     /* Re-merge the parents */\n> > +     merge_incore_recursive(&o,\n> > +                            bases, parents->item, parents->next->item,\n> > +                            &res);\n>\n> style: odd not to have arguments that fit on the line on\n> the line, i.e. \"&o, bases, ...\".\n\nYes, but this groups all the ancestors so nicely as opposed to the\ntypical happenstance of whatever fits on a line.  ;-)\n\n> > +     /* Clean up the temporary object directory */\n> > +     if (opt->remerge_objdir != NULL)\n>\n> style: if (!x) not if (x != NULL)\n\nOk, will change.\n"},{"id":"444668","messageId":"CABPp-BHDJMecupic=ybVhzZbRC5=WmY616JQdbfaqw_1A5sMHA@mail.gmail.com","threadId":"57133","inReplyTo":"211221.86zgotprz3.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 9/9] doc/diff-options: explain the new --remerge-diff option","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-21T22:24:14Z","receivedAt":"2021-12-21T22:24:29Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 1:29 PM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n>\n> On Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  Documentation/diff-options.txt | 8 ++++++++\n> >  1 file changed, 8 insertions(+)\n> >\n> > diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\n> > index c89d530d3d1..b05f1c9f1c9 100644\n> > --- a/Documentation/diff-options.txt\n> > +++ b/Documentation/diff-options.txt\n> > @@ -64,6 +64,14 @@ ifdef::git-log[]\n> >       each of the parents. Separate log entry and diff is generated\n> >       for each parent.\n> >  +\n> > +--diff-merges=remerge:::\n> > +--diff-merges=r:::\n> > +--remerge-diff:::\n> > +     With this option, two-parent merge commits are remerged to\n> > +     create a temporary tree object -- potentially containing files\n> > +     with conflict markers and such.  A diff is then shown between\n> > +     that temporary tree and the actual merge commit.\n> > ++\n> >  --diff-merges=combined:::\n> >  --diff-merges=c:::\n> >  -c:::\n>\n> This & 5/9 would I think be better squashed into their respective \"main\"\n> patches.\n\nI presume you mean the \"main\" patch for this one is 8/9.  I was trying\nto find a way to break up that large patch, but this is pretty small\nso...sure I'll squash it in.\n\nWhat are you referring to as the \"main\" patch for 5/9, though?  It\nonly seems related to 6/9 and 7/9 to me, but I very deliberately split\nthose patches off and don't want to confuse them with unrelated\nchanges.  I disagree with combining 5/9 with either of those.\n"},{"id":"444669","messageId":"211222.86v8zhpnm5.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"CABPp-BHZH=hdEZ=iYQK_Gg+3aXmJj4uv4SZcJB=J4_K=TTKwHQ@mail.gmail.com","subject":"Re: [PATCH 2/9] ll-merge: make callers responsible for showing warnings","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-21T23:02:54Z","receivedAt":"2021-12-21T23:03:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Dec 21 2021, Elijah Newren wrote:\n\n> On Tue, Dec 21, 2021 at 1:21 PM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n>>\n>> On Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n>>\n>> > From: Elijah Newren <newren@gmail.com>\n>>\n>> > +     if (status == LL_MERGE_BINARY_CONFLICT)\n>> > +             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n>> > +                     \"base\", \"ours\", \"theirs\");\n>>\n>> This & other messages in the series have warning/BUG etc. starting with\n>> upper-case.\n>\n> Yes, but I'm not introducing a new message here; I'm merely moving an\n> existing one.  It's important to me that readers of this patch be able\n> to verify that I have made no functional changes in this patch, so\n> fixing the case should definitely be a different patch from this one.\n> I kind of think that fixing the case distracts a bit from the point of\n> the series, and the series is already kind of long, but do you feel\n> strongly that I should fix the case with a new patch inserted into the\n> series?\n\nI just missed the bit where it was moved from below in the diff. Sorry\nabout the noise.\n"},{"id":"444671","messageId":"CABPp-BEh1y3X9GcEghEBNd_t+wog0ayXgD-Bftc5zA-nidPhZw@mail.gmail.com","threadId":"57133","inReplyTo":"211222.86v8zhpnm5.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 2/9] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-21T23:15:41Z","receivedAt":"2021-12-21T23:15:56Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 3:03 PM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Tue, Dec 21 2021, Elijah Newren wrote:\n>\n> > On Tue, Dec 21, 2021 at 1:21 PM Ævar Arnfjörð Bjarmason\n> > <avarab@gmail.com> wrote:\n> >>\n> >> On Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n> >>\n> >> > From: Elijah Newren <newren@gmail.com>\n> >>\n> >> > +     if (status == LL_MERGE_BINARY_CONFLICT)\n> >> > +             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> >> > +                     \"base\", \"ours\", \"theirs\");\n> >>\n> >> This & other messages in the series have warning/BUG etc. starting with\n> >> upper-case.\n> >\n> > Yes, but I'm not introducing a new message here; I'm merely moving an\n> > existing one.  It's important to me that readers of this patch be able\n> > to verify that I have made no functional changes in this patch, so\n> > fixing the case should definitely be a different patch from this one.\n> > I kind of think that fixing the case distracts a bit from the point of\n> > the series, and the series is already kind of long, but do you feel\n> > strongly that I should fix the case with a new patch inserted into the\n> > series?\n>\n> I just missed the bit where it was moved from below in the diff. Sorry\n> about the noise.\n\nNah, no worries; thanks for taking a look at the patches!\n"},{"id":"444672","messageId":"xmqqsfulo88n.fsf@gitster.g","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"Re: [PATCH 0/9] Add a new --remerge-diff capability to show & log","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-21T23:20:40Z","receivedAt":"2021-12-21T23:20:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Here are some patches to add a --remerge-diff capability to show & log,\n> which works by comparing merge commits to an automatic remerge (note that\n> the automatic remerge tree can contain files with conflict markers).\n>\n> Changes since original submission[1]:\n>\n>  * Rebased on top of the version of ns/tmp-objdir that Neeraj submitted\n>    (Neeraj's patches were based on v2.34, but ns/tmp-objdir got applied on\n>    an old commit and does not even build because of that).\n\nOh, that's bad.  I wish people do not rebase their updates on top of\nnewer 'master' only for the sake of it, once an older version is\nqueued.\n\n>  * Modify ll-merge API to return a status, instead of printing \"Cannot merge\n>    binary files\" on stdout[2] (as suggested by Peff)\n\nI wondered if we want to do the same for other error messages to\ngive callers greater control, but this change by itself already\nlooks quite good.\n\n>  * Make conflict messages and other such warnings into diff headers of the\n>    subsequent remerge-diff rather than appearing in the diff as file content\n>    of some funny looking filenames (as suggested by Peff[3] and Junio[4])\n\nOK.\n\n>  * Sergey ack'ed the diff-merges.c portion of the patches, but that wasn't\n>    limited to one patch so not sure where to record that ack.\n\nOn that single patch?\n\n"},{"id":"444674","messageId":"xmqqmtkto7yf.fsf@gitster.g","threadId":"57133","inReplyTo":"fab1b2c69eafbd3f211745886786c1d0ebdc05c2.1640109948.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/9] tmp_objdir: add a helper function for discarding all contained objects","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-21T23:26:48Z","receivedAt":"2021-12-21T23:26:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  tmp-objdir.c | 5 +++++\n>  tmp-objdir.h | 6 ++++++\n>  2 files changed, 11 insertions(+)\n>\n> diff --git a/tmp-objdir.c b/tmp-objdir.c\n> index 3d38eeab66b..adf6033549e 100644\n> --- a/tmp-objdir.c\n> +++ b/tmp-objdir.c\n> @@ -79,6 +79,11 @@ static void remove_tmp_objdir_on_signal(int signo)\n>  \traise(signo);\n>  }\n>  \n> +void tmp_objdir_discard_objects(struct tmp_objdir *t)\n> +{\n> +\tremove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n> +}\n> +\n\nOK.\n\nWithout a caller, it is a bit hard to judge if a separate helper\nmakes the caller easier to read and understand, or becomes an extra\nlayer of abstraction that obscures the logic.  Hopefully, having a\nmore specific function name with \"tmp\" and \"discard\" in it makes the\nintent at callers more clear than the function that is named after\nthe detail of the operation.\n\n> +/*\n> + * Remove all objects from the temporary object directory, while leaving it\n> + * around so more objects can be added.\n> + */\n> +void tmp_objdir_discard_objects(struct tmp_objdir *);\n> +\n>  /*\n>   * Add the temporary object directory as an alternate object store in the\n>   * current process.\n"},{"id":"444678","messageId":"CABPp-BFoOzQ7Er4BUKUj0mqHkAO0ePCdaFhiv4i10oURZi=Rvg@mail.gmail.com","threadId":"57133","inReplyTo":"xmqqsfulo88n.fsf@gitster.g","subject":"Re: [PATCH 0/9] Add a new --remerge-diff capability to show & log","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-21T23:43:50Z","receivedAt":"2021-12-21T23:44:04Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 3:20 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > Here are some patches to add a --remerge-diff capability to show & log,\n> > which works by comparing merge commits to an automatic remerge (note that\n> > the automatic remerge tree can contain files with conflict markers).\n> >\n> > Changes since original submission[1]:\n> >\n> >  * Rebased on top of the version of ns/tmp-objdir that Neeraj submitted\n> >    (Neeraj's patches were based on v2.34, but ns/tmp-objdir got applied on\n> >    an old commit and does not even build because of that).\n>\n> Oh, that's bad.  I wish people do not rebase their updates on top of\n> newer 'master' only for the sake of it, once an older version is\n> queued.\n>\n> >  * Modify ll-merge API to return a status, instead of printing \"Cannot merge\n> >    binary files\" on stdout[2] (as suggested by Peff)\n>\n> I wondered if we want to do the same for other error messages to\n> give callers greater control, but this change by itself already\n> looks quite good.\n>\n> >  * Make conflict messages and other such warnings into diff headers of the\n> >    subsequent remerge-diff rather than appearing in the diff as file content\n> >    of some funny looking filenames (as suggested by Peff[3] and Junio[4])\n>\n> OK.\n>\n> >  * Sergey ack'ed the diff-merges.c portion of the patches, but that wasn't\n> >    limited to one patch so not sure where to record that ack.\n>\n> On that single patch?\n\nYeah, the main patch near the end that changed 4 files; he acked the\nchanges to just one of those files in that patch.\n"},{"id":"444679","messageId":"xmqqa6gto74z.fsf@gitster.g","threadId":"57133","inReplyTo":"d022176618d76943743940da0787291d51c9b4f0.1640109948.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/9] ll-merge: make callers responsible for showing warnings","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-21T23:44:28Z","receivedAt":"2021-12-21T23:44:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Since some callers may want to send warning messages to somewhere other\n> than stdout/stderr, stop printing \"warning: Cannot merge binary files\"\n> from ll-merge and instead modify the return status of ll_merge() to\n> indicate when a merge of binary files has occurred.\n>\n> Note that my methodology included first modifying ll_merge() to return\n> a struct, so that the compiler would catch all the callers for me and\n> ensure I had modified all of them.  After modifying all of them, I then\n> changed the struct to an enum.\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  apply.c            |  5 ++++-\n>  builtin/checkout.c | 12 ++++++++----\n>  ll-merge.c         | 40 ++++++++++++++++++++++------------------\n>  ll-merge.h         |  9 ++++++++-\n>  merge-blobs.c      |  5 ++++-\n>  merge-ort.c        |  5 ++++-\n>  merge-recursive.c  |  5 ++++-\n>  notes-merge.c      |  5 ++++-\n>  rerere.c           | 10 +++++++---\n>  9 files changed, 65 insertions(+), 31 deletions(-)\n>\n> diff --git a/apply.c b/apply.c\n> index 43a0aebf4ee..12ea9c72a6b 100644\n> --- a/apply.c\n> +++ b/apply.c\n> @@ -3492,7 +3492,7 @@ static int three_way_merge(struct apply_state *state,\n>  {\n>  \tmmfile_t base_file, our_file, their_file;\n>  \tmmbuffer_t result = { NULL };\n> -\tint status;\n> +\tenum ll_merge_result status;\n>  \n>  \t/* resolve trivial cases first */\n>  \tif (oideq(base, ours))\n> @@ -3509,6 +3509,9 @@ static int three_way_merge(struct apply_state *state,\n>  \t\t\t  &their_file, \"theirs\",\n>  \t\t\t  state->repo->index,\n>  \t\t\t  NULL);\n> +\tif (status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\t\"base\", \"ours\", \"theirs\");\n\nThis used to come from ll_merge()\n\n> -\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> -\t\t\t\tpath, name1, name2);\n> -\t\t\t/* fallthru */\n\nAnd our call to ll_merge() above (half of it invisible in the\npre-context of the hunk) gave \"ours\" and \"theirs\" to our_label and\ntheir_label, which in turn are called name1 and name2, respectively,\nin ll_merge_binary() driver.\n\nI am not sure about the \"base\" string, though.  I suspect that your\n\"base\" should be a reference to the parameter 'path' of three_way_merge()\nfunction.\n\n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index cbf73b8c9f6..3a559d69303 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -237,6 +237,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n>  \tstruct cache_entry *ce = active_cache[pos];\n>  \tconst char *path = ce->name;\n>  \tmmfile_t ancestor, ours, theirs;\n> +\tenum ll_merge_result merge_status;\n>  \tint status;\n>  \tstruct object_id oid;\n>  \tmmbuffer_t result_buf;\n> @@ -267,13 +268,16 @@ static int checkout_merged(int pos, const struct checkout *state,\n>  \tmemset(&ll_opts, 0, sizeof(ll_opts));\n>  \tgit_config_get_bool(\"merge.renormalize\", &renormalize);\n>  \tll_opts.renormalize = renormalize;\n> -\tstatus = ll_merge(&result_buf, path, &ancestor, \"base\",\n> -\t\t\t  &ours, \"ours\", &theirs, \"theirs\",\n> -\t\t\t  state->istate, &ll_opts);\n> +\tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n> +\t\t\t\t&ours, \"ours\", &theirs, \"theirs\",\n> +\t\t\t\tstate->istate, &ll_opts);\n>  \tfree(ancestor.ptr);\n>  \tfree(ours.ptr);\n>  \tfree(theirs.ptr);\n> -\tif (status < 0 || !result_buf.ptr) {\n> +\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\tpath, \"ours\", \"theirs\");\n\nThis one looks correct.\n\n> +\tif (merge_status < 0 || !result_buf.ptr) {\n>  \t\tfree(result_buf.ptr);\n>  \t\treturn error(_(\"path '%s': cannot merge\"), path);\n>  \t}\n\n> diff --git a/merge-blobs.c b/merge-blobs.c\n> index ee0a0e90c94..8138090f81c 100644\n> --- a/merge-blobs.c\n> +++ b/merge-blobs.c\n> @@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate,\n>  \t\t\t\t mmfile_t *their,\n>  \t\t\t\t unsigned long *size)\n>  {\n> -\tint merge_status;\n> +\tenum ll_merge_result merge_status;\n>  \tmmbuffer_t res;\n>  \n>  \t/*\n> @@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate,\n>  \t\t\t\tistate, NULL);\n>  \tif (merge_status < 0)\n>  \t\treturn NULL;\n> +\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\tpath, \".our\", \".their\");\n\nOK.\n\n> diff --git a/merge-ort.c b/merge-ort.c\n> index 0342f104836..c24da2ba3cb 100644\n> --- a/merge-ort.c\n> +++ b/merge-ort.c\n> @@ -1743,7 +1743,7 @@ static int merge_3way(struct merge_options *opt,\n>  \tmmfile_t orig, src1, src2;\n>  \tstruct ll_merge_options ll_opts = {0};\n>  \tchar *base, *name1, *name2;\n> -\tint merge_status;\n> +\tenum ll_merge_result merge_status;\n>  \n>  \tif (!opt->priv->attr_index.initialized)\n>  \t\tinitialize_attr_index(opt);\n> @@ -1787,6 +1787,9 @@ static int merge_3way(struct merge_options *opt,\n>  \tmerge_status = ll_merge(result_buf, path, &orig, base,\n>  \t\t\t\t&src1, name1, &src2, name2,\n>  \t\t\t\t&opt->priv->attr_index, &ll_opts);\n> +\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\tpath, name1, name2);\n\nOK; this is your code and I do not have to read it too carefully,\nbut all we need is conveniently in the pre-context of the hunk ;-).\n\n> diff --git a/merge-recursive.c b/merge-recursive.c\n> index d9457797dbb..bc73c52dd84 100644\n> --- a/merge-recursive.c\n> +++ b/merge-recursive.c\n> @@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt,\n>  \tmmfile_t orig, src1, src2;\n>  \tstruct ll_merge_options ll_opts = {0};\n>  \tchar *base, *name1, *name2;\n> -\tint merge_status;\n> +\tenum ll_merge_result merge_status;\n>  \n>  \tll_opts.renormalize = opt->renormalize;\n>  \tll_opts.extra_marker_size = extra_marker_size;\n> @@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt,\n>  \tmerge_status = ll_merge(result_buf, a->path, &orig, base,\n>  \t\t\t\t&src1, name1, &src2, name2,\n>  \t\t\t\topt->repo->index, &ll_opts);\n> +\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\ta->path, name1, name2);\n\nOK.\n\n> diff --git a/notes-merge.c b/notes-merge.c\n> index b4a3a903e86..01d596920ea 100644\n> --- a/notes-merge.c\n> +++ b/notes-merge.c\n> @@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n>  {\n>  \tmmbuffer_t result_buf;\n>  \tmmfile_t base, local, remote;\n> -\tint status;\n> +\tenum ll_merge_result status;\n>  \n>  \tread_mmblob(&base, &p->base);\n>  \tread_mmblob(&local, &p->local);\n> @@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n>  \tfree(local.ptr);\n>  \tfree(remote.ptr);\n>  \n> +\tif (status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\toid_to_hex(&p->obj), o->local_ref, o->remote_ref);\n\nThis uses another slot in the rotating buffer used by oid_to_hex(),\nbut I do not think anybody grabbed a pointer to one of them and held\nonto it before we got here, so it would be OK.\n\n> diff --git a/rerere.c b/rerere.c\n> index d83d58df4fb..46fd01819b8 100644\n> --- a/rerere.c\n> +++ b/rerere.c\n> @@ -609,19 +609,23 @@ static int try_merge(struct index_state *istate,\n>  \t\t     const struct rerere_id *id, const char *path,\n>  \t\t     mmfile_t *cur, mmbuffer_t *result)\n>  {\n> -\tint ret;\n> +\tenum ll_merge_result ret;\n>  \tmmfile_t base = {NULL, 0}, other = {NULL, 0};\n>  \n>  \tif (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n>  \t    read_mmfile(&other, rerere_path(id, \"postimage\")))\n> -\t\tret = 1;\n> -\telse\n> +\t\tret = LL_MERGE_CONFLICT;\n> +\telse {\n\nLet's have {} around the if clause now the corresponding else clause\nneeds it.\n\n>  \t\t/*\n>  \t\t * A three-way merge. Note that this honors user-customizable\n>  \t\t * low-level merge driver settings.\n>  \t\t */\n>  \t\tret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n>  \t\t\t       istate, NULL);\n> +\t\tif (ret == LL_MERGE_BINARY_CONFLICT)\n> +\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\t\tpath, \"\", \"\");\n> +\t}\n\nThis is a faithful conversion of what should not happen in practice,\nas the rerere logic would not be able to reach here.  In a binary\nfile, we won't be able to identify <<< === >>> blocks, hash the text\nin the conflicted block to come up with the conflict ID to find the\npreimage and postimage files.  These files are the input to the low\nlevel merge driver call we are making here.\n\nLooking almost good except for a warning message bug I spotted\nearlier.\n\nThanks.\n"},{"id":"444680","messageId":"CABPp-BEuzorQkesgyNCvS05wsfqaH4Yv5JDFgy+AenrAC+JJNQ@mail.gmail.com","threadId":"57133","inReplyTo":"xmqqmtkto7yf.fsf@gitster.g","subject":"Re: [PATCH 1/9] tmp_objdir: add a helper function for discarding all contained objects","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-21T23:51:35Z","receivedAt":"2021-12-21T23:51:50Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 3:26 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  tmp-objdir.c | 5 +++++\n> >  tmp-objdir.h | 6 ++++++\n> >  2 files changed, 11 insertions(+)\n> >\n> > diff --git a/tmp-objdir.c b/tmp-objdir.c\n> > index 3d38eeab66b..adf6033549e 100644\n> > --- a/tmp-objdir.c\n> > +++ b/tmp-objdir.c\n> > @@ -79,6 +79,11 @@ static void remove_tmp_objdir_on_signal(int signo)\n> >       raise(signo);\n> >  }\n> >\n> > +void tmp_objdir_discard_objects(struct tmp_objdir *t)\n> > +{\n> > +     remove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n> > +}\n> > +\n>\n> OK.\n>\n> Without a caller, it is a bit hard to judge if a separate helper\n> makes the caller easier to read and understand, or becomes an extra\n> layer of abstraction that obscures the logic.  Hopefully, having a\n> more specific function name with \"tmp\" and \"discard\" in it makes the\n> intent at callers more clear than the function that is named after\n> the detail of the operation.\n\nThis isn't just a convenience; since tmp_objdir is defined in\ntmp-objdir.c rather than tmp-objdir.h, t->path is not accessible\noutside of tmp-objdir.c.  Because of this, some kind of helper\nfunction is necessary.\n"},{"id":"444681","messageId":"xmqq5yrho6f3.fsf@gitster.g","threadId":"57133","inReplyTo":"f36395fdee044a077a640bd3031a7a8da8a39657.1640109948.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 3/9] merge-ort: capture and print ll-merge warnings in our preferred fashion","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-22T00:00:00Z","receivedAt":"2021-12-22T00:00:11Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Instead of immediately printing ll-merge warnings to stderr, we save\n> them in our output strbuf.  Besides allowing us to move these warnings\n> to a special file for --remerge-diff, this has two other benefits for\n> regular merges done by merge-ort:\n>\n>   * The deferral of messages ensures we can print all messages about\n>     any given path together (merge-recursive was known to sometimes\n>     intersperse messages about other paths, particularly when renames\n>     were involved).\n\nI would imagine that with something like this, we can show such a\nwarning message differently when it happens during an inner\n\"synthesizing a virtual common ancestor\" merge (the most likely\nvalue for \"show differently\" would be to \"squelch\"), which may be a\ngood thing.\n\n>  \tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n> -\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> -\t\t\tpath, name1, name2);\n> +\t\tpath_msg(opt, path, 0,\n> +\t\t\t \"warning: Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\t path, name1, name2);\n>  \n\nNice.\n"},{"id":"444682","messageId":"xmqq1r25o651.fsf@gitster.g","threadId":"57133","inReplyTo":"1e7eef7705e2f7b4a456056e4335d82267680214.1640109948.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 4/9] merge-ort: mark a few more conflict messages as omittable","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-22T00:06:02Z","receivedAt":"2021-12-22T00:06:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> path_msg() has the ability to mark messages as omittable, designed for\n> remerge-diff where we'll instead be showing conflict messages as diff\n> headers for a subsequent diff.  While all these messages are very useful\n> when trying to create a merge initially, early use with the\n> --remerge-diff feature (the only user of this omittable conflict message\n> capability), suggests that the particular messages marked in this commit\n> are just noise when trying to see what changes users made to create a\n> merge commit.\n\nIt is likely because when somebody is looking at the output of\nremerge-diff, they are mostly concentrating on the _content_ level\nmerges and they are not keenly looking for a merge whose result is\ndeposited at a wrong path.  Since what is shown is something that\nhas already recorded in the history, we can safely assume that it is\nno longer a relevant (or \"it is way too late to matter\"), I would\nsay, to show these messages about \"file location\".\n\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  merge-ort.c | 6 +++---\n>  1 file changed, 3 insertions(+), 3 deletions(-)\n>\n> diff --git a/merge-ort.c b/merge-ort.c\n> index a18f47e23c5..fe27870e73e 100644\n> --- a/merge-ort.c\n> +++ b/merge-ort.c\n> @@ -2420,7 +2420,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n>  \t\t */\n>  \t\tci->path_conflict = 1;\n>  \t\tif (pair->status == 'A')\n> -\t\t\tpath_msg(opt, new_path, 0,\n> +\t\t\tpath_msg(opt, new_path, 1,\n>  \t\t\t\t _(\"CONFLICT (file location): %s added in %s \"\n>  \t\t\t\t   \"inside a directory that was renamed in %s, \"\n>  \t\t\t\t   \"suggesting it should perhaps be moved to \"\n> @@ -2428,7 +2428,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n>  \t\t\t\t old_path, branch_with_new_path,\n>  \t\t\t\t branch_with_dir_rename, new_path);\n>  \t\telse\n> -\t\t\tpath_msg(opt, new_path, 0,\n> +\t\t\tpath_msg(opt, new_path, 1,\n>  \t\t\t\t _(\"CONFLICT (file location): %s renamed to %s \"\n>  \t\t\t\t   \"in %s, inside a directory that was renamed \"\n>  \t\t\t\t   \"in %s, suggesting it should perhaps be \"\n> @@ -3825,7 +3825,7 @@ static void process_entry(struct merge_options *opt,\n>  \t\t\t\treason = _(\"add/add\");\n>  \t\t\tif (S_ISGITLINK(merged_file.mode))\n>  \t\t\t\treason = _(\"submodule\");\n> -\t\t\tpath_msg(opt, path, 0,\n> +\t\t\tpath_msg(opt, path, 1,\n>  \t\t\t\t _(\"CONFLICT (%s): Merge conflict in %s\"),\n>  \t\t\t\t reason, path);\n\nI am not as sure about this one as the other two, though.  I guess\nin the context of remerge-diff, resolving the add/add conflict into\nthe same file is also something that happened long time ago and\nthese messages are too late to matter the same way as the other two.\n\nOK.\n\n\n"},{"id":"444687","messageId":"xmqqlf0dmqp5.fsf@gitster.g","threadId":"57133","inReplyTo":"15600df925fb06ecf2c12afecd514f551a1bf7c2.1640109948.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 6/9] diff: add ability to insert additional headers for paths","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-22T00:24:54Z","receivedAt":"2021-12-22T00:24:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> In support of a remerge-diff ability we will add in a few commits, we\n> want to be able to provide additional headers to show along with a diff.\n> Add the plumbing necessary to enable this.\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  diff.c | 34 +++++++++++++++++++++++++++++++++-\n>  diff.h |  1 +\n>  2 files changed, 34 insertions(+), 1 deletion(-)\n>\n> diff --git a/diff.c b/diff.c\n> index 861282db1c3..a9490b9b2ba 100644\n> --- a/diff.c\n> +++ b/diff.c\n> @@ -27,6 +27,7 @@\n>  #include \"help.h\"\n>  #include \"promisor-remote.h\"\n>  #include \"dir.h\"\n> +#include \"strmap.h\"\n>  \n>  #ifdef NO_FAST_WORKING_DIRECTORY\n>  #define FAST_WORKING_DIRECTORY 0\n> @@ -3406,6 +3407,33 @@ struct userdiff_driver *get_textconv(struct repository *r,\n>  \treturn userdiff_get_textconv(r, one->driver);\n>  }\n>  \n> +static struct strbuf* additional_headers(struct diff_options *o,\n\nStyle.\n\n> +\t\t\t\t\t const char *path)\n> +{\n> +\tif (!o->additional_path_headers)\n> +\t\treturn NULL;\n> +\treturn strmap_get(o->additional_path_headers, path);\n> +}\n> +\n> +static void add_formatted_headers(struct strbuf *msg,\n> +\t\t\t\t  struct strbuf *more_headers,\n> +\t\t\t\t  const char *line_prefix,\n> +\t\t\t\t  const char *meta,\n> +\t\t\t\t  const char *reset)\n> +{\n> +\tchar *next, *newline;\n> +\n> +\tnext = more_headers->buf;\n> +\twhile ((newline = strchr(next, '\\n'))) {\n> +\t\t*newline = '\\0';\n> +\t\tstrbuf_addf(msg, \"%s%s%s%s\\n\", line_prefix, meta, next, reset);\n> +\t\t*newline = '\\n';\n> +\t\tnext = newline + 1;\n> +\t}\n\nThe above is not wrong per-se, but we do not need to do the\n\"temporarily terminate and then recover\" dance, and avoiding it\nwould make the code cleaner.\n\nOnce you learn the value of \"newline\" [*], you know the number of\nbytes between \"next\" and \"newline\" so you can use safely \"%.*s\"\nformat specifier without temporarily terminating the subsection of\nthe string.\n\n\tSide note. I would actually use strchrnul() instead, so that\n        we do not have to special case the end of the buffer.  For a\n        readily available example, see advice.c::vadvise().\n\n> +\tif (*next)\n> +\t\tstrbuf_addf(msg, \"%s%s%s%s\\n\", line_prefix, meta, next, reset);\n> +}\n\n> @@ -4328,9 +4356,13 @@ static void fill_metainfo(struct strbuf *msg,\n>  \tconst char *set = diff_get_color(use_color, DIFF_METAINFO);\n>  \tconst char *reset = diff_get_color(use_color, DIFF_RESET);\n>  \tconst char *line_prefix = diff_line_prefix(o);\n> +\tstruct strbuf *more_headers = NULL;\n>  \n>  \t*must_show_header = 1;\n>  \tstrbuf_init(msg, PATH_MAX * 2 + 300);\n> +\tif ((more_headers = additional_headers(o, name)))\n> +\t\tadd_formatted_headers(msg, more_headers,\n> +\t\t\t\t      line_prefix, set, reset);\n\nSo, we stuff what came via path_msg() without anything that allows\nreaders to identify them to the header part?  Just like we have\nfixed and known string taken from a bounded vocabulary such as\n\"index\", \"copy from\", \"old mode\", etc., don't we want to prefix the\nhints that came from the merge machinery with some identifiable\nstring?\n\n> @@ -5852,7 +5884,7 @@ int diff_unmodified_pair(struct diff_filepair *p)\n>  \n>  static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n>  {\n> -\tif (diff_unmodified_pair(p))\n> +\tif (diff_unmodified_pair(p) && !additional_headers(o, p->one->path))\n>  \t\treturn;\n\nThis does not feel quite right.  At least there needs a comment that\nsays the _current_ callers that add additional_headers() would do so\nonly for paths that the end-users cares about, even when there is no\nchange in the contents.  It is quite plausible that future callers\nmay want to add additional information to only paths that have some\nchanges that need to be shown, no?  And at that point, they want to\ntweak this condition we place here, but without explanation they\nwouldn't know what they would be breaking if they did so.\n\n"},{"id":"444688","messageId":"xmqqee65mqa3.fsf@gitster.g","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"Re: [PATCH 0/9] Add a new --remerge-diff capability to show & log","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-22T00:33:56Z","receivedAt":"2021-12-22T00:34:03Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  Documentation/diff-options.txt |  8 ++++\n>  apply.c                        |  5 ++-\n>  builtin/checkout.c             | 12 ++++--\n>  builtin/log.c                  | 16 ++++++++\n>  diff-merges.c                  | 12 ++++++\n>  diff.c                         | 34 ++++++++++++++++-\n>  diff.h                         |  1 +\n>  ll-merge.c                     | 40 ++++++++++---------\n>  ll-merge.h                     |  9 ++++-\n>  log-tree.c                     | 70 ++++++++++++++++++++++++++++++++++\n>  merge-blobs.c                  |  5 ++-\n>  merge-ort.c                    | 49 +++++++++++++++++++++---\n>  merge-ort.h                    | 10 +++++\n>  merge-recursive.c              |  8 +++-\n>  merge-recursive.h              |  1 +\n>  notes-merge.c                  |  5 ++-\n>  rerere.c                       | 10 +++--\n>  revision.h                     |  6 ++-\n>  t/t6404-recursive-merge.sh     |  9 ++++-\n>  t/t6406-merge-attr.sh          |  9 ++++-\n>  tmp-objdir.c                   |  5 +++\n>  tmp-objdir.h                   |  6 +++\n>  22 files changed, 288 insertions(+), 42 deletions(-)\n\nIt is somewhat disappointing that there is no test or documentation\nupdate that show how a typical remerge-diff output should look like.\nI was specifically interested in finding out how the \"conflict hint\nmessages from merge backend in the diff header\" output would look.\n\nI left some messages here and there on the patches I read carefully,\nand they looked mostly good.  I only skimmed 7 and 8 and did not\nfind anything glaringly wrong, but that wouldn't count as a review.\n\nThanks.\n"},{"id":"444690","messageId":"211222.86mtktphxl.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"CABPp-BHDJMecupic=ybVhzZbRC5=WmY616JQdbfaqw_1A5sMHA@mail.gmail.com","subject":"Re: [PATCH 9/9] doc/diff-options: explain the new --remerge-diff option","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-21T23:47:29Z","receivedAt":"2021-12-22T01:06:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Dec 21 2021, Elijah Newren wrote:\n\n> On Tue, Dec 21, 2021 at 1:29 PM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n>>\n>>\n>> On Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n>>\n>> > From: Elijah Newren <newren@gmail.com>\n>> >\n>> > Signed-off-by: Elijah Newren <newren@gmail.com>\n>> > ---\n>> >  Documentation/diff-options.txt | 8 ++++++++\n>> >  1 file changed, 8 insertions(+)\n>> >\n>> > diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\n>> > index c89d530d3d1..b05f1c9f1c9 100644\n>> > --- a/Documentation/diff-options.txt\n>> > +++ b/Documentation/diff-options.txt\n>> > @@ -64,6 +64,14 @@ ifdef::git-log[]\n>> >       each of the parents. Separate log entry and diff is generated\n>> >       for each parent.\n>> >  +\n>> > +--diff-merges=remerge:::\n>> > +--diff-merges=r:::\n>> > +--remerge-diff:::\n>> > +     With this option, two-parent merge commits are remerged to\n>> > +     create a temporary tree object -- potentially containing files\n>> > +     with conflict markers and such.  A diff is then shown between\n>> > +     that temporary tree and the actual merge commit.\n>> > ++\n>> >  --diff-merges=combined:::\n>> >  --diff-merges=c:::\n>> >  -c:::\n>>\n>> This & 5/9 would I think be better squashed into their respective \"main\"\n>> patches.\n>\n> I presume you mean the \"main\" patch for this one is 8/9.  I was trying\n> to find a way to break up that large patch, but this is pretty small\n> so...sure I'll squash it in.\n>\n> What are you referring to as the \"main\" patch for 5/9, though?  It\n> only seems related to 6/9 and 7/9 to me, but I very deliberately split\n> those patches off and don't want to confuse them with unrelated\n> changes.  I disagree with combining 5/9 with either of those.\n\nI just gave it a quick initial skim.\n\nI have sometimes found it a bit harder to review your patches due to\nover-splitting.\n\nE.g. (went back and looked) here tmp_objdir_discard_objects() is\nintroduced in 1/9 but used in 8/9. \"path_messages\" is then introduced in\n5/9 and used in 8/9, no?\n\nAnyway, just a bit of feedback. FWIW not just bikeshedding. I do find\nmyself stopping at 1/9, paging to 2/9, searching for the function, not\nthere, checking 3/9 etc.\n\nI realize this is a bit of a stones & glass houses comment, but I find\nit a bit easier to review things when a patch is larger v.s. having it\nsplit up in a way where preceding steps don't do anything yet except\nwait for use by a subsequent patch.\n\n0.02 etc.\n"},{"id":"444749","messageId":"xmqqlf0dkvig.fsf@gitster.g","threadId":"57133","inReplyTo":"CABPp-BEuzorQkesgyNCvS05wsfqaH4Yv5JDFgy+AenrAC+JJNQ@mail.gmail.com","subject":"Re: [PATCH 1/9] tmp_objdir: add a helper function for discarding all contained objects","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-22T06:23:51Z","receivedAt":"2021-12-22T06:23:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> > +void tmp_objdir_discard_objects(struct tmp_objdir *t)\n>> > +{\n>> > +     remove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n>> > +}\n>> > +\n>>\n>> OK.\n>>\n>> Without a caller, it is a bit hard to judge if a separate helper\n>> makes the caller easier to read and understand, or becomes an extra\n>> layer of abstraction that obscures the logic.  Hopefully, having a\n>> more specific function name with \"tmp\" and \"discard\" in it makes the\n>> intent at callers more clear than the function that is named after\n>> the detail of the operation.\n>\n> This isn't just a convenience; since tmp_objdir is defined in\n> tmp-objdir.c rather than tmp-objdir.h, t->path is not accessible\n> outside of tmp-objdir.c.  Because of this, some kind of helper\n> function is necessary.\n\nBut adding this function as an extra level of abstration is *not*\nthe only way to expose the feature.  Instead the internal of \"struct\ntmp_objdir\" could be exposed to the caller that wants to discard the\nfiles inside the path.\n\nI think we now have enough material to fill between these two lines\nto help readers ;-)\n\n>> > From: Elijah Newren <newren@gmail.com>\n>> >\n>> > Signed-off-by: Elijah Newren <newren@gmail.com>\n\nThanks.\n"},{"id":"444813","messageId":"CABPp-BFmMZUeJaOmyLHPR20XCKF=bj8ZYfZAw13vRLUNJy80-Q@mail.gmail.com","threadId":"57133","inReplyTo":"211222.86mtktphxl.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 9/9] doc/diff-options: explain the new --remerge-diff option","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-22T19:05:36Z","receivedAt":"2021-12-22T19:05:50Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 5:06 PM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Tue, Dec 21 2021, Elijah Newren wrote:\n>\n> > On Tue, Dec 21, 2021 at 1:29 PM Ævar Arnfjörð Bjarmason\n> > <avarab@gmail.com> wrote:\n> >>\n> >>\n> >> On Tue, Dec 21 2021, Elijah Newren via GitGitGadget wrote:\n> >>\n> >> > From: Elijah Newren <newren@gmail.com>\n> >> >\n> >> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> >> > ---\n> >> >  Documentation/diff-options.txt | 8 ++++++++\n> >> >  1 file changed, 8 insertions(+)\n> >> >\n> >> > diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\n> >> > index c89d530d3d1..b05f1c9f1c9 100644\n> >> > --- a/Documentation/diff-options.txt\n> >> > +++ b/Documentation/diff-options.txt\n> >> > @@ -64,6 +64,14 @@ ifdef::git-log[]\n> >> >       each of the parents. Separate log entry and diff is generated\n> >> >       for each parent.\n> >> >  +\n> >> > +--diff-merges=remerge:::\n> >> > +--diff-merges=r:::\n> >> > +--remerge-diff:::\n> >> > +     With this option, two-parent merge commits are remerged to\n> >> > +     create a temporary tree object -- potentially containing files\n> >> > +     with conflict markers and such.  A diff is then shown between\n> >> > +     that temporary tree and the actual merge commit.\n> >> > ++\n> >> >  --diff-merges=combined:::\n> >> >  --diff-merges=c:::\n> >> >  -c:::\n> >>\n> >> This & 5/9 would I think be better squashed into their respective \"main\"\n> >> patches.\n> >\n> > I presume you mean the \"main\" patch for this one is 8/9.  I was trying\n> > to find a way to break up that large patch, but this is pretty small\n> > so...sure I'll squash it in.\n> >\n> > What are you referring to as the \"main\" patch for 5/9, though?  It\n> > only seems related to 6/9 and 7/9 to me, but I very deliberately split\n> > those patches off and don't want to confuse them with unrelated\n> > changes.  I disagree with combining 5/9 with either of those.\n>\n> I just gave it a quick initial skim.\n>\n> I have sometimes found it a bit harder to review your patches due to\n> over-splitting.\n>\n> E.g. (went back and looked) here tmp_objdir_discard_objects() is\n> introduced in 1/9 but used in 8/9. \"path_messages\" is then introduced in\n> 5/9 and used in 8/9, no?\n>\n> Anyway, just a bit of feedback. FWIW not just bikeshedding. I do find\n> myself stopping at 1/9, paging to 2/9, searching for the function, not\n> there, checking 3/9 etc.\n>\n> I realize this is a bit of a stones & glass houses comment, but I find\n> it a bit easier to review things when a patch is larger v.s. having it\n> split up in a way where preceding steps don't do anything yet except\n> wait for use by a subsequent patch.\n>\n> 0.02 etc.\n\nOh, 8/9.  That one could make sense.\n\nAnd thanks for the feedback.  Perhaps I could restructure this series\nwith a top-down design instead of bottom-up.  Doing that would mean\neither adding functions with an instant-die() implementation in the\nfirst step or just leaving a placeholder comment, and then filling\nthose things in for later steps.\n"},{"id":"444868","messageId":"CABPp-BFN+54-poG3JAqeF7SfMdcc+8Q+23ri58FbNe=eYjNF6w@mail.gmail.com","threadId":"57133","inReplyTo":"xmqqa6gto74z.fsf@gitster.g","subject":"Re: [PATCH 2/9] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-23T18:26:23Z","receivedAt":"2021-12-23T18:26:41Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 3:44 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Since some callers may want to send warning messages to somewhere other\n> > than stdout/stderr, stop printing \"warning: Cannot merge binary files\"\n> > from ll-merge and instead modify the return status of ll_merge() to\n> > indicate when a merge of binary files has occurred.\n> >\n> > Note that my methodology included first modifying ll_merge() to return\n> > a struct, so that the compiler would catch all the callers for me and\n> > ensure I had modified all of them.  After modifying all of them, I then\n> > changed the struct to an enum.\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  apply.c            |  5 ++++-\n> >  builtin/checkout.c | 12 ++++++++----\n> >  ll-merge.c         | 40 ++++++++++++++++++++++------------------\n> >  ll-merge.h         |  9 ++++++++-\n> >  merge-blobs.c      |  5 ++++-\n> >  merge-ort.c        |  5 ++++-\n> >  merge-recursive.c  |  5 ++++-\n> >  notes-merge.c      |  5 ++++-\n> >  rerere.c           | 10 +++++++---\n> >  9 files changed, 65 insertions(+), 31 deletions(-)\n> >\n> > diff --git a/apply.c b/apply.c\n> > index 43a0aebf4ee..12ea9c72a6b 100644\n> > --- a/apply.c\n> > +++ b/apply.c\n> > @@ -3492,7 +3492,7 @@ static int three_way_merge(struct apply_state *state,\n> >  {\n> >       mmfile_t base_file, our_file, their_file;\n> >       mmbuffer_t result = { NULL };\n> > -     int status;\n> > +     enum ll_merge_result status;\n> >\n> >       /* resolve trivial cases first */\n> >       if (oideq(base, ours))\n> > @@ -3509,6 +3509,9 @@ static int three_way_merge(struct apply_state *state,\n> >                         &their_file, \"theirs\",\n> >                         state->repo->index,\n> >                         NULL);\n> > +     if (status == LL_MERGE_BINARY_CONFLICT)\n> > +             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > +                     \"base\", \"ours\", \"theirs\");\n>\n> This used to come from ll_merge()\n>\n> > -                     warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > -                             path, name1, name2);\n> > -                     /* fallthru */\n>\n> And our call to ll_merge() above (half of it invisible in the\n> pre-context of the hunk) gave \"ours\" and \"theirs\" to our_label and\n> their_label, which in turn are called name1 and name2, respectively,\n> in ll_merge_binary() driver.\n>\n> I am not sure about the \"base\" string, though.  I suspect that your\n> \"base\" should be a reference to the parameter 'path' of three_way_merge()\n> function.\n\nAh, indeed; thanks for reading carefully.\n\n> > diff --git a/builtin/checkout.c b/builtin/checkout.c\n> > index cbf73b8c9f6..3a559d69303 100644\n> > --- a/builtin/checkout.c\n> > +++ b/builtin/checkout.c\n> > @@ -237,6 +237,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n> >       struct cache_entry *ce = active_cache[pos];\n> >       const char *path = ce->name;\n> >       mmfile_t ancestor, ours, theirs;\n> > +     enum ll_merge_result merge_status;\n> >       int status;\n> >       struct object_id oid;\n> >       mmbuffer_t result_buf;\n> > @@ -267,13 +268,16 @@ static int checkout_merged(int pos, const struct checkout *state,\n> >       memset(&ll_opts, 0, sizeof(ll_opts));\n> >       git_config_get_bool(\"merge.renormalize\", &renormalize);\n> >       ll_opts.renormalize = renormalize;\n> > -     status = ll_merge(&result_buf, path, &ancestor, \"base\",\n> > -                       &ours, \"ours\", &theirs, \"theirs\",\n> > -                       state->istate, &ll_opts);\n> > +     merge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n> > +                             &ours, \"ours\", &theirs, \"theirs\",\n> > +                             state->istate, &ll_opts);\n> >       free(ancestor.ptr);\n> >       free(ours.ptr);\n> >       free(theirs.ptr);\n> > -     if (status < 0 || !result_buf.ptr) {\n> > +     if (merge_status == LL_MERGE_BINARY_CONFLICT)\n> > +             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > +                     path, \"ours\", \"theirs\");\n>\n> This one looks correct.\n>\n> > +     if (merge_status < 0 || !result_buf.ptr) {\n> >               free(result_buf.ptr);\n> >               return error(_(\"path '%s': cannot merge\"), path);\n> >       }\n>\n> > diff --git a/merge-blobs.c b/merge-blobs.c\n> > index ee0a0e90c94..8138090f81c 100644\n> > --- a/merge-blobs.c\n> > +++ b/merge-blobs.c\n> > @@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate,\n> >                                mmfile_t *their,\n> >                                unsigned long *size)\n> >  {\n> > -     int merge_status;\n> > +     enum ll_merge_result merge_status;\n> >       mmbuffer_t res;\n> >\n> >       /*\n> > @@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate,\n> >                               istate, NULL);\n> >       if (merge_status < 0)\n> >               return NULL;\n> > +     if (merge_status == LL_MERGE_BINARY_CONFLICT)\n> > +             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > +                     path, \".our\", \".their\");\n>\n> OK.\n>\n> > diff --git a/merge-ort.c b/merge-ort.c\n> > index 0342f104836..c24da2ba3cb 100644\n> > --- a/merge-ort.c\n> > +++ b/merge-ort.c\n> > @@ -1743,7 +1743,7 @@ static int merge_3way(struct merge_options *opt,\n> >       mmfile_t orig, src1, src2;\n> >       struct ll_merge_options ll_opts = {0};\n> >       char *base, *name1, *name2;\n> > -     int merge_status;\n> > +     enum ll_merge_result merge_status;\n> >\n> >       if (!opt->priv->attr_index.initialized)\n> >               initialize_attr_index(opt);\n> > @@ -1787,6 +1787,9 @@ static int merge_3way(struct merge_options *opt,\n> >       merge_status = ll_merge(result_buf, path, &orig, base,\n> >                               &src1, name1, &src2, name2,\n> >                               &opt->priv->attr_index, &ll_opts);\n> > +     if (merge_status == LL_MERGE_BINARY_CONFLICT)\n> > +             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > +                     path, name1, name2);\n>\n> OK; this is your code and I do not have to read it too carefully,\n> but all we need is conveniently in the pre-context of the hunk ;-).\n>\n> > diff --git a/merge-recursive.c b/merge-recursive.c\n> > index d9457797dbb..bc73c52dd84 100644\n> > --- a/merge-recursive.c\n> > +++ b/merge-recursive.c\n> > @@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt,\n> >       mmfile_t orig, src1, src2;\n> >       struct ll_merge_options ll_opts = {0};\n> >       char *base, *name1, *name2;\n> > -     int merge_status;\n> > +     enum ll_merge_result merge_status;\n> >\n> >       ll_opts.renormalize = opt->renormalize;\n> >       ll_opts.extra_marker_size = extra_marker_size;\n> > @@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt,\n> >       merge_status = ll_merge(result_buf, a->path, &orig, base,\n> >                               &src1, name1, &src2, name2,\n> >                               opt->repo->index, &ll_opts);\n> > +     if (merge_status == LL_MERGE_BINARY_CONFLICT)\n> > +             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > +                     a->path, name1, name2);\n>\n> OK.\n>\n> > diff --git a/notes-merge.c b/notes-merge.c\n> > index b4a3a903e86..01d596920ea 100644\n> > --- a/notes-merge.c\n> > +++ b/notes-merge.c\n> > @@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n> >  {\n> >       mmbuffer_t result_buf;\n> >       mmfile_t base, local, remote;\n> > -     int status;\n> > +     enum ll_merge_result status;\n> >\n> >       read_mmblob(&base, &p->base);\n> >       read_mmblob(&local, &p->local);\n> > @@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n> >       free(local.ptr);\n> >       free(remote.ptr);\n> >\n> > +     if (status == LL_MERGE_BINARY_CONFLICT)\n> > +             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > +                     oid_to_hex(&p->obj), o->local_ref, o->remote_ref);\n>\n> This uses another slot in the rotating buffer used by oid_to_hex(),\n> but I do not think anybody grabbed a pointer to one of them and held\n> onto it before we got here, so it would be OK.\n>\n> > diff --git a/rerere.c b/rerere.c\n> > index d83d58df4fb..46fd01819b8 100644\n> > --- a/rerere.c\n> > +++ b/rerere.c\n> > @@ -609,19 +609,23 @@ static int try_merge(struct index_state *istate,\n> >                    const struct rerere_id *id, const char *path,\n> >                    mmfile_t *cur, mmbuffer_t *result)\n> >  {\n> > -     int ret;\n> > +     enum ll_merge_result ret;\n> >       mmfile_t base = {NULL, 0}, other = {NULL, 0};\n> >\n> >       if (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n> >           read_mmfile(&other, rerere_path(id, \"postimage\")))\n> > -             ret = 1;\n> > -     else\n> > +             ret = LL_MERGE_CONFLICT;\n> > +     else {\n>\n> Let's have {} around the if clause now the corresponding else clause\n> needs it.\n\nWill fix.\n\n> >               /*\n> >                * A three-way merge. Note that this honors user-customizable\n> >                * low-level merge driver settings.\n> >                */\n> >               ret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n> >                              istate, NULL);\n> > +             if (ret == LL_MERGE_BINARY_CONFLICT)\n> > +                     warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > +                             path, \"\", \"\");\n> > +     }\n>\n> This is a faithful conversion of what should not happen in practice,\n> as the rerere logic would not be able to reach here.  In a binary\n> file, we won't be able to identify <<< === >>> blocks, hash the text\n> in the conflicted block to come up with the conflict ID to find the\n> preimage and postimage files.  These files are the input to the low\n> level merge driver call we are making here.\n>\n> Looking almost good except for a warning message bug I spotted\n> earlier.\n>\n> Thanks.\n"},{"id":"444870","messageId":"CABPp-BEqrB4cKb7QimJz9umKTcfLOrskkGBOYXnUDX36YyhROA@mail.gmail.com","threadId":"57133","inReplyTo":"xmqq5yrho6f3.fsf@gitster.g","subject":"Re: [PATCH 3/9] merge-ort: capture and print ll-merge warnings in our preferred fashion","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-23T18:36:48Z","receivedAt":"2021-12-23T18:37:02Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 4:00 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Instead of immediately printing ll-merge warnings to stderr, we save\n> > them in our output strbuf.  Besides allowing us to move these warnings\n> > to a special file for --remerge-diff, this has two other benefits for\n> > regular merges done by merge-ort:\n> >\n> >   * The deferral of messages ensures we can print all messages about\n> >     any given path together (merge-recursive was known to sometimes\n> >     intersperse messages about other paths, particularly when renames\n> >     were involved).\n>\n> I would imagine that with something like this, we can show such a\n> warning message differently when it happens during an inner\n> \"synthesizing a virtual common ancestor\" merge (the most likely\n> value for \"show differently\" would be to \"squelch\"), which may be a\n> good thing.\n\nYes, that is a possibility that opens up after this.  Which reminds\nme, merge-recursive nicely nested conflict/warnings messages from\ninner merges by adding 2*call_depth space characters before messages.\nI lost that in merge-ort (which becomes more problematic since\nmerge-ort tries to group messages about the same path together, thus\nmixing inner merge messages with outer ones and providing no way to\ndifferentiate the two).  I've got a patch to fix that up, but of\ncourse it conflicts with this series, so I'll be submitting it after\nthis one settles.\n\n> >       if (merge_status == LL_MERGE_BINARY_CONFLICT)\n> > -             warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > -                     path, name1, name2);\n> > +             path_msg(opt, path, 0,\n> > +                      \"warning: Cannot merge binary files: %s (%s vs. %s)\",\n> > +                      path, name1, name2);\n> >\n>\n> Nice.\n"},{"id":"444871","messageId":"CABPp-BEWs8zch2diE2n0oKW6J8PRDDFwujzCXzemfz2-2-pjQA@mail.gmail.com","threadId":"57133","inReplyTo":"xmqq1r25o651.fsf@gitster.g","subject":"Re: [PATCH 4/9] merge-ort: mark a few more conflict messages as omittable","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-23T18:38:21Z","receivedAt":"2021-12-23T18:38:35Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 4:06 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > path_msg() has the ability to mark messages as omittable, designed for\n> > remerge-diff where we'll instead be showing conflict messages as diff\n> > headers for a subsequent diff.  While all these messages are very useful\n> > when trying to create a merge initially, early use with the\n> > --remerge-diff feature (the only user of this omittable conflict message\n> > capability), suggests that the particular messages marked in this commit\n> > are just noise when trying to see what changes users made to create a\n> > merge commit.\n>\n> It is likely because when somebody is looking at the output of\n> remerge-diff, they are mostly concentrating on the _content_ level\n> merges and they are not keenly looking for a merge whose result is\n> deposited at a wrong path.  Since what is shown is something that\n> has already recorded in the history, we can safely assume that it is\n> no longer a relevant (or \"it is way too late to matter\"), I would\n> say, to show these messages about \"file location\".\n>\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  merge-ort.c | 6 +++---\n> >  1 file changed, 3 insertions(+), 3 deletions(-)\n> >\n> > diff --git a/merge-ort.c b/merge-ort.c\n> > index a18f47e23c5..fe27870e73e 100644\n> > --- a/merge-ort.c\n> > +++ b/merge-ort.c\n> > @@ -2420,7 +2420,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n> >                */\n> >               ci->path_conflict = 1;\n> >               if (pair->status == 'A')\n> > -                     path_msg(opt, new_path, 0,\n> > +                     path_msg(opt, new_path, 1,\n> >                                _(\"CONFLICT (file location): %s added in %s \"\n> >                                  \"inside a directory that was renamed in %s, \"\n> >                                  \"suggesting it should perhaps be moved to \"\n> > @@ -2428,7 +2428,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n> >                                old_path, branch_with_new_path,\n> >                                branch_with_dir_rename, new_path);\n> >               else\n> > -                     path_msg(opt, new_path, 0,\n> > +                     path_msg(opt, new_path, 1,\n> >                                _(\"CONFLICT (file location): %s renamed to %s \"\n> >                                  \"in %s, inside a directory that was renamed \"\n> >                                  \"in %s, suggesting it should perhaps be \"\n> > @@ -3825,7 +3825,7 @@ static void process_entry(struct merge_options *opt,\n> >                               reason = _(\"add/add\");\n> >                       if (S_ISGITLINK(merged_file.mode))\n> >                               reason = _(\"submodule\");\n> > -                     path_msg(opt, path, 0,\n> > +                     path_msg(opt, path, 1,\n> >                                _(\"CONFLICT (%s): Merge conflict in %s\"),\n> >                                reason, path);\n>\n> I am not as sure about this one as the other two, though.  I guess\n> in the context of remerge-diff, resolving the add/add conflict into\n> the same file is also something that happened long time ago and\n> these messages are too late to matter the same way as the other two.\n\nYeah, I'm not so sure about it either, and my notes are long, long\ngone.  I think I'll pull this one out, and we can always tweak it\nlater if needed.\n"},{"id":"444936","messageId":"CABPp-BEMBkG=+g7SUXmJHqZ=qQQn-2oPg33uRtYyRHkt3SbxaA@mail.gmail.com","threadId":"57133","inReplyTo":"xmqqlf0dkvig.fsf@gitster.g","subject":"Re: [PATCH 1/9] tmp_objdir: add a helper function for discarding all contained objects","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-25T02:29:45Z","receivedAt":"2021-12-25T02:29:59Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 10:23 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> >> > +void tmp_objdir_discard_objects(struct tmp_objdir *t)\n> >> > +{\n> >> > +     remove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n> >> > +}\n> >> > +\n> >>\n> >> OK.\n> >>\n> >> Without a caller, it is a bit hard to judge if a separate helper\n> >> makes the caller easier to read and understand, or becomes an extra\n> >> layer of abstraction that obscures the logic.  Hopefully, having a\n> >> more specific function name with \"tmp\" and \"discard\" in it makes the\n> >> intent at callers more clear than the function that is named after\n> >> the detail of the operation.\n> >\n> > This isn't just a convenience; since tmp_objdir is defined in\n> > tmp-objdir.c rather than tmp-objdir.h, t->path is not accessible\n> > outside of tmp-objdir.c.  Because of this, some kind of helper\n> > function is necessary.\n>\n> But adding this function as an extra level of abstration is *not*\n> the only way to expose the feature.  Instead the internal of \"struct\n> tmp_objdir\" could be exposed to the caller that wants to discard the\n> files inside the path.\n\nAh, yes, we talked about that during the tmp-objdir discussion back in\nSeptember/October.  Peff didn't want struct tmp_objdir exposed, and I\nwas operating with that in mind.\n\n> I think we now have enough material to fill between these two lines\n> to help readers ;-)\n\nI've restructured the series a bit based on Ævar's feedback, and this\nfunction is now only introduced along with its caller.  Hopefully that\nmakes it a bit clearer.\n"},{"id":"444937","messageId":"CABPp-BHXDniC2J6YYxK_9DbyEOEKUn_6fAc9KFvvatxu_tOzFw@mail.gmail.com","threadId":"57133","inReplyTo":"xmqqlf0dmqp5.fsf@gitster.g","subject":"Re: [PATCH 6/9] diff: add ability to insert additional headers for paths","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-25T02:35:16Z","receivedAt":"2021-12-25T02:35:30Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 21, 2021 at 4:24 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > In support of a remerge-diff ability we will add in a few commits, we\n> > want to be able to provide additional headers to show along with a diff.\n> > Add the plumbing necessary to enable this.\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  diff.c | 34 +++++++++++++++++++++++++++++++++-\n> >  diff.h |  1 +\n> >  2 files changed, 34 insertions(+), 1 deletion(-)\n> >\n> > diff --git a/diff.c b/diff.c\n> > index 861282db1c3..a9490b9b2ba 100644\n> > --- a/diff.c\n> > +++ b/diff.c\n> > @@ -27,6 +27,7 @@\n> >  #include \"help.h\"\n> >  #include \"promisor-remote.h\"\n> >  #include \"dir.h\"\n> > +#include \"strmap.h\"\n> >\n> >  #ifdef NO_FAST_WORKING_DIRECTORY\n> >  #define FAST_WORKING_DIRECTORY 0\n> > @@ -3406,6 +3407,33 @@ struct userdiff_driver *get_textconv(struct repository *r,\n> >       return userdiff_get_textconv(r, one->driver);\n> >  }\n> >\n> > +static struct strbuf* additional_headers(struct diff_options *o,\n>\n> Style.\n>\n> > +                                      const char *path)\n> > +{\n> > +     if (!o->additional_path_headers)\n> > +             return NULL;\n> > +     return strmap_get(o->additional_path_headers, path);\n> > +}\n> > +\n> > +static void add_formatted_headers(struct strbuf *msg,\n> > +                               struct strbuf *more_headers,\n> > +                               const char *line_prefix,\n> > +                               const char *meta,\n> > +                               const char *reset)\n> > +{\n> > +     char *next, *newline;\n> > +\n> > +     next = more_headers->buf;\n> > +     while ((newline = strchr(next, '\\n'))) {\n> > +             *newline = '\\0';\n> > +             strbuf_addf(msg, \"%s%s%s%s\\n\", line_prefix, meta, next, reset);\n> > +             *newline = '\\n';\n> > +             next = newline + 1;\n> > +     }\n>\n> The above is not wrong per-se, but we do not need to do the\n> \"temporarily terminate and then recover\" dance, and avoiding it\n> would make the code cleaner.\n>\n> Once you learn the value of \"newline\" [*], you know the number of\n> bytes between \"next\" and \"newline\" so you can use safely \"%.*s\"\n> format specifier without temporarily terminating the subsection of\n> the string.\n>\n>         Side note. I would actually use strchrnul() instead, so that\n>         we do not have to special case the end of the buffer.  For a\n>         readily available example, see advice.c::vadvise().\n>\n> > +     if (*next)\n> > +             strbuf_addf(msg, \"%s%s%s%s\\n\", line_prefix, meta, next, reset);\n> > +}\n>\n> > @@ -4328,9 +4356,13 @@ static void fill_metainfo(struct strbuf *msg,\n> >       const char *set = diff_get_color(use_color, DIFF_METAINFO);\n> >       const char *reset = diff_get_color(use_color, DIFF_RESET);\n> >       const char *line_prefix = diff_line_prefix(o);\n> > +     struct strbuf *more_headers = NULL;\n> >\n> >       *must_show_header = 1;\n> >       strbuf_init(msg, PATH_MAX * 2 + 300);\n> > +     if ((more_headers = additional_headers(o, name)))\n> > +             add_formatted_headers(msg, more_headers,\n> > +                                   line_prefix, set, reset);\n>\n> So, we stuff what came via path_msg() without anything that allows\n> readers to identify them to the header part?  Just like we have\n> fixed and known string taken from a bounded vocabulary such as\n> \"index\", \"copy from\", \"old mode\", etc., don't we want to prefix the\n> hints that came from the merge machinery with some identifiable\n> string?\n\nThat's a fair question.  Most of the involved messages are of the form\n    CONFLICT (<reason>): more details\nand \"CONFLICT\" seems like a pretty identifiable string.  There are\nsome others, which made me wonder if we wanted some kind of additional\nprefix, but I was having a hard time coming up with a meaningful\nprefix; most that I thought of didn't seem like they'd help.\n\nI've provided some testcases in the next re-roll so you can see some\nexamples; maybe that will help others judge if a prefix is needed and\nspur creative juices for coming up with a good once since I seem to be\nunable to.\n\n> > @@ -5852,7 +5884,7 @@ int diff_unmodified_pair(struct diff_filepair *p)\n> >\n> >  static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n> >  {\n> > -     if (diff_unmodified_pair(p))\n> > +     if (diff_unmodified_pair(p) && !additional_headers(o, p->one->path))\n> >               return;\n>\n> This does not feel quite right.  At least there needs a comment that\n> says the _current_ callers that add additional_headers() would do so\n> only for paths that the end-users cares about, even when there is no\n> change in the contents.  It is quite plausible that future callers\n> may want to add additional information to only paths that have some\n> changes that need to be shown, no?  And at that point, they want to\n> tweak this condition we place here, but without explanation they\n> wouldn't know what they would be breaking if they did so.\n\nI've added a comment.\n"},{"id":"444942","messageId":"b3ae62083e14aecdfe909735b4daf0a36fa5e48c.1640419159.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"[PATCH v2 1/8] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-25T07:59:12Z","receivedAt":"2021-12-25T07:59:28Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen this option is specified, we remerge all (two parent) merge commits\nand diff the actual merge commit to the automatically created version,\nin order to show how users removed conflict markers, resolved the\ndifferent conflict versions, and potentially added new changes outside\nof conflict regions in order to resolve semantic merge problems (or,\npossibly, just to hide other random changes).\n\nThis capability works by creating a temporary object directory and\nmarking it as the primary object store.  This makes it so that any blobs\nor trees created during the automatic merge easily removable afterwards\nby just deleting all objects from the temporary object directory.\n\nThere are a few ways that this implementation is suboptimal:\n  * `log --remerge-diff` becomes slow, because the temporary object\n    directory can fills with many loose objects while running\n  * the log output can be muddied with misplaced \"warning: cannot merge\n    binary files\" messages, since ll-merge.c unconditionally writes those\n    messages to stderr while running instead of allowing callers to\n    manage them.\n  * important conflict and warning messages are simply dropped; thus for\n    conflicts like modify/delete or rename/rename or file/directory which\n    are not representable with content conflict markers, there may be no\n    way for a user of --remerge-diff to know that there had been a\n    conflict which was resolved (and which possibly motivated other\n    changes in the merge commit).\nSubsequent commits will address these issues.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n Documentation/diff-options.txt |  8 ++++\n builtin/log.c                  | 14 ++++++\n diff-merges.c                  | 12 +++++\n log-tree.c                     | 59 +++++++++++++++++++++++\n revision.h                     |  3 +-\n t/t4069-remerge-diff.sh        | 86 ++++++++++++++++++++++++++++++++++\n 6 files changed, 181 insertions(+), 1 deletion(-)\n create mode 100755 t/t4069-remerge-diff.sh\n\ndiff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\nindex c89d530d3d1..b05f1c9f1c9 100644\n--- a/Documentation/diff-options.txt\n+++ b/Documentation/diff-options.txt\n@@ -64,6 +64,14 @@ ifdef::git-log[]\n \teach of the parents. Separate log entry and diff is generated\n \tfor each parent.\n +\n+--diff-merges=remerge:::\n+--diff-merges=r:::\n+--remerge-diff:::\n+\tWith this option, two-parent merge commits are remerged to\n+\tcreate a temporary tree object -- potentially containing files\n+\twith conflict markers and such.  A diff is then shown between\n+\tthat temporary tree and the actual merge commit.\n++\n --diff-merges=combined:::\n --diff-merges=c:::\n -c:::\ndiff --git a/builtin/log.c b/builtin/log.c\nindex f75d87e8d7f..d053418fddd 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -35,6 +35,7 @@\n #include \"repository.h\"\n #include \"commit-reach.h\"\n #include \"range-diff.h\"\n+#include \"tmp-objdir.h\"\n \n #define MAIL_DEFAULT_WRAP 72\n #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n@@ -406,6 +407,14 @@ static int cmd_log_walk(struct rev_info *rev)\n \tstruct commit *commit;\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n+\tstruct tmp_objdir *remerge_objdir = NULL;\n+\n+\tif (rev->remerge_diff) {\n+\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\tif (!remerge_objdir)\n+\t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n+\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n+\t}\n \n \tif (rev->early_output)\n \t\tsetup_early_output();\n@@ -449,6 +458,9 @@ static int cmd_log_walk(struct rev_info *rev)\n \trev->diffopt.no_free = 0;\n \tdiff_free(&rev->diffopt);\n \n+\tif (rev->remerge_diff)\n+\t\ttmp_objdir_destroy(remerge_objdir);\n+\n \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n \t    rev->diffopt.flags.check_failed) {\n \t\treturn 02;\n@@ -1943,6 +1955,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tdie(_(\"--name-status does not make sense\"));\n \tif (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF)\n \t\tdie(_(\"--check does not make sense\"));\n+\tif (rev.remerge_diff)\n+\t\tdie(_(\"--remerge-diff does not make sense\"));\n \n \tif (!use_patch_format &&\n \t\t(!rev.diffopt.output_format ||\ndiff --git a/diff-merges.c b/diff-merges.c\nindex 5060ccd890b..0af4b3f9191 100644\n--- a/diff-merges.c\n+++ b/diff-merges.c\n@@ -17,6 +17,7 @@ static void suppress(struct rev_info *revs)\n \trevs->combined_all_paths = 0;\n \trevs->merges_imply_patch = 0;\n \trevs->merges_need_diff = 0;\n+\trevs->remerge_diff = 0;\n }\n \n static void set_separate(struct rev_info *revs)\n@@ -45,6 +46,12 @@ static void set_dense_combined(struct rev_info *revs)\n \trevs->dense_combined_merges = 1;\n }\n \n+static void set_remerge_diff(struct rev_info *revs)\n+{\n+\tsuppress(revs);\n+\trevs->remerge_diff = 1;\n+}\n+\n static diff_merges_setup_func_t func_by_opt(const char *optarg)\n {\n \tif (!strcmp(optarg, \"off\") || !strcmp(optarg, \"none\"))\n@@ -57,6 +64,8 @@ static diff_merges_setup_func_t func_by_opt(const char *optarg)\n \t\treturn set_combined;\n \telse if (!strcmp(optarg, \"cc\") || !strcmp(optarg, \"dense-combined\"))\n \t\treturn set_dense_combined;\n+\telse if (!strcmp(optarg, \"r\") || !strcmp(optarg, \"remerge\"))\n+\t\treturn set_remerge_diff;\n \telse if (!strcmp(optarg, \"m\") || !strcmp(optarg, \"on\"))\n \t\treturn set_to_default;\n \treturn NULL;\n@@ -110,6 +119,9 @@ int diff_merges_parse_opts(struct rev_info *revs, const char **argv)\n \t} else if (!strcmp(arg, \"--cc\")) {\n \t\tset_dense_combined(revs);\n \t\trevs->merges_imply_patch = 1;\n+\t} else if (!strcmp(arg, \"--remerge-diff\")) {\n+\t\tset_remerge_diff(revs);\n+\t\trevs->merges_imply_patch = 1;\n \t} else if (!strcmp(arg, \"--no-diff-merges\")) {\n \t\tsuppress(revs);\n \t} else if (!strcmp(arg, \"--combined-all-paths\")) {\ndiff --git a/log-tree.c b/log-tree.c\nindex 644893fd8cf..84ed864fc81 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -1,4 +1,5 @@\n #include \"cache.h\"\n+#include \"commit-reach.h\"\n #include \"config.h\"\n #include \"diff.h\"\n #include \"object-store.h\"\n@@ -7,6 +8,7 @@\n #include \"tag.h\"\n #include \"graph.h\"\n #include \"log-tree.h\"\n+#include \"merge-ort.h\"\n #include \"reflog-walk.h\"\n #include \"refs.h\"\n #include \"string-list.h\"\n@@ -902,6 +904,51 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n \treturn !opt->loginfo;\n }\n \n+static int do_remerge_diff(struct rev_info *opt,\n+\t\t\t   struct commit_list *parents,\n+\t\t\t   struct object_id *oid,\n+\t\t\t   struct commit *commit)\n+{\n+\tstruct merge_options o;\n+\tstruct commit_list *bases;\n+\tstruct merge_result res = {0};\n+\tstruct pretty_print_context ctx = {0};\n+\tstruct commit *parent1 = parents->item;\n+\tstruct commit *parent2 = parents->next->item;\n+\tstruct strbuf parent1_desc = STRBUF_INIT;\n+\tstruct strbuf parent2_desc = STRBUF_INIT;\n+\n+\t/* Setup merge options */\n+\tinit_merge_options(&o, the_repository);\n+\to.show_rename_progress = 0;\n+\n+\tctx.abbrev = DEFAULT_ABBREV;\n+\tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n+\tformat_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n+\to.branch1 = parent1_desc.buf;\n+\to.branch2 = parent2_desc.buf;\n+\n+\t/* Parse the relevant commits and get the merge bases */\n+\tparse_commit_or_die(parent1);\n+\tparse_commit_or_die(parent2);\n+\tbases = get_merge_bases(parent1, parent2);\n+\n+\t/* Re-merge the parents */\n+\tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n+\n+\t/* Show the diff */\n+\tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n+\tlog_tree_diff_flush(opt);\n+\n+\t/* Cleanup */\n+\tstrbuf_release(&parent1_desc);\n+\tstrbuf_release(&parent2_desc);\n+\tmerge_finalize(&o, &res);\n+\t/* TODO: clean up the temporary object directory */\n+\n+\treturn !opt->loginfo;\n+}\n+\n /*\n  * Show the diff of a commit.\n  *\n@@ -936,6 +983,18 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log\n \t}\n \n \tif (is_merge) {\n+\t\tint octopus = (parents->next->next != NULL);\n+\n+\t\tif (opt->remerge_diff) {\n+\t\t\tif (octopus) {\n+\t\t\t\tshow_log(opt);\n+\t\t\t\tfprintf(opt->diffopt.file,\n+\t\t\t\t\t\"diff: warning: Skipping remerge-diff \"\n+\t\t\t\t\t\"for octopus merges.\\n\");\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\treturn do_remerge_diff(opt, parents, oid, commit);\n+\t\t}\n \t\tif (opt->combine_merges)\n \t\t\treturn do_diff_combined(opt, commit);\n \t\tif (opt->separate_merges) {\ndiff --git a/revision.h b/revision.h\nindex 5578bb4720a..13178e6b8f3 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -195,7 +195,8 @@ struct rev_info {\n \t\t\tcombine_merges:1,\n \t\t\tcombined_all_paths:1,\n \t\t\tdense_combined_merges:1,\n-\t\t\tfirst_parent_merges:1;\n+\t\t\tfirst_parent_merges:1,\n+\t\t\tremerge_diff:1;\n \n \t/* Format info */\n \tint\t\tshow_notes;\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nnew file mode 100755\nindex 00000000000..192dbce2bfe\n--- /dev/null\n+++ b/t/t4069-remerge-diff.sh\n@@ -0,0 +1,86 @@\n+#!/bin/sh\n+\n+test_description='remerge-diff handling'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup basic merges' '\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m base &&\n+\n+\tgit branch feature_a &&\n+\tgit branch feature_b &&\n+\tgit branch feature_c &&\n+\n+\tgit branch ab_resolution &&\n+\tgit branch bc_resolution &&\n+\n+\tgit checkout feature_a &&\n+\ttest_write_lines 1 2 three 4 5 6 7 eight 9 >numbers &&\n+\tgit commit -a -m change_a &&\n+\n+\tgit checkout feature_b &&\n+\ttest_write_lines 1 2 tres 4 5 6 7 8 9 >numbers &&\n+\tgit commit -a -m change_b &&\n+\n+\tgit checkout feature_c &&\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&\n+\tgit commit -a -m change_c &&\n+\n+\tgit checkout bc_resolution &&\n+\t# fast forward\n+\tgit merge feature_b &&\n+\t# no conflict\n+\tgit merge feature_c &&\n+\n+\tgit checkout ab_resolution &&\n+\t# fast forward\n+\tgit merge feature_a &&\n+\t# conflicts!\n+\ttest_must_fail git merge feature_b &&\n+\t# Resolve conflict...and make another change elsewhere\n+\ttest_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers &&\n+\tgit add numbers &&\n+\tgit merge --continue\n+'\n+\n+test_expect_success 'remerge-diff on a clean merge' '\n+\tgit log -1 --oneline bc_resolution >expect &&\n+\tgit show --oneline --remerge-diff bc_resolution >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' '\n+\tgit log -1 --oneline ab_resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/numbers b/numbers\n+\tindex a1fb731..6875544 100644\n+\t--- a/numbers\n+\t+++ b/numbers\n+\t@@ -1,13 +1,9 @@\n+\t 1\n+\t 2\n+\t-<<<<<<< b0ed5cb (change_a)\n+\t-three\n+\t-=======\n+\t-tres\n+\t->>>>>>> 6cd3f82 (change_b)\n+\t+drei\n+\t 4\n+\t 5\n+\t 6\n+\t 7\n+\t-eight\n+\t+acht\n+\t 9\n+\tEOF\n+\t# Hashes above are sha1; rip them out so test works with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff ab_resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"444943","messageId":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.git.1640109948.gitgitgadget@gmail.com","subject":"[PATCH v2 0/8] Add a new --remerge-diff capability to show & log","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-25T07:59:11Z","receivedAt":"2021-12-25T07:59:29Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Here are some patches to add a --remerge-diff capability to show & log,\nwhich works by comparing merge commits to an automatic remerge (note that\nthe automatic remerge tree can contain files with conflict markers).\n\nChanges since v1 (of the restarted submission, which technically was v2):\n\n * Restructured the series, so the first patch introduces the feature --\n   with a bunch of caveats. Subsequent patches clean up those caveats. This\n   avoids introducing not-yet-used functions, and hopefully makes review\n   easier.\n * added testcases\n * numerous small improvements suggested by Ævar and Junio\n\nChanges since original submission[1]:\n\n * Rebased on top of the version of ns/tmp-objdir that Neeraj submitted\n   (Neeraj's patches were based on v2.34, but ns/tmp-objdir got applied on\n   an old commit and does not even build because of that).\n * Modify ll-merge API to return a status, instead of printing \"Cannot merge\n   binary files\" on stdout[2] (as suggested by Peff)\n * Make conflict messages and other such warnings into diff headers of the\n   subsequent remerge-diff rather than appearing in the diff as file content\n   of some funny looking filenames (as suggested by Peff[3] and Junio[4])\n * Sergey ack'ed the diff-merges.c portion of the patches, but that wasn't\n   limited to one patch so not sure where to record that ack.\n\n[1]\nhttps://lore.kernel.org/git/pull.1080.git.git.1630376800.gitgitgadget@gmail.com/;\nGitHub wouldn't let me change the target branch for the PR, so I had to\ncreate a new one with the new base and thus the reason for not sending this\nas v2 even though it is. [2]\nhttps://lore.kernel.org/git/YVOZRhWttzF18Xql@coredump.intra.peff.net/,\nhttps://lore.kernel.org/git/YVOZty9D7NRbzhE5@coredump.intra.peff.net/ [3]\nhttps://lore.kernel.org/git/YVOXPTjsp9lrxmS6@coredump.intra.peff.net/ [4]\nhttps://lore.kernel.org/git/xmqqr1d7e4ug.fsf@gitster.g/\n\n=== FURTHER BACKGROUND (original cover letter material) ==\n\nHere are some example commits you can try this out on (with git show\n--remerge-diff $COMMIT):\n\n * git.git conflicted merge: 07601b5b36\n * git.git non-conflicted change: bf04590ecd\n * linux.git conflicted merge: eab3540562fb\n * linux.git non-conflicted change: 223cea6a4f05\n\nMany more can be found by just running git log --merges --remerge-diff in\nyour repository of choice and searching for diffs (most merges tend to be\nclean and unmodified and thus produce no diff but a search of '^diff' in the\nlog output tends to find the examples nicely).\n\nSome basic high level details about this new option:\n\n * This option is most naturally compared to --cc, though the output seems\n   to be much more understandable to most users than --cc output.\n * Since merges are often clean and unmodified, this new option results in\n   an empty diff for most merges.\n * This new option shows things like the removal of conflict markers, which\n   hunks users picked from the various conflicted sides to keep or remove,\n   and shows changes made outside of conflict markers (which might reflect\n   changes needed to resolve semantic conflicts or cleanups of e.g.\n   compilation warnings or other additional changes an integrator felt\n   belonged in the merged result).\n * This new option does not (currently) work for octopus merges, since\n   merge-ort is specific to two-parent merges[1].\n * This option will not work on a read-only or full filesystem[2].\n * We discussed this capability at Git Merge 2020, and one of the\n   suggestions was doing a periodic git gc --auto during the operation (due\n   to potential new blobs and trees created during the operation). I found a\n   way to avoid that; see [2].\n * This option is faster than you'd probably expect; it handles 33.5 merge\n   commits per second in linux.git on my computer; see below.\n\nIn regards to the performance point above, the timing for running the\nfollowing command:\n\ntime git log --min-parents=2 --max-parents=2 $DIFF_FLAG | wc -l\n\n\nin linux.git (with v5.4 checked out, since my copy of linux is very out of\ndate) is as follows:\n\nDIFF_FLAG=--cc:            71m 31.536s\nDIFF_FLAG=--remerge-diff:  31m  3.170s\n\n\nNote that there are 62476 merges in this history. Also, output size is:\n\nDIFF_FLAG=--cc:            2169111 lines\nDIFF_FLAG=--remerge-diff:  2458020 lines\n\n\nSo roughly the same amount of output as --cc, as you'd expect.\n\nAs a side note: git log --remerge-diff, when run in various repositories and\nallowed to run all the way back to the beginning(s) of history, is a nice\nstress test of sorts for merge-ort. Especially when users run it for you on\ntheir repositories they are working on, whether intentionally or via a bug\nin a tool triggering that command to be run unexpectedly. Long story short,\nsuch a bug in an internal tool existed last December and this command was\nrun on an internal repository and found a platform-specific bug in merge-ort\non some really old merge commit from that repo. I fixed that bug (a\nSTABLE_QSORT thing) while upstreaming all the merge-ort patches in the mean\ntime, but it was nice getting extra testing. Having more folks run this on\ntheir repositories might be useful extra testing of the new merge strategy.\n\nAlso, I previously mentioned --remerge-diff-only (a flag to show how\ncherry-picks or reverts differ from an automatic cherry-pick or revert, in\naddition to showing how merges differ from an automatic merge). This series\ndoes not include the patches to introduce that option; I'll submit them\nlater.\n\nTwo other things that might be interesting but are not included and which I\nhaven't investigated:\n\n * some mechanism for passing extra merge options through (e.g.\n   -Xignore-space-change)\n * a capability to compare the automatic merge to a second automatic merge\n   done with different merge options. (Not sure if this would be of interest\n   to end users, but might be interesting while developing new a\n   --strategy-option, or maybe checking how changing some default in the\n   merge algorithm would affect historical merges in various repositories).\n\n[1] I have nebulous ideas of how an Octopus-centric ORT strategy could be\nwritten -- basically, just repeatedly invoking ort and trying to make sure\nnested conflicts can be differentiated. For now, though, a simple warning is\nprinted that octopus merges are not handled and no diff will be shown. [2]\nNew blobs/trees can be written by the three-way merging step. These are\nwritten to a temporary area (via tmp-objdir.c) under the git object store\nthat is cleaned up at the end of the operation, with the new loose objects\nfrom the remerge being cleaned up after each individual merge.\n\nElijah Newren (8):\n  show, log: provide a --remerge-diff capability\n  log: clean unneeded objects during `log --remerge-diff`\n  ll-merge: make callers responsible for showing warnings\n  merge-ort: capture and print ll-merge warnings in our preferred\n    fashion\n  merge-ort: mark a few more conflict messages as omittable\n  merge-ort: format messages slightly different for use in headers\n  diff: add ability to insert additional headers for paths\n  show, log: include conflict/warning messages in --remerge-diff headers\n\n Documentation/diff-options.txt |   8 ++\n apply.c                        |   5 +-\n builtin/checkout.c             |  12 ++-\n builtin/log.c                  |  15 +++\n diff-merges.c                  |  12 +++\n diff.c                         | 116 +++++++++++++++++++++-\n diff.h                         |   3 +-\n ll-merge.c                     |  40 ++++----\n ll-merge.h                     |   9 +-\n log-tree.c                     |  70 +++++++++++++-\n merge-blobs.c                  |   5 +-\n merge-ort.c                    |  47 ++++++++-\n merge-ort.h                    |  10 ++\n merge-recursive.c              |   8 +-\n merge-recursive.h              |   1 +\n notes-merge.c                  |   5 +-\n rerere.c                       |  12 ++-\n revision.h                     |   6 +-\n t/t4069-remerge-diff.sh        | 172 +++++++++++++++++++++++++++++++++\n t/t6404-recursive-merge.sh     |   9 +-\n t/t6406-merge-attr.sh          |   9 +-\n tmp-objdir.c                   |   5 +\n tmp-objdir.h                   |   6 ++\n 23 files changed, 538 insertions(+), 47 deletions(-)\n create mode 100755 t/t4069-remerge-diff.sh\n\n\nbase-commit: 4e44121c2d7bced65e25eb7ec5156290132bec94\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1103%2Fnewren%2Fremerge-diff-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1103/newren/remerge-diff-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/1103\n\nRange-diff vs v1:\n\n  8:  5d5846be0bd !  1:  b3ae62083e1 show, log: provide a --remerge-diff capability\n     @@ Commit message\n          possibly, just to hide other random changes).\n      \n          This capability works by creating a temporary object directory and\n     -    marking it as the primary object store, so that any blobs or trees\n     -    created during the automatic merge, can be easily removed afterwards by\n     -    just deleting all objects from the temporary object directory.  We can\n     -    do this after handling each merge commit, in order to avoid the need to\n     -    worry about doing `git gc --auto` runs while running `git log\n     -    --remerge-diff`.\n     +    marking it as the primary object store.  This makes it so that any blobs\n     +    or trees created during the automatic merge easily removable afterwards\n     +    by just deleting all objects from the temporary object directory.\n     +\n     +    There are a few ways that this implementation is suboptimal:\n     +      * `log --remerge-diff` becomes slow, because the temporary object\n     +        directory can fills with many loose objects while running\n     +      * the log output can be muddied with misplaced \"warning: cannot merge\n     +        binary files\" messages, since ll-merge.c unconditionally writes those\n     +        messages to stderr while running instead of allowing callers to\n     +        manage them.\n     +      * important conflict and warning messages are simply dropped; thus for\n     +        conflicts like modify/delete or rename/rename or file/directory which\n     +        are not representable with content conflict markers, there may be no\n     +        way for a user of --remerge-diff to know that there had been a\n     +        conflict which was resolved (and which possibly motivated other\n     +        changes in the merge commit).\n     +    Subsequent commits will address these issues.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     + ## Documentation/diff-options.txt ##\n     +@@ Documentation/diff-options.txt: ifdef::git-log[]\n     + \teach of the parents. Separate log entry and diff is generated\n     + \tfor each parent.\n     + +\n     ++--diff-merges=remerge:::\n     ++--diff-merges=r:::\n     ++--remerge-diff:::\n     ++\tWith this option, two-parent merge commits are remerged to\n     ++\tcreate a temporary tree object -- potentially containing files\n     ++\twith conflict markers and such.  A diff is then shown between\n     ++\tthat temporary tree and the actual merge commit.\n     +++\n     + --diff-merges=combined:::\n     + --diff-merges=c:::\n     + -c:::\n     +\n       ## builtin/log.c ##\n      @@\n       #include \"repository.h\"\n       #include \"commit-reach.h\"\n       #include \"range-diff.h\"\n     -+#include \"dir.h\"\n      +#include \"tmp-objdir.h\"\n       \n       #define MAIL_DEFAULT_WRAP 72\n       #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n      @@ builtin/log.c: static int cmd_log_walk(struct rev_info *rev)\n     + \tstruct commit *commit;\n       \tint saved_nrl = 0;\n       \tint saved_dcctc = 0;\n     - \n     ++\tstruct tmp_objdir *remerge_objdir = NULL;\n     ++\n      +\tif (rev->remerge_diff) {\n     -+\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n     -+\t\tif (!rev->remerge_objdir)\n     -+\t\t\tdie(_(\"unable to create temporary object directory\"));\n     -+\t\ttmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n     ++\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n     ++\t\tif (!remerge_objdir)\n     ++\t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n     ++\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n      +\t}\n     -+\n     + \n       \tif (rev->early_output)\n       \t\tsetup_early_output();\n     - \n      @@ builtin/log.c: static int cmd_log_walk(struct rev_info *rev)\n       \trev->diffopt.no_free = 0;\n       \tdiff_free(&rev->diffopt);\n       \n     -+\tif (rev->remerge_diff) {\n     -+\t\ttmp_objdir_destroy(rev->remerge_objdir);\n     -+\t\trev->remerge_objdir = NULL;\n     -+\t}\n     ++\tif (rev->remerge_diff)\n     ++\t\ttmp_objdir_destroy(remerge_objdir);\n      +\n       \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n       \t    rev->diffopt.flags.check_failed) {\n     @@ builtin/log.c: int cmd_format_patch(int argc, const char **argv, const char *pre\n       \tif (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF)\n       \t\tdie(_(\"--check does not make sense\"));\n      +\tif (rev.remerge_diff)\n     -+\t\tdie(_(\"--remerge_diff does not make sense\"));\n     ++\t\tdie(_(\"--remerge-diff does not make sense\"));\n       \n       \tif (!use_patch_format &&\n       \t\t(!rev.diffopt.output_format ||\n     @@ log-tree.c\n       #include \"config.h\"\n       #include \"diff.h\"\n       #include \"object-store.h\"\n     - #include \"repository.h\"\n     -+#include \"tmp-objdir.h\"\n     - #include \"commit.h\"\n     +@@\n       #include \"tag.h\"\n       #include \"graph.h\"\n       #include \"log-tree.h\"\n     @@ log-tree.c\n       #include \"reflog-walk.h\"\n       #include \"refs.h\"\n       #include \"string-list.h\"\n     -@@\n     - #include \"line-log.h\"\n     - #include \"help.h\"\n     - #include \"range-diff.h\"\n     -+#include \"dir.h\"\n     - \n     - static struct decoration name_decoration = { \"object names\" };\n     - static int decoration_loaded;\n      @@ log-tree.c: static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n       \treturn !opt->loginfo;\n       }\n     @@ log-tree.c: static int do_diff_combined(struct rev_info *opt, struct commit *com\n      +{\n      +\tstruct merge_options o;\n      +\tstruct commit_list *bases;\n     -+\tstruct merge_result res;\n     ++\tstruct merge_result res = {0};\n      +\tstruct pretty_print_context ctx = {0};\n     -+\tstruct strbuf commit1 = STRBUF_INIT;\n     -+\tstruct strbuf commit2 = STRBUF_INIT;\n     ++\tstruct commit *parent1 = parents->item;\n     ++\tstruct commit *parent2 = parents->next->item;\n     ++\tstruct strbuf parent1_desc = STRBUF_INIT;\n     ++\tstruct strbuf parent2_desc = STRBUF_INIT;\n      +\n      +\t/* Setup merge options */\n      +\tinit_merge_options(&o, the_repository);\n     -+\tmemset(&res, 0, sizeof(res));\n      +\to.show_rename_progress = 0;\n      +\n      +\tctx.abbrev = DEFAULT_ABBREV;\n     -+\tformat_commit_message(parents->item,       \"%h (%s)\", &commit1, &ctx);\n     -+\tformat_commit_message(parents->next->item, \"%h (%s)\", &commit2, &ctx);\n     -+\to.branch1 = commit1.buf;\n     -+\to.branch2 = commit2.buf;\n     -+\to.record_conflict_msgs_as_headers = 1;\n     ++\tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n     ++\tformat_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n     ++\to.branch1 = parent1_desc.buf;\n     ++\to.branch2 = parent2_desc.buf;\n      +\n      +\t/* Parse the relevant commits and get the merge bases */\n     -+\tparse_commit_or_die(parents->item);\n     -+\tparse_commit_or_die(parents->next->item);\n     -+\tbases = get_merge_bases(parents->item, parents->next->item);\n     ++\tparse_commit_or_die(parent1);\n     ++\tparse_commit_or_die(parent2);\n     ++\tbases = get_merge_bases(parent1, parent2);\n      +\n      +\t/* Re-merge the parents */\n     -+\tmerge_incore_recursive(&o,\n     -+\t\t\t       bases, parents->item, parents->next->item,\n     -+\t\t\t       &res);\n     ++\tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n      +\n      +\t/* Show the diff */\n     -+\topt->diffopt.additional_path_headers = res.path_messages;\n      +\tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n      +\tlog_tree_diff_flush(opt);\n      +\n      +\t/* Cleanup */\n     -+\topt->diffopt.additional_path_headers = NULL;\n     -+\tstrbuf_release(&commit1);\n     -+\tstrbuf_release(&commit2);\n     ++\tstrbuf_release(&parent1_desc);\n     ++\tstrbuf_release(&parent2_desc);\n      +\tmerge_finalize(&o, &res);\n     -+\n     -+\t/* Clean up the temporary object directory */\n     -+\tif (opt->remerge_objdir != NULL)\n     -+\t\ttmp_objdir_discard_objects(opt->remerge_objdir);\n     -+\telse\n     -+\t\tBUG(\"unable to remove temporary object directory\");\n     ++\t/* TODO: clean up the temporary object directory */\n      +\n      +\treturn !opt->loginfo;\n      +}\n     @@ revision.h: struct rev_info {\n       \n       \t/* Format info */\n       \tint\t\tshow_notes;\n     -@@ revision.h: struct rev_info {\n     - \n     - \t/* misc. flags related to '--no-kept-objects' */\n     - \tunsigned keep_pack_cache_flags;\n     +\n     + ## t/t4069-remerge-diff.sh (new) ##\n     +@@\n     ++#!/bin/sh\n      +\n     -+\t/* Location where temporary objects for remerge-diff are written. */\n     -+\tstruct tmp_objdir *remerge_objdir;\n     - };\n     - \n     - int ref_excluded(struct string_list *, const char *path);\n     ++test_description='remerge-diff handling'\n     ++\n     ++. ./test-lib.sh\n     ++\n     ++test_expect_success 'setup basic merges' '\n     ++\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n     ++\tgit add numbers &&\n     ++\tgit commit -m base &&\n     ++\n     ++\tgit branch feature_a &&\n     ++\tgit branch feature_b &&\n     ++\tgit branch feature_c &&\n     ++\n     ++\tgit branch ab_resolution &&\n     ++\tgit branch bc_resolution &&\n     ++\n     ++\tgit checkout feature_a &&\n     ++\ttest_write_lines 1 2 three 4 5 6 7 eight 9 >numbers &&\n     ++\tgit commit -a -m change_a &&\n     ++\n     ++\tgit checkout feature_b &&\n     ++\ttest_write_lines 1 2 tres 4 5 6 7 8 9 >numbers &&\n     ++\tgit commit -a -m change_b &&\n     ++\n     ++\tgit checkout feature_c &&\n     ++\ttest_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&\n     ++\tgit commit -a -m change_c &&\n     ++\n     ++\tgit checkout bc_resolution &&\n     ++\t# fast forward\n     ++\tgit merge feature_b &&\n     ++\t# no conflict\n     ++\tgit merge feature_c &&\n     ++\n     ++\tgit checkout ab_resolution &&\n     ++\t# fast forward\n     ++\tgit merge feature_a &&\n     ++\t# conflicts!\n     ++\ttest_must_fail git merge feature_b &&\n     ++\t# Resolve conflict...and make another change elsewhere\n     ++\ttest_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers &&\n     ++\tgit add numbers &&\n     ++\tgit merge --continue\n     ++'\n     ++\n     ++test_expect_success 'remerge-diff on a clean merge' '\n     ++\tgit log -1 --oneline bc_resolution >expect &&\n     ++\tgit show --oneline --remerge-diff bc_resolution >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' '\n     ++\tgit log -1 --oneline ab_resolution >tmp &&\n     ++\tcat <<-EOF >>tmp &&\n     ++\tdiff --git a/numbers b/numbers\n     ++\tindex a1fb731..6875544 100644\n     ++\t--- a/numbers\n     ++\t+++ b/numbers\n     ++\t@@ -1,13 +1,9 @@\n     ++\t 1\n     ++\t 2\n     ++\t-<<<<<<< b0ed5cb (change_a)\n     ++\t-three\n     ++\t-=======\n     ++\t-tres\n     ++\t->>>>>>> 6cd3f82 (change_b)\n     ++\t+drei\n     ++\t 4\n     ++\t 5\n     ++\t 6\n     ++\t 7\n     ++\t-eight\n     ++\t+acht\n     ++\t 9\n     ++\tEOF\n     ++\t# Hashes above are sha1; rip them out so test works with sha256\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n     ++\n     ++\tgit show --oneline --remerge-diff ab_resolution >tmp &&\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_done\n  1:  fab1b2c69ea !  2:  54f1fb31d04 tmp_objdir: add a helper function for discarding all contained objects\n     @@ Metadata\n      Author: Elijah Newren <newren@gmail.com>\n      \n       ## Commit message ##\n     -    tmp_objdir: add a helper function for discarding all contained objects\n     +    log: clean unneeded objects during `log --remerge-diff`\n     +\n     +    The --remerge-diff option will need to create new blobs and trees\n     +    representing the \"automatic merge\" state.  If one is traversing a\n     +    long project history, one can easily get hundreds of thousands of\n     +    loose objects generated during `log --remerge-diff`.  However, none of\n     +    those loose objects are needed after we have completed our diff\n     +    operation; they can be summarily deleted.\n     +\n     +    Add a new helper function to tmp_objdir to discard all the contained\n     +    objects, and call it after each merge is handled.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     + ## builtin/log.c ##\n     +@@ builtin/log.c: static int cmd_log_walk(struct rev_info *rev)\n     + \tstruct commit *commit;\n     + \tint saved_nrl = 0;\n     + \tint saved_dcctc = 0;\n     +-\tstruct tmp_objdir *remerge_objdir = NULL;\n     + \n     + \tif (rev->remerge_diff) {\n     +-\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n     +-\t\tif (!remerge_objdir)\n     ++\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n     ++\t\tif (!rev->remerge_objdir)\n     + \t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n     +-\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n     ++\t\ttmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n     + \t}\n     + \n     + \tif (rev->early_output)\n     +@@ builtin/log.c: static int cmd_log_walk(struct rev_info *rev)\n     + \trev->diffopt.no_free = 0;\n     + \tdiff_free(&rev->diffopt);\n     + \n     +-\tif (rev->remerge_diff)\n     +-\t\ttmp_objdir_destroy(remerge_objdir);\n     ++\tif (rev->remerge_diff) {\n     ++\t\ttmp_objdir_destroy(rev->remerge_objdir);\n     ++\t\trev->remerge_objdir = NULL;\n     ++\t}\n     + \n     + \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n     + \t    rev->diffopt.flags.check_failed) {\n     +\n     + ## log-tree.c ##\n     +@@\n     + #include \"diff.h\"\n     + #include \"object-store.h\"\n     + #include \"repository.h\"\n     ++#include \"tmp-objdir.h\"\n     + #include \"commit.h\"\n     + #include \"tag.h\"\n     + #include \"graph.h\"\n     +@@ log-tree.c: static int do_remerge_diff(struct rev_info *opt,\n     + \tstrbuf_release(&parent1_desc);\n     + \tstrbuf_release(&parent2_desc);\n     + \tmerge_finalize(&o, &res);\n     +-\t/* TODO: clean up the temporary object directory */\n     ++\n     ++\t/* Clean up the contents of the temporary object directory */\n     ++\tif (opt->remerge_objdir)\n     ++\t\ttmp_objdir_discard_objects(opt->remerge_objdir);\n     ++\telse\n     ++\t\tBUG(\"unable to remove temporary object directory\");\n     + \n     + \treturn !opt->loginfo;\n     + }\n     +\n     + ## revision.h ##\n     +@@ revision.h: struct rev_info {\n     + \n     + \t/* misc. flags related to '--no-kept-objects' */\n     + \tunsigned keep_pack_cache_flags;\n     ++\n     ++\t/* Location where temporary objects for remerge-diff are written. */\n     ++\tstruct tmp_objdir *remerge_objdir;\n     + };\n     + \n     + int ref_excluded(struct string_list *, const char *path);\n     +\n       ## tmp-objdir.c ##\n      @@ tmp-objdir.c: static void remove_tmp_objdir_on_signal(int signo)\n       \traise(signo);\n  2:  d022176618d !  3:  d5566f5d136 ll-merge: make callers responsible for showing warnings\n     @@ Commit message\n          from ll-merge and instead modify the return status of ll_merge() to\n          indicate when a merge of binary files has occurred.\n      \n     +    This commit continues printing the message as-is; future changes will\n     +    start handling the new commit differently in the merge-ort codepath.\n     +\n          Note that my methodology included first modifying ll_merge() to return\n          a struct, so that the compiler would catch all the callers for me and\n          ensure I had modified all of them.  After modifying all of them, I then\n     @@ apply.c: static int three_way_merge(struct apply_state *state,\n       \t\t\t  NULL);\n      +\tif (status == LL_MERGE_BINARY_CONFLICT)\n      +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n     -+\t\t\t\"base\", \"ours\", \"theirs\");\n     ++\t\t\tpath, \"ours\", \"theirs\");\n       \tfree(base_file.ptr);\n       \tfree(our_file.ptr);\n       \tfree(their_file.ptr);\n     @@ rerere.c: static int try_merge(struct index_state *istate,\n       \tmmfile_t base = {NULL, 0}, other = {NULL, 0};\n       \n       \tif (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n     - \t    read_mmfile(&other, rerere_path(id, \"postimage\")))\n     +-\t    read_mmfile(&other, rerere_path(id, \"postimage\")))\n      -\t\tret = 1;\n      -\telse\n     ++\t    read_mmfile(&other, rerere_path(id, \"postimage\"))) {\n      +\t\tret = LL_MERGE_CONFLICT;\n     -+\telse {\n     ++\t} else {\n       \t\t/*\n       \t\t * A three-way merge. Note that this honors user-customizable\n       \t\t * low-level merge driver settings.\n  3:  f36395fdee0 =  4:  a02845f12db merge-ort: capture and print ll-merge warnings in our preferred fashion\n  4:  1e7eef7705e !  5:  000933c5d7f merge-ort: mark a few more conflict messages as omittable\n     @@ Commit message\n          are just noise when trying to see what changes users made to create a\n          merge commit.  Mark them as omittable.\n      \n     +    Note that there were already a few messages marked as omittable in\n     +    merge-ort when doing a remerge-diff, because the development of\n     +    --remerge-diff preceded the upstreaming of merge-ort and I was trying to\n     +    ensure merge-ort could handle all the necessary requirements.  See\n     +    commit c5a6f65527 (\"merge-ort: add modify/delete handling and delayed\n     +    output processing\", 2020-12-03) for the initial details.  For some\n     +    examples of already-marked-as-omittable messages, see either\n     +    \"Auto-merging <path>\" or some of the submodule update hints.  This\n     +    commit just adds two more messages that should also be omittable.\n     +\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## merge-ort.c ##\n     @@ merge-ort.c: static void apply_directory_rename_modifications(struct merge_optio\n       \t\t\t\t _(\"CONFLICT (file location): %s renamed to %s \"\n       \t\t\t\t   \"in %s, inside a directory that was renamed \"\n       \t\t\t\t   \"in %s, suggesting it should perhaps be \"\n     -@@ merge-ort.c: static void process_entry(struct merge_options *opt,\n     - \t\t\t\treason = _(\"add/add\");\n     - \t\t\tif (S_ISGITLINK(merged_file.mode))\n     - \t\t\t\treason = _(\"submodule\");\n     --\t\t\tpath_msg(opt, path, 0,\n     -+\t\t\tpath_msg(opt, path, 1,\n     - \t\t\t\t _(\"CONFLICT (%s): Merge conflict in %s\"),\n     - \t\t\t\t reason, path);\n     - \t\t}\n  7:  b307f63569f !  6:  887e46435c0 merge-ort: format messages slightly different for use in headers\n     @@ Metadata\n       ## Commit message ##\n          merge-ort: format messages slightly different for use in headers\n      \n     -    We want to add an ability for users to run\n     +    When users run\n              git show --remerge-diff $MERGE_COMMIT\n     -    or even\n     +    or\n              git log -p --remerge-diff ...\n     -    and have git show the differences between where the merge machinery\n     -    would stop and what is recorded in merge commits.  However, in such\n     -    cases, stdout is not an appropriate location to dump conflict messages.\n     -    We instead want these messages to appear as headers in the subsequent\n     -    diff.  For them to work as headers, though, we need for any multiline\n     +    stdout is not an appropriate location to dump conflict messages, but we\n     +    do want to provide them to users.  We will include them in the diff\n     +    headers instead...but for that to work, we need for any multiline\n          messages to replace newlines with both a newline and a space.  Add a new\n          flag to signal when we want these messages modified in such a fashion,\n          and use it in path_msg() to modify these messages this way.\n  6:  15600df925f !  7:  e9470651303 diff: add ability to insert additional headers for paths\n     @@ Metadata\n       ## Commit message ##\n          diff: add ability to insert additional headers for paths\n      \n     -    In support of a remerge-diff ability we will add in a few commits, we\n     -    want to be able to provide additional headers to show along with a diff.\n     -    Add the plumbing necessary to enable this.\n     +    When additional headers are provided, we need to\n     +      * add diff_filepairs to diff_queued_diff for each paths in the\n     +        additional headers map which, unless that path is part of\n     +        another diff_filepair already found in diff_queued_diff\n     +      * format the headers (colorization, line_prefix for --graph)\n     +      * make sure the various codepaths that attempt to return early\n     +        if there are \"no changes\" take into account the headers that\n     +        need to be shown.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     @@ diff.c: struct userdiff_driver *get_textconv(struct repository *r,\n       \treturn userdiff_get_textconv(r, one->driver);\n       }\n       \n     -+static struct strbuf* additional_headers(struct diff_options *o,\n     ++static struct strbuf *additional_headers(struct diff_options *o,\n      +\t\t\t\t\t const char *path)\n      +{\n      +\tif (!o->additional_path_headers)\n     @@ diff.c: struct userdiff_driver *get_textconv(struct repository *r,\n      +{\n      +\tchar *next, *newline;\n      +\n     -+\tnext = more_headers->buf;\n     -+\twhile ((newline = strchr(next, '\\n'))) {\n     -+\t\t*newline = '\\0';\n     -+\t\tstrbuf_addf(msg, \"%s%s%s%s\\n\", line_prefix, meta, next, reset);\n     -+\t\t*newline = '\\n';\n     -+\t\tnext = newline + 1;\n     ++\tfor (next = more_headers->buf; *next; next = newline) {\n     ++\t\tnewline = strchrnul(next, '\\n');\n     ++\t\tstrbuf_addf(msg, \"%s%s%.*s%s\\n\", line_prefix, meta,\n     ++\t\t\t    (int)(newline - next), next, reset);\n     ++\t\tif (*newline)\n     ++\t\t\tnewline++;\n      +\t}\n     -+\tif (*next)\n     -+\t\tstrbuf_addf(msg, \"%s%s%s%s\\n\", line_prefix, meta, next, reset);\n      +}\n      +\n       static void builtin_diff(const char *name_a,\n       \t\t\t const char *name_b,\n       \t\t\t struct diff_filespec *one,\n     +@@ diff.c: static void builtin_diff(const char *name_a,\n     + \tb_two = quote_two(b_prefix, name_b + (*name_b == '/'));\n     + \tlbl[0] = DIFF_FILE_VALID(one) ? a_one : \"/dev/null\";\n     + \tlbl[1] = DIFF_FILE_VALID(two) ? b_two : \"/dev/null\";\n     ++\tif (!DIFF_FILE_VALID(one) && !DIFF_FILE_VALID(two)) {\n     ++\t\t/*\n     ++\t\t * We should only reach this point for pairs from\n     ++\t\t * create_filepairs_for_header_only_notifications().  For\n     ++\t\t * these, we should avoid the \"/dev/null\" special casing\n     ++\t\t * above, meaning we avoid showing such pairs as either\n     ++\t\t * \"new file\" or \"deleted file\" below.\n     ++\t\t */\n     ++\t\tlbl[0] = a_one;\n     ++\t\tlbl[1] = b_two;\n     ++\t}\n     + \tstrbuf_addf(&header, \"%s%sdiff --git %s %s%s\\n\", line_prefix, meta, a_one, b_two, reset);\n     + \tif (lbl[0][0] == '/') {\n     + \t\t/* /dev/null */\n      @@ diff.c: static void fill_metainfo(struct strbuf *msg,\n       \tconst char *set = diff_get_color(use_color, DIFF_METAINFO);\n       \tconst char *reset = diff_get_color(use_color, DIFF_RESET);\n     @@ diff.c: static void fill_metainfo(struct strbuf *msg,\n       \n       \t*must_show_header = 1;\n       \tstrbuf_init(msg, PATH_MAX * 2 + 300);\n     -+\tif ((more_headers = additional_headers(o, name)))\n     +@@ diff.c: static void fill_metainfo(struct strbuf *msg,\n     + \tdefault:\n     + \t\t*must_show_header = 0;\n     + \t}\n     ++\tif ((more_headers = additional_headers(o, name))) {\n      +\t\tadd_formatted_headers(msg, more_headers,\n      +\t\t\t\t      line_prefix, set, reset);\n     - \tswitch (p->status) {\n     - \tcase DIFF_STATUS_COPIED:\n     - \t\tstrbuf_addf(msg, \"%s%ssimilarity index %d%%\",\n     ++\t\t*must_show_header = 1;\n     ++\t}\n     + \tif (one && two && !oideq(&one->oid, &two->oid)) {\n     + \t\tconst unsigned hexsz = the_hash_algo->hexsz;\n     + \t\tint abbrev = o->abbrev ? o->abbrev : DEFAULT_ABBREV;\n      @@ diff.c: int diff_unmodified_pair(struct diff_filepair *p)\n       \n       static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n       {\n      -\tif (diff_unmodified_pair(p))\n     ++\t/*\n     ++\t * Check if we can return early without showing a diff.  Note that\n     ++\t * diff_filepair only stores {oid, path, mode, is_valid}\n     ++\t * information for each path, and thus diff_unmodified_pair() only\n     ++\t * considers those bits of info.  However, we do not want pairs\n     ++\t * created by create_filepairs_for_header_only_notifications() to\n     ++\t * be ignored, so return early if both p is unmodified AND\n     ++\t * p->one->path is not in additional headers.\n     ++\t */\n      +\tif (diff_unmodified_pair(p) && !additional_headers(o, p->one->path))\n       \t\treturn;\n       \n     ++\t/* Actually, we can also return early to avoid showing tree diffs */\n       \tif ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||\n     + \t    (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))\n     +-\t\treturn; /* no tree diffs in patch format */\n     ++\t\treturn;\n     + \n     + \trun_diff(p, o);\n     + }\n     +@@ diff.c: static void diff_flush_checkdiff(struct diff_filepair *p,\n     + \trun_checkdiff(p, o);\n     + }\n     + \n     +-int diff_queue_is_empty(void)\n     ++int diff_queue_is_empty(struct diff_options *o)\n     + {\n     + \tstruct diff_queue_struct *q = &diff_queued_diff;\n     + \tint i;\n     ++\n     ++\tif (o->additional_path_headers &&\n     ++\t    !strmap_empty(o->additional_path_headers))\n     ++\t\treturn 0;\n     + \tfor (i = 0; i < q->nr; i++)\n     + \t\tif (!diff_unmodified_pair(q->queue[i]))\n     + \t\t\treturn 0;\n     +@@ diff.c: void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)\n     + \t\twarning(_(rename_limit_advice), varname, needed);\n     + }\n     + \n     ++static void create_filepairs_for_header_only_notifications(struct diff_options *o)\n     ++{\n     ++\tstruct strset present;\n     ++\tstruct diff_queue_struct *q = &diff_queued_diff;\n     ++\tstruct hashmap_iter iter;\n     ++\tstruct strmap_entry *e;\n     ++\tint i;\n     ++\n     ++\tstrset_init_with_options(&present, /*pool*/ NULL, /*strdup*/ 0);\n     ++\n     ++\t/*\n     ++\t * Find out which paths exist in diff_queued_diff, preferring\n     ++\t * one->path for any pair that has multiple paths.\n     ++\t */\n     ++\tfor (i = 0; i < q->nr; i++) {\n     ++\t\tstruct diff_filepair *p = q->queue[i];\n     ++\t\tchar *path = p->one->path ? p->one->path : p->two->path;\n     ++\n     ++\t\tif (strmap_contains(o->additional_path_headers, path))\n     ++\t\t\tstrset_add(&present, path);\n     ++\t}\n     ++\n     ++\t/*\n     ++\t * Loop over paths in additional_path_headers; for each NOT already\n     ++\t * in diff_queued_diff, create a synthetic filepair and insert that\n     ++\t * into diff_queued_diff.\n     ++\t */\n     ++\tstrmap_for_each_entry(o->additional_path_headers, &iter, e) {\n     ++\t\tif (!strset_contains(&present, e->key)) {\n     ++\t\t\tstruct diff_filespec *one, *two;\n     ++\t\t\tstruct diff_filepair *p;\n     ++\n     ++\t\t\tone = alloc_filespec(e->key);\n     ++\t\t\ttwo = alloc_filespec(e->key);\n     ++\t\t\tfill_filespec(one, null_oid(), 0, 0);\n     ++\t\t\tfill_filespec(two, null_oid(), 0, 0);\n     ++\t\t\tp = diff_queue(q, one, two);\n     ++\t\t\tp->status = DIFF_STATUS_MODIFIED;\n     ++\t\t}\n     ++\t}\n     ++\n     ++\t/* Re-sort the filepairs */\n     ++\tdiffcore_fix_diff_index();\n     ++\n     ++\t/* Cleanup */\n     ++\tstrset_clear(&present);\n     ++}\n     ++\n     + static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n     + {\n     + \tint i;\n     +@@ diff.c: static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n     + \tif (o->color_moved)\n     + \t\to->emitted_symbols = &esm;\n     + \n     ++\tif (o->additional_path_headers)\n     ++\t\tcreate_filepairs_for_header_only_notifications(o);\n     ++\n     + \tfor (i = 0; i < q->nr; i++) {\n     + \t\tstruct diff_filepair *p = q->queue[i];\n     + \t\tif (check_pair_status(p))\n     +@@ diff.c: void diff_flush(struct diff_options *options)\n     + \t * Order: raw, stat, summary, patch\n     + \t * or:    name/name-status/checkdiff (other bits clear)\n     + \t */\n     +-\tif (!q->nr)\n     ++\tif (!q->nr && !options->additional_path_headers)\n     + \t\tgoto free_queue;\n     + \n     + \tif (output_format & (DIFF_FORMAT_RAW |\n      \n       ## diff.h ##\n      @@ diff.h: struct diff_options {\n     @@ diff.h: struct diff_options {\n       \n       \tint no_free;\n       };\n     +@@ diff.h: void diffcore_fix_diff_index(void);\n     + \"                show all files diff when -S is used and hit is found.\\n\" \\\n     + \"  -a  --text    treat all files as text.\\n\"\n     + \n     +-int diff_queue_is_empty(void);\n     ++int diff_queue_is_empty(struct diff_options*);\n     + void diff_flush(struct diff_options*);\n     + void diff_free(struct diff_options*);\n     + void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);\n     +\n     + ## log-tree.c ##\n     +@@ log-tree.c: int log_tree_diff_flush(struct rev_info *opt)\n     + \topt->shown_dashes = 0;\n     + \tdiffcore_std(&opt->diffopt);\n     + \n     +-\tif (diff_queue_is_empty()) {\n     ++\tif (diff_queue_is_empty(&opt->diffopt)) {\n     + \t\tint saved_fmt = opt->diffopt.output_format;\n     + \t\topt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT;\n     + \t\tdiff_flush(&opt->diffopt);\n  5:  dd5461d45de !  8:  4cc53c55a6e merge-ort: make path_messages available to external callers\n     @@ Metadata\n      Author: Elijah Newren <newren@gmail.com>\n      \n       ## Commit message ##\n     -    merge-ort: make path_messages available to external callers\n     +    show, log: include conflict/warning messages in --remerge-diff headers\n      \n     -    merge-ort is designed to be more flexible so that it could be called as\n     -    more of a library function.  Part of that design is not writing to the\n     -    working tree or index unless and until requested.  Part of it is\n     -    returning tree objects (rather than creating commits and making them\n     -    part of HEAD), and allowing callers to do their own special thing with\n     -    that merged tree.  Along the same lines, we want to enable callers to do\n     -    something special with output messages (conflicts and other warnings)\n     -    besides just automatically displaying on stdout/stderr.  Do so by making\n     -    the output path messages accessible via a new member of struct\n     -    merge_result named path_messages.\n     +    Conflicts such as modify/delete, rename/rename, or file/directory are\n     +    not representable via content conflict markers, and the normal output\n     +    messages notifying users about these were dropped with --remerge-diff.\n     +    While we don't want these messages randomly shown before the commit\n     +    and diff headers, we do want them to still be shown; include them as\n     +    part of the diff headers instead.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     + ## log-tree.c ##\n     +@@ log-tree.c: static int do_remerge_diff(struct rev_info *opt,\n     + \t/* Setup merge options */\n     + \tinit_merge_options(&o, the_repository);\n     + \to.show_rename_progress = 0;\n     ++\to.record_conflict_msgs_as_headers = 1;\n     + \n     + \tctx.abbrev = DEFAULT_ABBREV;\n     + \tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n     +@@ log-tree.c: static int do_remerge_diff(struct rev_info *opt,\n     + \tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n     + \n     + \t/* Show the diff */\n     ++\topt->diffopt.additional_path_headers = res.path_messages;\n     + \tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n     + \tlog_tree_diff_flush(opt);\n     + \n     + \t/* Cleanup */\n     ++\topt->diffopt.additional_path_headers = NULL;\n     + \tstrbuf_release(&parent1_desc);\n     + \tstrbuf_release(&parent2_desc);\n     + \tmerge_finalize(&o, &res);\n     +\n       ## merge-ort.c ##\n      @@ merge-ort.c: redo:\n       \ttrace2_region_leave(\"merge\", \"process_entries\", opt->repo);\n     @@ merge-ort.h: struct merge_result {\n       \t/*\n       \t * Additional metadata used by merge_switch_to_result() or future calls\n       \t * to merge_incore_*().  Includes data needed to update the index (if\n     +\n     + ## t/t4069-remerge-diff.sh ##\n     +@@ t/t4069-remerge-diff.sh: test_description='remerge-diff handling'\n     + \n     + . ./test-lib.sh\n     + \n     ++# --remerge-diff uses ort under the hood regardless of setting.  However,\n     ++# we set up a file/directory conflict beforehand, and the different backends\n     ++# handle the conflict differently, which would require separate code paths\n     ++# to resolve.  There's not much point in making the code uglier to do that,\n     ++# though, when the real thing we are testing (--remerge-diff) will hardcode\n     ++# calls directly into the merge-ort API anyway.  So just force the use of\n     ++# ort on the setup too.\n     ++GIT_TEST_MERGE_ALGORITHM=ort\n     ++\n     + test_expect_success 'setup basic merges' '\n     + \ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n     + \tgit add numbers &&\n     +@@ t/t4069-remerge-diff.sh: test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n     + \tgit log -1 --oneline ab_resolution >tmp &&\n     + \tcat <<-EOF >>tmp &&\n     + \tdiff --git a/numbers b/numbers\n     ++\tCONFLICT (content): Merge conflict in numbers\n     + \tindex a1fb731..6875544 100644\n     + \t--- a/numbers\n     + \t+++ b/numbers\n     +@@ t/t4069-remerge-diff.sh: test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n     + \ttest_cmp expect actual\n     + '\n     + \n     ++test_expect_success 'setup non-content conflicts' '\n     ++\tgit switch --orphan base &&\n     ++\n     ++\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n     ++\ttest_write_lines a b c d e f g h i >letters &&\n     ++\ttest_write_lines in the way >content &&\n     ++\tgit add numbers letters content &&\n     ++\tgit commit -m base &&\n     ++\n     ++\tgit branch side1 &&\n     ++\tgit branch side2 &&\n     ++\n     ++\tgit checkout side1 &&\n     ++\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n     ++\tgit mv letters letters_side1 &&\n     ++\tgit mv content file_or_directory &&\n     ++\tgit add numbers &&\n     ++\tgit commit -m side1 &&\n     ++\n     ++\tgit checkout side2 &&\n     ++\tgit rm numbers &&\n     ++\tgit mv letters letters_side2 &&\n     ++\tmkdir file_or_directory &&\n     ++\techo hello >file_or_directory/world &&\n     ++\tgit add file_or_directory/world &&\n     ++\tgit commit -m side2 &&\n     ++\n     ++\tgit checkout -b resolution side1 &&\n     ++\ttest_must_fail git merge side2 &&\n     ++\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n     ++\tgit add numbers &&\n     ++\tgit add letters_side1 &&\n     ++\tgit rm letters &&\n     ++\tgit rm letters_side2 &&\n     ++\tgit add file_or_directory~HEAD &&\n     ++\tgit mv file_or_directory~HEAD wanted_content &&\n     ++\tgit commit -m resolved\n     ++'\n     ++\n     ++test_expect_success 'remerge-diff with non-content conflicts' '\n     ++\tgit log -1 --oneline resolution >tmp &&\n     ++\tcat <<-EOF >>tmp &&\n     ++\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n     ++\tsimilarity index 100%\n     ++\trename from file_or_directory~HASH (side1)\n     ++\trename to wanted_content\n     ++\tCONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n     ++\tdiff --git a/letters b/letters\n     ++\tCONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n     ++\tdiff --git a/letters_side2 b/letters_side2\n     ++\tdeleted file mode 100644\n     ++\tindex b236ae5..0000000\n     ++\t--- a/letters_side2\n     ++\t+++ /dev/null\n     ++\t@@ -1,9 +0,0 @@\n     ++\t-a\n     ++\t-b\n     ++\t-c\n     ++\t-d\n     ++\t-e\n     ++\t-f\n     ++\t-g\n     ++\t-h\n     ++\t-i\n     ++\tdiff --git a/numbers b/numbers\n     ++\tCONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n     ++\tEOF\n     ++\t# We still have some sha1 hashes above; rip them out so test works\n     ++\t# with sha256\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n     ++\n     ++\tgit show --oneline --remerge-diff resolution >tmp &&\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     + test_done\n  9:  4f21969e357 <  -:  ----------- doc/diff-options: explain the new --remerge-diff option\n\n-- \ngitgitgadget\n"},{"id":"444944","messageId":"54f1fb31d04e5a7e216554dd4d13ccd896e03fc2.1640419159.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"[PATCH v2 2/8] log: clean unneeded objects during `log --remerge-diff`","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-25T07:59:13Z","receivedAt":"2021-12-25T07:59:29Z","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 --remerge-diff option will need to create new blobs and trees\nrepresenting the \"automatic merge\" state.  If one is traversing a\nlong project history, one can easily get hundreds of thousands of\nloose objects generated during `log --remerge-diff`.  However, none of\nthose loose objects are needed after we have completed our diff\noperation; they can be summarily deleted.\n\nAdd a new helper function to tmp_objdir to discard all the contained\nobjects, and call it after each merge is handled.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/log.c | 13 +++++++------\n log-tree.c    |  8 +++++++-\n revision.h    |  3 +++\n tmp-objdir.c  |  5 +++++\n tmp-objdir.h  |  6 ++++++\n 5 files changed, 28 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex d053418fddd..e6a080df914 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -407,13 +407,12 @@ static int cmd_log_walk(struct rev_info *rev)\n \tstruct commit *commit;\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n-\tstruct tmp_objdir *remerge_objdir = NULL;\n \n \tif (rev->remerge_diff) {\n-\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n-\t\tif (!remerge_objdir)\n+\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\tif (!rev->remerge_objdir)\n \t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n-\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n+\t\ttmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n \t}\n \n \tif (rev->early_output)\n@@ -458,8 +457,10 @@ static int cmd_log_walk(struct rev_info *rev)\n \trev->diffopt.no_free = 0;\n \tdiff_free(&rev->diffopt);\n \n-\tif (rev->remerge_diff)\n-\t\ttmp_objdir_destroy(remerge_objdir);\n+\tif (rev->remerge_diff) {\n+\t\ttmp_objdir_destroy(rev->remerge_objdir);\n+\t\trev->remerge_objdir = NULL;\n+\t}\n \n \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n \t    rev->diffopt.flags.check_failed) {\ndiff --git a/log-tree.c b/log-tree.c\nindex 84ed864fc81..d4655b63d75 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -4,6 +4,7 @@\n #include \"diff.h\"\n #include \"object-store.h\"\n #include \"repository.h\"\n+#include \"tmp-objdir.h\"\n #include \"commit.h\"\n #include \"tag.h\"\n #include \"graph.h\"\n@@ -944,7 +945,12 @@ static int do_remerge_diff(struct rev_info *opt,\n \tstrbuf_release(&parent1_desc);\n \tstrbuf_release(&parent2_desc);\n \tmerge_finalize(&o, &res);\n-\t/* TODO: clean up the temporary object directory */\n+\n+\t/* Clean up the contents of the temporary object directory */\n+\tif (opt->remerge_objdir)\n+\t\ttmp_objdir_discard_objects(opt->remerge_objdir);\n+\telse\n+\t\tBUG(\"unable to remove temporary object directory\");\n \n \treturn !opt->loginfo;\n }\ndiff --git a/revision.h b/revision.h\nindex 13178e6b8f3..44efce3f410 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -318,6 +318,9 @@ struct rev_info {\n \n \t/* misc. flags related to '--no-kept-objects' */\n \tunsigned keep_pack_cache_flags;\n+\n+\t/* Location where temporary objects for remerge-diff are written. */\n+\tstruct tmp_objdir *remerge_objdir;\n };\n \n int ref_excluded(struct string_list *, const char *path);\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex 3d38eeab66b..adf6033549e 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -79,6 +79,11 @@ static void remove_tmp_objdir_on_signal(int signo)\n \traise(signo);\n }\n \n+void tmp_objdir_discard_objects(struct tmp_objdir *t)\n+{\n+\tremove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n+}\n+\n /*\n  * These env_* functions are for setting up the child environment; the\n  * \"replace\" variant overrides the value of any existing variable with that\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex cda5ec76778..76efc7edee5 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -46,6 +46,12 @@ int tmp_objdir_migrate(struct tmp_objdir *);\n  */\n int tmp_objdir_destroy(struct tmp_objdir *);\n \n+/*\n+ * Remove all objects from the temporary object directory, while leaving it\n+ * around so more objects can be added.\n+ */\n+void tmp_objdir_discard_objects(struct tmp_objdir *);\n+\n /*\n  * Add the temporary object directory as an alternate object store in the\n  * current process.\n-- \ngitgitgadget\n\n"},{"id":"444946","messageId":"d5566f5d13605f30be6fd221fc624479cbbd0392.1640419159.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"[PATCH v2 3/8] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-25T07:59:14Z","receivedAt":"2021-12-25T07:59:36Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince some callers may want to send warning messages to somewhere other\nthan stdout/stderr, stop printing \"warning: Cannot merge binary files\"\nfrom ll-merge and instead modify the return status of ll_merge() to\nindicate when a merge of binary files has occurred.\n\nThis commit continues printing the message as-is; future changes will\nstart handling the new commit differently in the merge-ort codepath.\n\nNote that my methodology included first modifying ll_merge() to return\na struct, so that the compiler would catch all the callers for me and\nensure I had modified all of them.  After modifying all of them, I then\nchanged the struct to an enum.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n apply.c            |  5 ++++-\n builtin/checkout.c | 12 ++++++++----\n ll-merge.c         | 40 ++++++++++++++++++++++------------------\n ll-merge.h         |  9 ++++++++-\n merge-blobs.c      |  5 ++++-\n merge-ort.c        |  5 ++++-\n merge-recursive.c  |  5 ++++-\n notes-merge.c      |  5 ++++-\n rerere.c           | 12 ++++++++----\n 9 files changed, 66 insertions(+), 32 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex 43a0aebf4ee..8079395755f 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -3492,7 +3492,7 @@ static int three_way_merge(struct apply_state *state,\n {\n \tmmfile_t base_file, our_file, their_file;\n \tmmbuffer_t result = { NULL };\n-\tint status;\n+\tenum ll_merge_result status;\n \n \t/* resolve trivial cases first */\n \tif (oideq(base, ours))\n@@ -3509,6 +3509,9 @@ static int three_way_merge(struct apply_state *state,\n \t\t\t  &their_file, \"theirs\",\n \t\t\t  state->repo->index,\n \t\t\t  NULL);\n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \"ours\", \"theirs\");\n \tfree(base_file.ptr);\n \tfree(our_file.ptr);\n \tfree(their_file.ptr);\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex cbf73b8c9f6..3a559d69303 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -237,6 +237,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tstruct cache_entry *ce = active_cache[pos];\n \tconst char *path = ce->name;\n \tmmfile_t ancestor, ours, theirs;\n+\tenum ll_merge_result merge_status;\n \tint status;\n \tstruct object_id oid;\n \tmmbuffer_t result_buf;\n@@ -267,13 +268,16 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tmemset(&ll_opts, 0, sizeof(ll_opts));\n \tgit_config_get_bool(\"merge.renormalize\", &renormalize);\n \tll_opts.renormalize = renormalize;\n-\tstatus = ll_merge(&result_buf, path, &ancestor, \"base\",\n-\t\t\t  &ours, \"ours\", &theirs, \"theirs\",\n-\t\t\t  state->istate, &ll_opts);\n+\tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n+\t\t\t\t&ours, \"ours\", &theirs, \"theirs\",\n+\t\t\t\tstate->istate, &ll_opts);\n \tfree(ancestor.ptr);\n \tfree(ours.ptr);\n \tfree(theirs.ptr);\n-\tif (status < 0 || !result_buf.ptr) {\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \"ours\", \"theirs\");\n+\tif (merge_status < 0 || !result_buf.ptr) {\n \t\tfree(result_buf.ptr);\n \t\treturn error(_(\"path '%s': cannot merge\"), path);\n \t}\ndiff --git a/ll-merge.c b/ll-merge.c\nindex 261657578c7..669c09eed6c 100644\n--- a/ll-merge.c\n+++ b/ll-merge.c\n@@ -14,7 +14,7 @@\n \n struct ll_merge_driver;\n \n-typedef int (*ll_merge_fn)(const struct ll_merge_driver *,\n+typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -49,7 +49,7 @@ void reset_merge_attributes(void)\n /*\n  * Built-in low-levels\n  */\n-static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -58,6 +58,7 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   const struct ll_merge_options *opts,\n \t\t\t   int marker_size)\n {\n+\tenum ll_merge_result ret;\n \tmmfile_t *stolen;\n \tassert(opts);\n \n@@ -68,16 +69,19 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t */\n \tif (opts->virtual_ancestor) {\n \t\tstolen = orig;\n+\t\tret = LL_MERGE_OK;\n \t} else {\n \t\tswitch (opts->variant) {\n \t\tdefault:\n-\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\t\tpath, name1, name2);\n-\t\t\t/* fallthru */\n+\t\t\tret = LL_MERGE_BINARY_CONFLICT;\n+\t\t\tstolen = src1;\n+\t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_OURS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src1;\n \t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_THEIRS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src2;\n \t\t\tbreak;\n \t\t}\n@@ -87,16 +91,10 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \tresult->size = stolen->size;\n \tstolen->ptr = NULL;\n \n-\t/*\n-\t * With -Xtheirs or -Xours, we have cleanly merged;\n-\t * otherwise we got a conflict.\n-\t */\n-\treturn opts->variant == XDL_MERGE_FAVOR_OURS ||\n-\t       opts->variant == XDL_MERGE_FAVOR_THEIRS ?\n-\t       0 : 1;\n+\treturn ret;\n }\n \n-static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -105,7 +103,9 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tconst struct ll_merge_options *opts,\n \t\t\tint marker_size)\n {\n+\tenum ll_merge_result ret;\n \txmparam_t xmp;\n+\tint status;\n \tassert(opts);\n \n \tif (orig->size > MAX_XDIFF_SIZE ||\n@@ -133,10 +133,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \txmp.ancestor = orig_name;\n \txmp.file1 = name1;\n \txmp.file2 = name2;\n-\treturn xdl_merge(orig, src1, src2, &xmp, result);\n+\tstatus = xdl_merge(orig, src1, src2, &xmp, result);\n+\tret = (status > 1 ) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n-static int ll_union_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_union_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t  mmbuffer_t *result,\n \t\t\t  const char *path,\n \t\t\t  mmfile_t *orig, const char *orig_name,\n@@ -178,7 +180,7 @@ static void create_temp(mmfile_t *src, char *path, size_t len)\n /*\n  * User defined low-level merge driver support.\n  */\n-static int ll_ext_merge(const struct ll_merge_driver *fn,\n+static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -194,6 +196,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \tconst char *args[] = { NULL, NULL };\n \tint status, fd, i;\n \tstruct stat st;\n+\tenum ll_merge_result ret;\n \tassert(opts);\n \n \tsq_quote_buf(&path_sq, path);\n@@ -236,7 +239,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \t\tunlink_or_warn(temp[i]);\n \tstrbuf_release(&cmd);\n \tstrbuf_release(&path_sq);\n-\treturn status;\n+\tret = (status > 1) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n /*\n@@ -362,7 +366,7 @@ static void normalize_file(mmfile_t *mm, const char *path, struct index_state *i\n \t}\n }\n \n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/ll-merge.h b/ll-merge.h\nindex aceb1b24132..e4a20e81a3a 100644\n--- a/ll-merge.h\n+++ b/ll-merge.h\n@@ -82,13 +82,20 @@ struct ll_merge_options {\n \tlong xdl_opts;\n };\n \n+enum ll_merge_result {\n+\tLL_MERGE_ERROR = -1,\n+\tLL_MERGE_OK = 0,\n+\tLL_MERGE_CONFLICT,\n+\tLL_MERGE_BINARY_CONFLICT,\n+};\n+\n /**\n  * Perform a three-way single-file merge in core.  This is a thin wrapper\n  * around `xdl_merge` that takes the path and any merge backend specified in\n  * `.gitattributes` or `.git/info/attributes` into account.\n  * Returns 0 for a clean merge.\n  */\n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/merge-blobs.c b/merge-blobs.c\nindex ee0a0e90c94..8138090f81c 100644\n--- a/merge-blobs.c\n+++ b/merge-blobs.c\n@@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\t mmfile_t *their,\n \t\t\t\t unsigned long *size)\n {\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \tmmbuffer_t res;\n \n \t/*\n@@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\tistate, NULL);\n \tif (merge_status < 0)\n \t\treturn NULL;\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \".our\", \".their\");\n \n \t*size = res.size;\n \treturn res.ptr;\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 0342f104836..c24da2ba3cb 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1743,7 +1743,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tif (!opt->priv->attr_index.initialized)\n \t\tinitialize_attr_index(opt);\n@@ -1787,6 +1787,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex d9457797dbb..bc73c52dd84 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tll_opts.renormalize = opt->renormalize;\n \tll_opts.extra_marker_size = extra_marker_size;\n@@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, a->path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\topt->repo->index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\ta->path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/notes-merge.c b/notes-merge.c\nindex b4a3a903e86..01d596920ea 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n {\n \tmmbuffer_t result_buf;\n \tmmfile_t base, local, remote;\n-\tint status;\n+\tenum ll_merge_result status;\n \n \tread_mmblob(&base, &p->base);\n \tread_mmblob(&local, &p->local);\n@@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n \tfree(local.ptr);\n \tfree(remote.ptr);\n \n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\toid_to_hex(&p->obj), o->local_ref, o->remote_ref);\n \tif ((status < 0) || !result_buf.ptr)\n \t\tdie(\"Failed to execute internal merge\");\n \ndiff --git a/rerere.c b/rerere.c\nindex d83d58df4fb..b1f8961ed9e 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -609,19 +609,23 @@ static int try_merge(struct index_state *istate,\n \t\t     const struct rerere_id *id, const char *path,\n \t\t     mmfile_t *cur, mmbuffer_t *result)\n {\n-\tint ret;\n+\tenum ll_merge_result ret;\n \tmmfile_t base = {NULL, 0}, other = {NULL, 0};\n \n \tif (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n-\t    read_mmfile(&other, rerere_path(id, \"postimage\")))\n-\t\tret = 1;\n-\telse\n+\t    read_mmfile(&other, rerere_path(id, \"postimage\"))) {\n+\t\tret = LL_MERGE_CONFLICT;\n+\t} else {\n \t\t/*\n \t\t * A three-way merge. Note that this honors user-customizable\n \t\t * low-level merge driver settings.\n \t\t */\n \t\tret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n \t\t\t       istate, NULL);\n+\t\tif (ret == LL_MERGE_BINARY_CONFLICT)\n+\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\t\tpath, \"\", \"\");\n+\t}\n \n \tfree(base.ptr);\n \tfree(other.ptr);\n-- \ngitgitgadget\n\n"},{"id":"444945","messageId":"a02845f12dbca3ba45973151da2504a78070898d.1640419160.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"[PATCH v2 4/8] merge-ort: capture and print ll-merge warnings in our preferred fashion","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-25T07:59:15Z","receivedAt":"2021-12-25T07:59:37Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nInstead of immediately printing ll-merge warnings to stderr, we save\nthem in our output strbuf.  Besides allowing us to move these warnings\nto a special file for --remerge-diff, this has two other benefits for\nregular merges done by merge-ort:\n\n  * The deferral of messages ensures we can print all messages about\n    any given path together (merge-recursive was known to sometimes\n    intersperse messages about other paths, particularly when renames\n    were involved).\n\n  * The deferral of messages means we can avoid printing spurious\n    conflict messages when we just end up aborting due to local user\n    modifications in the way.  (In contrast to merge-recursive.c which\n    prematurely checks for local modifications in the way via\n    unpack_trees() and gets the check wrong both in terms of false\n    positives and false negatives relative to renames, merge-ort does\n    not perform the local modifications in the way check until the\n    checkout() step after the full merge has been computed.)\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c                | 5 +++--\n t/t6404-recursive-merge.sh | 9 +++++++--\n t/t6406-merge-attr.sh      | 9 +++++++--\n 3 files changed, 17 insertions(+), 6 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex c24da2ba3cb..a18f47e23c5 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1788,8 +1788,9 @@ static int merge_3way(struct merge_options *opt,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n \tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n-\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\tpath, name1, name2);\n+\t\tpath_msg(opt, path, 0,\n+\t\t\t \"warning: Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\t path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/t/t6404-recursive-merge.sh b/t/t6404-recursive-merge.sh\nindex eaf48e941e2..b8735c6db4d 100755\n--- a/t/t6404-recursive-merge.sh\n+++ b/t/t6404-recursive-merge.sh\n@@ -108,8 +108,13 @@ test_expect_success 'refuse to merge binary files' '\n \tprintf \"\\0\\0\" >binary-file &&\n \tgit add binary-file &&\n \tgit commit -m binary2 &&\n-\ttest_must_fail git merge F >merge.out 2>merge.err &&\n-\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge.err\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge F >merge_output\n+\telse\n+\t\ttest_must_fail git merge F 2>merge_output\n+\tfi &&\n+\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge_output\n '\n \n test_expect_success 'mark rename/delete as unmerged' '\ndiff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh\nindex 84946458371..c41584eb33e 100755\n--- a/t/t6406-merge-attr.sh\n+++ b/t/t6406-merge-attr.sh\n@@ -221,8 +221,13 @@ test_expect_success 'binary files with union attribute' '\n \tprintf \"two\\0\" >bin.txt &&\n \tgit commit -am two &&\n \n-\ttest_must_fail git merge bin-main 2>stderr &&\n-\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" stderr\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge bin-main >output\n+\telse\n+\t\ttest_must_fail git merge bin-main 2>output\n+\tfi &&\n+\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" output\n '\n \n test_done\n-- \ngitgitgadget\n\n"},{"id":"444947","messageId":"000933c5d7ffe3431b141ba3c9947ba62fab278d.1640419160.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"[PATCH v2 5/8] merge-ort: mark a few more conflict messages as omittable","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-25T07:59:16Z","receivedAt":"2021-12-25T07:59:38Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\npath_msg() has the ability to mark messages as omittable, designed for\nremerge-diff where we'll instead be showing conflict messages as diff\nheaders for a subsequent diff.  While all these messages are very useful\nwhen trying to create a merge initially, early use with the\n--remerge-diff feature (the only user of this omittable conflict message\ncapability), suggests that the particular messages marked in this commit\nare just noise when trying to see what changes users made to create a\nmerge commit.  Mark them as omittable.\n\nNote that there were already a few messages marked as omittable in\nmerge-ort when doing a remerge-diff, because the development of\n--remerge-diff preceded the upstreaming of merge-ort and I was trying to\nensure merge-ort could handle all the necessary requirements.  See\ncommit c5a6f65527 (\"merge-ort: add modify/delete handling and delayed\noutput processing\", 2020-12-03) for the initial details.  For some\nexamples of already-marked-as-omittable messages, see either\n\"Auto-merging <path>\" or some of the submodule update hints.  This\ncommit just adds two more messages that should also be omittable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex a18f47e23c5..998e92ec593 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -2420,7 +2420,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t */\n \t\tci->path_conflict = 1;\n \t\tif (pair->status == 'A')\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s added in %s \"\n \t\t\t\t   \"inside a directory that was renamed in %s, \"\n \t\t\t\t   \"suggesting it should perhaps be moved to \"\n@@ -2428,7 +2428,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t\t\t old_path, branch_with_new_path,\n \t\t\t\t branch_with_dir_rename, new_path);\n \t\telse\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s renamed to %s \"\n \t\t\t\t   \"in %s, inside a directory that was renamed \"\n \t\t\t\t   \"in %s, suggesting it should perhaps be \"\n-- \ngitgitgadget\n\n"},{"id":"444948","messageId":"887e46435c0561e86f1858682fe53e9de926b69c.1640419160.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"[PATCH v2 6/8] merge-ort: format messages slightly different for use in headers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-25T07:59:17Z","receivedAt":"2021-12-25T07:59:40Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen users run\n    git show --remerge-diff $MERGE_COMMIT\nor\n    git log -p --remerge-diff ...\nstdout is not an appropriate location to dump conflict messages, but we\ndo want to provide them to users.  We will include them in the diff\nheaders instead...but for that to work, we need for any multiline\nmessages to replace newlines with both a newline and a space.  Add a new\nflag to signal when we want these messages modified in such a fashion,\nand use it in path_msg() to modify these messages this way.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c       | 36 ++++++++++++++++++++++++++++++++++--\n merge-recursive.c |  3 +++\n merge-recursive.h |  1 +\n 3 files changed, 38 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 998e92ec593..9142d56e0ad 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -634,17 +634,46 @@ static void path_msg(struct merge_options *opt,\n \t\t     const char *fmt, ...)\n {\n \tva_list ap;\n-\tstruct strbuf *sb = strmap_get(&opt->priv->output, path);\n+\tstruct strbuf *sb, *dest;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\n+\tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n+\t\treturn; /* Do not record mere hints in tree */\n+\tsb = strmap_get(&opt->priv->output, path);\n \tif (!sb) {\n \t\tsb = xmalloc(sizeof(*sb));\n \t\tstrbuf_init(sb, 0);\n \t\tstrmap_put(&opt->priv->output, path, sb);\n \t}\n \n+\tdest = (opt->record_conflict_msgs_as_headers ? &tmp : sb);\n+\n \tva_start(ap, fmt);\n-\tstrbuf_vaddf(sb, fmt, ap);\n+\tstrbuf_vaddf(dest, fmt, ap);\n \tva_end(ap);\n \n+\tif (opt->record_conflict_msgs_as_headers) {\n+\t\tint i_sb = 0, i_tmp = 0;\n+\n+\t\t/* Copy tmp to sb, adding spaces after newlines */\n+\t\tstrbuf_grow(sb, 2*tmp.len); /* more than sufficient */\n+\t\tfor (; i_tmp < tmp.len; i_tmp++, i_sb++) {\n+\t\t\t/* Copy next character from tmp to sb */\n+\t\t\tsb->buf[sb->len + i_sb] = tmp.buf[i_tmp];\n+\n+\t\t\t/* If we copied a newline, add a space */\n+\t\t\tif (tmp.buf[i_tmp] == '\\n')\n+\t\t\t\tsb->buf[++i_sb] = ' ';\n+\t\t}\n+\t\t/* Update length and ensure it's NUL-terminated */\n+\t\tsb->len += i_sb;\n+\t\tsb->buf[sb->len] = '\\0';\n+\n+\t\t/* Clean up tmp */\n+\t\tstrbuf_release(&tmp);\n+\t}\n+\n+\t/* Add final newline character to sb */\n \tstrbuf_addch(sb, '\\n');\n }\n \n@@ -4246,6 +4275,9 @@ void merge_switch_to_result(struct merge_options *opt,\n \t\tstruct string_list olist = STRING_LIST_INIT_NODUP;\n \t\tint i;\n \n+\t\tif (opt->record_conflict_msgs_as_headers)\n+\t\t\tBUG(\"Either display conflict messages or record them as headers, not both\");\n+\n \t\ttrace2_region_enter(\"merge\", \"display messages\", opt->repo);\n \n \t\t/* Hack to pre-allocate olist to the desired size */\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex bc73c52dd84..c9ba7e904a6 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -3714,6 +3714,9 @@ static int merge_start(struct merge_options *opt, struct tree *head)\n \n \tassert(opt->priv == NULL);\n \n+\t/* Not supported; option specific to merge-ort */\n+\tassert(!opt->record_conflict_msgs_as_headers);\n+\n \t/* Sanity check on repo state; index must match head */\n \tif (repo_index_has_changes(opt->repo, head, &sb)) {\n \t\terr(opt, _(\"Your local changes to the following files would be overwritten by merge:\\n  %s\"),\ndiff --git a/merge-recursive.h b/merge-recursive.h\nindex 0795a1d3ec1..ebfdb7f994e 100644\n--- a/merge-recursive.h\n+++ b/merge-recursive.h\n@@ -46,6 +46,7 @@ struct merge_options {\n \t/* miscellaneous control options */\n \tconst char *subtree_shift;\n \tunsigned renormalize : 1;\n+\tunsigned record_conflict_msgs_as_headers : 1;\n \n \t/* internal fields used by the implementation */\n \tstruct merge_options_internal *priv;\n-- \ngitgitgadget\n\n"},{"id":"444949","messageId":"e94706513038adc85e818e8736ad713b2e6b6be4.1640419160.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"[PATCH v2 7/8] diff: add ability to insert additional headers for paths","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-25T07:59:18Z","receivedAt":"2021-12-25T07:59:40Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen additional headers are provided, we need to\n  * add diff_filepairs to diff_queued_diff for each paths in the\n    additional headers map which, unless that path is part of\n    another diff_filepair already found in diff_queued_diff\n  * format the headers (colorization, line_prefix for --graph)\n  * make sure the various codepaths that attempt to return early\n    if there are \"no changes\" take into account the headers that\n    need to be shown.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n diff.c     | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++--\n diff.h     |   3 +-\n log-tree.c |   2 +-\n 3 files changed, 115 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 861282db1c3..aaa6a19f158 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -27,6 +27,7 @@\n #include \"help.h\"\n #include \"promisor-remote.h\"\n #include \"dir.h\"\n+#include \"strmap.h\"\n \n #ifdef NO_FAST_WORKING_DIRECTORY\n #define FAST_WORKING_DIRECTORY 0\n@@ -3406,6 +3407,31 @@ struct userdiff_driver *get_textconv(struct repository *r,\n \treturn userdiff_get_textconv(r, one->driver);\n }\n \n+static struct strbuf *additional_headers(struct diff_options *o,\n+\t\t\t\t\t const char *path)\n+{\n+\tif (!o->additional_path_headers)\n+\t\treturn NULL;\n+\treturn strmap_get(o->additional_path_headers, path);\n+}\n+\n+static void add_formatted_headers(struct strbuf *msg,\n+\t\t\t\t  struct strbuf *more_headers,\n+\t\t\t\t  const char *line_prefix,\n+\t\t\t\t  const char *meta,\n+\t\t\t\t  const char *reset)\n+{\n+\tchar *next, *newline;\n+\n+\tfor (next = more_headers->buf; *next; next = newline) {\n+\t\tnewline = strchrnul(next, '\\n');\n+\t\tstrbuf_addf(msg, \"%s%s%.*s%s\\n\", line_prefix, meta,\n+\t\t\t    (int)(newline - next), next, reset);\n+\t\tif (*newline)\n+\t\t\tnewline++;\n+\t}\n+}\n+\n static void builtin_diff(const char *name_a,\n \t\t\t const char *name_b,\n \t\t\t struct diff_filespec *one,\n@@ -3464,6 +3490,17 @@ static void builtin_diff(const char *name_a,\n \tb_two = quote_two(b_prefix, name_b + (*name_b == '/'));\n \tlbl[0] = DIFF_FILE_VALID(one) ? a_one : \"/dev/null\";\n \tlbl[1] = DIFF_FILE_VALID(two) ? b_two : \"/dev/null\";\n+\tif (!DIFF_FILE_VALID(one) && !DIFF_FILE_VALID(two)) {\n+\t\t/*\n+\t\t * We should only reach this point for pairs from\n+\t\t * create_filepairs_for_header_only_notifications().  For\n+\t\t * these, we should avoid the \"/dev/null\" special casing\n+\t\t * above, meaning we avoid showing such pairs as either\n+\t\t * \"new file\" or \"deleted file\" below.\n+\t\t */\n+\t\tlbl[0] = a_one;\n+\t\tlbl[1] = b_two;\n+\t}\n \tstrbuf_addf(&header, \"%s%sdiff --git %s %s%s\\n\", line_prefix, meta, a_one, b_two, reset);\n \tif (lbl[0][0] == '/') {\n \t\t/* /dev/null */\n@@ -4328,6 +4365,7 @@ static void fill_metainfo(struct strbuf *msg,\n \tconst char *set = diff_get_color(use_color, DIFF_METAINFO);\n \tconst char *reset = diff_get_color(use_color, DIFF_RESET);\n \tconst char *line_prefix = diff_line_prefix(o);\n+\tstruct strbuf *more_headers = NULL;\n \n \t*must_show_header = 1;\n \tstrbuf_init(msg, PATH_MAX * 2 + 300);\n@@ -4364,6 +4402,11 @@ static void fill_metainfo(struct strbuf *msg,\n \tdefault:\n \t\t*must_show_header = 0;\n \t}\n+\tif ((more_headers = additional_headers(o, name))) {\n+\t\tadd_formatted_headers(msg, more_headers,\n+\t\t\t\t      line_prefix, set, reset);\n+\t\t*must_show_header = 1;\n+\t}\n \tif (one && two && !oideq(&one->oid, &two->oid)) {\n \t\tconst unsigned hexsz = the_hash_algo->hexsz;\n \t\tint abbrev = o->abbrev ? o->abbrev : DEFAULT_ABBREV;\n@@ -5852,12 +5895,22 @@ int diff_unmodified_pair(struct diff_filepair *p)\n \n static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n {\n-\tif (diff_unmodified_pair(p))\n+\t/*\n+\t * Check if we can return early without showing a diff.  Note that\n+\t * diff_filepair only stores {oid, path, mode, is_valid}\n+\t * information for each path, and thus diff_unmodified_pair() only\n+\t * considers those bits of info.  However, we do not want pairs\n+\t * created by create_filepairs_for_header_only_notifications() to\n+\t * be ignored, so return early if both p is unmodified AND\n+\t * p->one->path is not in additional headers.\n+\t */\n+\tif (diff_unmodified_pair(p) && !additional_headers(o, p->one->path))\n \t\treturn;\n \n+\t/* Actually, we can also return early to avoid showing tree diffs */\n \tif ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||\n \t    (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))\n-\t\treturn; /* no tree diffs in patch format */\n+\t\treturn;\n \n \trun_diff(p, o);\n }\n@@ -5888,10 +5941,14 @@ static void diff_flush_checkdiff(struct diff_filepair *p,\n \trun_checkdiff(p, o);\n }\n \n-int diff_queue_is_empty(void)\n+int diff_queue_is_empty(struct diff_options *o)\n {\n \tstruct diff_queue_struct *q = &diff_queued_diff;\n \tint i;\n+\n+\tif (o->additional_path_headers &&\n+\t    !strmap_empty(o->additional_path_headers))\n+\t\treturn 0;\n \tfor (i = 0; i < q->nr; i++)\n \t\tif (!diff_unmodified_pair(q->queue[i]))\n \t\t\treturn 0;\n@@ -6325,6 +6382,54 @@ void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)\n \t\twarning(_(rename_limit_advice), varname, needed);\n }\n \n+static void create_filepairs_for_header_only_notifications(struct diff_options *o)\n+{\n+\tstruct strset present;\n+\tstruct diff_queue_struct *q = &diff_queued_diff;\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *e;\n+\tint i;\n+\n+\tstrset_init_with_options(&present, /*pool*/ NULL, /*strdup*/ 0);\n+\n+\t/*\n+\t * Find out which paths exist in diff_queued_diff, preferring\n+\t * one->path for any pair that has multiple paths.\n+\t */\n+\tfor (i = 0; i < q->nr; i++) {\n+\t\tstruct diff_filepair *p = q->queue[i];\n+\t\tchar *path = p->one->path ? p->one->path : p->two->path;\n+\n+\t\tif (strmap_contains(o->additional_path_headers, path))\n+\t\t\tstrset_add(&present, path);\n+\t}\n+\n+\t/*\n+\t * Loop over paths in additional_path_headers; for each NOT already\n+\t * in diff_queued_diff, create a synthetic filepair and insert that\n+\t * into diff_queued_diff.\n+\t */\n+\tstrmap_for_each_entry(o->additional_path_headers, &iter, e) {\n+\t\tif (!strset_contains(&present, e->key)) {\n+\t\t\tstruct diff_filespec *one, *two;\n+\t\t\tstruct diff_filepair *p;\n+\n+\t\t\tone = alloc_filespec(e->key);\n+\t\t\ttwo = alloc_filespec(e->key);\n+\t\t\tfill_filespec(one, null_oid(), 0, 0);\n+\t\t\tfill_filespec(two, null_oid(), 0, 0);\n+\t\t\tp = diff_queue(q, one, two);\n+\t\t\tp->status = DIFF_STATUS_MODIFIED;\n+\t\t}\n+\t}\n+\n+\t/* Re-sort the filepairs */\n+\tdiffcore_fix_diff_index();\n+\n+\t/* Cleanup */\n+\tstrset_clear(&present);\n+}\n+\n static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n {\n \tint i;\n@@ -6337,6 +6442,9 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n \tif (o->color_moved)\n \t\to->emitted_symbols = &esm;\n \n+\tif (o->additional_path_headers)\n+\t\tcreate_filepairs_for_header_only_notifications(o);\n+\n \tfor (i = 0; i < q->nr; i++) {\n \t\tstruct diff_filepair *p = q->queue[i];\n \t\tif (check_pair_status(p))\n@@ -6413,7 +6521,7 @@ void diff_flush(struct diff_options *options)\n \t * Order: raw, stat, summary, patch\n \t * or:    name/name-status/checkdiff (other bits clear)\n \t */\n-\tif (!q->nr)\n+\tif (!q->nr && !options->additional_path_headers)\n \t\tgoto free_queue;\n \n \tif (output_format & (DIFF_FORMAT_RAW |\ndiff --git a/diff.h b/diff.h\nindex 8ba85c5e605..06a0a67afda 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -395,6 +395,7 @@ struct diff_options {\n \n \tstruct repository *repo;\n \tstruct option *parseopts;\n+\tstruct strmap *additional_path_headers;\n \n \tint no_free;\n };\n@@ -593,7 +594,7 @@ void diffcore_fix_diff_index(void);\n \"                show all files diff when -S is used and hit is found.\\n\" \\\n \"  -a  --text    treat all files as text.\\n\"\n \n-int diff_queue_is_empty(void);\n+int diff_queue_is_empty(struct diff_options*);\n void diff_flush(struct diff_options*);\n void diff_free(struct diff_options*);\n void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);\ndiff --git a/log-tree.c b/log-tree.c\nindex d4655b63d75..33c28f537a6 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -850,7 +850,7 @@ int log_tree_diff_flush(struct rev_info *opt)\n \topt->shown_dashes = 0;\n \tdiffcore_std(&opt->diffopt);\n \n-\tif (diff_queue_is_empty()) {\n+\tif (diff_queue_is_empty(&opt->diffopt)) {\n \t\tint saved_fmt = opt->diffopt.output_format;\n \t\topt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT;\n \t\tdiff_flush(&opt->diffopt);\n-- \ngitgitgadget\n\n"},{"id":"444950","messageId":"4cc53c55a6ea1531342b23bc9343890a576d9f1c.1640419160.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"[PATCH v2 8/8] show, log: include conflict/warning messages in --remerge-diff headers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-25T07:59:19Z","receivedAt":"2021-12-25T07:59:40Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nConflicts such as modify/delete, rename/rename, or file/directory are\nnot representable via content conflict markers, and the normal output\nmessages notifying users about these were dropped with --remerge-diff.\nWhile we don't want these messages randomly shown before the commit\nand diff headers, we do want them to still be shown; include them as\npart of the diff headers instead.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n log-tree.c              |  3 ++\n merge-ort.c             |  1 +\n merge-ort.h             | 10 +++++\n t/t4069-remerge-diff.sh | 86 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 100 insertions(+)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 33c28f537a6..97fbb756d21 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -922,6 +922,7 @@ static int do_remerge_diff(struct rev_info *opt,\n \t/* Setup merge options */\n \tinit_merge_options(&o, the_repository);\n \to.show_rename_progress = 0;\n+\to.record_conflict_msgs_as_headers = 1;\n \n \tctx.abbrev = DEFAULT_ABBREV;\n \tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n@@ -938,10 +939,12 @@ static int do_remerge_diff(struct rev_info *opt,\n \tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n \n \t/* Show the diff */\n+\topt->diffopt.additional_path_headers = res.path_messages;\n \tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n \tlog_tree_diff_flush(opt);\n \n \t/* Cleanup */\n+\topt->diffopt.additional_path_headers = NULL;\n \tstrbuf_release(&parent1_desc);\n \tstrbuf_release(&parent2_desc);\n \tmerge_finalize(&o, &res);\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 9142d56e0ad..07e53083cbd 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -4579,6 +4579,7 @@ redo:\n \ttrace2_region_leave(\"merge\", \"process_entries\", opt->repo);\n \n \t/* Set return values */\n+\tresult->path_messages = &opt->priv->output;\n \tresult->tree = parse_tree_indirect(&working_tree_oid);\n \t/* existence of conflicted entries implies unclean */\n \tresult->clean &= strmap_empty(&opt->priv->conflicted);\ndiff --git a/merge-ort.h b/merge-ort.h\nindex c011864ffeb..fe599b87868 100644\n--- a/merge-ort.h\n+++ b/merge-ort.h\n@@ -5,6 +5,7 @@\n \n struct commit;\n struct tree;\n+struct strmap;\n \n struct merge_result {\n \t/*\n@@ -23,6 +24,15 @@ struct merge_result {\n \t */\n \tstruct tree *tree;\n \n+\t/*\n+\t * Special messages and conflict notices for various paths\n+\t *\n+\t * This is a map of pathnames to strbufs.  It contains various\n+\t * warning/conflict/notice messages (possibly multiple per path)\n+\t * that callers may want to use.\n+\t */\n+\tstruct strmap *path_messages;\n+\n \t/*\n \t * Additional metadata used by merge_switch_to_result() or future calls\n \t * to merge_incore_*().  Includes data needed to update the index (if\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nindex 192dbce2bfe..a040d3bcd91 100755\n--- a/t/t4069-remerge-diff.sh\n+++ b/t/t4069-remerge-diff.sh\n@@ -4,6 +4,15 @@ test_description='remerge-diff handling'\n \n . ./test-lib.sh\n \n+# --remerge-diff uses ort under the hood regardless of setting.  However,\n+# we set up a file/directory conflict beforehand, and the different backends\n+# handle the conflict differently, which would require separate code paths\n+# to resolve.  There's not much point in making the code uglier to do that,\n+# though, when the real thing we are testing (--remerge-diff) will hardcode\n+# calls directly into the merge-ort API anyway.  So just force the use of\n+# ort on the setup too.\n+GIT_TEST_MERGE_ALGORITHM=ort\n+\n test_expect_success 'setup basic merges' '\n \ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n \tgit add numbers &&\n@@ -55,6 +64,7 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n \tgit log -1 --oneline ab_resolution >tmp &&\n \tcat <<-EOF >>tmp &&\n \tdiff --git a/numbers b/numbers\n+\tCONFLICT (content): Merge conflict in numbers\n \tindex a1fb731..6875544 100644\n \t--- a/numbers\n \t+++ b/numbers\n@@ -83,4 +93,80 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup non-content conflicts' '\n+\tgit switch --orphan base &&\n+\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\ttest_write_lines a b c d e f g h i >letters &&\n+\ttest_write_lines in the way >content &&\n+\tgit add numbers letters content &&\n+\tgit commit -m base &&\n+\n+\tgit branch side1 &&\n+\tgit branch side2 &&\n+\n+\tgit checkout side1 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit mv letters letters_side1 &&\n+\tgit mv content file_or_directory &&\n+\tgit add numbers &&\n+\tgit commit -m side1 &&\n+\n+\tgit checkout side2 &&\n+\tgit rm numbers &&\n+\tgit mv letters letters_side2 &&\n+\tmkdir file_or_directory &&\n+\techo hello >file_or_directory/world &&\n+\tgit add file_or_directory/world &&\n+\tgit commit -m side2 &&\n+\n+\tgit checkout -b resolution side1 &&\n+\ttest_must_fail git merge side2 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit add letters_side1 &&\n+\tgit rm letters &&\n+\tgit rm letters_side2 &&\n+\tgit add file_or_directory~HEAD &&\n+\tgit mv file_or_directory~HEAD wanted_content &&\n+\tgit commit -m resolved\n+'\n+\n+test_expect_success 'remerge-diff with non-content conflicts' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n+\tsimilarity index 100%\n+\trename from file_or_directory~HASH (side1)\n+\trename to wanted_content\n+\tCONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n+\tdiff --git a/letters b/letters\n+\tCONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n+\tdiff --git a/letters_side2 b/letters_side2\n+\tdeleted file mode 100644\n+\tindex b236ae5..0000000\n+\t--- a/letters_side2\n+\t+++ /dev/null\n+\t@@ -1,9 +0,0 @@\n+\t-a\n+\t-b\n+\t-c\n+\t-d\n+\t-e\n+\t-f\n+\t-g\n+\t-h\n+\t-i\n+\tdiff --git a/numbers b/numbers\n+\tCONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"444969","messageId":"211226.86tuevfbk3.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"887e46435c0561e86f1858682fe53e9de926b69c.1640419160.git.gitgitgadget@gmail.com","subject":"In-tree strbuf \"in-place\" search/replace (was: [PATCH v2 6/8] merge-ort: format messages slightly different for use in headers)","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-26T18:30:34Z","receivedAt":"2021-12-26T18:47:29Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Sat, Dec 25 2021, Elijah Newren via GitGitGadget wrote:\n\n> @@ -634,17 +634,46 @@ static void path_msg(struct merge_options *opt,\n>  \t\t     const char *fmt, ...)\n>  {\n>  \tva_list ap;\n> -\tstruct strbuf *sb = strmap_get(&opt->priv->output, path);\n> +\tstruct strbuf *sb, *dest;\n> +\tstruct strbuf tmp = STRBUF_INIT;\n> +\n> +\tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n> +\t\treturn; /* Do not record mere hints in tree */\n> +\tsb = strmap_get(&opt->priv->output, path);\n>  \tif (!sb) {\n>  \t\tsb = xmalloc(sizeof(*sb));\n>  \t\tstrbuf_init(sb, 0);\n>  \t\tstrmap_put(&opt->priv->output, path, sb);\n>  \t}\n>  \n> +\tdest = (opt->record_conflict_msgs_as_headers ? &tmp : sb);\n> +\n>  \tva_start(ap, fmt);\n> -\tstrbuf_vaddf(sb, fmt, ap);\n> +\tstrbuf_vaddf(dest, fmt, ap);\n>  \tva_end(ap);\n>  \n> +\tif (opt->record_conflict_msgs_as_headers) {\n> +\t\tint i_sb = 0, i_tmp = 0;\n> +\n> +\t\t/* Copy tmp to sb, adding spaces after newlines */\n> +\t\tstrbuf_grow(sb, 2*tmp.len); /* more than sufficient */\n> +\t\tfor (; i_tmp < tmp.len; i_tmp++, i_sb++) {\n> +\t\t\t/* Copy next character from tmp to sb */\n> +\t\t\tsb->buf[sb->len + i_sb] = tmp.buf[i_tmp];\n> +\n> +\t\t\t/* If we copied a newline, add a space */\n> +\t\t\tif (tmp.buf[i_tmp] == '\\n')\n> +\t\t\t\tsb->buf[++i_sb] = ' ';\n> +\t\t}\n> +\t\t/* Update length and ensure it's NUL-terminated */\n> +\t\tsb->len += i_sb;\n> +\t\tsb->buf[sb->len] = '\\0';\n> +\n> +\t\t/* Clean up tmp */\n> +\t\tstrbuf_release(&tmp);\n> +\t}\n> +\n> +\t/* Add final newline character to sb */\n>  \tstrbuf_addch(sb, '\\n');\n>  }\n>  \n\nI'm not saying this is wrong or needs to change. Just a reader's note\nthat this sent me on an interesting journey of looking at various\nin-tree callers of strbufs that want to do the equivalent of\ns/$from/$to/ on a strbuf, with and without the equivalent of /g.\n\nI figured I'd change the $subject since this is more of a general\nmusing...\n\nIn trailer.c we've got strbuf_replace(), which looks like it could be\nmade to be general enough to serve most callers if it did a memmem()\ninstead of a strstr(), and knew to take a \"all\" flag to implement a /g.\n\nWe then have e.g. lf_to_crlf() in imap-send.c, which uses a newly\nalloc'd buffer followed by a strbuf_attach(), which is a common pattern.\n\nThen strbuf_reencode() in strbuf.c basically solves this problem, and\ncalls reencode_string_len(), both it and the underlying function are\n*almost* general enough to know to take some \"from/to\" string/length\npair, i.e. to not be bound to \"reencoding\" only with iconv().\n\nThen there's strbuf_add_percentencode() and strbuf_add_urlencode() whose\nAPI users might be happy with in-place replacing, but do a\nread-and-copy-maybe-expand.\n\nIt might be an interesting follow-up project for someone to come up with\na generic in-place search-replace function with a signature like:\n\n\tint strbuf_replace(struct strbuf *sb, const char *from,\n\t\t\t   size_t from_len, const char *to,\n\t\t\t   size_t to_len, int max);\n\nTo do e.g. in this case:\n\n\tif (opt->record_conflict_msgs_as_headers)\n\t\tstrbuf_replace(sb, \"\\n\", strlen(\"\\n\"), \"\\n \", strlen(\"\\n \"), -1);\n\nThe various in-tree implementations do some variant of over-mallocing to\nsave work in the loop (as is being done here), copying where a small\nrealloc/memmove might do, scanning the string to figure out how much to\nmalloc, then copying in a second pass etc.\n"},{"id":"444973","messageId":"211226.86lf07f1c5.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/8] Add a new --remerge-diff capability to show & log","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-26T21:52:56Z","receivedAt":"2021-12-26T22:28:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Sat, Dec 25 2021, Elijah Newren via GitGitGadget wrote:\n\n> === FURTHER BACKGROUND (original cover letter material) ==\n>\n> Here are some example commits you can try this out on (with git show\n> --remerge-diff $COMMIT):\n>\n>  * git.git conflicted merge: 07601b5b36\n>  * git.git non-conflicted change: bf04590ecd\n>  * linux.git conflicted merge: eab3540562fb\n>  * linux.git non-conflicted change: 223cea6a4f05\n>\n> Many more can be found by just running git log --merges --remerge-diff in\n> your repository of choice and searching for diffs (most merges tend to be\n> clean and unmodified and thus produce no diff but a search of '^diff' in the\n> log output tends to find the examples nicely).\n>\n> Some basic high level details about this new option:\n>\n>  * This option is most naturally compared to --cc, though the output seems\n>    to be much more understandable to most users than --cc output.\n>  * Since merges are often clean and unmodified, this new option results in\n>    an empty diff for most merges.\n>  * This new option shows things like the removal of conflict markers, which\n>    hunks users picked from the various conflicted sides to keep or remove,\n>    and shows changes made outside of conflict markers (which might reflect\n>    changes needed to resolve semantic conflicts or cleanups of e.g.\n>    compilation warnings or other additional changes an integrator felt\n>    belonged in the merged result).\n>  * This new option does not (currently) work for octopus merges, since\n>    merge-ort is specific to two-parent merges[1].\n>  * This option will not work on a read-only or full filesystem[2].\n>  * We discussed this capability at Git Merge 2020, and one of the\n>    suggestions was doing a periodic git gc --auto during the operation (due\n>    to potential new blobs and trees created during the operation). I found a\n>    way to avoid that; see [2].\n>  * This option is faster than you'd probably expect; it handles 33.5 merge\n>    commits per second in linux.git on my computer; see below.\n>\n> In regards to the performance point above, the timing for running the\n> following command:\n>\n> time git log --min-parents=2 --max-parents=2 $DIFF_FLAG | wc -l\n\nI've been trying to come up with some other useful recipies for this new\noption (which is already very useful, thanks!)\n\nSome of these (if correct) are suggestions for incorporating into the\n(now rather sparse) documentation. I.e. walking users through how to use\nthis, and how (if at all) it combines with other options.\n\nI wanted to find all merges between \"master\"..\"seen\" for which Junio's\nhad to resolve a conflict, a naïve version is:\n\n    $ git log --oneline --remerge-diff -p --min-parents=2 origin/master..origin/seen|grep ^diff -B1 | grep Merge\n    [...]\n\nBut I found that this new option nicely integrates with --diff-filter,\ni.e. we'll end up showing a diff, and the diff machinery allows you to\nto filter on it.\n\nIt seems to me like all the diffs you show fall under \"M\", so for\nmaster..seen (2ae0a9cb829..61055c2920d) this is equivalent (and the\noutput is the same as the above):\n\n    $ git -P log --oneline --remerge-diff --no-patch --min-parents=2 --diff-filter=M origin/master..origin/seen \n    95daa54b1c3 Merge branch 'hn/reftable-fixes' into seen\n    26c4c09dd34 Merge branch 'gc/fetch-negotiate-only-early-return' into seen\n    e3dc8d073f6 Merge branch 'gc/branch-recurse-submodules' into seen\n    aeada898196 Merge branch 'js/branch-track-inherit' into seen\n    4dd30e0da45 Merge branch 'jh/builtin-fsmonitor-part2' into seen\n    337743b17d0 Merge branch 'ab/config-based-hooks-2' into seen\n    261672178c0 Merge branch 'pw/fix-some-issues-in-reset-head' into seen\n    1296d35b041 Merge branch 'ms/customizable-ident-expansion' into seen\n    7a3d7d05126 Merge branch 'ja/i18n-similar-messages' into seen\n    eda714bb8bc Merge branch 'tb/midx-bitmap-corruption-fix' into seen\n    ba02295e3f8 Merge branch 'jh/p4-human-unit-numbers' into jch\n    751773fc38b Merge branch 'es/test-chain-lint' into jch\n    ec17879f495 Merge branch 'tb/cruft-packs' into tb/midx-bitmap-corruption-fix\n\nHowever for \"origin/master..origin/next\" (next = 510f9eba9a2 currently)\nwe'll oddly show this with \"-p\":\n    \n    9af51fd1d0d Sync with 'master'\n    diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\n    CONFLICT (content): Merge conflict in t/lib-gpg.sh\n    d6f56f3248e Merge branch 'es/test-chain-lint' into next\n    diff --git a/t/t4126-apply-empty.sh b/t/t4126-apply-empty.sh\n    CONFLICT (content): Merge conflict in t/t4126-apply-empty.sh\n    index 996c93329c6..33860d38290 100755\n    --- a/t/t4126-apply-empty.sh\n    +++ b/t/t4126-apply-empty.sh\n    [...]\n\nThe \"oddly\" applying only to that \"9af51fd1d0d Sync with 'master'\", not\nthe second d6f56f3248e, which shows the sort of conflict I'd expect. The\ntwo-line \"diff\" of:\n\n    diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\n    CONFLICT (content): Merge conflict in t/lib-gpg.sh\n\nShows up with -p --remerge-diff, not a mere -p. I also tried the other\n--diff-merges=* options, that behavior is new in\n--diff-merges=remerge. Is this a bug?\n\nMy local build also has a --pickaxe-patch option. It's something I\nsubmitted on-list before[1] and have been meaning to re-roll.\n\nI'm discussing it here because it skips the stripping of the \"+ \" and \"-\n\" prefixes under -G<regex> and allows you to search through the -U<n>\ncontext. With that I'm able to do:\n\n    git log --oneline --remerge-diff -p --min-parents=2 --pickaxe-patch -G'^\\+' --diff-filter=M origin/master..origin/seen\n\nI.e. on top of the above filter only show those diffs that have\nadditions. FAICT the conflicting diffs where the committer of the merge\nconflict picked one side or the other will only have \"-\" lines\".\n\nSo those diffs that have additions look to be those where the person\ndoing the merge needed to combine the two.\n\nWell, usually. E.g. 26c4c09dd34 (Merge branch\n'gc/fetch-negotiate-only-early-return' into seen, 2021-12-25) in that\nrange shows that isn't strictly true. Most such deletion-only diffs are\nless interesting in picking one side or the other of the conflict, but\nthat one combines the two:\n    \n    -<<<<<<< d3419aac9f4 (Merge branch 'pw/add-p-hunk-split-fix' into seen)\n                            warning(_(\"protocol does not support --negotiate-only, exiting\"));\n    -                       return 1;\n    -=======\n    -                       warning(_(\"Protocol does not support --negotiate-only, exiting.\"));\n                            result = 1;\n                            goto cleanup;\n    ->>>>>>> 495e8601f28 (builtin/fetch: die on --negotiate-only and --recurse-submodules)\n\nWhich I guess is partially commentary and partially a request (either\nfor this series, or some follow-up) for something like a\n--remerge-diff-filter option. I.e. it would be very useful to be able to\nfilter on some combination of:\n\n * Which side(s) of the conflict(s) were picked, or a combination?\n * Is there \"new work\" in the diff to resolve the conflict?\n   AFIACT this will always mean we'll have \"+ \" lines.\n\nOr maybe that's not useful at all, and just -G<rx> (maybe combined with\nmy --pickaxe-patch) will cover it?\n\n1. https://lore.kernel.org/git/20190424152215.16251-3-avarab@gmail.com/\n"},{"id":"445016","messageId":"CABPp-BFjZUHKq1OxkgompqbaugCN=c5PFiGxLp9U1sdBePpYvA@mail.gmail.com","threadId":"57133","inReplyTo":"211226.86lf07f1c5.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 0/8] Add a new --remerge-diff capability to show & log","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-27T21:11:13Z","receivedAt":"2021-12-27T21:11:29Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sun, Dec 26, 2021 at 2:28 PM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Sat, Dec 25 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > === FURTHER BACKGROUND (original cover letter material) ==\n> >\n> > Here are some example commits you can try this out on (with git show\n> > --remerge-diff $COMMIT):\n> >\n> >  * git.git conflicted merge: 07601b5b36\n> >  * git.git non-conflicted change: bf04590ecd\n> >  * linux.git conflicted merge: eab3540562fb\n> >  * linux.git non-conflicted change: 223cea6a4f05\n> >\n> > Many more can be found by just running git log --merges --remerge-diff in\n> > your repository of choice and searching for diffs (most merges tend to be\n> > clean and unmodified and thus produce no diff but a search of '^diff' in the\n> > log output tends to find the examples nicely).\n> >\n> > Some basic high level details about this new option:\n> >\n> >  * This option is most naturally compared to --cc, though the output seems\n> >    to be much more understandable to most users than --cc output.\n> >  * Since merges are often clean and unmodified, this new option results in\n> >    an empty diff for most merges.\n> >  * This new option shows things like the removal of conflict markers, which\n> >    hunks users picked from the various conflicted sides to keep or remove,\n> >    and shows changes made outside of conflict markers (which might reflect\n> >    changes needed to resolve semantic conflicts or cleanups of e.g.\n> >    compilation warnings or other additional changes an integrator felt\n> >    belonged in the merged result).\n> >  * This new option does not (currently) work for octopus merges, since\n> >    merge-ort is specific to two-parent merges[1].\n> >  * This option will not work on a read-only or full filesystem[2].\n> >  * We discussed this capability at Git Merge 2020, and one of the\n> >    suggestions was doing a periodic git gc --auto during the operation (due\n> >    to potential new blobs and trees created during the operation). I found a\n> >    way to avoid that; see [2].\n> >  * This option is faster than you'd probably expect; it handles 33.5 merge\n> >    commits per second in linux.git on my computer; see below.\n> >\n> > In regards to the performance point above, the timing for running the\n> > following command:\n> >\n> > time git log --min-parents=2 --max-parents=2 $DIFF_FLAG | wc -l\n>\n> I've been trying to come up with some other useful recipies for this new\n> option (which is already very useful, thanks!)\n\nI'm glad you like it.  :-)\n\n> Some of these (if correct) are suggestions for incorporating into the\n> (now rather sparse) documentation. I.e. walking users through how to use\n> this, and how (if at all) it combines with other options.\n>\n> I wanted to find all merges between \"master\"..\"seen\" for which Junio's\n> had to resolve a conflict, a naïve version is:\n>\n>     $ git log --oneline --remerge-diff -p --min-parents=2 origin/master..origin/seen|grep ^diff -B1 | grep Merge\n>     [...]\n\nI think the naive version is\n  $ git log --remerge-diff --min-parents=2 origin/master..origin/seen\n  <search for \"^diff\" using your pager's search functionality>\n\nWhere the \"--min-parents=2 origin/master..origin/seen\" comes from your\nproblem description (\"find all merges between master..seen\").\n\nYou can add --oneline to format it, though it's an orthogonal concern.\nAlso, adding -p is unnecessary: --remerge-diff, like --cc, implies -p.\n\n> But I found that this new option nicely integrates with --diff-filter,\n> i.e. we'll end up showing a diff, and the diff machinery allows you to\n> to filter on it.\n>\n> It seems to me like all the diffs you show fall under \"M\", so for\n\nYes, the diffs I happened to pick all fell under \"M\", but by no means\nshould you rely on that happening for all merges in history.  For\nexample, make a new merge commit, then add a completely new file (or\ndelete a file, or rename a file, or copy a file, or change its\nmode/type), stage the new/deleted/renamed/copied/changed file, and run\n\"git commit --amend\".\n\nSo, although --diff-filter=M can be interesting, I would not rely on it.\n\n> master..seen (2ae0a9cb829..61055c2920d) this is equivalent (and the\n> output is the same as the above):\n>\n>     $ git -P log --oneline --remerge-diff --no-patch --min-parents=2 --diff-filter=M origin/master..origin/seen\n>     95daa54b1c3 Merge branch 'hn/reftable-fixes' into seen\n>     26c4c09dd34 Merge branch 'gc/fetch-negotiate-only-early-return' into seen\n>     e3dc8d073f6 Merge branch 'gc/branch-recurse-submodules' into seen\n>     aeada898196 Merge branch 'js/branch-track-inherit' into seen\n>     4dd30e0da45 Merge branch 'jh/builtin-fsmonitor-part2' into seen\n>     337743b17d0 Merge branch 'ab/config-based-hooks-2' into seen\n>     261672178c0 Merge branch 'pw/fix-some-issues-in-reset-head' into seen\n>     1296d35b041 Merge branch 'ms/customizable-ident-expansion' into seen\n>     7a3d7d05126 Merge branch 'ja/i18n-similar-messages' into seen\n>     eda714bb8bc Merge branch 'tb/midx-bitmap-corruption-fix' into seen\n>     ba02295e3f8 Merge branch 'jh/p4-human-unit-numbers' into jch\n>     751773fc38b Merge branch 'es/test-chain-lint' into jch\n>     ec17879f495 Merge branch 'tb/cruft-packs' into tb/midx-bitmap-corruption-fix\n>\n> However for \"origin/master..origin/next\" (next = 510f9eba9a2 currently)\n> we'll oddly show this with \"-p\":\n>\n>     9af51fd1d0d Sync with 'master'\n>     diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\n>     CONFLICT (content): Merge conflict in t/lib-gpg.sh\n>     d6f56f3248e Merge branch 'es/test-chain-lint' into next\n>     diff --git a/t/t4126-apply-empty.sh b/t/t4126-apply-empty.sh\n>     CONFLICT (content): Merge conflict in t/t4126-apply-empty.sh\n>     index 996c93329c6..33860d38290 100755\n>     --- a/t/t4126-apply-empty.sh\n>     +++ b/t/t4126-apply-empty.sh\n>     [...]\n>\n> The \"oddly\" applying only to that \"9af51fd1d0d Sync with 'master'\", not\n> the second d6f56f3248e, which shows the sort of conflict I'd expect. The\n> two-line \"diff\" of:\n>\n>     diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\n>     CONFLICT (content): Merge conflict in t/lib-gpg.sh\n>\n> Shows up with -p --remerge-diff, not a mere -p. I also tried the other\n> --diff-merges=* options, that behavior is new in\n> --diff-merges=remerge. Is this a bug?\n\nUgh, this is related to my comment elsewhere that conflicts from inner\nmerges are not nicely differentiated.  If I also apply my other series\n(which has not yet been submitted), this instead appears as follows:\n\n$ git show --oneline --remerge-diff 9af51fd1d0d\n9af51fd1d0 Sync with 'master'\ndiff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\n  From inner merge:  CONFLICT (content): Merge conflict in t/lib-gpg.sh\n\nand the addition of the \"From inner merge: \" text makes it clearer why\nthat line appears.  This is an interesting case where a conflict\nnotice _only_ appears in the inner merge (i.e. the merge of merge\nbases), which means that both sides on the outer merge changed the\nrelevant portion of the file in the same way, so the outer merge had\nno conflict.\n\nHowever, instead of trying to differentiate messages from inner\nmerges, I think for --remerge-diff's purposes we should just drop all\nnotices that come from the inner merges.  Those conflict notices might\nbe helpful when initially resolving a merge, but at the --remerge-diff\nlevel, they're more likely to be distracting than helpful.\n\n> My local build also has a --pickaxe-patch option. It's something I\n> submitted on-list before[1] and have been meaning to re-roll.\n>\n> I'm discussing it here because it skips the stripping of the \"+ \" and \"-\n> \" prefixes under -G<regex> and allows you to search through the -U<n>\n> context. With that I'm able to do:\n>\n>     git log --oneline --remerge-diff -p --min-parents=2 --pickaxe-patch -G'^\\+' --diff-filter=M origin/master..origin/seen\n>\n> I.e. on top of the above filter only show those diffs that have\n> additions. FAICT the conflicting diffs where the committer of the merge\n> conflict picked one side or the other will only have \"-\" lines\".\n>\n> So those diffs that have additions look to be those where the person\n> doing the merge needed to combine the two.\n>\n> Well, usually. E.g. 26c4c09dd34 (Merge branch\n> 'gc/fetch-negotiate-only-early-return' into seen, 2021-12-25) in that\n> range shows that isn't strictly true. Most such deletion-only diffs are\n> less interesting in picking one side or the other of the conflict, but\n> that one combines the two:\n>\n>     -<<<<<<< d3419aac9f4 (Merge branch 'pw/add-p-hunk-split-fix' into seen)\n>                             warning(_(\"protocol does not support --negotiate-only, exiting\"));\n>     -                       return 1;\n>     -=======\n>     -                       warning(_(\"Protocol does not support --negotiate-only, exiting.\"));\n>                             result = 1;\n>                             goto cleanup;\n>     ->>>>>>> 495e8601f28 (builtin/fetch: die on --negotiate-only and --recurse-submodules)\n>\n> Which I guess is partially commentary and partially a request (either\n> for this series, or some follow-up) for something like a\n> --remerge-diff-filter option. I.e. it would be very useful to be able to\n> filter on some combination of:\n>\n>  * Which side(s) of the conflict(s) were picked, or a combination?\n>  * Is there \"new work\" in the diff to resolve the conflict?\n>    AFIACT this will always mean we'll have \"+ \" lines.\n\nDo any of the following count as \"new work\"? :\n\n  * the deletion of a file (perhaps one that had no conflict but was\ndeleted anyway)\n  * mode changes (again, perhaps on files that had no conflict)\n  * renames of files/directories?\n\nIf so, searching for \"^+\" lines might be insufficient, but it depends\non what you mean by new work.\n\n> Or maybe that's not useful at all, and just -G<rx> (maybe combined with\n> my --pickaxe-patch) will cover it?\n\nI'd rather wait until we have a good idea of the potential range of\nusecases before adding a filter.  (And I think for now, the -G and\n--pickaxe-patch are probably good enough for this usecase.)  These\nparticular usecases you point out are interesting; thanks for\ndetailing them.  Here's some others to consider:\n\n  * Finding out when text was added or removed: `git log\n--remerge-diff -S<text>` (note that with only -p instead of\n--remerge-diff, that command will annoyingly misses cases where a\nmerge introduced or removed the text)\n  * Finding out how a merge differed from one run with some\nnon-default options (e.g. `git show --remerge-diff -Xours` or `git\nshow --remerge-diff -Xno-space-change`; although show doesn't take -X\noptions so this is just an idea at this point)\n  * Finding out how a merge would have differed had it been run with\ndifferent options (so instead of comparing a remerge to the merge\nrecorded in history, compare one remerge with default options with a\ndifferent merge that uses e.g. -Xno-space-change)\n\nAlso, I've got a follow-up series that also introduces a\n--remerge-diff-only flag which:\n  * For single parent commits that cannot be identified as a revert or\ncherry-pick, do not show a diff.\n  * For single parent commits that can be identified as a revert or\ncherry-pick, instead of showing a diff against the parent of the\ncommit, redo the revert or cherry-pick in memory and show a diff\nagainst that.\n  * For merge commits, act the same as --remerge-diff\n"},{"id":"445058","messageId":"20211228105522.634k3ovahty773vz@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/8] Add a new --remerge-diff capability to show & log","fromName":"Johannes Altmanninger","fromEmail":"aclopte@gmail.com","sentAt":"2021-12-28T10:55:22Z","receivedAt":"2021-12-28T10:56:46Z","isPatch":true,"sender":{"key":"aclopte@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6853872?v=4"},"body":"On Sat, Dec 25, 2021 at 07:59:11AM +0000, Elijah Newren via GitGitGadget wrote:\n> Here are some example commits you can try this out on (with git show\n> --remerge-diff $COMMIT):\n> \n>  * git.git conflicted merge: 07601b5b36\n>  * git.git non-conflicted change: bf04590ecd\n>  * linux.git conflicted merge: eab3540562fb\n>  * linux.git non-conflicted change: 223cea6a4f05\n> \n> Many more can be found by just running git log --merges --remerge-diff in\n> your repository of choice and searching for diffs (most merges tend to be\n> clean and unmodified and thus produce no diff but a search of '^diff' in the\n> log output tends to find the examples nicely).\n> \n> Some basic high level details about this new option:\n> \n>  * This option is most naturally compared to --cc, though the output seems\n>    to be much more understandable to most users than --cc output.\n\nAgreed. --cc is *simple* but I'm more comfortable reading conflict markers\nfrom --remerge-diff, since I'm used to that.  So at least for content\nconflicts it looks simpler.\n"},{"id":"445059","messageId":"20211228105600.h32jl5iwot3munww@gmail.com","threadId":"57133","inReplyTo":"b3ae62083e14aecdfe909735b4daf0a36fa5e48c.1640419159.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 1/8] show, log: provide a --remerge-diff capability","fromName":"Johannes Altmanninger","fromEmail":"aclopte@gmail.com","sentAt":"2021-12-28T10:56:00Z","receivedAt":"2021-12-28T10:56:46Z","isPatch":true,"sender":{"key":"aclopte@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6853872?v=4"},"body":"On Sat, Dec 25, 2021 at 07:59:12AM +0000, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> When this option is specified, we remerge all (two parent) merge commits\n> and diff the actual merge commit to the automatically created version,\n> in order to show how users removed conflict markers, resolved the\n> different conflict versions, and potentially added new changes outside\n> of conflict regions in order to resolve semantic merge problems (or,\n> possibly, just to hide other random changes).\n> \n> This capability works by creating a temporary object directory and\n> marking it as the primary object store.  This makes it so that any blobs\n> or trees created during the automatic merge easily removable afterwards\n\ns/easily/are easily/ ?\n\n> by just deleting all objects from the temporary object directory.\n> \n> There are a few ways that this implementation is suboptimal:\n>   * `log --remerge-diff` becomes slow, because the temporary object\n>     directory can fills with many loose objects while running\n\ns/can fills/can fill/\n\n>   * the log output can be muddied with misplaced \"warning: cannot merge\n>     binary files\" messages, since ll-merge.c unconditionally writes those\n>     messages to stderr while running instead of allowing callers to\n>     manage them.\n>   * important conflict and warning messages are simply dropped; thus for\n>     conflicts like modify/delete or rename/rename or file/directory which\n>     are not representable with content conflict markers, there may be no\n>     way for a user of --remerge-diff to know that there had been a\n>     conflict which was resolved (and which possibly motivated other\n>     changes in the merge commit).\n> Subsequent commits will address these issues.\n> \n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  Documentation/diff-options.txt |  8 ++++\n>  builtin/log.c                  | 14 ++++++\n>  diff-merges.c                  | 12 +++++\n>  log-tree.c                     | 59 +++++++++++++++++++++++\n>  revision.h                     |  3 +-\n>  t/t4069-remerge-diff.sh        | 86 ++++++++++++++++++++++++++++++++++\n>  6 files changed, 181 insertions(+), 1 deletion(-)\n>  create mode 100755 t/t4069-remerge-diff.sh\n> \n> diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\n> index c89d530d3d1..b05f1c9f1c9 100644\n> --- a/Documentation/diff-options.txt\n> +++ b/Documentation/diff-options.txt\n> @@ -64,6 +64,14 @@ ifdef::git-log[]\n>  \teach of the parents. Separate log entry and diff is generated\n>  \tfor each parent.\n>  +\n> +--diff-merges=remerge:::\n> +--diff-merges=r:::\n> +--remerge-diff:::\n\nThe synopsis above needs an update, too:\n\n\tdiff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\n\tindex c89d530d3d..7a98ab3f85 100644\n\t--- a/Documentation/diff-options.txt\n\t+++ b/Documentation/diff-options.txt\n\t@@ -36,3 +36,3 @@ endif::git-format-patch[]\n\t ifdef::git-log[]\n\t---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc)::\n\t+--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r)::\n\t --no-diff-merges::\n\n> +\tWith this option, two-parent merge commits are remerged to\n> +\tcreate a temporary tree object -- potentially containing files\n> +\twith conflict markers and such.  A diff is then shown between\n> +\tthat temporary tree and the actual merge commit.\n\nI had not really looked at any of the --diff-merges options before.  The term\n\"remerge\" felt a bit opaque at first, because I didn't know what the diff\nwould look like. I might have found this easier:\n\n--diff-merges=resolution:::\n--diff-merges=r:::\n--resolution-diff:::\n\tThis makes two-parent merge commits show the diff with respect to\n\ta mechanical merge of their parents -- potentially containing files\n\twith conflict markers and such.\n\nBut on a second thought, remerge is actually consistent with the rest,\nbecause it states _what_ we compare to the merge commit, so nevermind.\n\n> ++\n>  --diff-merges=combined:::\n>  --diff-merges=c:::\n>  -c:::\n> diff --git a/builtin/log.c b/builtin/log.c\n> index f75d87e8d7f..d053418fddd 100644\n> --- a/builtin/log.c\n> +++ b/builtin/log.c\n> @@ -35,6 +35,7 @@\n>  #include \"repository.h\"\n>  #include \"commit-reach.h\"\n>  #include \"range-diff.h\"\n> +#include \"tmp-objdir.h\"\n>  \n>  #define MAIL_DEFAULT_WRAP 72\n>  #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n> @@ -406,6 +407,14 @@ static int cmd_log_walk(struct rev_info *rev)\n>  \tstruct commit *commit;\n>  \tint saved_nrl = 0;\n>  \tint saved_dcctc = 0;\n> +\tstruct tmp_objdir *remerge_objdir = NULL;\n> +\n> +\tif (rev->remerge_diff) {\n> +\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n> +\t\tif (!remerge_objdir)\n> +\t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n> +\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n> +\t}\n>  \n>  \tif (rev->early_output)\n>  \t\tsetup_early_output();\n> @@ -449,6 +458,9 @@ static int cmd_log_walk(struct rev_info *rev)\n>  \trev->diffopt.no_free = 0;\n>  \tdiff_free(&rev->diffopt);\n>  \n> +\tif (rev->remerge_diff)\n> +\t\ttmp_objdir_destroy(remerge_objdir);\n> +\n>  \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n>  \t    rev->diffopt.flags.check_failed) {\n>  \t\treturn 02;\n> @@ -1943,6 +1955,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n>  \t\tdie(_(\"--name-status does not make sense\"));\n>  \tif (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF)\n>  \t\tdie(_(\"--check does not make sense\"));\n> +\tif (rev.remerge_diff)\n> +\t\tdie(_(\"--remerge-diff does not make sense\"));\n>  \n>  \tif (!use_patch_format &&\n>  \t\t(!rev.diffopt.output_format ||\n> diff --git a/diff-merges.c b/diff-merges.c\n> index 5060ccd890b..0af4b3f9191 100644\n> --- a/diff-merges.c\n> +++ b/diff-merges.c\n> @@ -17,6 +17,7 @@ static void suppress(struct rev_info *revs)\n>  \trevs->combined_all_paths = 0;\n>  \trevs->merges_imply_patch = 0;\n>  \trevs->merges_need_diff = 0;\n> +\trevs->remerge_diff = 0;\n>  }\n>  \n>  static void set_separate(struct rev_info *revs)\n> @@ -45,6 +46,12 @@ static void set_dense_combined(struct rev_info *revs)\n>  \trevs->dense_combined_merges = 1;\n>  }\n>  \n> +static void set_remerge_diff(struct rev_info *revs)\n> +{\n> +\tsuppress(revs);\n> +\trevs->remerge_diff = 1;\n> +}\n> +\n>  static diff_merges_setup_func_t func_by_opt(const char *optarg)\n>  {\n>  \tif (!strcmp(optarg, \"off\") || !strcmp(optarg, \"none\"))\n> @@ -57,6 +64,8 @@ static diff_merges_setup_func_t func_by_opt(const char *optarg)\n>  \t\treturn set_combined;\n>  \telse if (!strcmp(optarg, \"cc\") || !strcmp(optarg, \"dense-combined\"))\n>  \t\treturn set_dense_combined;\n> +\telse if (!strcmp(optarg, \"r\") || !strcmp(optarg, \"remerge\"))\n> +\t\treturn set_remerge_diff;\n>  \telse if (!strcmp(optarg, \"m\") || !strcmp(optarg, \"on\"))\n>  \t\treturn set_to_default;\n>  \treturn NULL;\n> @@ -110,6 +119,9 @@ int diff_merges_parse_opts(struct rev_info *revs, const char **argv)\n>  \t} else if (!strcmp(arg, \"--cc\")) {\n>  \t\tset_dense_combined(revs);\n>  \t\trevs->merges_imply_patch = 1;\n> +\t} else if (!strcmp(arg, \"--remerge-diff\")) {\n> +\t\tset_remerge_diff(revs);\n> +\t\trevs->merges_imply_patch = 1;\n>  \t} else if (!strcmp(arg, \"--no-diff-merges\")) {\n>  \t\tsuppress(revs);\n>  \t} else if (!strcmp(arg, \"--combined-all-paths\")) {\n> diff --git a/log-tree.c b/log-tree.c\n> index 644893fd8cf..84ed864fc81 100644\n> --- a/log-tree.c\n> +++ b/log-tree.c\n> @@ -1,4 +1,5 @@\n>  #include \"cache.h\"\n> +#include \"commit-reach.h\"\n>  #include \"config.h\"\n>  #include \"diff.h\"\n>  #include \"object-store.h\"\n> @@ -7,6 +8,7 @@\n>  #include \"tag.h\"\n>  #include \"graph.h\"\n>  #include \"log-tree.h\"\n> +#include \"merge-ort.h\"\n>  #include \"reflog-walk.h\"\n>  #include \"refs.h\"\n>  #include \"string-list.h\"\n> @@ -902,6 +904,51 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n>  \treturn !opt->loginfo;\n>  }\n>  \n> +static int do_remerge_diff(struct rev_info *opt,\n> +\t\t\t   struct commit_list *parents,\n> +\t\t\t   struct object_id *oid,\n> +\t\t\t   struct commit *commit)\n> +{\n> +\tstruct merge_options o;\n> +\tstruct commit_list *bases;\n> +\tstruct merge_result res = {0};\n> +\tstruct pretty_print_context ctx = {0};\n> +\tstruct commit *parent1 = parents->item;\n> +\tstruct commit *parent2 = parents->next->item;\n> +\tstruct strbuf parent1_desc = STRBUF_INIT;\n> +\tstruct strbuf parent2_desc = STRBUF_INIT;\n> +\n> +\t/* Setup merge options */\n> +\tinit_merge_options(&o, the_repository);\n> +\to.show_rename_progress = 0;\n\nIs there a reason why we are repeating the default here (but not anywhere else)?\nFor example sequencer.c::do_merge() and builtin/am.c::fall_back_threeway()\ndon't, and probably also rely on this being disabled(?).\n\n> +\n> +\tctx.abbrev = DEFAULT_ABBREV;\n> +\tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n> +\tformat_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n> +\to.branch1 = parent1_desc.buf;\n> +\to.branch2 = parent2_desc.buf;\n> +\n> +\t/* Parse the relevant commits and get the merge bases */\n> +\tparse_commit_or_die(parent1);\n> +\tparse_commit_or_die(parent2);\n> +\tbases = get_merge_bases(parent1, parent2);\n> +\n> +\t/* Re-merge the parents */\n> +\tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n> +\n> +\t/* Show the diff */\n> +\tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n> +\tlog_tree_diff_flush(opt);\n> +\n> +\t/* Cleanup */\n> +\tstrbuf_release(&parent1_desc);\n> +\tstrbuf_release(&parent2_desc);\n> +\tmerge_finalize(&o, &res);\n> +\t/* TODO: clean up the temporary object directory */\n> +\n> +\treturn !opt->loginfo;\n> +}\n> +\n>  /*\n>   * Show the diff of a commit.\n>   *\n> @@ -936,6 +983,18 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log\n>  \t}\n>  \n>  \tif (is_merge) {\n> +\t\tint octopus = (parents->next->next != NULL);\n> +\n> +\t\tif (opt->remerge_diff) {\n> +\t\t\tif (octopus) {\n> +\t\t\t\tshow_log(opt);\n> +\t\t\t\tfprintf(opt->diffopt.file,\n> +\t\t\t\t\t\"diff: warning: Skipping remerge-diff \"\n> +\t\t\t\t\t\"for octopus merges.\\n\");\n> +\t\t\t\treturn 1;\n> +\t\t\t}\n> +\t\t\treturn do_remerge_diff(opt, parents, oid, commit);\n> +\t\t}\n>  \t\tif (opt->combine_merges)\n>  \t\t\treturn do_diff_combined(opt, commit);\n>  \t\tif (opt->separate_merges) {\n> diff --git a/revision.h b/revision.h\n> index 5578bb4720a..13178e6b8f3 100644\n> --- a/revision.h\n> +++ b/revision.h\n> @@ -195,7 +195,8 @@ struct rev_info {\n>  \t\t\tcombine_merges:1,\n>  \t\t\tcombined_all_paths:1,\n>  \t\t\tdense_combined_merges:1,\n> -\t\t\tfirst_parent_merges:1;\n> +\t\t\tfirst_parent_merges:1,\n> +\t\t\tremerge_diff:1;\n>  \n>  \t/* Format info */\n>  \tint\t\tshow_notes;\n> diff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\n> new file mode 100755\n> index 00000000000..192dbce2bfe\n> --- /dev/null\n> +++ b/t/t4069-remerge-diff.sh\n> @@ -0,0 +1,86 @@\n> +#!/bin/sh\n> +\n> +test_description='remerge-diff handling'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup basic merges' '\n> +\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n> +\tgit add numbers &&\n> +\tgit commit -m base &&\n> +\n> +\tgit branch feature_a &&\n> +\tgit branch feature_b &&\n> +\tgit branch feature_c &&\n> +\n> +\tgit branch ab_resolution &&\n> +\tgit branch bc_resolution &&\n> +\n> +\tgit checkout feature_a &&\n> +\ttest_write_lines 1 2 three 4 5 6 7 eight 9 >numbers &&\n> +\tgit commit -a -m change_a &&\n> +\n> +\tgit checkout feature_b &&\n> +\ttest_write_lines 1 2 tres 4 5 6 7 8 9 >numbers &&\n> +\tgit commit -a -m change_b &&\n> +\n> +\tgit checkout feature_c &&\n> +\ttest_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&\n> +\tgit commit -a -m change_c &&\n> +\n> +\tgit checkout bc_resolution &&\n> +\t# fast forward\n> +\tgit merge feature_b &&\n\nmaybe use --ff-only instead of the comment? Same below.\n(But if we did that we probably want to drop the \"no conflict\" comment too.)\n\n> +\t# no conflict\n> +\tgit merge feature_c &&\n> +\n> +\tgit checkout ab_resolution &&\n> +\t# fast forward\n> +\tgit merge feature_a &&\n> +\t# conflicts!\n> +\ttest_must_fail git merge feature_b &&\n> +\t# Resolve conflict...and make another change elsewhere\n> +\ttest_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers &&\n> +\tgit add numbers &&\n> +\tgit merge --continue\n> +'\n> +\n> +test_expect_success 'remerge-diff on a clean merge' '\n> +\tgit log -1 --oneline bc_resolution >expect &&\n> +\tgit show --oneline --remerge-diff bc_resolution >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' '\n> +\tgit log -1 --oneline ab_resolution >tmp &&\n> +\tcat <<-EOF >>tmp &&\n> +\tdiff --git a/numbers b/numbers\n> +\tindex a1fb731..6875544 100644\n> +\t--- a/numbers\n> +\t+++ b/numbers\n> +\t@@ -1,13 +1,9 @@\n> +\t 1\n> +\t 2\n> +\t-<<<<<<< b0ed5cb (change_a)\n> +\t-three\n> +\t-=======\n> +\t-tres\n> +\t->>>>>>> 6cd3f82 (change_b)\n> +\t+drei\n\nnice\n\n> +\t 4\n> +\t 5\n> +\t 6\n> +\t 7\n> +\t-eight\n> +\t+acht\n> +\t 9\n> +\tEOF\n> +\t# Hashes above are sha1; rip them out so test works with sha256\n> +\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n\nRight, sha256 could cause many noisy test changes. I wonder if there is a\nmore general way to avoid this; maybe default to SHA1 for existing tests?\n\n> +\n> +\tgit show --oneline --remerge-diff ab_resolution >tmp &&\n> +\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_done\n> -- \n> gitgitgadget\n> \n"},{"id":"445060","messageId":"20211228105614.qzmm3hglabtlcsx4@gmail.com","threadId":"57133","inReplyTo":"d5566f5d13605f30be6fd221fc624479cbbd0392.1640419159.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 3/8] ll-merge: make callers responsible for showing warnings","fromName":"Johannes Altmanninger","fromEmail":"aclopte@gmail.com","sentAt":"2021-12-28T10:56:14Z","receivedAt":"2021-12-28T10:56:46Z","isPatch":true,"sender":{"key":"aclopte@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6853872?v=4"},"body":"On Sat, Dec 25, 2021 at 07:59:14AM +0000, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> Since some callers may want to send warning messages to somewhere other\n> than stdout/stderr, stop printing \"warning: Cannot merge binary files\"\n> from ll-merge and instead modify the return status of ll_merge() to\n> indicate when a merge of binary files has occurred.\n> \n> This commit continues printing the message as-is; future changes will\n> start handling the new commit differently in the merge-ort codepath.\n\n\"the new commit\" looks like a typo, do you mean \"binary conflicts\"?\n\n> \n> Note that my methodology included first modifying ll_merge() to return\n> a struct, so that the compiler would catch all the callers for me and\n> ensure I had modified all of them.  After modifying all of them, I then\n> changed the struct to an enum.\n\nHeh, this is a clever way to work around C's weak typing.\n\nThe language server I'm using (clangd) supports the Call Hierarchy feature,\nwhich is intended to list callers or callees of the function at the editor's\ncursor. If I ask the server for callers of ll_merge I get this response\n(on 510f9eba9 plus this series)\n\n\tll-merge.h:98:1: ll_merge - list of callers\n\t  builtin/checkout.c:242:12: checkout_merged\n\t    builtin/checkout.c:279:17: \tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n\t  rerere.c:943:12: handle_cache\n\t    rerere.c:984:2: \tll_merge(&result, path, &mmfile[0], NULL,\n\t  notes-merge.c:342:12: ll_merge_in_worktree\n\t    notes-merge.c:353:11: \tstatus = ll_merge(&result_buf, oid_to_hex(&p->obj), &base, NULL,\n\t  merge-recursive.c:1035:12: merge_3way\n\t    merge-recursive.c:1090:17: \tmerge_status = ll_merge(result_buf, a->path, &orig, base,\n\t  merge-ort.c:1763:12: merge_3way\n\t    merge-ort.c:1816:17: \tmerge_status = ll_merge(result_buf, path, &orig, base,\n\t  merge-blobs.c:32:14: three_way_filemerge\n\t    merge-blobs.c:48:17: \tmerge_status = ll_merge(&res, path, base, NULL,\n\t  apply.c:3491:12: three_way_merge\n\t    apply.c:3511:11: \tstatus = ll_merge(&result, path,\n\t  rerere.c:608:12: try_merge\n\t    rerere.c:623:9: \t\tret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n\nSo there are 8 callers in total; but only 7 print the warning (including the\none in merge-ort which will change in the next commit). I think you missed\nthe call at rerere.c:984 because we ignore its return value.\n\n> \n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  apply.c            |  5 ++++-\n>  builtin/checkout.c | 12 ++++++++----\n>  ll-merge.c         | 40 ++++++++++++++++++++++------------------\n>  ll-merge.h         |  9 ++++++++-\n>  merge-blobs.c      |  5 ++++-\n>  merge-ort.c        |  5 ++++-\n>  merge-recursive.c  |  5 ++++-\n>  notes-merge.c      |  5 ++++-\n>  rerere.c           | 12 ++++++++----\n>  9 files changed, 66 insertions(+), 32 deletions(-)\n> \n> diff --git a/apply.c b/apply.c\n> index 43a0aebf4ee..8079395755f 100644\n> --- a/apply.c\n> +++ b/apply.c\n> @@ -3492,7 +3492,7 @@ static int three_way_merge(struct apply_state *state,\n>  {\n>  \tmmfile_t base_file, our_file, their_file;\n>  \tmmbuffer_t result = { NULL };\n> -\tint status;\n> +\tenum ll_merge_result status;\n>  \n>  \t/* resolve trivial cases first */\n>  \tif (oideq(base, ours))\n> @@ -3509,6 +3509,9 @@ static int three_way_merge(struct apply_state *state,\n>  \t\t\t  &their_file, \"theirs\",\n>  \t\t\t  state->repo->index,\n>  \t\t\t  NULL);\n> +\tif (status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\tpath, \"ours\", \"theirs\");\n>  \tfree(base_file.ptr);\n>  \tfree(our_file.ptr);\n>  \tfree(their_file.ptr);\n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index cbf73b8c9f6..3a559d69303 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -237,6 +237,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n>  \tstruct cache_entry *ce = active_cache[pos];\n>  \tconst char *path = ce->name;\n>  \tmmfile_t ancestor, ours, theirs;\n> +\tenum ll_merge_result merge_status;\n>  \tint status;\n>  \tstruct object_id oid;\n>  \tmmbuffer_t result_buf;\n> @@ -267,13 +268,16 @@ static int checkout_merged(int pos, const struct checkout *state,\n>  \tmemset(&ll_opts, 0, sizeof(ll_opts));\n>  \tgit_config_get_bool(\"merge.renormalize\", &renormalize);\n>  \tll_opts.renormalize = renormalize;\n> -\tstatus = ll_merge(&result_buf, path, &ancestor, \"base\",\n> -\t\t\t  &ours, \"ours\", &theirs, \"theirs\",\n> -\t\t\t  state->istate, &ll_opts);\n> +\tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n> +\t\t\t\t&ours, \"ours\", &theirs, \"theirs\",\n> +\t\t\t\tstate->istate, &ll_opts);\n>  \tfree(ancestor.ptr);\n>  \tfree(ours.ptr);\n>  \tfree(theirs.ptr);\n> -\tif (status < 0 || !result_buf.ptr) {\n> +\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\tpath, \"ours\", \"theirs\");\n> +\tif (merge_status < 0 || !result_buf.ptr) {\n>  \t\tfree(result_buf.ptr);\n>  \t\treturn error(_(\"path '%s': cannot merge\"), path);\n>  \t}\n> diff --git a/ll-merge.c b/ll-merge.c\n> index 261657578c7..669c09eed6c 100644\n> --- a/ll-merge.c\n> +++ b/ll-merge.c\n> @@ -14,7 +14,7 @@\n>  \n>  struct ll_merge_driver;\n>  \n> -typedef int (*ll_merge_fn)(const struct ll_merge_driver *,\n> +typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *,\n>  \t\t\t   mmbuffer_t *result,\n>  \t\t\t   const char *path,\n>  \t\t\t   mmfile_t *orig, const char *orig_name,\n> @@ -49,7 +49,7 @@ void reset_merge_attributes(void)\n>  /*\n>   * Built-in low-levels\n>   */\n> -static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n> +static enum ll_merge_result ll_binary_merge(const struct ll_merge_driver *drv_unused,\n>  \t\t\t   mmbuffer_t *result,\n>  \t\t\t   const char *path,\n>  \t\t\t   mmfile_t *orig, const char *orig_name,\n> @@ -58,6 +58,7 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n>  \t\t\t   const struct ll_merge_options *opts,\n>  \t\t\t   int marker_size)\n>  {\n> +\tenum ll_merge_result ret;\n>  \tmmfile_t *stolen;\n>  \tassert(opts);\n>  \n> @@ -68,16 +69,19 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n>  \t */\n>  \tif (opts->virtual_ancestor) {\n>  \t\tstolen = orig;\n> +\t\tret = LL_MERGE_OK;\n>  \t} else {\n>  \t\tswitch (opts->variant) {\n>  \t\tdefault:\n> -\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> -\t\t\t\tpath, name1, name2);\n> -\t\t\t/* fallthru */\n> +\t\t\tret = LL_MERGE_BINARY_CONFLICT;\n> +\t\t\tstolen = src1;\n> +\t\t\tbreak;\n>  \t\tcase XDL_MERGE_FAVOR_OURS:\n> +\t\t\tret = LL_MERGE_OK;\n>  \t\t\tstolen = src1;\n>  \t\t\tbreak;\n>  \t\tcase XDL_MERGE_FAVOR_THEIRS:\n> +\t\t\tret = LL_MERGE_OK;\n>  \t\t\tstolen = src2;\n>  \t\t\tbreak;\n>  \t\t}\n> @@ -87,16 +91,10 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n>  \tresult->size = stolen->size;\n>  \tstolen->ptr = NULL;\n>  \n> -\t/*\n> -\t * With -Xtheirs or -Xours, we have cleanly merged;\n> -\t * otherwise we got a conflict.\n> -\t */\n> -\treturn opts->variant == XDL_MERGE_FAVOR_OURS ||\n> -\t       opts->variant == XDL_MERGE_FAVOR_THEIRS ?\n> -\t       0 : 1;\n> +\treturn ret;\n>  }\n>  \n> -static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n> +static enum ll_merge_result ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n>  \t\t\tmmbuffer_t *result,\n>  \t\t\tconst char *path,\n>  \t\t\tmmfile_t *orig, const char *orig_name,\n> @@ -105,7 +103,9 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n>  \t\t\tconst struct ll_merge_options *opts,\n>  \t\t\tint marker_size)\n>  {\n> +\tenum ll_merge_result ret;\n>  \txmparam_t xmp;\n> +\tint status;\n>  \tassert(opts);\n>  \n>  \tif (orig->size > MAX_XDIFF_SIZE ||\n> @@ -133,10 +133,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n>  \txmp.ancestor = orig_name;\n>  \txmp.file1 = name1;\n>  \txmp.file2 = name2;\n> -\treturn xdl_merge(orig, src1, src2, &xmp, result);\n> +\tstatus = xdl_merge(orig, src1, src2, &xmp, result);\n> +\tret = (status > 1 ) ? LL_MERGE_CONFLICT : status;\n\n\" (status > 1 )\" has an extra space\n\nI'm not sure it's wise to handle status=1 and status=2 in two different code paths.\nBoth mean the same (the only difference is the number of conflicts).\nstatus=1 coincides with LL_MERGE_CONFLICT but that's purely coincidental\n\n\tret = (status > 0) ? LL_MERGE_CONFLICT : status;\n\n> +\treturn ret;\n>  }\n>  \n> -static int ll_union_merge(const struct ll_merge_driver *drv_unused,\n> +static enum ll_merge_result ll_union_merge(const struct ll_merge_driver *drv_unused,\n>  \t\t\t  mmbuffer_t *result,\n>  \t\t\t  const char *path,\n>  \t\t\t  mmfile_t *orig, const char *orig_name,\n> @@ -178,7 +180,7 @@ static void create_temp(mmfile_t *src, char *path, size_t len)\n>  /*\n>   * User defined low-level merge driver support.\n>   */\n> -static int ll_ext_merge(const struct ll_merge_driver *fn,\n> +static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,\n>  \t\t\tmmbuffer_t *result,\n>  \t\t\tconst char *path,\n>  \t\t\tmmfile_t *orig, const char *orig_name,\n> @@ -194,6 +196,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n>  \tconst char *args[] = { NULL, NULL };\n>  \tint status, fd, i;\n>  \tstruct stat st;\n> +\tenum ll_merge_result ret;\n>  \tassert(opts);\n>  \n>  \tsq_quote_buf(&path_sq, path);\n> @@ -236,7 +239,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n>  \t\tunlink_or_warn(temp[i]);\n>  \tstrbuf_release(&cmd);\n>  \tstrbuf_release(&path_sq);\n> -\treturn status;\n> +\tret = (status > 1) ? LL_MERGE_CONFLICT : status;\n\nsame here, I'd test for \"status > 0\" because that's the convention for\nexternal programs\n\n> +\treturn ret;\n>  }\n>  \n>  /*\n> @@ -362,7 +366,7 @@ static void normalize_file(mmfile_t *mm, const char *path, struct index_state *i\n>  \t}\n>  }\n>  \n> -int ll_merge(mmbuffer_t *result_buf,\n> +enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n>  \t     const char *path,\n>  \t     mmfile_t *ancestor, const char *ancestor_label,\n>  \t     mmfile_t *ours, const char *our_label,\n> diff --git a/ll-merge.h b/ll-merge.h\n> index aceb1b24132..e4a20e81a3a 100644\n> --- a/ll-merge.h\n> +++ b/ll-merge.h\n> @@ -82,13 +82,20 @@ struct ll_merge_options {\n>  \tlong xdl_opts;\n>  };\n>  \n> +enum ll_merge_result {\n> +\tLL_MERGE_ERROR = -1,\n> +\tLL_MERGE_OK = 0,\n> +\tLL_MERGE_CONFLICT,\n> +\tLL_MERGE_BINARY_CONFLICT,\n> +};\n> +\n>  /**\n>   * Perform a three-way single-file merge in core.  This is a thin wrapper\n>   * around `xdl_merge` that takes the path and any merge backend specified in\n>   * `.gitattributes` or `.git/info/attributes` into account.\n>   * Returns 0 for a clean merge.\n>   */\n> -int ll_merge(mmbuffer_t *result_buf,\n> +enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n>  \t     const char *path,\n>  \t     mmfile_t *ancestor, const char *ancestor_label,\n>  \t     mmfile_t *ours, const char *our_label,\n> diff --git a/merge-blobs.c b/merge-blobs.c\n> index ee0a0e90c94..8138090f81c 100644\n> --- a/merge-blobs.c\n> +++ b/merge-blobs.c\n> @@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate,\n>  \t\t\t\t mmfile_t *their,\n>  \t\t\t\t unsigned long *size)\n>  {\n> -\tint merge_status;\n> +\tenum ll_merge_result merge_status;\n>  \tmmbuffer_t res;\n>  \n>  \t/*\n> @@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate,\n>  \t\t\t\tistate, NULL);\n>  \tif (merge_status < 0)\n>  \t\treturn NULL;\n> +\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\tpath, \".our\", \".their\");\n>  \n>  \t*size = res.size;\n>  \treturn res.ptr;\n> diff --git a/merge-ort.c b/merge-ort.c\n> index 0342f104836..c24da2ba3cb 100644\n> --- a/merge-ort.c\n> +++ b/merge-ort.c\n> @@ -1743,7 +1743,7 @@ static int merge_3way(struct merge_options *opt,\n>  \tmmfile_t orig, src1, src2;\n>  \tstruct ll_merge_options ll_opts = {0};\n>  \tchar *base, *name1, *name2;\n> -\tint merge_status;\n> +\tenum ll_merge_result merge_status;\n>  \n>  \tif (!opt->priv->attr_index.initialized)\n>  \t\tinitialize_attr_index(opt);\n> @@ -1787,6 +1787,9 @@ static int merge_3way(struct merge_options *opt,\n>  \tmerge_status = ll_merge(result_buf, path, &orig, base,\n>  \t\t\t\t&src1, name1, &src2, name2,\n>  \t\t\t\t&opt->priv->attr_index, &ll_opts);\n> +\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\tpath, name1, name2);\n>  \n>  \tfree(base);\n>  \tfree(name1);\n> diff --git a/merge-recursive.c b/merge-recursive.c\n> index d9457797dbb..bc73c52dd84 100644\n> --- a/merge-recursive.c\n> +++ b/merge-recursive.c\n> @@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt,\n>  \tmmfile_t orig, src1, src2;\n>  \tstruct ll_merge_options ll_opts = {0};\n>  \tchar *base, *name1, *name2;\n> -\tint merge_status;\n> +\tenum ll_merge_result merge_status;\n>  \n>  \tll_opts.renormalize = opt->renormalize;\n>  \tll_opts.extra_marker_size = extra_marker_size;\n> @@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt,\n>  \tmerge_status = ll_merge(result_buf, a->path, &orig, base,\n>  \t\t\t\t&src1, name1, &src2, name2,\n>  \t\t\t\topt->repo->index, &ll_opts);\n> +\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\ta->path, name1, name2);\n>  \n>  \tfree(base);\n>  \tfree(name1);\n> diff --git a/notes-merge.c b/notes-merge.c\n> index b4a3a903e86..01d596920ea 100644\n> --- a/notes-merge.c\n> +++ b/notes-merge.c\n> @@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n>  {\n>  \tmmbuffer_t result_buf;\n>  \tmmfile_t base, local, remote;\n> -\tint status;\n> +\tenum ll_merge_result status;\n>  \n>  \tread_mmblob(&base, &p->base);\n>  \tread_mmblob(&local, &p->local);\n> @@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n>  \tfree(local.ptr);\n>  \tfree(remote.ptr);\n>  \n> +\tif (status == LL_MERGE_BINARY_CONFLICT)\n> +\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\toid_to_hex(&p->obj), o->local_ref, o->remote_ref);\n>  \tif ((status < 0) || !result_buf.ptr)\n>  \t\tdie(\"Failed to execute internal merge\");\n>  \n> diff --git a/rerere.c b/rerere.c\n> index d83d58df4fb..b1f8961ed9e 100644\n> --- a/rerere.c\n> +++ b/rerere.c\n> @@ -609,19 +609,23 @@ static int try_merge(struct index_state *istate,\n>  \t\t     const struct rerere_id *id, const char *path,\n>  \t\t     mmfile_t *cur, mmbuffer_t *result)\n>  {\n> -\tint ret;\n> +\tenum ll_merge_result ret;\n>  \tmmfile_t base = {NULL, 0}, other = {NULL, 0};\n>  \n>  \tif (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n> -\t    read_mmfile(&other, rerere_path(id, \"postimage\")))\n> -\t\tret = 1;\n> -\telse\n> +\t    read_mmfile(&other, rerere_path(id, \"postimage\"))) {\n> +\t\tret = LL_MERGE_CONFLICT;\n> +\t} else {\n>  \t\t/*\n>  \t\t * A three-way merge. Note that this honors user-customizable\n>  \t\t * low-level merge driver settings.\n>  \t\t */\n>  \t\tret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n>  \t\t\t       istate, NULL);\n> +\t\tif (ret == LL_MERGE_BINARY_CONFLICT)\n> +\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> +\t\t\t\tpath, \"\", \"\");\n\nWith the next patch, 7/8 callers of ll_merge (almost) immediately print\nthat warning.  Looks fine as is, but does it make sense to introduce a helper\nfunction for the common case, or add a flag to ll_merge_options?\n\n> +\t}\n>  \n>  \tfree(base.ptr);\n>  \tfree(other.ptr);\n> -- \n> gitgitgadget\n> \n"},{"id":"445061","messageId":"20211228105642.ajmr2kqjm5yikneu@gmail.com","threadId":"57133","inReplyTo":"887e46435c0561e86f1858682fe53e9de926b69c.1640419160.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 6/8] merge-ort: format messages slightly different for use in headers","fromName":"Johannes Altmanninger","fromEmail":"aclopte@gmail.com","sentAt":"2021-12-28T10:56:42Z","receivedAt":"2021-12-28T10:56:55Z","isPatch":true,"sender":{"key":"aclopte@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6853872?v=4"},"body":"On Sat, Dec 25, 2021 at 07:59:17AM +0000, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> When users run\n>     git show --remerge-diff $MERGE_COMMIT\n> or\n>     git log -p --remerge-diff ...\n> stdout is not an appropriate location to dump conflict messages, but we\n> do want to provide them to users.  We will include them in the diff\n> headers instead...but for that to work, we need for any multiline\n> messages to replace newlines with both a newline and a space.  Add a new\n> flag to signal when we want these messages modified in such a fashion,\n> and use it in path_msg() to modify these messages this way.\n\nmakes sense (same for patches 4 & 5)\n\n> \n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  merge-ort.c       | 36 ++++++++++++++++++++++++++++++++++--\n>  merge-recursive.c |  3 +++\n>  merge-recursive.h |  1 +\n>  3 files changed, 38 insertions(+), 2 deletions(-)\n> \n> diff --git a/merge-ort.c b/merge-ort.c\n> index 998e92ec593..9142d56e0ad 100644\n> --- a/merge-ort.c\n> +++ b/merge-ort.c\n> @@ -634,17 +634,46 @@ static void path_msg(struct merge_options *opt,\n>  \t\t     const char *fmt, ...)\n>  {\n>  \tva_list ap;\n> -\tstruct strbuf *sb = strmap_get(&opt->priv->output, path);\n> +\tstruct strbuf *sb, *dest;\n> +\tstruct strbuf tmp = STRBUF_INIT;\n> +\n> +\tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n> +\t\treturn; /* Do not record mere hints in tree */\n> +\tsb = strmap_get(&opt->priv->output, path);\n>  \tif (!sb) {\n>  \t\tsb = xmalloc(sizeof(*sb));\n>  \t\tstrbuf_init(sb, 0);\n>  \t\tstrmap_put(&opt->priv->output, path, sb);\n>  \t}\n>  \n> +\tdest = (opt->record_conflict_msgs_as_headers ? &tmp : sb);\n> +\n>  \tva_start(ap, fmt);\n> -\tstrbuf_vaddf(sb, fmt, ap);\n> +\tstrbuf_vaddf(dest, fmt, ap);\n>  \tva_end(ap);\n>  \n> +\tif (opt->record_conflict_msgs_as_headers) {\n> +\t\tint i_sb = 0, i_tmp = 0;\n> +\n> +\t\t/* Copy tmp to sb, adding spaces after newlines */\n> +\t\tstrbuf_grow(sb, 2*tmp.len); /* more than sufficient */\n> +\t\tfor (; i_tmp < tmp.len; i_tmp++, i_sb++) {\n> +\t\t\t/* Copy next character from tmp to sb */\n> +\t\t\tsb->buf[sb->len + i_sb] = tmp.buf[i_tmp];\n> +\n> +\t\t\t/* If we copied a newline, add a space */\n> +\t\t\tif (tmp.buf[i_tmp] == '\\n')\n> +\t\t\t\tsb->buf[++i_sb] = ' ';\n> +\t\t}\n> +\t\t/* Update length and ensure it's NUL-terminated */\n\nI think this and the two comments inside the loop are mostly redundant. I'd\ndrop them (except maybe this one because it's a common oversight I guess).\n\n> +\t\tsb->len += i_sb;\n> +\t\tsb->buf[sb->len] = '\\0';\n> +\n> +\t\t/* Clean up tmp */\n\nAlso this one I guess\n\n> +\t\tstrbuf_release(&tmp);\n> +\t}\n> +\n> +\t/* Add final newline character to sb */\n>  \tstrbuf_addch(sb, '\\n');\n>  }\n>  \n> @@ -4246,6 +4275,9 @@ void merge_switch_to_result(struct merge_options *opt,\n>  \t\tstruct string_list olist = STRING_LIST_INIT_NODUP;\n>  \t\tint i;\n>  \n> +\t\tif (opt->record_conflict_msgs_as_headers)\n> +\t\t\tBUG(\"Either display conflict messages or record them as headers, not both\");\n> +\n>  \t\ttrace2_region_enter(\"merge\", \"display messages\", opt->repo);\n>  \n>  \t\t/* Hack to pre-allocate olist to the desired size */\n> diff --git a/merge-recursive.c b/merge-recursive.c\n> index bc73c52dd84..c9ba7e904a6 100644\n> --- a/merge-recursive.c\n> +++ b/merge-recursive.c\n> @@ -3714,6 +3714,9 @@ static int merge_start(struct merge_options *opt, struct tree *head)\n>  \n>  \tassert(opt->priv == NULL);\n>  \n> +\t/* Not supported; option specific to merge-ort */\n> +\tassert(!opt->record_conflict_msgs_as_headers);\n> +\n>  \t/* Sanity check on repo state; index must match head */\n>  \tif (repo_index_has_changes(opt->repo, head, &sb)) {\n>  \t\terr(opt, _(\"Your local changes to the following files would be overwritten by merge:\\n  %s\"),\n> diff --git a/merge-recursive.h b/merge-recursive.h\n> index 0795a1d3ec1..ebfdb7f994e 100644\n> --- a/merge-recursive.h\n> +++ b/merge-recursive.h\n> @@ -46,6 +46,7 @@ struct merge_options {\n>  \t/* miscellaneous control options */\n>  \tconst char *subtree_shift;\n>  \tunsigned renormalize : 1;\n> +\tunsigned record_conflict_msgs_as_headers : 1;\n>  \n>  \t/* internal fields used by the implementation */\n>  \tstruct merge_options_internal *priv;\n> -- \n> gitgitgadget\n> \n"},{"id":"445062","messageId":"20211228105733.lomkg23htd2kjtii@gmail.com","threadId":"57133","inReplyTo":"e94706513038adc85e818e8736ad713b2e6b6be4.1640419160.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 7/8] diff: add ability to insert additional headers for paths","fromName":"Johannes Altmanninger","fromEmail":"aclopte@gmail.com","sentAt":"2021-12-28T10:57:33Z","receivedAt":"2021-12-28T10:57:39Z","isPatch":true,"sender":{"key":"aclopte@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6853872?v=4"},"body":"On Sat, Dec 25, 2021 at 07:59:18AM +0000, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> When additional headers are provided, we need to\n>   * add diff_filepairs to diff_queued_diff for each paths in the\n>     additional headers map which, unless that path is part of\n>     another diff_filepair already found in diff_queued_diff\n>   * format the headers (colorization, line_prefix for --graph)\n>   * make sure the various codepaths that attempt to return early\n>     if there are \"no changes\" take into account the headers that\n>     need to be shown.\n> \n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  diff.c     | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++--\n>  diff.h     |   3 +-\n>  log-tree.c |   2 +-\n>  3 files changed, 115 insertions(+), 6 deletions(-)\n> \n> diff --git a/diff.c b/diff.c\n> index 861282db1c3..aaa6a19f158 100644\n> --- a/diff.c\n> +++ b/diff.c\n> @@ -27,6 +27,7 @@\n>  #include \"help.h\"\n>  #include \"promisor-remote.h\"\n>  #include \"dir.h\"\n> +#include \"strmap.h\"\n>  \n>  #ifdef NO_FAST_WORKING_DIRECTORY\n>  #define FAST_WORKING_DIRECTORY 0\n> @@ -3406,6 +3407,31 @@ struct userdiff_driver *get_textconv(struct repository *r,\n>  \treturn userdiff_get_textconv(r, one->driver);\n>  }\n>  \n> +static struct strbuf *additional_headers(struct diff_options *o,\n> +\t\t\t\t\t const char *path)\n> +{\n> +\tif (!o->additional_path_headers)\n> +\t\treturn NULL;\n> +\treturn strmap_get(o->additional_path_headers, path);\n> +}\n> +\n> +static void add_formatted_headers(struct strbuf *msg,\n> +\t\t\t\t  struct strbuf *more_headers,\n> +\t\t\t\t  const char *line_prefix,\n> +\t\t\t\t  const char *meta,\n> +\t\t\t\t  const char *reset)\n> +{\n> +\tchar *next, *newline;\n> +\n> +\tfor (next = more_headers->buf; *next; next = newline) {\n> +\t\tnewline = strchrnul(next, '\\n');\n> +\t\tstrbuf_addf(msg, \"%s%s%.*s%s\\n\", line_prefix, meta,\n> +\t\t\t    (int)(newline - next), next, reset);\n> +\t\tif (*newline)\n> +\t\t\tnewline++;\n> +\t}\n> +}\n> +\n>  static void builtin_diff(const char *name_a,\n>  \t\t\t const char *name_b,\n>  \t\t\t struct diff_filespec *one,\n> @@ -3464,6 +3490,17 @@ static void builtin_diff(const char *name_a,\n>  \tb_two = quote_two(b_prefix, name_b + (*name_b == '/'));\n>  \tlbl[0] = DIFF_FILE_VALID(one) ? a_one : \"/dev/null\";\n>  \tlbl[1] = DIFF_FILE_VALID(two) ? b_two : \"/dev/null\";\n> +\tif (!DIFF_FILE_VALID(one) && !DIFF_FILE_VALID(two)) {\n> +\t\t/*\n> +\t\t * We should only reach this point for pairs from\n> +\t\t * create_filepairs_for_header_only_notifications().  For\n> +\t\t * these, we should avoid the \"/dev/null\" special casing\n> +\t\t * above, meaning we avoid showing such pairs as either\n> +\t\t * \"new file\" or \"deleted file\" below.\n> +\t\t */\n> +\t\tlbl[0] = a_one;\n> +\t\tlbl[1] = b_two;\n> +\t}\n\nnot so familiar with this logic, but I saw that without this change, the\nrename/rename conflict test fails. Is this because we add a file pair under\nthe original name (that's been renamed on both sides). I wonder if we\ncan sketch such a case in the comment.\n\n>  \tstrbuf_addf(&header, \"%s%sdiff --git %s %s%s\\n\", line_prefix, meta, a_one, b_two, reset);\n>  \tif (lbl[0][0] == '/') {\n>  \t\t/* /dev/null */\n> @@ -4328,6 +4365,7 @@ static void fill_metainfo(struct strbuf *msg,\n>  \tconst char *set = diff_get_color(use_color, DIFF_METAINFO);\n>  \tconst char *reset = diff_get_color(use_color, DIFF_RESET);\n>  \tconst char *line_prefix = diff_line_prefix(o);\n> +\tstruct strbuf *more_headers = NULL;\n>  \n>  \t*must_show_header = 1;\n>  \tstrbuf_init(msg, PATH_MAX * 2 + 300);\n> @@ -4364,6 +4402,11 @@ static void fill_metainfo(struct strbuf *msg,\n>  \tdefault:\n>  \t\t*must_show_header = 0;\n>  \t}\n> +\tif ((more_headers = additional_headers(o, name))) {\n> +\t\tadd_formatted_headers(msg, more_headers,\n> +\t\t\t\t      line_prefix, set, reset);\n> +\t\t*must_show_header = 1;\n> +\t}\n>  \tif (one && two && !oideq(&one->oid, &two->oid)) {\n>  \t\tconst unsigned hexsz = the_hash_algo->hexsz;\n>  \t\tint abbrev = o->abbrev ? o->abbrev : DEFAULT_ABBREV;\n> @@ -5852,12 +5895,22 @@ int diff_unmodified_pair(struct diff_filepair *p)\n>  \n>  static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n>  {\n> -\tif (diff_unmodified_pair(p))\n> +\t/*\n> +\t * Check if we can return early without showing a diff.  Note that\n> +\t * diff_filepair only stores {oid, path, mode, is_valid}\n> +\t * information for each path, and thus diff_unmodified_pair() only\n> +\t * considers those bits of info.  However, we do not want pairs\n> +\t * created by create_filepairs_for_header_only_notifications() to\n> +\t * be ignored, so return early if both p is unmodified AND\n> +\t * p->one->path is not in additional headers.\n> +\t */\n> +\tif (diff_unmodified_pair(p) && !additional_headers(o, p->one->path))\n>  \t\treturn;\n>  \n> +\t/* Actually, we can also return early to avoid showing tree diffs */\n>  \tif ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||\n>  \t    (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))\n> -\t\treturn; /* no tree diffs in patch format */\n> +\t\treturn;\n>  \n>  \trun_diff(p, o);\n>  }\n> @@ -5888,10 +5941,14 @@ static void diff_flush_checkdiff(struct diff_filepair *p,\n>  \trun_checkdiff(p, o);\n>  }\n>  \n> -int diff_queue_is_empty(void)\n> +int diff_queue_is_empty(struct diff_options *o)\n>  {\n>  \tstruct diff_queue_struct *q = &diff_queued_diff;\n>  \tint i;\n> +\n> +\tif (o->additional_path_headers &&\n> +\t    !strmap_empty(o->additional_path_headers))\n> +\t\treturn 0;\n>  \tfor (i = 0; i < q->nr; i++)\n>  \t\tif (!diff_unmodified_pair(q->queue[i]))\n>  \t\t\treturn 0;\n> @@ -6325,6 +6382,54 @@ void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)\n>  \t\twarning(_(rename_limit_advice), varname, needed);\n>  }\n>  \n> +static void create_filepairs_for_header_only_notifications(struct diff_options *o)\n> +{\n> +\tstruct strset present;\n> +\tstruct diff_queue_struct *q = &diff_queued_diff;\n> +\tstruct hashmap_iter iter;\n> +\tstruct strmap_entry *e;\n> +\tint i;\n> +\n> +\tstrset_init_with_options(&present, /*pool*/ NULL, /*strdup*/ 0);\n> +\n> +\t/*\n> +\t * Find out which paths exist in diff_queued_diff, preferring\n> +\t * one->path for any pair that has multiple paths.\n\nWhy do we prefer one->path?\n\n> +\t */\n> +\tfor (i = 0; i < q->nr; i++) {\n> +\t\tstruct diff_filepair *p = q->queue[i];\n> +\t\tchar *path = p->one->path ? p->one->path : p->two->path;\n> +\n> +\t\tif (strmap_contains(o->additional_path_headers, path))\n> +\t\t\tstrset_add(&present, path);\n> +\t}\n> +\n> +\t/*\n> +\t * Loop over paths in additional_path_headers; for each NOT already\n> +\t * in diff_queued_diff, create a synthetic filepair and insert that\n> +\t * into diff_queued_diff.\n> +\t */\n> +\tstrmap_for_each_entry(o->additional_path_headers, &iter, e) {\n> +\t\tif (!strset_contains(&present, e->key)) {\n> +\t\t\tstruct diff_filespec *one, *two;\n> +\t\t\tstruct diff_filepair *p;\n> +\n> +\t\t\tone = alloc_filespec(e->key);\n> +\t\t\ttwo = alloc_filespec(e->key);\n> +\t\t\tfill_filespec(one, null_oid(), 0, 0);\n> +\t\t\tfill_filespec(two, null_oid(), 0, 0);\n> +\t\t\tp = diff_queue(q, one, two);\n> +\t\t\tp->status = DIFF_STATUS_MODIFIED;\n> +\t\t}\n> +\t}\n\nAll these string hash-maps are not really typical for a C program. I'm sure\nthey are the best choice for an advanced merge algorithm but they are not\nreally necessary for computing/printing a diff. It feels like this is an\nimplementation detail from merge-ort that's leaking into other components.\n\nWhat we want to do is\n\n\tfor file_pair in additional_headers:\n\t\tif not already_queued(file_pair):\n\t\t\tqueue(file_pair)\n\nto do that, you use a temporary has-set (\"present\") that records everything\nthat's already queued (already_queued() is a lookup in that set).\n\nLet's assume both the queue and additional_headers are sorted arrays.\nThen we could efficiently merge them (like a merge-sort algorithm)\nwithout ever allocating a temporary hash map.\n\nI haven't checked if this is practical (better wait for feedback).\nWe'd probably need to convert the strmap additional_path_headers into an\narray and sort it (I guess our hash map does not guarantee any ordering?)\n\n> +\n> +\t/* Re-sort the filepairs */\n> +\tdiffcore_fix_diff_index();\n> +\n> +\t/* Cleanup */\n> +\tstrset_clear(&present);\n\nNot a strong opinion, but I'd probably drop this comment\n\n> +}\n> +\n>  static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n>  {\n>  \tint i;\n> @@ -6337,6 +6442,9 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n>  \tif (o->color_moved)\n>  \t\to->emitted_symbols = &esm;\n>  \n> +\tif (o->additional_path_headers)\n> +\t\tcreate_filepairs_for_header_only_notifications(o);\n> +\n>  \tfor (i = 0; i < q->nr; i++) {\n>  \t\tstruct diff_filepair *p = q->queue[i];\n>  \t\tif (check_pair_status(p))\n> @@ -6413,7 +6521,7 @@ void diff_flush(struct diff_options *options)\n>  \t * Order: raw, stat, summary, patch\n>  \t * or:    name/name-status/checkdiff (other bits clear)\n>  \t */\n> -\tif (!q->nr)\n> +\tif (!q->nr && !options->additional_path_headers)\n>  \t\tgoto free_queue;\n>  \n>  \tif (output_format & (DIFF_FORMAT_RAW |\n> diff --git a/diff.h b/diff.h\n> index 8ba85c5e605..06a0a67afda 100644\n> --- a/diff.h\n> +++ b/diff.h\n> @@ -395,6 +395,7 @@ struct diff_options {\n>  \n>  \tstruct repository *repo;\n>  \tstruct option *parseopts;\n> +\tstruct strmap *additional_path_headers;\n>  \n>  \tint no_free;\n>  };\n> @@ -593,7 +594,7 @@ void diffcore_fix_diff_index(void);\n>  \"                show all files diff when -S is used and hit is found.\\n\" \\\n>  \"  -a  --text    treat all files as text.\\n\"\n>  \n> -int diff_queue_is_empty(void);\n> +int diff_queue_is_empty(struct diff_options*);\n>  void diff_flush(struct diff_options*);\n>  void diff_free(struct diff_options*);\n>  void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);\n> diff --git a/log-tree.c b/log-tree.c\n> index d4655b63d75..33c28f537a6 100644\n> --- a/log-tree.c\n> +++ b/log-tree.c\n> @@ -850,7 +850,7 @@ int log_tree_diff_flush(struct rev_info *opt)\n>  \topt->shown_dashes = 0;\n>  \tdiffcore_std(&opt->diffopt);\n>  \n> -\tif (diff_queue_is_empty()) {\n> +\tif (diff_queue_is_empty(&opt->diffopt)) {\n>  \t\tint saved_fmt = opt->diffopt.output_format;\n>  \t\topt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT;\n>  \t\tdiff_flush(&opt->diffopt);\n> -- \n> gitgitgadget\n> \n"},{"id":"445063","messageId":"20211228105755.zgahkoxn6ewjpdol@gmail.com","threadId":"57133","inReplyTo":"4cc53c55a6ea1531342b23bc9343890a576d9f1c.1640419160.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 8/8] show, log: include conflict/warning messages in --remerge-diff headers","fromName":"Johannes Altmanninger","fromEmail":"aclopte@gmail.com","sentAt":"2021-12-28T10:57:55Z","receivedAt":"2021-12-28T10:58:00Z","isPatch":true,"sender":{"key":"aclopte@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6853872?v=4"},"body":"On Sat, Dec 25, 2021 at 07:59:19AM +0000, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> Conflicts such as modify/delete, rename/rename, or file/directory are\n> not representable via content conflict markers, and the normal output\n> messages notifying users about these were dropped with --remerge-diff.\n> While we don't want these messages randomly shown before the commit\n> and diff headers, we do want them to still be shown; include them as\n> part of the diff headers instead.\n> \n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  log-tree.c              |  3 ++\n>  merge-ort.c             |  1 +\n>  merge-ort.h             | 10 +++++\n>  t/t4069-remerge-diff.sh | 86 +++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 100 insertions(+)\n> \n> diff --git a/log-tree.c b/log-tree.c\n> index 33c28f537a6..97fbb756d21 100644\n> --- a/log-tree.c\n> +++ b/log-tree.c\n> @@ -922,6 +922,7 @@ static int do_remerge_diff(struct rev_info *opt,\n>  \t/* Setup merge options */\n>  \tinit_merge_options(&o, the_repository);\n>  \to.show_rename_progress = 0;\n> +\to.record_conflict_msgs_as_headers = 1;\n>  \n>  \tctx.abbrev = DEFAULT_ABBREV;\n>  \tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n> @@ -938,10 +939,12 @@ static int do_remerge_diff(struct rev_info *opt,\n>  \tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n>  \n>  \t/* Show the diff */\n> +\topt->diffopt.additional_path_headers = res.path_messages;\n>  \tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n>  \tlog_tree_diff_flush(opt);\n>  \n>  \t/* Cleanup */\n> +\topt->diffopt.additional_path_headers = NULL;\n>  \tstrbuf_release(&parent1_desc);\n>  \tstrbuf_release(&parent2_desc);\n>  \tmerge_finalize(&o, &res);\n> diff --git a/merge-ort.c b/merge-ort.c\n> index 9142d56e0ad..07e53083cbd 100644\n> --- a/merge-ort.c\n> +++ b/merge-ort.c\n> @@ -4579,6 +4579,7 @@ redo:\n>  \ttrace2_region_leave(\"merge\", \"process_entries\", opt->repo);\n>  \n>  \t/* Set return values */\n> +\tresult->path_messages = &opt->priv->output;\n>  \tresult->tree = parse_tree_indirect(&working_tree_oid);\n>  \t/* existence of conflicted entries implies unclean */\n>  \tresult->clean &= strmap_empty(&opt->priv->conflicted);\n> diff --git a/merge-ort.h b/merge-ort.h\n> index c011864ffeb..fe599b87868 100644\n> --- a/merge-ort.h\n> +++ b/merge-ort.h\n> @@ -5,6 +5,7 @@\n>  \n>  struct commit;\n>  struct tree;\n> +struct strmap;\n>  \n>  struct merge_result {\n>  \t/*\n> @@ -23,6 +24,15 @@ struct merge_result {\n>  \t */\n>  \tstruct tree *tree;\n>  \n> +\t/*\n> +\t * Special messages and conflict notices for various paths\n> +\t *\n> +\t * This is a map of pathnames to strbufs.  It contains various\n> +\t * warning/conflict/notice messages (possibly multiple per path)\n> +\t * that callers may want to use.\n> +\t */\n> +\tstruct strmap *path_messages;\n> +\n>  \t/*\n>  \t * Additional metadata used by merge_switch_to_result() or future calls\n>  \t * to merge_incore_*().  Includes data needed to update the index (if\n> diff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\n> index 192dbce2bfe..a040d3bcd91 100755\n> --- a/t/t4069-remerge-diff.sh\n> +++ b/t/t4069-remerge-diff.sh\n> @@ -4,6 +4,15 @@ test_description='remerge-diff handling'\n>  \n>  . ./test-lib.sh\n>  \n> +# --remerge-diff uses ort under the hood regardless of setting.  However,\n> +# we set up a file/directory conflict beforehand, and the different backends\n> +# handle the conflict differently, which would require separate code paths\n> +# to resolve.  There's not much point in making the code uglier to do that,\n> +# though, when the real thing we are testing (--remerge-diff) will hardcode\n> +# calls directly into the merge-ort API anyway.  So just force the use of\n> +# ort on the setup too.\n> +GIT_TEST_MERGE_ALGORITHM=ort\n> +\n>  test_expect_success 'setup basic merges' '\n>  \ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n>  \tgit add numbers &&\n> @@ -55,6 +64,7 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n>  \tgit log -1 --oneline ab_resolution >tmp &&\n>  \tcat <<-EOF >>tmp &&\n>  \tdiff --git a/numbers b/numbers\n> +\tCONFLICT (content): Merge conflict in numbers\n>  \tindex a1fb731..6875544 100644\n>  \t--- a/numbers\n>  \t+++ b/numbers\n> @@ -83,4 +93,80 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'setup non-content conflicts' '\n> +\tgit switch --orphan base &&\n> +\n> +\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n> +\ttest_write_lines a b c d e f g h i >letters &&\n> +\ttest_write_lines in the way >content &&\n> +\tgit add numbers letters content &&\n> +\tgit commit -m base &&\n> +\n> +\tgit branch side1 &&\n> +\tgit branch side2 &&\n> +\n> +\tgit checkout side1 &&\n> +\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n> +\tgit mv letters letters_side1 &&\n> +\tgit mv content file_or_directory &&\n> +\tgit add numbers &&\n> +\tgit commit -m side1 &&\n> +\n> +\tgit checkout side2 &&\n> +\tgit rm numbers &&\n> +\tgit mv letters letters_side2 &&\n> +\tmkdir file_or_directory &&\n> +\techo hello >file_or_directory/world &&\n> +\tgit add file_or_directory/world &&\n> +\tgit commit -m side2 &&\n> +\n> +\tgit checkout -b resolution side1 &&\n> +\ttest_must_fail git merge side2 &&\n> +\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n> +\tgit add numbers &&\n> +\tgit add letters_side1 &&\n> +\tgit rm letters &&\n> +\tgit rm letters_side2 &&\n> +\tgit add file_or_directory~HEAD &&\n> +\tgit mv file_or_directory~HEAD wanted_content &&\n> +\tgit commit -m resolved\n> +'\n> +\n> +test_expect_success 'remerge-diff with non-content conflicts' '\n> +\tgit log -1 --oneline resolution >tmp &&\n> +\tcat <<-EOF >>tmp &&\n> +\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n\nthe \"~HASH (side1)\" suffix will probably mess with some programs that extract\nthe filename from the diff.\nI don't know what programs are supposed to expect.  I can see arguments for\neither dropping the suffix or including only \"~HASH\" since that's part of\nthe actual filename that's left in the worktree.\nMaybe it's not so important.\n\nThe file/link typechange conflict test I'll add below exposes what looks\nlike an accidental interaction with the trailing tab characters that we emit\non --- and +++ lines if the \"filename\" contains a space (since 1a9eb3b9d5\n(git-diff/git-apply: make diff output a bit friendlier to GNU patch (part\n2), 2006-09-22)).\n\n\tindex 70885e4..0000000\n\t--- a/typechange~738109f (side1)\t<-- git diff adds a trailing tab!\n\t+++ /dev/null\n\nI haven't formed an opinion yet, but since Tig uses the --- and +++ lines\nto extract file names, I'd drop the \" (side1)\" suffix from at least the ---\nand +++ lines. Maybe also the ^diff lines, I'm not sure\n\n> +\tsimilarity index 100%\n> +\trename from file_or_directory~HASH (side1)\n> +\trename to wanted_content\n> +\tCONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n\nI wonder if it's better to have this line further up, before the \"rename\"\nresolution, to correct the temporal order.\n\n> +\tdiff --git a/letters b/letters\n> +\tCONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n> +\tdiff --git a/letters_side2 b/letters_side2\n> +\tdeleted file mode 100644\n> +\tindex b236ae5..0000000\n> +\t--- a/letters_side2\n> +\t+++ /dev/null\n> +\t@@ -1,9 +0,0 @@\n> +\t-a\n> +\t-b\n> +\t-c\n> +\t-d\n> +\t-e\n> +\t-f\n> +\t-g\n> +\t-h\n> +\t-i\n> +\tdiff --git a/numbers b/numbers\n> +\tCONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n> +\tEOF\n\nTook me some time to grok these but the output makes sense (it's loud and\nugly but that's okay since these are serious conflicts).\n\n> +\t# We still have some sha1 hashes above; rip them out so test works\n> +\t# with sha256\n> +\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n> +\n> +\tgit show --oneline --remerge-diff resolution >tmp &&\n> +\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n>  test_done\n> -- \n> gitgitgadget\n\nWe're missing a test case for typechange.  Here's is a quick draft I've been\nplaying around with. Seems ugly that the \"diff --git a/typechange b/typechange\"\nis doubled but okay.\n\nMaybe a rename/delete conflict is interesting as well, I'm not sure.  (Also I\nwonder if switching the order of parents will give any interesting difference,\nI guess not)\n\ntest_expect_success 'remerge-diff with file/link conflict' '\n\tgit branch -d base side1 side2 &&\n\tgit switch --orphan base &&\n\n\techo base >typechange &&\n\tgit add typechange &&\n\tgit commit -m base &&\n\n\tgit branch side1 &&\n\tgit branch side2 &&\n\n\tgit checkout side1 &&\n\techo orig-file-contents >typechange &&\n\tgit commit -a -m side1 &&\n\n\tgit checkout side2 &&\n\tln -sf . typechange &&\n\tgit add typechange &&\n\tgit commit -m side2 &&\n\n\tgit checkout -b resolution2 side1 &&\n\ttest_must_fail git merge side2 &&\n\trm typechange &&\n\tmv typechange~HEAD typechange &&\n\techo resolved >>typechange &&\n\tgit add typechange~HEAD typechange &&\n\tgit merge --continue &&\n\n\tgit show --oneline --remerge-diff resolution2 >tmp &&\n\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n\n\tcat <<-EOF >tmp &&\n\t7759b27 Merge branch ${SQ}side2${SQ} into resolution2\n\tdiff --git a/typechange b/typechange\n\tdeleted file mode 120000\n\tCONFLICT (distinct types): typechange had different types on each side; renamed one of them so each can be recorded somewhere.\n\tindex 945c9b4..0000000\n\t--- a/typechange\n\t+++ /dev/null\n\t@@ -1 +0,0 @@\n\t-.\n\t\\ No newline at end of file\n\tdiff --git a/typechange b/typechange\n\tnew file mode 100644\n\tCONFLICT (distinct types): typechange had different types on each side; renamed one of them so each can be recorded somewhere.\n\tindex 0000000..70885e4\n\t--- /dev/null\n\t+++ b/typechange\n\t@@ -0,0 +1,2 @@\n\t+orig-file-contents\n\t+resolved\n\tdiff --git a/typechange~738109f (side1) b/typechange~738109f (side1)\n\tdeleted file mode 100644\n\tindex 70885e4..0000000\n\t--- a/typechange~738109f (side1)\t\n\t+++ /dev/null\n\t@@ -1 +0,0 @@\n\t-orig-file-contents\n\tEOF\n\t# We still have some sha1 hashes above; rip them out so test works\n\t# with sha256\n\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n\n\ttest_cmp expect actual\n'\n"},{"id":"445133","messageId":"CABPp-BFONtGb5TYUAe2jazuRgKWMsvSJTxSnxrEPM9OYq68Wow@mail.gmail.com","threadId":"57133","inReplyTo":"20211228105614.qzmm3hglabtlcsx4@gmail.com","subject":"Re: [PATCH v2 3/8] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-28T19:37:01Z","receivedAt":"2021-12-28T19:37:18Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 28, 2021 at 2:56 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n>\n> On Sat, Dec 25, 2021 at 07:59:14AM +0000, Elijah Newren via GitGitGadget wrote:\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Since some callers may want to send warning messages to somewhere other\n> > than stdout/stderr, stop printing \"warning: Cannot merge binary files\"\n> > from ll-merge and instead modify the return status of ll_merge() to\n> > indicate when a merge of binary files has occurred.\n> >\n> > This commit continues printing the message as-is; future changes will\n> > start handling the new commit differently in the merge-ort codepath.\n>\n> \"the new commit\" looks like a typo, do you mean \"binary conflicts\"?\n\nGood catch, yeah should be \"the binary conflicts message\"\n\n> >\n> > Note that my methodology included first modifying ll_merge() to return\n> > a struct, so that the compiler would catch all the callers for me and\n> > ensure I had modified all of them.  After modifying all of them, I then\n> > changed the struct to an enum.\n>\n> Heh, this is a clever way to work around C's weak typing.\n>\n> The language server I'm using (clangd) supports the Call Hierarchy feature,\n> which is intended to list callers or callees of the function at the editor's\n> cursor. If I ask the server for callers of ll_merge I get this response\n> (on 510f9eba9 plus this series)\n>\n>         ll-merge.h:98:1: ll_merge - list of callers\n>           builtin/checkout.c:242:12: checkout_merged\n>             builtin/checkout.c:279:17:  merge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n>           rerere.c:943:12: handle_cache\n>             rerere.c:984:2:     ll_merge(&result, path, &mmfile[0], NULL,\n>           notes-merge.c:342:12: ll_merge_in_worktree\n>             notes-merge.c:353:11:       status = ll_merge(&result_buf, oid_to_hex(&p->obj), &base, NULL,\n>           merge-recursive.c:1035:12: merge_3way\n>             merge-recursive.c:1090:17:  merge_status = ll_merge(result_buf, a->path, &orig, base,\n>           merge-ort.c:1763:12: merge_3way\n>             merge-ort.c:1816:17:        merge_status = ll_merge(result_buf, path, &orig, base,\n>           merge-blobs.c:32:14: three_way_filemerge\n>             merge-blobs.c:48:17:        merge_status = ll_merge(&res, path, base, NULL,\n>           apply.c:3491:12: three_way_merge\n>             apply.c:3511:11:    status = ll_merge(&result, path,\n>           rerere.c:608:12: try_merge\n>             rerere.c:623:9:             ret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n>\n> So there are 8 callers in total; but only 7 print the warning (including the\n> one in merge-ort which will change in the next commit). I think you missed\n> the call at rerere.c:984 because we ignore its return value.\n\nDoh, I missed one!  Though, as pointed out by Junio, rerere won't\noperate on binary files and thus can't hit that codepath.  Still, I\nshould either have it in both rerere codepaths or neither.\n\n> > @@ -133,10 +133,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n> >       xmp.ancestor = orig_name;\n> >       xmp.file1 = name1;\n> >       xmp.file2 = name2;\n> > -     return xdl_merge(orig, src1, src2, &xmp, result);\n> > +     status = xdl_merge(orig, src1, src2, &xmp, result);\n> > +     ret = (status > 1 ) ? LL_MERGE_CONFLICT : status;\n>\n> \" (status > 1 )\" has an extra space\n>\n> I'm not sure it's wise to handle status=1 and status=2 in two different code paths.\n> Both mean the same (the only difference is the number of conflicts).\n> status=1 coincides with LL_MERGE_CONFLICT but that's purely coincidental\n>\n>         ret = (status > 0) ? LL_MERGE_CONFLICT : status;\n\nUm, whoops.  Yeah, this should be > 0, not > 1.  (As per\nxdl_do_merge() comment, status >= 0 means status returns the number of\nconflicts)  No clue how I messed that up so badly; kind of\nembarrassing, honestly.\n\nThanks for the careful reading.\n\n> > @@ -236,7 +239,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n> >               unlink_or_warn(temp[i]);\n> >       strbuf_release(&cmd);\n> >       strbuf_release(&path_sq);\n> > -     return status;\n> > +     ret = (status > 1) ? LL_MERGE_CONFLICT : status;\n>\n> same here, I'd test for \"status > 0\" because that's the convention for\n> external programs\n\nYep.\n\n...\n> > diff --git a/rerere.c b/rerere.c\n> > index d83d58df4fb..b1f8961ed9e 100644\n> > --- a/rerere.c\n> > +++ b/rerere.c\n> > @@ -609,19 +609,23 @@ static int try_merge(struct index_state *istate,\n> >                    const struct rerere_id *id, const char *path,\n> >                    mmfile_t *cur, mmbuffer_t *result)\n> >  {\n> > -     int ret;\n> > +     enum ll_merge_result ret;\n> >       mmfile_t base = {NULL, 0}, other = {NULL, 0};\n> >\n> >       if (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n> > -         read_mmfile(&other, rerere_path(id, \"postimage\")))\n> > -             ret = 1;\n> > -     else\n> > +         read_mmfile(&other, rerere_path(id, \"postimage\"))) {\n> > +             ret = LL_MERGE_CONFLICT;\n> > +     } else {\n> >               /*\n> >                * A three-way merge. Note that this honors user-customizable\n> >                * low-level merge driver settings.\n> >                */\n> >               ret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n> >                              istate, NULL);\n> > +             if (ret == LL_MERGE_BINARY_CONFLICT)\n> > +                     warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > +                             path, \"\", \"\");\n>\n> With the next patch, 7/8 callers of ll_merge (almost) immediately print\n> that warning.  Looks fine as is, but does it make sense to introduce a helper\n> function for the common case, or add a flag to ll_merge_options?\n\nI started by adding a flag, and Peff suggested not doing so (because\nthe printing doesn't belong in a \"low-level\" merge, as ll_merge stands\nfor[1]), but instead making the callers responsible.  We could add a\nhelper function, outside of ll-merge.[ch], but I'm not sure where to\nput it or what to call it and I'm leaning towards just leaving things\nas-is (well, other than fixing up the important issues you brought up\nbefore this).\n\n[1] https://lore.kernel.org/git/YVOZRhWttzF18Xql@coredump.intra.peff.net/\n"},{"id":"445136","messageId":"CABPp-BH5XUsmTo=BD7osUgi4o=eFWgaQkN1qYDky6uqb9SykHA@mail.gmail.com","threadId":"57133","inReplyTo":"20211228105733.lomkg23htd2kjtii@gmail.com","subject":"Re: [PATCH v2 7/8] diff: add ability to insert additional headers for paths","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-28T21:09:57Z","receivedAt":"2021-12-28T21:10:12Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 28, 2021 at 2:57 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n>\n> On Sat, Dec 25, 2021 at 07:59:18AM +0000, Elijah Newren via GitGitGadget wrote:\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > When additional headers are provided, we need to\n> >   * add diff_filepairs to diff_queued_diff for each paths in the\n> >     additional headers map which, unless that path is part of\n> >     another diff_filepair already found in diff_queued_diff\n> >   * format the headers (colorization, line_prefix for --graph)\n> >   * make sure the various codepaths that attempt to return early\n> >     if there are \"no changes\" take into account the headers that\n> >     need to be shown.\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  diff.c     | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++--\n> >  diff.h     |   3 +-\n> >  log-tree.c |   2 +-\n> >  3 files changed, 115 insertions(+), 6 deletions(-)\n> >\n> > diff --git a/diff.c b/diff.c\n> > index 861282db1c3..aaa6a19f158 100644\n> > --- a/diff.c\n> > +++ b/diff.c\n> > @@ -27,6 +27,7 @@\n> >  #include \"help.h\"\n> >  #include \"promisor-remote.h\"\n> >  #include \"dir.h\"\n> > +#include \"strmap.h\"\n> >\n> >  #ifdef NO_FAST_WORKING_DIRECTORY\n> >  #define FAST_WORKING_DIRECTORY 0\n> > @@ -3406,6 +3407,31 @@ struct userdiff_driver *get_textconv(struct repository *r,\n> >       return userdiff_get_textconv(r, one->driver);\n> >  }\n> >\n> > +static struct strbuf *additional_headers(struct diff_options *o,\n> > +                                      const char *path)\n> > +{\n> > +     if (!o->additional_path_headers)\n> > +             return NULL;\n> > +     return strmap_get(o->additional_path_headers, path);\n> > +}\n> > +\n> > +static void add_formatted_headers(struct strbuf *msg,\n> > +                               struct strbuf *more_headers,\n> > +                               const char *line_prefix,\n> > +                               const char *meta,\n> > +                               const char *reset)\n> > +{\n> > +     char *next, *newline;\n> > +\n> > +     for (next = more_headers->buf; *next; next = newline) {\n> > +             newline = strchrnul(next, '\\n');\n> > +             strbuf_addf(msg, \"%s%s%.*s%s\\n\", line_prefix, meta,\n> > +                         (int)(newline - next), next, reset);\n> > +             if (*newline)\n> > +                     newline++;\n> > +     }\n> > +}\n> > +\n> >  static void builtin_diff(const char *name_a,\n> >                        const char *name_b,\n> >                        struct diff_filespec *one,\n> > @@ -3464,6 +3490,17 @@ static void builtin_diff(const char *name_a,\n> >       b_two = quote_two(b_prefix, name_b + (*name_b == '/'));\n> >       lbl[0] = DIFF_FILE_VALID(one) ? a_one : \"/dev/null\";\n> >       lbl[1] = DIFF_FILE_VALID(two) ? b_two : \"/dev/null\";\n> > +     if (!DIFF_FILE_VALID(one) && !DIFF_FILE_VALID(two)) {\n> > +             /*\n> > +              * We should only reach this point for pairs from\n> > +              * create_filepairs_for_header_only_notifications().  For\n> > +              * these, we should avoid the \"/dev/null\" special casing\n> > +              * above, meaning we avoid showing such pairs as either\n> > +              * \"new file\" or \"deleted file\" below.\n> > +              */\n> > +             lbl[0] = a_one;\n> > +             lbl[1] = b_two;\n> > +     }\n>\n> not so familiar with this logic, but I saw that without this change, the\n> rename/rename conflict test fails. Is this because we add a file pair under\n> the original name (that's been renamed on both sides). I wonder if we\n> can sketch such a case in the comment.\n\nThat may be the only current test in the testsuite that fails without\nthis bit of logic, but I don't want the comment to be specific to the\nrename/rename case.  Whenever we have a conflict/warning/whatever\nmessage from the merge machinery tied to a path which doesn't show up\nin either the automatic merge or the recorded merge commit, we will\nhit this situation.  Even if I were to give a complete listing of all\nthe current cases, more could be added in the future.\n\n> > +static void create_filepairs_for_header_only_notifications(struct diff_options *o)\n> > +{\n> > +     struct strset present;\n> > +     struct diff_queue_struct *q = &diff_queued_diff;\n> > +     struct hashmap_iter iter;\n> > +     struct strmap_entry *e;\n> > +     int i;\n> > +\n> > +     strset_init_with_options(&present, /*pool*/ NULL, /*strdup*/ 0);\n> > +\n> > +     /*\n> > +      * Find out which paths exist in diff_queued_diff, preferring\n> > +      * one->path for any pair that has multiple paths.\n>\n> Why do we prefer one->path?\n\nrun_diff() sets name = one->path, passes it along to run_diff_cmd(),\nand from there it goes to fill_metainfo() and either\nrun_external_diff() or builtin_diff().\n\nI'm wondering if I should just ignore two->path entirely and only use\none->path; I think I partially looked at both because of various\nplaces in diff.c that already do but give preferential treatment to\none->path (diffnamecmp(), the calls to show_submodule*diff*(), what is\npassed to write_name_quoted() in diff_flush_raw()).\n\n> > +      */\n> > +     for (i = 0; i < q->nr; i++) {\n> > +             struct diff_filepair *p = q->queue[i];\n> > +             char *path = p->one->path ? p->one->path : p->two->path;\n> > +\n> > +             if (strmap_contains(o->additional_path_headers, path))\n> > +                     strset_add(&present, path);\n> > +     }\n> > +\n> > +     /*\n> > +      * Loop over paths in additional_path_headers; for each NOT already\n> > +      * in diff_queued_diff, create a synthetic filepair and insert that\n> > +      * into diff_queued_diff.\n> > +      */\n> > +     strmap_for_each_entry(o->additional_path_headers, &iter, e) {\n> > +             if (!strset_contains(&present, e->key)) {\n> > +                     struct diff_filespec *one, *two;\n> > +                     struct diff_filepair *p;\n> > +\n> > +                     one = alloc_filespec(e->key);\n> > +                     two = alloc_filespec(e->key);\n> > +                     fill_filespec(one, null_oid(), 0, 0);\n> > +                     fill_filespec(two, null_oid(), 0, 0);\n> > +                     p = diff_queue(q, one, two);\n> > +                     p->status = DIFF_STATUS_MODIFIED;\n> > +             }\n> > +     }\n>\n> All these string hash-maps are not really typical for a C program. I'm sure\n> they are the best choice for an advanced merge algorithm\n\nAgreed up to here.\n\n> but they are not\n> really necessary for computing/printing a diff.\n\nTechnically agree that it _could_ be solved a different way, but the\nstrmaps are a much more natural solution to this problem in this\nparticular case; more on this below.\n\n> It feels like this is an\n> implementation detail from merge-ort that's leaking into other components.\n\nAnd I disagree here, on _both_ the explicit point and the underlying\nsuggestion that you seem to be making that strmap should be avoided\noutside of merging.  The strmap.[ch] type was originally a suggestion\nfrom Peff for areas of git completely unrelated to merging (see the\nbeginning of https://lore.kernel.org/git/20200821194857.GD1165@coredump.intra.peff.net/,\nand the first link in that email).  It's a new datatype for git, much\nlike strbuf or string_list or whatever before it, that is there to be\nused when it's a natural fit for the problem at hand.  The lack of\nstrmap previously led folks to abuse other existing data structures\n(and in a way that often led to poor performance to boot).\n\n> What we want to do is\n>\n>         for file_pair in additional_headers:\n>                 if not already_queued(file_pair):\n>                         queue(file_pair)\n\nYes, precisely.\n\n> to do that, you use a temporary has-set (\"present\") that records everything\n> that's already queued (already_queued() is a lookup in that set).\n>\n> Let's assume both the queue and additional_headers are sorted arrays.\n\nThat's a bad assumption; we can't rely on *either* being sorted.  I\nactually started my implementation by trying exactly what you mention\nfirst; I too thought it'd be more natural and clearer to do this.  Of\ncourse, before implementing it, I had to verify whether\ndiff_queued_diff was sorted.  So, I added some code that would check\nthe order and fail if the queue wasn't sorted.  7 of the test files in\nthe regression testsuite had one or more failing tests.\n\nI think the queue was intended to be sorted (see\ndiffcore_fix_diff_index()), but in practice it's not.  And I'm worried\nthat if I find the current cases where it fails to be sorted and \"fix\"\nthem (though I don't actually know if this was intentional or not so I\ndon't know if that's really a fix or a break), that I'd end up with\nadditional cases in the future where they fail to be sorted anyway.\nSo, no matter what, relying on diff_queued_diff being sorted seems\nill-advised.\n\nAlso...\n\n> Then we could efficiently merge them (like a merge-sort algorithm)\n> without ever allocating a temporary hash map.\n>\n> I haven't checked if this is practical (better wait for feedback).\n> We'd probably need to convert the strmap additional_path_headers into an\n> array and sort it (I guess our hash map does not guarantee any ordering?)\n\nRight, strmap has no ordering either.  I was willing to stick those\ninto a string_list and sort them, but making temporary copies of both\nthe strmap and the diff_queued_diff just to sort them so that I can\nreasonably cheaply ask \"are items from this thing present in this\nother thing?\" seems to be stretching things a bit too far.\nmaps/hashes provide a very nice \"is this item present\" lookup and are\na natural way to ask that.  Since that is exactly the question I am\nasking, I think they are the better data structure here.  So, this was\nnot at all a leak of merge-ort datastructures, but rather a picking of\nthe appropriate data structures for the problem at hand.\n\n> > +\n> > +     /* Re-sort the filepairs */\n> > +     diffcore_fix_diff_index();\n> > +\n> > +     /* Cleanup */\n> > +     strset_clear(&present);\n>\n> Not a strong opinion, but I'd probably drop this comment\n>\n> > +}\n> > +\n> >  static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n> >  {\n> >       int i;\n> > @@ -6337,6 +6442,9 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n> >       if (o->color_moved)\n> >               o->emitted_symbols = &esm;\n> >\n> > +     if (o->additional_path_headers)\n> > +             create_filepairs_for_header_only_notifications(o);\n> > +\n> >       for (i = 0; i < q->nr; i++) {\n> >               struct diff_filepair *p = q->queue[i];\n> >               if (check_pair_status(p))\n> > @@ -6413,7 +6521,7 @@ void diff_flush(struct diff_options *options)\n> >        * Order: raw, stat, summary, patch\n> >        * or:    name/name-status/checkdiff (other bits clear)\n> >        */\n> > -     if (!q->nr)\n> > +     if (!q->nr && !options->additional_path_headers)\n> >               goto free_queue;\n> >\n> >       if (output_format & (DIFF_FORMAT_RAW |\n> > diff --git a/diff.h b/diff.h\n> > index 8ba85c5e605..06a0a67afda 100644\n> > --- a/diff.h\n> > +++ b/diff.h\n> > @@ -395,6 +395,7 @@ struct diff_options {\n> >\n> >       struct repository *repo;\n> >       struct option *parseopts;\n> > +     struct strmap *additional_path_headers;\n> >\n> >       int no_free;\n> >  };\n> > @@ -593,7 +594,7 @@ void diffcore_fix_diff_index(void);\n> >  \"                show all files diff when -S is used and hit is found.\\n\" \\\n> >  \"  -a  --text    treat all files as text.\\n\"\n> >\n> > -int diff_queue_is_empty(void);\n> > +int diff_queue_is_empty(struct diff_options*);\n> >  void diff_flush(struct diff_options*);\n> >  void diff_free(struct diff_options*);\n> >  void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);\n> > diff --git a/log-tree.c b/log-tree.c\n> > index d4655b63d75..33c28f537a6 100644\n> > --- a/log-tree.c\n> > +++ b/log-tree.c\n> > @@ -850,7 +850,7 @@ int log_tree_diff_flush(struct rev_info *opt)\n> >       opt->shown_dashes = 0;\n> >       diffcore_std(&opt->diffopt);\n> >\n> > -     if (diff_queue_is_empty()) {\n> > +     if (diff_queue_is_empty(&opt->diffopt)) {\n> >               int saved_fmt = opt->diffopt.output_format;\n> >               opt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT;\n> >               diff_flush(&opt->diffopt);\n> > --\n> > gitgitgadget\n> >\n"},{"id":"445144","messageId":"CABPp-BGbY-Kvsa_W7J0rcUBWuNOyVdkKbE-tHUOcNRnqwPnMWw@mail.gmail.com","threadId":"57133","inReplyTo":"20211228105642.ajmr2kqjm5yikneu@gmail.com","subject":"Re: [PATCH v2 6/8] merge-ort: format messages slightly different for use in headers","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-28T21:48:17Z","receivedAt":"2021-12-28T21:48:32Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 28, 2021 at 2:56 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n>\n> On Sat, Dec 25, 2021 at 07:59:17AM +0000, Elijah Newren via GitGitGadget wrote:\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > When users run\n> >     git show --remerge-diff $MERGE_COMMIT\n> > or\n> >     git log -p --remerge-diff ...\n> > stdout is not an appropriate location to dump conflict messages, but we\n> > do want to provide them to users.  We will include them in the diff\n> > headers instead...but for that to work, we need for any multiline\n> > messages to replace newlines with both a newline and a space.  Add a new\n> > flag to signal when we want these messages modified in such a fashion,\n> > and use it in path_msg() to modify these messages this way.\n>\n> makes sense (same for patches 4 & 5)\n>\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  merge-ort.c       | 36 ++++++++++++++++++++++++++++++++++--\n> >  merge-recursive.c |  3 +++\n> >  merge-recursive.h |  1 +\n> >  3 files changed, 38 insertions(+), 2 deletions(-)\n> >\n> > diff --git a/merge-ort.c b/merge-ort.c\n> > index 998e92ec593..9142d56e0ad 100644\n> > --- a/merge-ort.c\n> > +++ b/merge-ort.c\n> > @@ -634,17 +634,46 @@ static void path_msg(struct merge_options *opt,\n> >                    const char *fmt, ...)\n> >  {\n> >       va_list ap;\n> > -     struct strbuf *sb = strmap_get(&opt->priv->output, path);\n> > +     struct strbuf *sb, *dest;\n> > +     struct strbuf tmp = STRBUF_INIT;\n> > +\n> > +     if (opt->record_conflict_msgs_as_headers && omittable_hint)\n> > +             return; /* Do not record mere hints in tree */\n> > +     sb = strmap_get(&opt->priv->output, path);\n> >       if (!sb) {\n> >               sb = xmalloc(sizeof(*sb));\n> >               strbuf_init(sb, 0);\n> >               strmap_put(&opt->priv->output, path, sb);\n> >       }\n> >\n> > +     dest = (opt->record_conflict_msgs_as_headers ? &tmp : sb);\n> > +\n> >       va_start(ap, fmt);\n> > -     strbuf_vaddf(sb, fmt, ap);\n> > +     strbuf_vaddf(dest, fmt, ap);\n> >       va_end(ap);\n> >\n> > +     if (opt->record_conflict_msgs_as_headers) {\n> > +             int i_sb = 0, i_tmp = 0;\n> > +\n> > +             /* Copy tmp to sb, adding spaces after newlines */\n> > +             strbuf_grow(sb, 2*tmp.len); /* more than sufficient */\n> > +             for (; i_tmp < tmp.len; i_tmp++, i_sb++) {\n> > +                     /* Copy next character from tmp to sb */\n> > +                     sb->buf[sb->len + i_sb] = tmp.buf[i_tmp];\n> > +\n> > +                     /* If we copied a newline, add a space */\n> > +                     if (tmp.buf[i_tmp] == '\\n')\n> > +                             sb->buf[++i_sb] = ' ';\n> > +             }\n> > +             /* Update length and ensure it's NUL-terminated */\n>\n> I think this and the two comments inside the loop are mostly redundant. I'd\n> drop them (except maybe this one because it's a common oversight I guess).\n\nI don't think redundancy is (necessarily) a reason to drop comments.\nTake for example the following from early in abspath.c:\n\n    /* Find start of the last component */\n    while (offset < len && !is_dir_sep(path->buf[len - 1]))\n        len--;\n    /* Skip sequences of multiple path-separators */\n    while (offset < len && is_dir_sep(path->buf[len - 1]))\n        len--;\n\nThe comment quickly explains what might take a bit more time to reason\nout.  Since I'm dealing with multiple different indices and various\narithmetic, I figured a quick explanation was helpful.  And, of\ncourse, the reminder to make it NUL-terminated.  Granted, if the code\nis very readily obvious then comments are not helpful, and there's a\ngray area somewhere in between.  I think the code we're discussing\nhere is in that gray area, where it's a matter of taste what the\nthreshold is.  I don't find your taste here unreasonable, but I don't\nfind mine for the above examples to be unreasonable either.  I'd\nrather leave these in.\n\n>\n> > +             sb->len += i_sb;\n> > +             sb->buf[sb->len] = '\\0';\n> > +\n> > +             /* Clean up tmp */\n>\n> Also this one I guess\n\nYeah, I'll nuke this one.  The reason for this comment was more that\nsometimes I like having a comment apply to the code below it until the\nnext comment; do thing that way avoids the need to put a\nhere-ends-the-previous-comment comment, or arbitrarily avoid blank\nlines after a comment.  But that reasoning is a bit weaker here, and\nit clearly doesn't need any explanation, so I'll just drop it.\n\n> > +             strbuf_release(&tmp);\n> > +     }\n> > +\n> > +     /* Add final newline character to sb */\n> >       strbuf_addch(sb, '\\n');\n> >  }\n> >\n> > @@ -4246,6 +4275,9 @@ void merge_switch_to_result(struct merge_options *opt,\n> >               struct string_list olist = STRING_LIST_INIT_NODUP;\n> >               int i;\n> >\n> > +             if (opt->record_conflict_msgs_as_headers)\n> > +                     BUG(\"Either display conflict messages or record them as headers, not both\");\n> > +\n> >               trace2_region_enter(\"merge\", \"display messages\", opt->repo);\n> >\n> >               /* Hack to pre-allocate olist to the desired size */\n> > diff --git a/merge-recursive.c b/merge-recursive.c\n> > index bc73c52dd84..c9ba7e904a6 100644\n> > --- a/merge-recursive.c\n> > +++ b/merge-recursive.c\n> > @@ -3714,6 +3714,9 @@ static int merge_start(struct merge_options *opt, struct tree *head)\n> >\n> >       assert(opt->priv == NULL);\n> >\n> > +     /* Not supported; option specific to merge-ort */\n> > +     assert(!opt->record_conflict_msgs_as_headers);\n> > +\n> >       /* Sanity check on repo state; index must match head */\n> >       if (repo_index_has_changes(opt->repo, head, &sb)) {\n> >               err(opt, _(\"Your local changes to the following files would be overwritten by merge:\\n  %s\"),\n> > diff --git a/merge-recursive.h b/merge-recursive.h\n> > index 0795a1d3ec1..ebfdb7f994e 100644\n> > --- a/merge-recursive.h\n> > +++ b/merge-recursive.h\n> > @@ -46,6 +46,7 @@ struct merge_options {\n> >       /* miscellaneous control options */\n> >       const char *subtree_shift;\n> >       unsigned renormalize : 1;\n> > +     unsigned record_conflict_msgs_as_headers : 1;\n> >\n> >       /* internal fields used by the implementation */\n> >       struct merge_options_internal *priv;\n> > --\n> > gitgitgadget\n> >\n"},{"id":"445147","messageId":"20211228220543.wwoaitcm4luihgvu@gmail.com","threadId":"57133","inReplyTo":"CABPp-BFONtGb5TYUAe2jazuRgKWMsvSJTxSnxrEPM9OYq68Wow@mail.gmail.com","subject":"Re: [PATCH v2 3/8] ll-merge: make callers responsible for showing warnings","fromName":"Johannes Altmanninger","fromEmail":"aclopte@gmail.com","sentAt":"2021-12-28T22:05:43Z","receivedAt":"2021-12-28T22:05:49Z","isPatch":true,"sender":{"key":"aclopte@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6853872?v=4"},"body":"On Tue, Dec 28, 2021 at 11:37:01AM -0800, Elijah Newren wrote:\n> On Tue, Dec 28, 2021 at 2:56 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n> >\n> > On Sat, Dec 25, 2021 at 07:59:14AM +0000, Elijah Newren via GitGitGadget wrote:\n> >\n> > So there are 8 callers in total; but only 7 print the warning (including the\n> > one in merge-ort which will change in the next commit). I think you missed\n> > the call at rerere.c:984 because we ignore its return value.\n> \n> Doh, I missed one!  Though, as pointed out by Junio, rerere won't\n> operate on binary files and thus can't hit that codepath.  Still, I\n> should either have it in both rerere codepaths or neither.\n\n\"neither\" sounds good\n\n> > > +             if (ret == LL_MERGE_BINARY_CONFLICT)\n> > > +                     warning(\"Cannot merge binary files: %s (%s vs. %s)\",\n> > > +                             path, \"\", \"\");\n> >\n> > With the next patch, 7/8 callers of ll_merge (almost) immediately print\n> > that warning.  Looks fine as is, but does it make sense to introduce a helper\n> > function for the common case, or add a flag to ll_merge_options?\n> \n> I started by adding a flag, and Peff suggested not doing so (because\n> the printing doesn't belong in a \"low-level\" merge, as ll_merge stands\n> for[1]), but instead making the callers responsible.  We could add a\n> helper function, outside of ll-merge.[ch], but I'm not sure where to\n> put it or what to call it and I'm leaning towards just leaving things\n> as-is (well, other than fixing up the important issues you brought up\n> before this).\n\nSure, leaving this sounds fine.  If we can formulate good reasons against\nthe discarded approaches we should add them to the commit message.  I guess\nin this case the small number of call sites is a good indication that it's\nprobably not worth it.\n"},{"id":"445148","messageId":"CABPp-BEJ+RiripJSonrOcJfRWKZDeoL83PDqE6beWwFqs1HxTQ@mail.gmail.com","threadId":"57133","inReplyTo":"20211228105600.h32jl5iwot3munww@gmail.com","subject":"Re: [PATCH v2 1/8] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-28T22:34:03Z","receivedAt":"2021-12-28T22:34:18Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"CC'ing brian in case he has comments on the sha256 stuff and whether\nhe thinks there's a cleaner way to make my tests work with sha256.\n(brian: See the very end of the email.)\n\nOn Tue, Dec 28, 2021 at 2:56 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n>\n> On Sat, Dec 25, 2021 at 07:59:12AM +0000, Elijah Newren via GitGitGadget wrote:\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > When this option is specified, we remerge all (two parent) merge commits\n> > and diff the actual merge commit to the automatically created version,\n> > in order to show how users removed conflict markers, resolved the\n> > different conflict versions, and potentially added new changes outside\n> > of conflict regions in order to resolve semantic merge problems (or,\n> > possibly, just to hide other random changes).\n> >\n> > This capability works by creating a temporary object directory and\n> > marking it as the primary object store.  This makes it so that any blobs\n> > or trees created during the automatic merge easily removable afterwards\n>\n> s/easily/are easily/ ?\n\nsure\n\n> > by just deleting all objects from the temporary object directory.\n> >\n> > There are a few ways that this implementation is suboptimal:\n> >   * `log --remerge-diff` becomes slow, because the temporary object\n> >     directory can fills with many loose objects while running\n>\n> s/can fills/can fill/\n\nThanks.\n\n>\n> >   * the log output can be muddied with misplaced \"warning: cannot merge\n> >     binary files\" messages, since ll-merge.c unconditionally writes those\n> >     messages to stderr while running instead of allowing callers to\n> >     manage them.\n> >   * important conflict and warning messages are simply dropped; thus for\n> >     conflicts like modify/delete or rename/rename or file/directory which\n> >     are not representable with content conflict markers, there may be no\n> >     way for a user of --remerge-diff to know that there had been a\n> >     conflict which was resolved (and which possibly motivated other\n> >     changes in the merge commit).\n> > Subsequent commits will address these issues.\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  Documentation/diff-options.txt |  8 ++++\n> >  builtin/log.c                  | 14 ++++++\n> >  diff-merges.c                  | 12 +++++\n> >  log-tree.c                     | 59 +++++++++++++++++++++++\n> >  revision.h                     |  3 +-\n> >  t/t4069-remerge-diff.sh        | 86 ++++++++++++++++++++++++++++++++++\n> >  6 files changed, 181 insertions(+), 1 deletion(-)\n> >  create mode 100755 t/t4069-remerge-diff.sh\n> >\n> > diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\n> > index c89d530d3d1..b05f1c9f1c9 100644\n> > --- a/Documentation/diff-options.txt\n> > +++ b/Documentation/diff-options.txt\n> > @@ -64,6 +64,14 @@ ifdef::git-log[]\n> >       each of the parents. Separate log entry and diff is generated\n> >       for each parent.\n> >  +\n> > +--diff-merges=remerge:::\n> > +--diff-merges=r:::\n> > +--remerge-diff:::\n>\n> The synopsis above needs an update, too:\n>\n>         diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\n>         index c89d530d3d..7a98ab3f85 100644\n>         --- a/Documentation/diff-options.txt\n>         +++ b/Documentation/diff-options.txt\n>         @@ -36,3 +36,3 @@ endif::git-format-patch[]\n>          ifdef::git-log[]\n>         ---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc)::\n>         +--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r)::\n>          --no-diff-merges::\n\nAh, good catch.\n\n> > +     With this option, two-parent merge commits are remerged to\n> > +     create a temporary tree object -- potentially containing files\n> > +     with conflict markers and such.  A diff is then shown between\n> > +     that temporary tree and the actual merge commit.\n>\n> I had not really looked at any of the --diff-merges options before.  The term\n> \"remerge\" felt a bit opaque at first, because I didn't know what the diff\n> would look like. I might have found this easier:\n>\n> --diff-merges=resolution:::\n> --diff-merges=r:::\n> --resolution-diff:::\n>         This makes two-parent merge commits show the diff with respect to\n>         a mechanical merge of their parents -- potentially containing files\n>         with conflict markers and such.\n>\n> But on a second thought, remerge is actually consistent with the rest,\n> because it states _what_ we compare to the merge commit, so nevermind.\n>\n> > ++\n> >  --diff-merges=combined:::\n> >  --diff-merges=c:::\n> >  -c:::\n> > diff --git a/builtin/log.c b/builtin/log.c\n> > index f75d87e8d7f..d053418fddd 100644\n> > --- a/builtin/log.c\n> > +++ b/builtin/log.c\n> > @@ -35,6 +35,7 @@\n> >  #include \"repository.h\"\n> >  #include \"commit-reach.h\"\n> >  #include \"range-diff.h\"\n> > +#include \"tmp-objdir.h\"\n> >\n> >  #define MAIL_DEFAULT_WRAP 72\n> >  #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n> > @@ -406,6 +407,14 @@ static int cmd_log_walk(struct rev_info *rev)\n> >       struct commit *commit;\n> >       int saved_nrl = 0;\n> >       int saved_dcctc = 0;\n> > +     struct tmp_objdir *remerge_objdir = NULL;\n> > +\n> > +     if (rev->remerge_diff) {\n> > +             remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n> > +             if (!remerge_objdir)\n> > +                     die_errno(_(\"unable to create temporary object directory\"));\n> > +             tmp_objdir_replace_primary_odb(remerge_objdir, 1);\n> > +     }\n> >\n> >       if (rev->early_output)\n> >               setup_early_output();\n> > @@ -449,6 +458,9 @@ static int cmd_log_walk(struct rev_info *rev)\n> >       rev->diffopt.no_free = 0;\n> >       diff_free(&rev->diffopt);\n> >\n> > +     if (rev->remerge_diff)\n> > +             tmp_objdir_destroy(remerge_objdir);\n> > +\n> >       if (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n> >           rev->diffopt.flags.check_failed) {\n> >               return 02;\n> > @@ -1943,6 +1955,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n> >               die(_(\"--name-status does not make sense\"));\n> >       if (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF)\n> >               die(_(\"--check does not make sense\"));\n> > +     if (rev.remerge_diff)\n> > +             die(_(\"--remerge-diff does not make sense\"));\n> >\n> >       if (!use_patch_format &&\n> >               (!rev.diffopt.output_format ||\n> > diff --git a/diff-merges.c b/diff-merges.c\n> > index 5060ccd890b..0af4b3f9191 100644\n> > --- a/diff-merges.c\n> > +++ b/diff-merges.c\n> > @@ -17,6 +17,7 @@ static void suppress(struct rev_info *revs)\n> >       revs->combined_all_paths = 0;\n> >       revs->merges_imply_patch = 0;\n> >       revs->merges_need_diff = 0;\n> > +     revs->remerge_diff = 0;\n> >  }\n> >\n> >  static void set_separate(struct rev_info *revs)\n> > @@ -45,6 +46,12 @@ static void set_dense_combined(struct rev_info *revs)\n> >       revs->dense_combined_merges = 1;\n> >  }\n> >\n> > +static void set_remerge_diff(struct rev_info *revs)\n> > +{\n> > +     suppress(revs);\n> > +     revs->remerge_diff = 1;\n> > +}\n> > +\n> >  static diff_merges_setup_func_t func_by_opt(const char *optarg)\n> >  {\n> >       if (!strcmp(optarg, \"off\") || !strcmp(optarg, \"none\"))\n> > @@ -57,6 +64,8 @@ static diff_merges_setup_func_t func_by_opt(const char *optarg)\n> >               return set_combined;\n> >       else if (!strcmp(optarg, \"cc\") || !strcmp(optarg, \"dense-combined\"))\n> >               return set_dense_combined;\n> > +     else if (!strcmp(optarg, \"r\") || !strcmp(optarg, \"remerge\"))\n> > +             return set_remerge_diff;\n> >       else if (!strcmp(optarg, \"m\") || !strcmp(optarg, \"on\"))\n> >               return set_to_default;\n> >       return NULL;\n> > @@ -110,6 +119,9 @@ int diff_merges_parse_opts(struct rev_info *revs, const char **argv)\n> >       } else if (!strcmp(arg, \"--cc\")) {\n> >               set_dense_combined(revs);\n> >               revs->merges_imply_patch = 1;\n> > +     } else if (!strcmp(arg, \"--remerge-diff\")) {\n> > +             set_remerge_diff(revs);\n> > +             revs->merges_imply_patch = 1;\n> >       } else if (!strcmp(arg, \"--no-diff-merges\")) {\n> >               suppress(revs);\n> >       } else if (!strcmp(arg, \"--combined-all-paths\")) {\n> > diff --git a/log-tree.c b/log-tree.c\n> > index 644893fd8cf..84ed864fc81 100644\n> > --- a/log-tree.c\n> > +++ b/log-tree.c\n> > @@ -1,4 +1,5 @@\n> >  #include \"cache.h\"\n> > +#include \"commit-reach.h\"\n> >  #include \"config.h\"\n> >  #include \"diff.h\"\n> >  #include \"object-store.h\"\n> > @@ -7,6 +8,7 @@\n> >  #include \"tag.h\"\n> >  #include \"graph.h\"\n> >  #include \"log-tree.h\"\n> > +#include \"merge-ort.h\"\n> >  #include \"reflog-walk.h\"\n> >  #include \"refs.h\"\n> >  #include \"string-list.h\"\n> > @@ -902,6 +904,51 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n> >       return !opt->loginfo;\n> >  }\n> >\n> > +static int do_remerge_diff(struct rev_info *opt,\n> > +                        struct commit_list *parents,\n> > +                        struct object_id *oid,\n> > +                        struct commit *commit)\n> > +{\n> > +     struct merge_options o;\n> > +     struct commit_list *bases;\n> > +     struct merge_result res = {0};\n> > +     struct pretty_print_context ctx = {0};\n> > +     struct commit *parent1 = parents->item;\n> > +     struct commit *parent2 = parents->next->item;\n> > +     struct strbuf parent1_desc = STRBUF_INIT;\n> > +     struct strbuf parent2_desc = STRBUF_INIT;\n> > +\n> > +     /* Setup merge options */\n> > +     init_merge_options(&o, the_repository);\n> > +     o.show_rename_progress = 0;\n>\n> Is there a reason why we are repeating the default here (but not anywhere else)?\n> For example sequencer.c::do_merge() and builtin/am.c::fall_back_threeway()\n> don't, and probably also rely on this being disabled(?).\n\nNo, I think each of rebase, am, and merge could sensibly have progress\noutput be shown -- whether or not they do currently.  Whether or not\nshowing progress is the default now or in the future, though, we don't\nwant it for remerge diff.  So, yes, I explicitly made sure to turn it\noff.\n\n> > +\n> > +     ctx.abbrev = DEFAULT_ABBREV;\n> > +     format_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n> > +     format_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n> > +     o.branch1 = parent1_desc.buf;\n> > +     o.branch2 = parent2_desc.buf;\n> > +\n> > +     /* Parse the relevant commits and get the merge bases */\n> > +     parse_commit_or_die(parent1);\n> > +     parse_commit_or_die(parent2);\n> > +     bases = get_merge_bases(parent1, parent2);\n> > +\n> > +     /* Re-merge the parents */\n> > +     merge_incore_recursive(&o, bases, parent1, parent2, &res);\n> > +\n> > +     /* Show the diff */\n> > +     diff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n> > +     log_tree_diff_flush(opt);\n> > +\n> > +     /* Cleanup */\n> > +     strbuf_release(&parent1_desc);\n> > +     strbuf_release(&parent2_desc);\n> > +     merge_finalize(&o, &res);\n> > +     /* TODO: clean up the temporary object directory */\n> > +\n> > +     return !opt->loginfo;\n> > +}\n> > +\n> >  /*\n> >   * Show the diff of a commit.\n> >   *\n> > @@ -936,6 +983,18 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log\n> >       }\n> >\n> >       if (is_merge) {\n> > +             int octopus = (parents->next->next != NULL);\n> > +\n> > +             if (opt->remerge_diff) {\n> > +                     if (octopus) {\n> > +                             show_log(opt);\n> > +                             fprintf(opt->diffopt.file,\n> > +                                     \"diff: warning: Skipping remerge-diff \"\n> > +                                     \"for octopus merges.\\n\");\n> > +                             return 1;\n> > +                     }\n> > +                     return do_remerge_diff(opt, parents, oid, commit);\n> > +             }\n> >               if (opt->combine_merges)\n> >                       return do_diff_combined(opt, commit);\n> >               if (opt->separate_merges) {\n> > diff --git a/revision.h b/revision.h\n> > index 5578bb4720a..13178e6b8f3 100644\n> > --- a/revision.h\n> > +++ b/revision.h\n> > @@ -195,7 +195,8 @@ struct rev_info {\n> >                       combine_merges:1,\n> >                       combined_all_paths:1,\n> >                       dense_combined_merges:1,\n> > -                     first_parent_merges:1;\n> > +                     first_parent_merges:1,\n> > +                     remerge_diff:1;\n> >\n> >       /* Format info */\n> >       int             show_notes;\n> > diff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\n> > new file mode 100755\n> > index 00000000000..192dbce2bfe\n> > --- /dev/null\n> > +++ b/t/t4069-remerge-diff.sh\n> > @@ -0,0 +1,86 @@\n> > +#!/bin/sh\n> > +\n> > +test_description='remerge-diff handling'\n> > +\n> > +. ./test-lib.sh\n> > +\n> > +test_expect_success 'setup basic merges' '\n> > +     test_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n> > +     git add numbers &&\n> > +     git commit -m base &&\n> > +\n> > +     git branch feature_a &&\n> > +     git branch feature_b &&\n> > +     git branch feature_c &&\n> > +\n> > +     git branch ab_resolution &&\n> > +     git branch bc_resolution &&\n> > +\n> > +     git checkout feature_a &&\n> > +     test_write_lines 1 2 three 4 5 6 7 eight 9 >numbers &&\n> > +     git commit -a -m change_a &&\n> > +\n> > +     git checkout feature_b &&\n> > +     test_write_lines 1 2 tres 4 5 6 7 8 9 >numbers &&\n> > +     git commit -a -m change_b &&\n> > +\n> > +     git checkout feature_c &&\n> > +     test_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&\n> > +     git commit -a -m change_c &&\n> > +\n> > +     git checkout bc_resolution &&\n> > +     # fast forward\n> > +     git merge feature_b &&\n>\n> maybe use --ff-only instead of the comment? Same below.\n\nThat'd be fine.\n\n> (But if we did that we probably want to drop the \"no conflict\" comment too.)\n\nNah, I'd rather keep it.\n\n> > +     # no conflict\n> > +     git merge feature_c &&\n> > +\n> > +     git checkout ab_resolution &&\n> > +     # fast forward\n> > +     git merge feature_a &&\n> > +     # conflicts!\n> > +     test_must_fail git merge feature_b &&\n> > +     # Resolve conflict...and make another change elsewhere\n> > +     test_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers &&\n> > +     git add numbers &&\n> > +     git merge --continue\n> > +'\n> > +\n> > +test_expect_success 'remerge-diff on a clean merge' '\n> > +     git log -1 --oneline bc_resolution >expect &&\n> > +     git show --oneline --remerge-diff bc_resolution >actual &&\n> > +     test_cmp expect actual\n> > +'\n> > +\n> > +test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' '\n> > +     git log -1 --oneline ab_resolution >tmp &&\n> > +     cat <<-EOF >>tmp &&\n> > +     diff --git a/numbers b/numbers\n> > +     index a1fb731..6875544 100644\n> > +     --- a/numbers\n> > +     +++ b/numbers\n> > +     @@ -1,13 +1,9 @@\n> > +      1\n> > +      2\n> > +     -<<<<<<< b0ed5cb (change_a)\n> > +     -three\n> > +     -=======\n> > +     -tres\n> > +     ->>>>>>> 6cd3f82 (change_b)\n> > +     +drei\n>\n> nice\n>\n> > +      4\n> > +      5\n> > +      6\n> > +      7\n> > +     -eight\n> > +     +acht\n> > +      9\n> > +     EOF\n> > +     # Hashes above are sha1; rip them out so test works with sha256\n> > +     sed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n>\n> Right, sha256 could cause many noisy test changes. I wonder if there is a\n> more general way to avoid this; maybe default to SHA1 for existing tests?\n\nNot \"could\", but \"does\".  And this is not something to be avoided.\nThe default testsuite we run in CI involves a run of\nGIT_TEST_DEFAULT_HASH=sha256 under linux-clang.  Making these tests\nSHA1-only just reduces our coverage and makes the transition to SHA256\nharder; I think that's the opposite of the direction we want to go.\n\nThese changes I've made here are sufficient to make these tests work\nunder sha256; you can see the test results here:\nhttps://github.com/gitgitgadget/git/runs/4646949283?check_suite_focus=true.\nUnder \"Run ci/run-build-and-tests.sh\" note that there are two runs of\ntests, and the second has \"export GIT_TEST_DEFAULT_HASH=sha256\"\npreceding it.\n\nThere might be a cleaner way to make these tests sha256-compatible,\nbut this seemed like a pretty simple way to me.\n"},{"id":"445149","messageId":"YcuXQQHsl8iVdK8R@camp.crustytoothpaste.net","threadId":"57133","inReplyTo":"CABPp-BEJ+RiripJSonrOcJfRWKZDeoL83PDqE6beWwFqs1HxTQ@mail.gmail.com","subject":"Re: [PATCH v2 1/8] show, log: provide a --remerge-diff capability","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2021-12-28T23:01:21Z","receivedAt":"2021-12-28T23:01:27Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2021-12-28 at 22:34:03, Elijah Newren wrote:\n> CC'ing brian in case he has comments on the sha256 stuff and whether\n> he thinks there's a cleaner way to make my tests work with sha256.\n> (brian: See the very end of the email.)\n> \n> On Tue, Dec 28, 2021 at 2:56 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n> >\n> > On Sat, Dec 25, 2021 at 07:59:12AM +0000, Elijah Newren via GitGitGadget wrote:\n> > > +test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' '\n> > > +     git log -1 --oneline ab_resolution >tmp &&\n> > > +     cat <<-EOF >>tmp &&\n> > > +     diff --git a/numbers b/numbers\n> > > +     index a1fb731..6875544 100644\n> > > +     --- a/numbers\n> > > +     +++ b/numbers\n> > > +     @@ -1,13 +1,9 @@\n> > > +      1\n> > > +      2\n> > > +     -<<<<<<< b0ed5cb (change_a)\n> > > +     -three\n> > > +     -=======\n> > > +     -tres\n> > > +     ->>>>>>> 6cd3f82 (change_b)\n> > > +     +drei\n> >\n> > nice\n> >\n> > > +      4\n> > > +      5\n> > > +      6\n> > > +      7\n> > > +     -eight\n> > > +     +acht\n> > > +      9\n> > > +     EOF\n> > > +     # Hashes above are sha1; rip them out so test works with sha256\n> > > +     sed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n> >\n> > Right, sha256 could cause many noisy test changes. I wonder if there is a\n> > more general way to avoid this; maybe default to SHA1 for existing tests?\n> \n> Not \"could\", but \"does\".  And this is not something to be avoided.\n> The default testsuite we run in CI involves a run of\n> GIT_TEST_DEFAULT_HASH=sha256 under linux-clang.  Making these tests\n> SHA1-only just reduces our coverage and makes the transition to SHA256\n> harder; I think that's the opposite of the direction we want to go.\n>\n> These changes I've made here are sufficient to make these tests work\n> under sha256; you can see the test results here:\n> https://github.com/gitgitgadget/git/runs/4646949283?check_suite_focus=true.\n> Under \"Run ci/run-build-and-tests.sh\" note that there are two runs of\n> tests, and the second has \"export GIT_TEST_DEFAULT_HASH=sha256\"\n> preceding it.\n> \n> There might be a cleaner way to make these tests sha256-compatible,\n> but this seemed like a pretty simple way to me.\n\nThe question here is, do we care very much about testing these specific\nhashes?  If so, then we should use test_oid_cache to set up some OIDs\nand make sure they're correct for both SHA-1 and SHA-256, and then\nreplace them in the code with calls to test_oid.\n\nHowever, my impression is that we probably don't care very much about\nwhat the specific values are, and in that case, this is completely fine.\nWe do similar things elsewhere in the testsuite.\n-- \nbrian m. carlson (he/him or they/them)\nToronto, Ontario, CA\n"},{"id":"445152","messageId":"CABPp-BFP5Oc3vDQVJYZ4L=4ivSYuZBNk+Y9RHioJuSZKVzW0Dg@mail.gmail.com","threadId":"57133","inReplyTo":"20211228105755.zgahkoxn6ewjpdol@gmail.com","subject":"Re: [PATCH v2 8/8] show, log: include conflict/warning messages in --remerge-diff headers","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-28T23:42:13Z","receivedAt":"2021-12-28T23:42:27Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 28, 2021 at 2:57 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n>\n> On Sat, Dec 25, 2021 at 07:59:19AM +0000, Elijah Newren via GitGitGadget wrote:\n...\n> > +test_expect_success 'remerge-diff with non-content conflicts' '\n> > +     git log -1 --oneline resolution >tmp &&\n> > +     cat <<-EOF >>tmp &&\n> > +     diff --git a/file_or_directory~HASH (side1) b/wanted_content\n>\n> the \"~HASH (side1)\" suffix will probably mess with some programs that extract\n> the filename from the diff.\n\n\"~HASH (side1)\" is part of the filename, so this won't mess those\nprograms up at all (unless those programs can't deal with filenames\ncontaining spaces or parentheses or something).\n\n> I don't know what programs are supposed to expect.  I can see arguments for\n> either dropping the suffix or including only \"~HASH\" since that's part of\n> the actual filename that's left in the worktree.\n\nWhen there are conflicts that prevent the file from being recorded in\nthe tree, such as file/directory conflicts, we have to rename the file\nelsewhere.  We want the new name to be something that the user can\nfind and reason about.  So, both merge-recursive and merge-ort use\n${filename}~${branchname}, where ${branchname} is defined in the\nstruct merge_options (opt->branch1 or opt->branch2) that were passed\nin to the function.\n\nFor a regular `git merge` we just use opt->branch1 = \"HEAD\" and\nopt->branch2 = <name of branch/commit typed on command line to merge>.\n\nNeither of those strings make sense for remerge-diff.  We could just\nuse hashes, but why are users going to be familiar with the hashes of\nthe parents of a merge commit when looking at --remerge-diff output?\nParents are not part of the output by default.  So, log-tree.c uses\nthis bit of logic\n\n    format_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n    format_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n    o.branch1 = parent1_desc.buf;\n    o.branch2 = parent2_desc.buf;\n\nwhich means that the branch name is of the form \"$HASH\n($EXTRA_DESCRIPTION)\", and thus that files in file/directory conflicts\n(or add/add + file/symlink conflicts or file/submodule conflicts) will\nget renamed to a file of the form \"$FILENAME~$HASH\n($EXTRA_DESCRIPTION)\"\n\nNote that these branch names also appear in CONFLICT messages, in\nconflict markers, etc., and in fact are used much more frequently in\nthose locations.  In those places it's perhaps even more important to\nattempt to provide meaningful names, so dropping the extra description\ndoesn't make sense.\n\n> The file/link typechange conflict test I'll add below exposes what looks\n> like an accidental interaction with the trailing tab characters that we emit\n> on --- and +++ lines if the \"filename\" contains a space (since 1a9eb3b9d5\n> (git-diff/git-apply: make diff output a bit friendlier to GNU patch (part\n> 2), 2006-09-22)).\n>\n>         index 70885e4..0000000\n>         --- a/typechange~738109f (side1)        <-- git diff adds a trailing tab!\n>         +++ /dev/null\n>\n> I haven't formed an opinion yet, but since Tig uses the --- and +++ lines\n> to extract file names, I'd drop the \" (side1)\" suffix from at least the ---\n> and +++ lines. Maybe also the ^diff lines, I'm not sure\n\nAs above, \" (side1)\" is part of the filename and thus belongs here.\n\n> > +     similarity index 100%\n> > +     rename from file_or_directory~HASH (side1)\n> > +     rename to wanted_content\n> > +     CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n>\n> I wonder if it's better to have this line further up, before the \"rename\"\n> resolution, to correct the temporal order.\n\nYeah, I've gone back and forth about where these would best be placed.\nYou make a good point, even if the code is slightly uglier to move\nearlier.  However, I do really like having the CONFLICT notices being\nclose to the file text being shown, which makes me conflicted (no pun\nintended) about moving it earlier.  Hmm....\n\n> > +     diff --git a/letters b/letters\n> > +     CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n> > +     diff --git a/letters_side2 b/letters_side2\n> > +     deleted file mode 100644\n> > +     index b236ae5..0000000\n> > +     --- a/letters_side2\n> > +     +++ /dev/null\n> > +     @@ -1,9 +0,0 @@\n> > +     -a\n> > +     -b\n> > +     -c\n> > +     -d\n> > +     -e\n> > +     -f\n> > +     -g\n> > +     -h\n> > +     -i\n> > +     diff --git a/numbers b/numbers\n> > +     CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n> > +     EOF\n>\n> Took me some time to grok these but the output makes sense (it's loud and\n> ugly but that's okay since these are serious conflicts).\n>\n> > +     # We still have some sha1 hashes above; rip them out so test works\n> > +     # with sha256\n> > +     sed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n> > +\n> > +     git show --oneline --remerge-diff resolution >tmp &&\n> > +     sed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n> > +     test_cmp expect actual\n> > +'\n> > +\n> >  test_done\n> > --\n> > gitgitgadget\n>\n> We're missing a test case for typechange.\n\n<grin>\n...and a testcase for an add/add conflict, and a file/submodule\nconflict, and file/symlink, and symlink/submodule, and\nsubmodule/submodule (different submodules both added at same path),\nand binary merge conflict, and a variety of\nfailure-to-merge-submodule-updates, and at least half a dozen regular\nrename-based conflict types, and several directory-rename-based\nconflict types...and that's just beginning to scratch the surface once\nyou start dreaming of combinations of the different conflict types\noccurring for the same path (in particular, I'm thinking of examples\nfrom the testcases found in t6416, t6422, t6423 -- or at least\nsections 7, 9, & 12 of t6423).\n\nI don't think providing a comprehensive set of possible conflicts is\nuseful here; we just need a representative sample.  I was curious\nwhether that was best served with just two examples or three, but\nultimately decided on 3.  I would have been more likely to pick 2 than\n4, though.\n\nHowever, while I fail to see how typechange stresses --remerge-diff in\nways the other conflict types don't, or how it might help clarify the\noutput for users, I might be overlooking something.  Is there a\nparticular reason you wanted to see the typechange conflict included?\n\n>  Here's is a quick draft I've been\n> playing around with. Seems ugly that the \"diff --git a/typechange b/typechange\"\n> is doubled but okay.\n>\n> Maybe a rename/delete conflict is interesting as well, I'm not sure.  (Also I\n> wonder if switching the order of parents will give any interesting difference,\n> I guess not)\n>\n> test_expect_success 'remerge-diff with file/link conflict' '\n>         git branch -d base side1 side2 &&\n>         git switch --orphan base &&\n\nI'd rather have subdirectories with git repositories (much like t6416,\nt6422, and t6423) if we're going to be adding many more tests here.\n\n\n>         echo base >typechange &&\n>         git add typechange &&\n>         git commit -m base &&\n>\n>         git branch side1 &&\n>         git branch side2 &&\n>\n>         git checkout side1 &&\n>         echo orig-file-contents >typechange &&\n>         git commit -a -m side1 &&\n>\n>         git checkout side2 &&\n>         ln -sf . typechange &&\n>         git add typechange &&\n>         git commit -m side2 &&\n>\n>         git checkout -b resolution2 side1 &&\n>         test_must_fail git merge side2 &&\n>         rm typechange &&\n>         mv typechange~HEAD typechange &&\n>         echo resolved >>typechange &&\n>         git add typechange~HEAD typechange &&\n>         git merge --continue &&\n>\n>         git show --oneline --remerge-diff resolution2 >tmp &&\n>         sed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n>\n>         cat <<-EOF >tmp &&\n>         7759b27 Merge branch ${SQ}side2${SQ} into resolution2\n>         diff --git a/typechange b/typechange\n>         deleted file mode 120000\n>         CONFLICT (distinct types): typechange had different types on each side; renamed one of them so each can be recorded somewhere.\n>         index 945c9b4..0000000\n>         --- a/typechange\n>         +++ /dev/null\n>         @@ -1 +0,0 @@\n>         -.\n>         \\ No newline at end of file\n>         diff --git a/typechange b/typechange\n>         new file mode 100644\n>         CONFLICT (distinct types): typechange had different types on each side; renamed one of them so each can be recorded somewhere.\n>         index 0000000..70885e4\n>         --- /dev/null\n>         +++ b/typechange\n>         @@ -0,0 +1,2 @@\n>         +orig-file-contents\n>         +resolved\n>         diff --git a/typechange~738109f (side1) b/typechange~738109f (side1)\n>         deleted file mode 100644\n>         index 70885e4..0000000\n>         --- a/typechange~738109f (side1)\n>         +++ /dev/null\n>         @@ -1 +0,0 @@\n>         -orig-file-contents\n>         EOF\n>         # We still have some sha1 hashes above; rip them out so test works\n>         # with sha256\n>         sed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n>\n>         test_cmp expect actual\n> '\n\nbut otherwise the testcase looks good, if we want it.\n"},{"id":"445153","messageId":"CABPp-BHtnFWLpPxXF56BbMSayDOp9Qm1GhL3xCz7pQPcEJZ2Nw@mail.gmail.com","threadId":"57133","inReplyTo":"YcuXQQHsl8iVdK8R@camp.crustytoothpaste.net","subject":"Re: [PATCH v2 1/8] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-28T23:45:45Z","receivedAt":"2021-12-28T23:45:59Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 28, 2021 at 3:01 PM brian m. carlson\n<sandals@crustytoothpaste.net> wrote:\n>\n> On 2021-12-28 at 22:34:03, Elijah Newren wrote:\n> > CC'ing brian in case he has comments on the sha256 stuff and whether\n> > he thinks there's a cleaner way to make my tests work with sha256.\n> > (brian: See the very end of the email.)\n> >\n> > On Tue, Dec 28, 2021 at 2:56 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n> > >\n> > > On Sat, Dec 25, 2021 at 07:59:12AM +0000, Elijah Newren via GitGitGadget wrote:\n> > > > +test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' '\n> > > > +     git log -1 --oneline ab_resolution >tmp &&\n> > > > +     cat <<-EOF >>tmp &&\n> > > > +     diff --git a/numbers b/numbers\n> > > > +     index a1fb731..6875544 100644\n> > > > +     --- a/numbers\n> > > > +     +++ b/numbers\n> > > > +     @@ -1,13 +1,9 @@\n> > > > +      1\n> > > > +      2\n> > > > +     -<<<<<<< b0ed5cb (change_a)\n> > > > +     -three\n> > > > +     -=======\n> > > > +     -tres\n> > > > +     ->>>>>>> 6cd3f82 (change_b)\n> > > > +     +drei\n> > >\n> > > nice\n> > >\n> > > > +      4\n> > > > +      5\n> > > > +      6\n> > > > +      7\n> > > > +     -eight\n> > > > +     +acht\n> > > > +      9\n> > > > +     EOF\n> > > > +     # Hashes above are sha1; rip them out so test works with sha256\n> > > > +     sed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n> > >\n> > > Right, sha256 could cause many noisy test changes. I wonder if there is a\n> > > more general way to avoid this; maybe default to SHA1 for existing tests?\n> >\n> > Not \"could\", but \"does\".  And this is not something to be avoided.\n> > The default testsuite we run in CI involves a run of\n> > GIT_TEST_DEFAULT_HASH=sha256 under linux-clang.  Making these tests\n> > SHA1-only just reduces our coverage and makes the transition to SHA256\n> > harder; I think that's the opposite of the direction we want to go.\n> >\n> > These changes I've made here are sufficient to make these tests work\n> > under sha256; you can see the test results here:\n> > https://github.com/gitgitgadget/git/runs/4646949283?check_suite_focus=true.\n> > Under \"Run ci/run-build-and-tests.sh\" note that there are two runs of\n> > tests, and the second has \"export GIT_TEST_DEFAULT_HASH=sha256\"\n> > preceding it.\n> >\n> > There might be a cleaner way to make these tests sha256-compatible,\n> > but this seemed like a pretty simple way to me.\n>\n> The question here is, do we care very much about testing these specific\n> hashes?  If so, then we should use test_oid_cache to set up some OIDs\n> and make sure they're correct for both SHA-1 and SHA-256, and then\n> replace them in the code with calls to test_oid.\n>\n> However, my impression is that we probably don't care very much about\n> what the specific values are, and in that case, this is completely fine.\n> We do similar things elsewhere in the testsuite.\n\nThanks for chiming in.  Your impression is right; I don't care about\nthe specific hashes, just the general form of the diff content, so\nI'll keep it as is.\n"},{"id":"445158","messageId":"20211229001647.6pv5damtyt3dsiyr@gmail.com","threadId":"57133","inReplyTo":"CABPp-BH5XUsmTo=BD7osUgi4o=eFWgaQkN1qYDky6uqb9SykHA@mail.gmail.com","subject":"Re: [PATCH v2 7/8] diff: add ability to insert additional headers for paths","fromName":"Johannes Altmanninger","fromEmail":"aclopte@gmail.com","sentAt":"2021-12-29T00:16:47Z","receivedAt":"2021-12-29T00:16:56Z","isPatch":true,"sender":{"key":"aclopte@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6853872?v=4"},"body":"On Tue, Dec 28, 2021 at 01:09:57PM -0800, Elijah Newren wrote:\n> On Tue, Dec 28, 2021 at 2:57 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n> >\n> > On Sat, Dec 25, 2021 at 07:59:18AM +0000, Elijah Newren via GitGitGadget wrote:\n> > > +     for (i = 0; i < q->nr; i++) {\n> > > +             struct diff_filepair *p = q->queue[i];\n> > > +             char *path = p->one->path ? p->one->path : p->two->path;\n> > > +\n> > > +             if (strmap_contains(o->additional_path_headers, path))\n> > > +                     strset_add(&present, path);\n> > > +     }\n> > > +\n> > > +     /*\n> > > +      * Loop over paths in additional_path_headers; for each NOT already\n> > > +      * in diff_queued_diff, create a synthetic filepair and insert that\n> > > +      * into diff_queued_diff.\n> > > +      */\n> > > +     strmap_for_each_entry(o->additional_path_headers, &iter, e) {\n> > > +             if (!strset_contains(&present, e->key)) {\n> > > +                     struct diff_filespec *one, *two;\n> > > +                     struct diff_filepair *p;\n> > > +\n> > > +                     one = alloc_filespec(e->key);\n> > > +                     two = alloc_filespec(e->key);\n> > > +                     fill_filespec(one, null_oid(), 0, 0);\n> > > +                     fill_filespec(two, null_oid(), 0, 0);\n> > > +                     p = diff_queue(q, one, two);\n> > > +                     p->status = DIFF_STATUS_MODIFIED;\n> > > +             }\n> > > +     }\n> >\n> > All these string hash-maps are not really typical for a C program. I'm sure\n> > they are the best choice for an advanced merge algorithm\n> \n> Agreed up to here.\n> \n> > but they are not\n> > really necessary for computing/printing a diff.\n> \n> Technically agree that it _could_ be solved a different way, but the\n> strmaps are a much more natural solution to this problem in this\n> particular case; more on this below.\n\nOh yeah, I agree that strmaps are the more intuitive solution.\n\n> \n> > It feels like this is an\n> > implementation detail from merge-ort that's leaking into other components.\n> \n> And I disagree here, on _both_ the explicit point and the underlying\n> suggestion that you seem to be making that strmap should be avoided\n> outside of merging.  The strmap.[ch] type was originally a suggestion\n> from Peff for areas of git completely unrelated to merging (see the\n> beginning of https://lore.kernel.org/git/20200821194857.GD1165@coredump.intra.peff.net/,\n> and the first link in that email).  It's a new datatype for git, much\n> like strbuf or string_list or whatever before it, that is there to be\n> used when it's a natural fit for the problem at hand.  The lack of\n> strmap previously led folks to abuse other existing data structures\n> (and in a way that often led to poor performance to boot).\n\nRight, all those rename-detection performance fixes were pretty dazzling\n\n> \n> > What we want to do is\n> >\n> >         for file_pair in additional_headers:\n> >                 if not already_queued(file_pair):\n> >                         queue(file_pair)\n> \n> Yes, precisely.\n> \n> > to do that, you use a temporary has-set (\"present\") that records everything\n> > that's already queued (already_queued() is a lookup in that set).\n> >\n> > Let's assume both the queue and additional_headers are sorted arrays.\n> \n> That's a bad assumption; we can't rely on *either* being sorted.  I\n\nOK, I hadn't checked if the queue is sorted\n\n> actually started my implementation by trying exactly what you mention\n> first; I too thought it'd be more natural and clearer to do this.  Of\n> course, before implementing it, I had to verify whether\n> diff_queued_diff was sorted.  So, I added some code that would check\n> the order and fail if the queue wasn't sorted.  7 of the test files in\n> the regression testsuite had one or more failing tests.\n> \n> I think the queue was intended to be sorted (see\n> diffcore_fix_diff_index()), but in practice it's not.  And I'm worried\n> that if I find the current cases where it fails to be sorted and \"fix\"\n> them (though I don't actually know if this was intentional or not so I\n> don't know if that's really a fix or a break), that I'd end up with\n> additional cases in the future where they fail to be sorted anyway.\n> So, no matter what, relying on diff_queued_diff being sorted seems\n> ill-advised.\n> \n> Also...\n> \n> > Then we could efficiently merge them (like a merge-sort algorithm)\n> > without ever allocating a temporary hash map.\n> >\n> > I haven't checked if this is practical (better wait for feedback).\n> > We'd probably need to convert the strmap additional_path_headers into an\n> > array and sort it (I guess our hash map does not guarantee any ordering?)\n> \n> Right, strmap has no ordering either.  I was willing to stick those\n> into a string_list and sort them, but making temporary copies of both\n> the strmap and the diff_queued_diff just to sort them so that I can\n\nBut you already sort diff_queued_diff at the end of\ncreate_filepairs_for_header_only_notifications(), so sorting a bit earlier\nin that function, before enqueueing the new entries won't change the final\nresult, and allows us to work with a sorted queue; no need for a temporary\ncopy (we'd only need to copy the strmap).\n\n> reasonably cheaply ask \"are items from this thing present in this\n> other thing?\" seems to be stretching things a bit too far.\n> maps/hashes provide a very nice \"is this item present\" lookup and are\n> a natural way to ask that.  Since that is exactly the question I am\n> asking, I think they are the better data structure here.\n\nYeah that makes sense. In theory if we ask\n\"What is the union of the queued pairs and the extra pairs induced by\nconflict messages?\"  we could abstract away the \"is this item present\"\nlookup but in practice that's hard.\n\n> So, this was not at all a leak of merge-ort datastructures, but rather a\n> picking of the appropriate data structures for the problem at hand.\n\nI think we have two viable solutions to this problem\n1. use a temporary strset to figure out which pairs to add\n2. use a temporary array, sort it, and \"merge\" the two arrays\n\nI agree that 1 is more intuitive and natural for humans, and it's probably the\nway to go. But it is a bit less elegant because it adds a strmap entry for\neach pair in the queue, whereas 2 only needs to add an array element for\neach pair with non-content conflicts, which are much fewer. (Okay that's a\nminor detail.)  With the right abstractions 2 is pretty simple as well:\n\n\tj = 0\n\textra_headers = sorted((key, val) for key, val in additional_headers)\n\tfor i in 0..len(queue):\n\t\twhile j < len(extra_headers) && compare(extra_headers[j].key, queue[i]) <= 0:\n\t\t\tif compare(extra_headers[j].key, queue[i]) < 0:\n\t\t\t\tenqueue(file_pair_for(extra_headers[j]))\n\t\t\tj++\n\nwhere\n\n\tdef compare(key: str, pair: diff_filepair) -> int:\n\t\tother = pair.one ? pair.one.path : pair.two.path # Mimic diffnamecmp\n\t\treturn strcmp(key, other)\n"},{"id":"445241","messageId":"CABPp-BHoiNiYoVzknJEGQpe5a6nnQXj_bwwrtWbmKLBiUCOj_Q@mail.gmail.com","threadId":"57133","inReplyTo":"20211229001647.6pv5damtyt3dsiyr@gmail.com","subject":"Re: [PATCH v2 7/8] diff: add ability to insert additional headers for paths","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-30T22:04:30Z","receivedAt":"2021-12-30T22:04:45Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 28, 2021 at 4:16 PM Johannes Altmanninger <aclopte@gmail.com> wrote:\n>\n> On Tue, Dec 28, 2021 at 01:09:57PM -0800, Elijah Newren wrote:\n> > On Tue, Dec 28, 2021 at 2:57 AM Johannes Altmanninger <aclopte@gmail.com> wrote:\n> > >\n> > > On Sat, Dec 25, 2021 at 07:59:18AM +0000, Elijah Newren via GitGitGadget wrote:\n....\n> > > but they are not\n> > > really necessary for computing/printing a diff.\n> >\n> > Technically agree that it _could_ be solved a different way, but the\n> > strmaps are a much more natural solution to this problem in this\n> > particular case; more on this below.\n>\n> Oh yeah, I agree that strmaps are the more intuitive solution.\n\nCool, sounds like we're heading towards consensus.\n\n> > > It feels like this is an\n> > > implementation detail from merge-ort that's leaking into other components.\n> >\n> > And I disagree here, on _both_ the explicit point and the underlying\n> > suggestion that you seem to be making that strmap should be avoided\n> > outside of merging.  The strmap.[ch] type was originally a suggestion\n> > from Peff for areas of git completely unrelated to merging (see the\n> > beginning of https://lore.kernel.org/git/20200821194857.GD1165@coredump.intra.peff.net/,\n> > and the first link in that email).  It's a new datatype for git, much\n> > like strbuf or string_list or whatever before it, that is there to be\n> > used when it's a natural fit for the problem at hand.  The lack of\n> > strmap previously led folks to abuse other existing data structures\n> > (and in a way that often led to poor performance to boot).\n>\n> Right, all those rename-detection performance fixes were pretty dazzling\n\nI actually wasn't talking about rename-detection or merge machinery\n(though it applies there too).  The original strmap proposal was\nsuggested in an email entitled, \"ordered string-list considered\nharmful\", and neither rename-detection nor merge machinery were part\nof the thread at the time.  I also didn't participate in the\nthread...until I implemented the suggested API with some tweaks and\nsubmitted it about a year later.\n\n> > > What we want to do is\n> > >\n> > >         for file_pair in additional_headers:\n> > >                 if not already_queued(file_pair):\n> > >                         queue(file_pair)\n> >\n> > Yes, precisely.\n> >\n> > > to do that, you use a temporary has-set (\"present\") that records everything\n> > > that's already queued (already_queued() is a lookup in that set).\n> > >\n> > > Let's assume both the queue and additional_headers are sorted arrays.\n> >\n> > That's a bad assumption; we can't rely on *either* being sorted.  I\n>\n...\n> > > Then we could efficiently merge them (like a merge-sort algorithm)\n> > > without ever allocating a temporary hash map.\n> > >\n> > > I haven't checked if this is practical (better wait for feedback).\n> > > We'd probably need to convert the strmap additional_path_headers into an\n> > > array and sort it (I guess our hash map does not guarantee any ordering?)\n> >\n> > Right, strmap has no ordering either.  I was willing to stick those\n> > into a string_list and sort them, but making temporary copies of both\n> > the strmap and the diff_queued_diff just to sort them so that I can\n>\n> But you already sort diff_queued_diff at the end of\n> create_filepairs_for_header_only_notifications(), so sorting a bit earlier\n> in that function, before enqueueing the new entries won't change the final\n> result, and allows us to work with a sorted queue; no need for a temporary\n> copy (we'd only need to copy the strmap).\n\nGood point, although...\n\n> > reasonably cheaply ask \"are items from this thing present in this\n> > other thing?\" seems to be stretching things a bit too far.\n> > maps/hashes provide a very nice \"is this item present\" lookup and are\n> > a natural way to ask that.  Since that is exactly the question I am\n> > asking, I think they are the better data structure here.\n>\n> Yeah that makes sense. In theory if we ask\n> \"What is the union of the queued pairs and the extra pairs induced by\n> conflict messages?\"  we could abstract away the \"is this item present\"\n> lookup but in practice that's hard.\n>\n> > So, this was not at all a leak of merge-ort datastructures, but rather a\n> > picking of the appropriate data structures for the problem at hand.\n>\n> I think we have two viable solutions to this problem\n> 1. use a temporary strset to figure out which pairs to add\n> 2. use a temporary array, sort it, and \"merge\" the two arrays\n>\n> I agree that 1 is more intuitive and natural for humans, and it's probably the\n> way to go. But it is a bit less elegant because it adds a strmap entry for\n> each pair in the queue, whereas 2 only needs to add an array element for\n> each pair with non-content conflicts, which are much fewer. (Okay that's a\n> minor detail.)  With the right abstractions 2 is pretty simple as well:\n>\n>         j = 0\n>         extra_headers = sorted((key, val) for key, val in additional_headers)\n\nRight, so this is two sorts instead of one.  (Sorting both the\ndiff_queued_diff initially, as well as the additional_headers, before\nthen attempting to merge the two.)  Probably a win performance-wise,\nbut just noting that it makes the code slightly less simple.\n\n>         for i in 0..len(queue):\n>                 while j < len(extra_headers) && compare(extra_headers[j].key, queue[i]) <= 0:\n>                         if compare(extra_headers[j].key, queue[i]) < 0:\n\nThe duplicate comparison (two calls to strcmp) probably kills any\nperformance gain you were aiming for with this strategy.  Fixable, but\nit does make the code longer.\n\n>                                 enqueue(file_pair_for(extra_headers[j]))\n\nThe queue is an array of sorted items, so enqueue here would be\ninsertion into an already sorted list.  Inserting N items into a list\nof M items is quadratic (O(N*M)) -- unless you meant to just append to\nthe end and add a third sort at the end?\n\n>                         j++\n\nAt the end of the for loop, there may be remaining additional headers\nthat sort after all those found in the queue, so you'll need an\nadditional loop to handle those.\n\n> where\n>\n>         def compare(key: str, pair: diff_filepair) -> int:\n>                 other = pair.one ? pair.one.path : pair.two.path # Mimic diffnamecmp\n>                 return strcmp(key, other)\n\n\nSince you clearly felt this approach might be better, I went and\nimplemented it (+ tested and debugged):\n\n diff.c | 112 +++++++++++++++++++++++++++++++++++++++++++----------------------\n 1 file changed, 73 insertions(+), 38 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex d771406e69..0cdaa2e2ab 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -6389,52 +6389,87 @@ void diff_warn_rename_limit(const char\n*varname, int needed, int degraded_cc)\n                warning(_(rename_limit_advice), varname, needed);\n }\n\n+static void diff_queue_placeholder(char *path)\n+{\n+       struct diff_filespec *one, *two;\n+       struct diff_filepair *p;\n+\n+       one = alloc_filespec(path);\n+       two = alloc_filespec(path);\n+       fill_filespec(one, null_oid(), 0, 0);\n+       fill_filespec(two, null_oid(), 0, 0);\n+       p = diff_queue(&diff_queued_diff, one, two);\n+       p->status = DIFF_STATUS_MODIFIED;\n+}\n+\n static void create_filepairs_for_header_only_notifications(struct\ndiff_options *o)\n {\n-       struct strset present;\n-       struct diff_queue_struct *q = &diff_queued_diff;\n+       struct diff_queue_struct tmp_queue = { 0 };\n+       struct string_list tmp_list = STRING_LIST_INIT_NODUP;\n        struct hashmap_iter iter;\n        struct strmap_entry *e;\n+       char *queue_path = NULL, *list_path = NULL;\n        int i;\n+       int j;\n\n-       strset_init_with_options(&present, /*pool*/ NULL, /*strdup*/ 0);\n-\n-       /*\n-        * Find out which paths exist in diff_queued_diff, preferring\n-        * one->path for any pair that has multiple paths.\n-        */\n-       for (i = 0; i < q->nr; i++) {\n-               struct diff_filepair *p = q->queue[i];\n-               char *path = p->one->path ? p->one->path : p->two->path;\n-\n-               if (strmap_contains(o->additional_path_headers, path))\n-                       strset_add(&present, path);\n-       }\n-\n-       /*\n-        * Loop over paths in additional_path_headers; for each NOT already\n-        * in diff_queued_diff, create a synthetic filepair and insert that\n-        * into diff_queued_diff.\n-        */\n-       strmap_for_each_entry(o->additional_path_headers, &iter, e) {\n-               if (!strset_contains(&present, e->key)) {\n-                       struct diff_filespec *one, *two;\n-                       struct diff_filepair *p;\n-\n-                       one = alloc_filespec(e->key);\n-                       two = alloc_filespec(e->key);\n-                       fill_filespec(one, null_oid(), 0, 0);\n-                       fill_filespec(two, null_oid(), 0, 0);\n-                       p = diff_queue(q, one, two);\n-                       p->status = DIFF_STATUS_MODIFIED;\n-               }\n-       }\n-\n-       /* Re-sort the filepairs */\n+       /* Ensure existing filepairs are sorted */\n        diffcore_fix_diff_index();\n\n-       /* Cleanup */\n-       strset_clear(&present);\n+       /* Get a sorted list of additional_path_headers */\n+       strmap_for_each_entry(o->additional_path_headers, &iter, e) {\n+               string_list_append(&tmp_list, e->key);\n+       }\n+       string_list_sort(&tmp_list);\n+\n+       /*\n+        * Move everything from diff_queued_diff to tmp_queue.  We'll copy\n+        * them back one-by-one, with extra entries inserted from tmp_list.\n+        */\n+       SWAP(tmp_queue, diff_queued_diff);\n+\n+       /*\n+        * Add entries from tmp_queue and tmp_list to diff_queued_diff, keeping\n+        * the overall list sorted.\n+        */\n+       j = 0;\n+       for (i = 0; i < tmp_queue.nr; i++) {\n+               struct diff_filepair *p = tmp_queue.queue[i];\n+               queue_path = p->one->path ? p->one->path : p->two->path;\n+\n+               while (j < tmp_list.nr) {\n+                       int cmp;\n+\n+                       list_path = tmp_list.items[j].string;\n+                       cmp = strcmp(queue_path, list_path);\n+\n+                       if (cmp < 0)\n+                               break;\n+                       else if (cmp > 0)\n+                               diff_queue_placeholder(list_path);\n+                       j++;\n+               }\n+               diff_q(&diff_queued_diff, p);\n+       }\n+       /*\n+        * We've got all the entries from tmp_queue now, but may have more\n+        * from tmp_list to insert.  Make sure to only add new entries for\n+        * strings not already in diff_queued_diff.\n+        */\n+       if (j < tmp_list.nr && !strcmp(queue_path, list_path))\n+               j++;\n+       while (j < tmp_list.nr) {\n+               char *list_path = tmp_list.items[j].string;\n+               diff_queue_placeholder(list_path);\n+               j++;\n+       }\n+\n+       /*\n+        * We *only* free tmp_queue.queue, not the stuff it points to because\n+        * that has been copied into diff_queued_diff.  Zero out tmp_queue to\n+        * make it clear we don't want to free anything else.\n+        */\n+       free(tmp_queue.queue);\n+       memset(&tmp_queue, 0, sizeof(tmp_queue));\n }\n\n static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n\n\nIt's actually considerably more code as you can see from the diffstat,\nand feels like we're reaching into some ugly internals with tmp_queue\n(the SWAP and the special-case freeing) in order to get the desired\nperformance improvements.  And it was already O(NlogN) overall (due to\nthe sort), which doesn't change with this new algorithm.  It's really,\nreally hard for me to imagine a case where we have large numbers of\nadditional headers.  Even if someone else can imagine that we for some\nreason have a huge number of conflicts in order to generate a huge\nnumber of additional headers...how could the performance of sorting\nO(N) filenames and merging these lists possibly matter in comparison\nto the O(N) three-way file merges that would likely have been\nperformed from those conflicts?\n\nSo, I'm going to throw this code away and keep the original.\n\nIt was an interesting idea and exercise; thanks for keeping me on my toes.\n"},{"id":"445252","messageId":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v2.git.1640419159.gitgitgadget@gmail.com","subject":"[PATCH v3 0/9] Add a new --remerge-diff capability to show & log","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:00Z","receivedAt":"2021-12-30T23:36:14Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Here are some patches to add a --remerge-diff capability to show & log,\nwhich works by comparing merge commits to an automatic remerge (note that\nthe automatic remerge tree can contain files with conflict markers).\n\nChanges NOT included (mostly because I'm not sure what to add or where):\n\n * Ævar suggested also extending the docs with usage guidelines, but the\n   example he picked was IMO best handled by just add --remerge-diff, so I'm\n   not sure what to add to the docs. Maybe the log -S<string> --remerge-diff\n   example as a way to more reliably determine when a string was added to or\n   removed from the codebase? Where would that go anyway?\n * Johannes Altimanninger suggested changing the ordering of the new headers\n   relative to other headers. He made a good point, but I also like having\n   the conflict messages next to the text, so I'm conflicted about what's\n   best.\n * (Technically not part of this feature, but kind of related.) Months ago,\n   Junio suggested documenting ${GIT_DIR}/AUTO_MERGE better\n   (https://lore.kernel.org/git/xmqqtuj4nepe.fsf@gitster.g/). I looked at\n   the time, but couldn't find a place to put it that made sense to me.\n\nChanges since v2 (of the restarted submission):\n\n * Numerous small improvements suggested by Johannes Altmanninger\n * Avoid including conflict messages from inner merges (due to example\n   pointed out by Ævar).\n * Added a \"remerge\" prefix to all the new diff headers (suggested by Junio\n   in a previous round, but I couldn't come up with a good name before. It\n   suddenly hit me that \"remerge\" is an obvious prefix to use, and even\n   helps explain what the rest of the line is for.)\n\nChanges since v1 (of the restarted submission, which technically was v2):\n\n * Restructured the series, so the first patch introduces the feature --\n   with a bunch of caveats. Subsequent patches clean up those caveats. This\n   avoids introducing not-yet-used functions, and hopefully makes review\n   easier.\n * added testcases\n * numerous small improvements suggested by Ævar and Junio\n\nChanges since original submission[1]:\n\n * Rebased on top of the version of ns/tmp-objdir that Neeraj submitted\n   (Neeraj's patches were based on v2.34, but ns/tmp-objdir got applied on\n   an old commit and does not even build because of that).\n * Modify ll-merge API to return a status, instead of printing \"Cannot merge\n   binary files\" on stdout[2] (as suggested by Peff)\n * Make conflict messages and other such warnings into diff headers of the\n   subsequent remerge-diff rather than appearing in the diff as file content\n   of some funny looking filenames (as suggested by Peff[3] and Junio[4])\n * Sergey ack'ed the diff-merges.c portion of the patches, but that wasn't\n   limited to one patch so not sure where to record that ack.\n\n[1]\nhttps://lore.kernel.org/git/pull.1080.git.git.1630376800.gitgitgadget@gmail.com/;\nGitHub wouldn't let me change the target branch for the PR, so I had to\ncreate a new one with the new base and thus the reason for not sending this\nas v2 even though it is. [2]\nhttps://lore.kernel.org/git/YVOZRhWttzF18Xql@coredump.intra.peff.net/,\nhttps://lore.kernel.org/git/YVOZty9D7NRbzhE5@coredump.intra.peff.net/ [3]\nhttps://lore.kernel.org/git/YVOXPTjsp9lrxmS6@coredump.intra.peff.net/ [4]\nhttps://lore.kernel.org/git/xmqqr1d7e4ug.fsf@gitster.g/\n\n=== FURTHER BACKGROUND (original cover letter material) ==\n\nHere are some example commits you can try this out on (with git show\n--remerge-diff $COMMIT):\n\n * git.git conflicted merge: 07601b5b36\n * git.git non-conflicted change: bf04590ecd\n * linux.git conflicted merge: eab3540562fb\n * linux.git non-conflicted change: 223cea6a4f05\n\nMany more can be found by just running git log --merges --remerge-diff in\nyour repository of choice and searching for diffs (most merges tend to be\nclean and unmodified and thus produce no diff but a search of '^diff' in the\nlog output tends to find the examples nicely).\n\nSome basic high level details about this new option:\n\n * This option is most naturally compared to --cc, though the output seems\n   to be much more understandable to most users than --cc output.\n * Since merges are often clean and unmodified, this new option results in\n   an empty diff for most merges.\n * This new option shows things like the removal of conflict markers, which\n   hunks users picked from the various conflicted sides to keep or remove,\n   and shows changes made outside of conflict markers (which might reflect\n   changes needed to resolve semantic conflicts or cleanups of e.g.\n   compilation warnings or other additional changes an integrator felt\n   belonged in the merged result).\n * This new option does not (currently) work for octopus merges, since\n   merge-ort is specific to two-parent merges[1].\n * This option will not work on a read-only or full filesystem[2].\n * We discussed this capability at Git Merge 2020, and one of the\n   suggestions was doing a periodic git gc --auto during the operation (due\n   to potential new blobs and trees created during the operation). I found a\n   way to avoid that; see [2].\n * This option is faster than you'd probably expect; it handles 33.5 merge\n   commits per second in linux.git on my computer; see below.\n\nIn regards to the performance point above, the timing for running the\nfollowing command:\n\ntime git log --min-parents=2 --max-parents=2 $DIFF_FLAG | wc -l\n\n\nin linux.git (with v5.4 checked out, since my copy of linux is very out of\ndate) is as follows:\n\nDIFF_FLAG=--cc:            71m 31.536s\nDIFF_FLAG=--remerge-diff:  31m  3.170s\n\n\nNote that there are 62476 merges in this history. Also, output size is:\n\nDIFF_FLAG=--cc:            2169111 lines\nDIFF_FLAG=--remerge-diff:  2458020 lines\n\n\nSo roughly the same amount of output as --cc, as you'd expect.\n\nAs a side note: git log --remerge-diff, when run in various repositories and\nallowed to run all the way back to the beginning(s) of history, is a nice\nstress test of sorts for merge-ort. Especially when users run it for you on\ntheir repositories they are working on, whether intentionally or via a bug\nin a tool triggering that command to be run unexpectedly. Long story short,\nsuch a bug in an internal tool existed in December 2020 and this command was\nrun on an internal repository and found a platform-specific bug in merge-ort\non some really old merge commit from that repo. I fixed that bug (a\nSTABLE_QSORT thing) while upstreaming all the merge-ort patches in the mean\ntime, but it was nice getting extra testing. Having more folks run this on\ntheir repositories might be useful extra testing of the new merge strategy.\n\nAlso, I previously mentioned --remerge-diff-only (a flag to show how\ncherry-picks or reverts differ from an automatic cherry-pick or revert, in\naddition to showing how merges differ from an automatic merge). This series\ndoes not include the patches to introduce that option; I'll submit them\nlater.\n\nTwo other things that might be interesting but are not included and which I\nhaven't investigated:\n\n * some mechanism for passing extra merge options through (e.g.\n   -Xignore-space-change)\n * a capability to compare the automatic merge to a second automatic merge\n   done with different merge options. (Not sure if this would be of interest\n   to end users, but might be interesting while developing new a\n   --strategy-option, or maybe checking how changing some default in the\n   merge algorithm would affect historical merges in various repositories).\n\n[1] I have nebulous ideas of how an Octopus-centric ORT strategy could be\nwritten -- basically, just repeatedly invoking ort and trying to make sure\nnested conflicts can be differentiated. For now, though, a simple warning is\nprinted that octopus merges are not handled and no diff will be shown. [2]\nNew blobs/trees can be written by the three-way merging step. These are\nwritten to a temporary area (via tmp-objdir.c) under the git object store\nthat is cleaned up at the end of the operation, with the new loose objects\nfrom the remerge being cleaned up after each individual merge.\n\nElijah Newren (9):\n  show, log: provide a --remerge-diff capability\n  log: clean unneeded objects during `log --remerge-diff`\n  ll-merge: make callers responsible for showing warnings\n  merge-ort: capture and print ll-merge warnings in our preferred\n    fashion\n  merge-ort: mark a few more conflict messages as omittable\n  merge-ort: format messages slightly different for use in headers\n  diff: add ability to insert additional headers for paths\n  show, log: include conflict/warning messages in --remerge-diff headers\n  merge-ort: mark conflict/warning messages from inner merges as\n    omittable\n\n Documentation/diff-options.txt |  10 +-\n apply.c                        |   5 +-\n builtin/checkout.c             |  12 ++-\n builtin/log.c                  |  15 +++\n diff-merges.c                  |  12 +++\n diff.c                         | 116 +++++++++++++++++++++-\n diff.h                         |   3 +-\n ll-merge.c                     |  40 ++++----\n ll-merge.h                     |   9 +-\n log-tree.c                     |  71 +++++++++++++-\n merge-blobs.c                  |   5 +-\n merge-ort.c                    |  55 ++++++++++-\n merge-ort.h                    |  10 ++\n merge-recursive.c              |   9 +-\n merge-recursive.h              |   2 +\n notes-merge.c                  |   5 +-\n rerere.c                       |   9 +-\n revision.h                     |   6 +-\n t/t4069-remerge-diff.sh        | 170 +++++++++++++++++++++++++++++++++\n t/t6404-recursive-merge.sh     |   9 +-\n t/t6406-merge-attr.sh          |   9 +-\n tmp-objdir.c                   |   5 +\n tmp-objdir.h                   |   6 ++\n 23 files changed, 545 insertions(+), 48 deletions(-)\n create mode 100755 t/t4069-remerge-diff.sh\n\n\nbase-commit: 4e44121c2d7bced65e25eb7ec5156290132bec94\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1103%2Fnewren%2Fremerge-diff-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1103/newren/remerge-diff-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/1103\n\nRange-diff vs v2:\n\n  1:  b3ae62083e1 !  1:  d57ae218cf9 show, log: provide a --remerge-diff capability\n     @@ Commit message\n      \n          This capability works by creating a temporary object directory and\n          marking it as the primary object store.  This makes it so that any blobs\n     -    or trees created during the automatic merge easily removable afterwards\n     -    by just deleting all objects from the temporary object directory.\n     +    or trees created during the automatic merge are easily removable\n     +    afterwards by just deleting all objects from the temporary object\n     +    directory.\n      \n          There are a few ways that this implementation is suboptimal:\n            * `log --remerge-diff` becomes slow, because the temporary object\n     -        directory can fills with many loose objects while running\n     +        directory can fill with many loose objects while running\n            * the log output can be muddied with misplaced \"warning: cannot merge\n              binary files\" messages, since ll-merge.c unconditionally writes those\n              messages to stderr while running instead of allowing callers to\n     @@ Commit message\n              way for a user of --remerge-diff to know that there had been a\n              conflict which was resolved (and which possibly motivated other\n              changes in the merge commit).\n     +      * when fixing the previous issue, note that some unimportant conflict\n     +        and warning messages might start being included.  We should instead\n     +        make sure these remain dropped.\n          Subsequent commits will address these issues.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## Documentation/diff-options.txt ##\n     +@@ Documentation/diff-options.txt: endif::git-diff[]\n     + endif::git-format-patch[]\n     + \n     + ifdef::git-log[]\n     +---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc)::\n     ++--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r)::\n     + --no-diff-merges::\n     + \tSpecify diff format to be used for merge commits. Default is\n     + \t{diff-merges-default} unless `--first-parent` is in use, in which case\n      @@ Documentation/diff-options.txt: ifdef::git-log[]\n       \teach of the parents. Separate log entry and diff is generated\n       \tfor each parent.\n     @@ t/t4069-remerge-diff.sh (new)\n      +\tgit commit -a -m change_c &&\n      +\n      +\tgit checkout bc_resolution &&\n     -+\t# fast forward\n     -+\tgit merge feature_b &&\n     ++\tgit merge --ff-only feature_b &&\n      +\t# no conflict\n      +\tgit merge feature_c &&\n      +\n      +\tgit checkout ab_resolution &&\n     -+\t# fast forward\n     -+\tgit merge feature_a &&\n     ++\tgit merge --ff-only feature_a &&\n      +\t# conflicts!\n      +\ttest_must_fail git merge feature_b &&\n      +\t# Resolve conflict...and make another change elsewhere\n  2:  54f1fb31d04 =  2:  798625b53f2 log: clean unneeded objects during `log --remerge-diff`\n  3:  d5566f5d136 !  3:  b952f674df1 ll-merge: make callers responsible for showing warnings\n     @@ Commit message\n          Since some callers may want to send warning messages to somewhere other\n          than stdout/stderr, stop printing \"warning: Cannot merge binary files\"\n          from ll-merge and instead modify the return status of ll_merge() to\n     -    indicate when a merge of binary files has occurred.\n     +    indicate when a merge of binary files has occurred.  Message printing\n     +    probably does not belong in a \"low-level merge\" anyway.\n      \n     -    This commit continues printing the message as-is; future changes will\n     -    start handling the new commit differently in the merge-ort codepath.\n     +    This commit continues printing the message as-is, just from the callers\n     +    instead of within ll_merge().  Future changes will start handling the\n     +    message differently in the merge-ort codepath.\n     +\n     +    There was one special case here: the callers in rerere.c do NOT check\n     +    for and print such a message; since those code paths explicitly skip\n     +    over binary files, there is no reason to check for a return status of\n     +    LL_MERGE_BINARY_CONFLICT or print the related message.\n      \n          Note that my methodology included first modifying ll_merge() to return\n          a struct, so that the compiler would catch all the callers for me and\n     @@ ll-merge.c: static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n       \txmp.file2 = name2;\n      -\treturn xdl_merge(orig, src1, src2, &xmp, result);\n      +\tstatus = xdl_merge(orig, src1, src2, &xmp, result);\n     -+\tret = (status > 1 ) ? LL_MERGE_CONFLICT : status;\n     ++\tret = (status > 0) ? LL_MERGE_CONFLICT : status;\n      +\treturn ret;\n       }\n       \n     @@ ll-merge.c: static int ll_ext_merge(const struct ll_merge_driver *fn,\n       \tstrbuf_release(&cmd);\n       \tstrbuf_release(&path_sq);\n      -\treturn status;\n     -+\tret = (status > 1) ? LL_MERGE_CONFLICT : status;\n     ++\tret = (status > 0) ? LL_MERGE_CONFLICT : status;\n      +\treturn ret;\n       }\n       \n     @@ rerere.c: static int try_merge(struct index_state *istate,\n       \t\t */\n       \t\tret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n       \t\t\t       istate, NULL);\n     -+\t\tif (ret == LL_MERGE_BINARY_CONFLICT)\n     -+\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n     -+\t\t\t\tpath, \"\", \"\");\n      +\t}\n       \n       \tfree(base.ptr);\n  4:  a02845f12db =  4:  e8cf1626960 merge-ort: capture and print ll-merge warnings in our preferred fashion\n  5:  000933c5d7f =  5:  4d1848c8a29 merge-ort: mark a few more conflict messages as omittable\n  6:  887e46435c0 !  6:  81e736b847e merge-ort: format messages slightly different for use in headers\n     @@ Commit message\n          headers instead...but for that to work, we need for any multiline\n          messages to replace newlines with both a newline and a space.  Add a new\n          flag to signal when we want these messages modified in such a fashion,\n     -    and use it in path_msg() to modify these messages this way.\n     +    and use it in path_msg() to modify these messages this way.  Also, allow\n     +    a special prefix to be specified for these headers.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     @@ merge-ort.c: static void path_msg(struct merge_options *opt,\n      +\tif (opt->record_conflict_msgs_as_headers) {\n      +\t\tint i_sb = 0, i_tmp = 0;\n      +\n     ++\t\t/* Start with the specified prefix */\n     ++\t\tif (opt->msg_header_prefix)\n     ++\t\t\tstrbuf_addf(sb, \"%s \", opt->msg_header_prefix);\n     ++\n      +\t\t/* Copy tmp to sb, adding spaces after newlines */\n     -+\t\tstrbuf_grow(sb, 2*tmp.len); /* more than sufficient */\n     ++\t\tstrbuf_grow(sb, sb->len + 2*tmp.len); /* more than sufficient */\n      +\t\tfor (; i_tmp < tmp.len; i_tmp++, i_sb++) {\n      +\t\t\t/* Copy next character from tmp to sb */\n      +\t\t\tsb->buf[sb->len + i_sb] = tmp.buf[i_tmp];\n     @@ merge-ort.c: static void path_msg(struct merge_options *opt,\n      +\t\tsb->len += i_sb;\n      +\t\tsb->buf[sb->len] = '\\0';\n      +\n     -+\t\t/* Clean up tmp */\n      +\t\tstrbuf_release(&tmp);\n      +\t}\n      +\n     @@ merge-ort.c: void merge_switch_to_result(struct merge_options *opt,\n       \t\ttrace2_region_enter(\"merge\", \"display messages\", opt->repo);\n       \n       \t\t/* Hack to pre-allocate olist to the desired size */\n     +@@ merge-ort.c: static void merge_start(struct merge_options *opt, struct merge_result *result)\n     + \tassert(opt->recursive_variant >= MERGE_VARIANT_NORMAL &&\n     + \t       opt->recursive_variant <= MERGE_VARIANT_THEIRS);\n     + \n     ++\tif (opt->msg_header_prefix)\n     ++\t\tassert(opt->record_conflict_msgs_as_headers);\n     ++\n     + \t/*\n     + \t * detect_renames, verbosity, buffer_output, and obuf are ignored\n     + \t * fields that were used by \"recursive\" rather than \"ort\" -- but\n      \n       ## merge-recursive.c ##\n      @@ merge-recursive.c: static int merge_start(struct merge_options *opt, struct tree *head)\n     @@ merge-recursive.c: static int merge_start(struct merge_options *opt, struct tree\n       \n      +\t/* Not supported; option specific to merge-ort */\n      +\tassert(!opt->record_conflict_msgs_as_headers);\n     ++\tassert(!opt->msg_header_prefix);\n      +\n       \t/* Sanity check on repo state; index must match head */\n       \tif (repo_index_has_changes(opt->repo, head, &sb)) {\n     @@ merge-recursive.h: struct merge_options {\n       \tconst char *subtree_shift;\n       \tunsigned renormalize : 1;\n      +\tunsigned record_conflict_msgs_as_headers : 1;\n     ++\tconst char *msg_header_prefix;\n       \n       \t/* internal fields used by the implementation */\n       \tstruct merge_options_internal *priv;\n  7:  e9470651303 =  7:  5000a94aa98 diff: add ability to insert additional headers for paths\n  8:  4cc53c55a6e !  8:  78ec1f44e4e show, log: include conflict/warning messages in --remerge-diff headers\n     @@ log-tree.c: static int do_remerge_diff(struct rev_info *opt,\n       \tinit_merge_options(&o, the_repository);\n       \to.show_rename_progress = 0;\n      +\to.record_conflict_msgs_as_headers = 1;\n     ++\to.msg_header_prefix = \"remerge\";\n       \n       \tctx.abbrev = DEFAULT_ABBREV;\n       \tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n     @@ t/t4069-remerge-diff.sh: test_expect_success 'remerge-diff with both a resolved\n       \tgit log -1 --oneline ab_resolution >tmp &&\n       \tcat <<-EOF >>tmp &&\n       \tdiff --git a/numbers b/numbers\n     -+\tCONFLICT (content): Merge conflict in numbers\n     ++\tremerge CONFLICT (content): Merge conflict in numbers\n       \tindex a1fb731..6875544 100644\n       \t--- a/numbers\n       \t+++ b/numbers\n     @@ t/t4069-remerge-diff.sh: test_expect_success 'remerge-diff with both a resolved\n      +\tsimilarity index 100%\n      +\trename from file_or_directory~HASH (side1)\n      +\trename to wanted_content\n     -+\tCONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n     ++\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n      +\tdiff --git a/letters b/letters\n     -+\tCONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n     ++\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n      +\tdiff --git a/letters_side2 b/letters_side2\n      +\tdeleted file mode 100644\n      +\tindex b236ae5..0000000\n     @@ t/t4069-remerge-diff.sh: test_expect_success 'remerge-diff with both a resolved\n      +\t-h\n      +\t-i\n      +\tdiff --git a/numbers b/numbers\n     -+\tCONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n     ++\tremerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n      +\tEOF\n      +\t# We still have some sha1 hashes above; rip them out so test works\n      +\t# with sha256\n  -:  ----------- >  9:  64b44ee84f3 merge-ort: mark conflict/warning messages from inner merges as omittable\n\n-- \ngitgitgadget\n"},{"id":"445253","messageId":"d57ae218cf9eaee0b66db299ee1bba9b488b69b1.1640907369.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v3 1/9] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:01Z","receivedAt":"2021-12-30T23:36: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\nWhen this option is specified, we remerge all (two parent) merge commits\nand diff the actual merge commit to the automatically created version,\nin order to show how users removed conflict markers, resolved the\ndifferent conflict versions, and potentially added new changes outside\nof conflict regions in order to resolve semantic merge problems (or,\npossibly, just to hide other random changes).\n\nThis capability works by creating a temporary object directory and\nmarking it as the primary object store.  This makes it so that any blobs\nor trees created during the automatic merge are easily removable\nafterwards by just deleting all objects from the temporary object\ndirectory.\n\nThere are a few ways that this implementation is suboptimal:\n  * `log --remerge-diff` becomes slow, because the temporary object\n    directory can fill with many loose objects while running\n  * the log output can be muddied with misplaced \"warning: cannot merge\n    binary files\" messages, since ll-merge.c unconditionally writes those\n    messages to stderr while running instead of allowing callers to\n    manage them.\n  * important conflict and warning messages are simply dropped; thus for\n    conflicts like modify/delete or rename/rename or file/directory which\n    are not representable with content conflict markers, there may be no\n    way for a user of --remerge-diff to know that there had been a\n    conflict which was resolved (and which possibly motivated other\n    changes in the merge commit).\n  * when fixing the previous issue, note that some unimportant conflict\n    and warning messages might start being included.  We should instead\n    make sure these remain dropped.\nSubsequent commits will address these issues.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n Documentation/diff-options.txt | 10 +++-\n builtin/log.c                  | 14 ++++++\n diff-merges.c                  | 12 +++++\n log-tree.c                     | 59 ++++++++++++++++++++++++\n revision.h                     |  3 +-\n t/t4069-remerge-diff.sh        | 84 ++++++++++++++++++++++++++++++++++\n 6 files changed, 180 insertions(+), 2 deletions(-)\n create mode 100755 t/t4069-remerge-diff.sh\n\ndiff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\nindex c89d530d3d1..6b8175defe6 100644\n--- a/Documentation/diff-options.txt\n+++ b/Documentation/diff-options.txt\n@@ -34,7 +34,7 @@ endif::git-diff[]\n endif::git-format-patch[]\n \n ifdef::git-log[]\n---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc)::\n+--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r)::\n --no-diff-merges::\n \tSpecify diff format to be used for merge commits. Default is\n \t{diff-merges-default} unless `--first-parent` is in use, in which case\n@@ -64,6 +64,14 @@ ifdef::git-log[]\n \teach of the parents. Separate log entry and diff is generated\n \tfor each parent.\n +\n+--diff-merges=remerge:::\n+--diff-merges=r:::\n+--remerge-diff:::\n+\tWith this option, two-parent merge commits are remerged to\n+\tcreate a temporary tree object -- potentially containing files\n+\twith conflict markers and such.  A diff is then shown between\n+\tthat temporary tree and the actual merge commit.\n++\n --diff-merges=combined:::\n --diff-merges=c:::\n -c:::\ndiff --git a/builtin/log.c b/builtin/log.c\nindex f75d87e8d7f..d053418fddd 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -35,6 +35,7 @@\n #include \"repository.h\"\n #include \"commit-reach.h\"\n #include \"range-diff.h\"\n+#include \"tmp-objdir.h\"\n \n #define MAIL_DEFAULT_WRAP 72\n #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n@@ -406,6 +407,14 @@ static int cmd_log_walk(struct rev_info *rev)\n \tstruct commit *commit;\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n+\tstruct tmp_objdir *remerge_objdir = NULL;\n+\n+\tif (rev->remerge_diff) {\n+\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\tif (!remerge_objdir)\n+\t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n+\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n+\t}\n \n \tif (rev->early_output)\n \t\tsetup_early_output();\n@@ -449,6 +458,9 @@ static int cmd_log_walk(struct rev_info *rev)\n \trev->diffopt.no_free = 0;\n \tdiff_free(&rev->diffopt);\n \n+\tif (rev->remerge_diff)\n+\t\ttmp_objdir_destroy(remerge_objdir);\n+\n \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n \t    rev->diffopt.flags.check_failed) {\n \t\treturn 02;\n@@ -1943,6 +1955,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tdie(_(\"--name-status does not make sense\"));\n \tif (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF)\n \t\tdie(_(\"--check does not make sense\"));\n+\tif (rev.remerge_diff)\n+\t\tdie(_(\"--remerge-diff does not make sense\"));\n \n \tif (!use_patch_format &&\n \t\t(!rev.diffopt.output_format ||\ndiff --git a/diff-merges.c b/diff-merges.c\nindex 5060ccd890b..0af4b3f9191 100644\n--- a/diff-merges.c\n+++ b/diff-merges.c\n@@ -17,6 +17,7 @@ static void suppress(struct rev_info *revs)\n \trevs->combined_all_paths = 0;\n \trevs->merges_imply_patch = 0;\n \trevs->merges_need_diff = 0;\n+\trevs->remerge_diff = 0;\n }\n \n static void set_separate(struct rev_info *revs)\n@@ -45,6 +46,12 @@ static void set_dense_combined(struct rev_info *revs)\n \trevs->dense_combined_merges = 1;\n }\n \n+static void set_remerge_diff(struct rev_info *revs)\n+{\n+\tsuppress(revs);\n+\trevs->remerge_diff = 1;\n+}\n+\n static diff_merges_setup_func_t func_by_opt(const char *optarg)\n {\n \tif (!strcmp(optarg, \"off\") || !strcmp(optarg, \"none\"))\n@@ -57,6 +64,8 @@ static diff_merges_setup_func_t func_by_opt(const char *optarg)\n \t\treturn set_combined;\n \telse if (!strcmp(optarg, \"cc\") || !strcmp(optarg, \"dense-combined\"))\n \t\treturn set_dense_combined;\n+\telse if (!strcmp(optarg, \"r\") || !strcmp(optarg, \"remerge\"))\n+\t\treturn set_remerge_diff;\n \telse if (!strcmp(optarg, \"m\") || !strcmp(optarg, \"on\"))\n \t\treturn set_to_default;\n \treturn NULL;\n@@ -110,6 +119,9 @@ int diff_merges_parse_opts(struct rev_info *revs, const char **argv)\n \t} else if (!strcmp(arg, \"--cc\")) {\n \t\tset_dense_combined(revs);\n \t\trevs->merges_imply_patch = 1;\n+\t} else if (!strcmp(arg, \"--remerge-diff\")) {\n+\t\tset_remerge_diff(revs);\n+\t\trevs->merges_imply_patch = 1;\n \t} else if (!strcmp(arg, \"--no-diff-merges\")) {\n \t\tsuppress(revs);\n \t} else if (!strcmp(arg, \"--combined-all-paths\")) {\ndiff --git a/log-tree.c b/log-tree.c\nindex 644893fd8cf..84ed864fc81 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -1,4 +1,5 @@\n #include \"cache.h\"\n+#include \"commit-reach.h\"\n #include \"config.h\"\n #include \"diff.h\"\n #include \"object-store.h\"\n@@ -7,6 +8,7 @@\n #include \"tag.h\"\n #include \"graph.h\"\n #include \"log-tree.h\"\n+#include \"merge-ort.h\"\n #include \"reflog-walk.h\"\n #include \"refs.h\"\n #include \"string-list.h\"\n@@ -902,6 +904,51 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n \treturn !opt->loginfo;\n }\n \n+static int do_remerge_diff(struct rev_info *opt,\n+\t\t\t   struct commit_list *parents,\n+\t\t\t   struct object_id *oid,\n+\t\t\t   struct commit *commit)\n+{\n+\tstruct merge_options o;\n+\tstruct commit_list *bases;\n+\tstruct merge_result res = {0};\n+\tstruct pretty_print_context ctx = {0};\n+\tstruct commit *parent1 = parents->item;\n+\tstruct commit *parent2 = parents->next->item;\n+\tstruct strbuf parent1_desc = STRBUF_INIT;\n+\tstruct strbuf parent2_desc = STRBUF_INIT;\n+\n+\t/* Setup merge options */\n+\tinit_merge_options(&o, the_repository);\n+\to.show_rename_progress = 0;\n+\n+\tctx.abbrev = DEFAULT_ABBREV;\n+\tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n+\tformat_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n+\to.branch1 = parent1_desc.buf;\n+\to.branch2 = parent2_desc.buf;\n+\n+\t/* Parse the relevant commits and get the merge bases */\n+\tparse_commit_or_die(parent1);\n+\tparse_commit_or_die(parent2);\n+\tbases = get_merge_bases(parent1, parent2);\n+\n+\t/* Re-merge the parents */\n+\tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n+\n+\t/* Show the diff */\n+\tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n+\tlog_tree_diff_flush(opt);\n+\n+\t/* Cleanup */\n+\tstrbuf_release(&parent1_desc);\n+\tstrbuf_release(&parent2_desc);\n+\tmerge_finalize(&o, &res);\n+\t/* TODO: clean up the temporary object directory */\n+\n+\treturn !opt->loginfo;\n+}\n+\n /*\n  * Show the diff of a commit.\n  *\n@@ -936,6 +983,18 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log\n \t}\n \n \tif (is_merge) {\n+\t\tint octopus = (parents->next->next != NULL);\n+\n+\t\tif (opt->remerge_diff) {\n+\t\t\tif (octopus) {\n+\t\t\t\tshow_log(opt);\n+\t\t\t\tfprintf(opt->diffopt.file,\n+\t\t\t\t\t\"diff: warning: Skipping remerge-diff \"\n+\t\t\t\t\t\"for octopus merges.\\n\");\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\treturn do_remerge_diff(opt, parents, oid, commit);\n+\t\t}\n \t\tif (opt->combine_merges)\n \t\t\treturn do_diff_combined(opt, commit);\n \t\tif (opt->separate_merges) {\ndiff --git a/revision.h b/revision.h\nindex 5578bb4720a..13178e6b8f3 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -195,7 +195,8 @@ struct rev_info {\n \t\t\tcombine_merges:1,\n \t\t\tcombined_all_paths:1,\n \t\t\tdense_combined_merges:1,\n-\t\t\tfirst_parent_merges:1;\n+\t\t\tfirst_parent_merges:1,\n+\t\t\tremerge_diff:1;\n \n \t/* Format info */\n \tint\t\tshow_notes;\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nnew file mode 100755\nindex 00000000000..1b32028e897\n--- /dev/null\n+++ b/t/t4069-remerge-diff.sh\n@@ -0,0 +1,84 @@\n+#!/bin/sh\n+\n+test_description='remerge-diff handling'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup basic merges' '\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m base &&\n+\n+\tgit branch feature_a &&\n+\tgit branch feature_b &&\n+\tgit branch feature_c &&\n+\n+\tgit branch ab_resolution &&\n+\tgit branch bc_resolution &&\n+\n+\tgit checkout feature_a &&\n+\ttest_write_lines 1 2 three 4 5 6 7 eight 9 >numbers &&\n+\tgit commit -a -m change_a &&\n+\n+\tgit checkout feature_b &&\n+\ttest_write_lines 1 2 tres 4 5 6 7 8 9 >numbers &&\n+\tgit commit -a -m change_b &&\n+\n+\tgit checkout feature_c &&\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&\n+\tgit commit -a -m change_c &&\n+\n+\tgit checkout bc_resolution &&\n+\tgit merge --ff-only feature_b &&\n+\t# no conflict\n+\tgit merge feature_c &&\n+\n+\tgit checkout ab_resolution &&\n+\tgit merge --ff-only feature_a &&\n+\t# conflicts!\n+\ttest_must_fail git merge feature_b &&\n+\t# Resolve conflict...and make another change elsewhere\n+\ttest_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers &&\n+\tgit add numbers &&\n+\tgit merge --continue\n+'\n+\n+test_expect_success 'remerge-diff on a clean merge' '\n+\tgit log -1 --oneline bc_resolution >expect &&\n+\tgit show --oneline --remerge-diff bc_resolution >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' '\n+\tgit log -1 --oneline ab_resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/numbers b/numbers\n+\tindex a1fb731..6875544 100644\n+\t--- a/numbers\n+\t+++ b/numbers\n+\t@@ -1,13 +1,9 @@\n+\t 1\n+\t 2\n+\t-<<<<<<< b0ed5cb (change_a)\n+\t-three\n+\t-=======\n+\t-tres\n+\t->>>>>>> 6cd3f82 (change_b)\n+\t+drei\n+\t 4\n+\t 5\n+\t 6\n+\t 7\n+\t-eight\n+\t+acht\n+\t 9\n+\tEOF\n+\t# Hashes above are sha1; rip them out so test works with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff ab_resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"445254","messageId":"798625b53f2ee4860364e3898c3a1612f7b4e100.1640907369.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v3 2/9] log: clean unneeded objects during `log --remerge-diff`","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:02Z","receivedAt":"2021-12-30T23:36:18Z","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 --remerge-diff option will need to create new blobs and trees\nrepresenting the \"automatic merge\" state.  If one is traversing a\nlong project history, one can easily get hundreds of thousands of\nloose objects generated during `log --remerge-diff`.  However, none of\nthose loose objects are needed after we have completed our diff\noperation; they can be summarily deleted.\n\nAdd a new helper function to tmp_objdir to discard all the contained\nobjects, and call it after each merge is handled.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/log.c | 13 +++++++------\n log-tree.c    |  8 +++++++-\n revision.h    |  3 +++\n tmp-objdir.c  |  5 +++++\n tmp-objdir.h  |  6 ++++++\n 5 files changed, 28 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex d053418fddd..e6a080df914 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -407,13 +407,12 @@ static int cmd_log_walk(struct rev_info *rev)\n \tstruct commit *commit;\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n-\tstruct tmp_objdir *remerge_objdir = NULL;\n \n \tif (rev->remerge_diff) {\n-\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n-\t\tif (!remerge_objdir)\n+\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\tif (!rev->remerge_objdir)\n \t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n-\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n+\t\ttmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n \t}\n \n \tif (rev->early_output)\n@@ -458,8 +457,10 @@ static int cmd_log_walk(struct rev_info *rev)\n \trev->diffopt.no_free = 0;\n \tdiff_free(&rev->diffopt);\n \n-\tif (rev->remerge_diff)\n-\t\ttmp_objdir_destroy(remerge_objdir);\n+\tif (rev->remerge_diff) {\n+\t\ttmp_objdir_destroy(rev->remerge_objdir);\n+\t\trev->remerge_objdir = NULL;\n+\t}\n \n \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n \t    rev->diffopt.flags.check_failed) {\ndiff --git a/log-tree.c b/log-tree.c\nindex 84ed864fc81..d4655b63d75 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -4,6 +4,7 @@\n #include \"diff.h\"\n #include \"object-store.h\"\n #include \"repository.h\"\n+#include \"tmp-objdir.h\"\n #include \"commit.h\"\n #include \"tag.h\"\n #include \"graph.h\"\n@@ -944,7 +945,12 @@ static int do_remerge_diff(struct rev_info *opt,\n \tstrbuf_release(&parent1_desc);\n \tstrbuf_release(&parent2_desc);\n \tmerge_finalize(&o, &res);\n-\t/* TODO: clean up the temporary object directory */\n+\n+\t/* Clean up the contents of the temporary object directory */\n+\tif (opt->remerge_objdir)\n+\t\ttmp_objdir_discard_objects(opt->remerge_objdir);\n+\telse\n+\t\tBUG(\"unable to remove temporary object directory\");\n \n \treturn !opt->loginfo;\n }\ndiff --git a/revision.h b/revision.h\nindex 13178e6b8f3..44efce3f410 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -318,6 +318,9 @@ struct rev_info {\n \n \t/* misc. flags related to '--no-kept-objects' */\n \tunsigned keep_pack_cache_flags;\n+\n+\t/* Location where temporary objects for remerge-diff are written. */\n+\tstruct tmp_objdir *remerge_objdir;\n };\n \n int ref_excluded(struct string_list *, const char *path);\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex 3d38eeab66b..adf6033549e 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -79,6 +79,11 @@ static void remove_tmp_objdir_on_signal(int signo)\n \traise(signo);\n }\n \n+void tmp_objdir_discard_objects(struct tmp_objdir *t)\n+{\n+\tremove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n+}\n+\n /*\n  * These env_* functions are for setting up the child environment; the\n  * \"replace\" variant overrides the value of any existing variable with that\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex cda5ec76778..76efc7edee5 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -46,6 +46,12 @@ int tmp_objdir_migrate(struct tmp_objdir *);\n  */\n int tmp_objdir_destroy(struct tmp_objdir *);\n \n+/*\n+ * Remove all objects from the temporary object directory, while leaving it\n+ * around so more objects can be added.\n+ */\n+void tmp_objdir_discard_objects(struct tmp_objdir *);\n+\n /*\n  * Add the temporary object directory as an alternate object store in the\n  * current process.\n-- \ngitgitgadget\n\n"},{"id":"445255","messageId":"b952f674df13a1c3e4bffa823490f905e862ef7c.1640907369.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v3 3/9] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:03Z","receivedAt":"2021-12-30T23:36:19Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince some callers may want to send warning messages to somewhere other\nthan stdout/stderr, stop printing \"warning: Cannot merge binary files\"\nfrom ll-merge and instead modify the return status of ll_merge() to\nindicate when a merge of binary files has occurred.  Message printing\nprobably does not belong in a \"low-level merge\" anyway.\n\nThis commit continues printing the message as-is, just from the callers\ninstead of within ll_merge().  Future changes will start handling the\nmessage differently in the merge-ort codepath.\n\nThere was one special case here: the callers in rerere.c do NOT check\nfor and print such a message; since those code paths explicitly skip\nover binary files, there is no reason to check for a return status of\nLL_MERGE_BINARY_CONFLICT or print the related message.\n\nNote that my methodology included first modifying ll_merge() to return\na struct, so that the compiler would catch all the callers for me and\nensure I had modified all of them.  After modifying all of them, I then\nchanged the struct to an enum.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n apply.c            |  5 ++++-\n builtin/checkout.c | 12 ++++++++----\n ll-merge.c         | 40 ++++++++++++++++++++++------------------\n ll-merge.h         |  9 ++++++++-\n merge-blobs.c      |  5 ++++-\n merge-ort.c        |  5 ++++-\n merge-recursive.c  |  5 ++++-\n notes-merge.c      |  5 ++++-\n rerere.c           |  9 +++++----\n 9 files changed, 63 insertions(+), 32 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex 43a0aebf4ee..8079395755f 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -3492,7 +3492,7 @@ static int three_way_merge(struct apply_state *state,\n {\n \tmmfile_t base_file, our_file, their_file;\n \tmmbuffer_t result = { NULL };\n-\tint status;\n+\tenum ll_merge_result status;\n \n \t/* resolve trivial cases first */\n \tif (oideq(base, ours))\n@@ -3509,6 +3509,9 @@ static int three_way_merge(struct apply_state *state,\n \t\t\t  &their_file, \"theirs\",\n \t\t\t  state->repo->index,\n \t\t\t  NULL);\n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \"ours\", \"theirs\");\n \tfree(base_file.ptr);\n \tfree(our_file.ptr);\n \tfree(their_file.ptr);\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex cbf73b8c9f6..3a559d69303 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -237,6 +237,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tstruct cache_entry *ce = active_cache[pos];\n \tconst char *path = ce->name;\n \tmmfile_t ancestor, ours, theirs;\n+\tenum ll_merge_result merge_status;\n \tint status;\n \tstruct object_id oid;\n \tmmbuffer_t result_buf;\n@@ -267,13 +268,16 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tmemset(&ll_opts, 0, sizeof(ll_opts));\n \tgit_config_get_bool(\"merge.renormalize\", &renormalize);\n \tll_opts.renormalize = renormalize;\n-\tstatus = ll_merge(&result_buf, path, &ancestor, \"base\",\n-\t\t\t  &ours, \"ours\", &theirs, \"theirs\",\n-\t\t\t  state->istate, &ll_opts);\n+\tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n+\t\t\t\t&ours, \"ours\", &theirs, \"theirs\",\n+\t\t\t\tstate->istate, &ll_opts);\n \tfree(ancestor.ptr);\n \tfree(ours.ptr);\n \tfree(theirs.ptr);\n-\tif (status < 0 || !result_buf.ptr) {\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \"ours\", \"theirs\");\n+\tif (merge_status < 0 || !result_buf.ptr) {\n \t\tfree(result_buf.ptr);\n \t\treturn error(_(\"path '%s': cannot merge\"), path);\n \t}\ndiff --git a/ll-merge.c b/ll-merge.c\nindex 261657578c7..a937cec59a6 100644\n--- a/ll-merge.c\n+++ b/ll-merge.c\n@@ -14,7 +14,7 @@\n \n struct ll_merge_driver;\n \n-typedef int (*ll_merge_fn)(const struct ll_merge_driver *,\n+typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -49,7 +49,7 @@ void reset_merge_attributes(void)\n /*\n  * Built-in low-levels\n  */\n-static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -58,6 +58,7 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   const struct ll_merge_options *opts,\n \t\t\t   int marker_size)\n {\n+\tenum ll_merge_result ret;\n \tmmfile_t *stolen;\n \tassert(opts);\n \n@@ -68,16 +69,19 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t */\n \tif (opts->virtual_ancestor) {\n \t\tstolen = orig;\n+\t\tret = LL_MERGE_OK;\n \t} else {\n \t\tswitch (opts->variant) {\n \t\tdefault:\n-\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\t\tpath, name1, name2);\n-\t\t\t/* fallthru */\n+\t\t\tret = LL_MERGE_BINARY_CONFLICT;\n+\t\t\tstolen = src1;\n+\t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_OURS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src1;\n \t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_THEIRS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src2;\n \t\t\tbreak;\n \t\t}\n@@ -87,16 +91,10 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \tresult->size = stolen->size;\n \tstolen->ptr = NULL;\n \n-\t/*\n-\t * With -Xtheirs or -Xours, we have cleanly merged;\n-\t * otherwise we got a conflict.\n-\t */\n-\treturn opts->variant == XDL_MERGE_FAVOR_OURS ||\n-\t       opts->variant == XDL_MERGE_FAVOR_THEIRS ?\n-\t       0 : 1;\n+\treturn ret;\n }\n \n-static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -105,7 +103,9 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tconst struct ll_merge_options *opts,\n \t\t\tint marker_size)\n {\n+\tenum ll_merge_result ret;\n \txmparam_t xmp;\n+\tint status;\n \tassert(opts);\n \n \tif (orig->size > MAX_XDIFF_SIZE ||\n@@ -133,10 +133,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \txmp.ancestor = orig_name;\n \txmp.file1 = name1;\n \txmp.file2 = name2;\n-\treturn xdl_merge(orig, src1, src2, &xmp, result);\n+\tstatus = xdl_merge(orig, src1, src2, &xmp, result);\n+\tret = (status > 0) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n-static int ll_union_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_union_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t  mmbuffer_t *result,\n \t\t\t  const char *path,\n \t\t\t  mmfile_t *orig, const char *orig_name,\n@@ -178,7 +180,7 @@ static void create_temp(mmfile_t *src, char *path, size_t len)\n /*\n  * User defined low-level merge driver support.\n  */\n-static int ll_ext_merge(const struct ll_merge_driver *fn,\n+static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -194,6 +196,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \tconst char *args[] = { NULL, NULL };\n \tint status, fd, i;\n \tstruct stat st;\n+\tenum ll_merge_result ret;\n \tassert(opts);\n \n \tsq_quote_buf(&path_sq, path);\n@@ -236,7 +239,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \t\tunlink_or_warn(temp[i]);\n \tstrbuf_release(&cmd);\n \tstrbuf_release(&path_sq);\n-\treturn status;\n+\tret = (status > 0) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n /*\n@@ -362,7 +366,7 @@ static void normalize_file(mmfile_t *mm, const char *path, struct index_state *i\n \t}\n }\n \n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/ll-merge.h b/ll-merge.h\nindex aceb1b24132..e4a20e81a3a 100644\n--- a/ll-merge.h\n+++ b/ll-merge.h\n@@ -82,13 +82,20 @@ struct ll_merge_options {\n \tlong xdl_opts;\n };\n \n+enum ll_merge_result {\n+\tLL_MERGE_ERROR = -1,\n+\tLL_MERGE_OK = 0,\n+\tLL_MERGE_CONFLICT,\n+\tLL_MERGE_BINARY_CONFLICT,\n+};\n+\n /**\n  * Perform a three-way single-file merge in core.  This is a thin wrapper\n  * around `xdl_merge` that takes the path and any merge backend specified in\n  * `.gitattributes` or `.git/info/attributes` into account.\n  * Returns 0 for a clean merge.\n  */\n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/merge-blobs.c b/merge-blobs.c\nindex ee0a0e90c94..8138090f81c 100644\n--- a/merge-blobs.c\n+++ b/merge-blobs.c\n@@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\t mmfile_t *their,\n \t\t\t\t unsigned long *size)\n {\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \tmmbuffer_t res;\n \n \t/*\n@@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\tistate, NULL);\n \tif (merge_status < 0)\n \t\treturn NULL;\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \".our\", \".their\");\n \n \t*size = res.size;\n \treturn res.ptr;\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 0342f104836..c24da2ba3cb 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1743,7 +1743,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tif (!opt->priv->attr_index.initialized)\n \t\tinitialize_attr_index(opt);\n@@ -1787,6 +1787,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex d9457797dbb..bc73c52dd84 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tll_opts.renormalize = opt->renormalize;\n \tll_opts.extra_marker_size = extra_marker_size;\n@@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, a->path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\topt->repo->index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\ta->path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/notes-merge.c b/notes-merge.c\nindex b4a3a903e86..01d596920ea 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n {\n \tmmbuffer_t result_buf;\n \tmmfile_t base, local, remote;\n-\tint status;\n+\tenum ll_merge_result status;\n \n \tread_mmblob(&base, &p->base);\n \tread_mmblob(&local, &p->local);\n@@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n \tfree(local.ptr);\n \tfree(remote.ptr);\n \n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\toid_to_hex(&p->obj), o->local_ref, o->remote_ref);\n \tif ((status < 0) || !result_buf.ptr)\n \t\tdie(\"Failed to execute internal merge\");\n \ndiff --git a/rerere.c b/rerere.c\nindex d83d58df4fb..d26627c5932 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -609,19 +609,20 @@ static int try_merge(struct index_state *istate,\n \t\t     const struct rerere_id *id, const char *path,\n \t\t     mmfile_t *cur, mmbuffer_t *result)\n {\n-\tint ret;\n+\tenum ll_merge_result ret;\n \tmmfile_t base = {NULL, 0}, other = {NULL, 0};\n \n \tif (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n-\t    read_mmfile(&other, rerere_path(id, \"postimage\")))\n-\t\tret = 1;\n-\telse\n+\t    read_mmfile(&other, rerere_path(id, \"postimage\"))) {\n+\t\tret = LL_MERGE_CONFLICT;\n+\t} else {\n \t\t/*\n \t\t * A three-way merge. Note that this honors user-customizable\n \t\t * low-level merge driver settings.\n \t\t */\n \t\tret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n \t\t\t       istate, NULL);\n+\t}\n \n \tfree(base.ptr);\n \tfree(other.ptr);\n-- \ngitgitgadget\n\n"},{"id":"445256","messageId":"e8cf1626960774d524367c17db01e2a29eac7759.1640907369.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v3 4/9] merge-ort: capture and print ll-merge warnings in our preferred fashion","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:04Z","receivedAt":"2021-12-30T23:36:21Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nInstead of immediately printing ll-merge warnings to stderr, we save\nthem in our output strbuf.  Besides allowing us to move these warnings\nto a special file for --remerge-diff, this has two other benefits for\nregular merges done by merge-ort:\n\n  * The deferral of messages ensures we can print all messages about\n    any given path together (merge-recursive was known to sometimes\n    intersperse messages about other paths, particularly when renames\n    were involved).\n\n  * The deferral of messages means we can avoid printing spurious\n    conflict messages when we just end up aborting due to local user\n    modifications in the way.  (In contrast to merge-recursive.c which\n    prematurely checks for local modifications in the way via\n    unpack_trees() and gets the check wrong both in terms of false\n    positives and false negatives relative to renames, merge-ort does\n    not perform the local modifications in the way check until the\n    checkout() step after the full merge has been computed.)\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c                | 5 +++--\n t/t6404-recursive-merge.sh | 9 +++++++--\n t/t6406-merge-attr.sh      | 9 +++++++--\n 3 files changed, 17 insertions(+), 6 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex c24da2ba3cb..a18f47e23c5 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1788,8 +1788,9 @@ static int merge_3way(struct merge_options *opt,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n \tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n-\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\tpath, name1, name2);\n+\t\tpath_msg(opt, path, 0,\n+\t\t\t \"warning: Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\t path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/t/t6404-recursive-merge.sh b/t/t6404-recursive-merge.sh\nindex eaf48e941e2..b8735c6db4d 100755\n--- a/t/t6404-recursive-merge.sh\n+++ b/t/t6404-recursive-merge.sh\n@@ -108,8 +108,13 @@ test_expect_success 'refuse to merge binary files' '\n \tprintf \"\\0\\0\" >binary-file &&\n \tgit add binary-file &&\n \tgit commit -m binary2 &&\n-\ttest_must_fail git merge F >merge.out 2>merge.err &&\n-\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge.err\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge F >merge_output\n+\telse\n+\t\ttest_must_fail git merge F 2>merge_output\n+\tfi &&\n+\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge_output\n '\n \n test_expect_success 'mark rename/delete as unmerged' '\ndiff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh\nindex 84946458371..c41584eb33e 100755\n--- a/t/t6406-merge-attr.sh\n+++ b/t/t6406-merge-attr.sh\n@@ -221,8 +221,13 @@ test_expect_success 'binary files with union attribute' '\n \tprintf \"two\\0\" >bin.txt &&\n \tgit commit -am two &&\n \n-\ttest_must_fail git merge bin-main 2>stderr &&\n-\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" stderr\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge bin-main >output\n+\telse\n+\t\ttest_must_fail git merge bin-main 2>output\n+\tfi &&\n+\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" output\n '\n \n test_done\n-- \ngitgitgadget\n\n"},{"id":"445257","messageId":"4d1848c8a29b9cbbc62f9f928f94f0cf931106e1.1640907369.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v3 5/9] merge-ort: mark a few more conflict messages as omittable","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:05Z","receivedAt":"2021-12-30T23:36:21Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\npath_msg() has the ability to mark messages as omittable, designed for\nremerge-diff where we'll instead be showing conflict messages as diff\nheaders for a subsequent diff.  While all these messages are very useful\nwhen trying to create a merge initially, early use with the\n--remerge-diff feature (the only user of this omittable conflict message\ncapability), suggests that the particular messages marked in this commit\nare just noise when trying to see what changes users made to create a\nmerge commit.  Mark them as omittable.\n\nNote that there were already a few messages marked as omittable in\nmerge-ort when doing a remerge-diff, because the development of\n--remerge-diff preceded the upstreaming of merge-ort and I was trying to\nensure merge-ort could handle all the necessary requirements.  See\ncommit c5a6f65527 (\"merge-ort: add modify/delete handling and delayed\noutput processing\", 2020-12-03) for the initial details.  For some\nexamples of already-marked-as-omittable messages, see either\n\"Auto-merging <path>\" or some of the submodule update hints.  This\ncommit just adds two more messages that should also be omittable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex a18f47e23c5..998e92ec593 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -2420,7 +2420,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t */\n \t\tci->path_conflict = 1;\n \t\tif (pair->status == 'A')\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s added in %s \"\n \t\t\t\t   \"inside a directory that was renamed in %s, \"\n \t\t\t\t   \"suggesting it should perhaps be moved to \"\n@@ -2428,7 +2428,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t\t\t old_path, branch_with_new_path,\n \t\t\t\t branch_with_dir_rename, new_path);\n \t\telse\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s renamed to %s \"\n \t\t\t\t   \"in %s, inside a directory that was renamed \"\n \t\t\t\t   \"in %s, suggesting it should perhaps be \"\n-- \ngitgitgadget\n\n"},{"id":"445258","messageId":"81e736b847e8765d05af2704226254c4c667e142.1640907369.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v3 6/9] merge-ort: format messages slightly different for use in headers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:06Z","receivedAt":"2021-12-30T23:36:25Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen users run\n    git show --remerge-diff $MERGE_COMMIT\nor\n    git log -p --remerge-diff ...\nstdout is not an appropriate location to dump conflict messages, but we\ndo want to provide them to users.  We will include them in the diff\nheaders instead...but for that to work, we need for any multiline\nmessages to replace newlines with both a newline and a space.  Add a new\nflag to signal when we want these messages modified in such a fashion,\nand use it in path_msg() to modify these messages this way.  Also, allow\na special prefix to be specified for these headers.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c       | 42 ++++++++++++++++++++++++++++++++++++++++--\n merge-recursive.c |  4 ++++\n merge-recursive.h |  2 ++\n 3 files changed, 46 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 998e92ec593..481305d2bcf 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -634,17 +634,49 @@ static void path_msg(struct merge_options *opt,\n \t\t     const char *fmt, ...)\n {\n \tva_list ap;\n-\tstruct strbuf *sb = strmap_get(&opt->priv->output, path);\n+\tstruct strbuf *sb, *dest;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\n+\tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n+\t\treturn; /* Do not record mere hints in tree */\n+\tsb = strmap_get(&opt->priv->output, path);\n \tif (!sb) {\n \t\tsb = xmalloc(sizeof(*sb));\n \t\tstrbuf_init(sb, 0);\n \t\tstrmap_put(&opt->priv->output, path, sb);\n \t}\n \n+\tdest = (opt->record_conflict_msgs_as_headers ? &tmp : sb);\n+\n \tva_start(ap, fmt);\n-\tstrbuf_vaddf(sb, fmt, ap);\n+\tstrbuf_vaddf(dest, fmt, ap);\n \tva_end(ap);\n \n+\tif (opt->record_conflict_msgs_as_headers) {\n+\t\tint i_sb = 0, i_tmp = 0;\n+\n+\t\t/* Start with the specified prefix */\n+\t\tif (opt->msg_header_prefix)\n+\t\t\tstrbuf_addf(sb, \"%s \", opt->msg_header_prefix);\n+\n+\t\t/* Copy tmp to sb, adding spaces after newlines */\n+\t\tstrbuf_grow(sb, sb->len + 2*tmp.len); /* more than sufficient */\n+\t\tfor (; i_tmp < tmp.len; i_tmp++, i_sb++) {\n+\t\t\t/* Copy next character from tmp to sb */\n+\t\t\tsb->buf[sb->len + i_sb] = tmp.buf[i_tmp];\n+\n+\t\t\t/* If we copied a newline, add a space */\n+\t\t\tif (tmp.buf[i_tmp] == '\\n')\n+\t\t\t\tsb->buf[++i_sb] = ' ';\n+\t\t}\n+\t\t/* Update length and ensure it's NUL-terminated */\n+\t\tsb->len += i_sb;\n+\t\tsb->buf[sb->len] = '\\0';\n+\n+\t\tstrbuf_release(&tmp);\n+\t}\n+\n+\t/* Add final newline character to sb */\n \tstrbuf_addch(sb, '\\n');\n }\n \n@@ -4246,6 +4278,9 @@ void merge_switch_to_result(struct merge_options *opt,\n \t\tstruct string_list olist = STRING_LIST_INIT_NODUP;\n \t\tint i;\n \n+\t\tif (opt->record_conflict_msgs_as_headers)\n+\t\t\tBUG(\"Either display conflict messages or record them as headers, not both\");\n+\n \t\ttrace2_region_enter(\"merge\", \"display messages\", opt->repo);\n \n \t\t/* Hack to pre-allocate olist to the desired size */\n@@ -4347,6 +4382,9 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)\n \tassert(opt->recursive_variant >= MERGE_VARIANT_NORMAL &&\n \t       opt->recursive_variant <= MERGE_VARIANT_THEIRS);\n \n+\tif (opt->msg_header_prefix)\n+\t\tassert(opt->record_conflict_msgs_as_headers);\n+\n \t/*\n \t * detect_renames, verbosity, buffer_output, and obuf are ignored\n \t * fields that were used by \"recursive\" rather than \"ort\" -- but\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex bc73c52dd84..9ec1e6d043a 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -3714,6 +3714,10 @@ static int merge_start(struct merge_options *opt, struct tree *head)\n \n \tassert(opt->priv == NULL);\n \n+\t/* Not supported; option specific to merge-ort */\n+\tassert(!opt->record_conflict_msgs_as_headers);\n+\tassert(!opt->msg_header_prefix);\n+\n \t/* Sanity check on repo state; index must match head */\n \tif (repo_index_has_changes(opt->repo, head, &sb)) {\n \t\terr(opt, _(\"Your local changes to the following files would be overwritten by merge:\\n  %s\"),\ndiff --git a/merge-recursive.h b/merge-recursive.h\nindex 0795a1d3ec1..b88000e3c25 100644\n--- a/merge-recursive.h\n+++ b/merge-recursive.h\n@@ -46,6 +46,8 @@ struct merge_options {\n \t/* miscellaneous control options */\n \tconst char *subtree_shift;\n \tunsigned renormalize : 1;\n+\tunsigned record_conflict_msgs_as_headers : 1;\n+\tconst char *msg_header_prefix;\n \n \t/* internal fields used by the implementation */\n \tstruct merge_options_internal *priv;\n-- \ngitgitgadget\n\n"},{"id":"445259","messageId":"5000a94aa9881751e887728cf4a7fb4d9eb8aacb.1640907369.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v3 7/9] diff: add ability to insert additional headers for paths","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:07Z","receivedAt":"2021-12-30T23:36:25Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen additional headers are provided, we need to\n  * add diff_filepairs to diff_queued_diff for each paths in the\n    additional headers map which, unless that path is part of\n    another diff_filepair already found in diff_queued_diff\n  * format the headers (colorization, line_prefix for --graph)\n  * make sure the various codepaths that attempt to return early\n    if there are \"no changes\" take into account the headers that\n    need to be shown.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n diff.c     | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++--\n diff.h     |   3 +-\n log-tree.c |   2 +-\n 3 files changed, 115 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 861282db1c3..aaa6a19f158 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -27,6 +27,7 @@\n #include \"help.h\"\n #include \"promisor-remote.h\"\n #include \"dir.h\"\n+#include \"strmap.h\"\n \n #ifdef NO_FAST_WORKING_DIRECTORY\n #define FAST_WORKING_DIRECTORY 0\n@@ -3406,6 +3407,31 @@ struct userdiff_driver *get_textconv(struct repository *r,\n \treturn userdiff_get_textconv(r, one->driver);\n }\n \n+static struct strbuf *additional_headers(struct diff_options *o,\n+\t\t\t\t\t const char *path)\n+{\n+\tif (!o->additional_path_headers)\n+\t\treturn NULL;\n+\treturn strmap_get(o->additional_path_headers, path);\n+}\n+\n+static void add_formatted_headers(struct strbuf *msg,\n+\t\t\t\t  struct strbuf *more_headers,\n+\t\t\t\t  const char *line_prefix,\n+\t\t\t\t  const char *meta,\n+\t\t\t\t  const char *reset)\n+{\n+\tchar *next, *newline;\n+\n+\tfor (next = more_headers->buf; *next; next = newline) {\n+\t\tnewline = strchrnul(next, '\\n');\n+\t\tstrbuf_addf(msg, \"%s%s%.*s%s\\n\", line_prefix, meta,\n+\t\t\t    (int)(newline - next), next, reset);\n+\t\tif (*newline)\n+\t\t\tnewline++;\n+\t}\n+}\n+\n static void builtin_diff(const char *name_a,\n \t\t\t const char *name_b,\n \t\t\t struct diff_filespec *one,\n@@ -3464,6 +3490,17 @@ static void builtin_diff(const char *name_a,\n \tb_two = quote_two(b_prefix, name_b + (*name_b == '/'));\n \tlbl[0] = DIFF_FILE_VALID(one) ? a_one : \"/dev/null\";\n \tlbl[1] = DIFF_FILE_VALID(two) ? b_two : \"/dev/null\";\n+\tif (!DIFF_FILE_VALID(one) && !DIFF_FILE_VALID(two)) {\n+\t\t/*\n+\t\t * We should only reach this point for pairs from\n+\t\t * create_filepairs_for_header_only_notifications().  For\n+\t\t * these, we should avoid the \"/dev/null\" special casing\n+\t\t * above, meaning we avoid showing such pairs as either\n+\t\t * \"new file\" or \"deleted file\" below.\n+\t\t */\n+\t\tlbl[0] = a_one;\n+\t\tlbl[1] = b_two;\n+\t}\n \tstrbuf_addf(&header, \"%s%sdiff --git %s %s%s\\n\", line_prefix, meta, a_one, b_two, reset);\n \tif (lbl[0][0] == '/') {\n \t\t/* /dev/null */\n@@ -4328,6 +4365,7 @@ static void fill_metainfo(struct strbuf *msg,\n \tconst char *set = diff_get_color(use_color, DIFF_METAINFO);\n \tconst char *reset = diff_get_color(use_color, DIFF_RESET);\n \tconst char *line_prefix = diff_line_prefix(o);\n+\tstruct strbuf *more_headers = NULL;\n \n \t*must_show_header = 1;\n \tstrbuf_init(msg, PATH_MAX * 2 + 300);\n@@ -4364,6 +4402,11 @@ static void fill_metainfo(struct strbuf *msg,\n \tdefault:\n \t\t*must_show_header = 0;\n \t}\n+\tif ((more_headers = additional_headers(o, name))) {\n+\t\tadd_formatted_headers(msg, more_headers,\n+\t\t\t\t      line_prefix, set, reset);\n+\t\t*must_show_header = 1;\n+\t}\n \tif (one && two && !oideq(&one->oid, &two->oid)) {\n \t\tconst unsigned hexsz = the_hash_algo->hexsz;\n \t\tint abbrev = o->abbrev ? o->abbrev : DEFAULT_ABBREV;\n@@ -5852,12 +5895,22 @@ int diff_unmodified_pair(struct diff_filepair *p)\n \n static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n {\n-\tif (diff_unmodified_pair(p))\n+\t/*\n+\t * Check if we can return early without showing a diff.  Note that\n+\t * diff_filepair only stores {oid, path, mode, is_valid}\n+\t * information for each path, and thus diff_unmodified_pair() only\n+\t * considers those bits of info.  However, we do not want pairs\n+\t * created by create_filepairs_for_header_only_notifications() to\n+\t * be ignored, so return early if both p is unmodified AND\n+\t * p->one->path is not in additional headers.\n+\t */\n+\tif (diff_unmodified_pair(p) && !additional_headers(o, p->one->path))\n \t\treturn;\n \n+\t/* Actually, we can also return early to avoid showing tree diffs */\n \tif ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||\n \t    (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))\n-\t\treturn; /* no tree diffs in patch format */\n+\t\treturn;\n \n \trun_diff(p, o);\n }\n@@ -5888,10 +5941,14 @@ static void diff_flush_checkdiff(struct diff_filepair *p,\n \trun_checkdiff(p, o);\n }\n \n-int diff_queue_is_empty(void)\n+int diff_queue_is_empty(struct diff_options *o)\n {\n \tstruct diff_queue_struct *q = &diff_queued_diff;\n \tint i;\n+\n+\tif (o->additional_path_headers &&\n+\t    !strmap_empty(o->additional_path_headers))\n+\t\treturn 0;\n \tfor (i = 0; i < q->nr; i++)\n \t\tif (!diff_unmodified_pair(q->queue[i]))\n \t\t\treturn 0;\n@@ -6325,6 +6382,54 @@ void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)\n \t\twarning(_(rename_limit_advice), varname, needed);\n }\n \n+static void create_filepairs_for_header_only_notifications(struct diff_options *o)\n+{\n+\tstruct strset present;\n+\tstruct diff_queue_struct *q = &diff_queued_diff;\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *e;\n+\tint i;\n+\n+\tstrset_init_with_options(&present, /*pool*/ NULL, /*strdup*/ 0);\n+\n+\t/*\n+\t * Find out which paths exist in diff_queued_diff, preferring\n+\t * one->path for any pair that has multiple paths.\n+\t */\n+\tfor (i = 0; i < q->nr; i++) {\n+\t\tstruct diff_filepair *p = q->queue[i];\n+\t\tchar *path = p->one->path ? p->one->path : p->two->path;\n+\n+\t\tif (strmap_contains(o->additional_path_headers, path))\n+\t\t\tstrset_add(&present, path);\n+\t}\n+\n+\t/*\n+\t * Loop over paths in additional_path_headers; for each NOT already\n+\t * in diff_queued_diff, create a synthetic filepair and insert that\n+\t * into diff_queued_diff.\n+\t */\n+\tstrmap_for_each_entry(o->additional_path_headers, &iter, e) {\n+\t\tif (!strset_contains(&present, e->key)) {\n+\t\t\tstruct diff_filespec *one, *two;\n+\t\t\tstruct diff_filepair *p;\n+\n+\t\t\tone = alloc_filespec(e->key);\n+\t\t\ttwo = alloc_filespec(e->key);\n+\t\t\tfill_filespec(one, null_oid(), 0, 0);\n+\t\t\tfill_filespec(two, null_oid(), 0, 0);\n+\t\t\tp = diff_queue(q, one, two);\n+\t\t\tp->status = DIFF_STATUS_MODIFIED;\n+\t\t}\n+\t}\n+\n+\t/* Re-sort the filepairs */\n+\tdiffcore_fix_diff_index();\n+\n+\t/* Cleanup */\n+\tstrset_clear(&present);\n+}\n+\n static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n {\n \tint i;\n@@ -6337,6 +6442,9 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n \tif (o->color_moved)\n \t\to->emitted_symbols = &esm;\n \n+\tif (o->additional_path_headers)\n+\t\tcreate_filepairs_for_header_only_notifications(o);\n+\n \tfor (i = 0; i < q->nr; i++) {\n \t\tstruct diff_filepair *p = q->queue[i];\n \t\tif (check_pair_status(p))\n@@ -6413,7 +6521,7 @@ void diff_flush(struct diff_options *options)\n \t * Order: raw, stat, summary, patch\n \t * or:    name/name-status/checkdiff (other bits clear)\n \t */\n-\tif (!q->nr)\n+\tif (!q->nr && !options->additional_path_headers)\n \t\tgoto free_queue;\n \n \tif (output_format & (DIFF_FORMAT_RAW |\ndiff --git a/diff.h b/diff.h\nindex 8ba85c5e605..06a0a67afda 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -395,6 +395,7 @@ struct diff_options {\n \n \tstruct repository *repo;\n \tstruct option *parseopts;\n+\tstruct strmap *additional_path_headers;\n \n \tint no_free;\n };\n@@ -593,7 +594,7 @@ void diffcore_fix_diff_index(void);\n \"                show all files diff when -S is used and hit is found.\\n\" \\\n \"  -a  --text    treat all files as text.\\n\"\n \n-int diff_queue_is_empty(void);\n+int diff_queue_is_empty(struct diff_options*);\n void diff_flush(struct diff_options*);\n void diff_free(struct diff_options*);\n void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);\ndiff --git a/log-tree.c b/log-tree.c\nindex d4655b63d75..33c28f537a6 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -850,7 +850,7 @@ int log_tree_diff_flush(struct rev_info *opt)\n \topt->shown_dashes = 0;\n \tdiffcore_std(&opt->diffopt);\n \n-\tif (diff_queue_is_empty()) {\n+\tif (diff_queue_is_empty(&opt->diffopt)) {\n \t\tint saved_fmt = opt->diffopt.output_format;\n \t\topt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT;\n \t\tdiff_flush(&opt->diffopt);\n-- \ngitgitgadget\n\n"},{"id":"445260","messageId":"78ec1f44e4eacdb05c9da749a2160f4748565ad3.1640907369.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v3 8/9] show, log: include conflict/warning messages in --remerge-diff headers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:08Z","receivedAt":"2021-12-30T23:36:28Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nConflicts such as modify/delete, rename/rename, or file/directory are\nnot representable via content conflict markers, and the normal output\nmessages notifying users about these were dropped with --remerge-diff.\nWhile we don't want these messages randomly shown before the commit\nand diff headers, we do want them to still be shown; include them as\npart of the diff headers instead.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n log-tree.c              |  4 ++\n merge-ort.c             |  1 +\n merge-ort.h             | 10 +++++\n t/t4069-remerge-diff.sh | 86 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 101 insertions(+)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 33c28f537a6..a04172d2908 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -922,6 +922,8 @@ static int do_remerge_diff(struct rev_info *opt,\n \t/* Setup merge options */\n \tinit_merge_options(&o, the_repository);\n \to.show_rename_progress = 0;\n+\to.record_conflict_msgs_as_headers = 1;\n+\to.msg_header_prefix = \"remerge\";\n \n \tctx.abbrev = DEFAULT_ABBREV;\n \tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n@@ -938,10 +940,12 @@ static int do_remerge_diff(struct rev_info *opt,\n \tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n \n \t/* Show the diff */\n+\topt->diffopt.additional_path_headers = res.path_messages;\n \tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n \tlog_tree_diff_flush(opt);\n \n \t/* Cleanup */\n+\topt->diffopt.additional_path_headers = NULL;\n \tstrbuf_release(&parent1_desc);\n \tstrbuf_release(&parent2_desc);\n \tmerge_finalize(&o, &res);\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 481305d2bcf..43f980d2586 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -4585,6 +4585,7 @@ redo:\n \ttrace2_region_leave(\"merge\", \"process_entries\", opt->repo);\n \n \t/* Set return values */\n+\tresult->path_messages = &opt->priv->output;\n \tresult->tree = parse_tree_indirect(&working_tree_oid);\n \t/* existence of conflicted entries implies unclean */\n \tresult->clean &= strmap_empty(&opt->priv->conflicted);\ndiff --git a/merge-ort.h b/merge-ort.h\nindex c011864ffeb..fe599b87868 100644\n--- a/merge-ort.h\n+++ b/merge-ort.h\n@@ -5,6 +5,7 @@\n \n struct commit;\n struct tree;\n+struct strmap;\n \n struct merge_result {\n \t/*\n@@ -23,6 +24,15 @@ struct merge_result {\n \t */\n \tstruct tree *tree;\n \n+\t/*\n+\t * Special messages and conflict notices for various paths\n+\t *\n+\t * This is a map of pathnames to strbufs.  It contains various\n+\t * warning/conflict/notice messages (possibly multiple per path)\n+\t * that callers may want to use.\n+\t */\n+\tstruct strmap *path_messages;\n+\n \t/*\n \t * Additional metadata used by merge_switch_to_result() or future calls\n \t * to merge_incore_*().  Includes data needed to update the index (if\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nindex 1b32028e897..c1b44138145 100755\n--- a/t/t4069-remerge-diff.sh\n+++ b/t/t4069-remerge-diff.sh\n@@ -4,6 +4,15 @@ test_description='remerge-diff handling'\n \n . ./test-lib.sh\n \n+# --remerge-diff uses ort under the hood regardless of setting.  However,\n+# we set up a file/directory conflict beforehand, and the different backends\n+# handle the conflict differently, which would require separate code paths\n+# to resolve.  There's not much point in making the code uglier to do that,\n+# though, when the real thing we are testing (--remerge-diff) will hardcode\n+# calls directly into the merge-ort API anyway.  So just force the use of\n+# ort on the setup too.\n+GIT_TEST_MERGE_ALGORITHM=ort\n+\n test_expect_success 'setup basic merges' '\n \ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n \tgit add numbers &&\n@@ -53,6 +62,7 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n \tgit log -1 --oneline ab_resolution >tmp &&\n \tcat <<-EOF >>tmp &&\n \tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (content): Merge conflict in numbers\n \tindex a1fb731..6875544 100644\n \t--- a/numbers\n \t+++ b/numbers\n@@ -81,4 +91,80 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup non-content conflicts' '\n+\tgit switch --orphan base &&\n+\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\ttest_write_lines a b c d e f g h i >letters &&\n+\ttest_write_lines in the way >content &&\n+\tgit add numbers letters content &&\n+\tgit commit -m base &&\n+\n+\tgit branch side1 &&\n+\tgit branch side2 &&\n+\n+\tgit checkout side1 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit mv letters letters_side1 &&\n+\tgit mv content file_or_directory &&\n+\tgit add numbers &&\n+\tgit commit -m side1 &&\n+\n+\tgit checkout side2 &&\n+\tgit rm numbers &&\n+\tgit mv letters letters_side2 &&\n+\tmkdir file_or_directory &&\n+\techo hello >file_or_directory/world &&\n+\tgit add file_or_directory/world &&\n+\tgit commit -m side2 &&\n+\n+\tgit checkout -b resolution side1 &&\n+\ttest_must_fail git merge side2 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit add letters_side1 &&\n+\tgit rm letters &&\n+\tgit rm letters_side2 &&\n+\tgit add file_or_directory~HEAD &&\n+\tgit mv file_or_directory~HEAD wanted_content &&\n+\tgit commit -m resolved\n+'\n+\n+test_expect_success 'remerge-diff with non-content conflicts' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n+\tsimilarity index 100%\n+\trename from file_or_directory~HASH (side1)\n+\trename to wanted_content\n+\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n+\tdiff --git a/letters b/letters\n+\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n+\tdiff --git a/letters_side2 b/letters_side2\n+\tdeleted file mode 100644\n+\tindex b236ae5..0000000\n+\t--- a/letters_side2\n+\t+++ /dev/null\n+\t@@ -1,9 +0,0 @@\n+\t-a\n+\t-b\n+\t-c\n+\t-d\n+\t-e\n+\t-f\n+\t-g\n+\t-h\n+\t-i\n+\tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"445261","messageId":"64b44ee84f3f649219bcc9d9150b2c5cbe3c7f35.1640907369.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v3 9/9] merge-ort: mark conflict/warning messages from inner merges as omittable","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-30T23:36:09Z","receivedAt":"2021-12-30T23:36:31Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nA recursive merge involves merging the merge bases of the two branches\nbeing merged.  Such an inner merge can itself generate conflict notices.\nWhile such notices may be useful when initially trying to create a\nmerge, they seem to just be noise when investigating merges later with\n--remerge-diff.  (Especially when both sides of the outer merge resolved\nthe conflict the same way leading to no overall conflict.)  Remove them.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 43f980d2586..9bf15a01db8 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -638,7 +638,9 @@ static void path_msg(struct merge_options *opt,\n \tstruct strbuf tmp = STRBUF_INIT;\n \n \tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n-\t\treturn; /* Do not record mere hints in tree */\n+\t\treturn; /* Do not record mere hints in headers */\n+\tif (opt->record_conflict_msgs_as_headers && opt->priv->call_depth)\n+\t\treturn; /* Do not record inner merge issues in headers */\n \tsb = strmap_get(&opt->priv->output, path);\n \tif (!sb) {\n \t\tsb = xmalloc(sizeof(*sb));\n-- \ngitgitgadget\n"},{"id":"445266","messageId":"20211231030746.t42ehzftcdlu3pif@gmail.com","threadId":"57133","inReplyTo":"CABPp-BHoiNiYoVzknJEGQpe5a6nnQXj_bwwrtWbmKLBiUCOj_Q@mail.gmail.com","subject":"Re: [PATCH v2 7/8] diff: add ability to insert additional headers for paths","fromName":"Johannes Altmanninger","fromEmail":"aclopte@gmail.com","sentAt":"2021-12-31T03:07:46Z","receivedAt":"2021-12-31T03:07:51Z","isPatch":true,"sender":{"key":"aclopte@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6853872?v=4"},"body":"On Thu, Dec 30, 2021 at 02:04:30PM -0800, Elijah Newren wrote:\n> >                                 enqueue(file_pair_for(extra_headers[j]))\n> \n> The queue is an array of sorted items, so enqueue here would be\n> insertion into an already sorted list.  Inserting N items into a list\n> of M items is quadratic (O(N*M)) -- unless you meant to just append to\n> the end and add a third sort at the end?\n\nyeah I would have probably used a third sort\n\n> \n> >                         j++\n> \n> At the end of the for loop, there may be remaining additional headers\n> that sort after all those found in the queue, so you'll need an\n> additional loop to handle those.\n\nmy bad, I should have tried it\n\n> It's actually considerably more code as you can see from the diffstat,\n> and feels like we're reaching into some ugly internals with tmp_queue\n> (the SWAP and the special-case freeing) in order to get the desired\n> performance improvements.  And it was already O(NlogN) overall (due to\n> the sort), which doesn't change with this new algorithm.  It's really,\n> really hard for me to imagine a case where we have large numbers of\n> additional headers.  Even if someone else can imagine that we for some\n> reason have a huge number of conflicts in order to generate a huge\n> number of additional headers...how could the performance of sorting\n> O(N) filenames and merging these lists possibly matter in comparison\n> to the O(N) three-way file merges that would likely have been\n> performed from those conflicts?\n\nYeah, I agree with that conclusion, it's surely not worth the added complexity.\nSeeing the code definitely helps, thanks.\n\n> \n> So, I'm going to throw this code away and keep the original.\n> \n> It was an interesting idea and exercise; thanks for keeping me on my toes.\n"},{"id":"445284","messageId":"xmqq35m9maa5.fsf@gitster.g","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 0/9] Add a new --remerge-diff capability to show & log","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-31T08:46:42Z","receivedAt":"2021-12-31T08:46:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Changes since v2 (of the restarted submission):\n>\n>  * Numerous small improvements suggested by Johannes Altmanninger\n>  * Avoid including conflict messages from inner merges (due to example\n>    pointed out by Ævar).\n>  * Added a \"remerge\" prefix to all the new diff headers (suggested by Junio\n>    in a previous round, but I couldn't come up with a good name before. It\n>    suddenly hit me that \"remerge\" is an obvious prefix to use, and even\n>    helps explain what the rest of the line is for.)\n\nThat sounds sensible.\n\n"},{"id":"445835","messageId":"220110.86mtk3zi9m.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"CABPp-BFjZUHKq1OxkgompqbaugCN=c5PFiGxLp9U1sdBePpYvA@mail.gmail.com","subject":"Re: [PATCH v2 0/8] Add a new --remerge-diff capability to show & log","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-01-10T15:48:26Z","receivedAt":"2022-01-10T16:07:38Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Dec 27 2021, Elijah Newren wrote:\n\n> On Sun, Dec 26, 2021 at 2:28 PM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n>>\n>> On Sat, Dec 25 2021, Elijah Newren via GitGitGadget wrote:\n>>\n>> > === FURTHER BACKGROUND (original cover letter material) ==\n>> >\n>> > Here are some example commits you can try this out on (with git show\n>> > --remerge-diff $COMMIT):\n>> >\n>> >  * git.git conflicted merge: 07601b5b36\n>> >  * git.git non-conflicted change: bf04590ecd\n>> >  * linux.git conflicted merge: eab3540562fb\n>> >  * linux.git non-conflicted change: 223cea6a4f05\n>> >\n>> > Many more can be found by just running git log --merges --remerge-diff in\n>> > your repository of choice and searching for diffs (most merges tend to be\n>> > clean and unmodified and thus produce no diff but a search of '^diff' in the\n>> > log output tends to find the examples nicely).\n>> >\n>> > Some basic high level details about this new option:\n>> >\n>> >  * This option is most naturally compared to --cc, though the output seems\n>> >    to be much more understandable to most users than --cc output.\n>> >  * Since merges are often clean and unmodified, this new option results in\n>> >    an empty diff for most merges.\n>> >  * This new option shows things like the removal of conflict markers, which\n>> >    hunks users picked from the various conflicted sides to keep or remove,\n>> >    and shows changes made outside of conflict markers (which might reflect\n>> >    changes needed to resolve semantic conflicts or cleanups of e.g.\n>> >    compilation warnings or other additional changes an integrator felt\n>> >    belonged in the merged result).\n>> >  * This new option does not (currently) work for octopus merges, since\n>> >    merge-ort is specific to two-parent merges[1].\n>> >  * This option will not work on a read-only or full filesystem[2].\n>> >  * We discussed this capability at Git Merge 2020, and one of the\n>> >    suggestions was doing a periodic git gc --auto during the operation (due\n>> >    to potential new blobs and trees created during the operation). I found a\n>> >    way to avoid that; see [2].\n>> >  * This option is faster than you'd probably expect; it handles 33.5 merge\n>> >    commits per second in linux.git on my computer; see below.\n>> >\n>> > In regards to the performance point above, the timing for running the\n>> > following command:\n>> >\n>> > time git log --min-parents=2 --max-parents=2 $DIFF_FLAG | wc -l\n>>\n>> I've been trying to come up with some other useful recipies for this new\n>> option (which is already very useful, thanks!)\n>\n> I'm glad you like it.  :-)\n>\n>> Some of these (if correct) are suggestions for incorporating into the\n>> (now rather sparse) documentation. I.e. walking users through how to use\n>> this, and how (if at all) it combines with other options.\n>>\n>> I wanted to find all merges between \"master\"..\"seen\" for which Junio's\n>> had to resolve a conflict, a naïve version is:\n>>\n>>     $ git log --oneline --remerge-diff -p --min-parents=2 origin/master..origin/seen|grep ^diff -B1 | grep Merge\n>>     [...]\n>\n> I think the naive version is\n>   $ git log --remerge-diff --min-parents=2 origin/master..origin/seen\n>   <search for \"^diff\" using your pager's search functionality>\n>\n> Where the \"--min-parents=2 origin/master..origin/seen\" comes from your\n> problem description (\"find all merges between master..seen\").\n>\n> You can add --oneline to format it, though it's an orthogonal concern.\n> Also, adding -p is unnecessary: --remerge-diff, like --cc, implies -p.\n>\n>> But I found that this new option nicely integrates with --diff-filter,\n>> i.e. we'll end up showing a diff, and the diff machinery allows you to\n>> to filter on it.\n>>\n>> It seems to me like all the diffs you show fall under \"M\", so for\n>\n> Yes, the diffs I happened to pick all fell under \"M\", but by no means\n> should you rely on that happening for all merges in history.  For\n> example, make a new merge commit, then add a completely new file (or\n> delete a file, or rename a file, or copy a file, or change its\n> mode/type), stage the new/deleted/renamed/copied/changed file, and run\n> \"git commit --amend\".\n>\n> So, although --diff-filter=M can be interesting, I would not rely on it.\n\n*Nod* I hadn't thought of those (in retrospect, rather obvious) cases.\n\n>> master..seen (2ae0a9cb829..61055c2920d) this is equivalent (and the\n>> output is the same as the above):\n>>\n>>     $ git -P log --oneline --remerge-diff --no-patch --min-parents=2 --diff-filter=M origin/master..origin/seen\n>>     95daa54b1c3 Merge branch 'hn/reftable-fixes' into seen\n>>     26c4c09dd34 Merge branch 'gc/fetch-negotiate-only-early-return' into seen\n>>     e3dc8d073f6 Merge branch 'gc/branch-recurse-submodules' into seen\n>>     aeada898196 Merge branch 'js/branch-track-inherit' into seen\n>>     4dd30e0da45 Merge branch 'jh/builtin-fsmonitor-part2' into seen\n>>     337743b17d0 Merge branch 'ab/config-based-hooks-2' into seen\n>>     261672178c0 Merge branch 'pw/fix-some-issues-in-reset-head' into seen\n>>     1296d35b041 Merge branch 'ms/customizable-ident-expansion' into seen\n>>     7a3d7d05126 Merge branch 'ja/i18n-similar-messages' into seen\n>>     eda714bb8bc Merge branch 'tb/midx-bitmap-corruption-fix' into seen\n>>     ba02295e3f8 Merge branch 'jh/p4-human-unit-numbers' into jch\n>>     751773fc38b Merge branch 'es/test-chain-lint' into jch\n>>     ec17879f495 Merge branch 'tb/cruft-packs' into tb/midx-bitmap-corruption-fix\n>>\n>> However for \"origin/master..origin/next\" (next = 510f9eba9a2 currently)\n>> we'll oddly show this with \"-p\":\n>>\n>>     9af51fd1d0d Sync with 'master'\n>>     diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\n>>     CONFLICT (content): Merge conflict in t/lib-gpg.sh\n>>     d6f56f3248e Merge branch 'es/test-chain-lint' into next\n>>     diff --git a/t/t4126-apply-empty.sh b/t/t4126-apply-empty.sh\n>>     CONFLICT (content): Merge conflict in t/t4126-apply-empty.sh\n>>     index 996c93329c6..33860d38290 100755\n>>     --- a/t/t4126-apply-empty.sh\n>>     +++ b/t/t4126-apply-empty.sh\n>>     [...]\n>>\n>> The \"oddly\" applying only to that \"9af51fd1d0d Sync with 'master'\", not\n>> the second d6f56f3248e, which shows the sort of conflict I'd expect. The\n>> two-line \"diff\" of:\n>>\n>>     diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\n>>     CONFLICT (content): Merge conflict in t/lib-gpg.sh\n>>\n>> Shows up with -p --remerge-diff, not a mere -p. I also tried the other\n>> --diff-merges=* options, that behavior is new in\n>> --diff-merges=remerge. Is this a bug?\n>\n> Ugh, this is related to my comment elsewhere that conflicts from inner\n> merges are not nicely differentiated.  If I also apply my other series\n> (which has not yet been submitted), this instead appears as follows:\n>\n> $ git show --oneline --remerge-diff 9af51fd1d0d\n> 9af51fd1d0 Sync with 'master'\n> diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\n>   From inner merge:  CONFLICT (content): Merge conflict in t/lib-gpg.sh\n>\n> and the addition of the \"From inner merge: \" text makes it clearer why\n> that line appears.  This is an interesting case where a conflict\n> notice _only_ appears in the inner merge (i.e. the merge of merge\n> bases), which means that both sides on the outer merge changed the\n> relevant portion of the file in the same way, so the outer merge had\n> no conflict.\n>\n> However, instead of trying to differentiate messages from inner\n> merges, I think for --remerge-diff's purposes we should just drop all\n> notices that come from the inner merges.  Those conflict notices might\n> be helpful when initially resolving a merge, but at the --remerge-diff\n> level, they're more likely to be distracting than helpful.\n\nThanks. I see that v3 addresses this. Will try it out...\n\n>> My local build also has a --pickaxe-patch option. It's something I\n>> submitted on-list before[1] and have been meaning to re-roll.\n>>\n>> I'm discussing it here because it skips the stripping of the \"+ \" and \"-\n>> \" prefixes under -G<regex> and allows you to search through the -U<n>\n>> context. With that I'm able to do:\n>>\n>>     git log --oneline --remerge-diff -p --min-parents=2 --pickaxe-patch -G'^\\+' --diff-filter=M origin/master..origin/seen\n>>\n>> I.e. on top of the above filter only show those diffs that have\n>> additions. FAICT the conflicting diffs where the committer of the merge\n>> conflict picked one side or the other will only have \"-\" lines\".\n>>\n>> So those diffs that have additions look to be those where the person\n>> doing the merge needed to combine the two.\n>>\n>> Well, usually. E.g. 26c4c09dd34 (Merge branch\n>> 'gc/fetch-negotiate-only-early-return' into seen, 2021-12-25) in that\n>> range shows that isn't strictly true. Most such deletion-only diffs are\n>> less interesting in picking one side or the other of the conflict, but\n>> that one combines the two:\n>>\n>>     -<<<<<<< d3419aac9f4 (Merge branch 'pw/add-p-hunk-split-fix' into seen)\n>>                             warning(_(\"protocol does not support --negotiate-only, exiting\"));\n>>     -                       return 1;\n>>     -=======\n>>     -                       warning(_(\"Protocol does not support --negotiate-only, exiting.\"));\n>>                             result = 1;\n>>                             goto cleanup;\n>>     ->>>>>>> 495e8601f28 (builtin/fetch: die on --negotiate-only and --recurse-submodules)\n>>\n>> Which I guess is partially commentary and partially a request (either\n>> for this series, or some follow-up) for something like a\n>> --remerge-diff-filter option. I.e. it would be very useful to be able to\n>> filter on some combination of:\n>>\n>>  * Which side(s) of the conflict(s) were picked, or a combination?\n>>  * Is there \"new work\" in the diff to resolve the conflict?\n>>    AFIACT this will always mean we'll have \"+ \" lines.\n>\n> Do any of the following count as \"new work\"? :\n>\n>   * the deletion of a file (perhaps one that had no conflict but was\n> deleted anyway)\n>   * mode changes (again, perhaps on files that had no conflict)\n>   * renames of files/directories?\n>\n> If so, searching for \"^+\" lines might be insufficient, but it depends\n> on what you mean by new work.\n\nYes, I think at least for the use-cases I had in mind the useful thing\nis knowing which state a merge commit is in:\n\n A: \"Vanilla\" merge (i.e. no --remerge-diff output)\n B: non-\"vanilla\" merge\" (i.e. ew have --remerge-diff output)\n\nWhat I'm requesting/musing about here (which is very much in the\ncategory of stuff we can/should do later, and shouldn't prevent this\n\"good enough for now\" series going in) is being able to disambiguate\nthose \"B\" cases.\n\nI.e. being able to see if their/ours side \"won\", some mixture of the two\netc.\n\n>> Or maybe that's not useful at all, and just -G<rx> (maybe combined with\n>> my --pickaxe-patch) will cover it?\n>\n> I'd rather wait until we have a good idea of the potential range of\n> usecases before adding a filter.  (And I think for now, the -G and\n> --pickaxe-patch are probably good enough for this usecase.)  These\n> particular usecases you point out are interesting; thanks for\n> detailing them.  Here's some others to consider:\n>\n>   * Finding out when text was added or removed: `git log\n> --remerge-diff -S<text>` (note that with only -p instead of\n> --remerge-diff, that command will annoyingly misses cases where a\n> merge introduced or removed the text)\n>   * Finding out how a merge differed from one run with some\n> non-default options (e.g. `git show --remerge-diff -Xours` or `git\n> show --remerge-diff -Xno-space-change`; although show doesn't take -X\n> options so this is just an idea at this point)\n>   * Finding out how a merge would have differed had it been run with\n> different options (so instead of comparing a remerge to the merge\n> recorded in history, compare one remerge with default options with a\n> different merge that uses e.g. -Xno-space-change)\n>\n> Also, I've got a follow-up series that also introduces a\n> --remerge-diff-only flag which:\n>   * For single parent commits that cannot be identified as a revert or\n> cherry-pick, do not show a diff.\n>   * For single parent commits that can be identified as a revert or\n> cherry-pick, instead of showing a diff against the parent of the\n> commit, redo the revert or cherry-pick in memory and show a diff\n> against that.\n>   * For merge commits, act the same as --remerge-diff\n\n*nod*\n"},{"id":"446501","messageId":"220119.86pmonn2mb.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"d57ae218cf9eaee0b66db299ee1bba9b488b69b1.1640907369.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 1/9] show, log: provide a --remerge-diff capability","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-01-19T15:49:57Z","receivedAt":"2022-01-19T15:53:37Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Dec 30 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n\n> +static int do_remerge_diff(struct rev_info *opt,\n> +\t\t\t   struct commit_list *parents,\n> +\t\t\t   struct object_id *oid,\n> +\t\t\t   struct commit *commit)\n> +{\n> +\tstruct merge_options o;\n> +\tstruct commit_list *bases;\n> +\tstruct merge_result res = {0};\n> +\tstruct pretty_print_context ctx = {0};\n> +\tstruct commit *parent1 = parents->item;\n> +\tstruct commit *parent2 = parents->next->item;\n> +\tstruct strbuf parent1_desc = STRBUF_INIT;\n> +\tstruct strbuf parent2_desc = STRBUF_INIT;\n> +\n> +\t/* Setup merge options */\n> +\tinit_merge_options(&o, the_repository);\n> +\to.show_rename_progress = 0;\n> +\n> +\tctx.abbrev = DEFAULT_ABBREV;\n> +\tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n> +\tformat_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n> +\to.branch1 = parent1_desc.buf;\n> +\to.branch2 = parent2_desc.buf;\n> +\n> +\t/* Parse the relevant commits and get the merge bases */\n> +\tparse_commit_or_die(parent1);\n> +\tparse_commit_or_die(parent2);\n> +\tbases = get_merge_bases(parent1, parent2);\n\nThere's existing leaks all over the place here unrelated to this new\ncode, so this is no big deal, but I noticed that get_merge_bases() here\nleaks.\n\nShouldn't it call free_commit_list() like e.g. diff_get_merge_base()\nwhich invokes get_merge_bases() does on the return value?\n\n> +test_description='remerge-diff handling'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup basic merges' '\n> +\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n> +\tgit add numbers &&\n> +\tgit commit -m base &&\n> +\n> +\tgit branch feature_a &&\n> +\tgit branch feature_b &&\n> +\tgit branch feature_c &&\n> +\n> +\tgit branch ab_resolution &&\n> +\tgit branch bc_resolution &&\n> +\n> +\tgit checkout feature_a &&\n> +\ttest_write_lines 1 2 three 4 5 6 7 eight 9 >numbers &&\n> +\tgit commit -a -m change_a &&\n> +\n> +\tgit checkout feature_b &&\n> +\ttest_write_lines 1 2 tres 4 5 6 7 8 9 >numbers &&\n> +\tgit commit -a -m change_b &&\n> +\n> +\tgit checkout feature_c &&\n> +\ttest_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&\n> +\tgit commit -a -m change_c &&\n> +\n> +\tgit checkout bc_resolution &&\n> +\tgit merge --ff-only feature_b &&\n> +\t# no conflict\n> +\tgit merge feature_c &&\n> +\n> +\tgit checkout ab_resolution &&\n> +\tgit merge --ff-only feature_a &&\n> +\t# conflicts!\n> +\ttest_must_fail git merge feature_b &&\n> +\t# Resolve conflict...and make another change elsewhere\n> +\ttest_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers &&\n> +\tgit add numbers &&\n\nJust a matter of taste, but FWIW some of the custom\ntest_write_lines/commit here could nowadays use test_commit with\n--printf: 47c88d16ba6 (test-lib functions: add --printf option to\ntest_commit, 2021-05-10)\n\nI don't think it's worth the churn to change it here, just an FYI.\n"},{"id":"446502","messageId":"220119.86lezbn21b.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"d57ae218cf9eaee0b66db299ee1bba9b488b69b1.1640907369.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 1/9] show, log: provide a --remerge-diff capability","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-01-19T16:01:12Z","receivedAt":"2022-01-19T16:06:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Dec 30 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n> +\tstruct tmp_objdir *remerge_objdir = NULL;\n> +\n> +\tif (rev->remerge_diff) {\n> +\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n> +\t\tif (!remerge_objdir)\n> +\t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n> +\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n> +\t}\n\nRe the errno feedback on v1\nhttps://lore.kernel.org/git/211221.864k71r6kz.gmgdl@evledraar.gmail.com/\nthe API might lose the \"errno\" due to e.g. the remove_dir_recurse()\ncodepath. This seems like it would take care of that:\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 944d9c0d9b5..d4b8b1aa4b6 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -424,9 +424,9 @@ static int cmd_log_walk(struct rev_info *rev)\n \tint saved_dcctc = 0;\n \n \tif (rev->remerge_diff) {\n-\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\trev->remerge_objdir = tmp_objdir_create_gently(\"remerge-diff\", 0);\n \t\tif (!rev->remerge_objdir)\n-\t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n+\t\t\texit(128);\n \t\ttmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n \t}\n \ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex adf6033549e..3c656120003 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -121,19 +121,21 @@ static void env_replace(struct strvec *env, const char *key, const char *val)\n \tstrvec_pushf(env, \"%s=%s\", key, val);\n }\n \n-static int setup_tmp_objdir(const char *root)\n+static int setup_tmp_objdir(const char *root, int quiet)\n {\n \tchar *path;\n \tint ret = 0;\n \n \tpath = xstrfmt(\"%s/pack\", root);\n \tret = mkdir(path, 0777);\n+\tif (!quiet && ret < 0)\n+\t\tdie_errno(_(\"unable to create temporary object directory '%s'\"), path);\n \tfree(path);\n \n \treturn ret;\n }\n \n-struct tmp_objdir *tmp_objdir_create(const char *prefix)\n+struct tmp_objdir *tmp_objdir_create_gently(const char *prefix, int quiet)\n {\n \tstatic int installed_handlers;\n \tstruct tmp_objdir *t;\n@@ -161,6 +163,8 @@ struct tmp_objdir *tmp_objdir_create(const char *prefix)\n \tstrbuf_grow(&t->path, 1024);\n \n \tif (!mkdtemp(t->path.buf)) {\n+\t\tif (!quiet)\n+\t\t\terror_errno(_(\"unable to create temporary directory '%s'\"), t->path.buf);\n \t\t/* free, not destroy, as we never touched the filesystem */\n \t\ttmp_objdir_free(t);\n \t\treturn NULL;\n@@ -173,7 +177,7 @@ struct tmp_objdir *tmp_objdir_create(const char *prefix)\n \t\tinstalled_handlers++;\n \t}\n \n-\tif (setup_tmp_objdir(t->path.buf)) {\n+\tif (setup_tmp_objdir(t->path.buf, quiet)) {\n \t\ttmp_objdir_destroy(t);\n \t\treturn NULL;\n \t}\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex 76efc7edee5..5072fb860d9 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -24,8 +24,15 @@ struct tmp_objdir;\n /*\n  * Create a new temporary object directory with the specified prefix;\n  * returns NULL on failure.\n+ *\n+ * The tmp_objdir_create() is an a wrapper for\n+ * tmp_objdir_create_gently(..., 1).\n  */\n-struct tmp_objdir *tmp_objdir_create(const char *prefix);\n+struct tmp_objdir *tmp_objdir_create_gently(const char *prefix, int quiet);\n+static inline struct tmp_objdir *tmp_objdir_create(const char *prefix)\n+{\n+\treturn tmp_objdir_create_gently(prefix, 1);\n+}\n \n /*\n  * Return a list of environment strings, suitable for use with\n"},{"id":"446503","messageId":"220119.86h79zmzy5.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"b952f674df13a1c3e4bffa823490f905e862ef7c.1640907369.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 3/9] ll-merge: make callers responsible for showing warnings","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-01-19T16:41:14Z","receivedAt":"2022-01-19T16:51:20Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Dec 30 2021, Elijah Newren via GitGitGadget wrote:\n\n> Note that my methodology included first modifying ll_merge() to return\n> a struct, so that the compiler would catch all the callers for me and\n> ensure I had modified all of them.  After modifying all of them, I then\n> changed the struct to an enum.\n> [...]\n> -int ll_merge(mmbuffer_t *result_buf,\n> +enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n>  \t     const char *path,\n>  \t     mmfile_t *ancestor, const char *ancestor_label,\n>  \t     mmfile_t *ours, const char *our_label,\n> diff --git a/ll-merge.h b/ll-merge.h\n> index aceb1b24132..e4a20e81a3a 100644\n> --- a/ll-merge.h\n> +++ b/ll-merge.h\n> @@ -82,13 +82,20 @@ struct ll_merge_options {\n>  \tlong xdl_opts;\n>  };\n>  \n> +enum ll_merge_result {\n> +\tLL_MERGE_ERROR = -1,\n> +\tLL_MERGE_OK = 0,\n> +\tLL_MERGE_CONFLICT,\n> +\tLL_MERGE_BINARY_CONFLICT,\n> +};\n> +\n\nIsn't the other side of the enum checking missing in many cases?\n\nE.g. ll_ext_merge() returns \"enum ll_merge_result\" now, and does:\n\n        status = run_command_v_opt(args, RUN_USING_SHELL);\n        ret = (status > 0) ? LL_MERGE_CONFLICT : status;\n\nAnd grepping at the tip of this series shows:\n    \n    $ git grep LL_MERGE_OK\n    ll-merge.c:             ret = LL_MERGE_OK;\n    ll-merge.c:                     ret = LL_MERGE_OK;\n    ll-merge.c:                     ret = LL_MERGE_OK;\n    ll-merge.h:     LL_MERGE_OK = 0,\n\nSimilar for LL_MERGE_CONFLICT, the only one that's used outside of the\nfile itself and its header is LL_MERGE_BINARY_CONFLICT.\n\nI.e. shouldn't these codepaths:\n\n    git grep -w ll_merge\n\nBe doing a switch() on that new enum? E.g. we lose the type in\nthree_way_merge() in apply.c, it seems to me that that function should\nswitch over this new enum, and return the \"int\" that the callers of\nthree_way_merge() care about (i.e. just <0, 0, 1, not this enum's -1, 0,\n1, 2.\n"},{"id":"446531","messageId":"CABPp-BGsf8pcEZrY3vuq0yszy9G57fVa5jMSjT3H+sgDe6hWGg@mail.gmail.com","threadId":"57133","inReplyTo":"220119.86pmonn2mb.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v3 1/9] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-20T02:31:31Z","receivedAt":"2022-01-20T02:31:48Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Jan 19, 2022 at 7:53 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Thu, Dec 30 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n>\n> > +static int do_remerge_diff(struct rev_info *opt,\n> > +                        struct commit_list *parents,\n> > +                        struct object_id *oid,\n> > +                        struct commit *commit)\n> > +{\n> > +     struct merge_options o;\n> > +     struct commit_list *bases;\n> > +     struct merge_result res = {0};\n> > +     struct pretty_print_context ctx = {0};\n> > +     struct commit *parent1 = parents->item;\n> > +     struct commit *parent2 = parents->next->item;\n> > +     struct strbuf parent1_desc = STRBUF_INIT;\n> > +     struct strbuf parent2_desc = STRBUF_INIT;\n> > +\n> > +     /* Setup merge options */\n> > +     init_merge_options(&o, the_repository);\n> > +     o.show_rename_progress = 0;\n> > +\n> > +     ctx.abbrev = DEFAULT_ABBREV;\n> > +     format_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n> > +     format_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n> > +     o.branch1 = parent1_desc.buf;\n> > +     o.branch2 = parent2_desc.buf;\n> > +\n> > +     /* Parse the relevant commits and get the merge bases */\n> > +     parse_commit_or_die(parent1);\n> > +     parse_commit_or_die(parent2);\n> > +     bases = get_merge_bases(parent1, parent2);\n>\n> There's existing leaks all over the place here unrelated to this new\n> code, so this is no big deal, but I noticed that get_merge_bases() here\n> leaks.\n\nInteresting.\n\n> Shouldn't it call free_commit_list() like e.g. diff_get_merge_base()\n> which invokes get_merge_bases() does on the return value?\n\nSee the comment describing merge_incore_recursive() in merge-ort.h,\nparticularly this part:\n\n* merge_bases will be consumed (emptied) so make a copy if you need it.\n\nSo free_commit_list() seems like it'd lead to a double free or use-after-free.\n\nHowever, looking at merge_ort_internal() it looks like there is a bug\nin its consumption of the merge bases (which I copied from\nmerge_recursive; oops).  It pops the first one off the commit list,\nbut then merely iterates through the remainder of the list without\npopping.  So, if there's only one merge base, it'll consume it and the\ncode will look leak free (which must have been the cases I was looking\nat when I was doing leak testing).  But in recursive cases, it leaks\nthe second and later ones.\n\nSince the caller still has a pointer referring to the first (already\nfree'd) commit, I think that if they attempt to use it then it would\nprobably cause a use-after-free.\n\n\nSo, yes, I think there's a leak, but it's not due to this patch.  It's\none that has been around since...the introduction of merge-recursive\n(though it originally computed the merge bases internally rather than\nallowing them to be passed in).  So, it's been around for quite a\nwhile.\n\nI'll look into it, and see if I can come up with a fix, but it doesn't\nreally belong in this series.  I'll submit it separately.\n\nThanks for the report.\n\n> > +test_description='remerge-diff handling'\n> > +\n> > +. ./test-lib.sh\n> > +\n> > +test_expect_success 'setup basic merges' '\n> > +     test_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n> > +     git add numbers &&\n> > +     git commit -m base &&\n> > +\n> > +     git branch feature_a &&\n> > +     git branch feature_b &&\n> > +     git branch feature_c &&\n> > +\n> > +     git branch ab_resolution &&\n> > +     git branch bc_resolution &&\n> > +\n> > +     git checkout feature_a &&\n> > +     test_write_lines 1 2 three 4 5 6 7 eight 9 >numbers &&\n> > +     git commit -a -m change_a &&\n> > +\n> > +     git checkout feature_b &&\n> > +     test_write_lines 1 2 tres 4 5 6 7 8 9 >numbers &&\n> > +     git commit -a -m change_b &&\n> > +\n> > +     git checkout feature_c &&\n> > +     test_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&\n> > +     git commit -a -m change_c &&\n> > +\n> > +     git checkout bc_resolution &&\n> > +     git merge --ff-only feature_b &&\n> > +     # no conflict\n> > +     git merge feature_c &&\n> > +\n> > +     git checkout ab_resolution &&\n> > +     git merge --ff-only feature_a &&\n> > +     # conflicts!\n> > +     test_must_fail git merge feature_b &&\n> > +     # Resolve conflict...and make another change elsewhere\n> > +     test_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers &&\n> > +     git add numbers &&\n>\n> Just a matter of taste, but FWIW some of the custom\n> test_write_lines/commit here could nowadays use test_commit with\n> --printf: 47c88d16ba6 (test-lib functions: add --printf option to\n> test_commit, 2021-05-10)\n>\n> I don't think it's worth the churn to change it here, just an FYI.\n\nGood to know; thanks for the heads up.\n\nNote, though, and this has nothing to do with your patches, but I'm\nnot sure I'll ever use this particular feature since I don't much care\nfor test_commit except in trivial cases.  Others have recommended the\nfunction to me before, but my attempts to use it have cost me far more\ntime than it has saved due to its quirks not working well with the\nmerges I have attempted to setup.  Beyond the fact that its\ndocumentation is a lie and the filename defaults to <message>.t, one\nalso has to memorize the order of three positional arguments and add a\nsmattering of additional flags (--printf, --append, --no-tag) and add\na bunch of newline directives to get things right.  The function can\nbe useful and nice on non-merge tests (e.g. when you can pass it just\none positional argument) and I'm happy to use it there, but for\nmerge-related tests it's a needless time sink where the best you can\nhope for after fighting it is getting code that is overall _less_\nreadable than what you started with.\n"},{"id":"446532","messageId":"CABPp-BH9+RWE1SQfQiAYFtGZze3eQpeNDYNr2v9dmT77Pns0nA@mail.gmail.com","threadId":"57133","inReplyTo":"220119.86lezbn21b.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v3 1/9] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-20T02:33:34Z","receivedAt":"2022-01-20T02:33:53Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Jan 19, 2022 at 8:06 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Thu, Dec 30 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> > +     struct tmp_objdir *remerge_objdir = NULL;\n> > +\n> > +     if (rev->remerge_diff) {\n> > +             remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n> > +             if (!remerge_objdir)\n> > +                     die_errno(_(\"unable to create temporary object directory\"));\n> > +             tmp_objdir_replace_primary_odb(remerge_objdir, 1);\n> > +     }\n>\n> Re the errno feedback on v1\n> https://lore.kernel.org/git/211221.864k71r6kz.gmgdl@evledraar.gmail.com/\n> the API might lose the \"errno\" due to e.g. the remove_dir_recurse()\n> codepath. This seems like it would take care of that:\n>\n> diff --git a/builtin/log.c b/builtin/log.c\n> index 944d9c0d9b5..d4b8b1aa4b6 100644\n> --- a/builtin/log.c\n> +++ b/builtin/log.c\n> @@ -424,9 +424,9 @@ static int cmd_log_walk(struct rev_info *rev)\n>         int saved_dcctc = 0;\n>\n>         if (rev->remerge_diff) {\n> -               rev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n> +               rev->remerge_objdir = tmp_objdir_create_gently(\"remerge-diff\", 0);\n>                 if (!rev->remerge_objdir)\n> -                       die_errno(_(\"unable to create temporary object directory\"));\n> +                       exit(128);\n>                 tmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n>         }\n>\n> diff --git a/tmp-objdir.c b/tmp-objdir.c\n> index adf6033549e..3c656120003 100644\n> --- a/tmp-objdir.c\n> +++ b/tmp-objdir.c\n> @@ -121,19 +121,21 @@ static void env_replace(struct strvec *env, const char *key, const char *val)\n>         strvec_pushf(env, \"%s=%s\", key, val);\n>  }\n>\n> -static int setup_tmp_objdir(const char *root)\n> +static int setup_tmp_objdir(const char *root, int quiet)\n>  {\n>         char *path;\n>         int ret = 0;\n>\n>         path = xstrfmt(\"%s/pack\", root);\n>         ret = mkdir(path, 0777);\n> +       if (!quiet && ret < 0)\n> +               die_errno(_(\"unable to create temporary object directory '%s'\"), path);\n>         free(path);\n>\n>         return ret;\n>  }\n>\n> -struct tmp_objdir *tmp_objdir_create(const char *prefix)\n> +struct tmp_objdir *tmp_objdir_create_gently(const char *prefix, int quiet)\n>  {\n>         static int installed_handlers;\n>         struct tmp_objdir *t;\n> @@ -161,6 +163,8 @@ struct tmp_objdir *tmp_objdir_create(const char *prefix)\n>         strbuf_grow(&t->path, 1024);\n>\n>         if (!mkdtemp(t->path.buf)) {\n> +               if (!quiet)\n> +                       error_errno(_(\"unable to create temporary directory '%s'\"), t->path.buf);\n>                 /* free, not destroy, as we never touched the filesystem */\n>                 tmp_objdir_free(t);\n>                 return NULL;\n> @@ -173,7 +177,7 @@ struct tmp_objdir *tmp_objdir_create(const char *prefix)\n>                 installed_handlers++;\n>         }\n>\n> -       if (setup_tmp_objdir(t->path.buf)) {\n> +       if (setup_tmp_objdir(t->path.buf, quiet)) {\n>                 tmp_objdir_destroy(t);\n>                 return NULL;\n>         }\n> diff --git a/tmp-objdir.h b/tmp-objdir.h\n> index 76efc7edee5..5072fb860d9 100644\n> --- a/tmp-objdir.h\n> +++ b/tmp-objdir.h\n> @@ -24,8 +24,15 @@ struct tmp_objdir;\n>  /*\n>   * Create a new temporary object directory with the specified prefix;\n>   * returns NULL on failure.\n> + *\n> + * The tmp_objdir_create() is an a wrapper for\n> + * tmp_objdir_create_gently(..., 1).\n>   */\n> -struct tmp_objdir *tmp_objdir_create(const char *prefix);\n> +struct tmp_objdir *tmp_objdir_create_gently(const char *prefix, int quiet);\n> +static inline struct tmp_objdir *tmp_objdir_create(const char *prefix)\n> +{\n> +       return tmp_objdir_create_gently(prefix, 1);\n> +}\n>\n>  /*\n>   * Return a list of environment strings, suitable for use with\n\nYeah, I think this suggests that switching from die() to die_errno()\nwas a mistake.  Your patch looks right (though most of it belongs as\npart of ns/tmp-objdir rather than this series), but I think it makes\nthe code uglier and I don't see why this theoretical error path is\nworth all this trouble.  A die() is totally sufficient here.\n"},{"id":"446535","messageId":"CABPp-BHFk9hEFF92W0=MuQ0qOMuSmWz10xV0dYHzZAa=fDuNYA@mail.gmail.com","threadId":"57133","inReplyTo":"220119.86h79zmzy5.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v3 3/9] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-20T03:29:13Z","receivedAt":"2022-01-20T03:29:29Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Jan 19, 2022 at 8:51 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Thu, Dec 30 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > Note that my methodology included first modifying ll_merge() to return\n> > a struct, so that the compiler would catch all the callers for me and\n> > ensure I had modified all of them.  After modifying all of them, I then\n> > changed the struct to an enum.\n> > [...]\n> > -int ll_merge(mmbuffer_t *result_buf,\n> > +enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n> >            const char *path,\n> >            mmfile_t *ancestor, const char *ancestor_label,\n> >            mmfile_t *ours, const char *our_label,\n> > diff --git a/ll-merge.h b/ll-merge.h\n> > index aceb1b24132..e4a20e81a3a 100644\n> > --- a/ll-merge.h\n> > +++ b/ll-merge.h\n> > @@ -82,13 +82,20 @@ struct ll_merge_options {\n> >       long xdl_opts;\n> >  };\n> >\n> > +enum ll_merge_result {\n> > +     LL_MERGE_ERROR = -1,\n> > +     LL_MERGE_OK = 0,\n> > +     LL_MERGE_CONFLICT,\n> > +     LL_MERGE_BINARY_CONFLICT,\n> > +};\n> > +\n>\n> Isn't the other side of the enum checking missing in many cases?\n>\n> E.g. ll_ext_merge() returns \"enum ll_merge_result\" now, and does:\n>\n>         status = run_command_v_opt(args, RUN_USING_SHELL);\n>         ret = (status > 0) ? LL_MERGE_CONFLICT : status;\n>\n> And grepping at the tip of this series shows:\n>\n>     $ git grep LL_MERGE_OK\n>     ll-merge.c:             ret = LL_MERGE_OK;\n>     ll-merge.c:                     ret = LL_MERGE_OK;\n>     ll-merge.c:                     ret = LL_MERGE_OK;\n>     ll-merge.h:     LL_MERGE_OK = 0,\n>\n> Similar for LL_MERGE_CONFLICT, the only one that's used outside of the\n> file itself and its header is LL_MERGE_BINARY_CONFLICT.\n>\n> I.e. shouldn't these codepaths:\n>\n>     git grep -w ll_merge\n>\n> Be doing a switch() on that new enum? E.g. we lose the type in\n> three_way_merge() in apply.c, it seems to me that that function should\n> switch over this new enum, and return the \"int\" that the callers of\n> three_way_merge() care about (i.e. just <0, 0, 1, not this enum's -1, 0,\n> 1, 2.\n\nActually, three_way_merge()'s callers operate on <0, 0, >0; 1 is not\nspecial.  That's also the interface that ll_merge() traditionally\nalways used, and which still mostly applies, it's just they now want\nto differentiate between two of the >0 cases (namely LL_MERGE_CONFLICT\nvs. LL_MERGE_BINARY_CONFLICT).\n\nThat may sound like a big change, but since every single caller can\njust specially check for LL_MERGE_BINARY_CONFLICT as a first step if\nthey care (some callers don't), and then drop back to using old code\nas-is that assumes <0 vs. 0 vs. >0, that seems like a lot simpler\nchange.  And that's what this patch does.\n\nTrying to convert all these callers over to switch statements seems\nlike unnecessary churn to me.  Since Junio has already reviewed a\nformer round of this patch and found it to his liking (modulo a\ncompletely different one-line issue that I since corrected), I think\nI'd like to stick with the patch as-is.  If folks feel really strongly\nabout changing something here, though, I can change the return type of\nthe ll_*merge() functions back to int.\n"},{"id":"446541","messageId":"CABPp-BFqmFp7rY+aB2oHb=4u2JHZCZzhg+tz5DETEM5fZknKEg@mail.gmail.com","threadId":"57133","inReplyTo":"CABPp-BGsf8pcEZrY3vuq0yszy9G57fVa5jMSjT3H+sgDe6hWGg@mail.gmail.com","subject":"Re: [PATCH v3 1/9] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-20T07:53:03Z","receivedAt":"2022-01-20T07:53:18Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Jan 19, 2022 at 6:31 PM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Wed, Jan 19, 2022 at 7:53 AM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n> >\n> > On Thu, Dec 30 2021, Elijah Newren via GitGitGadget wrote:\n> >\n> > > From: Elijah Newren <newren@gmail.com>\n> >\n> > > +static int do_remerge_diff(struct rev_info *opt,\n> > > +                        struct commit_list *parents,\n> > > +                        struct object_id *oid,\n> > > +                        struct commit *commit)\n> > > +{\n> > > +     struct merge_options o;\n> > > +     struct commit_list *bases;\n> > > +     struct merge_result res = {0};\n> > > +     struct pretty_print_context ctx = {0};\n> > > +     struct commit *parent1 = parents->item;\n> > > +     struct commit *parent2 = parents->next->item;\n> > > +     struct strbuf parent1_desc = STRBUF_INIT;\n> > > +     struct strbuf parent2_desc = STRBUF_INIT;\n> > > +\n> > > +     /* Setup merge options */\n> > > +     init_merge_options(&o, the_repository);\n> > > +     o.show_rename_progress = 0;\n> > > +\n> > > +     ctx.abbrev = DEFAULT_ABBREV;\n> > > +     format_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n> > > +     format_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n> > > +     o.branch1 = parent1_desc.buf;\n> > > +     o.branch2 = parent2_desc.buf;\n> > > +\n> > > +     /* Parse the relevant commits and get the merge bases */\n> > > +     parse_commit_or_die(parent1);\n> > > +     parse_commit_or_die(parent2);\n> > > +     bases = get_merge_bases(parent1, parent2);\n> >\n> > There's existing leaks all over the place here unrelated to this new\n> > code, so this is no big deal, but I noticed that get_merge_bases() here\n> > leaks.\n>\n> Interesting.\n>\n> > Shouldn't it call free_commit_list() like e.g. diff_get_merge_base()\n> > which invokes get_merge_bases() does on the return value?\n\n...\n> So, yes, I think there's a leak, but it's not due to this patch.  It's\n> one that has been around since...the introduction of merge-recursive\n> (though it originally computed the merge bases internally rather than\n> allowing them to be passed in).  So, it's been around for quite a\n> while.\n>\n> I'll look into it, and see if I can come up with a fix, but it doesn't\n> really belong in this series.  I'll submit it separately.\n>\n> Thanks for the report.\n\nFix over here: https://lore.kernel.org/git/pull.1200.git.git.1642664835.gitgitgadget@gmail.com/T/\n\nHas a couple bonus memory leak fixes too.\n"},{"id":"446555","messageId":"220120.868rvamyu9.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"78ec1f44e4eacdb05c9da749a2160f4748565ad3.1640907369.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 8/9] show, log: include conflict/warning messages in --remerge-diff headers","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-01-19T16:19:14Z","receivedAt":"2022-01-20T11:27:42Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Dec 30 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Conflicts such as modify/delete, rename/rename, or file/directory are\n> not representable via content conflict markers, and the normal output\n> messages notifying users about these were dropped with --remerge-diff.\n> While we don't want these messages randomly shown before the commit\n> and diff headers, we do want them to still be shown; include them as\n> part of the diff headers instead.\n> [...]\n> +test_expect_success 'setup non-content conflicts' '\n> +\tgit switch --orphan base &&\n> +\n> +\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n> +\ttest_write_lines a b c d e f g h i >letters &&\n> +\ttest_write_lines in the way >content &&\n> +\tgit add numbers letters content &&\n> +\tgit commit -m base &&\n> +\n> +\tgit branch side1 &&\n> +\tgit branch side2 &&\n> +\n> +\tgit checkout side1 &&\n> +\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n> +\tgit mv letters letters_side1 &&\n> +\tgit mv content file_or_directory &&\n> +\tgit add numbers &&\n> +\tgit commit -m side1 &&\n> +\n> +\tgit checkout side2 &&\n> +\tgit rm numbers &&\n> +\tgit mv letters letters_side2 &&\n> +\tmkdir file_or_directory &&\n> +\techo hello >file_or_directory/world &&\n> +\tgit add file_or_directory/world &&\n> +\tgit commit -m side2 &&\n> +\n> +\tgit checkout -b resolution side1 &&\n> +\ttest_must_fail git merge side2 &&\n> +\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n> +\tgit add numbers &&\n> +\tgit add letters_side1 &&\n> +\tgit rm letters &&\n> +\tgit rm letters_side2 &&\n> +\tgit add file_or_directory~HEAD &&\n> +\tgit mv file_or_directory~HEAD wanted_content &&\n> +\tgit commit -m resolved\n> +'\n> +\n> +test_expect_success 'remerge-diff with non-content conflicts' '\n> +\tgit log -1 --oneline resolution >tmp &&\n> +\tcat <<-EOF >>tmp &&\n> +\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n> +\tsimilarity index 100%\n> +\trename from file_or_directory~HASH (side1)\n> +\trename to wanted_content\n> +\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n> +\tdiff --git a/letters b/letters\n> +\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n> +\tdiff --git a/letters_side2 b/letters_side2\n> +\tdeleted file mode 100644\n> +\tindex b236ae5..0000000\n> +\t--- a/letters_side2\n> +\t+++ /dev/null\n> +\t@@ -1,9 +0,0 @@\n> +\t-a\n> +\t-b\n> +\t-c\n> +\t-d\n> +\t-e\n> +\t-f\n> +\t-g\n> +\t-h\n> +\t-i\n> +\tdiff --git a/numbers b/numbers\n> +\tremerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n> +\tEOF\n> +\t# We still have some sha1 hashes above; rip them out so test works\n> +\t# with sha256\n> +\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n> +\n> +\tgit show --oneline --remerge-diff resolution >tmp &&\n> +\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n>  test_done\n\nRe my comment about --diff-filter in an earlier round, I think testing\nfor that option should really be added here.\n\nWith current master and seen:\n\n    $ git rev-parse origin/master\n    50b2d72e110cad39ecaf2322bfdf1b60cd13dd96\n    $ git rev-parse origin/seen\n    9e835a8bdafce2aaeb6df5f57f11014051bbfdca\n\nI will, with A, M, D get:\n\n    for i in A M D; do echo With $i: && git -P log --oneline --remerge-diff --diff-filter=$i origin/master..origin/seen; done\n\nSome of which is expected, and some of which is still weird, e.g.:\n    \n    $ git log --oneline --remerge-diff --diff-filter=D origin/master..origin/seen\n    d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n    diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n    remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n    61239ae3ee7 Merge branch 'pw/fix-some-issues-in-reset-head' into seen\n    diff --git a/reset.c b/reset.c\n    remerge CONFLICT (content): Merge conflict in reset.c\n    diff --git a/sequencer.c b/sequencer.c\n    remerge CONFLICT (content): Merge conflict in sequencer.c\n    9b44aca15e4 Merge branch 'hn/reftable-coverity-fixes' into jch\n    diff --git a/reftable/stack.c b/reftable/stack.c\n    remerge CONFLICT (content): Merge conflict in reftable/stack.c\n    [...]\n\nLet's take the jh/builtin-fsmonitor-part2 merge, with =M I get this\noutput:\n        \n    $ git -P show --oneline --remerge-diff --diff-filter=M d120673d7cc\n    d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n    diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n    remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n    index 03269b5553b..e70252ed65a 100755\n    --- a/t/perf/p7519-fsmonitor.sh\n    +++ b/t/perf/p7519-fsmonitor.sh\n    @@ -127,18 +127,11 @@ test_expect_success \"one time repo setup\" '\n            fi &&\n     \n            mkdir 1_file 10_files 100_files 1000_files 10000_files &&\n    -<<<<<<< 61239ae3ee7 (Merge branch 'pw/fix-some-issues-in-reset-head' into seen)\n    -       for i in $(test_seq 1 10); do touch 10_files/$i || return 1; done &&\n    -       for i in $(test_seq 1 100); do touch 100_files/$i || return 1; done &&\n    -       for i in $(test_seq 1 1000); do touch 1000_files/$i || return 1; done &&\n    -       for i in $(test_seq 1 10000); do touch 10000_files/$i || return 1; done &&\n    -=======\n            touch_files 1 &&\n            touch_files 10 &&\n            touch_files 100 &&\n            touch_files 1000 &&\n            touch_files 10000 &&\n    ->>>>>>> e89980feb1d (t7527: test status with untracked-cache and fsmonitor--daemon)\n            git add 1_file 10_files 100_files 1000_files 10000_files &&\n            git commit -qm \"Add files\" &&\n\nWhich is fully expected, i.e. here the diff is modified (M).\n\nBut there aren't any added lines, so why do I get it under =A, and why\nisn't the diff shown with =D (compare a normal 'git log --diff-filter=D\n-p')?:\n    \n    $ for i in A D; do echo With $i: && git -P show --oneline --remerge-diff --diff-filter=$i d120673d7cc; done\n    With A:\n    d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n    diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n    remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n    With D:\n    d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n    diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n    remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n\nFurthermore pathspec arguments seem to be broken. I.e. to use that\ncommit we can see without --remerge-diff that it's not directly modified\nin a non-merge in that range:\n    \n    $ git -P log --oneline origin/master..origin/next -- t/perf/p7519-fsmonitor.sh\n    d6f56f3248e Merge branch 'es/test-chain-lint' into next\n    \nBut this should surely work, but doesn't. It's faking up a diff with =M,\nso the pathspec filters should show it, shouldn't they?\n\n    $ for i in A M D; do git -P show --oneline --remerge-diff --diff-filter=$i d120673d7cc -- t/perf/p7519-fsmonitor.sh; done\n    $\n\nProbably what's happening is that the filtering is being done on the\npre-\"-remerge-diff\" output. I.e. the traversal code needs to be updated\nto inject modified paths into the commits we show --remerge-diff commits\nfor (but I'm just guessing).\n\nFor the rest of the --diff-filter flags the behavior also seems wrong, I\nreally didn't expect this to show any output:\n\n    $ for i in R T U X B; do echo With $i: && git -P show --oneline --remerge-diff --diff-filter=$i d120673d7cc; done\n    With R:\n    d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n    diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n    remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n    With T:\n    d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n    diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n    remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n    With U:\n    d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n    diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n    remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n    With X:\n    d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n    diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n    remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n    With B:\n    d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n    diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n    remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n\nI.e. we don't have a (R)ename, (T)type change, (U)nmerged (well, maybe,\nbut isn't it just for the index? See t6060-merge-index.sh) or Unknown\n(X) there. Are they all being shown because of that generic \"remerge\nCONFLICT\" line?\n\nIf the answer to all of the above is \"yes, some of it is weird or\nunintended, but let's deal with it later\" I'd think that would also be\nfine.\n\nBut let's then at least add something like what I added to the\ngit-range-diff.txt docs in df569c3f31f (range-diff doc: add a section\nabout output stability, 2018-11-09). I.e. explicitly say that we might\nchange the output when combined with other log options in the future,\nand that any combination not currently documented won't be supported.\n\nRe the CL mention of:\n    \n     * Ævar suggested also extending the docs with usage guidelines, but the\n       example he picked was IMO best handled by just add --remerge-diff, so I'm\n       not sure what to add to the docs. Maybe the log -S<string> --remerge-diff\n       example as a way to more reliably determine when a string was added to or\n       removed from the codebase? Where would that go anyway?\n\nI don't think we need to document how --remerge-diff interacts with -S,\n-G, or perhaps even most of --diff-filter.\n\nBut per the above it seems to me that we should at least have basic\ntests (perhaps TODO tests), or explicitly document/note that some of the\ninteractions are buggy/weird (or not, maybe I'm just missing something).\n\nThe same goes for some other diff options, particularly those where\nwe're showing output we didn't before because of --remerge-diff,\ne.g. --check is one such option. When I alter your tests with:\n    \n    diff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\n    index c1b44138145..d96320e6ab8 100755\n    --- a/t/t4069-remerge-diff.sh\n    +++ b/t/t4069-remerge-diff.sh\n    @@ -120,7 +120,8 @@ test_expect_success 'setup non-content conflicts' '\n     \n            git checkout -b resolution side1 &&\n            test_must_fail git merge side2 &&\n    -       test_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n    +       test_write_lines 1 2 three 4 5 6 7 8 >numbers &&\n    +       echo \"9 \" >>numbers &&\n            git add numbers &&\n            git add letters_side1 &&\n            git rm letters &&\n\nThe --check option works as expected, but we've got no test for the\ncombination of the two. Maybe we don't need them since we're confident\nenough in the shared machinery, but I'd think it would be better to\nconsider this a black box and test it. I.e. maybe another --check\nimplementation would filter on whatever we use for the pathspecs\n(showing it doesn't need to look at merge commits), and show nothing.\n    \nAll of the above is just noting the journy of testing this, i.e. \"hrm,\nwill it work with XYZ? No? Seems odd, and it's not tested at all...\".\n\nAs noted before I find the current output really useful already. I've\njust been trying to poke it in various ways to see if I can uncover any\nbugs or unintended behavior.\n"},{"id":"446613","messageId":"CABPp-BHjU+wDXNnf-rsGy86GvOFWH6qVLPEfAA2JDfLFRU4WSA@mail.gmail.com","threadId":"57133","inReplyTo":"220120.868rvamyu9.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v3 8/9] show, log: include conflict/warning messages in --remerge-diff headers","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-21T02:16:55Z","receivedAt":"2022-01-21T02:17:11Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Jan 20, 2022 at 3:27 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n>\n> On Thu, Dec 30 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Conflicts such as modify/delete, rename/rename, or file/directory are\n> > not representable via content conflict markers, and the normal output\n> > messages notifying users about these were dropped with --remerge-diff.\n> > While we don't want these messages randomly shown before the commit\n> > and diff headers, we do want them to still be shown; include them as\n> > part of the diff headers instead.\n> > [...]\n> > +test_expect_success 'setup non-content conflicts' '\n> > +     git switch --orphan base &&\n> > +\n> > +     test_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n> > +     test_write_lines a b c d e f g h i >letters &&\n> > +     test_write_lines in the way >content &&\n> > +     git add numbers letters content &&\n> > +     git commit -m base &&\n> > +\n> > +     git branch side1 &&\n> > +     git branch side2 &&\n> > +\n> > +     git checkout side1 &&\n> > +     test_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n> > +     git mv letters letters_side1 &&\n> > +     git mv content file_or_directory &&\n> > +     git add numbers &&\n> > +     git commit -m side1 &&\n> > +\n> > +     git checkout side2 &&\n> > +     git rm numbers &&\n> > +     git mv letters letters_side2 &&\n> > +     mkdir file_or_directory &&\n> > +     echo hello >file_or_directory/world &&\n> > +     git add file_or_directory/world &&\n> > +     git commit -m side2 &&\n> > +\n> > +     git checkout -b resolution side1 &&\n> > +     test_must_fail git merge side2 &&\n> > +     test_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n> > +     git add numbers &&\n> > +     git add letters_side1 &&\n> > +     git rm letters &&\n> > +     git rm letters_side2 &&\n> > +     git add file_or_directory~HEAD &&\n> > +     git mv file_or_directory~HEAD wanted_content &&\n> > +     git commit -m resolved\n> > +'\n> > +\n> > +test_expect_success 'remerge-diff with non-content conflicts' '\n> > +     git log -1 --oneline resolution >tmp &&\n> > +     cat <<-EOF >>tmp &&\n> > +     diff --git a/file_or_directory~HASH (side1) b/wanted_content\n> > +     similarity index 100%\n> > +     rename from file_or_directory~HASH (side1)\n> > +     rename to wanted_content\n> > +     remerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n> > +     diff --git a/letters b/letters\n> > +     remerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n> > +     diff --git a/letters_side2 b/letters_side2\n> > +     deleted file mode 100644\n> > +     index b236ae5..0000000\n> > +     --- a/letters_side2\n> > +     +++ /dev/null\n> > +     @@ -1,9 +0,0 @@\n> > +     -a\n> > +     -b\n> > +     -c\n> > +     -d\n> > +     -e\n> > +     -f\n> > +     -g\n> > +     -h\n> > +     -i\n> > +     diff --git a/numbers b/numbers\n> > +     remerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n> > +     EOF\n> > +     # We still have some sha1 hashes above; rip them out so test works\n> > +     # with sha256\n> > +     sed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n> > +\n> > +     git show --oneline --remerge-diff resolution >tmp &&\n> > +     sed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n> > +     test_cmp expect actual\n> > +'\n> > +\n> >  test_done\n>\n> Re my comment about --diff-filter in an earlier round, I think testing\n> for that option should really be added here.\n>\n> With current master and seen:\n>\n>     $ git rev-parse origin/master\n>     50b2d72e110cad39ecaf2322bfdf1b60cd13dd96\n>     $ git rev-parse origin/seen\n>     9e835a8bdafce2aaeb6df5f57f11014051bbfdca\n>\n> I will, with A, M, D get:\n>\n>     for i in A M D; do echo With $i: && git -P log --oneline --remerge-diff --diff-filter=$i origin/master..origin/seen; done\n>\n> Some of which is expected, and some of which is still weird, e.g.:\n>\n>     $ git log --oneline --remerge-diff --diff-filter=D origin/master..origin/seen\n>     d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n>     diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n>     remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n>     61239ae3ee7 Merge branch 'pw/fix-some-issues-in-reset-head' into seen\n>     diff --git a/reset.c b/reset.c\n>     remerge CONFLICT (content): Merge conflict in reset.c\n>     diff --git a/sequencer.c b/sequencer.c\n>     remerge CONFLICT (content): Merge conflict in sequencer.c\n>     9b44aca15e4 Merge branch 'hn/reftable-coverity-fixes' into jch\n>     diff --git a/reftable/stack.c b/reftable/stack.c\n>     remerge CONFLICT (content): Merge conflict in reftable/stack.c\n>     [...]\n\nThanks for the detailed testing and report.  Much appreciated.\n\nI agree that this output doesn't make sense...but...we might have a\nbit of a pickle.  I want to be able to do\n   $ git show --remerge-diff ${merge_commit}\nand have it show me whether the user did something to resolve that\nmerge.  That is NOT the same as asking whether the\nas-merged-as-possible-tree matches the final tree.  Some examples\nwhere those results differ:\n  * The original merge had a binary conflict.  Merge machinery bailed\nand put the copy from the first parent in the working tree.  User\nresolved it by keeping copy from the first parent.\n  * The original merge had a modify/delete conflict.  Merge machinery\nbailed and left the file from the modified side in place.  User\nresolved by keeping the file.\n  * Similar to the previous case, but rename/delete instead of modify/delete.\n  * Directory rename detection -- the merge machinery put the file in\nthe new directory but marked it as conflicted.  User decided they\nliked the location and kept it.\nIn any of the above cases, there were conflicts for the user to\nresolve, but none of them will show up by diffing the\ntree-at-start-of-conflict-resolution to the final-tree.  The only\nnotification we will have is the conflict header.  When I'm looking\nfor how users resolved merge conflicts, I do not want these merges to\nshow up empty for me (implying they were clean merges); that would\nmiss out on important information.\n\nPerhaps this means that the conflict header is a change of some type,\nbut not one that fits into the traditional ACDMRTUXB categories of\ndiff-filter?  Maybe we could attempt to categorize them (e.g. content\nconflicts are modifications, modify/delete are...deletes? adds?\nmodifies?), but I think madness and corner cases lie down that route.\nMy attempt to make these conflict headers show up despite empty diffs\nin the simple case (which required extra code in patch 7), caused it\nto also show up for all your diff-filter cases.  I agree it doesn't\nseem to make as much sense in your diff-filter cases...and I can\nremove it from them by removing my extra code in patch 7, but then I'm\nback to not seeing anything conflict headers for diffs that are\notherwise empty even when that is the info I'm looking for.\n\nWhat should be done?  Treat conflict notices as an \"unmerged\" type of\nchange (maybe flagged with diff-filter=U)?  Treat it as some new type\nof diff-filter change?  Something else?\n\nI'm open to bright ideas here.\n\n> Let's take the jh/builtin-fsmonitor-part2 merge, with =M I get this\n> output:\n>\n>     $ git -P show --oneline --remerge-diff --diff-filter=M d120673d7cc\n>     d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n>     diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n>     remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n>     index 03269b5553b..e70252ed65a 100755\n>     --- a/t/perf/p7519-fsmonitor.sh\n>     +++ b/t/perf/p7519-fsmonitor.sh\n>     @@ -127,18 +127,11 @@ test_expect_success \"one time repo setup\" '\n>             fi &&\n>\n>             mkdir 1_file 10_files 100_files 1000_files 10000_files &&\n>     -<<<<<<< 61239ae3ee7 (Merge branch 'pw/fix-some-issues-in-reset-head' into seen)\n>     -       for i in $(test_seq 1 10); do touch 10_files/$i || return 1; done &&\n>     -       for i in $(test_seq 1 100); do touch 100_files/$i || return 1; done &&\n>     -       for i in $(test_seq 1 1000); do touch 1000_files/$i || return 1; done &&\n>     -       for i in $(test_seq 1 10000); do touch 10000_files/$i || return 1; done &&\n>     -=======\n>             touch_files 1 &&\n>             touch_files 10 &&\n>             touch_files 100 &&\n>             touch_files 1000 &&\n>             touch_files 10000 &&\n>     ->>>>>>> e89980feb1d (t7527: test status with untracked-cache and fsmonitor--daemon)\n>             git add 1_file 10_files 100_files 1000_files 10000_files &&\n>             git commit -qm \"Add files\" &&\n>\n> Which is fully expected, i.e. here the diff is modified (M).\n>\n> But there aren't any added lines, so why do I get it under =A, and why\n> isn't the diff shown with =D (compare a normal 'git log --diff-filter=D\n> -p')?:\n\nNote that 'A' and 'D' diff filters are NOT about added/deleted lines,\nthey are about added/deleted files.  Those diff-filters will not find\nadd/deleted lines except when the entire file is added/deleted.  So\nthis part is fully expected.\n\n>     $ for i in A D; do echo With $i: && git -P show --oneline --remerge-diff --diff-filter=$i d120673d7cc; done\n>     With A:\n>     d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n>     diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n>     remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n>     With D:\n>     d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n>     diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n>     remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n\nYes, this is the same issue as the first one above: my extra code in\npatch 7 makes it always show the conflict headers (there are no add or\ndelete changes to show for this particular commit).\n\n> Furthermore pathspec arguments seem to be broken. I.e. to use that\n> commit we can see without --remerge-diff that it's not directly modified\n> in a non-merge in that range:\n>\n>     $ git -P log --oneline origin/master..origin/next -- t/perf/p7519-fsmonitor.sh\n>     d6f56f3248e Merge branch 'es/test-chain-lint' into next\n>\n> But this should surely work, but doesn't. It's faking up a diff with =M,\n> so the pathspec filters should show it, shouldn't they?\n>\n>     $ for i in A M D; do git -P show --oneline --remerge-diff --diff-filter=$i d120673d7cc -- t/perf/p7519-fsmonitor.sh; done\n>     $\n\nFor pinpointing this down a bit more, you could simplify this command\nline to just\n\n    $ git show --remerge-diff d120673d7cc -- t/perf/p7519-fsmonitor.sh\n    $\n\nand see that it doesn't include any output.  In other words, it's not\nan interaction with --diff-filter (or --oneline), but just the\npathspecs.\n\n(Also, it turns out that you can also leave out --remerge-diff and\nyou'll also get no output, but I'm not sure if that helps or hurts\nwith the \"debugging\", which I'll continue below.)\n\n> Probably what's happening is that the filtering is being done on the\n> pre-\"-remerge-diff\" output. I.e. the traversal code needs to be updated\n> to inject modified paths into the commits we show --remerge-diff commits\n> for (but I'm just guessing).\n\nYes, the filtering is being done on the commit before\ndo_remerge_diff() is ever called.  In fact, do_remerge_diff() is NOT\ncalled; it never even gets a chance to look at this commit.\n\nBut this isn't unique to remerge-diff; check this out:\n\n    $ git show --diff-merges=first-parent d120673d7cc --\nt/perf/p7519-fsmonitor.sh\n    $ git show --diff-merges=separate d120673d7cc -- t/perf/p7519-fsmonitor.sh\n    $\n\nThis might explain what's going on to you:\n\n    $ git diff --raw d120673d7cc^1 d120673d7cc -- t/perf/p7519-fsmonitor.sh\n    :100755 100755 c8be58f3c7 e70252ed65 M  t/perf/p7519-fsmonitor.sh\n    $ git diff --raw d120673d7cc^2 d120673d7cc -- t/perf/p7519-fsmonitor.sh\n    $\n\nBasically, git-log's (and git-show's) default history simplification\nsays \"oh, the file version matches what was seen in the second parent?\n UNINTERESTING.\"  Which is kinda broken when diffing against the first\nparent OR against the remerge-diff.\n\nThis particular issue is probably worth bringing up in the\nremerge-diff documentation (and perhaps also the diff-merges=separate\nand diff-merges=first-parent documentation), so that users know they\nmay want to specify --full-history together with these options.  (Or\nmaybe these particular types of diff-merges should just automatically\nturn full history on?)\n\nNow, just to show that this really is a history simplification issue,\nhere's the output when I include the --full-history flag together with\nyour command line:\n\n$ git show --remerge-diff --oneline --full-history --diff-filter=M\nd120673d7cc -- t/perf/p7519-fsmonitor.sh\nd120673d7c Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\ndiff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\nremerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\nindex 02b53bcaec..e70252ed65 100755\n--- a/t/perf/p7519-fsmonitor.sh\n+++ b/t/perf/p7519-fsmonitor.sh\n@@ -127,23 +127,11 @@ test_expect_success \"one time repo setup\" '\n        fi &&\n\n        mkdir 1_file 10_files 100_files 1000_files 10000_files &&\n-<<<<<<< 61239ae3ee (Merge branch 'pw/fix-some-issues-in-reset-head' into seen)\n-       for i in $(test_seq 1 10); do touch 10_files/$i || return 1; done &&\n-       for i in $(test_seq 1 100); do touch 100_files/$i || return 1; done &&\n-       for i in $(test_seq 1 1000); do touch 1000_files/$i || return 1; done &&\n-       for i in $(test_seq 1 10000); do touch 10000_files/$i ||\nreturn 1; done &&\n-||||||| 9d530dc002\n-       for i in $(test_seq 1 10); do touch 10_files/$i; done &&\n-       for i in $(test_seq 1 100); do touch 100_files/$i; done &&\n-       for i in $(test_seq 1 1000); do touch 1000_files/$i; done &&\n-       for i in $(test_seq 1 10000); do touch 10000_files/$i; done &&\n-=======\n        touch_files 1 &&\n        touch_files 10 &&\n        touch_files 100 &&\n        touch_files 1000 &&\n        touch_files 10000 &&\n->>>>>>> e89980feb1 (t7527: test status with untracked-cache and\nfsmonitor--daemon)\n        git add 1_file 10_files 100_files 1000_files 10000_files &&\n        git commit -qm \"Add files\" &&\n\nSo, for this pathspec with remerge-diff, we're at least behaving\nexactly as git-log is documented.  But I do agree that folks might not\nbe expecting the default history simplification that log/show do when\nusing these other diff types.\n\n> For the rest of the --diff-filter flags the behavior also seems wrong, I\n> really didn't expect this to show any output:\n>\n>     $ for i in R T U X B; do echo With $i: && git -P show --oneline --remerge-diff --diff-filter=$i d120673d7cc; done\n>     With R:\n>     d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n>     diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n>     remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n>     With T:\n>     d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n>     diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n>     remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n>     With U:\n>     d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n>     diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n>     remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n>     With X:\n>     d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n>     diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n>     remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n>     With B:\n>     d120673d7cc Merge branch 'jh/builtin-fsmonitor-part2' (early part) into seen\n>     diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh\n>     remerge CONFLICT (content): Merge conflict in t/perf/p7519-fsmonitor.sh\n>\n> I.e. we don't have a (R)ename, (T)type change, (U)nmerged (well, maybe,\n> but isn't it just for the index? See t6060-merge-index.sh) or Unknown\n> (X) there. Are they all being shown because of that generic \"remerge\n> CONFLICT\" line?\n\nHere you're repeating the same issue as at the beginning of the email,\nthough I guess just to check all the other diff-filter types besides\nA, M, and D?  I agree we want to be able to filter these away somehow,\nbut, if a merge had conflicts and was resolved without making any file\nchanges, I don't want those conflicts notices to be filtered away when\nI do a simple\n    $ git show --remerge-diff ${merge_commit}\nSo, we need a clever solution that handles both kinds of cases, and\nhopefully in a way that generically makes sense.  Perhaps the conflict\nnotices are selected by --diff-filter=U; maybe something else.  I'm\nopen to ideas.\n\n> If the answer to all of the above is \"yes, some of it is weird or\n> unintended, but let's deal with it later\" I'd think that would also be\n> fine.\n>\n> But let's then at least add something like what I added to the\n> git-range-diff.txt docs in df569c3f31f (range-diff doc: add a section\n> about output stability, 2018-11-09). I.e. explicitly say that we might\n> change the output when combined with other log options in the future,\n> and that any combination not currently documented won't be supported.\n>\n> Re the CL mention of:\n>\n>      * Ævar suggested also extending the docs with usage guidelines, but the\n>        example he picked was IMO best handled by just add --remerge-diff, so I'm\n>        not sure what to add to the docs. Maybe the log -S<string> --remerge-diff\n>        example as a way to more reliably determine when a string was added to or\n>        removed from the codebase? Where would that go anyway?\n>\n> I don't think we need to document how --remerge-diff interacts with -S,\n> -G, or perhaps even most of --diff-filter.\n>\n> But per the above it seems to me that we should at least have basic\n> tests (perhaps TODO tests), or explicitly document/note that some of the\n> interactions are buggy/weird (or not, maybe I'm just missing something).\n>\n> The same goes for some other diff options, particularly those where\n> we're showing output we didn't before because of --remerge-diff,\n> e.g. --check is one such option. When I alter your tests with:\n>\n>     diff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\n>     index c1b44138145..d96320e6ab8 100755\n>     --- a/t/t4069-remerge-diff.sh\n>     +++ b/t/t4069-remerge-diff.sh\n>     @@ -120,7 +120,8 @@ test_expect_success 'setup non-content conflicts' '\n>\n>             git checkout -b resolution side1 &&\n>             test_must_fail git merge side2 &&\n>     -       test_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n>     +       test_write_lines 1 2 three 4 5 6 7 8 >numbers &&\n>     +       echo \"9 \" >>numbers &&\n>             git add numbers &&\n>             git add letters_side1 &&\n>             git rm letters &&\n>\n> The --check option works as expected, but we've got no test for the\n> combination of the two. Maybe we don't need them since we're confident\n> enough in the shared machinery, but I'd think it would be better to\n> consider this a black box and test it. I.e. maybe another --check\n> implementation would filter on whatever we use for the pathspecs\n> (showing it doesn't need to look at merge commits), and show nothing.\n>\n> All of the above is just noting the journy of testing this, i.e. \"hrm,\n> will it work with XYZ? No? Seems odd, and it's not tested at all...\".\n>\n> As noted before I find the current output really useful already. I've\n> just been trying to poke it in various ways to see if I can uncover any\n> bugs or unintended behavior.\n\nVery helpful, thanks.  So, I think there are three issues here:\n\n   * default history simplification is surprising for\n--diff-merges={separate,first-parent,remerge}, particularly in\ncombination with pathspecs.  Document that, or just have these options\nturn on --full-history.\n   * more tests would be useful (though I'm worried about\ncombinatorial explosion, so I think just a few more would be good,\nparticularly around the --diff-filter options)\n   * interactions with --diff-filter are suboptimal.  We need\nsomething better, but if I revert my other changes to fix that issue\nthen I break the simple \"Did this merge have conflicts?\" usecase.  We\nneed some clever solution.\n"},{"id":"446633","messageId":"CABPp-BFgWH9Vn3oaiOe26ktSDUHzJZYzCa9FNpUxKt+r=PrRZw@mail.gmail.com","threadId":"57133","inReplyTo":"CABPp-BHjU+wDXNnf-rsGy86GvOFWH6qVLPEfAA2JDfLFRU4WSA@mail.gmail.com","subject":"Re: [PATCH v3 8/9] show, log: include conflict/warning messages in --remerge-diff headers","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-21T16:55:24Z","receivedAt":"2022-01-21T16:57:46Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Jan 20, 2022 at 6:16 PM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Thu, Jan 20, 2022 at 3:27 AM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n> >\n...\n> > As noted before I find the current output really useful already. I've\n> > just been trying to poke it in various ways to see if I can uncover any\n> > bugs or unintended behavior.\n>\n> Very helpful, thanks.  So, I think there are three issues here:\n>\n>    * default history simplification is surprising for\n> --diff-merges={separate,first-parent,remerge}, particularly in\n> combination with pathspecs.  Document that, or just have these options\n> turn on --full-history.\n>    * more tests would be useful (though I'm worried about\n> combinatorial explosion, so I think just a few more would be good,\n> particularly around the --diff-filter options)\n>    * interactions with --diff-filter are suboptimal.  We need\n> something better, but if I revert my other changes to fix that issue\n> then I break the simple \"Did this merge have conflicts?\" usecase.  We\n> need some clever solution.\n\nI tried out making the conflict headers be shown by --diff-filter=U\n*OR* whenever the associated diff for that file would be shown (e.g.\nif --diff-filter=R selects the file and it had a conflict header, it'd\nshow the conflict header.)  For diff-filters other than U that don't\nmatch the filediff, nothing for the file shows up.  Seems to work\ngreat, and cleans up the output quite a bit.\n\nI've added more tests, including one for pathspecs since that did need\njust a little more work.  I'll get it cleaned up and resubmitted soon.\n"},{"id":"446641","messageId":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v3.git.1640907369.gitgitgadget@gmail.com","subject":"[PATCH v4 00/10] Add a new --remerge-diff capability to show & log","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:10Z","receivedAt":"2022-01-21T19:12:26Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Here are some patches to add a --remerge-diff capability to show & log,\nwhich works by comparing merge commits to an automatic remerge (note that\nthe automatic remerge tree can contain files with conflict markers).\n\nChanges since v3:\n\n * Filter conflict headers according to pathspecs\n * Instead of always including conflict headers for all diff types, only\n   select them with --diff-filter=U OR whenever the associated diff in\n   question is selected\n * New testcases dealing with --diff-filter, pathspecs, and default history\n   simplification\n * Switched back from die_errno() to die()\n\nChanges NOT included (mostly because I'm not sure what to add or where):\n\n * Johannes Altimanninger suggested changing the ordering of the new headers\n   relative to other headers. He made a good point, but I also like having\n   the conflict messages next to the text, so I'm conflicted about what's\n   best.\n * (Technically not part of this feature, but kind of related.) Months ago,\n   Junio suggested documenting ${GIT_DIR}/AUTO_MERGE better\n   (https://lore.kernel.org/git/xmqqtuj4nepe.fsf@gitster.g/). I looked at\n   the time, but couldn't find a place to put it that made sense to me.\n\nChanges since v2 (of the restarted submission):\n\n * Numerous small improvements suggested by Johannes Altmanninger\n * Avoid including conflict messages from inner merges (due to example\n   pointed out by Ævar).\n * Added a \"remerge\" prefix to all the new diff headers (suggested by Junio\n   in a previous round, but I couldn't come up with a good name before. It\n   suddenly hit me that \"remerge\" is an obvious prefix to use, and even\n   helps explain what the rest of the line is for.)\n\nChanges since v1 (of the restarted submission, which technically was v2):\n\n * Restructured the series, so the first patch introduces the feature --\n   with a bunch of caveats. Subsequent patches clean up those caveats. This\n   avoids introducing not-yet-used functions, and hopefully makes review\n   easier.\n * added testcases\n * numerous small improvements suggested by Ævar and Junio\n\nChanges since original submission[1]:\n\n * Rebased on top of the version of ns/tmp-objdir that Neeraj submitted\n   (Neeraj's patches were based on v2.34, but ns/tmp-objdir got applied on\n   an old commit and does not even build because of that).\n * Modify ll-merge API to return a status, instead of printing \"Cannot merge\n   binary files\" on stdout[2] (as suggested by Peff)\n * Make conflict messages and other such warnings into diff headers of the\n   subsequent remerge-diff rather than appearing in the diff as file content\n   of some funny looking filenames (as suggested by Peff[3] and Junio[4])\n * Sergey ack'ed the diff-merges.c portion of the patches, but that wasn't\n   limited to one patch so not sure where to record that ack.\n\n[1]\nhttps://lore.kernel.org/git/pull.1080.git.git.1630376800.gitgitgadget@gmail.com/;\nGitHub wouldn't let me change the target branch for the PR, so I had to\ncreate a new one with the new base and thus the reason for not sending this\nas v2 even though it is. [2]\nhttps://lore.kernel.org/git/YVOZRhWttzF18Xql@coredump.intra.peff.net/,\nhttps://lore.kernel.org/git/YVOZty9D7NRbzhE5@coredump.intra.peff.net/ [3]\nhttps://lore.kernel.org/git/YVOXPTjsp9lrxmS6@coredump.intra.peff.net/ [4]\nhttps://lore.kernel.org/git/xmqqr1d7e4ug.fsf@gitster.g/\n\n=== FURTHER BACKGROUND (original cover letter material) ==\n\nHere are some example commits you can try this out on (with git show\n--remerge-diff $COMMIT):\n\n * git.git conflicted merge: 07601b5b36\n * git.git non-conflicted change: bf04590ecd\n * linux.git conflicted merge: eab3540562fb\n * linux.git non-conflicted change: 223cea6a4f05\n\nMany more can be found by just running git log --merges --remerge-diff in\nyour repository of choice and searching for diffs (most merges tend to be\nclean and unmodified and thus produce no diff but a search of '^diff' in the\nlog output tends to find the examples nicely).\n\nSome basic high level details about this new option:\n\n * This option is most naturally compared to --cc, though the output seems\n   to be much more understandable to most users than --cc output.\n * Since merges are often clean and unmodified, this new option results in\n   an empty diff for most merges.\n * This new option shows things like the removal of conflict markers, which\n   hunks users picked from the various conflicted sides to keep or remove,\n   and shows changes made outside of conflict markers (which might reflect\n   changes needed to resolve semantic conflicts or cleanups of e.g.\n   compilation warnings or other additional changes an integrator felt\n   belonged in the merged result).\n * This new option does not (currently) work for octopus merges, since\n   merge-ort is specific to two-parent merges[1].\n * This option will not work on a read-only or full filesystem[2].\n * We discussed this capability at Git Merge 2020, and one of the\n   suggestions was doing a periodic git gc --auto during the operation (due\n   to potential new blobs and trees created during the operation). I found a\n   way to avoid that; see [2].\n * This option is faster than you'd probably expect; it handles 33.5 merge\n   commits per second in linux.git on my computer; see below.\n\nIn regards to the performance point above, the timing for running the\nfollowing command:\n\ntime git log --min-parents=2 --max-parents=2 $DIFF_FLAG | wc -l\n\n\nin linux.git (with v5.4 checked out, since my copy of linux is very out of\ndate) is as follows:\n\nDIFF_FLAG=--cc:            71m 31.536s\nDIFF_FLAG=--remerge-diff:  31m  3.170s\n\n\nNote that there are 62476 merges in this history. Also, output size is:\n\nDIFF_FLAG=--cc:            2169111 lines\nDIFF_FLAG=--remerge-diff:  2458020 lines\n\n\nSo roughly the same amount of output as --cc, as you'd expect.\n\nAs a side note: git log --remerge-diff, when run in various repositories and\nallowed to run all the way back to the beginning(s) of history, is a nice\nstress test of sorts for merge-ort. Especially when users run it for you on\ntheir repositories they are working on, whether intentionally or via a bug\nin a tool triggering that command to be run unexpectedly. Long story short,\nsuch a bug in an internal tool existed in December 2020 and this command was\nrun on an internal repository and found a platform-specific bug in merge-ort\non some really old merge commit from that repo. I fixed that bug (a\nSTABLE_QSORT thing) while upstreaming all the merge-ort patches in the mean\ntime, but it was nice getting extra testing. Having more folks run this on\ntheir repositories might be useful extra testing of the new merge strategy.\n\nAlso, I previously mentioned --remerge-diff-only (a flag to show how\ncherry-picks or reverts differ from an automatic cherry-pick or revert, in\naddition to showing how merges differ from an automatic merge). This series\ndoes not include the patches to introduce that option; I'll submit them\nlater.\n\nTwo other things that might be interesting but are not included and which I\nhaven't investigated:\n\n * some mechanism for passing extra merge options through (e.g.\n   -Xignore-space-change)\n * a capability to compare the automatic merge to a second automatic merge\n   done with different merge options. (Not sure if this would be of interest\n   to end users, but might be interesting while developing new a\n   --strategy-option, or maybe checking how changing some default in the\n   merge algorithm would affect historical merges in various repositories).\n\n[1] I have nebulous ideas of how an Octopus-centric ORT strategy could be\nwritten -- basically, just repeatedly invoking ort and trying to make sure\nnested conflicts can be differentiated. For now, though, a simple warning is\nprinted that octopus merges are not handled and no diff will be shown. [2]\nNew blobs/trees can be written by the three-way merging step. These are\nwritten to a temporary area (via tmp-objdir.c) under the git object store\nthat is cleaned up at the end of the operation, with the new loose objects\nfrom the remerge being cleaned up after each individual merge.\n\nElijah Newren (10):\n  show, log: provide a --remerge-diff capability\n  log: clean unneeded objects during `log --remerge-diff`\n  ll-merge: make callers responsible for showing warnings\n  merge-ort: capture and print ll-merge warnings in our preferred\n    fashion\n  merge-ort: mark a few more conflict messages as omittable\n  merge-ort: format messages slightly different for use in headers\n  diff: add ability to insert additional headers for paths\n  show, log: include conflict/warning messages in --remerge-diff headers\n  merge-ort: mark conflict/warning messages from inner merges as\n    omittable\n  diff-merges: avoid history simplifications when diffing merges\n\n Documentation/diff-options.txt |  10 +-\n apply.c                        |   5 +-\n builtin/checkout.c             |  12 +-\n builtin/log.c                  |  15 ++\n diff-merges.c                  |  14 ++\n diff.c                         | 124 +++++++++++++-\n diff.h                         |   3 +-\n ll-merge.c                     |  40 +++--\n ll-merge.h                     |   9 +-\n log-tree.c                     | 118 +++++++++++++-\n merge-blobs.c                  |   5 +-\n merge-ort.c                    |  55 ++++++-\n merge-ort.h                    |  10 ++\n merge-recursive.c              |   9 +-\n merge-recursive.h              |   2 +\n notes-merge.c                  |   5 +-\n rerere.c                       |   9 +-\n revision.h                     |   6 +-\n t/t4069-remerge-diff.sh        | 290 +++++++++++++++++++++++++++++++++\n t/t6404-recursive-merge.sh     |   9 +-\n t/t6406-merge-attr.sh          |   9 +-\n tmp-objdir.c                   |   5 +\n tmp-objdir.h                   |   6 +\n 23 files changed, 722 insertions(+), 48 deletions(-)\n create mode 100755 t/t4069-remerge-diff.sh\n\n\nbase-commit: 4e44121c2d7bced65e25eb7ec5156290132bec94\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1103%2Fnewren%2Fremerge-diff-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1103/newren/remerge-diff-v4\nPull-Request: https://github.com/gitgitgadget/git/pull/1103\n\nRange-diff vs v3:\n\n  1:  d57ae218cf9 !  1:  0b94724311d show, log: provide a --remerge-diff capability\n     @@ builtin/log.c: static int cmd_log_walk(struct rev_info *rev)\n      +\tif (rev->remerge_diff) {\n      +\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n      +\t\tif (!remerge_objdir)\n     -+\t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n     ++\t\t\tdie(_(\"unable to create temporary object directory\"));\n      +\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n      +\t}\n       \n     @@ t/t4069-remerge-diff.sh (new)\n      +\n      +. ./test-lib.sh\n      +\n     ++# This test is ort-specific\n     ++test \"${GIT_TEST_MERGE_ALGORITHM:-ort}\" = ort || {\n     ++\tskip_all=\"GIT_TEST_MERGE_ALGORITHM != ort\"\n     ++\ttest_done\n     ++}\n     ++\n      +test_expect_success 'setup basic merges' '\n      +\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n      +\tgit add numbers &&\n  2:  798625b53f2 !  2:  f06de6c1b2f log: clean unneeded objects during `log --remerge-diff`\n     @@ builtin/log.c: static int cmd_log_walk(struct rev_info *rev)\n      -\t\tif (!remerge_objdir)\n      +\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n      +\t\tif (!rev->remerge_objdir)\n     - \t\t\tdie_errno(_(\"unable to create temporary object directory\"));\n     + \t\t\tdie(_(\"unable to create temporary object directory\"));\n      -\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n      +\t\ttmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n       \t}\n  3:  b952f674df1 =  3:  8d6c3d48f0e ll-merge: make callers responsible for showing warnings\n  4:  e8cf1626960 =  4:  de8e8f88fa4 merge-ort: capture and print ll-merge warnings in our preferred fashion\n  5:  4d1848c8a29 =  5:  6b535a4d55a merge-ort: mark a few more conflict messages as omittable\n  6:  81e736b847e =  6:  e2441608c63 merge-ort: format messages slightly different for use in headers\n  7:  5000a94aa98 !  7:  62734beb693 diff: add ability to insert additional headers for paths\n     @@ diff.c: int diff_unmodified_pair(struct diff_filepair *p)\n       static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n       {\n      -\tif (diff_unmodified_pair(p))\n     ++\tint include_conflict_headers =\n     ++\t    (additional_headers(o, p->one->path) &&\n     ++\t     (!o->filter || filter_bit_tst(DIFF_STATUS_UNMERGED, o)));\n     ++\n      +\t/*\n      +\t * Check if we can return early without showing a diff.  Note that\n      +\t * diff_filepair only stores {oid, path, mode, is_valid}\n      +\t * information for each path, and thus diff_unmodified_pair() only\n      +\t * considers those bits of info.  However, we do not want pairs\n     -+\t * created by create_filepairs_for_header_only_notifications() to\n     -+\t * be ignored, so return early if both p is unmodified AND\n     -+\t * p->one->path is not in additional headers.\n     ++\t * created by create_filepairs_for_header_only_notifications()\n     ++\t * (which always look like unmodified pairs) to be ignored, so\n     ++\t * return early if both p is unmodified AND we don't want to\n     ++\t * include_conflict_headers.\n      +\t */\n     -+\tif (diff_unmodified_pair(p) && !additional_headers(o, p->one->path))\n     ++\tif (diff_unmodified_pair(p) && !include_conflict_headers)\n       \t\treturn;\n       \n      +\t/* Actually, we can also return early to avoid showing tree diffs */\n     @@ diff.c: static void diff_flush_checkdiff(struct diff_filepair *p,\n       {\n       \tstruct diff_queue_struct *q = &diff_queued_diff;\n       \tint i;\n     ++\tint include_conflict_headers =\n     ++\t    (o->additional_path_headers &&\n     ++\t     (!o->filter || filter_bit_tst(DIFF_STATUS_UNMERGED, o)));\n      +\n     -+\tif (o->additional_path_headers &&\n     -+\t    !strmap_empty(o->additional_path_headers))\n     ++\tif (include_conflict_headers)\n      +\t\treturn 0;\n     ++\n       \tfor (i = 0; i < q->nr; i++)\n       \t\tif (!diff_unmodified_pair(q->queue[i]))\n       \t\t\treturn 0;\n     @@ diff.h: void diffcore_fix_diff_index(void);\n       \"  -a  --text    treat all files as text.\\n\"\n       \n      -int diff_queue_is_empty(void);\n     -+int diff_queue_is_empty(struct diff_options*);\n     ++int diff_queue_is_empty(struct diff_options *o);\n       void diff_flush(struct diff_options*);\n       void diff_free(struct diff_options*);\n       void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);\n  8:  78ec1f44e4e !  8:  17eccf7e0d6 show, log: include conflict/warning messages in --remerge-diff headers\n     @@ Commit message\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## log-tree.c ##\n     +@@\n     + #include \"line-log.h\"\n     + #include \"help.h\"\n     + #include \"range-diff.h\"\n     ++#include \"strmap.h\"\n     + \n     + static struct decoration name_decoration = { \"object names\" };\n     + static int decoration_loaded;\n     +@@ log-tree.c: static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n     + \treturn !opt->loginfo;\n     + }\n     + \n     ++static void setup_additional_headers(struct diff_options *o,\n     ++\t\t\t\t     struct strmap *all_headers)\n     ++{\n     ++\tstruct hashmap_iter iter;\n     ++\tstruct strmap_entry *entry;\n     ++\n     ++\t/*\n     ++\t * Make o->additional_path_headers contain the subset of all_headers\n     ++\t * that match o->pathspec.  If there aren't any that match o->pathspec,\n     ++\t * then make o->additional_path_headers be NULL.\n     ++\t */\n     ++\n     ++\tif (!o->pathspec.nr) {\n     ++\t\to->additional_path_headers = all_headers;\n     ++\t\treturn;\n     ++\t}\n     ++\n     ++\to->additional_path_headers = xmalloc(sizeof(struct strmap));\n     ++\tstrmap_init_with_options(o->additional_path_headers, NULL, 0);\n     ++\tstrmap_for_each_entry(all_headers, &iter, entry) {\n     ++\t\tif (match_pathspec(the_repository->index, &o->pathspec,\n     ++\t\t\t\t   entry->key, strlen(entry->key),\n     ++\t\t\t\t   0 /* prefix */, NULL /* seen */,\n     ++\t\t\t\t   0 /* is_dir */))\n     ++\t\t\tstrmap_put(o->additional_path_headers,\n     ++\t\t\t\t   entry->key, entry->value);\n     ++\t}\n     ++\tif (!strmap_get_size(o->additional_path_headers)) {\n     ++\t\tstrmap_clear(o->additional_path_headers, 0);\n     ++\t\tFREE_AND_NULL(o->additional_path_headers);\n     ++\t}\n     ++}\n     ++\n     ++static void cleanup_additional_headers(struct diff_options *o)\n     ++{\n     ++\tif (!o->pathspec.nr) {\n     ++\t\to->additional_path_headers = NULL;\n     ++\t\treturn;\n     ++\t}\n     ++\tif (!o->additional_path_headers)\n     ++\t\treturn;\n     ++\n     ++\tstrmap_clear(o->additional_path_headers, 0);\n     ++\tFREE_AND_NULL(o->additional_path_headers);\n     ++}\n     ++\n     + static int do_remerge_diff(struct rev_info *opt,\n     + \t\t\t   struct commit_list *parents,\n     + \t\t\t   struct object_id *oid,\n      @@ log-tree.c: static int do_remerge_diff(struct rev_info *opt,\n       \t/* Setup merge options */\n       \tinit_merge_options(&o, the_repository);\n     @@ log-tree.c: static int do_remerge_diff(struct rev_info *opt,\n       \tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n       \n       \t/* Show the diff */\n     -+\topt->diffopt.additional_path_headers = res.path_messages;\n     ++\tsetup_additional_headers(&opt->diffopt, res.path_messages);\n       \tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n       \tlog_tree_diff_flush(opt);\n       \n       \t/* Cleanup */\n     -+\topt->diffopt.additional_path_headers = NULL;\n     ++\tcleanup_additional_headers(&opt->diffopt);\n       \tstrbuf_release(&parent1_desc);\n       \tstrbuf_release(&parent2_desc);\n       \tmerge_finalize(&o, &res);\n     @@ merge-ort.h: struct merge_result {\n       \t * to merge_incore_*().  Includes data needed to update the index (if\n      \n       ## t/t4069-remerge-diff.sh ##\n     -@@ t/t4069-remerge-diff.sh: test_description='remerge-diff handling'\n     - \n     - . ./test-lib.sh\n     - \n     -+# --remerge-diff uses ort under the hood regardless of setting.  However,\n     -+# we set up a file/directory conflict beforehand, and the different backends\n     -+# handle the conflict differently, which would require separate code paths\n     -+# to resolve.  There's not much point in making the code uglier to do that,\n     -+# though, when the real thing we are testing (--remerge-diff) will hardcode\n     -+# calls directly into the merge-ort API anyway.  So just force the use of\n     -+# ort on the setup too.\n     -+GIT_TEST_MERGE_ALGORITHM=ort\n     -+\n     - test_expect_success 'setup basic merges' '\n     - \ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n     - \tgit add numbers &&\n      @@ t/t4069-remerge-diff.sh: test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n       \tgit log -1 --oneline ab_resolution >tmp &&\n       \tcat <<-EOF >>tmp &&\n     @@ t/t4069-remerge-diff.sh: test_expect_success 'remerge-diff with both a resolved\n      +\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n      +\ttest_cmp expect actual\n      +'\n     ++\n     ++test_expect_success 'remerge-diff w/ diff-filter=U: all conflict headers, no diff content' '\n     ++\tgit log -1 --oneline resolution >tmp &&\n     ++\tcat <<-EOF >>tmp &&\n     ++\tdiff --git a/file_or_directory~HASH (side1) b/file_or_directory~HASH (side1)\n     ++\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n     ++\tdiff --git a/letters b/letters\n     ++\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n     ++\tdiff --git a/numbers b/numbers\n     ++\tremerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n     ++\tEOF\n     ++\t# We still have some sha1 hashes above; rip them out so test works\n     ++\t# with sha256\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n     ++\n     ++\tgit show --oneline --remerge-diff --diff-filter=U resolution >tmp &&\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'remerge-diff w/ diff-filter=R: relevant file + conflict header' '\n     ++\tgit log -1 --oneline resolution >tmp &&\n     ++\tcat <<-EOF >>tmp &&\n     ++\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n     ++\tsimilarity index 100%\n     ++\trename from file_or_directory~HASH (side1)\n     ++\trename to wanted_content\n     ++\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n     ++\tEOF\n     ++\t# We still have some sha1 hashes above; rip them out so test works\n     ++\t# with sha256\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n     ++\n     ++\tgit show --oneline --remerge-diff --diff-filter=R resolution >tmp &&\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'remerge-diff w/ pathspec: limits to relevant file including conflict header' '\n     ++\tgit log -1 --oneline resolution >tmp &&\n     ++\tcat <<-EOF >>tmp &&\n     ++\tdiff --git a/letters b/letters\n     ++\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n     ++\tdiff --git a/letters_side2 b/letters_side2\n     ++\tdeleted file mode 100644\n     ++\tindex b236ae5..0000000\n     ++\t--- a/letters_side2\n     ++\t+++ /dev/null\n     ++\t@@ -1,9 +0,0 @@\n     ++\t-a\n     ++\t-b\n     ++\t-c\n     ++\t-d\n     ++\t-e\n     ++\t-f\n     ++\t-g\n     ++\t-h\n     ++\t-i\n     ++\tEOF\n     ++\t# We still have some sha1 hashes above; rip them out so test works\n     ++\t# with sha256\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n     ++\n     ++\tgit show --oneline --remerge-diff --full-history resolution -- \"letters*\" >tmp &&\n     ++\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n      +\n       test_done\n  9:  64b44ee84f3 =  9:  b3e7656cfc6 merge-ort: mark conflict/warning messages from inner merges as omittable\n  -:  ----------- > 10:  ea5df61cf35 diff-merges: avoid history simplifications when diffing merges\n\n-- \ngitgitgadget\n"},{"id":"446642","messageId":"0b94724311df34dd10debd43c466695ed406d790.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 01/10] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:11Z","receivedAt":"2022-01-21T19:12:28Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen this option is specified, we remerge all (two parent) merge commits\nand diff the actual merge commit to the automatically created version,\nin order to show how users removed conflict markers, resolved the\ndifferent conflict versions, and potentially added new changes outside\nof conflict regions in order to resolve semantic merge problems (or,\npossibly, just to hide other random changes).\n\nThis capability works by creating a temporary object directory and\nmarking it as the primary object store.  This makes it so that any blobs\nor trees created during the automatic merge are easily removable\nafterwards by just deleting all objects from the temporary object\ndirectory.\n\nThere are a few ways that this implementation is suboptimal:\n  * `log --remerge-diff` becomes slow, because the temporary object\n    directory can fill with many loose objects while running\n  * the log output can be muddied with misplaced \"warning: cannot merge\n    binary files\" messages, since ll-merge.c unconditionally writes those\n    messages to stderr while running instead of allowing callers to\n    manage them.\n  * important conflict and warning messages are simply dropped; thus for\n    conflicts like modify/delete or rename/rename or file/directory which\n    are not representable with content conflict markers, there may be no\n    way for a user of --remerge-diff to know that there had been a\n    conflict which was resolved (and which possibly motivated other\n    changes in the merge commit).\n  * when fixing the previous issue, note that some unimportant conflict\n    and warning messages might start being included.  We should instead\n    make sure these remain dropped.\nSubsequent commits will address these issues.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n Documentation/diff-options.txt | 10 +++-\n builtin/log.c                  | 14 ++++++\n diff-merges.c                  | 12 +++++\n log-tree.c                     | 59 ++++++++++++++++++++++\n revision.h                     |  3 +-\n t/t4069-remerge-diff.sh        | 90 ++++++++++++++++++++++++++++++++++\n 6 files changed, 186 insertions(+), 2 deletions(-)\n create mode 100755 t/t4069-remerge-diff.sh\n\ndiff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\nindex c89d530d3d1..6b8175defe6 100644\n--- a/Documentation/diff-options.txt\n+++ b/Documentation/diff-options.txt\n@@ -34,7 +34,7 @@ endif::git-diff[]\n endif::git-format-patch[]\n \n ifdef::git-log[]\n---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc)::\n+--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r)::\n --no-diff-merges::\n \tSpecify diff format to be used for merge commits. Default is\n \t{diff-merges-default} unless `--first-parent` is in use, in which case\n@@ -64,6 +64,14 @@ ifdef::git-log[]\n \teach of the parents. Separate log entry and diff is generated\n \tfor each parent.\n +\n+--diff-merges=remerge:::\n+--diff-merges=r:::\n+--remerge-diff:::\n+\tWith this option, two-parent merge commits are remerged to\n+\tcreate a temporary tree object -- potentially containing files\n+\twith conflict markers and such.  A diff is then shown between\n+\tthat temporary tree and the actual merge commit.\n++\n --diff-merges=combined:::\n --diff-merges=c:::\n -c:::\ndiff --git a/builtin/log.c b/builtin/log.c\nindex f75d87e8d7f..846ba0f995a 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -35,6 +35,7 @@\n #include \"repository.h\"\n #include \"commit-reach.h\"\n #include \"range-diff.h\"\n+#include \"tmp-objdir.h\"\n \n #define MAIL_DEFAULT_WRAP 72\n #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n@@ -406,6 +407,14 @@ static int cmd_log_walk(struct rev_info *rev)\n \tstruct commit *commit;\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n+\tstruct tmp_objdir *remerge_objdir = NULL;\n+\n+\tif (rev->remerge_diff) {\n+\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\tif (!remerge_objdir)\n+\t\t\tdie(_(\"unable to create temporary object directory\"));\n+\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n+\t}\n \n \tif (rev->early_output)\n \t\tsetup_early_output();\n@@ -449,6 +458,9 @@ static int cmd_log_walk(struct rev_info *rev)\n \trev->diffopt.no_free = 0;\n \tdiff_free(&rev->diffopt);\n \n+\tif (rev->remerge_diff)\n+\t\ttmp_objdir_destroy(remerge_objdir);\n+\n \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n \t    rev->diffopt.flags.check_failed) {\n \t\treturn 02;\n@@ -1943,6 +1955,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tdie(_(\"--name-status does not make sense\"));\n \tif (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF)\n \t\tdie(_(\"--check does not make sense\"));\n+\tif (rev.remerge_diff)\n+\t\tdie(_(\"--remerge-diff does not make sense\"));\n \n \tif (!use_patch_format &&\n \t\t(!rev.diffopt.output_format ||\ndiff --git a/diff-merges.c b/diff-merges.c\nindex 5060ccd890b..0af4b3f9191 100644\n--- a/diff-merges.c\n+++ b/diff-merges.c\n@@ -17,6 +17,7 @@ static void suppress(struct rev_info *revs)\n \trevs->combined_all_paths = 0;\n \trevs->merges_imply_patch = 0;\n \trevs->merges_need_diff = 0;\n+\trevs->remerge_diff = 0;\n }\n \n static void set_separate(struct rev_info *revs)\n@@ -45,6 +46,12 @@ static void set_dense_combined(struct rev_info *revs)\n \trevs->dense_combined_merges = 1;\n }\n \n+static void set_remerge_diff(struct rev_info *revs)\n+{\n+\tsuppress(revs);\n+\trevs->remerge_diff = 1;\n+}\n+\n static diff_merges_setup_func_t func_by_opt(const char *optarg)\n {\n \tif (!strcmp(optarg, \"off\") || !strcmp(optarg, \"none\"))\n@@ -57,6 +64,8 @@ static diff_merges_setup_func_t func_by_opt(const char *optarg)\n \t\treturn set_combined;\n \telse if (!strcmp(optarg, \"cc\") || !strcmp(optarg, \"dense-combined\"))\n \t\treturn set_dense_combined;\n+\telse if (!strcmp(optarg, \"r\") || !strcmp(optarg, \"remerge\"))\n+\t\treturn set_remerge_diff;\n \telse if (!strcmp(optarg, \"m\") || !strcmp(optarg, \"on\"))\n \t\treturn set_to_default;\n \treturn NULL;\n@@ -110,6 +119,9 @@ int diff_merges_parse_opts(struct rev_info *revs, const char **argv)\n \t} else if (!strcmp(arg, \"--cc\")) {\n \t\tset_dense_combined(revs);\n \t\trevs->merges_imply_patch = 1;\n+\t} else if (!strcmp(arg, \"--remerge-diff\")) {\n+\t\tset_remerge_diff(revs);\n+\t\trevs->merges_imply_patch = 1;\n \t} else if (!strcmp(arg, \"--no-diff-merges\")) {\n \t\tsuppress(revs);\n \t} else if (!strcmp(arg, \"--combined-all-paths\")) {\ndiff --git a/log-tree.c b/log-tree.c\nindex 644893fd8cf..84ed864fc81 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -1,4 +1,5 @@\n #include \"cache.h\"\n+#include \"commit-reach.h\"\n #include \"config.h\"\n #include \"diff.h\"\n #include \"object-store.h\"\n@@ -7,6 +8,7 @@\n #include \"tag.h\"\n #include \"graph.h\"\n #include \"log-tree.h\"\n+#include \"merge-ort.h\"\n #include \"reflog-walk.h\"\n #include \"refs.h\"\n #include \"string-list.h\"\n@@ -902,6 +904,51 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n \treturn !opt->loginfo;\n }\n \n+static int do_remerge_diff(struct rev_info *opt,\n+\t\t\t   struct commit_list *parents,\n+\t\t\t   struct object_id *oid,\n+\t\t\t   struct commit *commit)\n+{\n+\tstruct merge_options o;\n+\tstruct commit_list *bases;\n+\tstruct merge_result res = {0};\n+\tstruct pretty_print_context ctx = {0};\n+\tstruct commit *parent1 = parents->item;\n+\tstruct commit *parent2 = parents->next->item;\n+\tstruct strbuf parent1_desc = STRBUF_INIT;\n+\tstruct strbuf parent2_desc = STRBUF_INIT;\n+\n+\t/* Setup merge options */\n+\tinit_merge_options(&o, the_repository);\n+\to.show_rename_progress = 0;\n+\n+\tctx.abbrev = DEFAULT_ABBREV;\n+\tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n+\tformat_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n+\to.branch1 = parent1_desc.buf;\n+\to.branch2 = parent2_desc.buf;\n+\n+\t/* Parse the relevant commits and get the merge bases */\n+\tparse_commit_or_die(parent1);\n+\tparse_commit_or_die(parent2);\n+\tbases = get_merge_bases(parent1, parent2);\n+\n+\t/* Re-merge the parents */\n+\tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n+\n+\t/* Show the diff */\n+\tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n+\tlog_tree_diff_flush(opt);\n+\n+\t/* Cleanup */\n+\tstrbuf_release(&parent1_desc);\n+\tstrbuf_release(&parent2_desc);\n+\tmerge_finalize(&o, &res);\n+\t/* TODO: clean up the temporary object directory */\n+\n+\treturn !opt->loginfo;\n+}\n+\n /*\n  * Show the diff of a commit.\n  *\n@@ -936,6 +983,18 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log\n \t}\n \n \tif (is_merge) {\n+\t\tint octopus = (parents->next->next != NULL);\n+\n+\t\tif (opt->remerge_diff) {\n+\t\t\tif (octopus) {\n+\t\t\t\tshow_log(opt);\n+\t\t\t\tfprintf(opt->diffopt.file,\n+\t\t\t\t\t\"diff: warning: Skipping remerge-diff \"\n+\t\t\t\t\t\"for octopus merges.\\n\");\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\treturn do_remerge_diff(opt, parents, oid, commit);\n+\t\t}\n \t\tif (opt->combine_merges)\n \t\t\treturn do_diff_combined(opt, commit);\n \t\tif (opt->separate_merges) {\ndiff --git a/revision.h b/revision.h\nindex 5578bb4720a..13178e6b8f3 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -195,7 +195,8 @@ struct rev_info {\n \t\t\tcombine_merges:1,\n \t\t\tcombined_all_paths:1,\n \t\t\tdense_combined_merges:1,\n-\t\t\tfirst_parent_merges:1;\n+\t\t\tfirst_parent_merges:1,\n+\t\t\tremerge_diff:1;\n \n \t/* Format info */\n \tint\t\tshow_notes;\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nnew file mode 100755\nindex 00000000000..5ef191f4fc9\n--- /dev/null\n+++ b/t/t4069-remerge-diff.sh\n@@ -0,0 +1,90 @@\n+#!/bin/sh\n+\n+test_description='remerge-diff handling'\n+\n+. ./test-lib.sh\n+\n+# This test is ort-specific\n+test \"${GIT_TEST_MERGE_ALGORITHM:-ort}\" = ort || {\n+\tskip_all=\"GIT_TEST_MERGE_ALGORITHM != ort\"\n+\ttest_done\n+}\n+\n+test_expect_success 'setup basic merges' '\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m base &&\n+\n+\tgit branch feature_a &&\n+\tgit branch feature_b &&\n+\tgit branch feature_c &&\n+\n+\tgit branch ab_resolution &&\n+\tgit branch bc_resolution &&\n+\n+\tgit checkout feature_a &&\n+\ttest_write_lines 1 2 three 4 5 6 7 eight 9 >numbers &&\n+\tgit commit -a -m change_a &&\n+\n+\tgit checkout feature_b &&\n+\ttest_write_lines 1 2 tres 4 5 6 7 8 9 >numbers &&\n+\tgit commit -a -m change_b &&\n+\n+\tgit checkout feature_c &&\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&\n+\tgit commit -a -m change_c &&\n+\n+\tgit checkout bc_resolution &&\n+\tgit merge --ff-only feature_b &&\n+\t# no conflict\n+\tgit merge feature_c &&\n+\n+\tgit checkout ab_resolution &&\n+\tgit merge --ff-only feature_a &&\n+\t# conflicts!\n+\ttest_must_fail git merge feature_b &&\n+\t# Resolve conflict...and make another change elsewhere\n+\ttest_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers &&\n+\tgit add numbers &&\n+\tgit merge --continue\n+'\n+\n+test_expect_success 'remerge-diff on a clean merge' '\n+\tgit log -1 --oneline bc_resolution >expect &&\n+\tgit show --oneline --remerge-diff bc_resolution >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' '\n+\tgit log -1 --oneline ab_resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/numbers b/numbers\n+\tindex a1fb731..6875544 100644\n+\t--- a/numbers\n+\t+++ b/numbers\n+\t@@ -1,13 +1,9 @@\n+\t 1\n+\t 2\n+\t-<<<<<<< b0ed5cb (change_a)\n+\t-three\n+\t-=======\n+\t-tres\n+\t->>>>>>> 6cd3f82 (change_b)\n+\t+drei\n+\t 4\n+\t 5\n+\t 6\n+\t 7\n+\t-eight\n+\t+acht\n+\t 9\n+\tEOF\n+\t# Hashes above are sha1; rip them out so test works with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff ab_resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"446643","messageId":"f06de6c1b2fbd5c5a23b6755197a3683c7d18d2f.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 02/10] log: clean unneeded objects during `log --remerge-diff`","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:12Z","receivedAt":"2022-01-21T19:12:31Z","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 --remerge-diff option will need to create new blobs and trees\nrepresenting the \"automatic merge\" state.  If one is traversing a\nlong project history, one can easily get hundreds of thousands of\nloose objects generated during `log --remerge-diff`.  However, none of\nthose loose objects are needed after we have completed our diff\noperation; they can be summarily deleted.\n\nAdd a new helper function to tmp_objdir to discard all the contained\nobjects, and call it after each merge is handled.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/log.c | 13 +++++++------\n log-tree.c    |  8 +++++++-\n revision.h    |  3 +++\n tmp-objdir.c  |  5 +++++\n tmp-objdir.h  |  6 ++++++\n 5 files changed, 28 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 846ba0f995a..ac550e1ae62 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -407,13 +407,12 @@ static int cmd_log_walk(struct rev_info *rev)\n \tstruct commit *commit;\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n-\tstruct tmp_objdir *remerge_objdir = NULL;\n \n \tif (rev->remerge_diff) {\n-\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n-\t\tif (!remerge_objdir)\n+\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\tif (!rev->remerge_objdir)\n \t\t\tdie(_(\"unable to create temporary object directory\"));\n-\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n+\t\ttmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n \t}\n \n \tif (rev->early_output)\n@@ -458,8 +457,10 @@ static int cmd_log_walk(struct rev_info *rev)\n \trev->diffopt.no_free = 0;\n \tdiff_free(&rev->diffopt);\n \n-\tif (rev->remerge_diff)\n-\t\ttmp_objdir_destroy(remerge_objdir);\n+\tif (rev->remerge_diff) {\n+\t\ttmp_objdir_destroy(rev->remerge_objdir);\n+\t\trev->remerge_objdir = NULL;\n+\t}\n \n \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n \t    rev->diffopt.flags.check_failed) {\ndiff --git a/log-tree.c b/log-tree.c\nindex 84ed864fc81..d4655b63d75 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -4,6 +4,7 @@\n #include \"diff.h\"\n #include \"object-store.h\"\n #include \"repository.h\"\n+#include \"tmp-objdir.h\"\n #include \"commit.h\"\n #include \"tag.h\"\n #include \"graph.h\"\n@@ -944,7 +945,12 @@ static int do_remerge_diff(struct rev_info *opt,\n \tstrbuf_release(&parent1_desc);\n \tstrbuf_release(&parent2_desc);\n \tmerge_finalize(&o, &res);\n-\t/* TODO: clean up the temporary object directory */\n+\n+\t/* Clean up the contents of the temporary object directory */\n+\tif (opt->remerge_objdir)\n+\t\ttmp_objdir_discard_objects(opt->remerge_objdir);\n+\telse\n+\t\tBUG(\"unable to remove temporary object directory\");\n \n \treturn !opt->loginfo;\n }\ndiff --git a/revision.h b/revision.h\nindex 13178e6b8f3..44efce3f410 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -318,6 +318,9 @@ struct rev_info {\n \n \t/* misc. flags related to '--no-kept-objects' */\n \tunsigned keep_pack_cache_flags;\n+\n+\t/* Location where temporary objects for remerge-diff are written. */\n+\tstruct tmp_objdir *remerge_objdir;\n };\n \n int ref_excluded(struct string_list *, const char *path);\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex 3d38eeab66b..adf6033549e 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -79,6 +79,11 @@ static void remove_tmp_objdir_on_signal(int signo)\n \traise(signo);\n }\n \n+void tmp_objdir_discard_objects(struct tmp_objdir *t)\n+{\n+\tremove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n+}\n+\n /*\n  * These env_* functions are for setting up the child environment; the\n  * \"replace\" variant overrides the value of any existing variable with that\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex cda5ec76778..76efc7edee5 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -46,6 +46,12 @@ int tmp_objdir_migrate(struct tmp_objdir *);\n  */\n int tmp_objdir_destroy(struct tmp_objdir *);\n \n+/*\n+ * Remove all objects from the temporary object directory, while leaving it\n+ * around so more objects can be added.\n+ */\n+void tmp_objdir_discard_objects(struct tmp_objdir *);\n+\n /*\n  * Add the temporary object directory as an alternate object store in the\n  * current process.\n-- \ngitgitgadget\n\n"},{"id":"446644","messageId":"8d6c3d48f0eb0e3af00a01d6463f6cd601bb0076.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 03/10] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:13Z","receivedAt":"2022-01-21T19:12:32Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince some callers may want to send warning messages to somewhere other\nthan stdout/stderr, stop printing \"warning: Cannot merge binary files\"\nfrom ll-merge and instead modify the return status of ll_merge() to\nindicate when a merge of binary files has occurred.  Message printing\nprobably does not belong in a \"low-level merge\" anyway.\n\nThis commit continues printing the message as-is, just from the callers\ninstead of within ll_merge().  Future changes will start handling the\nmessage differently in the merge-ort codepath.\n\nThere was one special case here: the callers in rerere.c do NOT check\nfor and print such a message; since those code paths explicitly skip\nover binary files, there is no reason to check for a return status of\nLL_MERGE_BINARY_CONFLICT or print the related message.\n\nNote that my methodology included first modifying ll_merge() to return\na struct, so that the compiler would catch all the callers for me and\nensure I had modified all of them.  After modifying all of them, I then\nchanged the struct to an enum.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n apply.c            |  5 ++++-\n builtin/checkout.c | 12 ++++++++----\n ll-merge.c         | 40 ++++++++++++++++++++++------------------\n ll-merge.h         |  9 ++++++++-\n merge-blobs.c      |  5 ++++-\n merge-ort.c        |  5 ++++-\n merge-recursive.c  |  5 ++++-\n notes-merge.c      |  5 ++++-\n rerere.c           |  9 +++++----\n 9 files changed, 63 insertions(+), 32 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex 43a0aebf4ee..8079395755f 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -3492,7 +3492,7 @@ static int three_way_merge(struct apply_state *state,\n {\n \tmmfile_t base_file, our_file, their_file;\n \tmmbuffer_t result = { NULL };\n-\tint status;\n+\tenum ll_merge_result status;\n \n \t/* resolve trivial cases first */\n \tif (oideq(base, ours))\n@@ -3509,6 +3509,9 @@ static int three_way_merge(struct apply_state *state,\n \t\t\t  &their_file, \"theirs\",\n \t\t\t  state->repo->index,\n \t\t\t  NULL);\n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \"ours\", \"theirs\");\n \tfree(base_file.ptr);\n \tfree(our_file.ptr);\n \tfree(their_file.ptr);\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex cbf73b8c9f6..3a559d69303 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -237,6 +237,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tstruct cache_entry *ce = active_cache[pos];\n \tconst char *path = ce->name;\n \tmmfile_t ancestor, ours, theirs;\n+\tenum ll_merge_result merge_status;\n \tint status;\n \tstruct object_id oid;\n \tmmbuffer_t result_buf;\n@@ -267,13 +268,16 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tmemset(&ll_opts, 0, sizeof(ll_opts));\n \tgit_config_get_bool(\"merge.renormalize\", &renormalize);\n \tll_opts.renormalize = renormalize;\n-\tstatus = ll_merge(&result_buf, path, &ancestor, \"base\",\n-\t\t\t  &ours, \"ours\", &theirs, \"theirs\",\n-\t\t\t  state->istate, &ll_opts);\n+\tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n+\t\t\t\t&ours, \"ours\", &theirs, \"theirs\",\n+\t\t\t\tstate->istate, &ll_opts);\n \tfree(ancestor.ptr);\n \tfree(ours.ptr);\n \tfree(theirs.ptr);\n-\tif (status < 0 || !result_buf.ptr) {\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \"ours\", \"theirs\");\n+\tif (merge_status < 0 || !result_buf.ptr) {\n \t\tfree(result_buf.ptr);\n \t\treturn error(_(\"path '%s': cannot merge\"), path);\n \t}\ndiff --git a/ll-merge.c b/ll-merge.c\nindex 261657578c7..a937cec59a6 100644\n--- a/ll-merge.c\n+++ b/ll-merge.c\n@@ -14,7 +14,7 @@\n \n struct ll_merge_driver;\n \n-typedef int (*ll_merge_fn)(const struct ll_merge_driver *,\n+typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -49,7 +49,7 @@ void reset_merge_attributes(void)\n /*\n  * Built-in low-levels\n  */\n-static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -58,6 +58,7 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   const struct ll_merge_options *opts,\n \t\t\t   int marker_size)\n {\n+\tenum ll_merge_result ret;\n \tmmfile_t *stolen;\n \tassert(opts);\n \n@@ -68,16 +69,19 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t */\n \tif (opts->virtual_ancestor) {\n \t\tstolen = orig;\n+\t\tret = LL_MERGE_OK;\n \t} else {\n \t\tswitch (opts->variant) {\n \t\tdefault:\n-\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\t\tpath, name1, name2);\n-\t\t\t/* fallthru */\n+\t\t\tret = LL_MERGE_BINARY_CONFLICT;\n+\t\t\tstolen = src1;\n+\t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_OURS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src1;\n \t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_THEIRS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src2;\n \t\t\tbreak;\n \t\t}\n@@ -87,16 +91,10 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \tresult->size = stolen->size;\n \tstolen->ptr = NULL;\n \n-\t/*\n-\t * With -Xtheirs or -Xours, we have cleanly merged;\n-\t * otherwise we got a conflict.\n-\t */\n-\treturn opts->variant == XDL_MERGE_FAVOR_OURS ||\n-\t       opts->variant == XDL_MERGE_FAVOR_THEIRS ?\n-\t       0 : 1;\n+\treturn ret;\n }\n \n-static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -105,7 +103,9 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tconst struct ll_merge_options *opts,\n \t\t\tint marker_size)\n {\n+\tenum ll_merge_result ret;\n \txmparam_t xmp;\n+\tint status;\n \tassert(opts);\n \n \tif (orig->size > MAX_XDIFF_SIZE ||\n@@ -133,10 +133,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \txmp.ancestor = orig_name;\n \txmp.file1 = name1;\n \txmp.file2 = name2;\n-\treturn xdl_merge(orig, src1, src2, &xmp, result);\n+\tstatus = xdl_merge(orig, src1, src2, &xmp, result);\n+\tret = (status > 0) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n-static int ll_union_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_union_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t  mmbuffer_t *result,\n \t\t\t  const char *path,\n \t\t\t  mmfile_t *orig, const char *orig_name,\n@@ -178,7 +180,7 @@ static void create_temp(mmfile_t *src, char *path, size_t len)\n /*\n  * User defined low-level merge driver support.\n  */\n-static int ll_ext_merge(const struct ll_merge_driver *fn,\n+static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -194,6 +196,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \tconst char *args[] = { NULL, NULL };\n \tint status, fd, i;\n \tstruct stat st;\n+\tenum ll_merge_result ret;\n \tassert(opts);\n \n \tsq_quote_buf(&path_sq, path);\n@@ -236,7 +239,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \t\tunlink_or_warn(temp[i]);\n \tstrbuf_release(&cmd);\n \tstrbuf_release(&path_sq);\n-\treturn status;\n+\tret = (status > 0) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n /*\n@@ -362,7 +366,7 @@ static void normalize_file(mmfile_t *mm, const char *path, struct index_state *i\n \t}\n }\n \n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/ll-merge.h b/ll-merge.h\nindex aceb1b24132..e4a20e81a3a 100644\n--- a/ll-merge.h\n+++ b/ll-merge.h\n@@ -82,13 +82,20 @@ struct ll_merge_options {\n \tlong xdl_opts;\n };\n \n+enum ll_merge_result {\n+\tLL_MERGE_ERROR = -1,\n+\tLL_MERGE_OK = 0,\n+\tLL_MERGE_CONFLICT,\n+\tLL_MERGE_BINARY_CONFLICT,\n+};\n+\n /**\n  * Perform a three-way single-file merge in core.  This is a thin wrapper\n  * around `xdl_merge` that takes the path and any merge backend specified in\n  * `.gitattributes` or `.git/info/attributes` into account.\n  * Returns 0 for a clean merge.\n  */\n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/merge-blobs.c b/merge-blobs.c\nindex ee0a0e90c94..8138090f81c 100644\n--- a/merge-blobs.c\n+++ b/merge-blobs.c\n@@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\t mmfile_t *their,\n \t\t\t\t unsigned long *size)\n {\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \tmmbuffer_t res;\n \n \t/*\n@@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\tistate, NULL);\n \tif (merge_status < 0)\n \t\treturn NULL;\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \".our\", \".their\");\n \n \t*size = res.size;\n \treturn res.ptr;\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 0342f104836..c24da2ba3cb 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1743,7 +1743,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tif (!opt->priv->attr_index.initialized)\n \t\tinitialize_attr_index(opt);\n@@ -1787,6 +1787,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex d9457797dbb..bc73c52dd84 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tll_opts.renormalize = opt->renormalize;\n \tll_opts.extra_marker_size = extra_marker_size;\n@@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, a->path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\topt->repo->index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\ta->path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/notes-merge.c b/notes-merge.c\nindex b4a3a903e86..01d596920ea 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n {\n \tmmbuffer_t result_buf;\n \tmmfile_t base, local, remote;\n-\tint status;\n+\tenum ll_merge_result status;\n \n \tread_mmblob(&base, &p->base);\n \tread_mmblob(&local, &p->local);\n@@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n \tfree(local.ptr);\n \tfree(remote.ptr);\n \n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\toid_to_hex(&p->obj), o->local_ref, o->remote_ref);\n \tif ((status < 0) || !result_buf.ptr)\n \t\tdie(\"Failed to execute internal merge\");\n \ndiff --git a/rerere.c b/rerere.c\nindex d83d58df4fb..d26627c5932 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -609,19 +609,20 @@ static int try_merge(struct index_state *istate,\n \t\t     const struct rerere_id *id, const char *path,\n \t\t     mmfile_t *cur, mmbuffer_t *result)\n {\n-\tint ret;\n+\tenum ll_merge_result ret;\n \tmmfile_t base = {NULL, 0}, other = {NULL, 0};\n \n \tif (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n-\t    read_mmfile(&other, rerere_path(id, \"postimage\")))\n-\t\tret = 1;\n-\telse\n+\t    read_mmfile(&other, rerere_path(id, \"postimage\"))) {\n+\t\tret = LL_MERGE_CONFLICT;\n+\t} else {\n \t\t/*\n \t\t * A three-way merge. Note that this honors user-customizable\n \t\t * low-level merge driver settings.\n \t\t */\n \t\tret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n \t\t\t       istate, NULL);\n+\t}\n \n \tfree(base.ptr);\n \tfree(other.ptr);\n-- \ngitgitgadget\n\n"},{"id":"446645","messageId":"de8e8f88fa475681672990b04c00b5ed397a8e01.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 04/10] merge-ort: capture and print ll-merge warnings in our preferred fashion","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:14Z","receivedAt":"2022-01-21T19:12:33Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nInstead of immediately printing ll-merge warnings to stderr, we save\nthem in our output strbuf.  Besides allowing us to move these warnings\nto a special file for --remerge-diff, this has two other benefits for\nregular merges done by merge-ort:\n\n  * The deferral of messages ensures we can print all messages about\n    any given path together (merge-recursive was known to sometimes\n    intersperse messages about other paths, particularly when renames\n    were involved).\n\n  * The deferral of messages means we can avoid printing spurious\n    conflict messages when we just end up aborting due to local user\n    modifications in the way.  (In contrast to merge-recursive.c which\n    prematurely checks for local modifications in the way via\n    unpack_trees() and gets the check wrong both in terms of false\n    positives and false negatives relative to renames, merge-ort does\n    not perform the local modifications in the way check until the\n    checkout() step after the full merge has been computed.)\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c                | 5 +++--\n t/t6404-recursive-merge.sh | 9 +++++++--\n t/t6406-merge-attr.sh      | 9 +++++++--\n 3 files changed, 17 insertions(+), 6 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex c24da2ba3cb..a18f47e23c5 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1788,8 +1788,9 @@ static int merge_3way(struct merge_options *opt,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n \tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n-\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\tpath, name1, name2);\n+\t\tpath_msg(opt, path, 0,\n+\t\t\t \"warning: Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\t path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/t/t6404-recursive-merge.sh b/t/t6404-recursive-merge.sh\nindex eaf48e941e2..b8735c6db4d 100755\n--- a/t/t6404-recursive-merge.sh\n+++ b/t/t6404-recursive-merge.sh\n@@ -108,8 +108,13 @@ test_expect_success 'refuse to merge binary files' '\n \tprintf \"\\0\\0\" >binary-file &&\n \tgit add binary-file &&\n \tgit commit -m binary2 &&\n-\ttest_must_fail git merge F >merge.out 2>merge.err &&\n-\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge.err\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge F >merge_output\n+\telse\n+\t\ttest_must_fail git merge F 2>merge_output\n+\tfi &&\n+\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge_output\n '\n \n test_expect_success 'mark rename/delete as unmerged' '\ndiff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh\nindex 84946458371..c41584eb33e 100755\n--- a/t/t6406-merge-attr.sh\n+++ b/t/t6406-merge-attr.sh\n@@ -221,8 +221,13 @@ test_expect_success 'binary files with union attribute' '\n \tprintf \"two\\0\" >bin.txt &&\n \tgit commit -am two &&\n \n-\ttest_must_fail git merge bin-main 2>stderr &&\n-\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" stderr\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge bin-main >output\n+\telse\n+\t\ttest_must_fail git merge bin-main 2>output\n+\tfi &&\n+\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" output\n '\n \n test_done\n-- \ngitgitgadget\n\n"},{"id":"446646","messageId":"6b535a4d55a864a0294ed0e881ecd035bd685117.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 05/10] merge-ort: mark a few more conflict messages as omittable","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:15Z","receivedAt":"2022-01-21T19:12:33Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\npath_msg() has the ability to mark messages as omittable, designed for\nremerge-diff where we'll instead be showing conflict messages as diff\nheaders for a subsequent diff.  While all these messages are very useful\nwhen trying to create a merge initially, early use with the\n--remerge-diff feature (the only user of this omittable conflict message\ncapability), suggests that the particular messages marked in this commit\nare just noise when trying to see what changes users made to create a\nmerge commit.  Mark them as omittable.\n\nNote that there were already a few messages marked as omittable in\nmerge-ort when doing a remerge-diff, because the development of\n--remerge-diff preceded the upstreaming of merge-ort and I was trying to\nensure merge-ort could handle all the necessary requirements.  See\ncommit c5a6f65527 (\"merge-ort: add modify/delete handling and delayed\noutput processing\", 2020-12-03) for the initial details.  For some\nexamples of already-marked-as-omittable messages, see either\n\"Auto-merging <path>\" or some of the submodule update hints.  This\ncommit just adds two more messages that should also be omittable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex a18f47e23c5..998e92ec593 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -2420,7 +2420,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t */\n \t\tci->path_conflict = 1;\n \t\tif (pair->status == 'A')\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s added in %s \"\n \t\t\t\t   \"inside a directory that was renamed in %s, \"\n \t\t\t\t   \"suggesting it should perhaps be moved to \"\n@@ -2428,7 +2428,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t\t\t old_path, branch_with_new_path,\n \t\t\t\t branch_with_dir_rename, new_path);\n \t\telse\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s renamed to %s \"\n \t\t\t\t   \"in %s, inside a directory that was renamed \"\n \t\t\t\t   \"in %s, suggesting it should perhaps be \"\n-- \ngitgitgadget\n\n"},{"id":"446647","messageId":"e2441608c6385a231ecb70f22424672a8e022862.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 06/10] merge-ort: format messages slightly different for use in headers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:16Z","receivedAt":"2022-01-21T19:12:36Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen users run\n    git show --remerge-diff $MERGE_COMMIT\nor\n    git log -p --remerge-diff ...\nstdout is not an appropriate location to dump conflict messages, but we\ndo want to provide them to users.  We will include them in the diff\nheaders instead...but for that to work, we need for any multiline\nmessages to replace newlines with both a newline and a space.  Add a new\nflag to signal when we want these messages modified in such a fashion,\nand use it in path_msg() to modify these messages this way.  Also, allow\na special prefix to be specified for these headers.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c       | 42 ++++++++++++++++++++++++++++++++++++++++--\n merge-recursive.c |  4 ++++\n merge-recursive.h |  2 ++\n 3 files changed, 46 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 998e92ec593..481305d2bcf 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -634,17 +634,49 @@ static void path_msg(struct merge_options *opt,\n \t\t     const char *fmt, ...)\n {\n \tva_list ap;\n-\tstruct strbuf *sb = strmap_get(&opt->priv->output, path);\n+\tstruct strbuf *sb, *dest;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\n+\tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n+\t\treturn; /* Do not record mere hints in tree */\n+\tsb = strmap_get(&opt->priv->output, path);\n \tif (!sb) {\n \t\tsb = xmalloc(sizeof(*sb));\n \t\tstrbuf_init(sb, 0);\n \t\tstrmap_put(&opt->priv->output, path, sb);\n \t}\n \n+\tdest = (opt->record_conflict_msgs_as_headers ? &tmp : sb);\n+\n \tva_start(ap, fmt);\n-\tstrbuf_vaddf(sb, fmt, ap);\n+\tstrbuf_vaddf(dest, fmt, ap);\n \tva_end(ap);\n \n+\tif (opt->record_conflict_msgs_as_headers) {\n+\t\tint i_sb = 0, i_tmp = 0;\n+\n+\t\t/* Start with the specified prefix */\n+\t\tif (opt->msg_header_prefix)\n+\t\t\tstrbuf_addf(sb, \"%s \", opt->msg_header_prefix);\n+\n+\t\t/* Copy tmp to sb, adding spaces after newlines */\n+\t\tstrbuf_grow(sb, sb->len + 2*tmp.len); /* more than sufficient */\n+\t\tfor (; i_tmp < tmp.len; i_tmp++, i_sb++) {\n+\t\t\t/* Copy next character from tmp to sb */\n+\t\t\tsb->buf[sb->len + i_sb] = tmp.buf[i_tmp];\n+\n+\t\t\t/* If we copied a newline, add a space */\n+\t\t\tif (tmp.buf[i_tmp] == '\\n')\n+\t\t\t\tsb->buf[++i_sb] = ' ';\n+\t\t}\n+\t\t/* Update length and ensure it's NUL-terminated */\n+\t\tsb->len += i_sb;\n+\t\tsb->buf[sb->len] = '\\0';\n+\n+\t\tstrbuf_release(&tmp);\n+\t}\n+\n+\t/* Add final newline character to sb */\n \tstrbuf_addch(sb, '\\n');\n }\n \n@@ -4246,6 +4278,9 @@ void merge_switch_to_result(struct merge_options *opt,\n \t\tstruct string_list olist = STRING_LIST_INIT_NODUP;\n \t\tint i;\n \n+\t\tif (opt->record_conflict_msgs_as_headers)\n+\t\t\tBUG(\"Either display conflict messages or record them as headers, not both\");\n+\n \t\ttrace2_region_enter(\"merge\", \"display messages\", opt->repo);\n \n \t\t/* Hack to pre-allocate olist to the desired size */\n@@ -4347,6 +4382,9 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)\n \tassert(opt->recursive_variant >= MERGE_VARIANT_NORMAL &&\n \t       opt->recursive_variant <= MERGE_VARIANT_THEIRS);\n \n+\tif (opt->msg_header_prefix)\n+\t\tassert(opt->record_conflict_msgs_as_headers);\n+\n \t/*\n \t * detect_renames, verbosity, buffer_output, and obuf are ignored\n \t * fields that were used by \"recursive\" rather than \"ort\" -- but\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex bc73c52dd84..9ec1e6d043a 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -3714,6 +3714,10 @@ static int merge_start(struct merge_options *opt, struct tree *head)\n \n \tassert(opt->priv == NULL);\n \n+\t/* Not supported; option specific to merge-ort */\n+\tassert(!opt->record_conflict_msgs_as_headers);\n+\tassert(!opt->msg_header_prefix);\n+\n \t/* Sanity check on repo state; index must match head */\n \tif (repo_index_has_changes(opt->repo, head, &sb)) {\n \t\terr(opt, _(\"Your local changes to the following files would be overwritten by merge:\\n  %s\"),\ndiff --git a/merge-recursive.h b/merge-recursive.h\nindex 0795a1d3ec1..b88000e3c25 100644\n--- a/merge-recursive.h\n+++ b/merge-recursive.h\n@@ -46,6 +46,8 @@ struct merge_options {\n \t/* miscellaneous control options */\n \tconst char *subtree_shift;\n \tunsigned renormalize : 1;\n+\tunsigned record_conflict_msgs_as_headers : 1;\n+\tconst char *msg_header_prefix;\n \n \t/* internal fields used by the implementation */\n \tstruct merge_options_internal *priv;\n-- \ngitgitgadget\n\n"},{"id":"446648","messageId":"62734beb693a6a6256106f0ffad9519a76c9d6d8.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 07/10] diff: add ability to insert additional headers for paths","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:17Z","receivedAt":"2022-01-21T19:12:38Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen additional headers are provided, we need to\n  * add diff_filepairs to diff_queued_diff for each paths in the\n    additional headers map which, unless that path is part of\n    another diff_filepair already found in diff_queued_diff\n  * format the headers (colorization, line_prefix for --graph)\n  * make sure the various codepaths that attempt to return early\n    if there are \"no changes\" take into account the headers that\n    need to be shown.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n diff.c     | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++--\n diff.h     |   3 +-\n log-tree.c |   2 +-\n 3 files changed, 123 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 861282db1c3..1bfb01c18ec 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -27,6 +27,7 @@\n #include \"help.h\"\n #include \"promisor-remote.h\"\n #include \"dir.h\"\n+#include \"strmap.h\"\n \n #ifdef NO_FAST_WORKING_DIRECTORY\n #define FAST_WORKING_DIRECTORY 0\n@@ -3406,6 +3407,31 @@ struct userdiff_driver *get_textconv(struct repository *r,\n \treturn userdiff_get_textconv(r, one->driver);\n }\n \n+static struct strbuf *additional_headers(struct diff_options *o,\n+\t\t\t\t\t const char *path)\n+{\n+\tif (!o->additional_path_headers)\n+\t\treturn NULL;\n+\treturn strmap_get(o->additional_path_headers, path);\n+}\n+\n+static void add_formatted_headers(struct strbuf *msg,\n+\t\t\t\t  struct strbuf *more_headers,\n+\t\t\t\t  const char *line_prefix,\n+\t\t\t\t  const char *meta,\n+\t\t\t\t  const char *reset)\n+{\n+\tchar *next, *newline;\n+\n+\tfor (next = more_headers->buf; *next; next = newline) {\n+\t\tnewline = strchrnul(next, '\\n');\n+\t\tstrbuf_addf(msg, \"%s%s%.*s%s\\n\", line_prefix, meta,\n+\t\t\t    (int)(newline - next), next, reset);\n+\t\tif (*newline)\n+\t\t\tnewline++;\n+\t}\n+}\n+\n static void builtin_diff(const char *name_a,\n \t\t\t const char *name_b,\n \t\t\t struct diff_filespec *one,\n@@ -3464,6 +3490,17 @@ static void builtin_diff(const char *name_a,\n \tb_two = quote_two(b_prefix, name_b + (*name_b == '/'));\n \tlbl[0] = DIFF_FILE_VALID(one) ? a_one : \"/dev/null\";\n \tlbl[1] = DIFF_FILE_VALID(two) ? b_two : \"/dev/null\";\n+\tif (!DIFF_FILE_VALID(one) && !DIFF_FILE_VALID(two)) {\n+\t\t/*\n+\t\t * We should only reach this point for pairs from\n+\t\t * create_filepairs_for_header_only_notifications().  For\n+\t\t * these, we should avoid the \"/dev/null\" special casing\n+\t\t * above, meaning we avoid showing such pairs as either\n+\t\t * \"new file\" or \"deleted file\" below.\n+\t\t */\n+\t\tlbl[0] = a_one;\n+\t\tlbl[1] = b_two;\n+\t}\n \tstrbuf_addf(&header, \"%s%sdiff --git %s %s%s\\n\", line_prefix, meta, a_one, b_two, reset);\n \tif (lbl[0][0] == '/') {\n \t\t/* /dev/null */\n@@ -4328,6 +4365,7 @@ static void fill_metainfo(struct strbuf *msg,\n \tconst char *set = diff_get_color(use_color, DIFF_METAINFO);\n \tconst char *reset = diff_get_color(use_color, DIFF_RESET);\n \tconst char *line_prefix = diff_line_prefix(o);\n+\tstruct strbuf *more_headers = NULL;\n \n \t*must_show_header = 1;\n \tstrbuf_init(msg, PATH_MAX * 2 + 300);\n@@ -4364,6 +4402,11 @@ static void fill_metainfo(struct strbuf *msg,\n \tdefault:\n \t\t*must_show_header = 0;\n \t}\n+\tif ((more_headers = additional_headers(o, name))) {\n+\t\tadd_formatted_headers(msg, more_headers,\n+\t\t\t\t      line_prefix, set, reset);\n+\t\t*must_show_header = 1;\n+\t}\n \tif (one && two && !oideq(&one->oid, &two->oid)) {\n \t\tconst unsigned hexsz = the_hash_algo->hexsz;\n \t\tint abbrev = o->abbrev ? o->abbrev : DEFAULT_ABBREV;\n@@ -5852,12 +5895,27 @@ int diff_unmodified_pair(struct diff_filepair *p)\n \n static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n {\n-\tif (diff_unmodified_pair(p))\n+\tint include_conflict_headers =\n+\t    (additional_headers(o, p->one->path) &&\n+\t     (!o->filter || filter_bit_tst(DIFF_STATUS_UNMERGED, o)));\n+\n+\t/*\n+\t * Check if we can return early without showing a diff.  Note that\n+\t * diff_filepair only stores {oid, path, mode, is_valid}\n+\t * information for each path, and thus diff_unmodified_pair() only\n+\t * considers those bits of info.  However, we do not want pairs\n+\t * created by create_filepairs_for_header_only_notifications()\n+\t * (which always look like unmodified pairs) to be ignored, so\n+\t * return early if both p is unmodified AND we don't want to\n+\t * include_conflict_headers.\n+\t */\n+\tif (diff_unmodified_pair(p) && !include_conflict_headers)\n \t\treturn;\n \n+\t/* Actually, we can also return early to avoid showing tree diffs */\n \tif ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||\n \t    (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))\n-\t\treturn; /* no tree diffs in patch format */\n+\t\treturn;\n \n \trun_diff(p, o);\n }\n@@ -5888,10 +5946,17 @@ static void diff_flush_checkdiff(struct diff_filepair *p,\n \trun_checkdiff(p, o);\n }\n \n-int diff_queue_is_empty(void)\n+int diff_queue_is_empty(struct diff_options *o)\n {\n \tstruct diff_queue_struct *q = &diff_queued_diff;\n \tint i;\n+\tint include_conflict_headers =\n+\t    (o->additional_path_headers &&\n+\t     (!o->filter || filter_bit_tst(DIFF_STATUS_UNMERGED, o)));\n+\n+\tif (include_conflict_headers)\n+\t\treturn 0;\n+\n \tfor (i = 0; i < q->nr; i++)\n \t\tif (!diff_unmodified_pair(q->queue[i]))\n \t\t\treturn 0;\n@@ -6325,6 +6390,54 @@ void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)\n \t\twarning(_(rename_limit_advice), varname, needed);\n }\n \n+static void create_filepairs_for_header_only_notifications(struct diff_options *o)\n+{\n+\tstruct strset present;\n+\tstruct diff_queue_struct *q = &diff_queued_diff;\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *e;\n+\tint i;\n+\n+\tstrset_init_with_options(&present, /*pool*/ NULL, /*strdup*/ 0);\n+\n+\t/*\n+\t * Find out which paths exist in diff_queued_diff, preferring\n+\t * one->path for any pair that has multiple paths.\n+\t */\n+\tfor (i = 0; i < q->nr; i++) {\n+\t\tstruct diff_filepair *p = q->queue[i];\n+\t\tchar *path = p->one->path ? p->one->path : p->two->path;\n+\n+\t\tif (strmap_contains(o->additional_path_headers, path))\n+\t\t\tstrset_add(&present, path);\n+\t}\n+\n+\t/*\n+\t * Loop over paths in additional_path_headers; for each NOT already\n+\t * in diff_queued_diff, create a synthetic filepair and insert that\n+\t * into diff_queued_diff.\n+\t */\n+\tstrmap_for_each_entry(o->additional_path_headers, &iter, e) {\n+\t\tif (!strset_contains(&present, e->key)) {\n+\t\t\tstruct diff_filespec *one, *two;\n+\t\t\tstruct diff_filepair *p;\n+\n+\t\t\tone = alloc_filespec(e->key);\n+\t\t\ttwo = alloc_filespec(e->key);\n+\t\t\tfill_filespec(one, null_oid(), 0, 0);\n+\t\t\tfill_filespec(two, null_oid(), 0, 0);\n+\t\t\tp = diff_queue(q, one, two);\n+\t\t\tp->status = DIFF_STATUS_MODIFIED;\n+\t\t}\n+\t}\n+\n+\t/* Re-sort the filepairs */\n+\tdiffcore_fix_diff_index();\n+\n+\t/* Cleanup */\n+\tstrset_clear(&present);\n+}\n+\n static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n {\n \tint i;\n@@ -6337,6 +6450,9 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n \tif (o->color_moved)\n \t\to->emitted_symbols = &esm;\n \n+\tif (o->additional_path_headers)\n+\t\tcreate_filepairs_for_header_only_notifications(o);\n+\n \tfor (i = 0; i < q->nr; i++) {\n \t\tstruct diff_filepair *p = q->queue[i];\n \t\tif (check_pair_status(p))\n@@ -6413,7 +6529,7 @@ void diff_flush(struct diff_options *options)\n \t * Order: raw, stat, summary, patch\n \t * or:    name/name-status/checkdiff (other bits clear)\n \t */\n-\tif (!q->nr)\n+\tif (!q->nr && !options->additional_path_headers)\n \t\tgoto free_queue;\n \n \tif (output_format & (DIFF_FORMAT_RAW |\ndiff --git a/diff.h b/diff.h\nindex 8ba85c5e605..ce9e2cf2e4f 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -395,6 +395,7 @@ struct diff_options {\n \n \tstruct repository *repo;\n \tstruct option *parseopts;\n+\tstruct strmap *additional_path_headers;\n \n \tint no_free;\n };\n@@ -593,7 +594,7 @@ void diffcore_fix_diff_index(void);\n \"                show all files diff when -S is used and hit is found.\\n\" \\\n \"  -a  --text    treat all files as text.\\n\"\n \n-int diff_queue_is_empty(void);\n+int diff_queue_is_empty(struct diff_options *o);\n void diff_flush(struct diff_options*);\n void diff_free(struct diff_options*);\n void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);\ndiff --git a/log-tree.c b/log-tree.c\nindex d4655b63d75..33c28f537a6 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -850,7 +850,7 @@ int log_tree_diff_flush(struct rev_info *opt)\n \topt->shown_dashes = 0;\n \tdiffcore_std(&opt->diffopt);\n \n-\tif (diff_queue_is_empty()) {\n+\tif (diff_queue_is_empty(&opt->diffopt)) {\n \t\tint saved_fmt = opt->diffopt.output_format;\n \t\topt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT;\n \t\tdiff_flush(&opt->diffopt);\n-- \ngitgitgadget\n\n"},{"id":"446649","messageId":"17eccf7e0d6daed2c0934c481d6a52c5a77a0d34.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 08/10] show, log: include conflict/warning messages in --remerge-diff headers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:18Z","receivedAt":"2022-01-21T19:12:42Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nConflicts such as modify/delete, rename/rename, or file/directory are\nnot representable via content conflict markers, and the normal output\nmessages notifying users about these were dropped with --remerge-diff.\nWhile we don't want these messages randomly shown before the commit\nand diff headers, we do want them to still be shown; include them as\npart of the diff headers instead.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n log-tree.c              |  51 ++++++++++++++\n merge-ort.c             |   1 +\n merge-ort.h             |  10 +++\n t/t4069-remerge-diff.sh | 144 ++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 206 insertions(+)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 33c28f537a6..85bfd9e50d8 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -19,6 +19,7 @@\n #include \"line-log.h\"\n #include \"help.h\"\n #include \"range-diff.h\"\n+#include \"strmap.h\"\n \n static struct decoration name_decoration = { \"object names\" };\n static int decoration_loaded;\n@@ -905,6 +906,52 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n \treturn !opt->loginfo;\n }\n \n+static void setup_additional_headers(struct diff_options *o,\n+\t\t\t\t     struct strmap *all_headers)\n+{\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *entry;\n+\n+\t/*\n+\t * Make o->additional_path_headers contain the subset of all_headers\n+\t * that match o->pathspec.  If there aren't any that match o->pathspec,\n+\t * then make o->additional_path_headers be NULL.\n+\t */\n+\n+\tif (!o->pathspec.nr) {\n+\t\to->additional_path_headers = all_headers;\n+\t\treturn;\n+\t}\n+\n+\to->additional_path_headers = xmalloc(sizeof(struct strmap));\n+\tstrmap_init_with_options(o->additional_path_headers, NULL, 0);\n+\tstrmap_for_each_entry(all_headers, &iter, entry) {\n+\t\tif (match_pathspec(the_repository->index, &o->pathspec,\n+\t\t\t\t   entry->key, strlen(entry->key),\n+\t\t\t\t   0 /* prefix */, NULL /* seen */,\n+\t\t\t\t   0 /* is_dir */))\n+\t\t\tstrmap_put(o->additional_path_headers,\n+\t\t\t\t   entry->key, entry->value);\n+\t}\n+\tif (!strmap_get_size(o->additional_path_headers)) {\n+\t\tstrmap_clear(o->additional_path_headers, 0);\n+\t\tFREE_AND_NULL(o->additional_path_headers);\n+\t}\n+}\n+\n+static void cleanup_additional_headers(struct diff_options *o)\n+{\n+\tif (!o->pathspec.nr) {\n+\t\to->additional_path_headers = NULL;\n+\t\treturn;\n+\t}\n+\tif (!o->additional_path_headers)\n+\t\treturn;\n+\n+\tstrmap_clear(o->additional_path_headers, 0);\n+\tFREE_AND_NULL(o->additional_path_headers);\n+}\n+\n static int do_remerge_diff(struct rev_info *opt,\n \t\t\t   struct commit_list *parents,\n \t\t\t   struct object_id *oid,\n@@ -922,6 +969,8 @@ static int do_remerge_diff(struct rev_info *opt,\n \t/* Setup merge options */\n \tinit_merge_options(&o, the_repository);\n \to.show_rename_progress = 0;\n+\to.record_conflict_msgs_as_headers = 1;\n+\to.msg_header_prefix = \"remerge\";\n \n \tctx.abbrev = DEFAULT_ABBREV;\n \tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n@@ -938,10 +987,12 @@ static int do_remerge_diff(struct rev_info *opt,\n \tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n \n \t/* Show the diff */\n+\tsetup_additional_headers(&opt->diffopt, res.path_messages);\n \tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n \tlog_tree_diff_flush(opt);\n \n \t/* Cleanup */\n+\tcleanup_additional_headers(&opt->diffopt);\n \tstrbuf_release(&parent1_desc);\n \tstrbuf_release(&parent2_desc);\n \tmerge_finalize(&o, &res);\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 481305d2bcf..43f980d2586 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -4585,6 +4585,7 @@ redo:\n \ttrace2_region_leave(\"merge\", \"process_entries\", opt->repo);\n \n \t/* Set return values */\n+\tresult->path_messages = &opt->priv->output;\n \tresult->tree = parse_tree_indirect(&working_tree_oid);\n \t/* existence of conflicted entries implies unclean */\n \tresult->clean &= strmap_empty(&opt->priv->conflicted);\ndiff --git a/merge-ort.h b/merge-ort.h\nindex c011864ffeb..fe599b87868 100644\n--- a/merge-ort.h\n+++ b/merge-ort.h\n@@ -5,6 +5,7 @@\n \n struct commit;\n struct tree;\n+struct strmap;\n \n struct merge_result {\n \t/*\n@@ -23,6 +24,15 @@ struct merge_result {\n \t */\n \tstruct tree *tree;\n \n+\t/*\n+\t * Special messages and conflict notices for various paths\n+\t *\n+\t * This is a map of pathnames to strbufs.  It contains various\n+\t * warning/conflict/notice messages (possibly multiple per path)\n+\t * that callers may want to use.\n+\t */\n+\tstruct strmap *path_messages;\n+\n \t/*\n \t * Additional metadata used by merge_switch_to_result() or future calls\n \t * to merge_incore_*().  Includes data needed to update the index (if\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nindex 5ef191f4fc9..86c5a33bd77 100755\n--- a/t/t4069-remerge-diff.sh\n+++ b/t/t4069-remerge-diff.sh\n@@ -59,6 +59,7 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n \tgit log -1 --oneline ab_resolution >tmp &&\n \tcat <<-EOF >>tmp &&\n \tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (content): Merge conflict in numbers\n \tindex a1fb731..6875544 100644\n \t--- a/numbers\n \t+++ b/numbers\n@@ -87,4 +88,147 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup non-content conflicts' '\n+\tgit switch --orphan base &&\n+\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\ttest_write_lines a b c d e f g h i >letters &&\n+\ttest_write_lines in the way >content &&\n+\tgit add numbers letters content &&\n+\tgit commit -m base &&\n+\n+\tgit branch side1 &&\n+\tgit branch side2 &&\n+\n+\tgit checkout side1 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit mv letters letters_side1 &&\n+\tgit mv content file_or_directory &&\n+\tgit add numbers &&\n+\tgit commit -m side1 &&\n+\n+\tgit checkout side2 &&\n+\tgit rm numbers &&\n+\tgit mv letters letters_side2 &&\n+\tmkdir file_or_directory &&\n+\techo hello >file_or_directory/world &&\n+\tgit add file_or_directory/world &&\n+\tgit commit -m side2 &&\n+\n+\tgit checkout -b resolution side1 &&\n+\ttest_must_fail git merge side2 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit add letters_side1 &&\n+\tgit rm letters &&\n+\tgit rm letters_side2 &&\n+\tgit add file_or_directory~HEAD &&\n+\tgit mv file_or_directory~HEAD wanted_content &&\n+\tgit commit -m resolved\n+'\n+\n+test_expect_success 'remerge-diff with non-content conflicts' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n+\tsimilarity index 100%\n+\trename from file_or_directory~HASH (side1)\n+\trename to wanted_content\n+\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n+\tdiff --git a/letters b/letters\n+\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n+\tdiff --git a/letters_side2 b/letters_side2\n+\tdeleted file mode 100644\n+\tindex b236ae5..0000000\n+\t--- a/letters_side2\n+\t+++ /dev/null\n+\t@@ -1,9 +0,0 @@\n+\t-a\n+\t-b\n+\t-c\n+\t-d\n+\t-e\n+\t-f\n+\t-g\n+\t-h\n+\t-i\n+\tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff w/ diff-filter=U: all conflict headers, no diff content' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/file_or_directory~HASH (side1) b/file_or_directory~HASH (side1)\n+\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n+\tdiff --git a/letters b/letters\n+\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n+\tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff --diff-filter=U resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff w/ diff-filter=R: relevant file + conflict header' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n+\tsimilarity index 100%\n+\trename from file_or_directory~HASH (side1)\n+\trename to wanted_content\n+\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff --diff-filter=R resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff w/ pathspec: limits to relevant file including conflict header' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/letters b/letters\n+\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n+\tdiff --git a/letters_side2 b/letters_side2\n+\tdeleted file mode 100644\n+\tindex b236ae5..0000000\n+\t--- a/letters_side2\n+\t+++ /dev/null\n+\t@@ -1,9 +0,0 @@\n+\t-a\n+\t-b\n+\t-c\n+\t-d\n+\t-e\n+\t-f\n+\t-g\n+\t-h\n+\t-i\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff --full-history resolution -- \"letters*\" >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"446650","messageId":"b3e7656cfc6eee19c0868b94d69db0d7b622aedc.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 09/10] merge-ort: mark conflict/warning messages from inner merges as omittable","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:19Z","receivedAt":"2022-01-21T19:12:44Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nA recursive merge involves merging the merge bases of the two branches\nbeing merged.  Such an inner merge can itself generate conflict notices.\nWhile such notices may be useful when initially trying to create a\nmerge, they seem to just be noise when investigating merges later with\n--remerge-diff.  (Especially when both sides of the outer merge resolved\nthe conflict the same way leading to no overall conflict.)  Remove them.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 43f980d2586..9bf15a01db8 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -638,7 +638,9 @@ static void path_msg(struct merge_options *opt,\n \tstruct strbuf tmp = STRBUF_INIT;\n \n \tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n-\t\treturn; /* Do not record mere hints in tree */\n+\t\treturn; /* Do not record mere hints in headers */\n+\tif (opt->record_conflict_msgs_as_headers && opt->priv->call_depth)\n+\t\treturn; /* Do not record inner merge issues in headers */\n \tsb = strmap_get(&opt->priv->output, path);\n \tif (!sb) {\n \t\tsb = xmalloc(sizeof(*sb));\n-- \ngitgitgadget\n\n"},{"id":"446651","messageId":"ea5df61cf358d3c831189e2f04863abc2157e3e1.1642792341.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v4 10/10] diff-merges: avoid history simplifications when diffing merges","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-01-21T19:12:20Z","receivedAt":"2022-01-21T19:12:46Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nDoing diffs for merges are special; they should typically avoid history\nsimplification.  For example, with\n\n    git log --diff-merges=first-parent -- path\n\nthe default history simplification would remove merge commits from\nconsideration if the file \"path\" matched the second parent.  That is\ncounter to what the user wants when looking for first-parent diffs.\nSimilar comments can be made for --diff-merges=separate (which diffs\nagainst both parents) and --diff-merges=remerge (which diffs against a\nremerge of the merge commit).\n\nHowever, history simplification still makes sense if not doing diffing\nmerges, and it also makes sense for the combined and dense-combined\nforms of diffing merges (because both of those are defined to only show\na diff when the merge result at the relevant paths differs from *both*\nparents).\n\nSo, for separate, first-parent, and remerge styles of diff-merges, turn\noff history simplification.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n diff-merges.c           |  2 ++\n t/t4069-remerge-diff.sh | 58 ++++++++++++++++++++++++++++++++++++++++-\n 2 files changed, 59 insertions(+), 1 deletion(-)\n\ndiff --git a/diff-merges.c b/diff-merges.c\nindex 0af4b3f9191..a833fd747ad 100644\n--- a/diff-merges.c\n+++ b/diff-merges.c\n@@ -24,6 +24,7 @@ static void set_separate(struct rev_info *revs)\n {\n \tsuppress(revs);\n \trevs->separate_merges = 1;\n+\trevs->simplify_history = 0;\n }\n \n static void set_first_parent(struct rev_info *revs)\n@@ -50,6 +51,7 @@ static void set_remerge_diff(struct rev_info *revs)\n {\n \tsuppress(revs);\n \trevs->remerge_diff = 1;\n+\trevs->simplify_history = 0;\n }\n \n static diff_merges_setup_func_t func_by_opt(const char *optarg)\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nindex 86c5a33bd77..962888cc7fb 100755\n--- a/t/t4069-remerge-diff.sh\n+++ b/t/t4069-remerge-diff.sh\n@@ -226,7 +226,63 @@ test_expect_success 'remerge-diff w/ pathspec: limits to relevant file including\n \t# with sha256\n \tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n \n-\tgit show --oneline --remerge-diff --full-history resolution -- \"letters*\" >tmp &&\n+\tgit show --oneline --remerge-diff resolution -- \"letters*\" >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup non-content conflicts' '\n+\tgit switch --orphan newbase &&\n+\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m base &&\n+\n+\tgit branch newside1 &&\n+\tgit branch newside2 &&\n+\n+\tgit checkout newside1 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m side1 &&\n+\n+\tgit checkout newside2 &&\n+\ttest_write_lines 1 2 drei 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m side2 &&\n+\n+\tgit checkout -b newresolution newside1 &&\n+\ttest_must_fail git merge newside2 &&\n+\tgit checkout --theirs numbers &&\n+\tgit add -u numbers &&\n+\tgit commit -m resolved\n+'\n+\n+test_expect_success 'remerge-diff turns off history simplification' '\n+\tgit log -1 --oneline newresolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (content): Merge conflict in numbers\n+\tindex 070e9e7..5335e78 100644\n+\t--- a/numbers\n+\t+++ b/numbers\n+\t@@ -1,10 +1,6 @@\n+\t 1\n+\t 2\n+\t-<<<<<<< 96f1e45 (side1)\n+\t-three\n+\t-=======\n+\t drei\n+\t->>>>>>> 4fd522f (side2)\n+\t 4\n+\t 5\n+\t 6\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff newresolution -- numbers >tmp &&\n \tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n \ttest_cmp expect actual\n '\n-- \ngitgitgadget\n"},{"id":"447399","messageId":"220201.86zgnb9bf9.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"0b94724311df34dd10debd43c466695ed406d790.1642792341.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 01/10] show, log: provide a --remerge-diff capability","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-02-01T09:09:39Z","receivedAt":"2022-02-01T09:35:43Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Jan 21 2022, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n> [...]\n>  ifdef::git-log[]\n> ---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc)::\n> +--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r)::\n>  --no-diff-merges::\n>  \tSpecify diff format to be used for merge commits. Default is\n>  \t{diff-merges-default} unless `--first-parent` is in use, in which case\n> @@ -64,6 +64,14 @@ ifdef::git-log[]\n>  \teach of the parents. Separate log entry and diff is generated\n>  \tfor each parent.\n>  +\n> +--diff-merges=remerge:::\n> +--diff-merges=r:::\n> +--remerge-diff:::\n> +\tWith this option, two-parent merge commits are remerged to\n> +\tcreate a temporary tree object -- potentially containing files\n> +\twith conflict markers and such.  A diff is then shown between\n> +\tthat temporary tree and the actual merge commit.\n> ++\n\nRe some previous discussion. I really think we should add something like\nthis paragraph to this:\n    \n    The output emitted when this option is used is subject to change, and so\n    is its interaction with other options (unless explicitly\n    documented). I.e. many of the same caveats as the \"OUTPUT STABILITY\" in\n    the linkgit:git-range-diff[1] documentation describes apply here. In\n    particular other diff filtering options, pathspec limitations etc. may\n    not produce the expected results, as some of those may apply to the\n    \"real\" diff of the merge, and not on the generated \"remerge-diff\".\n\nI think that would nicely give us permission to develop this further\nwithout having to think about all the option interaction etc.\n\nThis is really useful right now, but I'd hate for it to get merged with\nsome bug/behavior that's not obvious to us now, and it being hard to fix\nthat because we'd have to consider the implicitly promised backwards\ncompatibility.\n\n>  \tint saved_dcctc = 0;\n> +\tstruct tmp_objdir *remerge_objdir = NULL;\n> +\n> +\tif (rev->remerge_diff) {\n> +\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n> +\t\tif (!remerge_objdir)\n> +\t\t\tdie(_(\"unable to create temporary object directory\"));\n\nI guess the s/die_errno/die/ here is better for now as we won't report\nthe wrong errno, but also lose the common case of errno being right. But\nthat can be fixed up with some other series to the tmp-objdir API.\n\n> [...]\n> +# This test is ort-specific\n> +test \"${GIT_TEST_MERGE_ALGORITHM:-ort}\" = ort || {\n> +\tskip_all=\"GIT_TEST_MERGE_ALGORITHM != ort\"\n> +\ttest_done\n> +}\n\nFWIW this is still on a more complex pattern that it needs to be, see\nthis v1 discussion (which you seemed to ack):\n\nhttps://lore.kernel.org/git/CABPp-BE+4rZNP-5mT2MNOWR6y6BgEG6mt1r_qcrZtarom6aGsw@mail.gmail.com/\n"},{"id":"447401","messageId":"220201.86v8xyapj9.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"f06de6c1b2fbd5c5a23b6755197a3683c7d18d2f.1642792341.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 02/10] log: clean unneeded objects during `log --remerge-diff`","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-02-01T09:35:41Z","receivedAt":"2022-02-01T09:45:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Jan 21 2022, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n> [...]\n> @@ -944,7 +945,12 @@ static int do_remerge_diff(struct rev_info *opt,\n>  \tstrbuf_release(&parent1_desc);\n>  \tstrbuf_release(&parent2_desc);\n>  \tmerge_finalize(&o, &res);\n> -\t/* TODO: clean up the temporary object directory */\n> +\n> +\t/* Clean up the contents of the temporary object directory */\n> +\tif (opt->remerge_objdir)\n> +\t\ttmp_objdir_discard_objects(opt->remerge_objdir);\n> +\telse\n> +\t\tBUG(\"unable to remove temporary object directory\");\n\nRe the die in 1/10 I don't think this will ever trigger the way this bug\nsuggests.\n\nIf we didn't manage to remove the directory that'll be signalled with\nthe return code of tmp_objdir_discard_objects() which you're adding\nhere, but which doesn't have a meaningful return value.\n\nSo shouldn't it first of all be returning the \"int\" like the\nremove_dir_recursively() user in tmp_objdir_destroy_1() makes use of?\n\nWhat this bug is really about is:\n\n    BUG(\"our juggling of opt->remerge_objdir between here and builtin/log.c is screwy\")\n\nOr something, because if we failed to remove the director(ies) we'll\njust ignore that here.\n\n> +void tmp_objdir_discard_objects(struct tmp_objdir *t)\n> +{\n> +\tremove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n> +}\n\nI skimmed remove_dir_recurse() a bit, but didn't test this, does this\nremove just the \"de/eadbeef...\" in \"de/eadbeef...\" or also \"de/\",\ni.e. do we (and do we want) to keep the fanned-out 256 loose top-level\ndirectories throughout the operation?\n"},{"id":"447425","messageId":"CABPp-BE1p4bvm40-w5UDczSG8Ws+rR5tvjBL4gq_cgvYa92Q+Q@mail.gmail.com","threadId":"57133","inReplyTo":"220201.86zgnb9bf9.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v4 01/10] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-02-01T16:40:08Z","receivedAt":"2022-02-01T16:40:24Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Feb 1, 2022 at 1:35 AM Ævar Arnfjörð Bjarmason <avarab@gmail.com> wrote:\n>\n> On Fri, Jan 21 2022, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> > [...]\n> >  ifdef::git-log[]\n> > ---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc)::\n> > +--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r)::\n> >  --no-diff-merges::\n> >       Specify diff format to be used for merge commits. Default is\n> >       {diff-merges-default} unless `--first-parent` is in use, in which case\n> > @@ -64,6 +64,14 @@ ifdef::git-log[]\n> >       each of the parents. Separate log entry and diff is generated\n> >       for each parent.\n> >  +\n> > +--diff-merges=remerge:::\n> > +--diff-merges=r:::\n> > +--remerge-diff:::\n> > +     With this option, two-parent merge commits are remerged to\n> > +     create a temporary tree object -- potentially containing files\n> > +     with conflict markers and such.  A diff is then shown between\n> > +     that temporary tree and the actual merge commit.\n> > ++\n>\n> Re some previous discussion. I really think we should add something like\n> this paragraph to this:\n>\n>     The output emitted when this option is used is subject to change, and so\n>     is its interaction with other options (unless explicitly\n>     documented). I.e. many of the same caveats as the \"OUTPUT STABILITY\" in\n>     the linkgit:git-range-diff[1] documentation describes apply here. In\n>     particular other diff filtering options, pathspec limitations etc. may\n>     not produce the expected results, as some of those may apply to the\n>     \"real\" diff of the merge, and not on the generated \"remerge-diff\".\n>\n> I think that would nicely give us permission to develop this further\n> without having to think about all the option interaction etc.\n>\n> This is really useful right now, but I'd hate for it to get merged with\n> some bug/behavior that's not obvious to us now, and it being hard to fix\n> that because we'd have to consider the implicitly promised backwards\n> compatibility.\n\nSure I can add something.  I think the first sentence should be\nsufficient though.\n\n> >       int saved_dcctc = 0;\n> > +     struct tmp_objdir *remerge_objdir = NULL;\n> > +\n> > +     if (rev->remerge_diff) {\n> > +             remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n> > +             if (!remerge_objdir)\n> > +                     die(_(\"unable to create temporary object directory\"));\n>\n> I guess the s/die_errno/die/ here is better for now as we won't report\n> the wrong errno, but also lose the common case of errno being right. But\n> that can be fixed up with some other series to the tmp-objdir API.\n>\n> > [...]\n> > +# This test is ort-specific\n> > +test \"${GIT_TEST_MERGE_ALGORITHM:-ort}\" = ort || {\n> > +     skip_all=\"GIT_TEST_MERGE_ALGORITHM != ort\"\n> > +     test_done\n> > +}\n>\n> FWIW this is still on a more complex pattern that it needs to be, see\n> this v1 discussion (which you seemed to ack):\n>\n> https://lore.kernel.org/git/CABPp-BE+4rZNP-5mT2MNOWR6y6BgEG6mt1r_qcrZtarom6aGsw@mail.gmail.com/\n\nUm, I thought I made this change.  How did I lose it?\n\nThanks for catching; will fix.\n"},{"id":"447427","messageId":"CABPp-BHPZZuGoJO8PXCngBhStBJmDhbyCYOmR+CVZX5wv3O+4w@mail.gmail.com","threadId":"57133","inReplyTo":"220201.86v8xyapj9.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v4 02/10] log: clean unneeded objects during `log --remerge-diff`","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-02-01T16:54:54Z","receivedAt":"2022-02-01T16:55:10Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Feb 1, 2022 at 1:45 AM Ævar Arnfjörð Bjarmason <avarab@gmail.com> wrote:\n>\n> On Fri, Jan 21 2022, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> > [...]\n> > @@ -944,7 +945,12 @@ static int do_remerge_diff(struct rev_info *opt,\n> >       strbuf_release(&parent1_desc);\n> >       strbuf_release(&parent2_desc);\n> >       merge_finalize(&o, &res);\n> > -     /* TODO: clean up the temporary object directory */\n> > +\n> > +     /* Clean up the contents of the temporary object directory */\n> > +     if (opt->remerge_objdir)\n> > +             tmp_objdir_discard_objects(opt->remerge_objdir);\n> > +     else\n> > +             BUG(\"unable to remove temporary object directory\");\n>\n> Re the die in 1/10 I don't think this will ever trigger the way this bug\n> suggests.\n>\n> If we didn't manage to remove the directory that'll be signalled with\n> the return code of tmp_objdir_discard_objects() which you're adding\n> here, but which doesn't have a meaningful return value.\n>\n> So shouldn't it first of all be returning the \"int\" like the\n> remove_dir_recursively() user in tmp_objdir_destroy_1() makes use of?\n>\n> What this bug is really about is:\n>\n>     BUG(\"our juggling of opt->remerge_objdir between here and builtin/log.c is screwy\")\n>\n> Or something, because if we failed to remove the director(ies) we'll\n> just ignore that here.\n\nYeah, I think I'm suffering from leftover bits from earlier versions\nsince this patch series has been waiting for 17 months now.  I\nswitched it to\n\n    BUG(\"did a remerge diff without remerge_objdir?!?\");\n\n>\n> > +void tmp_objdir_discard_objects(struct tmp_objdir *t)\n> > +{\n> > +     remove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n> > +}\n>\n> I skimmed remove_dir_recurse() a bit, but didn't test this, does this\n> remove just the \"de/eadbeef...\" in \"de/eadbeef...\" or also \"de/\",\n> i.e. do we (and do we want) to keep the fanned-out 256 loose top-level\n> directories throughout the operation?\n\nIt will remove everything below t->path, but leave t->path.  As such,\nit'll nuke any of the 256 loose top-level directories that exist.\n\nIf someone wants to come along later and measure performance and\ndetermine if leaving those 256 loose top-level directories around\nimproves things, I think that's fine, but I'm not going to look at it\nas part of this series.  I'm more curious about where tmp_objdir\ncreates the temporary directory; when the intent is to migrate the\nobjects into the main directory, it should probably be created on the\nsame filesystem.  When the intent is scratch space, like it is for\n--remerge-diff, the tmp_objdir should probably be shoved in /dev/shm\nor something like that.  But again, that's outside of this series.\nThis series already has had a long list of things keeping it from the\nlight of day; there's no need to add frills to it as part of the\ninitial submission.\n"},{"id":"447485","messageId":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v4.git.1642792341.gitgitgadget@gmail.com","subject":"[PATCH v5 00/10] Add a new --remerge-diff capability to show & log","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:27Z","receivedAt":"2022-02-02T02:37:43Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Here are some patches to add a --remerge-diff capability to show & log,\nwhich works by comparing merge commits to an automatic remerge (note that\nthe automatic remerge tree can contain files with conflict markers).\n\nChanges since v4:\n\n * Just a few minor tweaks -- comment on output being subject to change,\n   simpler test skipping check, and improved bug message\n\nChanges since v3:\n\n * Filter conflict headers according to pathspecs\n * Instead of always including conflict headers for all diff types, only\n   select them with --diff-filter=U OR whenever the associated diff in\n   question is selected\n * New testcases dealing with --diff-filter, pathspecs, and default history\n   simplification\n * Switched back from die_errno() to die()\n\nChanges NOT included (mostly because I'm not sure what to add or where):\n\n * Johannes Altimanninger suggested changing the ordering of the new headers\n   relative to other headers. He made a good point, but I also like having\n   the conflict messages next to the text, so I'm conflicted about what's\n   best.\n * (Technically not part of this feature, but kind of related.) Months ago,\n   Junio suggested documenting ${GIT_DIR}/AUTO_MERGE better\n   (https://lore.kernel.org/git/xmqqtuj4nepe.fsf@gitster.g/). I looked at\n   the time, but couldn't find a place to put it that made sense to me.\n\nChanges since v2 (of the restarted submission):\n\n * Numerous small improvements suggested by Johannes Altmanninger\n * Avoid including conflict messages from inner merges (due to example\n   pointed out by Ævar).\n * Added a \"remerge\" prefix to all the new diff headers (suggested by Junio\n   in a previous round, but I couldn't come up with a good name before. It\n   suddenly hit me that \"remerge\" is an obvious prefix to use, and even\n   helps explain what the rest of the line is for.)\n\nChanges since v1 (of the restarted submission, which technically was v2):\n\n * Restructured the series, so the first patch introduces the feature --\n   with a bunch of caveats. Subsequent patches clean up those caveats. This\n   avoids introducing not-yet-used functions, and hopefully makes review\n   easier.\n * added testcases\n * numerous small improvements suggested by Ævar and Junio\n\nChanges since original submission[1]:\n\n * Rebased on top of the version of ns/tmp-objdir that Neeraj submitted\n   (Neeraj's patches were based on v2.34, but ns/tmp-objdir got applied on\n   an old commit and does not even build because of that).\n * Modify ll-merge API to return a status, instead of printing \"Cannot merge\n   binary files\" on stdout[2] (as suggested by Peff)\n * Make conflict messages and other such warnings into diff headers of the\n   subsequent remerge-diff rather than appearing in the diff as file content\n   of some funny looking filenames (as suggested by Peff[3] and Junio[4])\n * Sergey ack'ed the diff-merges.c portion of the patches, but that wasn't\n   limited to one patch so not sure where to record that ack.\n\n[1]\nhttps://lore.kernel.org/git/pull.1080.git.git.1630376800.gitgitgadget@gmail.com/;\nGitHub wouldn't let me change the target branch for the PR, so I had to\ncreate a new one with the new base and thus the reason for not sending this\nas v2 even though it is. [2]\nhttps://lore.kernel.org/git/YVOZRhWttzF18Xql@coredump.intra.peff.net/,\nhttps://lore.kernel.org/git/YVOZty9D7NRbzhE5@coredump.intra.peff.net/ [3]\nhttps://lore.kernel.org/git/YVOXPTjsp9lrxmS6@coredump.intra.peff.net/ [4]\nhttps://lore.kernel.org/git/xmqqr1d7e4ug.fsf@gitster.g/\n\n=== FURTHER BACKGROUND (original cover letter material) ==\n\nHere are some example commits you can try this out on (with git show\n--remerge-diff $COMMIT):\n\n * git.git conflicted merge: 07601b5b36\n * git.git non-conflicted change: bf04590ecd\n * linux.git conflicted merge: eab3540562fb\n * linux.git non-conflicted change: 223cea6a4f05\n\nMany more can be found by just running git log --merges --remerge-diff in\nyour repository of choice and searching for diffs (most merges tend to be\nclean and unmodified and thus produce no diff but a search of '^diff' in the\nlog output tends to find the examples nicely).\n\nSome basic high level details about this new option:\n\n * This option is most naturally compared to --cc, though the output seems\n   to be much more understandable to most users than --cc output.\n * Since merges are often clean and unmodified, this new option results in\n   an empty diff for most merges.\n * This new option shows things like the removal of conflict markers, which\n   hunks users picked from the various conflicted sides to keep or remove,\n   and shows changes made outside of conflict markers (which might reflect\n   changes needed to resolve semantic conflicts or cleanups of e.g.\n   compilation warnings or other additional changes an integrator felt\n   belonged in the merged result).\n * This new option does not (currently) work for octopus merges, since\n   merge-ort is specific to two-parent merges[1].\n * This option will not work on a read-only or full filesystem[2].\n * We discussed this capability at Git Merge 2020, and one of the\n   suggestions was doing a periodic git gc --auto during the operation (due\n   to potential new blobs and trees created during the operation). I found a\n   way to avoid that; see [2].\n * This option is faster than you'd probably expect; it handles 33.5 merge\n   commits per second in linux.git on my computer; see below.\n\nIn regards to the performance point above, the timing for running the\nfollowing command:\n\ntime git log --min-parents=2 --max-parents=2 $DIFF_FLAG | wc -l\n\n\nin linux.git (with v5.4 checked out, since my copy of linux is very out of\ndate) is as follows:\n\nDIFF_FLAG=--cc:            71m 31.536s\nDIFF_FLAG=--remerge-diff:  31m  3.170s\n\n\nNote that there are 62476 merges in this history. Also, output size is:\n\nDIFF_FLAG=--cc:            2169111 lines\nDIFF_FLAG=--remerge-diff:  2458020 lines\n\n\nSo roughly the same amount of output as --cc, as you'd expect.\n\nAs a side note: git log --remerge-diff, when run in various repositories and\nallowed to run all the way back to the beginning(s) of history, is a nice\nstress test of sorts for merge-ort. Especially when users run it for you on\ntheir repositories they are working on, whether intentionally or via a bug\nin a tool triggering that command to be run unexpectedly. Long story short,\nsuch a bug in an internal tool existed in December 2020 and this command was\nrun on an internal repository and found a platform-specific bug in merge-ort\non some really old merge commit from that repo. I fixed that bug (a\nSTABLE_QSORT thing) while upstreaming all the merge-ort patches in the mean\ntime, but it was nice getting extra testing. Having more folks run this on\ntheir repositories might be useful extra testing of the new merge strategy.\n\nAlso, I previously mentioned --remerge-diff-only (a flag to show how\ncherry-picks or reverts differ from an automatic cherry-pick or revert, in\naddition to showing how merges differ from an automatic merge). This series\ndoes not include the patches to introduce that option; I'll submit them\nlater.\n\nTwo other things that might be interesting but are not included and which I\nhaven't investigated:\n\n * some mechanism for passing extra merge options through (e.g.\n   -Xignore-space-change)\n * a capability to compare the automatic merge to a second automatic merge\n   done with different merge options. (Not sure if this would be of interest\n   to end users, but might be interesting while developing new a\n   --strategy-option, or maybe checking how changing some default in the\n   merge algorithm would affect historical merges in various repositories).\n\n[1] I have nebulous ideas of how an Octopus-centric ORT strategy could be\nwritten -- basically, just repeatedly invoking ort and trying to make sure\nnested conflicts can be differentiated. For now, though, a simple warning is\nprinted that octopus merges are not handled and no diff will be shown. [2]\nNew blobs/trees can be written by the three-way merging step. These are\nwritten to a temporary area (via tmp-objdir.c) under the git object store\nthat is cleaned up at the end of the operation, with the new loose objects\nfrom the remerge being cleaned up after each individual merge.\n\nElijah Newren (10):\n  show, log: provide a --remerge-diff capability\n  log: clean unneeded objects during `log --remerge-diff`\n  ll-merge: make callers responsible for showing warnings\n  merge-ort: capture and print ll-merge warnings in our preferred\n    fashion\n  merge-ort: mark a few more conflict messages as omittable\n  merge-ort: format messages slightly different for use in headers\n  diff: add ability to insert additional headers for paths\n  show, log: include conflict/warning messages in --remerge-diff headers\n  merge-ort: mark conflict/warning messages from inner merges as\n    omittable\n  diff-merges: avoid history simplifications when diffing merges\n\n Documentation/diff-options.txt |  14 +-\n apply.c                        |   5 +-\n builtin/checkout.c             |  12 +-\n builtin/log.c                  |  15 ++\n diff-merges.c                  |  14 ++\n diff.c                         | 124 +++++++++++++-\n diff.h                         |   3 +-\n ll-merge.c                     |  40 +++--\n ll-merge.h                     |   9 +-\n log-tree.c                     | 118 ++++++++++++-\n merge-blobs.c                  |   5 +-\n merge-ort.c                    |  55 ++++++-\n merge-ort.h                    |  10 ++\n merge-recursive.c              |   9 +-\n merge-recursive.h              |   2 +\n notes-merge.c                  |   5 +-\n rerere.c                       |   9 +-\n revision.h                     |   6 +-\n t/t4069-remerge-diff.sh        | 291 +++++++++++++++++++++++++++++++++\n t/t6404-recursive-merge.sh     |   9 +-\n t/t6406-merge-attr.sh          |   9 +-\n tmp-objdir.c                   |   5 +\n tmp-objdir.h                   |   6 +\n 23 files changed, 727 insertions(+), 48 deletions(-)\n create mode 100755 t/t4069-remerge-diff.sh\n\n\nbase-commit: 4e44121c2d7bced65e25eb7ec5156290132bec94\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1103%2Fnewren%2Fremerge-diff-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1103/newren/remerge-diff-v5\nPull-Request: https://github.com/gitgitgadget/git/pull/1103\n\nRange-diff vs v4:\n\n  1:  0b94724311d !  1:  0a260125266 show, log: provide a --remerge-diff capability\n     @@ Documentation/diff-options.txt: ifdef::git-log[]\n      +\tcreate a temporary tree object -- potentially containing files\n      +\twith conflict markers and such.  A diff is then shown between\n      +\tthat temporary tree and the actual merge commit.\n     +++\n     ++The output emitted when this option is used is subject to change, and\n     ++so is its interaction with other options (unless explicitly\n     ++documented).\n      ++\n       --diff-merges=combined:::\n       --diff-merges=c:::\n     @@ t/t4069-remerge-diff.sh (new)\n      +. ./test-lib.sh\n      +\n      +# This test is ort-specific\n     -+test \"${GIT_TEST_MERGE_ALGORITHM:-ort}\" = ort || {\n     ++if test \"${GIT_TEST_MERGE_ALGORITHM}\" != ort\n     ++then\n      +\tskip_all=\"GIT_TEST_MERGE_ALGORITHM != ort\"\n      +\ttest_done\n     -+}\n     ++fi\n      +\n      +test_expect_success 'setup basic merges' '\n      +\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n  2:  f06de6c1b2f !  2:  ed0d60de24c log: clean unneeded objects during `log --remerge-diff`\n     @@ log-tree.c: static int do_remerge_diff(struct rev_info *opt,\n      +\tif (opt->remerge_objdir)\n      +\t\ttmp_objdir_discard_objects(opt->remerge_objdir);\n      +\telse\n     -+\t\tBUG(\"unable to remove temporary object directory\");\n     ++\t\tBUG(\"did a remerge diff without remerge_objdir?!?\");\n       \n       \treturn !opt->loginfo;\n       }\n  3:  8d6c3d48f0e =  3:  ba4de88f2c4 ll-merge: make callers responsible for showing warnings\n  4:  de8e8f88fa4 =  4:  d7a1f4e1f9f merge-ort: capture and print ll-merge warnings in our preferred fashion\n  5:  6b535a4d55a =  5:  cbde4e5d372 merge-ort: mark a few more conflict messages as omittable\n  6:  e2441608c63 =  6:  d3e4242a5bd merge-ort: format messages slightly different for use in headers\n  7:  62734beb693 =  7:  4d79da6e20a diff: add ability to insert additional headers for paths\n  8:  17eccf7e0d6 =  8:  ff9c14b0b7c show, log: include conflict/warning messages in --remerge-diff headers\n  9:  b3e7656cfc6 =  9:  aa63860cd0f merge-ort: mark conflict/warning messages from inner merges as omittable\n 10:  ea5df61cf35 = 10:  59d12f213b2 diff-merges: avoid history simplifications when diffing merges\n\n-- \ngitgitgadget\n"},{"id":"447486","messageId":"0a260125266e3482558089f9bca4f17623df3e11.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 01/10] show, log: provide a --remerge-diff capability","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:28Z","receivedAt":"2022-02-02T02:37:44Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen this option is specified, we remerge all (two parent) merge commits\nand diff the actual merge commit to the automatically created version,\nin order to show how users removed conflict markers, resolved the\ndifferent conflict versions, and potentially added new changes outside\nof conflict regions in order to resolve semantic merge problems (or,\npossibly, just to hide other random changes).\n\nThis capability works by creating a temporary object directory and\nmarking it as the primary object store.  This makes it so that any blobs\nor trees created during the automatic merge are easily removable\nafterwards by just deleting all objects from the temporary object\ndirectory.\n\nThere are a few ways that this implementation is suboptimal:\n  * `log --remerge-diff` becomes slow, because the temporary object\n    directory can fill with many loose objects while running\n  * the log output can be muddied with misplaced \"warning: cannot merge\n    binary files\" messages, since ll-merge.c unconditionally writes those\n    messages to stderr while running instead of allowing callers to\n    manage them.\n  * important conflict and warning messages are simply dropped; thus for\n    conflicts like modify/delete or rename/rename or file/directory which\n    are not representable with content conflict markers, there may be no\n    way for a user of --remerge-diff to know that there had been a\n    conflict which was resolved (and which possibly motivated other\n    changes in the merge commit).\n  * when fixing the previous issue, note that some unimportant conflict\n    and warning messages might start being included.  We should instead\n    make sure these remain dropped.\nSubsequent commits will address these issues.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n Documentation/diff-options.txt | 14 +++++-\n builtin/log.c                  | 14 ++++++\n diff-merges.c                  | 12 +++++\n log-tree.c                     | 59 ++++++++++++++++++++++\n revision.h                     |  3 +-\n t/t4069-remerge-diff.sh        | 91 ++++++++++++++++++++++++++++++++++\n 6 files changed, 191 insertions(+), 2 deletions(-)\n create mode 100755 t/t4069-remerge-diff.sh\n\ndiff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\nindex c89d530d3d1..7e27841a95b 100644\n--- a/Documentation/diff-options.txt\n+++ b/Documentation/diff-options.txt\n@@ -34,7 +34,7 @@ endif::git-diff[]\n endif::git-format-patch[]\n \n ifdef::git-log[]\n---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc)::\n+--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r)::\n --no-diff-merges::\n \tSpecify diff format to be used for merge commits. Default is\n \t{diff-merges-default} unless `--first-parent` is in use, in which case\n@@ -64,6 +64,18 @@ ifdef::git-log[]\n \teach of the parents. Separate log entry and diff is generated\n \tfor each parent.\n +\n+--diff-merges=remerge:::\n+--diff-merges=r:::\n+--remerge-diff:::\n+\tWith this option, two-parent merge commits are remerged to\n+\tcreate a temporary tree object -- potentially containing files\n+\twith conflict markers and such.  A diff is then shown between\n+\tthat temporary tree and the actual merge commit.\n++\n+The output emitted when this option is used is subject to change, and\n+so is its interaction with other options (unless explicitly\n+documented).\n++\n --diff-merges=combined:::\n --diff-merges=c:::\n -c:::\ndiff --git a/builtin/log.c b/builtin/log.c\nindex f75d87e8d7f..846ba0f995a 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -35,6 +35,7 @@\n #include \"repository.h\"\n #include \"commit-reach.h\"\n #include \"range-diff.h\"\n+#include \"tmp-objdir.h\"\n \n #define MAIL_DEFAULT_WRAP 72\n #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100\n@@ -406,6 +407,14 @@ static int cmd_log_walk(struct rev_info *rev)\n \tstruct commit *commit;\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n+\tstruct tmp_objdir *remerge_objdir = NULL;\n+\n+\tif (rev->remerge_diff) {\n+\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\tif (!remerge_objdir)\n+\t\t\tdie(_(\"unable to create temporary object directory\"));\n+\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n+\t}\n \n \tif (rev->early_output)\n \t\tsetup_early_output();\n@@ -449,6 +458,9 @@ static int cmd_log_walk(struct rev_info *rev)\n \trev->diffopt.no_free = 0;\n \tdiff_free(&rev->diffopt);\n \n+\tif (rev->remerge_diff)\n+\t\ttmp_objdir_destroy(remerge_objdir);\n+\n \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n \t    rev->diffopt.flags.check_failed) {\n \t\treturn 02;\n@@ -1943,6 +1955,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)\n \t\tdie(_(\"--name-status does not make sense\"));\n \tif (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF)\n \t\tdie(_(\"--check does not make sense\"));\n+\tif (rev.remerge_diff)\n+\t\tdie(_(\"--remerge-diff does not make sense\"));\n \n \tif (!use_patch_format &&\n \t\t(!rev.diffopt.output_format ||\ndiff --git a/diff-merges.c b/diff-merges.c\nindex 5060ccd890b..0af4b3f9191 100644\n--- a/diff-merges.c\n+++ b/diff-merges.c\n@@ -17,6 +17,7 @@ static void suppress(struct rev_info *revs)\n \trevs->combined_all_paths = 0;\n \trevs->merges_imply_patch = 0;\n \trevs->merges_need_diff = 0;\n+\trevs->remerge_diff = 0;\n }\n \n static void set_separate(struct rev_info *revs)\n@@ -45,6 +46,12 @@ static void set_dense_combined(struct rev_info *revs)\n \trevs->dense_combined_merges = 1;\n }\n \n+static void set_remerge_diff(struct rev_info *revs)\n+{\n+\tsuppress(revs);\n+\trevs->remerge_diff = 1;\n+}\n+\n static diff_merges_setup_func_t func_by_opt(const char *optarg)\n {\n \tif (!strcmp(optarg, \"off\") || !strcmp(optarg, \"none\"))\n@@ -57,6 +64,8 @@ static diff_merges_setup_func_t func_by_opt(const char *optarg)\n \t\treturn set_combined;\n \telse if (!strcmp(optarg, \"cc\") || !strcmp(optarg, \"dense-combined\"))\n \t\treturn set_dense_combined;\n+\telse if (!strcmp(optarg, \"r\") || !strcmp(optarg, \"remerge\"))\n+\t\treturn set_remerge_diff;\n \telse if (!strcmp(optarg, \"m\") || !strcmp(optarg, \"on\"))\n \t\treturn set_to_default;\n \treturn NULL;\n@@ -110,6 +119,9 @@ int diff_merges_parse_opts(struct rev_info *revs, const char **argv)\n \t} else if (!strcmp(arg, \"--cc\")) {\n \t\tset_dense_combined(revs);\n \t\trevs->merges_imply_patch = 1;\n+\t} else if (!strcmp(arg, \"--remerge-diff\")) {\n+\t\tset_remerge_diff(revs);\n+\t\trevs->merges_imply_patch = 1;\n \t} else if (!strcmp(arg, \"--no-diff-merges\")) {\n \t\tsuppress(revs);\n \t} else if (!strcmp(arg, \"--combined-all-paths\")) {\ndiff --git a/log-tree.c b/log-tree.c\nindex 644893fd8cf..84ed864fc81 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -1,4 +1,5 @@\n #include \"cache.h\"\n+#include \"commit-reach.h\"\n #include \"config.h\"\n #include \"diff.h\"\n #include \"object-store.h\"\n@@ -7,6 +8,7 @@\n #include \"tag.h\"\n #include \"graph.h\"\n #include \"log-tree.h\"\n+#include \"merge-ort.h\"\n #include \"reflog-walk.h\"\n #include \"refs.h\"\n #include \"string-list.h\"\n@@ -902,6 +904,51 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n \treturn !opt->loginfo;\n }\n \n+static int do_remerge_diff(struct rev_info *opt,\n+\t\t\t   struct commit_list *parents,\n+\t\t\t   struct object_id *oid,\n+\t\t\t   struct commit *commit)\n+{\n+\tstruct merge_options o;\n+\tstruct commit_list *bases;\n+\tstruct merge_result res = {0};\n+\tstruct pretty_print_context ctx = {0};\n+\tstruct commit *parent1 = parents->item;\n+\tstruct commit *parent2 = parents->next->item;\n+\tstruct strbuf parent1_desc = STRBUF_INIT;\n+\tstruct strbuf parent2_desc = STRBUF_INIT;\n+\n+\t/* Setup merge options */\n+\tinit_merge_options(&o, the_repository);\n+\to.show_rename_progress = 0;\n+\n+\tctx.abbrev = DEFAULT_ABBREV;\n+\tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n+\tformat_commit_message(parent2, \"%h (%s)\", &parent2_desc, &ctx);\n+\to.branch1 = parent1_desc.buf;\n+\to.branch2 = parent2_desc.buf;\n+\n+\t/* Parse the relevant commits and get the merge bases */\n+\tparse_commit_or_die(parent1);\n+\tparse_commit_or_die(parent2);\n+\tbases = get_merge_bases(parent1, parent2);\n+\n+\t/* Re-merge the parents */\n+\tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n+\n+\t/* Show the diff */\n+\tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n+\tlog_tree_diff_flush(opt);\n+\n+\t/* Cleanup */\n+\tstrbuf_release(&parent1_desc);\n+\tstrbuf_release(&parent2_desc);\n+\tmerge_finalize(&o, &res);\n+\t/* TODO: clean up the temporary object directory */\n+\n+\treturn !opt->loginfo;\n+}\n+\n /*\n  * Show the diff of a commit.\n  *\n@@ -936,6 +983,18 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log\n \t}\n \n \tif (is_merge) {\n+\t\tint octopus = (parents->next->next != NULL);\n+\n+\t\tif (opt->remerge_diff) {\n+\t\t\tif (octopus) {\n+\t\t\t\tshow_log(opt);\n+\t\t\t\tfprintf(opt->diffopt.file,\n+\t\t\t\t\t\"diff: warning: Skipping remerge-diff \"\n+\t\t\t\t\t\"for octopus merges.\\n\");\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\treturn do_remerge_diff(opt, parents, oid, commit);\n+\t\t}\n \t\tif (opt->combine_merges)\n \t\t\treturn do_diff_combined(opt, commit);\n \t\tif (opt->separate_merges) {\ndiff --git a/revision.h b/revision.h\nindex 5578bb4720a..13178e6b8f3 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -195,7 +195,8 @@ struct rev_info {\n \t\t\tcombine_merges:1,\n \t\t\tcombined_all_paths:1,\n \t\t\tdense_combined_merges:1,\n-\t\t\tfirst_parent_merges:1;\n+\t\t\tfirst_parent_merges:1,\n+\t\t\tremerge_diff:1;\n \n \t/* Format info */\n \tint\t\tshow_notes;\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nnew file mode 100755\nindex 00000000000..d7ab0f50066\n--- /dev/null\n+++ b/t/t4069-remerge-diff.sh\n@@ -0,0 +1,91 @@\n+#!/bin/sh\n+\n+test_description='remerge-diff handling'\n+\n+. ./test-lib.sh\n+\n+# This test is ort-specific\n+if test \"${GIT_TEST_MERGE_ALGORITHM}\" != ort\n+then\n+\tskip_all=\"GIT_TEST_MERGE_ALGORITHM != ort\"\n+\ttest_done\n+fi\n+\n+test_expect_success 'setup basic merges' '\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m base &&\n+\n+\tgit branch feature_a &&\n+\tgit branch feature_b &&\n+\tgit branch feature_c &&\n+\n+\tgit branch ab_resolution &&\n+\tgit branch bc_resolution &&\n+\n+\tgit checkout feature_a &&\n+\ttest_write_lines 1 2 three 4 5 6 7 eight 9 >numbers &&\n+\tgit commit -a -m change_a &&\n+\n+\tgit checkout feature_b &&\n+\ttest_write_lines 1 2 tres 4 5 6 7 8 9 >numbers &&\n+\tgit commit -a -m change_b &&\n+\n+\tgit checkout feature_c &&\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&\n+\tgit commit -a -m change_c &&\n+\n+\tgit checkout bc_resolution &&\n+\tgit merge --ff-only feature_b &&\n+\t# no conflict\n+\tgit merge feature_c &&\n+\n+\tgit checkout ab_resolution &&\n+\tgit merge --ff-only feature_a &&\n+\t# conflicts!\n+\ttest_must_fail git merge feature_b &&\n+\t# Resolve conflict...and make another change elsewhere\n+\ttest_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers &&\n+\tgit add numbers &&\n+\tgit merge --continue\n+'\n+\n+test_expect_success 'remerge-diff on a clean merge' '\n+\tgit log -1 --oneline bc_resolution >expect &&\n+\tgit show --oneline --remerge-diff bc_resolution >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' '\n+\tgit log -1 --oneline ab_resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/numbers b/numbers\n+\tindex a1fb731..6875544 100644\n+\t--- a/numbers\n+\t+++ b/numbers\n+\t@@ -1,13 +1,9 @@\n+\t 1\n+\t 2\n+\t-<<<<<<< b0ed5cb (change_a)\n+\t-three\n+\t-=======\n+\t-tres\n+\t->>>>>>> 6cd3f82 (change_b)\n+\t+drei\n+\t 4\n+\t 5\n+\t 6\n+\t 7\n+\t-eight\n+\t+acht\n+\t 9\n+\tEOF\n+\t# Hashes above are sha1; rip them out so test works with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff ab_resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"447487","messageId":"ed0d60de24cf2538af3880f014fd5e10bd0275c1.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 02/10] log: clean unneeded objects during `log --remerge-diff`","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:29Z","receivedAt":"2022-02-02T02:37:46Z","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 --remerge-diff option will need to create new blobs and trees\nrepresenting the \"automatic merge\" state.  If one is traversing a\nlong project history, one can easily get hundreds of thousands of\nloose objects generated during `log --remerge-diff`.  However, none of\nthose loose objects are needed after we have completed our diff\noperation; they can be summarily deleted.\n\nAdd a new helper function to tmp_objdir to discard all the contained\nobjects, and call it after each merge is handled.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/log.c | 13 +++++++------\n log-tree.c    |  8 +++++++-\n revision.h    |  3 +++\n tmp-objdir.c  |  5 +++++\n tmp-objdir.h  |  6 ++++++\n 5 files changed, 28 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 846ba0f995a..ac550e1ae62 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -407,13 +407,12 @@ static int cmd_log_walk(struct rev_info *rev)\n \tstruct commit *commit;\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n-\tstruct tmp_objdir *remerge_objdir = NULL;\n \n \tif (rev->remerge_diff) {\n-\t\tremerge_objdir = tmp_objdir_create(\"remerge-diff\");\n-\t\tif (!remerge_objdir)\n+\t\trev->remerge_objdir = tmp_objdir_create(\"remerge-diff\");\n+\t\tif (!rev->remerge_objdir)\n \t\t\tdie(_(\"unable to create temporary object directory\"));\n-\t\ttmp_objdir_replace_primary_odb(remerge_objdir, 1);\n+\t\ttmp_objdir_replace_primary_odb(rev->remerge_objdir, 1);\n \t}\n \n \tif (rev->early_output)\n@@ -458,8 +457,10 @@ static int cmd_log_walk(struct rev_info *rev)\n \trev->diffopt.no_free = 0;\n \tdiff_free(&rev->diffopt);\n \n-\tif (rev->remerge_diff)\n-\t\ttmp_objdir_destroy(remerge_objdir);\n+\tif (rev->remerge_diff) {\n+\t\ttmp_objdir_destroy(rev->remerge_objdir);\n+\t\trev->remerge_objdir = NULL;\n+\t}\n \n \tif (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF &&\n \t    rev->diffopt.flags.check_failed) {\ndiff --git a/log-tree.c b/log-tree.c\nindex 84ed864fc81..89da7de5dbf 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -4,6 +4,7 @@\n #include \"diff.h\"\n #include \"object-store.h\"\n #include \"repository.h\"\n+#include \"tmp-objdir.h\"\n #include \"commit.h\"\n #include \"tag.h\"\n #include \"graph.h\"\n@@ -944,7 +945,12 @@ static int do_remerge_diff(struct rev_info *opt,\n \tstrbuf_release(&parent1_desc);\n \tstrbuf_release(&parent2_desc);\n \tmerge_finalize(&o, &res);\n-\t/* TODO: clean up the temporary object directory */\n+\n+\t/* Clean up the contents of the temporary object directory */\n+\tif (opt->remerge_objdir)\n+\t\ttmp_objdir_discard_objects(opt->remerge_objdir);\n+\telse\n+\t\tBUG(\"did a remerge diff without remerge_objdir?!?\");\n \n \treturn !opt->loginfo;\n }\ndiff --git a/revision.h b/revision.h\nindex 13178e6b8f3..44efce3f410 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -318,6 +318,9 @@ struct rev_info {\n \n \t/* misc. flags related to '--no-kept-objects' */\n \tunsigned keep_pack_cache_flags;\n+\n+\t/* Location where temporary objects for remerge-diff are written. */\n+\tstruct tmp_objdir *remerge_objdir;\n };\n \n int ref_excluded(struct string_list *, const char *path);\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex 3d38eeab66b..adf6033549e 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -79,6 +79,11 @@ static void remove_tmp_objdir_on_signal(int signo)\n \traise(signo);\n }\n \n+void tmp_objdir_discard_objects(struct tmp_objdir *t)\n+{\n+\tremove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n+}\n+\n /*\n  * These env_* functions are for setting up the child environment; the\n  * \"replace\" variant overrides the value of any existing variable with that\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex cda5ec76778..76efc7edee5 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -46,6 +46,12 @@ int tmp_objdir_migrate(struct tmp_objdir *);\n  */\n int tmp_objdir_destroy(struct tmp_objdir *);\n \n+/*\n+ * Remove all objects from the temporary object directory, while leaving it\n+ * around so more objects can be added.\n+ */\n+void tmp_objdir_discard_objects(struct tmp_objdir *);\n+\n /*\n  * Add the temporary object directory as an alternate object store in the\n  * current process.\n-- \ngitgitgadget\n\n"},{"id":"447488","messageId":"ba4de88f2c403a58e4d0aa6ce6f9186e820b0817.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 03/10] ll-merge: make callers responsible for showing warnings","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:30Z","receivedAt":"2022-02-02T02:37:55Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince some callers may want to send warning messages to somewhere other\nthan stdout/stderr, stop printing \"warning: Cannot merge binary files\"\nfrom ll-merge and instead modify the return status of ll_merge() to\nindicate when a merge of binary files has occurred.  Message printing\nprobably does not belong in a \"low-level merge\" anyway.\n\nThis commit continues printing the message as-is, just from the callers\ninstead of within ll_merge().  Future changes will start handling the\nmessage differently in the merge-ort codepath.\n\nThere was one special case here: the callers in rerere.c do NOT check\nfor and print such a message; since those code paths explicitly skip\nover binary files, there is no reason to check for a return status of\nLL_MERGE_BINARY_CONFLICT or print the related message.\n\nNote that my methodology included first modifying ll_merge() to return\na struct, so that the compiler would catch all the callers for me and\nensure I had modified all of them.  After modifying all of them, I then\nchanged the struct to an enum.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n apply.c            |  5 ++++-\n builtin/checkout.c | 12 ++++++++----\n ll-merge.c         | 40 ++++++++++++++++++++++------------------\n ll-merge.h         |  9 ++++++++-\n merge-blobs.c      |  5 ++++-\n merge-ort.c        |  5 ++++-\n merge-recursive.c  |  5 ++++-\n notes-merge.c      |  5 ++++-\n rerere.c           |  9 +++++----\n 9 files changed, 63 insertions(+), 32 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex 43a0aebf4ee..8079395755f 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -3492,7 +3492,7 @@ static int three_way_merge(struct apply_state *state,\n {\n \tmmfile_t base_file, our_file, their_file;\n \tmmbuffer_t result = { NULL };\n-\tint status;\n+\tenum ll_merge_result status;\n \n \t/* resolve trivial cases first */\n \tif (oideq(base, ours))\n@@ -3509,6 +3509,9 @@ static int three_way_merge(struct apply_state *state,\n \t\t\t  &their_file, \"theirs\",\n \t\t\t  state->repo->index,\n \t\t\t  NULL);\n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \"ours\", \"theirs\");\n \tfree(base_file.ptr);\n \tfree(our_file.ptr);\n \tfree(their_file.ptr);\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex cbf73b8c9f6..3a559d69303 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -237,6 +237,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tstruct cache_entry *ce = active_cache[pos];\n \tconst char *path = ce->name;\n \tmmfile_t ancestor, ours, theirs;\n+\tenum ll_merge_result merge_status;\n \tint status;\n \tstruct object_id oid;\n \tmmbuffer_t result_buf;\n@@ -267,13 +268,16 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tmemset(&ll_opts, 0, sizeof(ll_opts));\n \tgit_config_get_bool(\"merge.renormalize\", &renormalize);\n \tll_opts.renormalize = renormalize;\n-\tstatus = ll_merge(&result_buf, path, &ancestor, \"base\",\n-\t\t\t  &ours, \"ours\", &theirs, \"theirs\",\n-\t\t\t  state->istate, &ll_opts);\n+\tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n+\t\t\t\t&ours, \"ours\", &theirs, \"theirs\",\n+\t\t\t\tstate->istate, &ll_opts);\n \tfree(ancestor.ptr);\n \tfree(ours.ptr);\n \tfree(theirs.ptr);\n-\tif (status < 0 || !result_buf.ptr) {\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \"ours\", \"theirs\");\n+\tif (merge_status < 0 || !result_buf.ptr) {\n \t\tfree(result_buf.ptr);\n \t\treturn error(_(\"path '%s': cannot merge\"), path);\n \t}\ndiff --git a/ll-merge.c b/ll-merge.c\nindex 261657578c7..a937cec59a6 100644\n--- a/ll-merge.c\n+++ b/ll-merge.c\n@@ -14,7 +14,7 @@\n \n struct ll_merge_driver;\n \n-typedef int (*ll_merge_fn)(const struct ll_merge_driver *,\n+typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -49,7 +49,7 @@ void reset_merge_attributes(void)\n /*\n  * Built-in low-levels\n  */\n-static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   mmbuffer_t *result,\n \t\t\t   const char *path,\n \t\t\t   mmfile_t *orig, const char *orig_name,\n@@ -58,6 +58,7 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t   const struct ll_merge_options *opts,\n \t\t\t   int marker_size)\n {\n+\tenum ll_merge_result ret;\n \tmmfile_t *stolen;\n \tassert(opts);\n \n@@ -68,16 +69,19 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \t */\n \tif (opts->virtual_ancestor) {\n \t\tstolen = orig;\n+\t\tret = LL_MERGE_OK;\n \t} else {\n \t\tswitch (opts->variant) {\n \t\tdefault:\n-\t\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\t\tpath, name1, name2);\n-\t\t\t/* fallthru */\n+\t\t\tret = LL_MERGE_BINARY_CONFLICT;\n+\t\t\tstolen = src1;\n+\t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_OURS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src1;\n \t\t\tbreak;\n \t\tcase XDL_MERGE_FAVOR_THEIRS:\n+\t\t\tret = LL_MERGE_OK;\n \t\t\tstolen = src2;\n \t\t\tbreak;\n \t\t}\n@@ -87,16 +91,10 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,\n \tresult->size = stolen->size;\n \tstolen->ptr = NULL;\n \n-\t/*\n-\t * With -Xtheirs or -Xours, we have cleanly merged;\n-\t * otherwise we got a conflict.\n-\t */\n-\treturn opts->variant == XDL_MERGE_FAVOR_OURS ||\n-\t       opts->variant == XDL_MERGE_FAVOR_THEIRS ?\n-\t       0 : 1;\n+\treturn ret;\n }\n \n-static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -105,7 +103,9 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \t\t\tconst struct ll_merge_options *opts,\n \t\t\tint marker_size)\n {\n+\tenum ll_merge_result ret;\n \txmparam_t xmp;\n+\tint status;\n \tassert(opts);\n \n \tif (orig->size > MAX_XDIFF_SIZE ||\n@@ -133,10 +133,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,\n \txmp.ancestor = orig_name;\n \txmp.file1 = name1;\n \txmp.file2 = name2;\n-\treturn xdl_merge(orig, src1, src2, &xmp, result);\n+\tstatus = xdl_merge(orig, src1, src2, &xmp, result);\n+\tret = (status > 0) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n-static int ll_union_merge(const struct ll_merge_driver *drv_unused,\n+static enum ll_merge_result ll_union_merge(const struct ll_merge_driver *drv_unused,\n \t\t\t  mmbuffer_t *result,\n \t\t\t  const char *path,\n \t\t\t  mmfile_t *orig, const char *orig_name,\n@@ -178,7 +180,7 @@ static void create_temp(mmfile_t *src, char *path, size_t len)\n /*\n  * User defined low-level merge driver support.\n  */\n-static int ll_ext_merge(const struct ll_merge_driver *fn,\n+static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,\n \t\t\tmmbuffer_t *result,\n \t\t\tconst char *path,\n \t\t\tmmfile_t *orig, const char *orig_name,\n@@ -194,6 +196,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \tconst char *args[] = { NULL, NULL };\n \tint status, fd, i;\n \tstruct stat st;\n+\tenum ll_merge_result ret;\n \tassert(opts);\n \n \tsq_quote_buf(&path_sq, path);\n@@ -236,7 +239,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,\n \t\tunlink_or_warn(temp[i]);\n \tstrbuf_release(&cmd);\n \tstrbuf_release(&path_sq);\n-\treturn status;\n+\tret = (status > 0) ? LL_MERGE_CONFLICT : status;\n+\treturn ret;\n }\n \n /*\n@@ -362,7 +366,7 @@ static void normalize_file(mmfile_t *mm, const char *path, struct index_state *i\n \t}\n }\n \n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/ll-merge.h b/ll-merge.h\nindex aceb1b24132..e4a20e81a3a 100644\n--- a/ll-merge.h\n+++ b/ll-merge.h\n@@ -82,13 +82,20 @@ struct ll_merge_options {\n \tlong xdl_opts;\n };\n \n+enum ll_merge_result {\n+\tLL_MERGE_ERROR = -1,\n+\tLL_MERGE_OK = 0,\n+\tLL_MERGE_CONFLICT,\n+\tLL_MERGE_BINARY_CONFLICT,\n+};\n+\n /**\n  * Perform a three-way single-file merge in core.  This is a thin wrapper\n  * around `xdl_merge` that takes the path and any merge backend specified in\n  * `.gitattributes` or `.git/info/attributes` into account.\n  * Returns 0 for a clean merge.\n  */\n-int ll_merge(mmbuffer_t *result_buf,\n+enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const char *path,\n \t     mmfile_t *ancestor, const char *ancestor_label,\n \t     mmfile_t *ours, const char *our_label,\ndiff --git a/merge-blobs.c b/merge-blobs.c\nindex ee0a0e90c94..8138090f81c 100644\n--- a/merge-blobs.c\n+++ b/merge-blobs.c\n@@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\t mmfile_t *their,\n \t\t\t\t unsigned long *size)\n {\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \tmmbuffer_t res;\n \n \t/*\n@@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate,\n \t\t\t\tistate, NULL);\n \tif (merge_status < 0)\n \t\treturn NULL;\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, \".our\", \".their\");\n \n \t*size = res.size;\n \treturn res.ptr;\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 0342f104836..c24da2ba3cb 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1743,7 +1743,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tif (!opt->priv->attr_index.initialized)\n \t\tinitialize_attr_index(opt);\n@@ -1787,6 +1787,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\tpath, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex d9457797dbb..bc73c52dd84 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt,\n \tmmfile_t orig, src1, src2;\n \tstruct ll_merge_options ll_opts = {0};\n \tchar *base, *name1, *name2;\n-\tint merge_status;\n+\tenum ll_merge_result merge_status;\n \n \tll_opts.renormalize = opt->renormalize;\n \tll_opts.extra_marker_size = extra_marker_size;\n@@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt,\n \tmerge_status = ll_merge(result_buf, a->path, &orig, base,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\topt->repo->index, &ll_opts);\n+\tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\ta->path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/notes-merge.c b/notes-merge.c\nindex b4a3a903e86..01d596920ea 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n {\n \tmmbuffer_t result_buf;\n \tmmfile_t base, local, remote;\n-\tint status;\n+\tenum ll_merge_result status;\n \n \tread_mmblob(&base, &p->base);\n \tread_mmblob(&local, &p->local);\n@@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,\n \tfree(local.ptr);\n \tfree(remote.ptr);\n \n+\tif (status == LL_MERGE_BINARY_CONFLICT)\n+\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\toid_to_hex(&p->obj), o->local_ref, o->remote_ref);\n \tif ((status < 0) || !result_buf.ptr)\n \t\tdie(\"Failed to execute internal merge\");\n \ndiff --git a/rerere.c b/rerere.c\nindex d83d58df4fb..d26627c5932 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -609,19 +609,20 @@ static int try_merge(struct index_state *istate,\n \t\t     const struct rerere_id *id, const char *path,\n \t\t     mmfile_t *cur, mmbuffer_t *result)\n {\n-\tint ret;\n+\tenum ll_merge_result ret;\n \tmmfile_t base = {NULL, 0}, other = {NULL, 0};\n \n \tif (read_mmfile(&base, rerere_path(id, \"preimage\")) ||\n-\t    read_mmfile(&other, rerere_path(id, \"postimage\")))\n-\t\tret = 1;\n-\telse\n+\t    read_mmfile(&other, rerere_path(id, \"postimage\"))) {\n+\t\tret = LL_MERGE_CONFLICT;\n+\t} else {\n \t\t/*\n \t\t * A three-way merge. Note that this honors user-customizable\n \t\t * low-level merge driver settings.\n \t\t */\n \t\tret = ll_merge(result, path, &base, NULL, cur, \"\", &other, \"\",\n \t\t\t       istate, NULL);\n+\t}\n \n \tfree(base.ptr);\n \tfree(other.ptr);\n-- \ngitgitgadget\n\n"},{"id":"447489","messageId":"d7a1f4e1f9f5bdbd2ebff1586af73d2ea1a0319e.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 04/10] merge-ort: capture and print ll-merge warnings in our preferred fashion","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:31Z","receivedAt":"2022-02-02T02:37:57Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nInstead of immediately printing ll-merge warnings to stderr, we save\nthem in our output strbuf.  Besides allowing us to move these warnings\nto a special file for --remerge-diff, this has two other benefits for\nregular merges done by merge-ort:\n\n  * The deferral of messages ensures we can print all messages about\n    any given path together (merge-recursive was known to sometimes\n    intersperse messages about other paths, particularly when renames\n    were involved).\n\n  * The deferral of messages means we can avoid printing spurious\n    conflict messages when we just end up aborting due to local user\n    modifications in the way.  (In contrast to merge-recursive.c which\n    prematurely checks for local modifications in the way via\n    unpack_trees() and gets the check wrong both in terms of false\n    positives and false negatives relative to renames, merge-ort does\n    not perform the local modifications in the way check until the\n    checkout() step after the full merge has been computed.)\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c                | 5 +++--\n t/t6404-recursive-merge.sh | 9 +++++++--\n t/t6406-merge-attr.sh      | 9 +++++++--\n 3 files changed, 17 insertions(+), 6 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex c24da2ba3cb..a18f47e23c5 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -1788,8 +1788,9 @@ static int merge_3way(struct merge_options *opt,\n \t\t\t\t&src1, name1, &src2, name2,\n \t\t\t\t&opt->priv->attr_index, &ll_opts);\n \tif (merge_status == LL_MERGE_BINARY_CONFLICT)\n-\t\twarning(\"Cannot merge binary files: %s (%s vs. %s)\",\n-\t\t\tpath, name1, name2);\n+\t\tpath_msg(opt, path, 0,\n+\t\t\t \"warning: Cannot merge binary files: %s (%s vs. %s)\",\n+\t\t\t path, name1, name2);\n \n \tfree(base);\n \tfree(name1);\ndiff --git a/t/t6404-recursive-merge.sh b/t/t6404-recursive-merge.sh\nindex eaf48e941e2..b8735c6db4d 100755\n--- a/t/t6404-recursive-merge.sh\n+++ b/t/t6404-recursive-merge.sh\n@@ -108,8 +108,13 @@ test_expect_success 'refuse to merge binary files' '\n \tprintf \"\\0\\0\" >binary-file &&\n \tgit add binary-file &&\n \tgit commit -m binary2 &&\n-\ttest_must_fail git merge F >merge.out 2>merge.err &&\n-\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge.err\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge F >merge_output\n+\telse\n+\t\ttest_must_fail git merge F 2>merge_output\n+\tfi &&\n+\tgrep \"Cannot merge binary files: binary-file (HEAD vs. F)\" merge_output\n '\n \n test_expect_success 'mark rename/delete as unmerged' '\ndiff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh\nindex 84946458371..c41584eb33e 100755\n--- a/t/t6406-merge-attr.sh\n+++ b/t/t6406-merge-attr.sh\n@@ -221,8 +221,13 @@ test_expect_success 'binary files with union attribute' '\n \tprintf \"two\\0\" >bin.txt &&\n \tgit commit -am two &&\n \n-\ttest_must_fail git merge bin-main 2>stderr &&\n-\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" stderr\n+\tif test \"$GIT_TEST_MERGE_ALGORITHM\" = ort\n+\tthen\n+\t\ttest_must_fail git merge bin-main >output\n+\telse\n+\t\ttest_must_fail git merge bin-main 2>output\n+\tfi &&\n+\tgrep -i \"warning.*cannot merge.*HEAD vs. bin-main\" output\n '\n \n test_done\n-- \ngitgitgadget\n\n"},{"id":"447490","messageId":"cbde4e5d37280c5bfb2e61889506cfd0c3ed30c4.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 05/10] merge-ort: mark a few more conflict messages as omittable","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:32Z","receivedAt":"2022-02-02T02:37:58Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\npath_msg() has the ability to mark messages as omittable, designed for\nremerge-diff where we'll instead be showing conflict messages as diff\nheaders for a subsequent diff.  While all these messages are very useful\nwhen trying to create a merge initially, early use with the\n--remerge-diff feature (the only user of this omittable conflict message\ncapability), suggests that the particular messages marked in this commit\nare just noise when trying to see what changes users made to create a\nmerge commit.  Mark them as omittable.\n\nNote that there were already a few messages marked as omittable in\nmerge-ort when doing a remerge-diff, because the development of\n--remerge-diff preceded the upstreaming of merge-ort and I was trying to\nensure merge-ort could handle all the necessary requirements.  See\ncommit c5a6f65527 (\"merge-ort: add modify/delete handling and delayed\noutput processing\", 2020-12-03) for the initial details.  For some\nexamples of already-marked-as-omittable messages, see either\n\"Auto-merging <path>\" or some of the submodule update hints.  This\ncommit just adds two more messages that should also be omittable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex a18f47e23c5..998e92ec593 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -2420,7 +2420,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t */\n \t\tci->path_conflict = 1;\n \t\tif (pair->status == 'A')\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s added in %s \"\n \t\t\t\t   \"inside a directory that was renamed in %s, \"\n \t\t\t\t   \"suggesting it should perhaps be moved to \"\n@@ -2428,7 +2428,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,\n \t\t\t\t old_path, branch_with_new_path,\n \t\t\t\t branch_with_dir_rename, new_path);\n \t\telse\n-\t\t\tpath_msg(opt, new_path, 0,\n+\t\t\tpath_msg(opt, new_path, 1,\n \t\t\t\t _(\"CONFLICT (file location): %s renamed to %s \"\n \t\t\t\t   \"in %s, inside a directory that was renamed \"\n \t\t\t\t   \"in %s, suggesting it should perhaps be \"\n-- \ngitgitgadget\n\n"},{"id":"447491","messageId":"4d79da6e20a1ebbfac9869d7584d279d280016f0.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 07/10] diff: add ability to insert additional headers for paths","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:34Z","receivedAt":"2022-02-02T02:38:00Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen additional headers are provided, we need to\n  * add diff_filepairs to diff_queued_diff for each paths in the\n    additional headers map which, unless that path is part of\n    another diff_filepair already found in diff_queued_diff\n  * format the headers (colorization, line_prefix for --graph)\n  * make sure the various codepaths that attempt to return early\n    if there are \"no changes\" take into account the headers that\n    need to be shown.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n diff.c     | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++--\n diff.h     |   3 +-\n log-tree.c |   2 +-\n 3 files changed, 123 insertions(+), 6 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 861282db1c3..1bfb01c18ec 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -27,6 +27,7 @@\n #include \"help.h\"\n #include \"promisor-remote.h\"\n #include \"dir.h\"\n+#include \"strmap.h\"\n \n #ifdef NO_FAST_WORKING_DIRECTORY\n #define FAST_WORKING_DIRECTORY 0\n@@ -3406,6 +3407,31 @@ struct userdiff_driver *get_textconv(struct repository *r,\n \treturn userdiff_get_textconv(r, one->driver);\n }\n \n+static struct strbuf *additional_headers(struct diff_options *o,\n+\t\t\t\t\t const char *path)\n+{\n+\tif (!o->additional_path_headers)\n+\t\treturn NULL;\n+\treturn strmap_get(o->additional_path_headers, path);\n+}\n+\n+static void add_formatted_headers(struct strbuf *msg,\n+\t\t\t\t  struct strbuf *more_headers,\n+\t\t\t\t  const char *line_prefix,\n+\t\t\t\t  const char *meta,\n+\t\t\t\t  const char *reset)\n+{\n+\tchar *next, *newline;\n+\n+\tfor (next = more_headers->buf; *next; next = newline) {\n+\t\tnewline = strchrnul(next, '\\n');\n+\t\tstrbuf_addf(msg, \"%s%s%.*s%s\\n\", line_prefix, meta,\n+\t\t\t    (int)(newline - next), next, reset);\n+\t\tif (*newline)\n+\t\t\tnewline++;\n+\t}\n+}\n+\n static void builtin_diff(const char *name_a,\n \t\t\t const char *name_b,\n \t\t\t struct diff_filespec *one,\n@@ -3464,6 +3490,17 @@ static void builtin_diff(const char *name_a,\n \tb_two = quote_two(b_prefix, name_b + (*name_b == '/'));\n \tlbl[0] = DIFF_FILE_VALID(one) ? a_one : \"/dev/null\";\n \tlbl[1] = DIFF_FILE_VALID(two) ? b_two : \"/dev/null\";\n+\tif (!DIFF_FILE_VALID(one) && !DIFF_FILE_VALID(two)) {\n+\t\t/*\n+\t\t * We should only reach this point for pairs from\n+\t\t * create_filepairs_for_header_only_notifications().  For\n+\t\t * these, we should avoid the \"/dev/null\" special casing\n+\t\t * above, meaning we avoid showing such pairs as either\n+\t\t * \"new file\" or \"deleted file\" below.\n+\t\t */\n+\t\tlbl[0] = a_one;\n+\t\tlbl[1] = b_two;\n+\t}\n \tstrbuf_addf(&header, \"%s%sdiff --git %s %s%s\\n\", line_prefix, meta, a_one, b_two, reset);\n \tif (lbl[0][0] == '/') {\n \t\t/* /dev/null */\n@@ -4328,6 +4365,7 @@ static void fill_metainfo(struct strbuf *msg,\n \tconst char *set = diff_get_color(use_color, DIFF_METAINFO);\n \tconst char *reset = diff_get_color(use_color, DIFF_RESET);\n \tconst char *line_prefix = diff_line_prefix(o);\n+\tstruct strbuf *more_headers = NULL;\n \n \t*must_show_header = 1;\n \tstrbuf_init(msg, PATH_MAX * 2 + 300);\n@@ -4364,6 +4402,11 @@ static void fill_metainfo(struct strbuf *msg,\n \tdefault:\n \t\t*must_show_header = 0;\n \t}\n+\tif ((more_headers = additional_headers(o, name))) {\n+\t\tadd_formatted_headers(msg, more_headers,\n+\t\t\t\t      line_prefix, set, reset);\n+\t\t*must_show_header = 1;\n+\t}\n \tif (one && two && !oideq(&one->oid, &two->oid)) {\n \t\tconst unsigned hexsz = the_hash_algo->hexsz;\n \t\tint abbrev = o->abbrev ? o->abbrev : DEFAULT_ABBREV;\n@@ -5852,12 +5895,27 @@ int diff_unmodified_pair(struct diff_filepair *p)\n \n static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)\n {\n-\tif (diff_unmodified_pair(p))\n+\tint include_conflict_headers =\n+\t    (additional_headers(o, p->one->path) &&\n+\t     (!o->filter || filter_bit_tst(DIFF_STATUS_UNMERGED, o)));\n+\n+\t/*\n+\t * Check if we can return early without showing a diff.  Note that\n+\t * diff_filepair only stores {oid, path, mode, is_valid}\n+\t * information for each path, and thus diff_unmodified_pair() only\n+\t * considers those bits of info.  However, we do not want pairs\n+\t * created by create_filepairs_for_header_only_notifications()\n+\t * (which always look like unmodified pairs) to be ignored, so\n+\t * return early if both p is unmodified AND we don't want to\n+\t * include_conflict_headers.\n+\t */\n+\tif (diff_unmodified_pair(p) && !include_conflict_headers)\n \t\treturn;\n \n+\t/* Actually, we can also return early to avoid showing tree diffs */\n \tif ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||\n \t    (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))\n-\t\treturn; /* no tree diffs in patch format */\n+\t\treturn;\n \n \trun_diff(p, o);\n }\n@@ -5888,10 +5946,17 @@ static void diff_flush_checkdiff(struct diff_filepair *p,\n \trun_checkdiff(p, o);\n }\n \n-int diff_queue_is_empty(void)\n+int diff_queue_is_empty(struct diff_options *o)\n {\n \tstruct diff_queue_struct *q = &diff_queued_diff;\n \tint i;\n+\tint include_conflict_headers =\n+\t    (o->additional_path_headers &&\n+\t     (!o->filter || filter_bit_tst(DIFF_STATUS_UNMERGED, o)));\n+\n+\tif (include_conflict_headers)\n+\t\treturn 0;\n+\n \tfor (i = 0; i < q->nr; i++)\n \t\tif (!diff_unmodified_pair(q->queue[i]))\n \t\t\treturn 0;\n@@ -6325,6 +6390,54 @@ void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)\n \t\twarning(_(rename_limit_advice), varname, needed);\n }\n \n+static void create_filepairs_for_header_only_notifications(struct diff_options *o)\n+{\n+\tstruct strset present;\n+\tstruct diff_queue_struct *q = &diff_queued_diff;\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *e;\n+\tint i;\n+\n+\tstrset_init_with_options(&present, /*pool*/ NULL, /*strdup*/ 0);\n+\n+\t/*\n+\t * Find out which paths exist in diff_queued_diff, preferring\n+\t * one->path for any pair that has multiple paths.\n+\t */\n+\tfor (i = 0; i < q->nr; i++) {\n+\t\tstruct diff_filepair *p = q->queue[i];\n+\t\tchar *path = p->one->path ? p->one->path : p->two->path;\n+\n+\t\tif (strmap_contains(o->additional_path_headers, path))\n+\t\t\tstrset_add(&present, path);\n+\t}\n+\n+\t/*\n+\t * Loop over paths in additional_path_headers; for each NOT already\n+\t * in diff_queued_diff, create a synthetic filepair and insert that\n+\t * into diff_queued_diff.\n+\t */\n+\tstrmap_for_each_entry(o->additional_path_headers, &iter, e) {\n+\t\tif (!strset_contains(&present, e->key)) {\n+\t\t\tstruct diff_filespec *one, *two;\n+\t\t\tstruct diff_filepair *p;\n+\n+\t\t\tone = alloc_filespec(e->key);\n+\t\t\ttwo = alloc_filespec(e->key);\n+\t\t\tfill_filespec(one, null_oid(), 0, 0);\n+\t\t\tfill_filespec(two, null_oid(), 0, 0);\n+\t\t\tp = diff_queue(q, one, two);\n+\t\t\tp->status = DIFF_STATUS_MODIFIED;\n+\t\t}\n+\t}\n+\n+\t/* Re-sort the filepairs */\n+\tdiffcore_fix_diff_index();\n+\n+\t/* Cleanup */\n+\tstrset_clear(&present);\n+}\n+\n static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n {\n \tint i;\n@@ -6337,6 +6450,9 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)\n \tif (o->color_moved)\n \t\to->emitted_symbols = &esm;\n \n+\tif (o->additional_path_headers)\n+\t\tcreate_filepairs_for_header_only_notifications(o);\n+\n \tfor (i = 0; i < q->nr; i++) {\n \t\tstruct diff_filepair *p = q->queue[i];\n \t\tif (check_pair_status(p))\n@@ -6413,7 +6529,7 @@ void diff_flush(struct diff_options *options)\n \t * Order: raw, stat, summary, patch\n \t * or:    name/name-status/checkdiff (other bits clear)\n \t */\n-\tif (!q->nr)\n+\tif (!q->nr && !options->additional_path_headers)\n \t\tgoto free_queue;\n \n \tif (output_format & (DIFF_FORMAT_RAW |\ndiff --git a/diff.h b/diff.h\nindex 8ba85c5e605..ce9e2cf2e4f 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -395,6 +395,7 @@ struct diff_options {\n \n \tstruct repository *repo;\n \tstruct option *parseopts;\n+\tstruct strmap *additional_path_headers;\n \n \tint no_free;\n };\n@@ -593,7 +594,7 @@ void diffcore_fix_diff_index(void);\n \"                show all files diff when -S is used and hit is found.\\n\" \\\n \"  -a  --text    treat all files as text.\\n\"\n \n-int diff_queue_is_empty(void);\n+int diff_queue_is_empty(struct diff_options *o);\n void diff_flush(struct diff_options*);\n void diff_free(struct diff_options*);\n void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);\ndiff --git a/log-tree.c b/log-tree.c\nindex 89da7de5dbf..8013edcc5d4 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -850,7 +850,7 @@ int log_tree_diff_flush(struct rev_info *opt)\n \topt->shown_dashes = 0;\n \tdiffcore_std(&opt->diffopt);\n \n-\tif (diff_queue_is_empty()) {\n+\tif (diff_queue_is_empty(&opt->diffopt)) {\n \t\tint saved_fmt = opt->diffopt.output_format;\n \t\topt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT;\n \t\tdiff_flush(&opt->diffopt);\n-- \ngitgitgadget\n\n"},{"id":"447492","messageId":"59d12f213b25087fc610d1151f3bb790046fe6ca.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 10/10] diff-merges: avoid history simplifications when diffing merges","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:37Z","receivedAt":"2022-02-02T02:38:01Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nDoing diffs for merges are special; they should typically avoid history\nsimplification.  For example, with\n\n    git log --diff-merges=first-parent -- path\n\nthe default history simplification would remove merge commits from\nconsideration if the file \"path\" matched the second parent.  That is\ncounter to what the user wants when looking for first-parent diffs.\nSimilar comments can be made for --diff-merges=separate (which diffs\nagainst both parents) and --diff-merges=remerge (which diffs against a\nremerge of the merge commit).\n\nHowever, history simplification still makes sense if not doing diffing\nmerges, and it also makes sense for the combined and dense-combined\nforms of diffing merges (because both of those are defined to only show\na diff when the merge result at the relevant paths differs from *both*\nparents).\n\nSo, for separate, first-parent, and remerge styles of diff-merges, turn\noff history simplification.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n diff-merges.c           |  2 ++\n t/t4069-remerge-diff.sh | 58 ++++++++++++++++++++++++++++++++++++++++-\n 2 files changed, 59 insertions(+), 1 deletion(-)\n\ndiff --git a/diff-merges.c b/diff-merges.c\nindex 0af4b3f9191..a833fd747ad 100644\n--- a/diff-merges.c\n+++ b/diff-merges.c\n@@ -24,6 +24,7 @@ static void set_separate(struct rev_info *revs)\n {\n \tsuppress(revs);\n \trevs->separate_merges = 1;\n+\trevs->simplify_history = 0;\n }\n \n static void set_first_parent(struct rev_info *revs)\n@@ -50,6 +51,7 @@ static void set_remerge_diff(struct rev_info *revs)\n {\n \tsuppress(revs);\n \trevs->remerge_diff = 1;\n+\trevs->simplify_history = 0;\n }\n \n static diff_merges_setup_func_t func_by_opt(const char *optarg)\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nindex fd6bce64781..35f94957fce 100755\n--- a/t/t4069-remerge-diff.sh\n+++ b/t/t4069-remerge-diff.sh\n@@ -227,7 +227,63 @@ test_expect_success 'remerge-diff w/ pathspec: limits to relevant file including\n \t# with sha256\n \tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n \n-\tgit show --oneline --remerge-diff --full-history resolution -- \"letters*\" >tmp &&\n+\tgit show --oneline --remerge-diff resolution -- \"letters*\" >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup non-content conflicts' '\n+\tgit switch --orphan newbase &&\n+\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m base &&\n+\n+\tgit branch newside1 &&\n+\tgit branch newside2 &&\n+\n+\tgit checkout newside1 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m side1 &&\n+\n+\tgit checkout newside2 &&\n+\ttest_write_lines 1 2 drei 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit commit -m side2 &&\n+\n+\tgit checkout -b newresolution newside1 &&\n+\ttest_must_fail git merge newside2 &&\n+\tgit checkout --theirs numbers &&\n+\tgit add -u numbers &&\n+\tgit commit -m resolved\n+'\n+\n+test_expect_success 'remerge-diff turns off history simplification' '\n+\tgit log -1 --oneline newresolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (content): Merge conflict in numbers\n+\tindex 070e9e7..5335e78 100644\n+\t--- a/numbers\n+\t+++ b/numbers\n+\t@@ -1,10 +1,6 @@\n+\t 1\n+\t 2\n+\t-<<<<<<< 96f1e45 (side1)\n+\t-three\n+\t-=======\n+\t drei\n+\t->>>>>>> 4fd522f (side2)\n+\t 4\n+\t 5\n+\t 6\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff newresolution -- numbers >tmp &&\n \tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n \ttest_cmp expect actual\n '\n-- \ngitgitgadget\n"},{"id":"447493","messageId":"ff9c14b0b7c11399ba27aae2a8541a1efa185884.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 08/10] show, log: include conflict/warning messages in --remerge-diff headers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:35Z","receivedAt":"2022-02-02T02:38:10Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nConflicts such as modify/delete, rename/rename, or file/directory are\nnot representable via content conflict markers, and the normal output\nmessages notifying users about these were dropped with --remerge-diff.\nWhile we don't want these messages randomly shown before the commit\nand diff headers, we do want them to still be shown; include them as\npart of the diff headers instead.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n log-tree.c              |  51 ++++++++++++++\n merge-ort.c             |   1 +\n merge-ort.h             |  10 +++\n t/t4069-remerge-diff.sh | 144 ++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 206 insertions(+)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 8013edcc5d4..d93bafa5be3 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -19,6 +19,7 @@\n #include \"line-log.h\"\n #include \"help.h\"\n #include \"range-diff.h\"\n+#include \"strmap.h\"\n \n static struct decoration name_decoration = { \"object names\" };\n static int decoration_loaded;\n@@ -905,6 +906,52 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)\n \treturn !opt->loginfo;\n }\n \n+static void setup_additional_headers(struct diff_options *o,\n+\t\t\t\t     struct strmap *all_headers)\n+{\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *entry;\n+\n+\t/*\n+\t * Make o->additional_path_headers contain the subset of all_headers\n+\t * that match o->pathspec.  If there aren't any that match o->pathspec,\n+\t * then make o->additional_path_headers be NULL.\n+\t */\n+\n+\tif (!o->pathspec.nr) {\n+\t\to->additional_path_headers = all_headers;\n+\t\treturn;\n+\t}\n+\n+\to->additional_path_headers = xmalloc(sizeof(struct strmap));\n+\tstrmap_init_with_options(o->additional_path_headers, NULL, 0);\n+\tstrmap_for_each_entry(all_headers, &iter, entry) {\n+\t\tif (match_pathspec(the_repository->index, &o->pathspec,\n+\t\t\t\t   entry->key, strlen(entry->key),\n+\t\t\t\t   0 /* prefix */, NULL /* seen */,\n+\t\t\t\t   0 /* is_dir */))\n+\t\t\tstrmap_put(o->additional_path_headers,\n+\t\t\t\t   entry->key, entry->value);\n+\t}\n+\tif (!strmap_get_size(o->additional_path_headers)) {\n+\t\tstrmap_clear(o->additional_path_headers, 0);\n+\t\tFREE_AND_NULL(o->additional_path_headers);\n+\t}\n+}\n+\n+static void cleanup_additional_headers(struct diff_options *o)\n+{\n+\tif (!o->pathspec.nr) {\n+\t\to->additional_path_headers = NULL;\n+\t\treturn;\n+\t}\n+\tif (!o->additional_path_headers)\n+\t\treturn;\n+\n+\tstrmap_clear(o->additional_path_headers, 0);\n+\tFREE_AND_NULL(o->additional_path_headers);\n+}\n+\n static int do_remerge_diff(struct rev_info *opt,\n \t\t\t   struct commit_list *parents,\n \t\t\t   struct object_id *oid,\n@@ -922,6 +969,8 @@ static int do_remerge_diff(struct rev_info *opt,\n \t/* Setup merge options */\n \tinit_merge_options(&o, the_repository);\n \to.show_rename_progress = 0;\n+\to.record_conflict_msgs_as_headers = 1;\n+\to.msg_header_prefix = \"remerge\";\n \n \tctx.abbrev = DEFAULT_ABBREV;\n \tformat_commit_message(parent1, \"%h (%s)\", &parent1_desc, &ctx);\n@@ -938,10 +987,12 @@ static int do_remerge_diff(struct rev_info *opt,\n \tmerge_incore_recursive(&o, bases, parent1, parent2, &res);\n \n \t/* Show the diff */\n+\tsetup_additional_headers(&opt->diffopt, res.path_messages);\n \tdiff_tree_oid(&res.tree->object.oid, oid, \"\", &opt->diffopt);\n \tlog_tree_diff_flush(opt);\n \n \t/* Cleanup */\n+\tcleanup_additional_headers(&opt->diffopt);\n \tstrbuf_release(&parent1_desc);\n \tstrbuf_release(&parent2_desc);\n \tmerge_finalize(&o, &res);\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 481305d2bcf..43f980d2586 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -4585,6 +4585,7 @@ redo:\n \ttrace2_region_leave(\"merge\", \"process_entries\", opt->repo);\n \n \t/* Set return values */\n+\tresult->path_messages = &opt->priv->output;\n \tresult->tree = parse_tree_indirect(&working_tree_oid);\n \t/* existence of conflicted entries implies unclean */\n \tresult->clean &= strmap_empty(&opt->priv->conflicted);\ndiff --git a/merge-ort.h b/merge-ort.h\nindex c011864ffeb..fe599b87868 100644\n--- a/merge-ort.h\n+++ b/merge-ort.h\n@@ -5,6 +5,7 @@\n \n struct commit;\n struct tree;\n+struct strmap;\n \n struct merge_result {\n \t/*\n@@ -23,6 +24,15 @@ struct merge_result {\n \t */\n \tstruct tree *tree;\n \n+\t/*\n+\t * Special messages and conflict notices for various paths\n+\t *\n+\t * This is a map of pathnames to strbufs.  It contains various\n+\t * warning/conflict/notice messages (possibly multiple per path)\n+\t * that callers may want to use.\n+\t */\n+\tstruct strmap *path_messages;\n+\n \t/*\n \t * Additional metadata used by merge_switch_to_result() or future calls\n \t * to merge_incore_*().  Includes data needed to update the index (if\ndiff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh\nindex d7ab0f50066..fd6bce64781 100755\n--- a/t/t4069-remerge-diff.sh\n+++ b/t/t4069-remerge-diff.sh\n@@ -60,6 +60,7 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n \tgit log -1 --oneline ab_resolution >tmp &&\n \tcat <<-EOF >>tmp &&\n \tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (content): Merge conflict in numbers\n \tindex a1fb731..6875544 100644\n \t--- a/numbers\n \t+++ b/numbers\n@@ -88,4 +89,147 @@ test_expect_success 'remerge-diff with both a resolved conflict and an unrelated\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup non-content conflicts' '\n+\tgit switch --orphan base &&\n+\n+\ttest_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&\n+\ttest_write_lines a b c d e f g h i >letters &&\n+\ttest_write_lines in the way >content &&\n+\tgit add numbers letters content &&\n+\tgit commit -m base &&\n+\n+\tgit branch side1 &&\n+\tgit branch side2 &&\n+\n+\tgit checkout side1 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit mv letters letters_side1 &&\n+\tgit mv content file_or_directory &&\n+\tgit add numbers &&\n+\tgit commit -m side1 &&\n+\n+\tgit checkout side2 &&\n+\tgit rm numbers &&\n+\tgit mv letters letters_side2 &&\n+\tmkdir file_or_directory &&\n+\techo hello >file_or_directory/world &&\n+\tgit add file_or_directory/world &&\n+\tgit commit -m side2 &&\n+\n+\tgit checkout -b resolution side1 &&\n+\ttest_must_fail git merge side2 &&\n+\ttest_write_lines 1 2 three 4 5 6 7 8 9 >numbers &&\n+\tgit add numbers &&\n+\tgit add letters_side1 &&\n+\tgit rm letters &&\n+\tgit rm letters_side2 &&\n+\tgit add file_or_directory~HEAD &&\n+\tgit mv file_or_directory~HEAD wanted_content &&\n+\tgit commit -m resolved\n+'\n+\n+test_expect_success 'remerge-diff with non-content conflicts' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n+\tsimilarity index 100%\n+\trename from file_or_directory~HASH (side1)\n+\trename to wanted_content\n+\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n+\tdiff --git a/letters b/letters\n+\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n+\tdiff --git a/letters_side2 b/letters_side2\n+\tdeleted file mode 100644\n+\tindex b236ae5..0000000\n+\t--- a/letters_side2\n+\t+++ /dev/null\n+\t@@ -1,9 +0,0 @@\n+\t-a\n+\t-b\n+\t-c\n+\t-d\n+\t-e\n+\t-f\n+\t-g\n+\t-h\n+\t-i\n+\tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff w/ diff-filter=U: all conflict headers, no diff content' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/file_or_directory~HASH (side1) b/file_or_directory~HASH (side1)\n+\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n+\tdiff --git a/letters b/letters\n+\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n+\tdiff --git a/numbers b/numbers\n+\tremerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1).  Version HASH (side1) of numbers left in tree.\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff --diff-filter=U resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff w/ diff-filter=R: relevant file + conflict header' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/file_or_directory~HASH (side1) b/wanted_content\n+\tsimilarity index 100%\n+\trename from file_or_directory~HASH (side1)\n+\trename to wanted_content\n+\tremerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead.\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff --diff-filter=R resolution >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remerge-diff w/ pathspec: limits to relevant file including conflict header' '\n+\tgit log -1 --oneline resolution >tmp &&\n+\tcat <<-EOF >>tmp &&\n+\tdiff --git a/letters b/letters\n+\tremerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2).\n+\tdiff --git a/letters_side2 b/letters_side2\n+\tdeleted file mode 100644\n+\tindex b236ae5..0000000\n+\t--- a/letters_side2\n+\t+++ /dev/null\n+\t@@ -1,9 +0,0 @@\n+\t-a\n+\t-b\n+\t-c\n+\t-d\n+\t-e\n+\t-f\n+\t-g\n+\t-h\n+\t-i\n+\tEOF\n+\t# We still have some sha1 hashes above; rip them out so test works\n+\t# with sha256\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >expect &&\n+\n+\tgit show --oneline --remerge-diff --full-history resolution -- \"letters*\" >tmp &&\n+\tsed -e \"s/[0-9a-f]\\{7,\\}/HASH/g\" tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"447494","messageId":"d3e4242a5bdf4cfc8a6ca3a8d2c9f4868bf6f729.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 06/10] merge-ort: format messages slightly different for use in headers","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:33Z","receivedAt":"2022-02-02T02:40:01Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen users run\n    git show --remerge-diff $MERGE_COMMIT\nor\n    git log -p --remerge-diff ...\nstdout is not an appropriate location to dump conflict messages, but we\ndo want to provide them to users.  We will include them in the diff\nheaders instead...but for that to work, we need for any multiline\nmessages to replace newlines with both a newline and a space.  Add a new\nflag to signal when we want these messages modified in such a fashion,\nand use it in path_msg() to modify these messages this way.  Also, allow\na special prefix to be specified for these headers.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c       | 42 ++++++++++++++++++++++++++++++++++++++++--\n merge-recursive.c |  4 ++++\n merge-recursive.h |  2 ++\n 3 files changed, 46 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 998e92ec593..481305d2bcf 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -634,17 +634,49 @@ static void path_msg(struct merge_options *opt,\n \t\t     const char *fmt, ...)\n {\n \tva_list ap;\n-\tstruct strbuf *sb = strmap_get(&opt->priv->output, path);\n+\tstruct strbuf *sb, *dest;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\n+\tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n+\t\treturn; /* Do not record mere hints in tree */\n+\tsb = strmap_get(&opt->priv->output, path);\n \tif (!sb) {\n \t\tsb = xmalloc(sizeof(*sb));\n \t\tstrbuf_init(sb, 0);\n \t\tstrmap_put(&opt->priv->output, path, sb);\n \t}\n \n+\tdest = (opt->record_conflict_msgs_as_headers ? &tmp : sb);\n+\n \tva_start(ap, fmt);\n-\tstrbuf_vaddf(sb, fmt, ap);\n+\tstrbuf_vaddf(dest, fmt, ap);\n \tva_end(ap);\n \n+\tif (opt->record_conflict_msgs_as_headers) {\n+\t\tint i_sb = 0, i_tmp = 0;\n+\n+\t\t/* Start with the specified prefix */\n+\t\tif (opt->msg_header_prefix)\n+\t\t\tstrbuf_addf(sb, \"%s \", opt->msg_header_prefix);\n+\n+\t\t/* Copy tmp to sb, adding spaces after newlines */\n+\t\tstrbuf_grow(sb, sb->len + 2*tmp.len); /* more than sufficient */\n+\t\tfor (; i_tmp < tmp.len; i_tmp++, i_sb++) {\n+\t\t\t/* Copy next character from tmp to sb */\n+\t\t\tsb->buf[sb->len + i_sb] = tmp.buf[i_tmp];\n+\n+\t\t\t/* If we copied a newline, add a space */\n+\t\t\tif (tmp.buf[i_tmp] == '\\n')\n+\t\t\t\tsb->buf[++i_sb] = ' ';\n+\t\t}\n+\t\t/* Update length and ensure it's NUL-terminated */\n+\t\tsb->len += i_sb;\n+\t\tsb->buf[sb->len] = '\\0';\n+\n+\t\tstrbuf_release(&tmp);\n+\t}\n+\n+\t/* Add final newline character to sb */\n \tstrbuf_addch(sb, '\\n');\n }\n \n@@ -4246,6 +4278,9 @@ void merge_switch_to_result(struct merge_options *opt,\n \t\tstruct string_list olist = STRING_LIST_INIT_NODUP;\n \t\tint i;\n \n+\t\tif (opt->record_conflict_msgs_as_headers)\n+\t\t\tBUG(\"Either display conflict messages or record them as headers, not both\");\n+\n \t\ttrace2_region_enter(\"merge\", \"display messages\", opt->repo);\n \n \t\t/* Hack to pre-allocate olist to the desired size */\n@@ -4347,6 +4382,9 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)\n \tassert(opt->recursive_variant >= MERGE_VARIANT_NORMAL &&\n \t       opt->recursive_variant <= MERGE_VARIANT_THEIRS);\n \n+\tif (opt->msg_header_prefix)\n+\t\tassert(opt->record_conflict_msgs_as_headers);\n+\n \t/*\n \t * detect_renames, verbosity, buffer_output, and obuf are ignored\n \t * fields that were used by \"recursive\" rather than \"ort\" -- but\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex bc73c52dd84..9ec1e6d043a 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -3714,6 +3714,10 @@ static int merge_start(struct merge_options *opt, struct tree *head)\n \n \tassert(opt->priv == NULL);\n \n+\t/* Not supported; option specific to merge-ort */\n+\tassert(!opt->record_conflict_msgs_as_headers);\n+\tassert(!opt->msg_header_prefix);\n+\n \t/* Sanity check on repo state; index must match head */\n \tif (repo_index_has_changes(opt->repo, head, &sb)) {\n \t\terr(opt, _(\"Your local changes to the following files would be overwritten by merge:\\n  %s\"),\ndiff --git a/merge-recursive.h b/merge-recursive.h\nindex 0795a1d3ec1..b88000e3c25 100644\n--- a/merge-recursive.h\n+++ b/merge-recursive.h\n@@ -46,6 +46,8 @@ struct merge_options {\n \t/* miscellaneous control options */\n \tconst char *subtree_shift;\n \tunsigned renormalize : 1;\n+\tunsigned record_conflict_msgs_as_headers : 1;\n+\tconst char *msg_header_prefix;\n \n \t/* internal fields used by the implementation */\n \tstruct merge_options_internal *priv;\n-- \ngitgitgadget\n\n"},{"id":"447495","messageId":"aa63860cd0fe57e9a568e724f683af89f99dcd3e.1643769457.git.gitgitgadget@gmail.com","threadId":"57133","inReplyTo":"pull.1103.v5.git.1643769457.gitgitgadget@gmail.com","subject":"[PATCH v5 09/10] merge-ort: mark conflict/warning messages from inner merges as omittable","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-02-02T02:37:36Z","receivedAt":"2022-02-02T02:40:01Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nA recursive merge involves merging the merge bases of the two branches\nbeing merged.  Such an inner merge can itself generate conflict notices.\nWhile such notices may be useful when initially trying to create a\nmerge, they seem to just be noise when investigating merges later with\n--remerge-diff.  (Especially when both sides of the outer merge resolved\nthe conflict the same way leading to no overall conflict.)  Remove them.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-ort.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/merge-ort.c b/merge-ort.c\nindex 43f980d2586..9bf15a01db8 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -638,7 +638,9 @@ static void path_msg(struct merge_options *opt,\n \tstruct strbuf tmp = STRBUF_INIT;\n \n \tif (opt->record_conflict_msgs_as_headers && omittable_hint)\n-\t\treturn; /* Do not record mere hints in tree */\n+\t\treturn; /* Do not record mere hints in headers */\n+\tif (opt->record_conflict_msgs_as_headers && opt->priv->call_depth)\n+\t\treturn; /* Do not record inner merge issues in headers */\n \tsb = strmap_get(&opt->priv->output, path);\n \tif (!sb) {\n \t\tsb = xmalloc(sizeof(*sb));\n-- \ngitgitgadget\n\n"},{"id":"447532","messageId":"220202.868rut8qkf.gmgdl@evledraar.gmail.com","threadId":"57133","inReplyTo":"CABPp-BHPZZuGoJO8PXCngBhStBJmDhbyCYOmR+CVZX5wv3O+4w@mail.gmail.com","subject":"Re: [PATCH v4 02/10] log: clean unneeded objects during `log --remerge-diff`","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-02-02T11:17:30Z","receivedAt":"2022-02-02T11:18:29Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Feb 01 2022, Elijah Newren wrote:\n\n> On Tue, Feb 1, 2022 at 1:45 AM Ævar Arnfjörð Bjarmason <avarab@gmail.com> wrote:\n>>\n>> On Fri, Jan 21 2022, Elijah Newren via GitGitGadget wrote:\n>>\n>> > From: Elijah Newren <newren@gmail.com>\n>> > [...]\n>> > @@ -944,7 +945,12 @@ static int do_remerge_diff(struct rev_info *opt,\n>> >       strbuf_release(&parent1_desc);\n>> >       strbuf_release(&parent2_desc);\n>> >       merge_finalize(&o, &res);\n>> > -     /* TODO: clean up the temporary object directory */\n>> > +\n>> > +     /* Clean up the contents of the temporary object directory */\n>> > +     if (opt->remerge_objdir)\n>> > +             tmp_objdir_discard_objects(opt->remerge_objdir);\n>> > +     else\n>> > +             BUG(\"unable to remove temporary object directory\");\n>>\n>> Re the die in 1/10 I don't think this will ever trigger the way this bug\n>> suggests.\n>>\n>> If we didn't manage to remove the directory that'll be signalled with\n>> the return code of tmp_objdir_discard_objects() which you're adding\n>> here, but which doesn't have a meaningful return value.\n>>\n>> So shouldn't it first of all be returning the \"int\" like the\n>> remove_dir_recursively() user in tmp_objdir_destroy_1() makes use of?\n>>\n>> What this bug is really about is:\n>>\n>>     BUG(\"our juggling of opt->remerge_objdir between here and builtin/log.c is screwy\")\n>>\n>> Or something, because if we failed to remove the director(ies) we'll\n>> just ignore that here.\n>\n> Yeah, I think I'm suffering from leftover bits from earlier versions\n> since this patch series has been waiting for 17 months now.  I\n> switched it to\n>\n>     BUG(\"did a remerge diff without remerge_objdir?!?\");\n\nThanks :)\n\n>>\n>> > +void tmp_objdir_discard_objects(struct tmp_objdir *t)\n>> > +{\n>> > +     remove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL);\n>> > +}\n>>\n>> I skimmed remove_dir_recurse() a bit, but didn't test this, does this\n>> remove just the \"de/eadbeef...\" in \"de/eadbeef...\" or also \"de/\",\n>> i.e. do we (and do we want) to keep the fanned-out 256 loose top-level\n>> directories throughout the operation?\n>\n> It will remove everything below t->path, but leave t->path.  As such,\n> it'll nuke any of the 256 loose top-level directories that exist.\n>\n> If someone wants to come along later and measure performance and\n> determine if leaving those 256 loose top-level directories around\n> improves things, I think that's fine, but I'm not going to look at it\n> as part of this series.  I'm more curious about where tmp_objdir\n> creates the temporary directory; when the intent is to migrate the\n> objects into the main directory, it should probably be created on the\n> same filesystem.  When the intent is scratch space, like it is for\n> --remerge-diff, the tmp_objdir should probably be shoved in /dev/shm\n> or something like that.  But again, that's outside of this series.\n> This series already has had a long list of things keeping it from the\n> light of day; there's no need to add frills to it as part of the\n> initial submission.\n\nSorry to add to the frustration. I really didn't mean that as a\nsuggestion for a thing to be addressed, I think this is way past good\nenough. It was just something I found curious, didn't quite know how it\nworked, and thought I'd ask if you knew offhand. Thanks!\n"}]}