xdiff/xhistogram: rework handling of recursed results
[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
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
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
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 "$(git checkout-index --temp --stage="$1" "$2")" : '\([^     ]*\)    ')
185
186     if test $? -eq 0 -a -n "$tmpfile" ; then
187         mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3"
188     fi
189 }
190
191 merge_file () {
192     MERGED="$1"
193
194     f=$(git ls-files -u -- "$MERGED")
195     if test -z "$f" ; then
196         if test ! -f "$MERGED" ; then
197             echo "$MERGED: file not found"
198         else
199             echo "$MERGED: file does not need merging"
200         fi
201         return 1
202     fi
203
204     ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
205     BACKUP="./$MERGED.BACKUP.$ext"
206     LOCAL="./$MERGED.LOCAL.$ext"
207     REMOTE="./$MERGED.REMOTE.$ext"
208     BASE="./$MERGED.BASE.$ext"
209
210     base_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}')
211     local_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}')
212     remote_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}')
213
214     if is_submodule "$local_mode" || is_submodule "$remote_mode"; then
215         echo "Submodule merge conflict for '$MERGED':"
216         local_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $2;}')
217         remote_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $2;}')
218         describe_file "$local_mode" "local" "$local_sha1"
219         describe_file "$remote_mode" "remote" "$remote_sha1"
220         resolve_submodule_merge
221         return
222     fi
223
224     mv -- "$MERGED" "$BACKUP"
225     cp -- "$BACKUP" "$MERGED"
226
227     base_present   && checkout_staged_file 1 "$MERGED" "$BASE"
228     local_present  && checkout_staged_file 2 "$MERGED" "$LOCAL"
229     remote_present && checkout_staged_file 3 "$MERGED" "$REMOTE"
230
231     if test -z "$local_mode" -o -z "$remote_mode"; then
232         echo "Deleted merge conflict for '$MERGED':"
233         describe_file "$local_mode" "local" "$LOCAL"
234         describe_file "$remote_mode" "remote" "$REMOTE"
235         resolve_deleted_merge
236         return
237     fi
238
239     if is_symlink "$local_mode" || is_symlink "$remote_mode"; then
240         echo "Symbolic link merge conflict for '$MERGED':"
241         describe_file "$local_mode" "local" "$LOCAL"
242         describe_file "$remote_mode" "remote" "$REMOTE"
243         resolve_symlink_merge
244         return
245     fi
246
247     echo "Normal merge conflict for '$MERGED':"
248     describe_file "$local_mode" "local" "$LOCAL"
249     describe_file "$remote_mode" "remote" "$REMOTE"
250     if "$prompt" = true; then
251         printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
252         read ans
253     fi
254
255     if base_present; then
256             present=true
257     else
258             present=false
259     fi
260
261     if ! run_merge_tool "$merge_tool" "$present"; then
262         echo "merge of $MERGED failed" 1>&2
263         mv -- "$BACKUP" "$MERGED"
264
265         if test "$merge_keep_temporaries" = "false"; then
266             cleanup_temp_files
267         fi
268
269         return 1
270     fi
271
272     if test "$merge_keep_backup" = "true"; then
273         mv -- "$BACKUP" "$MERGED.orig"
274     else
275         rm -- "$BACKUP"
276     fi
277
278     git add -- "$MERGED"
279     cleanup_temp_files
280     return 0
281 }
282
283 prompt=$(git config --bool mergetool.prompt || echo true)
284
285 while test $# != 0
286 do
287     case "$1" in
288         -t|--tool*)
289             case "$#,$1" in
290                 *,*=*)
291                     merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
292                     ;;
293                 1,*)
294                     usage ;;
295                 *)
296                     merge_tool="$2"
297                     shift ;;
298             esac
299             ;;
300         -y|--no-prompt)
301             prompt=false
302             ;;
303         --prompt)
304             prompt=true
305             ;;
306         --)
307             shift
308             break
309             ;;
310         -*)
311             usage
312             ;;
313         *)
314             break
315             ;;
316     esac
317     shift
318 done
319
320 prompt_after_failed_merge() {
321     while true; do
322         printf "Continue merging other unresolved paths (y/n) ? "
323         read ans
324         case "$ans" in
325
326             [yY]*)
327                 return 0
328                 ;;
329
330             [nN]*)
331                 return 1
332                 ;;
333         esac
334     done
335 }
336
337 if test -z "$merge_tool"; then
338     merge_tool=$(get_merge_tool "$merge_tool") || exit
339 fi
340 merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
341 merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
342
343 last_status=0
344 rollup_status=0
345 rerere=false
346
347 files_to_merge() {
348     if test "$rerere" = true
349     then
350         git rerere remaining
351     else
352         git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u
353     fi
354 }
355
356
357 if test $# -eq 0 ; then
358     cd_to_toplevel
359
360     if test -e "$GIT_DIR/MERGE_RR"
361     then
362         rerere=true
363     fi
364
365     files=$(files_to_merge)
366     if test -z "$files" ; then
367         echo "No files need merging"
368         exit 0
369     fi
370
371     # Save original stdin
372     exec 3<&0
373
374     printf "Merging:\n"
375     printf "$files\n"
376
377     files_to_merge |
378     while IFS= read i
379     do
380         if test $last_status -ne 0; then
381             prompt_after_failed_merge <&3 || exit 1
382         fi
383         printf "\n"
384         merge_file "$i" <&3
385         last_status=$?
386         if test $last_status -ne 0; then
387             rollup_status=1
388         fi
389     done
390 else
391     while test $# -gt 0; do
392         if test $last_status -ne 0; then
393             prompt_after_failed_merge || exit 1
394         fi
395         printf "\n"
396         merge_file "$1"
397         last_status=$?
398         if test $last_status -ne 0; then
399             rollup_status=1
400         fi
401         shift
402     done
403 fi
404
405 exit $rollup_status