t7408: merge short tests, factor out testing method
[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 NONGIT_OK=Yes
14 OPTIONS_SPEC=
15 TOOL_MODE=merge
16 . git-sh-setup
17 . git-mergetool--lib
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 mergetool_tmpdir_init () {
41         if test "$(git config --bool mergetool.writeToTemp)" != true
42         then
43                 MERGETOOL_TMPDIR=.
44                 return 0
45         fi
46         if MERGETOOL_TMPDIR=$(mktemp -d -t "git-mergetool-XXXXXX" 2>/dev/null)
47         then
48                 return 0
49         fi
50         die "error: mktemp is needed when 'mergetool.writeToTemp' is true"
51 }
52
53 cleanup_temp_files () {
54         if test "$1" = --save-backup
55         then
56                 rm -rf -- "$MERGED.orig"
57                 test -e "$BACKUP" && mv -- "$BACKUP" "$MERGED.orig"
58                 rm -f -- "$LOCAL" "$REMOTE" "$BASE"
59         else
60                 rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
61         fi
62         if test "$MERGETOOL_TMPDIR" != "."
63         then
64                 rmdir "$MERGETOOL_TMPDIR"
65         fi
66 }
67
68 describe_file () {
69         mode="$1"
70         branch="$2"
71         file="$3"
72
73         printf "  {%s}: " "$branch"
74         if test -z "$mode"
75         then
76                 echo "deleted"
77         elif is_symlink "$mode"
78         then
79                 echo "a symbolic link -> '$(cat "$file")'"
80         elif is_submodule "$mode"
81         then
82                 echo "submodule commit $file"
83         elif base_present
84         then
85                 echo "modified file"
86         else
87                 echo "created file"
88         fi
89 }
90
91 resolve_symlink_merge () {
92         while true
93         do
94                 printf "Use (l)ocal or (r)emote, or (a)bort? "
95                 read ans || return 1
96                 case "$ans" in
97                 [lL]*)
98                         git checkout-index -f --stage=2 -- "$MERGED"
99                         git add -- "$MERGED"
100                         cleanup_temp_files --save-backup
101                         return 0
102                         ;;
103                 [rR]*)
104                         git checkout-index -f --stage=3 -- "$MERGED"
105                         git add -- "$MERGED"
106                         cleanup_temp_files --save-backup
107                         return 0
108                         ;;
109                 [aA]*)
110                         return 1
111                         ;;
112                 esac
113         done
114 }
115
116 resolve_deleted_merge () {
117         while true
118         do
119                 if base_present
120                 then
121                         printf "Use (m)odified or (d)eleted file, or (a)bort? "
122                 else
123                         printf "Use (c)reated or (d)eleted file, or (a)bort? "
124                 fi
125                 read ans || return 1
126                 case "$ans" in
127                 [mMcC]*)
128                         git add -- "$MERGED"
129                         if test "$merge_keep_backup" = "true"
130                         then
131                                 cleanup_temp_files --save-backup
132                         else
133                                 cleanup_temp_files
134                         fi
135                         return 0
136                         ;;
137                 [dD]*)
138                         git rm -- "$MERGED" > /dev/null
139                         cleanup_temp_files
140                         return 0
141                         ;;
142                 [aA]*)
143                         if test "$merge_keep_temporaries" = "false"
144                         then
145                                 cleanup_temp_files
146                         fi
147                         return 1
148                         ;;
149                 esac
150         done
151 }
152
153 resolve_submodule_merge () {
154         while true
155         do
156                 printf "Use (l)ocal or (r)emote, or (a)bort? "
157                 read ans || return 1
158                 case "$ans" in
159                 [lL]*)
160                         if ! local_present
161                         then
162                                 if test -n "$(git ls-tree HEAD -- "$MERGED")"
163                                 then
164                                         # Local isn't present, but it's a subdirectory
165                                         git ls-tree --full-name -r HEAD -- "$MERGED" |
166                                         git update-index --index-info || exit $?
167                                 else
168                                         test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
169                                         git update-index --force-remove "$MERGED"
170                                         cleanup_temp_files --save-backup
171                                 fi
172                         elif is_submodule "$local_mode"
173                         then
174                                 stage_submodule "$MERGED" "$local_sha1"
175                         else
176                                 git checkout-index -f --stage=2 -- "$MERGED"
177                                 git add -- "$MERGED"
178                         fi
179                         return 0
180                         ;;
181                 [rR]*)
182                         if ! remote_present
183                         then
184                                 if test -n "$(git ls-tree MERGE_HEAD -- "$MERGED")"
185                                 then
186                                         # Remote isn't present, but it's a subdirectory
187                                         git ls-tree --full-name -r MERGE_HEAD -- "$MERGED" |
188                                         git update-index --index-info || exit $?
189                                 else
190                                         test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
191                                         git update-index --force-remove "$MERGED"
192                                 fi
193                         elif is_submodule "$remote_mode"
194                         then
195                                 ! is_submodule "$local_mode" &&
196                                 test -e "$MERGED" &&
197                                 mv -- "$MERGED" "$BACKUP"
198                                 stage_submodule "$MERGED" "$remote_sha1"
199                         else
200                                 test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
201                                 git checkout-index -f --stage=3 -- "$MERGED"
202                                 git add -- "$MERGED"
203                         fi
204                         cleanup_temp_files --save-backup
205                         return 0
206                         ;;
207                 [aA]*)
208                         return 1
209                         ;;
210                 esac
211         done
212 }
213
214 stage_submodule () {
215         path="$1"
216         submodule_sha1="$2"
217         mkdir -p "$path" ||
218         die "fatal: unable to create directory for module at $path"
219         # Find $path relative to work tree
220         work_tree_root=$(cd_to_toplevel && pwd)
221         work_rel_path=$(cd "$path" &&
222                 GIT_WORK_TREE="${work_tree_root}" git rev-parse --show-prefix
223         )
224         test -n "$work_rel_path" ||
225         die "fatal: unable to get path of module $path relative to work tree"
226         git update-index --add --replace --cacheinfo 160000 "$submodule_sha1" "${work_rel_path%/}" || die
227 }
228
229 checkout_staged_file () {
230         tmpfile=$(expr \
231                 "$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" \
232                 : '\([^ ]*\)    ')
233
234         if test $? -eq 0 && test -n "$tmpfile"
235         then
236                 mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3"
237         else
238                 >"$3"
239         fi
240 }
241
242 merge_file () {
243         MERGED="$1"
244
245         f=$(git ls-files -u -- "$MERGED")
246         if test -z "$f"
247         then
248                 if test ! -f "$MERGED"
249                 then
250                         echo "$MERGED: file not found"
251                 else
252                         echo "$MERGED: file does not need merging"
253                 fi
254                 return 1
255         fi
256
257         if BASE=$(expr "$MERGED" : '\(.*\)\.[^/]*$')
258         then
259                 ext=$(expr "$MERGED" : '.*\(\.[^/]*\)$')
260         else
261                 BASE=$MERGED
262                 ext=
263         fi
264
265         mergetool_tmpdir_init
266
267         if test "$MERGETOOL_TMPDIR" != "."
268         then
269                 # If we're using a temporary directory then write to the
270                 # top-level of that directory.
271                 BASE=${BASE##*/}
272         fi
273
274         BACKUP="$MERGETOOL_TMPDIR/${BASE}_BACKUP_$$$ext"
275         LOCAL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_$$$ext"
276         REMOTE="$MERGETOOL_TMPDIR/${BASE}_REMOTE_$$$ext"
277         BASE="$MERGETOOL_TMPDIR/${BASE}_BASE_$$$ext"
278
279         base_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}')
280         local_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}')
281         remote_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}')
282
283         if is_submodule "$local_mode" || is_submodule "$remote_mode"
284         then
285                 echo "Submodule merge conflict for '$MERGED':"
286                 local_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $2;}')
287                 remote_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $2;}')
288                 describe_file "$local_mode" "local" "$local_sha1"
289                 describe_file "$remote_mode" "remote" "$remote_sha1"
290                 resolve_submodule_merge
291                 return
292         fi
293
294         if test -f "$MERGED"
295         then
296                 mv -- "$MERGED" "$BACKUP"
297                 cp -- "$BACKUP" "$MERGED"
298         fi
299         # Create a parent directory to handle delete/delete conflicts
300         # where the base's directory no longer exists.
301         mkdir -p "$(dirname "$MERGED")"
302
303         checkout_staged_file 1 "$MERGED" "$BASE"
304         checkout_staged_file 2 "$MERGED" "$LOCAL"
305         checkout_staged_file 3 "$MERGED" "$REMOTE"
306
307         if test -z "$local_mode" || test -z "$remote_mode"
308         then
309                 echo "Deleted merge conflict for '$MERGED':"
310                 describe_file "$local_mode" "local" "$LOCAL"
311                 describe_file "$remote_mode" "remote" "$REMOTE"
312                 resolve_deleted_merge
313                 status=$?
314                 rmdir -p "$(dirname "$MERGED")" 2>/dev/null
315                 return $status
316         fi
317
318         if is_symlink "$local_mode" || is_symlink "$remote_mode"
319         then
320                 echo "Symbolic link merge conflict for '$MERGED':"
321                 describe_file "$local_mode" "local" "$LOCAL"
322                 describe_file "$remote_mode" "remote" "$REMOTE"
323                 resolve_symlink_merge
324                 return
325         fi
326
327         echo "Normal merge conflict for '$MERGED':"
328         describe_file "$local_mode" "local" "$LOCAL"
329         describe_file "$remote_mode" "remote" "$REMOTE"
330         if test "$guessed_merge_tool" = true || test "$prompt" = true
331         then
332                 printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
333                 read ans || return 1
334         fi
335
336         if base_present
337         then
338                 present=true
339         else
340                 present=false
341         fi
342
343         if ! run_merge_tool "$merge_tool" "$present"
344         then
345                 echo "merge of $MERGED failed" 1>&2
346                 mv -- "$BACKUP" "$MERGED"
347
348                 if test "$merge_keep_temporaries" = "false"
349                 then
350                         cleanup_temp_files
351                 fi
352
353                 return 1
354         fi
355
356         if test "$merge_keep_backup" = "true"
357         then
358                 mv -- "$BACKUP" "$MERGED.orig"
359         else
360                 rm -- "$BACKUP"
361         fi
362
363         git add -- "$MERGED"
364         cleanup_temp_files
365         return 0
366 }
367
368 prompt=$(git config --bool mergetool.prompt)
369 guessed_merge_tool=false
370
371 while test $# != 0
372 do
373         case "$1" in
374         --tool-help=*)
375                 TOOL_MODE=${1#--tool-help=}
376                 show_tool_help
377                 ;;
378         --tool-help)
379                 show_tool_help
380                 ;;
381         -t|--tool*)
382                 case "$#,$1" in
383                 *,*=*)
384                         merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
385                         ;;
386                 1,*)
387                         usage ;;
388                 *)
389                         merge_tool="$2"
390                         shift ;;
391                 esac
392                 ;;
393         -y|--no-prompt)
394                 prompt=false
395                 ;;
396         --prompt)
397                 prompt=true
398                 ;;
399         --)
400                 shift
401                 break
402                 ;;
403         -*)
404                 usage
405                 ;;
406         *)
407                 break
408                 ;;
409         esac
410         shift
411 done
412
413 prompt_after_failed_merge () {
414         while true
415         do
416                 printf "Continue merging other unresolved paths [y/n]? "
417                 read ans || return 1
418                 case "$ans" in
419                 [yY]*)
420                         return 0
421                         ;;
422                 [nN]*)
423                         return 1
424                         ;;
425                 esac
426         done
427 }
428
429 git_dir_init
430 require_work_tree
431
432 if test -z "$merge_tool"
433 then
434         # Check if a merge tool has been configured
435         merge_tool=$(get_configured_merge_tool)
436         # Try to guess an appropriate merge tool if no tool has been set.
437         if test -z "$merge_tool"
438         then
439                 merge_tool=$(guess_merge_tool) || exit
440                 guessed_merge_tool=true
441         fi
442 fi
443 merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
444 merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
445
446 files=
447
448 if test $# -eq 0
449 then
450         cd_to_toplevel
451
452         if test -e "$GIT_DIR/MERGE_RR"
453         then
454                 files=$(git rerere remaining)
455         else
456                 files=$(git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u)
457         fi
458 else
459         files=$(git ls-files -u -- "$@" | sed -e 's/^[^ ]*      //' | sort -u)
460 fi
461
462 if test -z "$files"
463 then
464         echo "No files need merging"
465         exit 0
466 fi
467
468 printf "Merging:\n"
469 printf "%s\n" "$files"
470
471 rc=0
472 for i in $files
473 do
474         printf "\n"
475         if ! merge_file "$i"
476         then
477                 rc=1
478                 prompt_after_failed_merge || exit 1
479         fi
480 done
481
482 exit $rc