mergetool: new config guiDefault supports auto-toggling gui by DISPLAY

When no merge.tool or diff.tool is configured or manually selected, the selection of a default tool is sensitive to the DISPLAY variable; in a GUI session a gui-specific tool will be proposed if found, and otherwise a terminal-based one. This "GUI-optimizing" behavior is important because a GUI can make a huge difference to a user's ability to understand and correctly complete a non-trivial conflicting merge. Some time ago the merge.guitool and diff.guitool config options were introduced to enable users to configure both a GUI tool, and a non-GUI tool (with fallback if no GUI tool configured), in the same environment. Unfortunately, the --gui argument introduced to support the selection of the guitool is still explicit. When using configured tools, there is no equivalent of the no-tool-configured "propose a GUI tool if we are in a GUI environment" behavior. As proposed in <xmqqmtb8jsej.fsf@gitster.g>, introduce new configuration options, difftool.guiDefault and mergetool.guiDefault, supporting a special value "auto" which causes the corresponding tool or guitool to be selected depending on the presence of a non-empty DISPLAY value. Also support "true" to say "default to the guitool (unless --no-gui is passed on the commandline)", and "false" as the previous default behavior when these new configuration options are not specified. Signed-off-by: Tao Klerks <tao@klerks.biz> Acked-by: David Aguilar <davvid@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Tao Klerks committed Mar 18, 2023 at 15:27 UTC 42943b950e12f2d3e56688ecef1b0502e162b436
10 files changed +179 -14
Documentation/config/difftool.txt
+7
@@ -34,3 +34,10 @@ See the `--trust-exit-code` option in linkgit:git-difftool[1] for more details.
34
35 difftool.prompt::
36 Prompt before each invocation of the diff tool.
37 +
38 +difftool.guiDefault::
39 + Set `true` to use the `diff.guitool` by default (equivalent to specifying
40 + the `--gui` argument), or `auto` to select `diff.guitool` or `diff.tool`
41 + depending on the presence of a `DISPLAY` environment variable value. The
42 + default is `false`, where the `--gui` argument must be provided
43 + explicitly for the `diff.guitool` to be used.
Documentation/config/mergetool.txt
+7
@@ -85,3 +85,10 @@ mergetool.writeToTemp::
85
86 mergetool.prompt::
87 Prompt before each invocation of the merge resolution program.
88 +
89 +mergetool.guiDefault::
90 + Set `true` to use the `merge.guitool` by default (equivalent to
91 + specifying the `--gui` argument), or `auto` to select `merge.guitool`
92 + or `merge.tool` depending on the presence of a `DISPLAY` environment
93 + variable value. The default is `false`, where the `--gui` argument
94 + must be provided explicitly for the `merge.guitool` to be used.
Documentation/git-difftool.txt
+6 -4
@@ -97,10 +97,12 @@ instead. `--no-symlinks` is the default on Windows.
97 --[no-]gui::
98 When 'git-difftool' is invoked with the `-g` or `--gui` option
99 the default diff tool will be read from the configured
100 - `diff.guitool` variable instead of `diff.tool`. The `--no-gui`
101 - option can be used to override this setting. If `diff.guitool`
102 - is not set, we will fallback in the order of `merge.guitool`,
103 - `diff.tool`, `merge.tool` until a tool is found.
100 + `diff.guitool` variable instead of `diff.tool`. This may be
101 + selected automatically using the configuration variable
102 + `difftool.guiDefault`. The `--no-gui` option can be used to
103 + override these settings. If `diff.guitool` is not set, we will
104 + fallback in the order of `merge.guitool`, `diff.tool`,
105 + `merge.tool` until a tool is found.
106
107 --[no-]trust-exit-code::
108 'git-difftool' invokes a diff tool individually on each file.
Documentation/git-mergetool.txt
+5 -4
@@ -85,12 +85,13 @@ success of the resolution after the custom tool has exited.
85 the default merge tool will be read from the configured
86 `merge.guitool` variable instead of `merge.tool`. If
87 `merge.guitool` is not set, we will fallback to the tool
88 - configured under `merge.tool`.
88 + configured under `merge.tool`. This may be autoselected using
89 + the configuration variable `mergetool.guiDefault`.
90
91 --no-gui::
91 - This overrides a previous `-g` or `--gui` setting and reads the
92 - default merge tool will be read from the configured `merge.tool`
93 - variable.
92 + This overrides a previous `-g` or `--gui` setting or
93 + `mergetool.guiDefault` configuration and reads the default merge
94 + tool from the configured `merge.tool` variable.
95
96 -O<orderfile>::
97 Process files in the order specified in the
builtin/difftool.c
+12 -4
@@ -685,7 +685,7 @@ static int run_file_diff(int prompt, const char *prefix,
685
686 int cmd_difftool(int argc, const char **argv, const char *prefix)
687 {
688 - int use_gui_tool = 0, dir_diff = 0, prompt = -1, symlinks = 0,
688 + int use_gui_tool = -1, dir_diff = 0, prompt = -1, symlinks = 0,
689 tool_help = 0, no_index = 0;
690 static char *difftool_cmd = NULL, *extcmd = NULL;
691 struct option builtin_difftool_options[] = {
@@ -735,13 +735,21 @@ int cmd_difftool(int argc, const char **argv, const char *prefix)
735 } else if (dir_diff)
736 die(_("options '%s' and '%s' cannot be used together"), "--dir-diff", "--no-index");
737
738 - die_for_incompatible_opt3(use_gui_tool, "--gui",
738 + die_for_incompatible_opt3(use_gui_tool == 1, "--gui",
739 !!difftool_cmd, "--tool",
740 !!extcmd, "--extcmd");
741
742 - if (use_gui_tool)
742 + /*
743 + * Explicitly specified GUI option is forwarded to git-mergetool--lib.sh;
744 + * empty or unset means "use the difftool.guiDefault config or default to
745 + * false".
746 + */
747 + if (use_gui_tool == 1)
748 setenv("GIT_MERGETOOL_GUI", "true", 1);
744 - else if (difftool_cmd) {
749 + else if (use_gui_tool == 0)
750 + setenv("GIT_MERGETOOL_GUI", "false", 1);
751 +
752 + if (difftool_cmd) {
753 if (*difftool_cmd)
754 setenv("GIT_DIFF_TOOL", difftool_cmd, 1);
755 else
git-difftool--helper.sh
+5
@@ -75,6 +75,11 @@ then
75 merge_tool="$GIT_DIFF_TOOL"
76 else
77 merge_tool="$(get_merge_tool)"
78 + subshell_exit_status=$?
79 + if test $subshell_exit_status -gt 1
80 + then
81 + exit $subshell_exit_status
82 + fi
83 fi
84 fi
85
git-mergetool--lib.sh
+40
@@ -97,7 +97,42 @@ merge_mode () {
97 test "$TOOL_MODE" = merge
98 }
99
100 +get_gui_default () {
101 + if diff_mode
102 + then
103 + GUI_DEFAULT_KEY="difftool.guiDefault"
104 + else
105 + GUI_DEFAULT_KEY="mergetool.guiDefault"
106 + fi
107 + GUI_DEFAULT_CONFIG_LCASE=$(git config --default false --get "$GUI_DEFAULT_KEY" | tr 'A-Z' 'a-z')
108 + if test "$GUI_DEFAULT_CONFIG_LCASE" = "auto"
109 + then
110 + if test -n "$DISPLAY"
111 + then
112 + GUI_DEFAULT=true
113 + else
114 + GUI_DEFAULT=false
115 + fi
116 + else
117 + GUI_DEFAULT=$(git config --default false --bool --get "$GUI_DEFAULT_KEY")
118 + subshell_exit_status=$?
119 + if test $subshell_exit_status -ne 0
120 + then
121 + exit $subshell_exit_status
122 + fi
123 + fi
124 + echo $GUI_DEFAULT
125 +}
126 +
127 gui_mode () {
128 + if test -z "$GIT_MERGETOOL_GUI"
129 + then
130 + GIT_MERGETOOL_GUI=$(get_gui_default)
131 + if test $? -ne 0
132 + then
133 + exit 2
134 + fi
135 + fi
136 test "$GIT_MERGETOOL_GUI" = true
137 }
138
@@ -467,6 +502,11 @@ get_merge_tool () {
502 is_guessed=false
503 # Check if a merge tool has been configured
504 merge_tool=$(get_configured_merge_tool)
505 + subshell_exit_status=$?
506 + if test $subshell_exit_status -gt "1"
507 + then
508 + exit $subshell_exit_status
509 + fi
510 # Try to guess an appropriate merge tool if no tool has been set.
511 if test -z "$merge_tool"
512 then
git-mergetool.sh
+7 -2
@@ -451,7 +451,7 @@ print_noop_and_exit () {
451
452 main () {
453 prompt=$(git config --bool mergetool.prompt)
454 - GIT_MERGETOOL_GUI=false
454 + GIT_MERGETOOL_GUI=
455 guessed_merge_tool=false
456 orderfile=
457
@@ -511,9 +511,14 @@ main () {
511
512 if test -z "$merge_tool"
513 then
514 - if ! merge_tool=$(get_merge_tool)
514 + merge_tool=$(get_merge_tool)
515 + subshell_exit_status=$?
516 + if test $subshell_exit_status = 1
517 then
518 guessed_merge_tool=true
519 + elif test $subshell_exit_status -gt 1
520 + then
521 + exit $subshell_exit_status
522 fi
523 fi
524 merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
t/t7610-mergetool.sh
+38
@@ -860,4 +860,42 @@ test_expect_success 'mergetool hideResolved' '
860 git commit -m "test resolved with mergetool"
861 '
862
863 +test_expect_success 'mergetool with guiDefault' '
864 + test_config merge.guitool myguitool &&
865 + test_config mergetool.myguitool.cmd "(printf \"gui \" && cat \"\$REMOTE\") >\"\$MERGED\"" &&
866 + test_config mergetool.myguitool.trustExitCode true &&
867 + test_when_finished "git reset --hard" &&
868 + git checkout -b test$test_count branch1 &&
869 + git submodule update -N &&
870 + test_must_fail git merge main &&
871 +
872 + test_config mergetool.guiDefault auto &&
873 + DISPLAY=SOMETHING && export DISPLAY &&
874 + yes "" | git mergetool both &&
875 + yes "" | git mergetool file1 file1 &&
876 +
877 + DISPLAY= && export DISPLAY &&
878 + yes "" | git mergetool file2 "spaced name" &&
879 +
880 + test_config mergetool.guiDefault true &&
881 + yes "" | git mergetool subdir/file3 &&
882 +
883 + yes "d" | git mergetool file11 &&
884 + yes "d" | git mergetool file12 &&
885 + yes "l" | git mergetool submod &&
886 +
887 + echo "gui main updated" >expect &&
888 + test_cmp expect file1 &&
889 +
890 + echo "main new" >expect &&
891 + test_cmp expect file2 &&
892 +
893 + echo "gui main new sub" >expect &&
894 + test_cmp expect subdir/file3 &&
895 +
896 + echo "branch1 submodule" >expect &&
897 + test_cmp expect submod/bar &&
898 + git commit -m "branch1 resolved with mergetool"
899 +'
900 +
901 test_done
t/t7800-difftool.sh
+52
@@ -155,6 +155,58 @@ test_expect_success 'difftool honors --gui' '
155 test_cmp expect actual
156 '
157
158 +test_expect_success 'difftool with guiDefault auto selects gui tool when there is DISPLAY' '
159 + difftool_test_setup &&
160 + test_config merge.tool bogus-tool &&
161 + test_config diff.tool bogus-tool &&
162 + test_config diff.guitool test-tool &&
163 + test_config difftool.guiDefault auto &&
164 + DISPLAY=SOMETHING && export DISPLAY &&
165 +
166 + echo branch >expect &&
167 + git difftool --no-prompt branch >actual &&
168 + test_cmp expect actual
169 +'
170 +test_expect_success 'difftool with guiDefault auto selects regular tool when no DISPLAY' '
171 + difftool_test_setup &&
172 + test_config diff.guitool bogus-tool &&
173 + test_config diff.tool test-tool &&
174 + test_config difftool.guiDefault Auto &&
175 + DISPLAY= && export DISPLAY &&
176 +
177 + echo branch >expect &&
178 + git difftool --no-prompt branch >actual &&
179 + test_cmp expect actual
180 +'
181 +
182 +test_expect_success 'difftool with guiDefault true selects gui tool' '
183 + difftool_test_setup &&
184 + test_config diff.tool bogus-tool &&
185 + test_config diff.guitool test-tool &&
186 + test_config difftool.guiDefault true &&
187 +
188 + DISPLAY= && export DISPLAY &&
189 + echo branch >expect &&
190 + git difftool --no-prompt branch >actual &&
191 + test_cmp expect actual &&
192 +
193 + DISPLAY=Something && export DISPLAY &&
194 + echo branch >expect &&
195 + git difftool --no-prompt branch >actual &&
196 + test_cmp expect actual
197 +'
198 +
199 +test_expect_success 'difftool --no-gui trumps config guiDefault' '
200 + difftool_test_setup &&
201 + test_config diff.guitool bogus-tool &&
202 + test_config diff.tool test-tool &&
203 + test_config difftool.guiDefault true &&
204 +
205 + echo branch >expect &&
206 + git difftool --no-prompt --no-gui branch >actual &&
207 + test_cmp expect actual
208 +'
209 +
210 test_expect_success 'difftool --gui last setting wins' '
211 difftool_test_setup &&
212 : >expect &&