Add a new --remerge-diff capability to show & log
PATCH v4 8/10 show, log: include conflict/warning messages in --remerge-diff headers
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Conflicts such as modify/delete, rename/rename, or file/directory are not representable via content conflict markers, and the normal output messages notifying users about these were dropped with --remerge-diff. While we don't want these messages randomly shown before the commit and diff headers, we do want them to still be shown; include them as part of the diff headers instead.
--- log-tree.c | 51 ++++++++++++++ merge-ort.c | 1 + merge-ort.h | 10 +++ t/t4069-remerge-diff.sh | 144 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+)
log-tree.c +51 -0
@@ -19,6 +19,7 @@#include "line-log.h" #include "help.h" #include "range-diff.h" +#include "strmap.h" static struct decoration name_decoration = {"object names" };static int decoration_loaded;
@@ -905,6 +906,52 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)return !opt->loginfo; } +static void setup_additional_headers (struct diff_options *o, +struct strmap *all_headers) +{ +struct hashmap_iter iter; +struct strmap_entry *entry; + +/* +* Make o->additional_path_headers contain the subset of all_headers +* that match o->pathspec. If there aren't any that match o->pathspec, +* then make o->additional_path_headers be NULL. +*/ + +if (!o->pathspec.nr) { + o->additional_path_headers = all_headers; +return ; + } + + o->additional_path_headers =xmalloc (sizeof (struct strmap )); +strmap_init_with_options (o->additional_path_headers,NULL ,0 ); +strmap_for_each_entry (all_headers, &iter, entry) { +if (match_pathspec (the_repository->index, &o->pathspec, + entry->key,strlen (entry->key), +0 /* prefix */ ,NULL /* seen */ , +0 /* is_dir */ )) +strmap_put (o->additional_path_headers, + entry->key, entry->value); + } +if (!strmap_get_size (o->additional_path_headers)) { +strmap_clear (o->additional_path_headers,0 ); +FREE_AND_NULL (o->additional_path_headers); + } +} + +static void cleanup_additional_headers (struct diff_options *o) +{ +if (!o->pathspec.nr) { + o->additional_path_headers =NULL ; +return ; + } +if (!o->additional_path_headers) +return ; + +strmap_clear (o->additional_path_headers,0 ); +FREE_AND_NULL (o->additional_path_headers); +} + static int do_remerge_diff(struct rev_info *opt, struct commit_list *parents, struct object_id *oid,
@@ -922,6 +969,8 @@ static int do_remerge_diff(struct rev_info *opt,/* Setup merge options */ init_merge_options (&o, the_repository); o.show_rename_progress =0 ; + o.record_conflict_msgs_as_headers =1 ; + o.msg_header_prefix ="remerge" ; ctx.abbrev =DEFAULT_ABBREV ;format_commit_message (parent1,"%h (%s)" , &parent1_desc, &ctx);
@@ -938,10 +987,12 @@ static int do_remerge_diff(struct rev_info *opt,merge_incore_recursive (&o, bases, parent1, parent2, &res);/* Show the diff */ +setup_additional_headers (&opt->diffopt, res.path_messages);diff_tree_oid (&res.tree->object.oid, oid,"" , &opt->diffopt);log_tree_diff_flush (opt);/* Cleanup */ +cleanup_additional_headers (&opt->diffopt);strbuf_release (&parent1_desc);strbuf_release (&parent2_desc);merge_finalize (&o, &res);
merge-ort.c +1 -0
@@ -4585,6 +4585,7 @@ redo:trace2_region_leave ("merge" ,"process_entries" , opt->repo);/* Set return values */ + result->path_messages = &opt->priv->output; result->tree =parse_tree_indirect (&working_tree_oid);/* existence of conflicted entries implies unclean */ result->clean &=strmap_empty (&opt->priv->conflicted);
merge-ort.h +10 -0
@@ -5,6 +5,7 @@struct commit ;struct tree ; +struct strmap ;struct merge_result {/*
@@ -23,6 +24,15 @@ struct merge_result {*/ struct tree *tree; +/* +* Special messages and conflict notices for various paths +* +* This is a map of pathnames to strbufs. It contains various +* warning/conflict/notice messages (possibly multiple per path) +* that callers may want to use. +*/ +struct strmap *path_messages; +/* * Additional metadata used by merge_switch_to_result() or future calls * to merge_incore_*(). Includes data needed to update the index (if
t/t4069-remerge-diff.sh +144 -0
154 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v4 9/10 merge-ort: mark conflict/warning messages from inner merges as omittable
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
A recursive merge involves merging the merge bases of the two branches being merged. Such an inner merge can itself generate conflict notices. While such notices may be useful when initially trying to create a merge, they seem to just be noise when investigating merges later with --remerge-diff. (Especially when both sides of the outer merge resolved the conflict the same way leading to no overall conflict.) Remove them.
--- merge-ort.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-)
merge-ort.c +3 -1
@@ -638,7 +638,9 @@ static void path_msg(struct merge_options *opt,struct strbuf tmp =STRBUF_INIT ;if (opt->record_conflict_msgs_as_headers && omittable_hint) -return ;/* Do not record mere hints in tree */ +return ;/* Do not record mere hints in headers */ +if (opt->record_conflict_msgs_as_headers && opt->priv->call_depth) +return ;/* Do not record inner merge issues in headers */ sb =strmap_get (&opt->priv->output, path);if (!sb) { sb =xmalloc (sizeof (*sb));
Signature · 1 line
gitgitgadget
PATCH v4 10/10 diff-merges: avoid history simplifications when diffing merges
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Doing diffs for merges are special; they should typically avoid history simplification. For example, with
git log --diff-merges=first-parent -- path
the default history simplification would remove merge commits from consideration if the file "path" matched the second parent. That is counter to what the user wants when looking for first-parent diffs. Similar comments can be made for --diff-merges=separate (which diffs against both parents) and --diff-merges=remerge (which diffs against a remerge of the merge commit).
However, history simplification still makes sense if not doing diffing merges, and it also makes sense for the combined and dense-combined forms of diffing merges (because both of those are defined to only show a diff when the merge result at the relevant paths differs from *both* parents).
So, for separate, first-parent, and remerge styles of diff-merges, turn off history simplification.
--- diff-merges.c | 2 ++ t/t4069-remerge-diff.sh | 58 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-)
diff-merges.c +2 -0
@@ -24,6 +24,7 @@ static void set_separate(struct rev_info *revs) {suppress (revs); revs->separate_merges =1 ; + revs->simplify_history =0 ; }static void set_first_parent (struct rev_info *revs)
@@ -50,6 +51,7 @@ static void set_remerge_diff(struct rev_info *revs) {suppress (revs); revs->remerge_diff =1 ; + revs->simplify_history =0 ; }static diff_merges_setup_func_t func_by_opt (const char *optarg)
t/t4069-remerge-diff.sh +57 -1
64 lines not on this page · raw message
Signature · 1 line
gitgitgadget
v5 cover Add a new --remerge-diff capability to show & log
In reply to Elijah Newren via GitGitGadgetHere are some patches to add a --remerge-diff capability to show & log, which works by comparing merge commits to an automatic remerge (note that the automatic remerge tree can contain files with conflict markers).
Changes since v4:
* Just a few minor tweaks -- comment on output being subject to change, simpler test skipping check, and improved bug message
Changes since v3:
* Filter conflict headers according to pathspecs
* Instead of always including conflict headers for all diff types, only select them with --diff-filter=U OR whenever the associated diff in question is selected
* New testcases dealing with --diff-filter, pathspecs, and default history simplification
* Switched back from die_errno() to die()
Changes NOT included (mostly because I'm not sure what to add or where):
* Johannes Altimanninger suggested changing the ordering of the new headers relative to other headers. He made a good point, but I also like having the conflict messages next to the text, so I'm conflicted about what's best.
* (Technically not part of this feature, but kind of related.) Months ago, Junio suggested documenting ${GIT_DIR}/AUTO_MERGE better
(https://lore.kernel.org/git/[email protected]/). I looked at the time, but couldn't find a place to put it that made sense to me.
Changes since v2 (of the restarted submission):
* Numerous small improvements suggested by Johannes Altmanninger
* Avoid including conflict messages from inner merges (due to example pointed out by Ævar).
* Added a "remerge" prefix to all the new diff headers (suggested by Junio in a previous round, but I couldn't come up with a good name before. It suddenly hit me that "remerge" is an obvious prefix to use, and even helps explain what the rest of the line is for.)
Changes since v1 (of the restarted submission, which technically was v2):
* Restructured the series, so the first patch introduces the feature -- with a bunch of caveats. Subsequent patches clean up those caveats. This avoids introducing not-yet-used functions, and hopefully makes review easier.
* added testcases
* numerous small improvements suggested by Ævar and Junio
Changes since original submission[1]:
* Rebased on top of the version of ns/tmp-objdir that Neeraj submitted (Neeraj's patches were based on v2.34, but ns/tmp-objdir got applied on an old commit and does not even build because of that).
* Modify ll-merge API to return a status, instead of printing "Cannot merge binary files" on stdout[2] (as suggested by Peff)
* Make conflict messages and other such warnings into diff headers of the subsequent remerge-diff rather than appearing in the diff as file content of some funny looking filenames (as suggested by Peff[3] and Junio[4])
* Sergey ack'ed the diff-merges.c portion of the patches, but that wasn't limited to one patch so not sure where to record that ack.
[1]
https://lore.kernel.org/git/[email protected]/;
GitHub wouldn't let me change the target branch for the PR, so I had to create a new one with the new base and thus the reason for not sending this as v2 even though it is. [2]
https://lore.kernel.org/git/[email protected]/,
https://lore.kernel.org/git/[email protected]/ [3]
https://lore.kernel.org/git/[email protected]/ [4]
https://lore.kernel.org/git/[email protected]/
=== FURTHER BACKGROUND (original cover letter material) ==
Here are some example commits you can try this out on (with git show --remerge-diff $COMMIT):
* git.git conflicted merge: 07601b5b36
* git.git non-conflicted change: bf04590ecd
* linux.git conflicted merge: eab3540562fb
* linux.git non-conflicted change: 223cea6a4f05
Many more can be found by just running git log --merges --remerge-diff in your repository of choice and searching for diffs (most merges tend to be clean and unmodified and thus produce no diff but a search of '^diff' in the log output tends to find the examples nicely).
Some basic high level details about this new option:
* This option is most naturally compared to --cc, though the output seems to be much more understandable to most users than --cc output.
* Since merges are often clean and unmodified, this new option results in an empty diff for most merges.
* This new option shows things like the removal of conflict markers, which hunks users picked from the various conflicted sides to keep or remove, and shows changes made outside of conflict markers (which might reflect changes needed to resolve semantic conflicts or cleanups of e.g. compilation warnings or other additional changes an integrator felt belonged in the merged result).
* This new option does not (currently) work for octopus merges, since merge-ort is specific to two-parent merges[1].
* This option will not work on a read-only or full filesystem[2].
* We discussed this capability at Git Merge 2020, and one of the suggestions was doing a periodic git gc --auto during the operation (due to potential new blobs and trees created during the operation). I found a way to avoid that; see [2].
* This option is faster than you'd probably expect; it handles 33.5 merge commits per second in linux.git on my computer; see below.
In regards to the performance point above, the timing for running the following command:
time git log --min-parents=2 --max-parents=2 $DIFF_FLAG | wc -l
in linux.git (with v5.4 checked out, since my copy of linux is very out of date) is as follows:
DIFF_FLAG=--cc: 71m 31.536s DIFF_FLAG=--remerge-diff: 31m 3.170s
Note that there are 62476 merges in this history. Also, output size is:
DIFF_FLAG=--cc: 2169111 lines DIFF_FLAG=--remerge-diff: 2458020 lines
So roughly the same amount of output as --cc, as you'd expect.
As a side note: git log --remerge-diff, when run in various repositories and allowed to run all the way back to the beginning(s) of history, is a nice stress test of sorts for merge-ort. Especially when users run it for you on their repositories they are working on, whether intentionally or via a bug in a tool triggering that command to be run unexpectedly. Long story short, such a bug in an internal tool existed in December 2020 and this command was run on an internal repository and found a platform-specific bug in merge-ort on some really old merge commit from that repo. I fixed that bug (a STABLE_QSORT thing) while upstreaming all the merge-ort patches in the mean time, but it was nice getting extra testing. Having more folks run this on their repositories might be useful extra testing of the new merge strategy.
Also, I previously mentioned --remerge-diff-only (a flag to show how cherry-picks or reverts differ from an automatic cherry-pick or revert, in addition to showing how merges differ from an automatic merge). This series does not include the patches to introduce that option; I'll submit them later.
Two other things that might be interesting but are not included and which I haven't investigated:
* some mechanism for passing extra merge options through (e.g. -Xignore-space-change)
* a capability to compare the automatic merge to a second automatic merge done with different merge options. (Not sure if this would be of interest to end users, but might be interesting while developing new a --strategy-option, or maybe checking how changing some default in the merge algorithm would affect historical merges in various repositories).
[1] I have nebulous ideas of how an Octopus-centric ORT strategy could be written -- basically, just repeatedly invoking ort and trying to make sure nested conflicts can be differentiated. For now, though, a simple warning is printed that octopus merges are not handled and no diff will be shown. [2] New blobs/trees can be written by the three-way merging step. These are written to a temporary area (via tmp-objdir.c) under the git object store that is cleaned up at the end of the operation, with the new loose objects from the remerge being cleaned up after each individual merge.
Elijah Newren (10):
show, log: provide a --remerge-diff capability
log: clean unneeded objects during log --remerge-diff
ll-merge: make callers responsible for showing warnings
merge-ort: capture and print ll-merge warnings in our preferred
fashion
merge-ort: mark a few more conflict messages as omittable
merge-ort: format messages slightly different for use in headers
diff: add ability to insert additional headers for paths show, log: include conflict/warning messages in --remerge-diff headers
merge-ort: mark conflict/warning messages from inner merges as
omittable
diff-merges: avoid history simplifications when diffing merges
Documentation/diff-options.txt | 14 +- apply.c | 5 +- builtin/checkout.c | 12 +- builtin/log.c | 15 ++ diff-merges.c | 14 ++ diff.c | 124 +++++++++++++- diff.h | 3 +- ll-merge.c | 40 +++-- ll-merge.h | 9 +- log-tree.c | 118 ++++++++++++- merge-blobs.c | 5 +- merge-ort.c | 55 ++++++- merge-ort.h | 10 ++ merge-recursive.c | 9 +- merge-recursive.h | 2 + notes-merge.c | 5 +- rerere.c | 9 +- revision.h | 6 +- t/t4069-remerge-diff.sh | 291 +++++++++++++++++++++++++++++++++ t/t6404-recursive-merge.sh | 9 +- t/t6406-merge-attr.sh | 9 +- tmp-objdir.c | 5 + tmp-objdir.h | 6 + 23 files changed, 727 insertions(+), 48 deletions(-) create mode 100755 t/t4069-remerge-diff.sh
base-commit: 4e44121c2d7bced65e25eb7ec5156290132bec94
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1103%2Fnewren%2Fremerge-diff-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1103/newren/remerge-diff-v5
Pull-Request: https://github.com/gitgitgadget/git/pull/1103
Range-diff vs v4:
1: 0b94724311d ! 1: 0a260125266 show, log: provide a --remerge-diff capability
@@ Documentation/diff-options.txt: ifdef::git-log[]
+ create a temporary tree object -- potentially containing files
+ with conflict markers and such. A diff is then shown between
+ that temporary tree and the actual merge commit.
+++
++The output emitted when this option is used is subject to change, and
++so is its interaction with other options (unless explicitly
++documented).
++
--diff-merges=combined:::
--diff-merges=c:::
@@ t/t4069-remerge-diff.sh (new)
+. ./test-lib.sh
+
+# This test is ort-specific
-+test "${GIT_TEST_MERGE_ALGORITHM:-ort}" = ort || {
++if test "${GIT_TEST_MERGE_ALGORITHM}" != ort
++then
+ skip_all="GIT_TEST_MERGE_ALGORITHM != ort"
+ test_done
-+}
++fi
+
+test_expect_success 'setup basic merges' '
+ test_write_lines 1 2 3 4 5 6 7 8 9 >numbers &&
2: f06de6c1b2f ! 2: ed0d60de24c log: clean unneeded objects during `log --remerge-diff`
@@ log-tree.c: static int do_remerge_diff(struct rev_info *opt,
+ if (opt->remerge_objdir)
+ tmp_objdir_discard_objects(opt->remerge_objdir);
+ else
-+ BUG("unable to remove temporary object directory");
++ BUG("did a remerge diff without remerge_objdir?!?");
return !opt->loginfo;
}
3: 8d6c3d48f0e = 3: ba4de88f2c4 ll-merge: make callers responsible for showing warnings
4: de8e8f88fa4 = 4: d7a1f4e1f9f merge-ort: capture and print ll-merge warnings in our preferred fashion
5: 6b535a4d55a = 5: cbde4e5d372 merge-ort: mark a few more conflict messages as omittable
6: e2441608c63 = 6: d3e4242a5bd merge-ort: format messages slightly different for use in headers
7: 62734beb693 = 7: 4d79da6e20a diff: add ability to insert additional headers for paths
8: 17eccf7e0d6 = 8: ff9c14b0b7c show, log: include conflict/warning messages in --remerge-diff headers
9: b3e7656cfc6 = 9: aa63860cd0f merge-ort: mark conflict/warning messages from inner merges as omittable
10: ea5df61cf35 = 10: 59d12f213b2 diff-merges: avoid history simplifications when diffing mergesSignature · 1 line
gitgitgadget
PATCH v5 1/10 show, log: provide a --remerge-diff capability
From: Elijah Newren <[email protected]>
When this option is specified, we remerge all (two parent) merge commits and diff the actual merge commit to the automatically created version, in order to show how users removed conflict markers, resolved the different conflict versions, and potentially added new changes outside of conflict regions in order to resolve semantic merge problems (or, possibly, just to hide other random changes).
This capability works by creating a temporary object directory and marking it as the primary object store. This makes it so that any blobs or trees created during the automatic merge are easily removable afterwards by just deleting all objects from the temporary object directory.
There are a few ways that this implementation is suboptimal:
* log --remerge-diff becomes slow, because the temporary object directory can fill with many loose objects while running
* the log output can be muddied with misplaced "warning: cannot merge binary files" messages, since ll-merge.c unconditionally writes those messages to stderr while running instead of allowing callers to manage them.
* important conflict and warning messages are simply dropped; thus for conflicts like modify/delete or rename/rename or file/directory which are not representable with content conflict markers, there may be no way for a user of --remerge-diff to know that there had been a conflict which was resolved (and which possibly motivated other changes in the merge commit).
* when fixing the previous issue, note that some unimportant conflict and warning messages might start being included. We should instead make sure these remain dropped.
Subsequent commits will address these issues.
--- Documentation/diff-options.txt | 14 +++++- builtin/log.c | 14 ++++++ diff-merges.c | 12 +++++ log-tree.c | 59 ++++++++++++++++++++++ revision.h | 3 +- t/t4069-remerge-diff.sh | 91 ++++++++++++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100755 t/t4069-remerge-diff.sh
Documentation/diff-options.txt +13 -1
@@ -34,7 +34,7 @@ endif::git-diff[] endif::git-format-patch[] ifdef::git-log[] ---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc):: +--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r):: --no-diff-merges:: Specify diff format to be used for merge commits. Default is {diff-merges-default} unless `--first-parent` is in use, in which case
@@ -64,6 +64,18 @@ ifdef::git-log[] each of the parents. Separate log entry and diff is generated for each parent. + +--diff-merges=remerge::: +--diff-merges=r::: +--remerge-diff::: + With this option, two-parent merge commits are remerged to + create a temporary tree object -- potentially containing files + with conflict markers and such. A diff is then shown between + that temporary tree and the actual merge commit. ++ +The output emitted when this option is used is subject to change, and +so is its interaction with other options (unless explicitly +documented). ++ --diff-merges=combined::: --diff-merges=c::: -c:::
builtin/log.c +14 -0
@@ -35,6 +35,7 @@#include "repository.h" #include "commit-reach.h" #include "range-diff.h" +#include "tmp-objdir.h" #define MAIL_DEFAULT_WRAP 72#define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100
@@ -406,6 +407,14 @@ static int cmd_log_walk(struct rev_info *rev)struct commit *commit;int saved_nrl =0 ;int saved_dcctc =0 ; +struct tmp_objdir *remerge_objdir =NULL ; + +if (rev->remerge_diff) { + remerge_objdir =tmp_objdir_create ("remerge-diff" ); +if (!remerge_objdir) +die (_ ("unable to create temporary object directory" )); +tmp_objdir_replace_primary_odb (remerge_objdir,1 ); + }if (rev->early_output)setup_early_output ();
@@ -449,6 +458,9 @@ static int cmd_log_walk(struct rev_info *rev) rev->diffopt.no_free =0 ;diff_free (&rev->diffopt); +if (rev->remerge_diff) +tmp_objdir_destroy (remerge_objdir); +if (rev->diffopt.output_format &DIFF_FORMAT_CHECKDIFF && rev->diffopt.flags.check_failed) {return 02 ;
@@ -1943,6 +1955,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)die (_ ("--name-status does not make sense" ));if (rev.diffopt.output_format &DIFF_FORMAT_CHECKDIFF )die (_ ("--check does not make sense" )); +if (rev.remerge_diff) +die (_ ("--remerge-diff does not make sense" )); if (!use_patch_format && (!rev.diffopt.output_format ||
diff-merges.c +12 -0
@@ -17,6 +17,7 @@ static void suppress(struct rev_info *revs) revs->combined_all_paths =0 ; revs->merges_imply_patch =0 ; revs->merges_need_diff =0 ; + revs->remerge_diff =0 ; }static void set_separate (struct rev_info *revs)
@@ -45,6 +46,12 @@ static void set_dense_combined(struct rev_info *revs) revs->dense_combined_merges =1 ; } +static void set_remerge_diff (struct rev_info *revs) +{ +suppress (revs); + revs->remerge_diff =1 ; +} +static diff_merges_setup_func_t func_by_opt (const char *optarg) {if (!strcmp (optarg,"off" ) || !strcmp (optarg,"none" ))
@@ -57,6 +64,8 @@ static diff_merges_setup_func_t func_by_opt(const char *optarg)return set_combined; else if (!strcmp(optarg, "cc") || !strcmp(optarg, "dense-combined"))return set_dense_combined; + else if (!strcmp(optarg, "r") || !strcmp(optarg, "remerge")) +return set_remerge_diff; else if (!strcmp(optarg, "m") || !strcmp(optarg, "on"))return set_to_default;return NULL ;
@@ -110,6 +119,9 @@ int diff_merges_parse_opts(struct rev_info *revs, const char **argv) }else if (!strcmp (arg,"--cc" )) {set_dense_combined (revs); revs->merges_imply_patch =1 ; + }else if (!strcmp (arg,"--remerge-diff" )) { +set_remerge_diff (revs); + revs->merges_imply_patch =1 ; }else if (!strcmp (arg,"--no-diff-merges" )) {suppress (revs); }else if (!strcmp (arg,"--combined-all-paths" )) {
log-tree.c +59 -0
81 lines not on this page · raw message
revision.h +2 -1
@@ -195,7 +195,8 @@ struct rev_info { combine_merges:1, combined_all_paths:1, dense_combined_merges:1, - first_parent_merges:1; + first_parent_merges:1, + remerge_diff:1;/* Format info */ int show_notes;
t/t4069-remerge-diff.sh new file +91 -0
91 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v5 2/10 log: clean unneeded objects during `log --remerge-diff`
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
The --remerge-diff option will need to create new blobs and trees representing the "automatic merge" state. If one is traversing a long project history, one can easily get hundreds of thousands of loose objects generated during log --remerge-diff. However, none of those loose objects are needed after we have completed our diff operation; they can be summarily deleted.
Add a new helper function to tmp_objdir to discard all the contained objects, and call it after each merge is handled.
--- builtin/log.c | 13 +++++++------ log-tree.c | 8 +++++++- revision.h | 3 +++ tmp-objdir.c | 5 +++++ tmp-objdir.h | 6 ++++++ 5 files changed, 28 insertions(+), 7 deletions(-)
builtin/log.c +7 -6
@@ -407,13 +407,12 @@ static int cmd_log_walk(struct rev_info *rev)struct commit *commit;int saved_nrl =0 ;int saved_dcctc =0 ; -struct tmp_objdir *remerge_objdir =NULL ;if (rev->remerge_diff) { - remerge_objdir =tmp_objdir_create ("remerge-diff" ); -if (!remerge_objdir) + rev->remerge_objdir =tmp_objdir_create ("remerge-diff" ); +if (!rev->remerge_objdir)die (_ ("unable to create temporary object directory" )); -tmp_objdir_replace_primary_odb (remerge_objdir,1 ); +tmp_objdir_replace_primary_odb (rev->remerge_objdir,1 ); }if (rev->early_output)
@@ -458,8 +457,10 @@ static int cmd_log_walk(struct rev_info *rev) rev->diffopt.no_free =0 ;diff_free (&rev->diffopt); -if (rev->remerge_diff) -tmp_objdir_destroy (remerge_objdir); +if (rev->remerge_diff) { +tmp_objdir_destroy (rev->remerge_objdir); + rev->remerge_objdir =NULL ; + }if (rev->diffopt.output_format &DIFF_FORMAT_CHECKDIFF && rev->diffopt.flags.check_failed) {
log-tree.c +7 -1
@@ -4,6 +4,7 @@#include "diff.h" #include "object-store.h" #include "repository.h" +#include "tmp-objdir.h" #include "commit.h" #include "tag.h" #include "graph.h"
@@ -944,7 +945,12 @@ static int do_remerge_diff(struct rev_info *opt,strbuf_release (&parent1_desc);strbuf_release (&parent2_desc);merge_finalize (&o, &res); -/* TODO: clean up the temporary object directory */ + +/* Clean up the contents of the temporary object directory */ +if (opt->remerge_objdir) +tmp_objdir_discard_objects (opt->remerge_objdir); +else +BUG ("did a remerge diff without remerge_objdir?!?" );return !opt->loginfo; }
revision.h +3 -0
@@ -318,6 +318,9 @@ struct rev_info {/* misc. flags related to '--no-kept-objects' */ unsigned keep_pack_cache_flags; + +/* Location where temporary objects for remerge-diff are written. */ +struct tmp_objdir *remerge_objdir; };int ref_excluded (struct string_list *,const char *path);
tmp-objdir.c +5 -0
@@ -79,6 +79,11 @@ static void remove_tmp_objdir_on_signal(int signo)raise (signo); } +void tmp_objdir_discard_objects (struct tmp_objdir *t) +{ +remove_dir_recursively (&t->path,REMOVE_DIR_KEEP_TOPLEVEL ); +} +/* * These env_* functions are for setting up the child environment; the * "replace" variant overrides the value of any existing variable with that
tmp-objdir.h +6 -0
@@ -46,6 +46,12 @@ int tmp_objdir_migrate(struct tmp_objdir *);*/ int tmp_objdir_destroy (struct tmp_objdir *); +/* +* Remove all objects from the temporary object directory, while leaving it +* around so more objects can be added. +*/ +void tmp_objdir_discard_objects (struct tmp_objdir *); +/* * Add the temporary object directory as an alternate object store in the * current process.
Signature · 1 line
gitgitgadget
PATCH v5 3/10 ll-merge: make callers responsible for showing warnings
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Since some callers may want to send warning messages to somewhere other than stdout/stderr, stop printing "warning: Cannot merge binary files" from ll-merge and instead modify the return status of ll_merge() to indicate when a merge of binary files has occurred. Message printing probably does not belong in a "low-level merge" anyway.
This commit continues printing the message as-is, just from the callers instead of within ll_merge(). Future changes will start handling the message differently in the merge-ort codepath.
There was one special case here: the callers in rerere.c do NOT check for and print such a message; since those code paths explicitly skip over binary files, there is no reason to check for a return status of LL_MERGE_BINARY_CONFLICT or print the related message.
Note that my methodology included first modifying ll_merge() to return a struct, so that the compiler would catch all the callers for me and ensure I had modified all of them. After modifying all of them, I then changed the struct to an enum.
--- apply.c | 5 ++++- builtin/checkout.c | 12 ++++++++---- ll-merge.c | 40 ++++++++++++++++++++++------------------ ll-merge.h | 9 ++++++++- merge-blobs.c | 5 ++++- merge-ort.c | 5 ++++- merge-recursive.c | 5 ++++- notes-merge.c | 5 ++++- rerere.c | 9 +++++---- 9 files changed, 63 insertions(+), 32 deletions(-)
apply.c +4 -1
@@ -3492,7 +3492,7 @@ static int three_way_merge(struct apply_state *state, {mmfile_t base_file, our_file, their_file;mmbuffer_t result = {NULL }; -int status; +enum ll_merge_result status;/* resolve trivial cases first */ if (oideq (base, ours))
@@ -3509,6 +3509,9 @@ static int three_way_merge(struct apply_state *state, &their_file,"theirs" , state->repo->index, NULL); +if (status ==LL_MERGE_BINARY_CONFLICT ) +warning ("Cannot merge binary files: %s (%s vs. %s)" , + path,"ours" ,"theirs" );free (base_file.ptr);free (our_file.ptr);free (their_file.ptr);
builtin/checkout.c +8 -4
@@ -237,6 +237,7 @@ static int checkout_merged(int pos, const struct checkout *state,struct cache_entry *ce = active_cache[pos];const char *path = ce->name;mmfile_t ancestor, ours, theirs; +enum ll_merge_result merge_status;int status;struct object_id oid;mmbuffer_t result_buf;
@@ -267,13 +268,16 @@ static int checkout_merged(int pos, const struct checkout *state,memset (&ll_opts,0 ,sizeof (ll_opts));git_config_get_bool ("merge.renormalize" , &renormalize); ll_opts.renormalize = renormalize; - status =ll_merge (&result_buf, path, &ancestor,"base" , - &ours,"ours" , &theirs,"theirs" , - state->istate, &ll_opts); + merge_status =ll_merge (&result_buf, path, &ancestor,"base" , + &ours,"ours" , &theirs,"theirs" , + state->istate, &ll_opts);free (ancestor.ptr);free (ours.ptr);free (theirs.ptr); -if (status <0 || !result_buf.ptr) { +if (merge_status ==LL_MERGE_BINARY_CONFLICT ) +warning ("Cannot merge binary files: %s (%s vs. %s)" , + path,"ours" ,"theirs" ); +if (merge_status <0 || !result_buf.ptr) {free (result_buf.ptr);return error (_ ("path '%s': cannot merge" ), path); }
ll-merge.c +22 -18
@@ -14,7 +14,7 @@struct ll_merge_driver ; -typedef int (*ll_merge_fn)(const struct ll_merge_driver *, +typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name,
@@ -49,7 +49,7 @@ void reset_merge_attributes(void)/* * Built-in low-levels */ -static int ll_binary_merge(const struct ll_merge_driver *drv_unused, +static enum ll_merge_result ll_binary_merge(const struct ll_merge_driver *drv_unused, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name,
@@ -58,6 +58,7 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused, const struct ll_merge_options *opts, int marker_size) { +enum ll_merge_result ret;mmfile_t *stolen;assert (opts);
@@ -68,16 +69,19 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,*/ if (opts->virtual_ancestor) { stolen = orig; + ret =LL_MERGE_OK ; }else {switch (opts->variant) {default : -warning ("Cannot merge binary files: %s (%s vs. %s)" , - path, name1, name2); -/* fallthru */ + ret =LL_MERGE_BINARY_CONFLICT ; + stolen = src1; +break ;case XDL_MERGE_FAVOR_OURS : + ret =LL_MERGE_OK ; stolen = src1;break ;case XDL_MERGE_FAVOR_THEIRS : + ret =LL_MERGE_OK ; stolen = src2;break ; }
@@ -87,16 +91,10 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused, result->size = stolen->size; stolen->ptr =NULL ; -/* -* With -Xtheirs or -Xours, we have cleanly merged; -* otherwise we got a conflict. -*/ -return opts->variant ==XDL_MERGE_FAVOR_OURS || - opts->variant ==XDL_MERGE_FAVOR_THEIRS ? -0 :1 ; +return ret; } -static int ll_xdl_merge(const struct ll_merge_driver *drv_unused, +static enum ll_merge_result ll_xdl_merge(const struct ll_merge_driver *drv_unused, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name,
@@ -105,7 +103,9 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused, const struct ll_merge_options *opts, int marker_size) { +enum ll_merge_result ret;xmparam_t xmp; +int status;assert (opts); if (orig->size > MAX_XDIFF_SIZE ||
@@ -133,10 +133,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused, xmp.ancestor = orig_name; xmp.file1 = name1; xmp.file2 = name2; -return xdl_merge (orig, src1, src2, &xmp, result); + status =xdl_merge (orig, src1, src2, &xmp, result); + ret = (status >0 ) ?LL_MERGE_CONFLICT : status; +return ret; } -static int ll_union_merge(const struct ll_merge_driver *drv_unused, +static enum ll_merge_result ll_union_merge(const struct ll_merge_driver *drv_unused, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name,
@@ -178,7 +180,7 @@ static void create_temp(mmfile_t *src, char *path, size_t len)/* * User defined low-level merge driver support. */ -static int ll_ext_merge(const struct ll_merge_driver *fn, +static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name,
@@ -194,6 +196,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,const char *args[] = {NULL ,NULL };int status, fd, i;struct stat st; +enum ll_merge_result ret;assert (opts);sq_quote_buf (&path_sq, path);
@@ -236,7 +239,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,unlink_or_warn (temp[i]);strbuf_release (&cmd);strbuf_release (&path_sq); -return status; + ret = (status >0 ) ?LL_MERGE_CONFLICT : status; +return ret; }/*
@@ -362,7 +366,7 @@ static void normalize_file(mmfile_t *mm, const char *path, struct index_state *i } } -int ll_merge(mmbuffer_t *result_buf, +enum ll_merge_result ll_merge(mmbuffer_t *result_buf, const char *path, mmfile_t *ancestor, const char *ancestor_label, mmfile_t *ours, const char *our_label,
ll-merge.h +8 -1
@@ -82,13 +82,20 @@ struct ll_merge_options {long xdl_opts; }; +enum ll_merge_result { +LL_MERGE_ERROR =-1 , +LL_MERGE_OK =0 , +LL_MERGE_CONFLICT , +LL_MERGE_BINARY_CONFLICT , +}; +/** * Perform a three-way single-file merge in core. This is a thin wrapper * around `xdl_merge` that takes the path and any merge backend specified in * `.gitattributes` or `.git/info/attributes` into account. * Returns 0 for a clean merge. */ -int ll_merge(mmbuffer_t *result_buf, +enum ll_merge_result ll_merge(mmbuffer_t *result_buf, const char *path, mmfile_t *ancestor, const char *ancestor_label, mmfile_t *ours, const char *our_label,
merge-blobs.c +4 -1
@@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate,mmfile_t *their, unsigned long *size) { - int merge_status; + enum ll_merge_result merge_status;mmbuffer_t res;/*
@@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate, istate, NULL);if (merge_status <0 )return NULL ; +if (merge_status ==LL_MERGE_BINARY_CONFLICT ) +warning ("Cannot merge binary files: %s (%s vs. %s)" , + path,".our" ,".their" ); *size = res.size;return res.ptr;
merge-ort.c +4 -1
@@ -1743,7 +1743,7 @@ static int merge_3way(struct merge_options *opt,mmfile_t orig, src1, src2;struct ll_merge_options ll_opts = {0 };char *base, *name1, *name2; -int merge_status; +enum ll_merge_result merge_status;if (!opt->priv->attr_index.initialized)initialize_attr_index (opt);
@@ -1787,6 +1787,9 @@ static int merge_3way(struct merge_options *opt, merge_status =ll_merge (result_buf, path, &orig, base, &src1, name1, &src2, name2, &opt->priv->attr_index, &ll_opts); +if (merge_status ==LL_MERGE_BINARY_CONFLICT ) +warning ("Cannot merge binary files: %s (%s vs. %s)" , + path, name1, name2);free (base);free (name1);
merge-recursive.c +4 -1
@@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt,mmfile_t orig, src1, src2;struct ll_merge_options ll_opts = {0 };char *base, *name1, *name2; -int merge_status; +enum ll_merge_result merge_status; ll_opts.renormalize = opt->renormalize; ll_opts.extra_marker_size = extra_marker_size;
@@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt, merge_status =ll_merge (result_buf, a->path, &orig, base, &src1, name1, &src2, name2, opt->repo->index, &ll_opts); +if (merge_status ==LL_MERGE_BINARY_CONFLICT ) +warning ("Cannot merge binary files: %s (%s vs. %s)" , + a->path, name1, name2);free (base);free (name1);
notes-merge.c +4 -1
@@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o, {mmbuffer_t result_buf;mmfile_t base, local, remote; -int status; +enum ll_merge_result status;read_mmblob (&base, &p->base);read_mmblob (&local, &p->local);
@@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o,free (local.ptr);free (remote.ptr); +if (status ==LL_MERGE_BINARY_CONFLICT ) +warning ("Cannot merge binary files: %s (%s vs. %s)" , +oid_to_hex (&p->obj), o->local_ref, o->remote_ref);if ((status <0 ) || !result_buf.ptr)die ("Failed to execute internal merge" );
rerere.c +5 -4
@@ -609,19 +609,20 @@ static int try_merge(struct index_state *istate, const struct rerere_id *id, const char *path, mmfile_t *cur, mmbuffer_t *result) { - int ret; + enum ll_merge_result ret;mmfile_t base = {NULL ,0 }, other = {NULL ,0 };if (read_mmfile (&base,rerere_path (id,"preimage" )) || -read_mmfile (&other,rerere_path (id,"postimage" ))) - ret =1 ; -else +read_mmfile (&other,rerere_path (id,"postimage" ))) { + ret =LL_MERGE_CONFLICT ; + }else {/* * A three-way merge. Note that this honors user-customizable * low-level merge driver settings. */ ret =ll_merge (result, path, &base,NULL , cur,"" , &other,"" , istate,NULL ); + }free (base.ptr);free (other.ptr);
Signature · 1 line
gitgitgadget
PATCH v5 4/10 merge-ort: capture and print ll-merge warnings in our preferred fashion
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Instead of immediately printing ll-merge warnings to stderr, we save them in our output strbuf. Besides allowing us to move these warnings to a special file for --remerge-diff, this has two other benefits for regular merges done by merge-ort:
* The deferral of messages ensures we can print all messages about any given path together (merge-recursive was known to sometimes intersperse messages about other paths, particularly when renames were involved).
* The deferral of messages means we can avoid printing spurious conflict messages when we just end up aborting due to local user modifications in the way. (In contrast to merge-recursive.c which prematurely checks for local modifications in the way via unpack_trees() and gets the check wrong both in terms of false positives and false negatives relative to renames, merge-ort does not perform the local modifications in the way check until the checkout() step after the full merge has been computed.)
--- merge-ort.c | 5 +++-- t/t6404-recursive-merge.sh | 9 +++++++-- t/t6406-merge-attr.sh | 9 +++++++-- 3 files changed, 17 insertions(+), 6 deletions(-)
merge-ort.c +3 -2
@@ -1788,8 +1788,9 @@ static int merge_3way(struct merge_options *opt, &src1, name1, &src2, name2, &opt->priv->attr_index, &ll_opts);if (merge_status ==LL_MERGE_BINARY_CONFLICT ) -warning ("Cannot merge binary files: %s (%s vs. %s)" , - path, name1, name2); +path_msg (opt, path,0 , +"warning: Cannot merge binary files: %s (%s vs. %s)" , + path, name1, name2);free (base);free (name1);
t/t6404-recursive-merge.sh +7 -2
@@ -108,8 +108,13 @@ test_expect_success 'refuse to merge binary files' ' printf "\0\0" >binary-file && git add binary-file && git commit -m binary2 && - test_must_fail git merge F >merge.out 2>merge.err && - grep "Cannot merge binary files: binary-file (HEAD vs. F)" merge.err + if test "$GIT_TEST_MERGE_ALGORITHM" = ort + then + test_must_fail git merge F >merge_output + else + test_must_fail git merge F 2>merge_output + fi && + grep "Cannot merge binary files: binary-file (HEAD vs. F)" merge_output ' test_expect_success 'mark rename/delete as unmerged' '
t/t6406-merge-attr.sh +7 -2
@@ -221,8 +221,13 @@ test_expect_success 'binary files with union attribute' ' printf "two\0" >bin.txt && git commit -am two && - test_must_fail git merge bin-main 2>stderr && - grep -i "warning.*cannot merge.*HEAD vs. bin-main" stderr + if test "$GIT_TEST_MERGE_ALGORITHM" = ort + then + test_must_fail git merge bin-main >output + else + test_must_fail git merge bin-main 2>output + fi && + grep -i "warning.*cannot merge.*HEAD vs. bin-main" output ' test_done
Signature · 1 line
gitgitgadget
PATCH v5 5/10 merge-ort: mark a few more conflict messages as omittable
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
path_msg() has the ability to mark messages as omittable, designed for remerge-diff where we'll instead be showing conflict messages as diff headers for a subsequent diff. While all these messages are very useful when trying to create a merge initially, early use with the --remerge-diff feature (the only user of this omittable conflict message capability), suggests that the particular messages marked in this commit are just noise when trying to see what changes users made to create a merge commit. Mark them as omittable.
Note that there were already a few messages marked as omittable in merge-ort when doing a remerge-diff, because the development of --remerge-diff preceded the upstreaming of merge-ort and I was trying to ensure merge-ort could handle all the necessary requirements. See commit c5a6f65527 ("merge-ort: add modify/delete handling and delayed output processing", 2020-12-03) for the initial details. For some examples of already-marked-as-omittable messages, see either "Auto-merging <path>" or some of the submodule update hints. This commit just adds two more messages that should also be omittable.
--- merge-ort.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
merge-ort.c +2 -2
@@ -2420,7 +2420,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,*/ ci->path_conflict =1 ;if (pair->status =='A' ) - path_msg(opt, new_path, 0, + path_msg(opt, new_path, 1, _("CONFLICT (file location): %s added in %s " "inside a directory that was renamed in %s, " "suggesting it should perhaps be moved to "
@@ -2428,7 +2428,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt, old_path, branch_with_new_path, branch_with_dir_rename, new_path); else - path_msg(opt, new_path, 0, + path_msg(opt, new_path, 1, _("CONFLICT (file location): %s renamed to %s ""in %s, inside a directory that was renamed " "in %s, suggesting it should perhaps be "
Signature · 1 line
gitgitgadget
PATCH v5 6/10 merge-ort: format messages slightly different for use in headers
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
When users run
git show --remerge-diff $MERGE_COMMIT
or
git log -p --remerge-diff ...
stdout is not an appropriate location to dump conflict messages, but we do want to provide them to users. We will include them in the diff headers instead...but for that to work, we need for any multiline messages to replace newlines with both a newline and a space. Add a new flag to signal when we want these messages modified in such a fashion, and use it in path_msg() to modify these messages this way. Also, allow a special prefix to be specified for these headers.
--- merge-ort.c | 42 ++++++++++++++++++++++++++++++++++++++++-- merge-recursive.c | 4 ++++ merge-recursive.h | 2 ++ 3 files changed, 46 insertions(+), 2 deletions(-)
merge-ort.c +40 -2
@@ -634,17 +634,49 @@ static void path_msg(struct merge_options *opt, const char *fmt, ...) {va_list ap; -struct strbuf *sb =strmap_get (&opt->priv->output, path); +struct strbuf *sb, *dest; +struct strbuf tmp =STRBUF_INIT ; + +if (opt->record_conflict_msgs_as_headers && omittable_hint) +return ;/* Do not record mere hints in tree */ + sb =strmap_get (&opt->priv->output, path);if (!sb) { sb =xmalloc (sizeof (*sb));strbuf_init (sb,0 );strmap_put (&opt->priv->output, path, sb); } + dest = (opt->record_conflict_msgs_as_headers ? &tmp : sb); +va_start (ap, fmt); -strbuf_vaddf (sb, fmt, ap); +strbuf_vaddf (dest, fmt, ap);va_end (ap); +if (opt->record_conflict_msgs_as_headers) { +int i_sb =0 , i_tmp =0 ; + +/* Start with the specified prefix */ +if (opt->msg_header_prefix) +strbuf_addf (sb,"%s " , opt->msg_header_prefix); + +/* Copy tmp to sb, adding spaces after newlines */ +strbuf_grow (sb, sb->len +2 *tmp.len);/* more than sufficient */ +for (; i_tmp < tmp.len; i_tmp++, i_sb++) { +/* Copy next character from tmp to sb */ + sb->buf[sb->len + i_sb] = tmp.buf[i_tmp]; + +/* If we copied a newline, add a space */ +if (tmp.buf[i_tmp] =='\n' ) + sb->buf[++i_sb] =' ' ; + } +/* Update length and ensure it's NUL-terminated */ + sb->len += i_sb; + sb->buf[sb->len] ='\0' ; + +strbuf_release (&tmp); + } + +/* Add final newline character to sb */ strbuf_addch (sb,'\n' ); }
@@ -4246,6 +4278,9 @@ void merge_switch_to_result(struct merge_options *opt,struct string_list olist =STRING_LIST_INIT_NODUP ;int i; +if (opt->record_conflict_msgs_as_headers) +BUG ("Either display conflict messages or record them as headers, not both" ); +trace2_region_enter ("merge" ,"display messages" , opt->repo);/* Hack to pre-allocate olist to the desired size */
@@ -4347,6 +4382,9 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)assert (opt->recursive_variant >=MERGE_VARIANT_NORMAL && opt->recursive_variant <=MERGE_VARIANT_THEIRS ); +if (opt->msg_header_prefix) +assert (opt->record_conflict_msgs_as_headers); +/* * detect_renames, verbosity, buffer_output, and obuf are ignored * fields that were used by "recursive" rather than "ort" -- but
merge-recursive.c +4 -0
@@ -3714,6 +3714,10 @@ static int merge_start(struct merge_options *opt, struct tree *head)assert (opt->priv ==NULL ); +/* Not supported; option specific to merge-ort */ +assert (!opt->record_conflict_msgs_as_headers); +assert (!opt->msg_header_prefix); +/* Sanity check on repo state; index must match head */ if (repo_index_has_changes (opt->repo, head, &sb)) { err(opt, _("Your local changes to the following files would be overwritten by merge:\n %s"),
merge-recursive.h +2 -0
@@ -46,6 +46,8 @@ struct merge_options {/* miscellaneous control options */ const char *subtree_shift; unsigned renormalize : 1; + unsigned record_conflict_msgs_as_headers : 1; +const char *msg_header_prefix;/* internal fields used by the implementation */ struct merge_options_internal *priv;
Signature · 1 line
gitgitgadget
PATCH v5 7/10 diff: add ability to insert additional headers for paths
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
When additional headers are provided, we need to
* add diff_filepairs to diff_queued_diff for each paths in the additional headers map which, unless that path is part of another diff_filepair already found in diff_queued_diff
* format the headers (colorization, line_prefix for --graph)
* make sure the various codepaths that attempt to return early if there are "no changes" take into account the headers that need to be shown.
--- diff.c | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++-- diff.h | 3 +- log-tree.c | 2 +- 3 files changed, 123 insertions(+), 6 deletions(-)
diff.c +120 -4
@@ -27,6 +27,7 @@#include "help.h" #include "promisor-remote.h" #include "dir.h" +#include "strmap.h" #ifdef NO_FAST_WORKING_DIRECTORY #define FAST_WORKING_DIRECTORY 0
@@ -3406,6 +3407,31 @@ struct userdiff_driver *get_textconv(struct repository *r,return userdiff_get_textconv (r, one->driver); } +static struct strbuf *additional_headers (struct diff_options *o, +const char *path) +{ +if (!o->additional_path_headers) +return NULL ; +return strmap_get (o->additional_path_headers, path); +} + +static void add_formatted_headers (struct strbuf *msg, +struct strbuf *more_headers, +const char *line_prefix, +const char *meta, +const char *reset) +{ +char *next, *newline; + +for (next = more_headers->buf; *next; next = newline) { + newline =strchrnul (next,'\n' ); +strbuf_addf (msg,"%s%s%.*s%s\n" , line_prefix, meta, + (int )(newline - next), next, reset); +if (*newline) + newline++; + } +} + static void builtin_diff(const char *name_a, const char *name_b, struct diff_filespec *one,
@@ -3464,6 +3490,17 @@ static void builtin_diff(const char *name_a, b_two =quote_two (b_prefix, name_b + (*name_b =='/' )); lbl[0 ] =DIFF_FILE_VALID (one) ? a_one :"/dev/null" ; lbl[1 ] =DIFF_FILE_VALID (two) ? b_two :"/dev/null" ; +if (!DIFF_FILE_VALID (one) && !DIFF_FILE_VALID (two)) { +/* +* We should only reach this point for pairs from +* create_filepairs_for_header_only_notifications(). For +* these, we should avoid the "/dev/null" special casing +* above, meaning we avoid showing such pairs as either +* "new file" or "deleted file" below. +*/ + lbl[0 ] = a_one; + lbl[1 ] = b_two; + }strbuf_addf (&header,"%s%sdiff --git %s %s%s\n" , line_prefix, meta, a_one, b_two, reset);if (lbl[0 ][0 ] =='/' ) {/* /dev/null */
@@ -4328,6 +4365,7 @@ static void fill_metainfo(struct strbuf *msg,const char *set =diff_get_color (use_color,DIFF_METAINFO );const char *reset =diff_get_color (use_color,DIFF_RESET );const char *line_prefix =diff_line_prefix (o); +struct strbuf *more_headers =NULL ; *must_show_header =1 ;strbuf_init (msg,PATH_MAX *2 +300 );
@@ -4364,6 +4402,11 @@ static void fill_metainfo(struct strbuf *msg,default : *must_show_header =0 ; } +if ((more_headers =additional_headers (o, name))) { +add_formatted_headers (msg, more_headers, + line_prefix, set, reset); + *must_show_header =1 ; + }if (one && two && !oideq (&one->oid, &two->oid)) {const unsigned hexsz = the_hash_algo->hexsz;int abbrev = o->abbrev ? o->abbrev :DEFAULT_ABBREV ;
@@ -5852,12 +5895,27 @@ int diff_unmodified_pair(struct diff_filepair *p)static void diff_flush_patch (struct diff_filepair *p,struct diff_options *o) { -if (diff_unmodified_pair (p)) +int include_conflict_headers = + (additional_headers (o, p->one->path) && + (!o->filter ||filter_bit_tst (DIFF_STATUS_UNMERGED , o))); + +/* +* Check if we can return early without showing a diff. Note that +* diff_filepair only stores {oid, path, mode, is_valid} +* information for each path, and thus diff_unmodified_pair() only +* considers those bits of info. However, we do not want pairs +* created by create_filepairs_for_header_only_notifications() +* (which always look like unmodified pairs) to be ignored, so +* return early if both p is unmodified AND we don't want to +* include_conflict_headers. +*/ +if (diff_unmodified_pair (p) && !include_conflict_headers)return ; +/* Actually, we can also return early to avoid showing tree diffs */ if ((DIFF_FILE_VALID (p->one) &&S_ISDIR (p->one->mode)) || (DIFF_FILE_VALID (p->two) &&S_ISDIR (p->two->mode))) -return ;/* no tree diffs in patch format */ +return ;run_diff (p, o); }
@@ -5888,10 +5946,17 @@ static void diff_flush_checkdiff(struct diff_filepair *p,run_checkdiff (p, o); } -int diff_queue_is_empty (void ) +int diff_queue_is_empty (struct diff_options *o) {struct diff_queue_struct *q = &diff_queued_diff;int i; +int include_conflict_headers = + (o->additional_path_headers && + (!o->filter ||filter_bit_tst (DIFF_STATUS_UNMERGED , o))); + +if (include_conflict_headers) +return 0 ; +for (i =0 ; i < q->nr; i++)if (!diff_unmodified_pair (q->queue[i]))return 0 ;
@@ -6325,6 +6390,54 @@ void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)warning (_ (rename_limit_advice), varname, needed); } +static void create_filepairs_for_header_only_notifications (struct diff_options *o) +{ +struct strset present; +struct diff_queue_struct *q = &diff_queued_diff; +struct hashmap_iter iter; +struct strmap_entry *e; +int i; + +strset_init_with_options (&present,/*pool*/ NULL ,/*strdup*/ 0 ); + +/* +* Find out which paths exist in diff_queued_diff, preferring +* one->path for any pair that has multiple paths. +*/ +for (i =0 ; i < q->nr; i++) { +struct diff_filepair *p = q->queue[i]; +char *path = p->one->path ? p->one->path : p->two->path; + +if (strmap_contains (o->additional_path_headers, path)) +strset_add (&present, path); + } + +/* +* Loop over paths in additional_path_headers; for each NOT already +* in diff_queued_diff, create a synthetic filepair and insert that +* into diff_queued_diff. +*/ +strmap_for_each_entry (o->additional_path_headers, &iter, e) { +if (!strset_contains (&present, e->key)) { +struct diff_filespec *one, *two; +struct diff_filepair *p; + + one =alloc_filespec (e->key); + two =alloc_filespec (e->key); +fill_filespec (one,null_oid (),0 ,0 ); +fill_filespec (two,null_oid (),0 ,0 ); + p =diff_queue (q, one, two); + p->status =DIFF_STATUS_MODIFIED ; + } + } + +/* Re-sort the filepairs */ +diffcore_fix_diff_index (); + +/* Cleanup */ +strset_clear (&present); +} +static void diff_flush_patch_all_file_pairs (struct diff_options *o) {int i;
@@ -6337,6 +6450,9 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)if (o->color_moved) o->emitted_symbols = &esm; +if (o->additional_path_headers) +create_filepairs_for_header_only_notifications (o); +for (i =0 ; i < q->nr; i++) {struct diff_filepair *p = q->queue[i];if (check_pair_status (p))
@@ -6413,7 +6529,7 @@ void diff_flush(struct diff_options *options)* Order: raw, stat, summary, patch * or: name/name-status/checkdiff (other bits clear) */ -if (!q->nr) +if (!q->nr && !options->additional_path_headers) goto free_queue; if (output_format & (DIFF_FORMAT_RAW |
diff.h +2 -1
@@ -395,6 +395,7 @@ struct diff_options {struct repository *repo;struct option *parseopts; +struct strmap *additional_path_headers;int no_free; };
@@ -593,7 +594,7 @@ void diffcore_fix_diff_index(void);" show all files diff when -S is used and hit is found.\n" \" -a --text treat all files as text.\n" -int diff_queue_is_empty(void); +int diff_queue_is_empty(struct diff_options *o);void diff_flush (struct diff_options *);void diff_free (struct diff_options *);void diff_warn_rename_limit (const char *varname,int needed,int degraded_cc);
log-tree.c +1 -1
@@ -850,7 +850,7 @@ int log_tree_diff_flush(struct rev_info *opt) opt->shown_dashes =0 ;diffcore_std (&opt->diffopt); -if (diff_queue_is_empty ()) { +if (diff_queue_is_empty (&opt->diffopt)) {int saved_fmt = opt->diffopt.output_format; opt->diffopt.output_format =DIFF_FORMAT_NO_OUTPUT ;diff_flush (&opt->diffopt);
Signature · 1 line
gitgitgadget
PATCH v5 8/10 show, log: include conflict/warning messages in --remerge-diff headers
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Conflicts such as modify/delete, rename/rename, or file/directory are not representable via content conflict markers, and the normal output messages notifying users about these were dropped with --remerge-diff. While we don't want these messages randomly shown before the commit and diff headers, we do want them to still be shown; include them as part of the diff headers instead.
--- log-tree.c | 51 ++++++++++++++ merge-ort.c | 1 + merge-ort.h | 10 +++ t/t4069-remerge-diff.sh | 144 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+)
log-tree.c +51 -0
@@ -19,6 +19,7 @@#include "line-log.h" #include "help.h" #include "range-diff.h" +#include "strmap.h" static struct decoration name_decoration = {"object names" };static int decoration_loaded;
@@ -905,6 +906,52 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit)return !opt->loginfo; } +static void setup_additional_headers (struct diff_options *o, +struct strmap *all_headers) +{ +struct hashmap_iter iter; +struct strmap_entry *entry; + +/* +* Make o->additional_path_headers contain the subset of all_headers +* that match o->pathspec. If there aren't any that match o->pathspec, +* then make o->additional_path_headers be NULL. +*/ + +if (!o->pathspec.nr) { + o->additional_path_headers = all_headers; +return ; + } + + o->additional_path_headers =xmalloc (sizeof (struct strmap )); +strmap_init_with_options (o->additional_path_headers,NULL ,0 ); +strmap_for_each_entry (all_headers, &iter, entry) { +if (match_pathspec (the_repository->index, &o->pathspec, + entry->key,strlen (entry->key), +0 /* prefix */ ,NULL /* seen */ , +0 /* is_dir */ )) +strmap_put (o->additional_path_headers, + entry->key, entry->value); + } +if (!strmap_get_size (o->additional_path_headers)) { +strmap_clear (o->additional_path_headers,0 ); +FREE_AND_NULL (o->additional_path_headers); + } +} + +static void cleanup_additional_headers (struct diff_options *o) +{ +if (!o->pathspec.nr) { + o->additional_path_headers =NULL ; +return ; + } +if (!o->additional_path_headers) +return ; + +strmap_clear (o->additional_path_headers,0 ); +FREE_AND_NULL (o->additional_path_headers); +} + static int do_remerge_diff(struct rev_info *opt, struct commit_list *parents, struct object_id *oid,
@@ -922,6 +969,8 @@ static int do_remerge_diff(struct rev_info *opt,/* Setup merge options */ init_merge_options (&o, the_repository); o.show_rename_progress =0 ; + o.record_conflict_msgs_as_headers =1 ; + o.msg_header_prefix ="remerge" ; ctx.abbrev =DEFAULT_ABBREV ;format_commit_message (parent1,"%h (%s)" , &parent1_desc, &ctx);
@@ -938,10 +987,12 @@ static int do_remerge_diff(struct rev_info *opt,merge_incore_recursive (&o, bases, parent1, parent2, &res);/* Show the diff */ +setup_additional_headers (&opt->diffopt, res.path_messages);diff_tree_oid (&res.tree->object.oid, oid,"" , &opt->diffopt);log_tree_diff_flush (opt);/* Cleanup */ +cleanup_additional_headers (&opt->diffopt);strbuf_release (&parent1_desc);strbuf_release (&parent2_desc);merge_finalize (&o, &res);
merge-ort.c +1 -0
@@ -4585,6 +4585,7 @@ redo:trace2_region_leave ("merge" ,"process_entries" , opt->repo);/* Set return values */ + result->path_messages = &opt->priv->output; result->tree =parse_tree_indirect (&working_tree_oid);/* existence of conflicted entries implies unclean */ result->clean &=strmap_empty (&opt->priv->conflicted);
merge-ort.h +10 -0
@@ -5,6 +5,7 @@struct commit ;struct tree ; +struct strmap ;struct merge_result {/*
@@ -23,6 +24,15 @@ struct merge_result {*/ struct tree *tree; +/* +* Special messages and conflict notices for various paths +* +* This is a map of pathnames to strbufs. It contains various +* warning/conflict/notice messages (possibly multiple per path) +* that callers may want to use. +*/ +struct strmap *path_messages; +/* * Additional metadata used by merge_switch_to_result() or future calls * to merge_incore_*(). Includes data needed to update the index (if
t/t4069-remerge-diff.sh +144 -0
154 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v5 9/10 merge-ort: mark conflict/warning messages from inner merges as omittable
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
A recursive merge involves merging the merge bases of the two branches being merged. Such an inner merge can itself generate conflict notices. While such notices may be useful when initially trying to create a merge, they seem to just be noise when investigating merges later with --remerge-diff. (Especially when both sides of the outer merge resolved the conflict the same way leading to no overall conflict.) Remove them.
--- merge-ort.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-)
merge-ort.c +3 -1
@@ -638,7 +638,9 @@ static void path_msg(struct merge_options *opt,struct strbuf tmp =STRBUF_INIT ;if (opt->record_conflict_msgs_as_headers && omittable_hint) -return ;/* Do not record mere hints in tree */ +return ;/* Do not record mere hints in headers */ +if (opt->record_conflict_msgs_as_headers && opt->priv->call_depth) +return ;/* Do not record inner merge issues in headers */ sb =strmap_get (&opt->priv->output, path);if (!sb) { sb =xmalloc (sizeof (*sb));
Signature · 1 line
gitgitgadget
PATCH v5 10/10 diff-merges: avoid history simplifications when diffing merges
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Doing diffs for merges are special; they should typically avoid history simplification. For example, with
git log --diff-merges=first-parent -- path
the default history simplification would remove merge commits from consideration if the file "path" matched the second parent. That is counter to what the user wants when looking for first-parent diffs. Similar comments can be made for --diff-merges=separate (which diffs against both parents) and --diff-merges=remerge (which diffs against a remerge of the merge commit).
However, history simplification still makes sense if not doing diffing merges, and it also makes sense for the combined and dense-combined forms of diffing merges (because both of those are defined to only show a diff when the merge result at the relevant paths differs from *both* parents).
So, for separate, first-parent, and remerge styles of diff-merges, turn off history simplification.
--- diff-merges.c | 2 ++ t/t4069-remerge-diff.sh | 58 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-)
diff-merges.c +2 -0
@@ -24,6 +24,7 @@ static void set_separate(struct rev_info *revs) {suppress (revs); revs->separate_merges =1 ; + revs->simplify_history =0 ; }static void set_first_parent (struct rev_info *revs)
@@ -50,6 +51,7 @@ static void set_remerge_diff(struct rev_info *revs) {suppress (revs); revs->remerge_diff =1 ; + revs->simplify_history =0 ; }static diff_merges_setup_func_t func_by_opt (const char *optarg)
t/t4069-remerge-diff.sh +57 -1
64 lines not on this page · raw message
Signature · 1 line
gitgitgadget