#!/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. # setvar USAGE = ''[--tool=tool] [--tool-help] [-y|--no-prompt|--prompt] [-O] [file to merge] ...'' setvar SUBDIRECTORY_OK = 'Yes' setvar NONGIT_OK = 'Yes' setvar OPTIONS_SPEC = '' setvar 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 { setvar MERGETOOL_TMPDIR = '.' return 0 } if setvar 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 { setvar mode = "$1" setvar branch = "$2" setvar 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 case (ans) { [lL]* { git checkout-index -f --stage=2 -- $MERGED git add -- $MERGED cleanup_temp_files --save-backup return 0 } [rR]* { git checkout-index -f --stage=3 -- $MERGED git add -- $MERGED cleanup_temp_files --save-backup return 0 } [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 case (ans) { [mMcC]* { git add -- $MERGED if test $merge_keep_backup = "true" { cleanup_temp_files --save-backup } else { cleanup_temp_files } return 0 } [dD]* { git rm -- $MERGED > /dev/null cleanup_temp_files return 0 } [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 case (ans) { [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 } [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 } [aA]* { return 1 } } } } proc stage_submodule { setvar path = "$1" setvar submodule_sha1 = "$2" mkdir -p $path || die "fatal: unable to create directory for module at $path" # Find $path relative to work tree setvar work_tree_root = $(cd_to_toplevel && pwd) setvar work_rel_path = $(cd "$path" && 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 { setvar tmpfile = $(expr \ "$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" \ : '\([^ ]*\) ') if test $? -eq 0 && test -n $tmpfile { mv -- "$(git rev-parse --show-cdup)$tmpfile" $3 } else { >"$3" } } proc merge_file { setvar MERGED = "$1" setvar 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 setvar BASE = $(expr "$MERGED" : '\(.*\)\.[^/]*$') { setvar ext = $(expr "$MERGED" : '.*\(\.[^/]*\)$') } else { setvar BASE = "$MERGED" setvar ext = '' } mergetool_tmpdir_init if test $MERGETOOL_TMPDIR != "." { # If we're using a temporary directory then write to the # top-level of that directory. setvar BASE = ${BASE##*/} } setvar BACKUP = ""$MERGETOOL_TMPDIR/${BASE}_BACKUP_$$$ext"" setvar LOCAL = ""$MERGETOOL_TMPDIR/${BASE}_LOCAL_$$$ext"" setvar REMOTE = ""$MERGETOOL_TMPDIR/${BASE}_REMOTE_$$$ext"" setvar BASE = ""$MERGETOOL_TMPDIR/${BASE}_BASE_$$$ext"" setvar base_mode = $(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}') setvar local_mode = $(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}') setvar 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':" setvar local_sha1 = $(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $2;}') setvar 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 setvar 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 { setvar present = 'true' } else { setvar 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 case (ans) { [yY]* { return 0 } [nN]* { return 1 } } } } proc print_noop_and_exit { echo "No files need merging" exit 0 } proc main { setvar prompt = $(git config --bool mergetool.prompt) setvar guessed_merge_tool = 'false' setvar orderfile = '' while test $# != 0 { case (1) { --tool-help=* { setvar TOOL_MODE = ${1#--tool-help=} show_tool_help } --tool-help { show_tool_help } -t|--tool* { case{ *,*=* { setvar merge_tool = $(expr "z$1" : 'z-[^=]*=\(.*\)') } 1,* { usage } * { setvar merge_tool = "$2" shift } } } -y|--no-prompt { setvar prompt = 'false' } --prompt { setvar prompt = 'true' } -O* { setvar orderfile = "$1" } -- { shift break } -* { usage } * { break } } shift } git_dir_init require_work_tree if test -z $merge_tool { # Check if a merge tool has been configured setvar merge_tool = $(get_configured_merge_tool) # Try to guess an appropriate merge tool if no tool has been set. if test -z $merge_tool { setvar merge_tool = $(guess_merge_tool) || exit setvar guessed_merge_tool = 'true' } } setvar merge_keep_backup = "$(git config --bool mergetool.keepBackup || echo true)" setvar 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 } } setvar files = $(git -c core.quotePath=false \ diff --name-only --diff-filter=U \ ${orderfile:+"$orderfile"} -- "$@") cd_to_toplevel if test -z $files { print_noop_and_exit } printf "Merging:\n" printf "%s\n" $files setvar rc = '0' for i in $files { printf "\n" if ! merge_file $i { setvar rc = '1' prompt_after_failed_merge || exit 1 } } exit $rc } main @ARGV