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