config: add core.mode = progress pseudo-config
[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                         cleanup_temp_files --save-backup
130                         return 0
131                         ;;
132                 [dD]*)
133                         git rm -- "$MERGED" > /dev/null
134                         cleanup_temp_files
135                         return 0
136                         ;;
137                 [aA]*)
138                         return 1
139                         ;;
140                 esac
141         done
142 }
143
144 resolve_submodule_merge () {
145         while true
146         do
147                 printf "Use (l)ocal or (r)emote, or (a)bort? "
148                 read ans || return 1
149                 case "$ans" in
150                 [lL]*)
151                         if ! local_present
152                         then
153                                 if test -n "$(git ls-tree HEAD -- "$MERGED")"
154                                 then
155                                         # Local isn't present, but it's a subdirectory
156                                         git ls-tree --full-name -r HEAD -- "$MERGED" |
157                                         git update-index --index-info || exit $?
158                                 else
159                                         test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
160                                         git update-index --force-remove "$MERGED"
161                                         cleanup_temp_files --save-backup
162                                 fi
163                         elif is_submodule "$local_mode"
164                         then
165                                 stage_submodule "$MERGED" "$local_sha1"
166                         else
167                                 git checkout-index -f --stage=2 -- "$MERGED"
168                                 git add -- "$MERGED"
169                         fi
170                         return 0
171                         ;;
172                 [rR]*)
173                         if ! remote_present
174                         then
175                                 if test -n "$(git ls-tree MERGE_HEAD -- "$MERGED")"
176                                 then
177                                         # Remote isn't present, but it's a subdirectory
178                                         git ls-tree --full-name -r MERGE_HEAD -- "$MERGED" |
179                                         git update-index --index-info || exit $?
180                                 else
181                                         test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
182                                         git update-index --force-remove "$MERGED"
183                                 fi
184                         elif is_submodule "$remote_mode"
185                         then
186                                 ! is_submodule "$local_mode" &&
187                                 test -e "$MERGED" &&
188                                 mv -- "$MERGED" "$BACKUP"
189                                 stage_submodule "$MERGED" "$remote_sha1"
190                         else
191                                 test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
192                                 git checkout-index -f --stage=3 -- "$MERGED"
193                                 git add -- "$MERGED"
194                         fi
195                         cleanup_temp_files --save-backup
196                         return 0
197                         ;;
198                 [aA]*)
199                         return 1
200                         ;;
201                 esac
202         done
203 }
204
205 stage_submodule () {
206         path="$1"
207         submodule_sha1="$2"
208         mkdir -p "$path" ||
209         die "fatal: unable to create directory for module at $path"
210         # Find $path relative to work tree
211         work_tree_root=$(cd_to_toplevel && pwd)
212         work_rel_path=$(cd "$path" &&
213                 GIT_WORK_TREE="${work_tree_root}" git rev-parse --show-prefix
214         )
215         test -n "$work_rel_path" ||
216         die "fatal: unable to get path of module $path relative to work tree"
217         git update-index --add --replace --cacheinfo 160000 "$submodule_sha1" "${work_rel_path%/}" || die
218 }
219
220 checkout_staged_file () {
221         tmpfile=$(expr \
222                 "$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" \
223                 : '\([^ ]*\)    ')
224
225         if test $? -eq 0 && test -n "$tmpfile"
226         then
227                 mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3"
228         else
229                 >"$3"
230         fi
231 }
232
233 merge_file () {
234         MERGED="$1"
235
236         f=$(git ls-files -u -- "$MERGED")
237         if test -z "$f"
238         then
239                 if test ! -f "$MERGED"
240                 then
241                         echo "$MERGED: file not found"
242                 else
243                         echo "$MERGED: file does not need merging"
244                 fi
245                 return 1
246         fi
247
248         if BASE=$(expr "$MERGED" : '\(.*\)\.[^/]*$')
249         then
250                 ext=$(expr "$MERGED" : '.*\(\.[^/]*\)$')
251         else
252                 BASE=$MERGED
253                 ext=
254         fi
255
256         mergetool_tmpdir_init
257
258         if test "$MERGETOOL_TMPDIR" != "."
259         then
260                 # If we're using a temporary directory then write to the
261                 # top-level of that directory.
262                 BASE=${BASE##*/}
263         fi
264
265         BACKUP="$MERGETOOL_TMPDIR/${BASE}_BACKUP_$$$ext"
266         LOCAL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_$$$ext"
267         REMOTE="$MERGETOOL_TMPDIR/${BASE}_REMOTE_$$$ext"
268         BASE="$MERGETOOL_TMPDIR/${BASE}_BASE_$$$ext"
269
270         base_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}')
271         local_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}')
272         remote_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}')
273
274         if is_submodule "$local_mode" || is_submodule "$remote_mode"
275         then
276                 echo "Submodule merge conflict for '$MERGED':"
277                 local_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $2;}')
278                 remote_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $2;}')
279                 describe_file "$local_mode" "local" "$local_sha1"
280                 describe_file "$remote_mode" "remote" "$remote_sha1"
281                 resolve_submodule_merge
282                 return
283         fi
284
285         mv -- "$MERGED" "$BACKUP"
286         cp -- "$BACKUP" "$MERGED"
287
288         checkout_staged_file 1 "$MERGED" "$BASE"
289         checkout_staged_file 2 "$MERGED" "$LOCAL"
290         checkout_staged_file 3 "$MERGED" "$REMOTE"
291
292         if test -z "$local_mode" || test -z "$remote_mode"
293         then
294                 echo "Deleted merge conflict for '$MERGED':"
295                 describe_file "$local_mode" "local" "$LOCAL"
296                 describe_file "$remote_mode" "remote" "$REMOTE"
297                 resolve_deleted_merge
298                 return
299         fi
300
301         if is_symlink "$local_mode" || is_symlink "$remote_mode"
302         then
303                 echo "Symbolic link merge conflict for '$MERGED':"
304                 describe_file "$local_mode" "local" "$LOCAL"
305                 describe_file "$remote_mode" "remote" "$REMOTE"
306                 resolve_symlink_merge
307                 return
308         fi
309
310         echo "Normal merge conflict for '$MERGED':"
311         describe_file "$local_mode" "local" "$LOCAL"
312         describe_file "$remote_mode" "remote" "$REMOTE"
313         if test "$guessed_merge_tool" = true || test "$prompt" = true
314         then
315                 printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
316                 read ans || return 1
317         fi
318
319         if base_present
320         then
321                 present=true
322         else
323                 present=false
324         fi
325
326         if ! run_merge_tool "$merge_tool" "$present"
327         then
328                 echo "merge of $MERGED failed" 1>&2
329                 mv -- "$BACKUP" "$MERGED"
330
331                 if test "$merge_keep_temporaries" = "false"
332                 then
333                         cleanup_temp_files
334                 fi
335
336                 return 1
337         fi
338
339         if test "$merge_keep_backup" = "true"
340         then
341                 mv -- "$BACKUP" "$MERGED.orig"
342         else
343                 rm -- "$BACKUP"
344         fi
345
346         git add -- "$MERGED"
347         cleanup_temp_files
348         return 0
349 }
350
351 prompt=$(git config --bool mergetool.prompt)
352 if test "$(git config core.mode)" != "progress" && test -z "$prompt"
353 then
354         prompt=true
355 fi
356 guessed_merge_tool=false
357
358 while test $# != 0
359 do
360         case "$1" in
361         --tool-help=*)
362                 TOOL_MODE=${1#--tool-help=}
363                 show_tool_help
364                 ;;
365         --tool-help)
366                 show_tool_help
367                 ;;
368         -t|--tool*)
369                 case "$#,$1" in
370                 *,*=*)
371                         merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
372                         ;;
373                 1,*)
374                         usage ;;
375                 *)
376                         merge_tool="$2"
377                         shift ;;
378                 esac
379                 ;;
380         -y|--no-prompt)
381                 prompt=false
382                 ;;
383         --prompt)
384                 prompt=true
385                 ;;
386         --)
387                 shift
388                 break
389                 ;;
390         -*)
391                 usage
392                 ;;
393         *)
394                 break
395                 ;;
396         esac
397         shift
398 done
399
400 prompt_after_failed_merge () {
401         while true
402         do
403                 printf "Continue merging other unresolved paths (y/n) ? "
404                 read ans || return 1
405                 case "$ans" in
406                 [yY]*)
407                         return 0
408                         ;;
409                 [nN]*)
410                         return 1
411                         ;;
412                 esac
413         done
414 }
415
416 git_dir_init
417 require_work_tree
418
419 if test -z "$merge_tool"
420 then
421         # Check if a merge tool has been configured
422         merge_tool=$(get_configured_merge_tool)
423         # Try to guess an appropriate merge tool if no tool has been set.
424         if test -z "$merge_tool"
425         then
426                 merge_tool=$(guess_merge_tool) || exit
427                 guessed_merge_tool=true
428         fi
429 fi
430 merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
431 merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
432
433 files=
434
435 if test $# -eq 0
436 then
437         cd_to_toplevel
438
439         if test -e "$GIT_DIR/MERGE_RR"
440         then
441                 files=$(git rerere remaining)
442         else
443                 files=$(git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u)
444         fi
445 else
446         files=$(git ls-files -u -- "$@" | sed -e 's/^[^ ]*      //' | sort -u)
447 fi
448
449 if test -z "$files"
450 then
451         echo "No files need merging"
452         exit 0
453 fi
454
455 printf "Merging:\n"
456 printf "%s\n" "$files"
457
458 rc=0
459 for i in $files
460 do
461         printf "\n"
462         if ! merge_file "$i"
463         then
464                 rc=1
465                 prompt_after_failed_merge || exit 1
466         fi
467 done
468
469 exit $rc