#!/bin/sh # # This program resolves merge conflicts in git # # Copyright (c) 2006 Theodore Y. Ts'o # Copyright (c) 2009-2016 David Aguilar # # This file is licensed under the GPL v2, or a later version # at the discretion of Junio C Hamano. # setglobal USAGE = ''[--tool=tool] [--tool-help] [-y|--no-prompt|--prompt] [-O] [file to merge] ...'' setglobal SUBDIRECTORY_OK = 'Yes' setglobal NONGIT_OK = 'Yes' setglobal OPTIONS_SPEC = '' setglobal TOOL_MODE = 'merge' source git-sh-setup source git-mergetool--lib # Returns true if the mode reflects a symlink proc is_symlink { test $1 = 120000 } proc is_submodule { test $1 = 160000 } proc local_present { test -n $local_mode } proc remote_present { test -n $remote_mode } proc base_present { test -n $base_mode } proc mergetool_tmpdir_init { if test $[git config --bool mergetool.writeToTemp] != true { setglobal MERGETOOL_TMPDIR = '.' return 0 } if setglobal MERGETOOL_TMPDIR = $[mktemp -d -t "git-mergetool-XXXXXX" !2 >/dev/null] { return 0 } die "error: mktemp is needed when 'mergetool.writeToTemp' is true" } proc cleanup_temp_files { if test $1 = --save-backup { rm -rf -- "$MERGED.orig" test -e $BACKUP && mv -- $BACKUP "$MERGED.orig" rm -f -- $LOCAL $REMOTE $BASE } else { rm -f -- $LOCAL $REMOTE $BASE $BACKUP } if test $MERGETOOL_TMPDIR != "." { rmdir $MERGETOOL_TMPDIR } } proc describe_file { setglobal mode = $1 setglobal branch = $2 setglobal file = $3 printf " {%s}: " $branch if test -z $mode { echo "deleted" } elif is_symlink $mode { echo "a symbolic link -> '$[cat $file]'" } elif is_submodule $mode { echo "submodule commit $file" } elif base_present { echo "modified file" } else { echo "created file" } } proc resolve_symlink_merge { while true { printf "Use (l)ocal or (r)emote, or (a)bort? " read ans || return 1 match $ans { with [lL]* git checkout-index -f --stage=2 -- $MERGED git add -- $MERGED cleanup_temp_files --save-backup return 0 with [rR]* git checkout-index -f --stage=3 -- $MERGED git add -- $MERGED cleanup_temp_files --save-backup return 0 with [aA]* return 1 } } } proc resolve_deleted_merge { while true { if base_present { printf "Use (m)odified or (d)eleted file, or (a)bort? " } else { printf "Use (c)reated or (d)eleted file, or (a)bort? " } read ans || return 1 match $ans { with [mMcC]* git add -- $MERGED if test $merge_keep_backup = "true" { cleanup_temp_files --save-backup } else { cleanup_temp_files } return 0 with [dD]* git rm -- $MERGED > /dev/null cleanup_temp_files return 0 with [aA]* if test $merge_keep_temporaries = "false" { cleanup_temp_files } return 1 } } } proc resolve_submodule_merge { while true { printf "Use (l)ocal or (r)emote, or (a)bort? " read ans || return 1 match $ans { with [lL]* if ! local_present { if test -n $[git ls-tree HEAD -- $MERGED] { # Local isn't present, but it's a subdirectory git ls-tree --full-name -r HEAD -- $MERGED | git update-index --index-info || exit $? } else { test -e $MERGED && mv -- $MERGED $BACKUP git update-index --force-remove $MERGED cleanup_temp_files --save-backup } } elif is_submodule $local_mode { stage_submodule $MERGED $local_sha1 } else { git checkout-index -f --stage=2 -- $MERGED git add -- $MERGED } return 0 with [rR]* if ! remote_present { if test -n $[git ls-tree MERGE_HEAD -- $MERGED] { # Remote isn't present, but it's a subdirectory git ls-tree --full-name -r MERGE_HEAD -- $MERGED | git update-index --index-info || exit $? } else { test -e $MERGED && mv -- $MERGED $BACKUP git update-index --force-remove $MERGED } } elif is_submodule $remote_mode { ! is_submodule $local_mode && test -e $MERGED && mv -- $MERGED $BACKUP stage_submodule $MERGED $remote_sha1 } else { test -e $MERGED && mv -- $MERGED $BACKUP git checkout-index -f --stage=3 -- $MERGED git add -- $MERGED } cleanup_temp_files --save-backup return 0 with [aA]* return 1 } } } proc stage_submodule { setglobal path = $1 setglobal submodule_sha1 = $2 mkdir -p $path || die "fatal: unable to create directory for module at $path" # Find $path relative to work tree setglobal work_tree_root = $[cd_to_toplevel && pwd] setglobal work_rel_path = $[cd $path && env GIT_WORK_TREE=$(work_tree_root) git rev-parse --show-prefix] test -n $work_rel_path || die "fatal: unable to get path of module $path relative to work tree" git update-index --add --replace --cacheinfo 160000 $submodule_sha1 $(work_rel_path%/) || die } proc checkout_staged_file { setglobal tmpfile = $[expr \ $[git checkout-index --temp --stage="$1" $2 !2 >/dev/null] \ : '\([^ ]*\) ] if test $Status -eq 0 && test -n $tmpfile { mv -- "$[git rev-parse --show-cdup]$tmpfile" $3 } else { >$3 } } proc merge_file { setglobal MERGED = $1 setglobal f = $[git ls-files -u -- $MERGED] if test -z $f { if test ! -f $MERGED { echo "$MERGED: file not found" } else { echo "$MERGED: file does not need merging" } return 1 } if setglobal BASE = $[expr $MERGED : '\(.*\)\.[^/]*$] { setglobal ext = $[expr $MERGED : '.*\(\.[^/]*\)$] } else { setglobal BASE = $MERGED setglobal ext = '' } mergetool_tmpdir_init if test $MERGETOOL_TMPDIR != "." { # If we're using a temporary directory then write to the # top-level of that directory. setglobal BASE = $(BASE##*/) } setglobal BACKUP = ""$MERGETOOL_TMPDIR/$(BASE)_BACKUP_$Pid$ext"" setglobal LOCAL = ""$MERGETOOL_TMPDIR/$(BASE)_LOCAL_$Pid$ext"" setglobal REMOTE = ""$MERGETOOL_TMPDIR/$(BASE)_REMOTE_$Pid$ext"" setglobal BASE = ""$MERGETOOL_TMPDIR/$(BASE)_BASE_$Pid$ext"" setglobal base_mode = $[git ls-files -u -- $MERGED | awk '{if ($3==1) print $1;}] setglobal local_mode = $[git ls-files -u -- $MERGED | awk '{if ($3==2) print $1;}] setglobal remote_mode = $[git ls-files -u -- $MERGED | awk '{if ($3==3) print $1;}] if is_submodule $local_mode || is_submodule $remote_mode { echo "Submodule merge conflict for '$MERGED':" setglobal local_sha1 = $[git ls-files -u -- $MERGED | awk '{if ($3==2) print $2;}] setglobal remote_sha1 = $[git ls-files -u -- $MERGED | awk '{if ($3==3) print $2;}] describe_file $local_mode "local" $local_sha1 describe_file $remote_mode "remote" $remote_sha1 resolve_submodule_merge return } if test -f $MERGED { mv -- $MERGED $BACKUP cp -- $BACKUP $MERGED } # Create a parent directory to handle delete/delete conflicts # where the base's directory no longer exists. mkdir -p $[dirname $MERGED] checkout_staged_file 1 $MERGED $BASE checkout_staged_file 2 $MERGED $LOCAL checkout_staged_file 3 $MERGED $REMOTE if test -z $local_mode || test -z $remote_mode { echo "Deleted merge conflict for '$MERGED':" describe_file $local_mode "local" $LOCAL describe_file $remote_mode "remote" $REMOTE resolve_deleted_merge setglobal status = $Status rmdir -p $[dirname $MERGED] !2 >/dev/null return $status } if is_symlink $local_mode || is_symlink $remote_mode { echo "Symbolic link merge conflict for '$MERGED':" describe_file $local_mode "local" $LOCAL describe_file $remote_mode "remote" $REMOTE resolve_symlink_merge return } echo "Normal merge conflict for '$MERGED':" describe_file $local_mode "local" $LOCAL describe_file $remote_mode "remote" $REMOTE if test $guessed_merge_tool = true || test $prompt = true { printf "Hit return to start merge resolution tool (%s): " $merge_tool read ans || return 1 } if base_present { setglobal present = 'true' } else { setglobal present = 'false' } if ! run_merge_tool $merge_tool $present { echo "merge of $MERGED failed" !1 > !2 mv -- $BACKUP $MERGED if test $merge_keep_temporaries = "false" { cleanup_temp_files } return 1 } if test $merge_keep_backup = "true" { mv -- $BACKUP "$MERGED.orig" } else { rm -- $BACKUP } git add -- $MERGED cleanup_temp_files return 0 } proc prompt_after_failed_merge { while true { printf "Continue merging other unresolved paths [y/n]? " read ans || return 1 match $ans { with [yY]* return 0 with [nN]* return 1 } } } proc print_noop_and_exit { echo "No files need merging" exit 0 } proc main { setglobal prompt = $[git config --bool mergetool.prompt] setglobal guessed_merge_tool = 'false' setglobal orderfile = '' while test $# != 0 { match $1 { with --tool-help=* setglobal TOOL_MODE = $(1#--tool-help=) show_tool_help with --tool-help show_tool_help with -t|--tool* match "$Argc,$1" { with *,*=* setglobal merge_tool = $[expr "z$1" : 'z-[^=]*=\(.*\)] with 1,* usage with * setglobal merge_tool = $2 shift } with -y|--no-prompt setglobal prompt = 'false' with --prompt setglobal prompt = 'true' with -O* setglobal orderfile = $1 with -- shift break with -* usage with * break } shift } git_dir_init require_work_tree if test -z $merge_tool { # Check if a merge tool has been configured setglobal merge_tool = $[get_configured_merge_tool] # Try to guess an appropriate merge tool if no tool has been set. if test -z $merge_tool { setglobal merge_tool = $[guess_merge_tool] || exit setglobal guessed_merge_tool = 'true' } } setglobal merge_keep_backup = $[git config --bool mergetool.keepBackup || echo true] setglobal merge_keep_temporaries = $[git config --bool mergetool.keepTemporaries || echo false] if test $Argc -eq 0 && test -e "$GIT_DIR/MERGE_RR" { set -- $[git rerere remaining] if test $Argc -eq 0 { print_noop_and_exit } } setglobal files = $[git -c core.quotePath=false \ diff --name-only --diff-filter=U \ $(orderfile:+"$orderfile") -- @Argv] cd_to_toplevel if test -z $files { print_noop_and_exit } printf "Merging:\n" printf "%s\n" $files setglobal rc = '0' for i in [$files] { printf "\n" if ! merge_file $i { setglobal rc = '1' prompt_after_failed_merge || exit 1 } } exit $rc } main @Argv