difftool launchers: better behavior when amending commits
[git-cola.git] / bin / git-difftool
blobf0880a1e7ff8a70b27e691b6300e548d224d6966
1 #!/bin/sh
3 # This program uses merge tools to stage and compare commits
5 # Copyright (c) 2008 David Aguilar
7 # Adapted from git-mergetool.sh
8 # Copyright (c) 2006 Theodore Y. Ts'o
10 # This file is licensed under the GPL v2, or a later version
12 USAGE='
13 [--tool=tool] [--no-prompt]
14 [--commit=ref] [--start=ref --end=ref]
15 [--] [file to view] ...'
16 SUBDIRECTORY_OK=Yes
17 OPTIONS_SPEC=
18 . git-sh-setup
19 require_work_tree
20 cd_to_toplevel
22 keep_backup_mode="$(git config --bool merge.keepBackup || echo true)"
23 PREFIX=.git-difftool."$$"."$(date +%N)".
24 FILEDIR=
26 keep_backup () {
27 # whether to keep the .orig file
28 test "$keep_backup_mode" = "true"
31 parse_arg () {
32 # parses --foo=BAR
33 expr "z$1" : 'z-[^=]*=\(.*\)'
36 index_present () {
37 # does the index contain staged changes?
38 test -n "$index_mode"
41 modified_present () {
42 # are there changes in the requested comparison?
43 test -n "$modified_mode"
46 commitish_present () {
47 # are we comparing against an arbitrary commit?
48 test -n "$commitish"
51 should_prompt () {
52 # are we running interactively?
53 ! test -n "$no_prompt"
56 use_rev_range () {
57 # are we using --start=REF and --end=REF
58 test -n "$start" && test -n "$end"
61 merge_dir_missing () {
62 # does dirname($MERGED) exist in the work tree?
63 test -n "$non_existant_dir"
66 base_present () {
67 # whether we have 3 things to compare (index, worktree, other)
68 index_present && modified_present && ! use_rev_range
71 modified_files () {
72 # returns the appopriate list of differences based on the mode
73 if use_rev_range; then
74 git diff --name-only "$start".."$end" -- "$@" 2>/dev/null
75 elif commitish_present; then
76 git diff --name-only "$commitish" -- "$@" 2>/dev/null
77 else
78 git diff --name-only -- "$@" 2>/dev/null
82 staged_files() {
83 # returns any staged changes
84 git diff --name-only --cached "$@" 2>/dev/null
87 cleanup_temp_files () {
88 # removes temporary files
89 if test -n "$MERGED"; then
90 if keep_backup && test "$MERGED" -nt "$BACKUP"; then
91 test -f "$BACKUP" && mv -- "$BACKUP" "$MERGED.orig"
92 rm -f -- "$LOCAL" "$REMOTE" "$BASE"
93 else
94 rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
96 test -n "$MERGEDIR" && rmdir -p "$MERGEDIR"
100 sigint_handler () {
101 echo
102 cleanup_temp_files
103 exit 1
106 merge_file () {
107 # prepares temporary files and launches the appropriate merge tool
108 MERGED="$1"
110 modified_mode=$(modified_files "$MERGED")
111 index_mode=$(staged_files "$MERGED")
113 if ! modified_present && use_rev_range; then
114 echo "$MERGED: no changes between '$start' and '$end'."
115 exit 1
116 elif ! modified_present && ! index_present; then
117 if ! test -f "$MERGED"; then
118 echo "$MERGED: file not found"
119 else
120 echo "$MERGED: file is unchanged."
122 exit 1
125 # handle comparing a file that doesn't exist in the current checkout
126 MERGEDIR=$(dirname "$MERGED")
127 non_existant_dir=
128 test -d "$MERGEDIR" || non_existant_dir=true
129 if merge_dir_missing; then
130 mkdir -p -- "$MERGEDIR"
131 else
132 MERGEDIR=
135 ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
136 BACKUP="./$MERGED.BACKUP.$ext"
137 test -f "$MERGED" && cp -- "$MERGED" "$BACKUP"
139 if use_rev_range; then
140 # we're comparing two arbitrary commits
141 BASE="./$MERGED.CURRENT.$ext"
142 LOCAL="./$MERGED.START.$ext"
143 REMOTE="./$MERGED.END.$ext"
144 base=current
145 local=start
146 remote=end
147 touch "$BASE"
148 touch "$LOCAL"
149 touch "$REMOTE"
151 # detects renames.. sweet!
152 oldname=$(git diff --follow "$start".."$end" -- "$MERGED" |
153 head -n9 |
154 grep '^rename from' |
155 sed -e 's/rename from //')
156 startname="$MERGED"
157 test -n "$oldname" && startname="$oldname"
159 if ! git show "$start":"$startname" > "$LOCAL" 2>/dev/null; then
160 if should_prompt; then
161 echo
162 echo -n "Warning: "
163 echo -n "'$startname' does not exist at $start."
165 else
166 cp -- "$LOCAL" "$BASE"
168 if ! git show "$end":"$MERGED" > "$REMOTE" 2>/dev/null; then
169 if should_prompt; then
170 echo
171 echo -n "Warning: "
172 echo -n "'$MERGED' does not exist at $end."
174 else
175 ! test -f "$BASE" && cp -- "$REMOTE" "$BASE"
178 # $BASE could be used by custom mergetool commands
179 if test -f "$MERGED"; then
180 cp -- "$MERGED" "$BASE"
182 else
183 # We're either comparing against the index or an
184 # arbitrary commit.
185 # The goal is to re-use existing mergetool.$tool.cmd
186 # configurations so we provide $BASE $LOCAL and $REMOTE
187 if commitish_present; then
188 HEAD=OTHER
189 local=Other
190 else
191 HEAD=HEAD
192 local=Index
194 base=${commitish-HEAD}
195 remote=Current
196 BASE="./$MERGED.$HEAD.$ext"
197 LOCAL="./$MERGED.INDEX.$ext"
198 REMOTE="./$MERGED.CURRENT.$ext"
199 touch "$BASE"
200 touch "$LOCAL"
201 touch "$REMOTE"
202 if ! git show "$base":"$MERGED" > "$BASE" 2>&1; then
203 echo
204 echo -n "Warning: "
205 echo -n "'$MERGED' does not exist at $base."
206 else
207 cp "$BASE" "$LOCAL"
208 if commitish_present; then
209 rm -f "$LOCAL"
210 LOCAL="$BASE"
213 # If changes are present in the index use them as $LOCAL
214 # but only if $MERGED exists at $base
215 if ! commitish_present; then
216 git checkout-index --prefix="$PREFIX" "$MERGED"
217 if test -f "$PREFIX""$MERGED"; then
218 mv -- "$PREFIX""$MERGED" "$LOCAL"
219 tmpdir=$(dirname "$PREFIX""$MERGED")
220 test -d "$tmpdir" &&
221 test "$tmpdir" != "." &&
222 rmdir -p "$tmpdir"
223 else
224 index_mode=
226 else
227 index_mode=
229 if test -f "$MERGED"; then
230 cp -- "$MERGED" "$REMOTE"
234 # ensure that we clean up after ourselves
235 trap sigint_handler SIGINT
237 if should_prompt; then
238 printf "\nEditing: '$MERGED'\n"
239 printf "Hit return to launch '%s': " "$merge_tool"
240 read ans
243 case "$merge_tool" in
244 kdiff3)
245 basename=$(basename "$MERGED")
246 if base_present; then
248 "$merge_tool_path" --auto \
249 --L1 "($base) $basename" \
250 --L2 "($local) $basename" \
251 --L3 "($remote) $basename" \
252 -o "$MERGED" "$BASE" "$LOCAL" "$REMOTE" \
253 > /dev/null 2>&1
255 else
257 "$merge_tool_path" --auto \
258 --L1 "($local) $basename" \
259 --L2 "($remote) $basename" \
260 -o "$MERGED" "$LOCAL" "$REMOTE" \
261 > /dev/null 2>&1
266 tkdiff)
267 if base_present; then
268 "$merge_tool_path" \
269 -a "$BASE" \
270 -o "$MERGED" "$LOCAL" "$REMOTE"
271 else
272 "$merge_tool_path" \
273 -o "$MERGED" "$LOCAL" "$REMOTE"
277 meld)
278 "$merge_tool_path" "$LOCAL" "$MERGED"
281 vimdiff)
282 if base_present; then
283 "$merge_tool_path" "$BASE" "$LOCAL" "$MERGED"
284 else
285 "$merge_tool_path" "$LOCAL" "$MERGED"
289 gvimdiff)
290 if base_present; then
291 "$merge_tool_path" -f "$BASE" "$LOCAL" "$MERGED"
292 else
293 "$merge_tool_path" -f "$LOCAL" "$MERGED"
297 xxdiff)
298 if base_present; then
299 "$merge_tool_path" -X --show-merged-pane \
300 -R 'Accel.SaveAsMerged: "Ctrl-S"' \
301 -R 'Accel.Search: "Ctrl+F"' \
302 -R 'Accel.SearchForward: "Ctrl-G"' \
303 --merged-file "$MERGED" \
304 "$BASE" "$LOCAL" "$REMOTE"
305 else
306 "$merge_tool_path" -X --show-merged-pane \
307 -R 'Accel.SaveAsMerged: "Ctrl-S"' \
308 -R 'Accel.Search: "Ctrl+F"' \
309 -R 'Accel.SearchForward: "Ctrl-G"' \
310 --merged-file "$MERGED" \
311 "$LOCAL" "$REMOTE"
315 opendiff)
316 if base_present; then
317 "$merge_tool_path" "$LOCAL" "$REMOTE" \
318 -ancestor "$BASE" \
319 -merge "$MERGED" | cat
320 else
321 "$merge_tool_path" "$LOCAL" "$REMOTE" \
322 -merge "$MERGED" | cat
326 ecmerge)
327 if base_present; then
328 "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" \
329 --default --mode=merge3 --to="$MERGED"
330 else
331 "$merge_tool_path" "$LOCAL" "$REMOTE" \
332 --default --mode=merge2 --to="$MERGED"
336 emerge)
337 if base_present; then
338 "$merge_tool_path" \
339 -f emerge-files-with-ancestor-command \
340 "$LOCAL" "$REMOTE" "$BASE" \
341 "$(basename "$MERGED")"
342 else
343 "$merge_tool_path" -f emerge-files-command \
344 "$LOCAL" "$REMOTE" "$(basename "$MERGED")"
348 if test -n "$merge_tool_cmd"; then
349 if test "$merge_tool_trust_exit_code" = "false"; then
350 ( eval $merge_tool_cmd )
351 else
352 ( eval $merge_tool_cmd )
356 esac
357 cleanup_temp_files
360 while test $# != 0
362 case "$1" in
363 -t|--tool*)
364 case "$#,$1" in
365 *,*=*)
366 merge_tool=$(parse_arg "$1")
367 shift
369 1,*)
370 usage
373 shift
374 merge_tool="$1"
375 shift
377 esac
379 -c|--commit*)
380 case "$#,$1" in
381 *,*=*)
382 commitish=$(parse_arg "$1")
383 shift
385 1,*)
386 usage
389 shift
390 commitish="$1"
391 shift
393 esac
395 -s|--start*)
396 case "$#,$1" in
397 *,*=*)
398 start=$(parse_arg "$1")
399 shift
401 1,*)
402 usage
405 shift
406 start="$1"
407 shift
409 esac
411 -e|--end*)
412 case "$#,$1" in
413 *,*=*)
414 end=$(parse_arg "$1")
415 shift
417 1,*)
418 usage
421 shift
422 end="$1"
423 shift
425 esac
427 --no-prompt)
428 no_prompt=true
429 shift
432 shift
433 break
436 usage
439 break
441 esac
442 done
444 valid_custom_tool() {
445 merge_tool_cmd="$(git config mergetool.$1.cmd)"
446 test -n "$merge_tool_cmd"
449 valid_tool() {
450 case "$1" in
451 kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
452 ;; # happy
454 if ! valid_custom_tool "$1"
455 then
456 return 1
459 esac
462 init_merge_tool_path() {
463 merge_tool_path=$(git config mergetool."$1".path)
464 if test -z "$merge_tool_path"; then
465 case "$1" in
466 emerge)
467 merge_tool_path=emacs
470 merge_tool_path="$1"
472 esac
477 if test -z "$merge_tool"; then
478 merge_tool=$(git config merge.tool)
479 if test -n "$merge_tool" && ! valid_tool "$merge_tool"; then
480 echo >&2 "git config option merge.tool set to unknown tool: $merge_tool"
481 echo >&2 "Resetting to default..."
482 unset merge_tool
486 if test -z "$merge_tool"; then
487 if test -n "$DISPLAY"; then
488 merge_tool_candidates="kdiff3 tkdiff xxdiff meld gvimdiff"
489 if test -n "$GNOME_DESKTOP_SESSION_ID"; then
490 merge_tool_candidates="meld $merge_tool_candidates"
492 if test "$KDE_FULL_SESSION" = "true"; then
493 merge_tool_candidates="kdiff3 $merge_tool_candidates"
497 if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
498 merge_tool_candidates="$merge_tool_candidates emerge"
501 if echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
502 merge_tool_candidates="$merge_tool_candidates vimdiff"
505 merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
506 echo "merge tool candidates: $merge_tool_candidates"
508 for i in $merge_tool_candidates
510 init_merge_tool_path $i
511 if type "$merge_tool_path" > /dev/null 2>&1; then
512 merge_tool=$i
513 break
515 done
517 if test -z "$merge_tool" ; then
518 echo "No known merge resolution program available."
519 exit 1
522 else
523 if ! valid_tool "$merge_tool"; then
524 echo >&2 "Unknown merge tool $merge_tool"
525 exit 1
528 init_merge_tool_path "$merge_tool"
530 if test -z "$merge_tool_cmd" && ! type "$merge_tool_path" > /dev/null 2>&1; then
531 echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
532 exit 1
535 if ! test -z "$merge_tool_cmd"; then
536 merge_tool_trust_exit_code="$(git config --bool mergetool.$merge_tool.trustExitCode || echo false)"
541 if test $# -eq 0; then
542 use_index=0
543 files=$(modified_files)
545 if test -z "$files"; then
546 use_index=1
547 files=$(staged_files)
550 if test -z "$files"; then
551 echo "No modified files exist."
552 exit 1
556 if test $use_index -eq 0; then
557 modified_files |
558 while IFS= read i
560 merge_file "$i" < /dev/tty > /dev/tty
561 done
562 elif ! use_rev_range; then
563 staged_files |
564 while IFS= read i
566 merge_file "$i" < /dev/tty > /dev/tty
567 done
568 else
569 echo "Nothing to compare."
570 exit 1
572 else
573 while test $# -gt 0
575 merge_file "$1"
576 shift
577 done
579 exit 0