git-mergetool.shon commit Merge branch 'cc/apply-introduce-state' (0bbda4b)
   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
  11USAGE='[--tool=tool] [--tool-help] [-y|--no-prompt|--prompt] [file to merge] ...'
  12SUBDIRECTORY_OK=Yes
  13NONGIT_OK=Yes
  14OPTIONS_SPEC=
  15TOOL_MODE=merge
  16. git-sh-setup
  17. git-mergetool--lib
  18
  19# Returns true if the mode reflects a symlink
  20is_symlink () {
  21        test "$1" = 120000
  22}
  23
  24is_submodule () {
  25        test "$1" = 160000
  26}
  27
  28local_present () {
  29        test -n "$local_mode"
  30}
  31
  32remote_present () {
  33        test -n "$remote_mode"
  34}
  35
  36base_present () {
  37        test -n "$base_mode"
  38}
  39
  40mergetool_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
  53cleanup_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
  68describe_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
  91resolve_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
 116resolve_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
 153resolve_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
 214stage_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
 229checkout_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
 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        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
 368prompt=$(git config --bool mergetool.prompt)
 369guessed_merge_tool=false
 370
 371while test $# != 0
 372do
 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
 411done
 412
 413prompt_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
 429git_dir_init
 430require_work_tree
 431
 432if test -z "$merge_tool"
 433then
 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
 442fi
 443merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
 444merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
 445
 446files=
 447
 448if test $# -eq 0
 449then
 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
 458else
 459        files=$(git ls-files -u -- "$@" | sed -e 's/^[^ ]*      //' | sort -u)
 460fi
 461
 462if test -z "$files"
 463then
 464        echo "No files need merging"
 465        exit 0
 466fi
 467
 468printf "Merging:\n"
 469printf "%s\n" "$files"
 470
 471rc=0
 472for i in $files
 473do
 474        printf "\n"
 475        if ! merge_file "$i"
 476        then
 477                rc=1
 478                prompt_after_failed_merge || exit 1
 479        fi
 480done
 481
 482exit $rc