git-mergetool.shon commit conditional markdown preprocessing (c8b1cd9)
   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= local_mode= remote_mode=
 283
 284        # here, $IFS is just a LF
 285        for line in $f
 286        do
 287                mode=${line%% *}                # 1st word
 288                sha1=${line#"$mode "}
 289                sha1=${sha1%% *}                # 2nd word
 290                case "${line#$mode $sha1 }" in  # remainder
 291                '1      '*)
 292                        base_mode=$mode
 293                        ;;
 294                '2      '*)
 295                        local_mode=$mode local_sha1=$sha1
 296                        ;;
 297                '3      '*)
 298                        remote_mode=$mode remote_sha1=$sha1
 299                        ;;
 300                esac
 301        done
 302
 303        if is_submodule "$local_mode" || is_submodule "$remote_mode"
 304        then
 305                echo "Submodule merge conflict for '$MERGED':"
 306                describe_file "$local_mode" "local" "$local_sha1"
 307                describe_file "$remote_mode" "remote" "$remote_sha1"
 308                resolve_submodule_merge
 309                return
 310        fi
 311
 312        if test -f "$MERGED"
 313        then
 314                mv -- "$MERGED" "$BACKUP"
 315                cp -- "$BACKUP" "$MERGED"
 316        fi
 317        # Create a parent directory to handle delete/delete conflicts
 318        # where the base's directory no longer exists.
 319        mkdir -p "$(dirname "$MERGED")"
 320
 321        checkout_staged_file 1 "$MERGED" "$BASE"
 322        checkout_staged_file 2 "$MERGED" "$LOCAL"
 323        checkout_staged_file 3 "$MERGED" "$REMOTE"
 324
 325        if test -z "$local_mode" || test -z "$remote_mode"
 326        then
 327                echo "Deleted merge conflict for '$MERGED':"
 328                describe_file "$local_mode" "local" "$LOCAL"
 329                describe_file "$remote_mode" "remote" "$REMOTE"
 330                resolve_deleted_merge
 331                status=$?
 332                rmdir -p "$(dirname "$MERGED")" 2>/dev/null
 333                return $status
 334        fi
 335
 336        if is_symlink "$local_mode" || is_symlink "$remote_mode"
 337        then
 338                echo "Symbolic link merge conflict for '$MERGED':"
 339                describe_file "$local_mode" "local" "$LOCAL"
 340                describe_file "$remote_mode" "remote" "$REMOTE"
 341                resolve_symlink_merge
 342                return
 343        fi
 344
 345        echo "Normal merge conflict for '$MERGED':"
 346        describe_file "$local_mode" "local" "$LOCAL"
 347        describe_file "$remote_mode" "remote" "$REMOTE"
 348        if test "$guessed_merge_tool" = true || test "$prompt" = true
 349        then
 350                printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
 351                read ans || return 1
 352        fi
 353
 354        if base_present
 355        then
 356                present=true
 357        else
 358                present=false
 359        fi
 360
 361        if ! run_merge_tool "$merge_tool" "$present"
 362        then
 363                echo "merge of $MERGED failed" 1>&2
 364                mv -- "$BACKUP" "$MERGED"
 365
 366                if test "$merge_keep_temporaries" = "false"
 367                then
 368                        cleanup_temp_files
 369                fi
 370
 371                return 1
 372        fi
 373
 374        if test "$merge_keep_backup" = "true"
 375        then
 376                mv -- "$BACKUP" "$MERGED.orig"
 377        else
 378                rm -- "$BACKUP"
 379        fi
 380
 381        git add -- "$MERGED"
 382        cleanup_temp_files
 383        return 0
 384}
 385
 386prompt_after_failed_merge () {
 387        while true
 388        do
 389                printf "Continue merging other unresolved paths [y/n]? "
 390                read ans || return 1
 391                case "$ans" in
 392                [yY]*)
 393                        return 0
 394                        ;;
 395                [nN]*)
 396                        return 1
 397                        ;;
 398                esac
 399        done
 400}
 401
 402print_noop_and_exit () {
 403        echo "No files need merging"
 404        exit 0
 405}
 406
 407main () {
 408        prompt=$(git config --bool mergetool.prompt)
 409        GIT_MERGETOOL_GUI=false
 410        guessed_merge_tool=false
 411        orderfile=
 412
 413        while test $# != 0
 414        do
 415                case "$1" in
 416                --tool-help=*)
 417                        TOOL_MODE=${1#--tool-help=}
 418                        show_tool_help
 419                        ;;
 420                --tool-help)
 421                        show_tool_help
 422                        ;;
 423                -t|--tool*)
 424                        case "$#,$1" in
 425                        *,*=*)
 426                                merge_tool=${1#*=}
 427                                ;;
 428                        1,*)
 429                                usage ;;
 430                        *)
 431                                merge_tool="$2"
 432                                shift ;;
 433                        esac
 434                        ;;
 435                --no-gui)
 436                        GIT_MERGETOOL_GUI=false
 437                        ;;
 438                -g|--gui)
 439                        GIT_MERGETOOL_GUI=true
 440                        ;;
 441                -y|--no-prompt)
 442                        prompt=false
 443                        ;;
 444                --prompt)
 445                        prompt=true
 446                        ;;
 447                -O*)
 448                        orderfile="${1#-O}"
 449                        ;;
 450                --)
 451                        shift
 452                        break
 453                        ;;
 454                -*)
 455                        usage
 456                        ;;
 457                *)
 458                        break
 459                        ;;
 460                esac
 461                shift
 462        done
 463
 464        git_dir_init
 465        require_work_tree
 466
 467        if test -z "$merge_tool"
 468        then
 469                if ! merge_tool=$(get_merge_tool)
 470                then
 471                        guessed_merge_tool=true
 472                fi
 473        fi
 474        merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
 475        merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
 476
 477        prefix=$(git rev-parse --show-prefix) || exit 1
 478        cd_to_toplevel
 479
 480        if test -n "$orderfile"
 481        then
 482                orderfile=$(
 483                        git rev-parse --prefix "$prefix" -- "$orderfile" |
 484                        sed -e 1d
 485                )
 486        fi
 487
 488        if test $# -eq 0 && test -e "$GIT_DIR/MERGE_RR"
 489        then
 490                set -- $(git rerere remaining)
 491                if test $# -eq 0
 492                then
 493                        print_noop_and_exit
 494                fi
 495        elif test $# -ge 0
 496        then
 497                # rev-parse provides the -- needed for 'set'
 498                eval "set $(git rev-parse --sq --prefix "$prefix" -- "$@")"
 499        fi
 500
 501        files=$(git -c core.quotePath=false \
 502                diff --name-only --diff-filter=U \
 503                ${orderfile:+"-O$orderfile"} -- "$@")
 504
 505        if test -z "$files"
 506        then
 507                print_noop_and_exit
 508        fi
 509
 510        printf "Merging:\n"
 511        printf "%s\n" "$files"
 512
 513        rc=0
 514        set -- $files
 515        while test $# -ne 0
 516        do
 517                printf "\n"
 518                if ! merge_file "$1"
 519                then
 520                        rc=1
 521                        test $# -ne 1 && prompt_after_failed_merge || exit 1
 522                fi
 523                shift
 524        done
 525
 526        exit $rc
 527}
 528
 529main "$@"