1# git-mergetool--lib is a shell library for common merge tool functions
2
3: ${MERGE_TOOLS_DIR=$(git --exec-path)/mergetools}
4
5IFS='
6'
7
8mode_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
20is_available () {
21	merge_tool_path=$(translate_merge_tool_path "$1") &&
22	type "$merge_tool_path" >/dev/null 2>&1
23}
24
25list_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
39show_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
92diff_mode () {
93	test "$TOOL_MODE" = diff
94}
95
96merge_mode () {
97	test "$TOOL_MODE" = merge
98}
99
100gui_mode () {
101	test "$GIT_MERGETOOL_GUI" = true
102}
103
104translate_merge_tool_path () {
105	echo "$1"
106}
107
108check_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
126valid_tool () {
127	setup_tool "$1" && return 0
128	cmd=$(get_merge_tool_cmd "$1")
129	test -n "$cmd"
130}
131
132setup_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
149setup_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
234get_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
245trust_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
257initialize_merge_tool () {
258	# Bring tool-specific functions into scope
259	setup_tool "$1" || return 1
260}
261
262# Entry point for running tools
263run_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
281run_diff_cmd () {
282	diff_cmd "$1"
283}
284
285# Run a either a configured or built-in merge tool
286run_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
298list_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
330show_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
368guess_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
389get_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
429get_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
458get_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
472mergetool_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}
496