Code

git.el: Better handling of subprocess errors.
[git.git] / git-mergetool.sh
1 #!/bin/sh
2 #
3 # This program resolves merge conflicts in git
4 #
5 # Copyright (c) 2006 Theodore Y. Ts'o
6 #
7 # This file is licensed under the GPL v2, or a later version
8 # at the discretion of Junio C Hamano.
9 #
11 USAGE='[--tool=tool] [file to merge] ...'
12 SUBDIRECTORY_OK=Yes
13 OPTIONS_SPEC=
14 . git-sh-setup
15 require_work_tree
16 prefix=$(git rev-parse --show-prefix)
18 # Returns true if the mode reflects a symlink
19 is_symlink () {
20     test "$1" = 120000
21 }
23 local_present () {
24     test -n "$local_mode"
25 }
27 remote_present () {
28     test -n "$remote_mode"
29 }
31 base_present () {
32     test -n "$base_mode"
33 }
35 cleanup_temp_files () {
36     if test "$1" = --save-backup ; then
37         mv -- "$BACKUP" "$path.orig"
38         rm -f -- "$LOCAL" "$REMOTE" "$BASE"
39     else
40         rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
41     fi
42 }
44 describe_file () {
45     mode="$1"
46     branch="$2"
47     file="$3"
49     printf "  {%s}: " "$branch"
50     if test -z "$mode"; then
51         echo "deleted"
52     elif is_symlink "$mode" ; then
53         echo "a symbolic link -> '$(cat "$file")'"
54     else
55         if base_present; then
56             echo "modified"
57         else
58             echo "created"
59         fi
60     fi
61 }
64 resolve_symlink_merge () {
65     while true; do
66         printf "Use (l)ocal or (r)emote, or (a)bort? "
67         read ans
68         case "$ans" in
69             [lL]*)
70                 git checkout-index -f --stage=2 -- "$path"
71                 git add -- "$path"
72                 cleanup_temp_files --save-backup
73                 return
74                 ;;
75             [rR]*)
76                 git checkout-index -f --stage=3 -- "$path"
77                 git add -- "$path"
78                 cleanup_temp_files --save-backup
79                 return
80                 ;;
81             [aA]*)
82                 exit 1
83                 ;;
84             esac
85         done
86 }
88 resolve_deleted_merge () {
89     while true; do
90         if base_present; then
91             printf "Use (m)odified or (d)eleted file, or (a)bort? "
92         else
93             printf "Use (c)reated or (d)eleted file, or (a)bort? "
94         fi
95         read ans
96         case "$ans" in
97             [mMcC]*)
98                 git add -- "$path"
99                 cleanup_temp_files --save-backup
100                 return
101                 ;;
102             [dD]*)
103                 git rm -- "$path" > /dev/null
104                 cleanup_temp_files
105                 return
106                 ;;
107             [aA]*)
108                 exit 1
109                 ;;
110             esac
111         done
114 check_unchanged () {
115     if test "$path" -nt "$BACKUP" ; then
116         status=0;
117     else
118         while true; do
119             echo "$path seems unchanged."
120             printf "Was the merge successful? [y/n] "
121             read answer < /dev/tty
122             case "$answer" in
123                 y*|Y*) status=0; break ;;
124                 n*|N*) status=1; break ;;
125             esac
126         done
127     fi
130 save_backup () {
131     if test "$status" -eq 0; then
132         mv -- "$BACKUP" "$path.orig"
133     fi
136 remove_backup () {
137     if test "$status" -eq 0; then
138         rm "$BACKUP"
139     fi
142 merge_file () {
143     path="$1"
145     f=`git ls-files -u -- "$path"`
146     if test -z "$f" ; then
147         if test ! -f "$path" ; then
148             echo "$path: file not found"
149         else
150             echo "$path: file does not need merging"
151         fi
152         exit 1
153     fi
155     ext="$$$(expr "$path" : '.*\(\.[^/]*\)$')"
156     BACKUP="$path.BACKUP.$ext"
157     LOCAL="$path.LOCAL.$ext"
158     REMOTE="$path.REMOTE.$ext"
159     BASE="$path.BASE.$ext"
161     mv -- "$path" "$BACKUP"
162     cp -- "$BACKUP" "$path"
164     base_mode=`git ls-files -u -- "$path" | awk '{if ($3==1) print $1;}'`
165     local_mode=`git ls-files -u -- "$path" | awk '{if ($3==2) print $1;}'`
166     remote_mode=`git ls-files -u -- "$path" | awk '{if ($3==3) print $1;}'`
168     base_present   && git cat-file blob ":1:$prefix$path" >"$BASE" 2>/dev/null
169     local_present  && git cat-file blob ":2:$prefix$path" >"$LOCAL" 2>/dev/null
170     remote_present && git cat-file blob ":3:$prefix$path" >"$REMOTE" 2>/dev/null
172     if test -z "$local_mode" -o -z "$remote_mode"; then
173         echo "Deleted merge conflict for '$path':"
174         describe_file "$local_mode" "local" "$LOCAL"
175         describe_file "$remote_mode" "remote" "$REMOTE"
176         resolve_deleted_merge
177         return
178     fi
180     if is_symlink "$local_mode" || is_symlink "$remote_mode"; then
181         echo "Symbolic link merge conflict for '$path':"
182         describe_file "$local_mode" "local" "$LOCAL"
183         describe_file "$remote_mode" "remote" "$REMOTE"
184         resolve_symlink_merge
185         return
186     fi
188     echo "Normal merge conflict for '$path':"
189     describe_file "$local_mode" "local" "$LOCAL"
190     describe_file "$remote_mode" "remote" "$REMOTE"
191     printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
192     read ans
194     case "$merge_tool" in
195         kdiff3)
196             if base_present ; then
197                 ("$merge_tool_path" --auto --L1 "$path (Base)" --L2 "$path (Local)" --L3 "$path (Remote)" \
198                     -o "$path" -- "$BASE" "$LOCAL" "$REMOTE" > /dev/null 2>&1)
199             else
200                 ("$merge_tool_path" --auto --L1 "$path (Local)" --L2 "$path (Remote)" \
201                     -o "$path" -- "$LOCAL" "$REMOTE" > /dev/null 2>&1)
202             fi
203             status=$?
204             remove_backup
205             ;;
206         tkdiff)
207             if base_present ; then
208                 "$merge_tool_path" -a "$BASE" -o "$path" -- "$LOCAL" "$REMOTE"
209             else
210                 "$merge_tool_path" -o "$path" -- "$LOCAL" "$REMOTE"
211             fi
212             status=$?
213             save_backup
214             ;;
215         meld|vimdiff)
216             touch "$BACKUP"
217             "$merge_tool_path" -- "$LOCAL" "$path" "$REMOTE"
218             check_unchanged
219             save_backup
220             ;;
221         gvimdiff)
222                 touch "$BACKUP"
223                 "$merge_tool_path" -f -- "$LOCAL" "$path" "$REMOTE"
224                 check_unchanged
225                 save_backup
226                 ;;
227         xxdiff)
228             touch "$BACKUP"
229             if base_present ; then
230                 "$merge_tool_path" -X --show-merged-pane \
231                     -R 'Accel.SaveAsMerged: "Ctrl-S"' \
232                     -R 'Accel.Search: "Ctrl+F"' \
233                     -R 'Accel.SearchForward: "Ctrl-G"' \
234                     --merged-file "$path" -- "$LOCAL" "$BASE" "$REMOTE"
235             else
236                 "$merge_tool_path" -X --show-merged-pane \
237                     -R 'Accel.SaveAsMerged: "Ctrl-S"' \
238                     -R 'Accel.Search: "Ctrl+F"' \
239                     -R 'Accel.SearchForward: "Ctrl-G"' \
240                     --merged-file "$path" -- "$LOCAL" "$REMOTE"
241             fi
242             check_unchanged
243             save_backup
244             ;;
245         opendiff)
246             touch "$BACKUP"
247             if base_present; then
248                 "$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$path" | cat
249             else
250                 "$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$path" | cat
251             fi
252             check_unchanged
253             save_backup
254             ;;
255         ecmerge)
256             touch "$BACKUP"
257             if base_present; then
258                 "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --mode=merge3 --to="$path"
259             else
260                 "$merge_tool_path" "$LOCAL" "$REMOTE" --mode=merge2 --to="$path"
261             fi
262             check_unchanged
263             save_backup
264             ;;
265         emerge)
266             if base_present ; then
267                 "$merge_tool_path" -f emerge-files-with-ancestor-command "$LOCAL" "$REMOTE" "$BASE" "$(basename "$path")"
268             else
269                 "$merge_tool_path" -f emerge-files-command "$LOCAL" "$REMOTE" "$(basename "$path")"
270             fi
271             status=$?
272             save_backup
273             ;;
274     esac
275     if test "$status" -ne 0; then
276         echo "merge of $path failed" 1>&2
277         mv -- "$BACKUP" "$path"
278         exit 1
279     fi
280     git add -- "$path"
281     cleanup_temp_files
284 while test $# != 0
285 do
286     case "$1" in
287         -t|--tool*)
288             case "$#,$1" in
289                 *,*=*)
290                     merge_tool=`expr "z$1" : 'z-[^=]*=\(.*\)'`
291                     ;;
292                 1,*)
293                     usage ;;
294                 *)
295                     merge_tool="$2"
296                     shift ;;
297             esac
298             ;;
299         --)
300             break
301             ;;
302         -*)
303             usage
304             ;;
305         *)
306             break
307             ;;
308     esac
309     shift
310 done
312 valid_tool() {
313         case "$1" in
314                 kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
315                         ;; # happy
316                 *)
317                         return 1
318                         ;;
319         esac
322 init_merge_tool_path() {
323         merge_tool_path=`git config mergetool.$1.path`
324         if test -z "$merge_tool_path" ; then
325                 case "$1" in
326                         emerge)
327                                 merge_tool_path=emacs
328                                 ;;
329                         *)
330                                 merge_tool_path=$1
331                                 ;;
332                 esac
333         fi
337 if test -z "$merge_tool"; then
338     merge_tool=`git config merge.tool`
339     if test -n "$merge_tool" && ! valid_tool "$merge_tool"; then
340             echo >&2 "git config option merge.tool set to unknown tool: $merge_tool"
341             echo >&2 "Resetting to default..."
342             unset merge_tool
343     fi
344 fi
346 if test -z "$merge_tool" ; then
347     if test -n "$DISPLAY"; then
348         merge_tool_candidates="kdiff3 tkdiff xxdiff meld gvimdiff"
349         if test -n "$GNOME_DESKTOP_SESSION_ID" ; then
350             merge_tool_candidates="meld $merge_tool_candidates"
351         fi
352         if test "$KDE_FULL_SESSION" = "true"; then
353             merge_tool_candidates="kdiff3 $merge_tool_candidates"
354         fi
355     fi
356     if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
357         merge_tool_candidates="$merge_tool_candidates emerge"
358     fi
359     if echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
360         merge_tool_candidates="$merge_tool_candidates vimdiff"
361     fi
362     merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
363     echo "merge tool candidates: $merge_tool_candidates"
364     for i in $merge_tool_candidates; do
365         init_merge_tool_path $i
366         if type "$merge_tool_path" > /dev/null 2>&1; then
367             merge_tool=$i
368             break
369         fi
370     done
371     if test -z "$merge_tool" ; then
372         echo "No known merge resolution program available."
373         exit 1
374     fi
375 else
376     if ! valid_tool "$merge_tool"; then
377         echo >&2 "Unknown merge_tool $merge_tool"
378         exit 1
379     fi
381     init_merge_tool_path "$merge_tool"
383     if ! type "$merge_tool_path" > /dev/null 2>&1; then
384         echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
385         exit 1
386     fi
387 fi
390 if test $# -eq 0 ; then
391         files=`git ls-files -u | sed -e 's/^[^  ]*      //' | sort -u`
392         if test -z "$files" ; then
393                 echo "No files need merging"
394                 exit 0
395         fi
396         echo Merging the files: "$files"
397         git ls-files -u |
398         sed -e 's/^[^   ]*      //' |
399         sort -u |
400         while IFS= read i
401         do
402                 printf "\n"
403                 merge_file "$i" < /dev/tty > /dev/tty
404         done
405 else
406         while test $# -gt 0; do
407                 printf "\n"
408                 merge_file "$1"
409                 shift
410         done
411 fi
412 exit 0