git-mergetool.shon commit send-email: only consider lines containing @ or <> for automatic Cc'ing (fb34fe6)
   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] [-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=$(expr \
 232                "$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" \
 233                : '\([^ ]*\)    ')
 234
 235        if test $? -eq 0 && test -n "$tmpfile"
 236        then
 237                mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3"
 238        else
 239                >"$3"
 240        fi
 241}
 242
 243merge_file () {
 244        MERGED="$1"
 245
 246        f=$(git ls-files -u -- "$MERGED")
 247        if test -z "$f"
 248        then
 249                if test ! -f "$MERGED"
 250                then
 251                        echo "$MERGED: file not found"
 252                else
 253                        echo "$MERGED: file does not need merging"
 254                fi
 255                return 1
 256        fi
 257
 258        if BASE=$(expr "$MERGED" : '\(.*\)\.[^/]*$')
 259        then
 260                ext=$(expr "$MERGED" : '.*\(\.[^/]*\)$')
 261        else
 262                BASE=$MERGED
 263                ext=
 264        fi
 265
 266        mergetool_tmpdir_init
 267
 268        if test "$MERGETOOL_TMPDIR" != "."
 269        then
 270                # If we're using a temporary directory then write to the
 271                # top-level of that directory.
 272                BASE=${BASE##*/}
 273        fi
 274
 275        BACKUP="$MERGETOOL_TMPDIR/${BASE}_BACKUP_$$$ext"
 276        LOCAL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_$$$ext"
 277        REMOTE="$MERGETOOL_TMPDIR/${BASE}_REMOTE_$$$ext"
 278        BASE="$MERGETOOL_TMPDIR/${BASE}_BASE_$$$ext"
 279
 280        base_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}')
 281        local_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}')
 282        remote_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}')
 283
 284        if is_submodule "$local_mode" || is_submodule "$remote_mode"
 285        then
 286                echo "Submodule merge conflict for '$MERGED':"
 287                local_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $2;}')
 288                remote_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $2;}')
 289                describe_file "$local_mode" "local" "$local_sha1"
 290                describe_file "$remote_mode" "remote" "$remote_sha1"
 291                resolve_submodule_merge
 292                return
 293        fi
 294
 295        if test -f "$MERGED"
 296        then
 297                mv -- "$MERGED" "$BACKUP"
 298                cp -- "$BACKUP" "$MERGED"
 299        fi
 300        # Create a parent directory to handle delete/delete conflicts
 301        # where the base's directory no longer exists.
 302        mkdir -p "$(dirname "$MERGED")"
 303
 304        checkout_staged_file 1 "$MERGED" "$BASE"
 305        checkout_staged_file 2 "$MERGED" "$LOCAL"
 306        checkout_staged_file 3 "$MERGED" "$REMOTE"
 307
 308        if test -z "$local_mode" || test -z "$remote_mode"
 309        then
 310                echo "Deleted merge conflict for '$MERGED':"
 311                describe_file "$local_mode" "local" "$LOCAL"
 312                describe_file "$remote_mode" "remote" "$REMOTE"
 313                resolve_deleted_merge
 314                status=$?
 315                rmdir -p "$(dirname "$MERGED")" 2>/dev/null
 316                return $status
 317        fi
 318
 319        if is_symlink "$local_mode" || is_symlink "$remote_mode"
 320        then
 321                echo "Symbolic link merge conflict for '$MERGED':"
 322                describe_file "$local_mode" "local" "$LOCAL"
 323                describe_file "$remote_mode" "remote" "$REMOTE"
 324                resolve_symlink_merge
 325                return
 326        fi
 327
 328        echo "Normal merge conflict for '$MERGED':"
 329        describe_file "$local_mode" "local" "$LOCAL"
 330        describe_file "$remote_mode" "remote" "$REMOTE"
 331        if test "$guessed_merge_tool" = true || test "$prompt" = true
 332        then
 333                printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
 334                read ans || return 1
 335        fi
 336
 337        if base_present
 338        then
 339                present=true
 340        else
 341                present=false
 342        fi
 343
 344        if ! run_merge_tool "$merge_tool" "$present"
 345        then
 346                echo "merge of $MERGED failed" 1>&2
 347                mv -- "$BACKUP" "$MERGED"
 348
 349                if test "$merge_keep_temporaries" = "false"
 350                then
 351                        cleanup_temp_files
 352                fi
 353
 354                return 1
 355        fi
 356
 357        if test "$merge_keep_backup" = "true"
 358        then
 359                mv -- "$BACKUP" "$MERGED.orig"
 360        else
 361                rm -- "$BACKUP"
 362        fi
 363
 364        git add -- "$MERGED"
 365        cleanup_temp_files
 366        return 0
 367}
 368
 369prompt_after_failed_merge () {
 370        while true
 371        do
 372                printf "Continue merging other unresolved paths [y/n]? "
 373                read ans || return 1
 374                case "$ans" in
 375                [yY]*)
 376                        return 0
 377                        ;;
 378                [nN]*)
 379                        return 1
 380                        ;;
 381                esac
 382        done
 383}
 384
 385print_noop_and_exit () {
 386        echo "No files need merging"
 387        exit 0
 388}
 389
 390main () {
 391        prompt=$(git config --bool mergetool.prompt)
 392        guessed_merge_tool=false
 393        orderfile=
 394
 395        while test $# != 0
 396        do
 397                case "$1" in
 398                --tool-help=*)
 399                        TOOL_MODE=${1#--tool-help=}
 400                        show_tool_help
 401                        ;;
 402                --tool-help)
 403                        show_tool_help
 404                        ;;
 405                -t|--tool*)
 406                        case "$#,$1" in
 407                        *,*=*)
 408                                merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
 409                                ;;
 410                        1,*)
 411                                usage ;;
 412                        *)
 413                                merge_tool="$2"
 414                                shift ;;
 415                        esac
 416                        ;;
 417                -y|--no-prompt)
 418                        prompt=false
 419                        ;;
 420                --prompt)
 421                        prompt=true
 422                        ;;
 423                -O*)
 424                        orderfile="${1#-O}"
 425                        ;;
 426                --)
 427                        shift
 428                        break
 429                        ;;
 430                -*)
 431                        usage
 432                        ;;
 433                *)
 434                        break
 435                        ;;
 436                esac
 437                shift
 438        done
 439
 440        git_dir_init
 441        require_work_tree
 442
 443        if test -z "$merge_tool"
 444        then
 445                # Check if a merge tool has been configured
 446                merge_tool=$(get_configured_merge_tool)
 447                # Try to guess an appropriate merge tool if no tool has been set.
 448                if test -z "$merge_tool"
 449                then
 450                        merge_tool=$(guess_merge_tool) || exit
 451                        guessed_merge_tool=true
 452                fi
 453        fi
 454        merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
 455        merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
 456
 457        prefix=$(git rev-parse --show-prefix) || exit 1
 458        cd_to_toplevel
 459
 460        if test -n "$orderfile"
 461        then
 462                orderfile=$(
 463                        git rev-parse --prefix "$prefix" -- "$orderfile" |
 464                        sed -e 1d
 465                )
 466        fi
 467
 468        if test $# -eq 0 && test -e "$GIT_DIR/MERGE_RR"
 469        then
 470                set -- $(git rerere remaining)
 471                if test $# -eq 0
 472                then
 473                        print_noop_and_exit
 474                fi
 475        elif test $# -ge 0
 476        then
 477                # rev-parse provides the -- needed for 'set'
 478                eval "set $(git rev-parse --sq --prefix "$prefix" -- "$@")"
 479        fi
 480
 481        files=$(git -c core.quotePath=false \
 482                diff --name-only --diff-filter=U \
 483                ${orderfile:+"-O$orderfile"} -- "$@")
 484
 485        if test -z "$files"
 486        then
 487                print_noop_and_exit
 488        fi
 489
 490        printf "Merging:\n"
 491        printf "%s\n" "$files"
 492
 493        rc=0
 494        set -- $files
 495        while test $# -ne 0
 496        do
 497                printf "\n"
 498                if ! merge_file "$1"
 499                then
 500                        rc=1
 501                        test $# -ne 1 && prompt_after_failed_merge || exit 1
 502                fi
 503                shift
 504        done
 505
 506        exit $rc
 507}
 508
 509main "$@"