From 98ea309b3fa4818c1591b9071925ccb22c2e786b Mon Sep 17 00:00:00 2001 From: Seth House Date: Tue, 9 Feb 2021 13:07:10 -0700 Subject: [PATCH 1/3] mergetool: add hideResolved configuration The purpose of a mergetool is to help the user resolve any conflicts that Git cannot automatically resolve. If there is a conflict that must be resolved manually Git will write a file named MERGED which contains everything Git was able to resolve by itself and also everything that it was not able to resolve wrapped in conflict markers. One way to think of MERGED is as a two- or three-way diff. If each "side" of the conflict markers is separately extracted an external tool can represent those conflicts as a side-by-side diff. However many mergetools instead diff LOCAL and REMOTE both of which contain versions of the file from before the merge. Since the conflicts Git resolved automatically are not present it forces the user to manually re-resolve those conflicts. Some mergetools also show MERGED but often only for reference and not as the focal point to resolve the conflicts. This adds a `mergetool.hideResolved` flag that will overwrite LOCAL and REMOTE with each corresponding "side" of a conflicted file and thus hide all conflicts that Git was able to resolve itself. Overwriting these files will immediately benefit any mergetool that uses them without requiring any changes to the tool. No adverse effects were noted in a small survey of popular mergetools[1] so this behavior defaults to `true`. However it can be globally disabled by setting `mergetool.hideResolved` to `false`. [1] https://www.eseth.org/2020/mergetools.html https://github.com/whiteinge/eseth/blob/c884424769fffb05d87afb33b2cf80cecb4044c3/2020/mergetools.md Original-implementation-by: Felipe Contreras Signed-off-by: Seth House Signed-off-by: Junio C Hamano --- Documentation/config/mergetool.txt | 10 ++++++++++ git-mergetool.sh | 14 ++++++++++++++ t/t7610-mergetool.sh | 18 ++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/Documentation/config/mergetool.txt b/Documentation/config/mergetool.txt index 16a27443a3e..b858191970c 100644 --- a/Documentation/config/mergetool.txt +++ b/Documentation/config/mergetool.txt @@ -40,6 +40,16 @@ mergetool.meld.useAutoMerge:: value of `false` avoids using `--auto-merge` altogether, and is the default value. +mergetool.hideResolved:: + During a merge Git will automatically resolve as many conflicts as + possible and write the 'MERGED' file containing conflict markers around + any conflicts that it cannot resolve; 'LOCAL' and 'REMOTE' normally + represent the versions of the file from before Git's conflict + resolution. This flag causes 'LOCAL' and 'REMOTE' to be overwriten so + that only the unresolved conflicts are presented to the merge tool. Can + be configured per-tool via the `mergetool..hideResolved` + configuration variable. Defaults to `true`. + mergetool.keepBackup:: After performing a merge, the original file with conflict markers can be saved as a file with a `.orig` extension. If this variable diff --git a/git-mergetool.sh b/git-mergetool.sh index e3f6d543fb5..40a103443d9 100755 --- a/git-mergetool.sh +++ b/git-mergetool.sh @@ -239,6 +239,13 @@ checkout_staged_file () { fi } +hide_resolved () { + git merge-file --ours -q -p "$LOCAL" "$BASE" "$REMOTE" >"$LCONFL" + git merge-file --theirs -q -p "$LOCAL" "$BASE" "$REMOTE" >"$RCONFL" + mv -- "$LCONFL" "$LOCAL" + mv -- "$RCONFL" "$REMOTE" +} + merge_file () { MERGED="$1" @@ -276,7 +283,9 @@ merge_file () { BACKUP="$MERGETOOL_TMPDIR/${BASE}_BACKUP_$$$ext" LOCAL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_$$$ext" + LCONFL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_LCONFL_$$$ext" REMOTE="$MERGETOOL_TMPDIR/${BASE}_REMOTE_$$$ext" + RCONFL="$MERGETOOL_TMPDIR/${BASE}_REMOTE_RCONFL_$$$ext" BASE="$MERGETOOL_TMPDIR/${BASE}_BASE_$$$ext" base_mode= local_mode= remote_mode= @@ -322,6 +331,11 @@ merge_file () { checkout_staged_file 2 "$MERGED" "$LOCAL" checkout_staged_file 3 "$MERGED" "$REMOTE" + if test "$(git config --type=bool mergetool.hideResolved)" != "false" + then + hide_resolved + fi + if test -z "$local_mode" || test -z "$remote_mode" then echo "Deleted merge conflict for '$MERGED':" diff --git a/t/t7610-mergetool.sh b/t/t7610-mergetool.sh index 70afdd06fa7..0e34b87e376 100755 --- a/t/t7610-mergetool.sh +++ b/t/t7610-mergetool.sh @@ -828,4 +828,22 @@ test_expect_success 'mergetool -Oorder-file is honored' ' test_cmp expect actual ' +test_expect_success 'mergetool hideResolved' ' + test_config mergetool.hideResolved true && + test_when_finished "git reset --hard" && + git checkout -b test${test_count}_b master && + test_write_lines >file1 base "" a && + git commit -a -m "base" && + test_write_lines >file1 base "" c && + git commit -a -m "remote update" && + git checkout -b test${test_count}_a HEAD~ && + test_write_lines >file1 local "" b && + git commit -a -m "local update" && + test_must_fail git merge test${test_count}_b && + yes "" | git mergetool file1 && + test_write_lines >expect local "" c && + test_cmp expect file1 && + git commit -m "test resolved with mergetool" +' + test_done From de8dafbada811bc1bc8e1288541931f3c5406231 Mon Sep 17 00:00:00 2001 From: Seth House Date: Tue, 9 Feb 2021 13:07:11 -0700 Subject: [PATCH 2/3] mergetool: break setup_tool out into separate initialization function This is preparation for the following commit where we need to source the mergetool shell script to look for overrides before `run_merge_tool` is called. Previously `run_merge_tool` both sourced that script and invoked the mergetool. In the case of the following commit, we need the result of the `hide_resolved` override, if present, before we actually run `run_merge_tool`. The new `initialize_merge_tool` wrapper is exposed and documented as a public interface for consistency with the existing `run_merge_tool` which is also public. Although `setup_tool` could instead be exposed directly, the related `setup_user_tool` would probably also want to be elevated to match and this felt the cleanest to me. Signed-off-by: Seth House Signed-off-by: Junio C Hamano --- Documentation/git-mergetool--lib.txt | 4 ++++ git-difftool--helper.sh | 6 ++++++ git-mergetool--lib.sh | 7 ++++--- git-mergetool.sh | 2 ++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Documentation/git-mergetool--lib.txt b/Documentation/git-mergetool--lib.txt index 4da9d240962..3e8f59ac0e4 100644 --- a/Documentation/git-mergetool--lib.txt +++ b/Documentation/git-mergetool--lib.txt @@ -38,6 +38,10 @@ get_merge_tool_cmd:: get_merge_tool_path:: returns the custom path for a merge tool. +initialize_merge_tool:: + bring merge tool specific functions into scope so they can be used or + overridden. + run_merge_tool:: launches a merge tool given the tool name and a true/false flag to indicate whether a merge base is present. diff --git a/git-difftool--helper.sh b/git-difftool--helper.sh index 46af3e60b71..992124cc67c 100755 --- a/git-difftool--helper.sh +++ b/git-difftool--helper.sh @@ -61,6 +61,9 @@ launch_merge_tool () { export BASE eval $GIT_DIFFTOOL_EXTCMD '"$LOCAL"' '"$REMOTE"' else + initialize_merge_tool "$merge_tool" + # ignore the error from the above --- run_merge_tool + # will diagnose unusable tool by itself run_merge_tool "$merge_tool" fi } @@ -79,6 +82,9 @@ if test -n "$GIT_DIFFTOOL_DIRDIFF" then LOCAL="$1" REMOTE="$2" + initialize_merge_tool "$merge_tool" + # ignore the error from the above --- run_merge_tool + # will diagnose unusable tool by itself run_merge_tool "$merge_tool" false else # Launch the merge tool on each path provided by 'git diff' diff --git a/git-mergetool--lib.sh b/git-mergetool--lib.sh index 7225abd8112..e059b3559e0 100644 --- a/git-mergetool--lib.sh +++ b/git-mergetool--lib.sh @@ -248,6 +248,10 @@ trust_exit_code () { fi } +initialize_merge_tool () { + # Bring tool-specific functions into scope + setup_tool "$1" || return 1 +} # Entry point for running tools run_merge_tool () { @@ -259,9 +263,6 @@ run_merge_tool () { merge_tool_path=$(get_merge_tool_path "$1") || exit base_present="$2" - # Bring tool-specific functions into scope - setup_tool "$1" || return 1 - if merge_mode then run_merge_cmd "$1" diff --git a/git-mergetool.sh b/git-mergetool.sh index 40a103443d9..e5eac935f3f 100755 --- a/git-mergetool.sh +++ b/git-mergetool.sh @@ -272,6 +272,8 @@ merge_file () { ext= esac + initialize_merge_tool "$merge_tool" || return + mergetool_tmpdir_init if test "$MERGETOOL_TMPDIR" != "." From 9d9cf230317f7fe7cb153f61b537e6e9bef22e3b Mon Sep 17 00:00:00 2001 From: Seth House Date: Tue, 9 Feb 2021 13:07:12 -0700 Subject: [PATCH 3/3] mergetool: add per-tool support and overrides for the hideResolved flag Add a per-tool override flag so that users may enable the flag for one tool and disable it for another by setting `mergetool..hideResolved` to `false`. In addition, the author or maintainer of a mergetool may optionally override the default `hideResolved` value for that mergetool. If the `mergetools/` shell script contains a `hide_resolved_enabled` function it will be called when the mergetool is invoked and the return value will be used as the default for the `hideResolved` flag. hide_resolved_enabled () { return 1 } Disabling may be desirable if the mergetool wants or needs access to the original, unmodified 'LOCAL' and 'REMOTE' versions of the conflicted file. For example: - A tool may use a custom conflict resolution algorithm and prefer to ignore the results of Git's conflict resolution. - A tool may want to visually compare/constrast the version of the file from before the merge (saved to 'LOCAL', 'REMOTE', and 'BASE') with Git's conflict resolution results (saved to 'MERGED'). Helped-by: Johannes Sixt Helped-by: Junio C Hamano Signed-off-by: Seth House Signed-off-by: Junio C Hamano --- Documentation/config/mergetool.txt | 5 +++++ git-mergetool--lib.sh | 4 ++++ git-mergetool.sh | 36 +++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Documentation/config/mergetool.txt b/Documentation/config/mergetool.txt index b858191970c..90f76f5b9ba 100644 --- a/Documentation/config/mergetool.txt +++ b/Documentation/config/mergetool.txt @@ -13,6 +13,11 @@ mergetool..cmd:: merged; 'MERGED' contains the name of the file to which the merge tool should write the results of a successful merge. +mergetool..hideResolved:: + Allows the user to override the global `mergetool.hideResolved` value + for a specific tool. See `mergetool.hideResolved` for the full + description. + mergetool..trustExitCode:: For a custom merge command, specify whether the exit code of the merge command can be used to determine whether the merge was diff --git a/git-mergetool--lib.sh b/git-mergetool--lib.sh index e059b3559e0..11f00dde41b 100644 --- a/git-mergetool--lib.sh +++ b/git-mergetool--lib.sh @@ -164,6 +164,10 @@ setup_tool () { return 1 } + hide_resolved_enabled () { + return 0 + } + translate_merge_tool_path () { echo "$1" } diff --git a/git-mergetool.sh b/git-mergetool.sh index e5eac935f3f..911470a5b2c 100755 --- a/git-mergetool.sh +++ b/git-mergetool.sh @@ -333,7 +333,41 @@ merge_file () { checkout_staged_file 2 "$MERGED" "$LOCAL" checkout_staged_file 3 "$MERGED" "$REMOTE" - if test "$(git config --type=bool mergetool.hideResolved)" != "false" + # hideResolved preferences hierarchy. + global_config="mergetool.hideResolved" + tool_config="mergetool.${merge_tool}.hideResolved" + + if enabled=$(git config --type=bool "$tool_config") + then + # The user has a specific preference for a specific tool and no + # other preferences should override that. + : ; + elif enabled=$(git config --type=bool "$global_config") + then + # The user has a general preference for all tools. + # + # 'true' means the user likes the feature so we should use it + # where possible but tool authors can still override. + # + # 'false' means the user doesn't like the feature so we should + # not use it anywhere. + if test "$enabled" = true && hide_resolved_enabled + then + enabled=true + else + enabled=false + fi + else + # The user does not have a preference. Ask the tool. + if hide_resolved_enabled + then + enabled=true + else + enabled=false + fi + fi + + if test "$enabled" = true then hide_resolved fi