b06ae787393c9ee94b82b26dbac028cec777a56b
   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                        cleanup_temp_files --save-backup
 130                        return 0
 131                        ;;
 132                [dD]*)
 133                        git rm -- "$MERGED" > /dev/null
 134                        cleanup_temp_files
 135                        return 0
 136                        ;;
 137                [aA]*)
 138                        return 1
 139                        ;;
 140                esac
 141        done
 142}
 143
 144resolve_submodule_merge () {
 145        while true
 146        do
 147                printf "Use (l)ocal or (r)emote, or (a)bort? "
 148                read ans || return 1
 149                case "$ans" in
 150                [lL]*)
 151                        if ! local_present
 152                        then
 153                                if test -n "$(git ls-tree HEAD -- "$MERGED")"
 154                                then
 155                                        # Local isn't present, but it's a subdirectory
 156                                        git ls-tree --full-name -r HEAD -- "$MERGED" |
 157                                        git update-index --index-info || exit $?
 158                                else
 159                                        test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
 160                                        git update-index --force-remove "$MERGED"
 161                                        cleanup_temp_files --save-backup
 162                                fi
 163                        elif is_submodule "$local_mode"
 164                        then
 165                                stage_submodule "$MERGED" "$local_sha1"
 166                        else
 167                                git checkout-index -f --stage=2 -- "$MERGED"
 168                                git add -- "$MERGED"
 169                        fi
 170                        return 0
 171                        ;;
 172                [rR]*)
 173                        if ! remote_present
 174                        then
 175                                if test -n "$(git ls-tree MERGE_HEAD -- "$MERGED")"
 176                                then
 177                                        # Remote isn't present, but it's a subdirectory
 178                                        git ls-tree --full-name -r MERGE_HEAD -- "$MERGED" |
 179                                        git update-index --index-info || exit $?
 180                                else
 181                                        test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
 182                                        git update-index --force-remove "$MERGED"
 183                                fi
 184                        elif is_submodule "$remote_mode"
 185                        then
 186                                ! is_submodule "$local_mode" &&
 187                                test -e "$MERGED" &&
 188                                mv -- "$MERGED" "$BACKUP"
 189                                stage_submodule "$MERGED" "$remote_sha1"
 190                        else
 191                                test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
 192                                git checkout-index -f --stage=3 -- "$MERGED"
 193                                git add -- "$MERGED"
 194                        fi
 195                        cleanup_temp_files --save-backup
 196                        return 0
 197                        ;;
 198                [aA]*)
 199                        return 1
 200                        ;;
 201                esac
 202        done
 203}
 204
 205stage_submodule () {
 206        path="$1"
 207        submodule_sha1="$2"
 208        mkdir -p "$path" ||
 209        die "fatal: unable to create directory for module at $path"
 210        # Find $path relative to work tree
 211        work_tree_root=$(cd_to_toplevel && pwd)
 212        work_rel_path=$(cd "$path" &&
 213                GIT_WORK_TREE="${work_tree_root}" git rev-parse --show-prefix
 214        )
 215        test -n "$work_rel_path" ||
 216        die "fatal: unable to get path of module $path relative to work tree"
 217        git update-index --add --replace --cacheinfo 160000 "$submodule_sha1" "${work_rel_path%/}" || die
 218}
 219
 220checkout_staged_file () {
 221        tmpfile=$(expr \
 222                "$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" \
 223                : '\([^ ]*\)    ')
 224
 225        if test $? -eq 0 && test -n "$tmpfile"
 226        then
 227                mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3"
 228        else
 229                >"$3"
 230        fi
 231}
 232
 233merge_file () {
 234        MERGED="$1"
 235
 236        f=$(git ls-files -u -- "$MERGED")
 237        if test -z "$f"
 238        then
 239                if test ! -f "$MERGED"
 240                then
 241                        echo "$MERGED: file not found"
 242                else
 243                        echo "$MERGED: file does not need merging"
 244                fi
 245                return 1
 246        fi
 247
 248        if BASE=$(expr "$MERGED" : '\(.*\)\.[^/]*$')
 249        then
 250                ext=$(expr "$MERGED" : '.*\(\.[^/]*\)$')
 251        else
 252                BASE=$MERGED
 253                ext=
 254        fi
 255
 256        mergetool_tmpdir_init
 257
 258        if test "$MERGETOOL_TMPDIR" != "."
 259        then
 260                # If we're using a temporary directory then write to the
 261                # top-level of that directory.
 262                BASE=${BASE##*/}
 263        fi
 264
 265        BACKUP="$MERGETOOL_TMPDIR/${BASE}_BACKUP_$$$ext"
 266        LOCAL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_$$$ext"
 267        REMOTE="$MERGETOOL_TMPDIR/${BASE}_REMOTE_$$$ext"
 268        BASE="$MERGETOOL_TMPDIR/${BASE}_BASE_$$$ext"
 269
 270        base_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}')
 271        local_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}')
 272        remote_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}')
 273
 274        if is_submodule "$local_mode" || is_submodule "$remote_mode"
 275        then
 276                echo "Submodule merge conflict for '$MERGED':"
 277                local_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $2;}')
 278                remote_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $2;}')
 279                describe_file "$local_mode" "local" "$local_sha1"
 280                describe_file "$remote_mode" "remote" "$remote_sha1"
 281                resolve_submodule_merge
 282                return
 283        fi
 284
 285        if test -f "$MERGED"
 286        then
 287                mv -- "$MERGED" "$BACKUP"
 288                cp -- "$BACKUP" "$MERGED"
 289        fi
 290        # Create a parent directory to handle delete/delete conflicts
 291        # where the base's directory no longer exists.
 292        mkdir -p "$(dirname "$MERGED")"
 293
 294        checkout_staged_file 1 "$MERGED" "$BASE"
 295        checkout_staged_file 2 "$MERGED" "$LOCAL"
 296        checkout_staged_file 3 "$MERGED" "$REMOTE"
 297
 298        if test -z "$local_mode" || test -z "$remote_mode"
 299        then
 300                echo "Deleted merge conflict for '$MERGED':"
 301                describe_file "$local_mode" "local" "$LOCAL"
 302                describe_file "$remote_mode" "remote" "$REMOTE"
 303                resolve_deleted_merge
 304                status=$?
 305                rmdir -p "$(dirname "$MERGED")" 2>/dev/null
 306                return $status
 307        fi
 308
 309        if is_symlink "$local_mode" || is_symlink "$remote_mode"
 310        then
 311                echo "Symbolic link merge conflict for '$MERGED':"
 312                describe_file "$local_mode" "local" "$LOCAL"
 313                describe_file "$remote_mode" "remote" "$REMOTE"
 314                resolve_symlink_merge
 315                return
 316        fi
 317
 318        echo "Normal merge conflict for '$MERGED':"
 319        describe_file "$local_mode" "local" "$LOCAL"
 320        describe_file "$remote_mode" "remote" "$REMOTE"
 321        if test "$guessed_merge_tool" = true || test "$prompt" = true
 322        then
 323                printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
 324                read ans || return 1
 325        fi
 326
 327        if base_present
 328        then
 329                present=true
 330        else
 331                present=false
 332        fi
 333
 334        if ! run_merge_tool "$merge_tool" "$present"
 335        then
 336                echo "merge of $MERGED failed" 1>&2
 337                mv -- "$BACKUP" "$MERGED"
 338
 339                if test "$merge_keep_temporaries" = "false"
 340                then
 341                        cleanup_temp_files
 342                fi
 343
 344                return 1
 345        fi
 346
 347        if test "$merge_keep_backup" = "true"
 348        then
 349                mv -- "$BACKUP" "$MERGED.orig"
 350        else
 351                rm -- "$BACKUP"
 352        fi
 353
 354        git add -- "$MERGED"
 355        cleanup_temp_files
 356        return 0
 357}
 358
 359prompt=$(git config --bool mergetool.prompt)
 360guessed_merge_tool=false
 361
 362while test $# != 0
 363do
 364        case "$1" in
 365        --tool-help=*)
 366                TOOL_MODE=${1#--tool-help=}
 367                show_tool_help
 368                ;;
 369        --tool-help)
 370                show_tool_help
 371                ;;
 372        -t|--tool*)
 373                case "$#,$1" in
 374                *,*=*)
 375                        merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
 376                        ;;
 377                1,*)
 378                        usage ;;
 379                *)
 380                        merge_tool="$2"
 381                        shift ;;
 382                esac
 383                ;;
 384        -y|--no-prompt)
 385                prompt=false
 386                ;;
 387        --prompt)
 388                prompt=true
 389                ;;
 390        --)
 391                shift
 392                break
 393                ;;
 394        -*)
 395                usage
 396                ;;
 397        *)
 398                break
 399                ;;
 400        esac
 401        shift
 402done
 403
 404prompt_after_failed_merge () {
 405        while true
 406        do
 407                printf "Continue merging other unresolved paths (y/n) ? "
 408                read ans || return 1
 409                case "$ans" in
 410                [yY]*)
 411                        return 0
 412                        ;;
 413                [nN]*)
 414                        return 1
 415                        ;;
 416                esac
 417        done
 418}
 419
 420git_dir_init
 421require_work_tree
 422
 423if test -z "$merge_tool"
 424then
 425        # Check if a merge tool has been configured
 426        merge_tool=$(get_configured_merge_tool)
 427        # Try to guess an appropriate merge tool if no tool has been set.
 428        if test -z "$merge_tool"
 429        then
 430                merge_tool=$(guess_merge_tool) || exit
 431                guessed_merge_tool=true
 432        fi
 433fi
 434merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
 435merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
 436
 437files=
 438
 439if test $# -eq 0
 440then
 441        cd_to_toplevel
 442
 443        if test -e "$GIT_DIR/MERGE_RR"
 444        then
 445                files=$(git rerere remaining)
 446        else
 447                files=$(git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u)
 448        fi
 449else
 450        files=$(git ls-files -u -- "$@" | sed -e 's/^[^ ]*      //' | sort -u)
 451fi
 452
 453if test -z "$files"
 454then
 455        echo "No files need merging"
 456        exit 0
 457fi
 458
 459printf "Merging:\n"
 460printf "%s\n" "$files"
 461
 462rc=0
 463for i in $files
 464do
 465        printf "\n"
 466        if ! merge_file "$i"
 467        then
 468                rc=1
 469                prompt_after_failed_merge || exit 1
 470        fi
 471done
 472
 473exit $rc