Merge branch 'jc/diff-algo-cleanup'
[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] [-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 prompt=$(git config --bool mergetool.prompt || echo true)
288
289 while test $# != 0
290 do
291     case "$1" in
292         -t|--tool*)
293             case "$#,$1" in
294                 *,*=*)
295                     merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
296                     ;;
297                 1,*)
298                     usage ;;
299                 *)
300                     merge_tool="$2"
301                     shift ;;
302             esac
303             ;;
304         -y|--no-prompt)
305             prompt=false
306             ;;
307         --prompt)
308             prompt=true
309             ;;
310         --)
311             shift
312             break
313             ;;
314         -*)
315             usage
316             ;;
317         *)
318             break
319             ;;
320     esac
321     shift
322 done
323
324 prompt_after_failed_merge() {
325     while true; do
326         printf "Continue merging other unresolved paths (y/n) ? "
327         read ans || return 1
328         case "$ans" in
329
330             [yY]*)
331                 return 0
332                 ;;
333
334             [nN]*)
335                 return 1
336                 ;;
337         esac
338     done
339 }
340
341 if test -z "$merge_tool"; then
342     merge_tool=$(get_merge_tool "$merge_tool") || exit
343 fi
344 merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
345 merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
346
347 last_status=0
348 rollup_status=0
349 files=
350
351 if test $# -eq 0 ; then
352     cd_to_toplevel
353
354     if test -e "$GIT_DIR/MERGE_RR"
355     then
356         files=$(git rerere remaining)
357     else
358         files=$(git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u)
359     fi
360 else
361     files=$(git ls-files -u -- "$@" | sed -e 's/^[^     ]*      //' | sort -u)
362 fi
363
364 if test -z "$files" ; then
365     echo "No files need merging"
366     exit 0
367 fi
368
369 printf "Merging:\n"
370 printf "$files\n"
371
372 IFS='
373 '
374 for i in $files
375 do
376     if test $last_status -ne 0; then
377         prompt_after_failed_merge || exit 1
378     fi
379     printf "\n"
380     merge_file "$i"
381     last_status=$?
382     if test $last_status -ne 0; then
383         rollup_status=1
384     fi
385 done
386
387 exit $rollup_status