revision: ensure full index
[git] / git-mergetool--lib.sh
1 # git-mergetool--lib is a shell library for common merge tool functions
2
3 : ${MERGE_TOOLS_DIR=$(git --exec-path)/mergetools}
4
5 IFS='
6 '
7
8 mode_ok () {
9         if diff_mode
10         then
11                 can_diff
12         elif merge_mode
13         then
14                 can_merge
15         else
16                 false
17         fi
18 }
19
20 is_available () {
21         merge_tool_path=$(translate_merge_tool_path "$1") &&
22         type "$merge_tool_path" >/dev/null 2>&1
23 }
24
25 list_config_tools () {
26         section=$1
27         line_prefix=${2:-}
28
29         git config --get-regexp $section'\..*\.cmd' |
30         while read -r key value
31         do
32                 toolname=${key#$section.}
33                 toolname=${toolname%.cmd}
34
35                 printf "%s%s\n" "$line_prefix" "$toolname"
36         done
37 }
38
39 show_tool_names () {
40         condition=${1:-true} per_line_prefix=${2:-} preamble=${3:-}
41         not_found_msg=${4:-}
42         extra_content=${5:-}
43
44         shown_any=
45         ( cd "$MERGE_TOOLS_DIR" && ls ) | {
46                 while read scriptname
47                 do
48                         setup_tool "$scriptname" 2>/dev/null
49                         # We need an actual line feed here
50                         variants="$variants
51 $(list_tool_variants)"
52                 done
53                 variants="$(echo "$variants" | sort -u)"
54
55                 for toolname in $variants
56                 do
57                         if setup_tool "$toolname" 2>/dev/null &&
58                                 (eval "$condition" "$toolname")
59                         then
60                                 if test -n "$preamble"
61                                 then
62                                         printf "%s\n" "$preamble"
63                                         preamble=
64                                 fi
65                                 shown_any=yes
66                                 printf "%s%s\n" "$per_line_prefix" "$toolname"
67                         fi
68                 done
69
70                 if test -n "$extra_content"
71                 then
72                         if test -n "$preamble"
73                         then
74                                 # Note: no '\n' here since we don't want a
75                                 # blank line if there is no initial content.
76                                 printf "%s" "$preamble"
77                                 preamble=
78                         fi
79                         shown_any=yes
80                         printf "\n%s\n" "$extra_content"
81                 fi
82
83                 if test -n "$preamble" && test -n "$not_found_msg"
84                 then
85                         printf "%s\n" "$not_found_msg"
86                 fi
87
88                 test -n "$shown_any"
89         }
90 }
91
92 diff_mode () {
93         test "$TOOL_MODE" = diff
94 }
95
96 merge_mode () {
97         test "$TOOL_MODE" = merge
98 }
99
100 gui_mode () {
101         test "$GIT_MERGETOOL_GUI" = true
102 }
103
104 translate_merge_tool_path () {
105         echo "$1"
106 }
107
108 check_unchanged () {
109         if test "$MERGED" -nt "$BACKUP"
110         then
111                 return 0
112         else
113                 while true
114                 do
115                         echo "$MERGED seems unchanged."
116                         printf "Was the merge successful [y/n]? "
117                         read answer || return 1
118                         case "$answer" in
119                         y*|Y*) return 0 ;;
120                         n*|N*) return 1 ;;
121                         esac
122                 done
123         fi
124 }
125
126 valid_tool () {
127         setup_tool "$1" && return 0
128         cmd=$(get_merge_tool_cmd "$1")
129         test -n "$cmd"
130 }
131
132 setup_user_tool () {
133         merge_tool_cmd=$(get_merge_tool_cmd "$tool")
134         test -n "$merge_tool_cmd" || return 1
135
136         diff_cmd () {
137                 ( eval $merge_tool_cmd )
138         }
139
140         merge_cmd () {
141                 ( eval $merge_tool_cmd )
142         }
143
144         list_tool_variants () {
145                 echo "$tool"
146         }
147 }
148
149 setup_tool () {
150         tool="$1"
151
152         # Fallback definitions, to be overridden by tools.
153         can_merge () {
154                 return 0
155         }
156
157         can_diff () {
158                 return 0
159         }
160
161         diff_cmd () {
162                 return 1
163         }
164
165         merge_cmd () {
166                 return 1
167         }
168
169         hide_resolved_enabled () {
170                 return 0
171         }
172
173         translate_merge_tool_path () {
174                 echo "$1"
175         }
176
177         list_tool_variants () {
178                 echo "$tool"
179         }
180
181         # Most tools' exit codes cannot be trusted, so By default we ignore
182         # their exit code and check the merged file's modification time in
183         # check_unchanged() to determine whether or not the merge was
184         # successful.  The return value from run_merge_cmd, by default, is
185         # determined by check_unchanged().
186         #
187         # When a tool's exit code can be trusted then the return value from
188         # run_merge_cmd is simply the tool's exit code, and check_unchanged()
189         # is not called.
190         #
191         # The return value of exit_code_trustable() tells us whether or not we
192         # can trust the tool's exit code.
193         #
194         # User-defined and built-in tools default to false.
195         # Built-in tools advertise that their exit code is trustable by
196         # redefining exit_code_trustable() to true.
197
198         exit_code_trustable () {
199                 false
200         }
201
202         if test -f "$MERGE_TOOLS_DIR/$tool"
203         then
204                 . "$MERGE_TOOLS_DIR/$tool"
205         elif test -f "$MERGE_TOOLS_DIR/${tool%[0-9]}"
206         then
207                 . "$MERGE_TOOLS_DIR/${tool%[0-9]}"
208         else
209                 setup_user_tool
210                 return $?
211         fi
212
213         # Now let the user override the default command for the tool.  If
214         # they have not done so then this will return 1 which we ignore.
215         setup_user_tool
216
217         if ! list_tool_variants | grep -q "^$tool$"
218         then
219                 return 1
220         fi
221
222         if merge_mode && ! can_merge
223         then
224                 echo "error: '$tool' can not be used to resolve merges" >&2
225                 return 1
226         elif diff_mode && ! can_diff
227         then
228                 echo "error: '$tool' can only be used to resolve merges" >&2
229                 return 1
230         fi
231         return 0
232 }
233
234 get_merge_tool_cmd () {
235         merge_tool="$1"
236         if diff_mode
237         then
238                 git config "difftool.$merge_tool.cmd" ||
239                 git config "mergetool.$merge_tool.cmd"
240         else
241                 git config "mergetool.$merge_tool.cmd"
242         fi
243 }
244
245 trust_exit_code () {
246         if git config --bool "mergetool.$1.trustExitCode"
247         then
248                 :; # OK
249         elif exit_code_trustable
250         then
251                 echo true
252         else
253                 echo false
254         fi
255 }
256
257 initialize_merge_tool () {
258         # Bring tool-specific functions into scope
259         setup_tool "$1" || return 1
260 }
261
262 # Entry point for running tools
263 run_merge_tool () {
264         # If GIT_PREFIX is empty then we cannot use it in tools
265         # that expect to be able to chdir() to its value.
266         GIT_PREFIX=${GIT_PREFIX:-.}
267         export GIT_PREFIX
268
269         merge_tool_path=$(get_merge_tool_path "$1") || exit
270         base_present="$2"
271
272         if merge_mode
273         then
274                 run_merge_cmd "$1"
275         else
276                 run_diff_cmd "$1"
277         fi
278 }
279
280 # Run a either a configured or built-in diff tool
281 run_diff_cmd () {
282         diff_cmd "$1"
283 }
284
285 # Run a either a configured or built-in merge tool
286 run_merge_cmd () {
287         mergetool_trust_exit_code=$(trust_exit_code "$1")
288         if test "$mergetool_trust_exit_code" = "true"
289         then
290                 merge_cmd "$1"
291         else
292                 touch "$BACKUP"
293                 merge_cmd "$1"
294                 check_unchanged
295         fi
296 }
297
298 list_merge_tool_candidates () {
299         if merge_mode
300         then
301                 tools="tortoisemerge"
302         else
303                 tools="kompare"
304         fi
305         if test -n "$DISPLAY"
306         then
307                 if test -n "$GNOME_DESKTOP_SESSION_ID"
308                 then
309                         tools="meld opendiff kdiff3 tkdiff xxdiff $tools"
310                 else
311                         tools="opendiff kdiff3 tkdiff xxdiff meld $tools"
312                 fi
313                 tools="$tools gvimdiff diffuse diffmerge ecmerge"
314                 tools="$tools p4merge araxis bc codecompare"
315                 tools="$tools smerge"
316         fi
317         case "${VISUAL:-$EDITOR}" in
318         *nvim*)
319                 tools="$tools nvimdiff vimdiff emerge"
320                 ;;
321         *vim*)
322                 tools="$tools vimdiff nvimdiff emerge"
323                 ;;
324         *)
325                 tools="$tools emerge vimdiff nvimdiff"
326                 ;;
327         esac
328 }
329
330 show_tool_help () {
331         tool_opt="'git ${TOOL_MODE}tool --tool=<tool>'"
332
333         tab='   '
334         LF='
335 '
336         any_shown=no
337
338         cmd_name=${TOOL_MODE}tool
339         config_tools=$({
340                 diff_mode && list_config_tools difftool "$tab$tab"
341                 list_config_tools mergetool "$tab$tab"
342         } | sort)
343         extra_content=
344         if test -n "$config_tools"
345         then
346                 extra_content="${tab}user-defined:${LF}$config_tools"
347         fi
348
349         show_tool_names 'mode_ok && is_available' "$tab$tab" \
350                 "$tool_opt may be set to one of the following:" \
351                 "No suitable tool for 'git $cmd_name --tool=<tool>' found." \
352                 "$extra_content" &&
353                 any_shown=yes
354
355         show_tool_names 'mode_ok && ! is_available' "$tab$tab" \
356                 "${LF}The following tools are valid, but not currently available:" &&
357                 any_shown=yes
358
359         if test "$any_shown" = yes
360         then
361                 echo
362                 echo "Some of the tools listed above only work in a windowed"
363                 echo "environment. If run in a terminal-only session, they will fail."
364         fi
365         exit 0
366 }
367
368 guess_merge_tool () {
369         list_merge_tool_candidates
370         cat >&2 <<-EOF
371
372         This message is displayed because '$TOOL_MODE.tool' is not configured.
373         See 'git ${TOOL_MODE}tool --tool-help' or 'git help config' for more details.
374         'git ${TOOL_MODE}tool' will now attempt to use one of the following tools:
375         $tools
376         EOF
377
378         # Loop over each candidate and stop when a valid merge tool is found.
379         IFS=' '
380         for tool in $tools
381         do
382                 is_available "$tool" && echo "$tool" && return 0
383         done
384
385         echo >&2 "No known ${TOOL_MODE} tool is available."
386         return 1
387 }
388
389 get_configured_merge_tool () {
390         keys=
391         if diff_mode
392         then
393                 if gui_mode
394                 then
395                         keys="diff.guitool merge.guitool diff.tool merge.tool"
396                 else
397                         keys="diff.tool merge.tool"
398                 fi
399         else
400                 if gui_mode
401                 then
402                         keys="merge.guitool merge.tool"
403                 else
404                         keys="merge.tool"
405                 fi
406         fi
407
408         merge_tool=$(
409                 IFS=' '
410                 for key in $keys
411                 do
412                         selected=$(git config $key)
413                         if test -n "$selected"
414                         then
415                                 echo "$selected"
416                                 return
417                         fi
418                 done)
419
420         if test -n "$merge_tool" && ! valid_tool "$merge_tool"
421         then
422                 echo >&2 "git config option $TOOL_MODE.${gui_prefix}tool set to unknown tool: $merge_tool"
423                 echo >&2 "Resetting to default..."
424                 return 1
425         fi
426         echo "$merge_tool"
427 }
428
429 get_merge_tool_path () {
430         # A merge tool has been set, so verify that it's valid.
431         merge_tool="$1"
432         if ! valid_tool "$merge_tool"
433         then
434                 echo >&2 "Unknown merge tool $merge_tool"
435                 exit 1
436         fi
437         if diff_mode
438         then
439                 merge_tool_path=$(git config difftool."$merge_tool".path ||
440                                   git config mergetool."$merge_tool".path)
441         else
442                 merge_tool_path=$(git config mergetool."$merge_tool".path)
443         fi
444         if test -z "$merge_tool_path"
445         then
446                 merge_tool_path=$(translate_merge_tool_path "$merge_tool")
447         fi
448         if test -z "$(get_merge_tool_cmd "$merge_tool")" &&
449                 ! type "$merge_tool_path" >/dev/null 2>&1
450         then
451                 echo >&2 "The $TOOL_MODE tool $merge_tool is not available as"\
452                          "'$merge_tool_path'"
453                 exit 1
454         fi
455         echo "$merge_tool_path"
456 }
457
458 get_merge_tool () {
459         is_guessed=false
460         # Check if a merge tool has been configured
461         merge_tool=$(get_configured_merge_tool)
462         # Try to guess an appropriate merge tool if no tool has been set.
463         if test -z "$merge_tool"
464         then
465                 merge_tool=$(guess_merge_tool) || exit
466                 is_guessed=true
467         fi
468         echo "$merge_tool"
469         test "$is_guessed" = false
470 }
471
472 mergetool_find_win32_cmd () {
473         executable=$1
474         sub_directory=$2
475
476         # Use $executable if it exists in $PATH
477         if type -p "$executable" >/dev/null 2>&1
478         then
479                 printf '%s' "$executable"
480                 return
481         fi
482
483         # Look for executable in the typical locations
484         for directory in $(env | grep -Ei '^PROGRAM(FILES(\(X86\))?|W6432)=' |
485                 cut -d '=' -f 2- | sort -u)
486         do
487                 if test -n "$directory" && test -x "$directory/$sub_directory/$executable"
488                 then
489                         printf '%s' "$directory/$sub_directory/$executable"
490                         return
491                 fi
492         done
493
494         printf '%s' "$executable"
495 }