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