git-difftool: detect and handle renamed files
[git-cola.git] / bin / git-difftool
blobf548c0e320d3c810b637bd55d2a584c6073ca3d3
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='[--tool=tool] [--commit=ref] [--start=ref --end=ref] [--no-prompt] [file to merge] ...'
13 SUBDIRECTORY_OK=Yes
14 OPTIONS_SPEC=
15 . git-sh-setup
16 require_work_tree
17 cd_to_toplevel
19 keep_backup_mode="$(git config --bool merge.keepBackup || echo true)"
20 PREFIX=.git-difftool."$$"."$(date +%N)".
21 FILEDIR=
23 keep_backup () {
24 # whether to keep the .orig file
25 test "$keep_backup_mode" = "true"
28 parse_arg () {
29 # parses --foo=BAR
30 expr "z$1" : 'z-[^=]*=\(.*\)'
33 index_present () {
34 # does the index contain staged changes?
35 test -n "$index_mode"
38 modified_present () {
39 # are there changes in the requested comparison?
40 test -n "$modified_mode"
43 commitish_present () {
44 # are we comparing against an arbitrary commit?
45 test -n "$commitish"
48 should_prompt () {
49 # are we running interactively?
50 ! test -n "$no_prompt"
53 use_rev_range () {
54 # are we using --start=REF and --end=REF
55 test -n "$start" && test -n "$end"
58 merge_dir_missing () {
59 # does dirname($MERGED) exist in the work tree?
60 test -n "$non_existant_dir"
63 merge_three () {
64 # whether we have 3 things to compare (index, worktree, other)
65 index_present && modified_present && ! use_rev_range
68 modified_files () {
69 # returns the appopriate list of differences based on the mode
70 if use_rev_range; then
71 git diff --name-only "$start".."$end" -- "$@" 2>/dev/null
72 elif commitish_present; then
73 git diff --name-only "$commitish" -- "$@" 2>/dev/null
74 else
75 git diff --name-only -- "$@" 2>/dev/null
79 staged_files() {
80 # returns any staged changes
81 git diff --name-only --cached "$@" 2>/dev/null
84 cleanup_temp_files () {
85 # removes temporary files
86 if test -n "$MERGED"; then
87 if keep_backup && test "$MERGED" -nt "$BACKUP"; then
88 test -f "$BACKUP" && mv -- "$BACKUP" "$MERGED.orig"
89 rm -f -- "$LOCAL" "$REMOTE" "$BASE"
90 else
91 rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
93 test -n "$MERGEDIR" && rmdir -p "$MERGEDIR"
97 sigint_handler () {
98 echo
99 cleanup_temp_files
100 exit 1
103 merge_file () {
104 # prepares temporary files and launches the appropriate merge tool
105 MERGED="$1"
107 modified_mode=$(modified_files "$MERGED")
108 index_mode=$(staged_files "$MERGED")
110 if ! modified_present && use_rev_range; then
111 echo "$MERGED: no changes between '$start' and '$end'."
112 exit 1
113 elif ! modified_present && ! index_present; then
114 if ! test -f "$MERGED"; then
115 echo "$MERGED: file not found"
116 else
117 echo "$MERGED: file is unchanged."
119 exit 1
122 # handle comparing a file that doesn't exist in the current checkout
123 MERGEDIR=$(dirname "$MERGED")
124 non_existant_dir=
125 test -d "$MERGEDIR" || non_existant_dir=true
126 if merge_dir_missing; then
127 mkdir -p -- "$MERGEDIR"
128 else
129 MERGEDIR=
132 ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
133 BACKUP="./$MERGED.BACKUP.$ext"
134 test -f "$MERGED" && cp -- "$MERGED" "$BACKUP"
136 if use_rev_range; then
137 # we're comparing two arbitrary commits
138 BASE="./$MERGED.LOCAL.$ext"
139 LOCAL="./$MERGED.START.$ext"
140 REMOTE="./$MERGED.END.$ext"
141 base=local
142 local=start
143 remote=end
145 # detects renames.. sweet!
146 oldname=$(git diff --follow "$start".."$end" -- "$MERGED" |
147 head -n9 |
148 grep '^rename from' |
149 sed -e 's/rename from //')
150 startname="$MERGED"
151 test -n "$oldname" && startname="$oldname"
153 if ! git show "$start":"$startname" > "$LOCAL" 2>/dev/null; then
154 if should_prompt; then
155 echo
156 echo -n "Warning: "
157 echo -n "'$startname' does not exist at $start."
159 touch "$LOCAL"
161 if ! git show "$end":"$MERGED" > "$REMOTE" 2>/dev/null; then
162 if should_prompt; then
163 echo
164 echo -n "Warning: "
165 echo -n "'$MERGED' does not exist at $end."
167 touch "$REMOTE"
170 # $BASE could be used by custom mergetool commands, so provide
171 # it. $MERGED might not exist in the worktree, start or end so
172 # commit so check in that order.
173 if test -f "$MERGED"; then
174 cp -- "$MERGED" "$BASE"
175 elif test -f "$REMOTE"; then
176 cp -- "$REMOTE" "$BASE"
177 elif test -f "$LOCAL"; then
178 cp -- "$LOCAL" "$BASE"
179 else
180 touch "$BASE"
182 else
183 # we're either comparing against the index or an
184 # arbitrary commit
185 base=index
186 local=local
187 remote=${commitish-HEAD}
188 HEAD=HEAD
189 commitish_present && HEAD=OTHER
190 LOCAL="./$MERGED.LOCAL.$ext"
191 REMOTE="./$MERGED.$HEAD.$ext"
192 BASE="./$MERGED.INDEX.$ext"
193 git show "$remote":"$MERGED" > "$REMOTE" 2>&1
194 # If changes are present in the index use them as $BASE
195 if test -f "$MERGED"; then
196 cp -- "$MERGED" "$LOCAL"
197 git checkout-index --prefix="$PREFIX" "$MERGED"
198 mv -- "$PREFIX""$MERGED" "$BASE"
199 tmpdir=$(dirname "$PREFIX""$MERGED")
200 test -d "$tmpdir" && rmdir -p "$tmpdir"
201 else
202 # $MERGED doesn't exist here...
203 touch "$LOCAL"
204 touch "$BASE"
208 # ensure that we clean up after ourselves
209 trap sigint_handler SIGINT
211 if should_prompt; then
212 printf "\nEditing: '$MERGED'\n"
213 printf "Hit return to launch '%s': " "$merge_tool"
214 read ans
217 case "$merge_tool" in
218 kdiff3)
219 basename=$(basename "$MERGED")
220 if merge_three; then
222 "$merge_tool_path" --auto \
223 --L1 "[$base] $basename" \
224 --L2 "[$local] $basename" \
225 --L3 "[$remote] $basename" \
226 -o "$MERGED" "$BASE" "$LOCAL" "$REMOTE" \
227 > /dev/null 2>&1
229 else
231 "$merge_tool_path" --auto \
232 --L1 "[$local] $basename" \
233 --L2 "[$remote] $basename" \
234 -o "$MERGED" "$LOCAL" "$REMOTE" \
235 > /dev/null 2>&1
240 tkdiff)
241 if merge_three; then
242 "$merge_tool_path" \
243 -a "$BASE" \
244 -o "$MERGED" "$LOCAL" "$REMOTE"
245 else
246 "$merge_tool_path" \
247 -o "$MERGED" "$LOCAL" "$REMOTE"
251 meld|vimdiff)
252 "$merge_tool_path" "$LOCAL" "$MERGED" "$REMOTE"
255 gvimdiff)
256 "$merge_tool_path" -f "$LOCAL" "$MERGED" "$REMOTE"
259 xxdiff)
260 if merge_three; then
261 "$merge_tool_path" -X --show-merged-pane \
262 -R 'Accel.SaveAsMerged: "Ctrl-S"' \
263 -R 'Accel.Search: "Ctrl+F"' \
264 -R 'Accel.SearchForward: "Ctrl-G"' \
265 --merged-file "$MERGED" \
266 "$LOCAL" "$BASE" "$REMOTE"
267 else
268 "$merge_tool_path" -X --show-merged-pane \
269 -R 'Accel.SaveAsMerged: "Ctrl-S"' \
270 -R 'Accel.Search: "Ctrl+F"' \
271 -R 'Accel.SearchForward: "Ctrl-G"' \
272 --merged-file "$MERGED" \
273 "$LOCAL" "$REMOTE"
277 opendiff)
278 if merge_three; then
279 "$merge_tool_path" "$LOCAL" "$REMOTE" \
280 -ancestor "$BASE" \
281 -merge "$MERGED" | cat
282 else
283 "$merge_tool_path" "$LOCAL" "$REMOTE" \
284 -merge "$MERGED" | cat
288 ecmerge)
289 if merge_three; then
290 "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" \
291 --default --mode=merge3 --to="$MERGED"
292 else
293 "$merge_tool_path" "$LOCAL" "$REMOTE" \
294 --default --mode=merge2 --to="$MERGED"
298 emerge)
299 if merge_three; then
300 "$merge_tool_path" \
301 -f emerge-files-with-ancestor-command \
302 "$LOCAL" "$REMOTE" "$BASE" \
303 "$(basename "$MERGED")"
304 else
305 "$merge_tool_path" -f emerge-files-command \
306 "$LOCAL" "$REMOTE" "$(basename "$MERGED")"
310 if test -n "$merge_tool_cmd"; then
311 if test "$merge_tool_trust_exit_code" = "false"; then
312 ( eval $merge_tool_cmd )
313 else
314 ( eval $merge_tool_cmd )
318 esac
319 cleanup_temp_files
322 while test $# != 0
324 case "$1" in
325 -t|--tool*)
326 case "$#,$1" in
327 *,*=*)
328 merge_tool=$(parse_arg "$1")
329 shift
331 1,*)
332 usage
335 shift
336 merge_tool="$1"
337 shift
339 esac
341 -c|--commit*)
342 case "$#,$1" in
343 *,*=*)
344 commitish=$(parse_arg "$1")
345 shift
347 1,*)
348 usage
351 shift
352 commitish="$1"
353 shift
355 esac
357 -s|--start*)
358 case "$#,$1" in
359 *,*=*)
360 start=$(parse_arg "$1")
361 shift
363 1,*)
364 usage
367 shift
368 start="$1"
369 shift
371 esac
373 -e|--end*)
374 case "$#,$1" in
375 *,*=*)
376 end=$(parse_arg "$1")
377 shift
379 1,*)
380 usage
383 shift
384 end="$1"
385 shift
387 esac
389 --no-prompt)
390 no_prompt=true
391 shift
394 shift
395 break
398 usage
401 break
403 esac
404 done
406 valid_custom_tool() {
407 merge_tool_cmd="$(git config mergetool.$1.cmd)"
408 test -n "$merge_tool_cmd"
411 valid_tool() {
412 case "$1" in
413 kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
414 ;; # happy
416 if ! valid_custom_tool "$1"
417 then
418 return 1
421 esac
424 init_merge_tool_path() {
425 merge_tool_path=$(git config mergetool."$1".path)
426 if test -z "$merge_tool_path"; then
427 case "$1" in
428 emerge)
429 merge_tool_path=emacs
432 merge_tool_path="$1"
434 esac
439 if test -z "$merge_tool"; then
440 merge_tool=$(git config merge.tool)
441 if test -n "$merge_tool" && ! valid_tool "$merge_tool"; then
442 echo >&2 "git config option merge.tool set to unknown tool: $merge_tool"
443 echo >&2 "Resetting to default..."
444 unset merge_tool
448 if test -z "$merge_tool"; then
449 if test -n "$DISPLAY"; then
450 merge_tool_candidates="kdiff3 tkdiff xxdiff meld gvimdiff"
451 if test -n "$GNOME_DESKTOP_SESSION_ID"; then
452 merge_tool_candidates="meld $merge_tool_candidates"
454 if test "$KDE_FULL_SESSION" = "true"; then
455 merge_tool_candidates="kdiff3 $merge_tool_candidates"
459 if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
460 merge_tool_candidates="$merge_tool_candidates emerge"
463 if echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
464 merge_tool_candidates="$merge_tool_candidates vimdiff"
467 merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
468 echo "merge tool candidates: $merge_tool_candidates"
470 for i in $merge_tool_candidates
472 init_merge_tool_path $i
473 if type "$merge_tool_path" > /dev/null 2>&1; then
474 merge_tool=$i
475 break
477 done
479 if test -z "$merge_tool" ; then
480 echo "No known merge resolution program available."
481 exit 1
484 else
485 if ! valid_tool "$merge_tool"; then
486 echo >&2 "Unknown merge tool $merge_tool"
487 exit 1
490 init_merge_tool_path "$merge_tool"
492 if test -z "$merge_tool_cmd" && ! type "$merge_tool_path" > /dev/null 2>&1; then
493 echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
494 exit 1
497 if ! test -z "$merge_tool_cmd"; then
498 merge_tool_trust_exit_code="$(git config --bool mergetool.$merge_tool.trustExitCode || echo false)"
503 if test $# -eq 0; then
504 use_index=0
505 files=$(modified_files)
507 if test -z "$files"; then
508 use_index=1
509 files=$(staged_files)
512 if test -z "$files"; then
513 echo "No modified files exist."
514 exit 1
518 if test $use_index -eq 0; then
519 modified_files |
520 while IFS= read i
522 merge_file "$i" < /dev/tty > /dev/tty
523 done
524 elif ! use_rev_range; then
525 staged_files |
526 while IFS= read i
528 merge_file "$i" < /dev/tty > /dev/tty
529 done
530 else
531 echo "Nothing to compare."
532 exit 1
534 else
535 while test $# -gt 0
537 merge_file "$1"
538 shift
539 done
541 exit 0