Merge branch 'jc/mergetool-tool-help' into maint
[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 #
10
11 USAGE='[--tool=tool] [--tool-help] [-y|--no-prompt|--prompt] [file to merge] ...'
12 SUBDIRECTORY_OK=Yes
13 OPTIONS_SPEC=
14 TOOL_MODE=merge
15 . git-sh-setup
16 . git-mergetool--lib
17 require_work_tree
18
19 # Returns true if the mode reflects a symlink
20 is_symlink () {
21     test "$1" = 120000
22 }
23
24 is_submodule () {
25     test "$1" = 160000
26 }
27
28 local_present () {
29     test -n "$local_mode"
30 }
31
32 remote_present () {
33     test -n "$remote_mode"
34 }
35
36 base_present () {
37     test -n "$base_mode"
38 }
39
40 cleanup_temp_files () {
41     if test "$1" = --save-backup ; then
42         rm -rf -- "$MERGED.orig"
43         test -e "$BACKUP" && mv -- "$BACKUP" "$MERGED.orig"
44         rm -f -- "$LOCAL" "$REMOTE" "$BASE"
45     else
46         rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
47     fi
48 }
49
50 describe_file () {
51     mode="$1"
52     branch="$2"
53     file="$3"
54
55     printf "  {%s}: " "$branch"
56     if test -z "$mode"; then
57         echo "deleted"
58     elif is_symlink "$mode" ; then
59         echo "a symbolic link -> '$(cat "$file")'"
60     elif is_submodule "$mode" ; then
61         echo "submodule commit $file"
62     else
63         if base_present; then
64             echo "modified file"
65         else
66             echo "created file"
67         fi
68     fi
69 }
70
71
72 resolve_symlink_merge () {
73     while true; do
74         printf "Use (l)ocal or (r)emote, or (a)bort? "
75         read ans || return 1
76         case "$ans" in
77             [lL]*)
78                 git checkout-index -f --stage=2 -- "$MERGED"
79                 git add -- "$MERGED"
80                 cleanup_temp_files --save-backup
81                 return 0
82                 ;;
83             [rR]*)
84                 git checkout-index -f --stage=3 -- "$MERGED"
85                 git add -- "$MERGED"
86                 cleanup_temp_files --save-backup
87                 return 0
88                 ;;
89             [aA]*)
90                 return 1
91                 ;;
92             esac
93         done
94 }
95
96 resolve_deleted_merge () {
97     while true; do
98         if base_present; then
99             printf "Use (m)odified or (d)eleted file, or (a)bort? "
100         else
101             printf "Use (c)reated or (d)eleted file, or (a)bort? "
102         fi
103         read ans || return 1
104         case "$ans" in
105             [mMcC]*)
106                 git add -- "$MERGED"
107                 cleanup_temp_files --save-backup
108                 return 0
109                 ;;
110             [dD]*)
111                 git rm -- "$MERGED" > /dev/null
112                 cleanup_temp_files
113                 return 0
114                 ;;
115             [aA]*)
116                 return 1
117                 ;;
118             esac
119         done
120 }
121
122 resolve_submodule_merge () {
123     while true; do
124         printf "Use (l)ocal or (r)emote, or (a)bort? "
125         read ans || return 1
126         case "$ans" in
127             [lL]*)
128                 if ! local_present; then
129                     if test -n "$(git ls-tree HEAD -- "$MERGED")"; then
130                         # Local isn't present, but it's a subdirectory
131                         git ls-tree --full-name -r HEAD -- "$MERGED" | git update-index --index-info || exit $?
132                     else
133                         test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
134                         git update-index --force-remove "$MERGED"
135                         cleanup_temp_files --save-backup
136                     fi
137                 elif is_submodule "$local_mode"; then
138                     stage_submodule "$MERGED" "$local_sha1"
139                 else
140                     git checkout-index -f --stage=2 -- "$MERGED"
141                     git add -- "$MERGED"
142                 fi
143                 return 0
144                 ;;
145             [rR]*)
146                 if ! remote_present; then
147                     if test -n "$(git ls-tree MERGE_HEAD -- "$MERGED")"; then
148                         # Remote isn't present, but it's a subdirectory
149                         git ls-tree --full-name -r MERGE_HEAD -- "$MERGED" | git update-index --index-info || exit $?
150                     else
151                         test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
152                         git update-index --force-remove "$MERGED"
153                     fi
154                 elif is_submodule "$remote_mode"; then
155                     ! is_submodule "$local_mode" && test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
156                     stage_submodule "$MERGED" "$remote_sha1"
157                 else
158                     test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
159                     git checkout-index -f --stage=3 -- "$MERGED"
160                     git add -- "$MERGED"
161                 fi
162                 cleanup_temp_files --save-backup
163                 return 0
164                 ;;
165             [aA]*)
166                 return 1
167                 ;;
168             esac
169         done
170 }
171
172 stage_submodule () {
173     path="$1"
174     submodule_sha1="$2"
175     mkdir -p "$path" || die "fatal: unable to create directory for module at $path"
176     # Find $path relative to work tree
177     work_tree_root=$(cd_to_toplevel && pwd)
178     work_rel_path=$(cd "$path" && GIT_WORK_TREE="${work_tree_root}" git rev-parse --show-prefix)
179     test -n "$work_rel_path" || die "fatal: unable to get path of module $path relative to work tree"
180     git update-index --add --replace --cacheinfo 160000 "$submodule_sha1" "${work_rel_path%/}" || die
181 }
182
183 checkout_staged_file () {
184     tmpfile=$(expr \
185             "$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" \
186             : '\([^     ]*\)    ')
187
188     if test $? -eq 0 -a -n "$tmpfile" ; then
189         mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3"
190     else
191         >"$3"
192     fi
193 }
194
195 merge_file () {
196     MERGED="$1"
197
198     f=$(git ls-files -u -- "$MERGED")
199     if test -z "$f" ; then
200         if test ! -f "$MERGED" ; then
201             echo "$MERGED: file not found"
202         else
203             echo "$MERGED: file does not need merging"
204         fi
205         return 1
206     fi
207
208     ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
209     BACKUP="./$MERGED.BACKUP.$ext"
210     LOCAL="./$MERGED.LOCAL.$ext"
211     REMOTE="./$MERGED.REMOTE.$ext"
212     BASE="./$MERGED.BASE.$ext"
213
214     base_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}')
215     local_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}')
216     remote_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}')
217
218     if is_submodule "$local_mode" || is_submodule "$remote_mode"; then
219         echo "Submodule merge conflict for '$MERGED':"
220         local_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $2;}')
221         remote_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $2;}')
222         describe_file "$local_mode" "local" "$local_sha1"
223         describe_file "$remote_mode" "remote" "$remote_sha1"
224         resolve_submodule_merge
225         return
226     fi
227
228     mv -- "$MERGED" "$BACKUP"
229     cp -- "$BACKUP" "$MERGED"
230
231     checkout_staged_file 1 "$MERGED" "$BASE"
232     checkout_staged_file 2 "$MERGED" "$LOCAL"
233     checkout_staged_file 3 "$MERGED" "$REMOTE"
234
235     if test -z "$local_mode" -o -z "$remote_mode"; then
236         echo "Deleted merge conflict for '$MERGED':"
237         describe_file "$local_mode" "local" "$LOCAL"
238         describe_file "$remote_mode" "remote" "$REMOTE"
239         resolve_deleted_merge
240         return
241     fi
242
243     if is_symlink "$local_mode" || is_symlink "$remote_mode"; then
244         echo "Symbolic link merge conflict for '$MERGED':"
245         describe_file "$local_mode" "local" "$LOCAL"
246         describe_file "$remote_mode" "remote" "$REMOTE"
247         resolve_symlink_merge
248         return
249     fi
250
251     echo "Normal merge conflict for '$MERGED':"
252     describe_file "$local_mode" "local" "$LOCAL"
253     describe_file "$remote_mode" "remote" "$REMOTE"
254     if "$prompt" = true; then
255         printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
256         read ans || return 1
257     fi
258
259     if base_present; then
260             present=true
261     else
262             present=false
263     fi
264
265     if ! run_merge_tool "$merge_tool" "$present"; then
266         echo "merge of $MERGED failed" 1>&2
267         mv -- "$BACKUP" "$MERGED"
268
269         if test "$merge_keep_temporaries" = "false"; then
270             cleanup_temp_files
271         fi
272
273         return 1
274     fi
275
276     if test "$merge_keep_backup" = "true"; then
277         mv -- "$BACKUP" "$MERGED.orig"
278     else
279         rm -- "$BACKUP"
280     fi
281
282     git add -- "$MERGED"
283     cleanup_temp_files
284     return 0
285 }
286
287 show_tool_help () {
288         TOOL_MODE=merge
289         list_merge_tool_candidates
290         unavailable= available= LF='
291 '
292         for i in $tools
293         do
294                 merge_tool_path=$(translate_merge_tool_path "$i")
295                 if type "$merge_tool_path" >/dev/null 2>&1
296                 then
297                         available="$available$i$LF"
298                 else
299                         unavailable="$unavailable$i$LF"
300                 fi
301         done
302         if test -n "$available"
303         then
304                 echo "'git mergetool --tool=<tool>' may be set to one of the following:"
305                 echo "$available" | sort | sed -e 's/^/ /'
306         else
307                 echo "No suitable tool for 'git mergetool --tool=<tool>' found."
308         fi
309         if test -n "$unavailable"
310         then
311                 echo
312                 echo 'The following tools are valid, but not currently available:'
313                 echo "$unavailable" | sort | sed -e 's/^/       /'
314         fi
315         if test -n "$unavailable$available"
316         then
317                 echo
318                 echo "Some of the tools listed above only work in a windowed"
319                 echo "environment. If run in a terminal-only session, they will fail."
320         fi
321         exit 0
322 }
323
324 prompt=$(git config --bool mergetool.prompt || echo true)
325
326 while test $# != 0
327 do
328     case "$1" in
329         --tool-help)
330                 show_tool_help
331                 ;;
332         -t|--tool*)
333             case "$#,$1" in
334                 *,*=*)
335                     merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
336                     ;;
337                 1,*)
338                     usage ;;
339                 *)
340                     merge_tool="$2"
341                     shift ;;
342             esac
343             ;;
344         -y|--no-prompt)
345             prompt=false
346             ;;
347         --prompt)
348             prompt=true
349             ;;
350         --)
351             shift
352             break
353             ;;
354         -*)
355             usage
356             ;;
357         *)
358             break
359             ;;
360     esac
361     shift
362 done
363
364 prompt_after_failed_merge() {
365     while true; do
366         printf "Continue merging other unresolved paths (y/n) ? "
367         read ans || return 1
368         case "$ans" in
369
370             [yY]*)
371                 return 0
372                 ;;
373
374             [nN]*)
375                 return 1
376                 ;;
377         esac
378     done
379 }
380
381 if test -z "$merge_tool"; then
382     merge_tool=$(get_merge_tool "$merge_tool") || exit
383 fi
384 merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
385 merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
386
387 last_status=0
388 rollup_status=0
389 files=
390
391 if test $# -eq 0 ; then
392     cd_to_toplevel
393
394     if test -e "$GIT_DIR/MERGE_RR"
395     then
396         files=$(git rerere remaining)
397     else
398         files=$(git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u)
399     fi
400 else
401     files=$(git ls-files -u -- "$@" | sed -e 's/^[^     ]*      //' | sort -u)
402 fi
403
404 if test -z "$files" ; then
405     echo "No files need merging"
406     exit 0
407 fi
408
409 printf "Merging:\n"
410 printf "$files\n"
411
412 IFS='
413 '
414 for i in $files
415 do
416     if test $last_status -ne 0; then
417         prompt_after_failed_merge || exit 1
418     fi
419     printf "\n"
420     merge_file "$i"
421     last_status=$?
422     if test $last_status -ne 0; then
423         rollup_status=1
424     fi
425 done
426
427 exit $rollup_status