a4855d9444bfe5e1f5b7f4bcba402d3855c052e5
   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] [-y|--no-prompt|--prompt] [file to merge] ...'
  12SUBDIRECTORY_OK=Yes
  13OPTIONS_SPEC=
  14. git-sh-setup
  15require_work_tree
  16prefix=$(git rev-parse --show-prefix)
  17
  18# Returns true if the mode reflects a symlink
  19is_symlink () {
  20    test "$1" = 120000
  21}
  22
  23local_present () {
  24    test -n "$local_mode"
  25}
  26
  27remote_present () {
  28    test -n "$remote_mode"
  29}
  30
  31base_present () {
  32    test -n "$base_mode"
  33}
  34
  35cleanup_temp_files () {
  36    if test "$1" = --save-backup ; then
  37        mv -- "$BACKUP" "$MERGED.orig"
  38        rm -f -- "$LOCAL" "$REMOTE" "$BASE"
  39    else
  40        rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
  41    fi
  42}
  43
  44describe_file () {
  45    mode="$1"
  46    branch="$2"
  47    file="$3"
  48
  49    printf "  {%s}: " "$branch"
  50    if test -z "$mode"; then
  51        echo "deleted"
  52    elif is_symlink "$mode" ; then
  53        echo "a symbolic link -> '$(cat "$file")'"
  54    else
  55        if base_present; then
  56            echo "modified"
  57        else
  58            echo "created"
  59        fi
  60    fi
  61}
  62
  63
  64resolve_symlink_merge () {
  65    while true; do
  66        printf "Use (l)ocal or (r)emote, or (a)bort? "
  67        read ans
  68        case "$ans" in
  69            [lL]*)
  70                git checkout-index -f --stage=2 -- "$MERGED"
  71                git add -- "$MERGED"
  72                cleanup_temp_files --save-backup
  73                return 0
  74                ;;
  75            [rR]*)
  76                git checkout-index -f --stage=3 -- "$MERGED"
  77                git add -- "$MERGED"
  78                cleanup_temp_files --save-backup
  79                return 0
  80                ;;
  81            [aA]*)
  82                return 1
  83                ;;
  84            esac
  85        done
  86}
  87
  88resolve_deleted_merge () {
  89    while true; do
  90        if base_present; then
  91            printf "Use (m)odified or (d)eleted file, or (a)bort? "
  92        else
  93            printf "Use (c)reated or (d)eleted file, or (a)bort? "
  94        fi
  95        read ans
  96        case "$ans" in
  97            [mMcC]*)
  98                git add -- "$MERGED"
  99                cleanup_temp_files --save-backup
 100                return 0
 101                ;;
 102            [dD]*)
 103                git rm -- "$MERGED" > /dev/null
 104                cleanup_temp_files
 105                return 0
 106                ;;
 107            [aA]*)
 108                return 1
 109                ;;
 110            esac
 111        done
 112}
 113
 114check_unchanged () {
 115    if test "$MERGED" -nt "$BACKUP" ; then
 116        status=0;
 117    else
 118        while true; do
 119            echo "$MERGED seems unchanged."
 120            printf "Was the merge successful? [y/n] "
 121            read answer < /dev/tty
 122            case "$answer" in
 123                y*|Y*) status=0; break ;;
 124                n*|N*) status=1; break ;;
 125            esac
 126        done
 127    fi
 128}
 129
 130checkout_staged_file () {
 131    tmpfile=$(expr "$(git checkout-index --temp --stage="$1" "$2")" : '\([^     ]*\)    ')
 132
 133    if test $? -eq 0 -a -n "$tmpfile" ; then
 134        mv -- "$tmpfile" "$3"
 135    fi
 136}
 137
 138merge_file () {
 139    MERGED="$1"
 140
 141    f=`git ls-files -u -- "$MERGED"`
 142    if test -z "$f" ; then
 143        if test ! -f "$MERGED" ; then
 144            echo "$MERGED: file not found"
 145        else
 146            echo "$MERGED: file does not need merging"
 147        fi
 148        return 1
 149    fi
 150
 151    ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
 152    BACKUP="./$MERGED.BACKUP.$ext"
 153    LOCAL="./$MERGED.LOCAL.$ext"
 154    REMOTE="./$MERGED.REMOTE.$ext"
 155    BASE="./$MERGED.BASE.$ext"
 156
 157    mv -- "$MERGED" "$BACKUP"
 158    cp -- "$BACKUP" "$MERGED"
 159
 160    base_mode=`git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}'`
 161    local_mode=`git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}'`
 162    remote_mode=`git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}'`
 163
 164    base_present   && checkout_staged_file 1 "$prefix$MERGED" "$BASE"
 165    local_present  && checkout_staged_file 2 "$prefix$MERGED" "$LOCAL"
 166    remote_present && checkout_staged_file 3 "$prefix$MERGED" "$REMOTE"
 167
 168    if test -z "$local_mode" -o -z "$remote_mode"; then
 169        echo "Deleted merge conflict for '$MERGED':"
 170        describe_file "$local_mode" "local" "$LOCAL"
 171        describe_file "$remote_mode" "remote" "$REMOTE"
 172        resolve_deleted_merge
 173        return
 174    fi
 175
 176    if is_symlink "$local_mode" || is_symlink "$remote_mode"; then
 177        echo "Symbolic link merge conflict for '$MERGED':"
 178        describe_file "$local_mode" "local" "$LOCAL"
 179        describe_file "$remote_mode" "remote" "$REMOTE"
 180        resolve_symlink_merge
 181        return
 182    fi
 183
 184    echo "Normal merge conflict for '$MERGED':"
 185    describe_file "$local_mode" "local" "$LOCAL"
 186    describe_file "$remote_mode" "remote" "$REMOTE"
 187    if "$prompt" = true; then
 188        printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
 189        read ans
 190    fi
 191
 192    case "$merge_tool" in
 193        kdiff3)
 194            if base_present ; then
 195                ("$merge_tool_path" --auto --L1 "$MERGED (Base)" --L2 "$MERGED (Local)" --L3 "$MERGED (Remote)" \
 196                    -o "$MERGED" "$BASE" "$LOCAL" "$REMOTE" > /dev/null 2>&1)
 197            else
 198                ("$merge_tool_path" --auto --L1 "$MERGED (Local)" --L2 "$MERGED (Remote)" \
 199                    -o "$MERGED" "$LOCAL" "$REMOTE" > /dev/null 2>&1)
 200            fi
 201            status=$?
 202            ;;
 203        tkdiff)
 204            if base_present ; then
 205                "$merge_tool_path" -a "$BASE" -o "$MERGED" "$LOCAL" "$REMOTE"
 206            else
 207                "$merge_tool_path" -o "$MERGED" "$LOCAL" "$REMOTE"
 208            fi
 209            status=$?
 210            ;;
 211        meld)
 212            touch "$BACKUP"
 213            "$merge_tool_path" "$LOCAL" "$MERGED" "$REMOTE"
 214            check_unchanged
 215            ;;
 216        vimdiff)
 217            touch "$BACKUP"
 218            "$merge_tool_path" -c "wincmd l" "$LOCAL" "$MERGED" "$REMOTE"
 219            check_unchanged
 220            ;;
 221        gvimdiff)
 222            touch "$BACKUP"
 223            "$merge_tool_path" -c "wincmd l" -f "$LOCAL" "$MERGED" "$REMOTE"
 224            check_unchanged
 225            ;;
 226        xxdiff)
 227            touch "$BACKUP"
 228            if base_present ; then
 229                "$merge_tool_path" -X --show-merged-pane \
 230                    -R 'Accel.SaveAsMerged: "Ctrl-S"' \
 231                    -R 'Accel.Search: "Ctrl+F"' \
 232                    -R 'Accel.SearchForward: "Ctrl-G"' \
 233                    --merged-file "$MERGED" "$LOCAL" "$BASE" "$REMOTE"
 234            else
 235                "$merge_tool_path" -X --show-merged-pane \
 236                    -R 'Accel.SaveAsMerged: "Ctrl-S"' \
 237                    -R 'Accel.Search: "Ctrl+F"' \
 238                    -R 'Accel.SearchForward: "Ctrl-G"' \
 239                    --merged-file "$MERGED" "$LOCAL" "$REMOTE"
 240            fi
 241            check_unchanged
 242            ;;
 243        opendiff)
 244            touch "$BACKUP"
 245            if base_present; then
 246                "$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$MERGED" | cat
 247            else
 248                "$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$MERGED" | cat
 249            fi
 250            check_unchanged
 251            ;;
 252        ecmerge)
 253            touch "$BACKUP"
 254            if base_present; then
 255                "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --default --mode=merge3 --to="$MERGED"
 256            else
 257                "$merge_tool_path" "$LOCAL" "$REMOTE" --default --mode=merge2 --to="$MERGED"
 258            fi
 259            check_unchanged
 260            ;;
 261        emerge)
 262            if base_present ; then
 263                "$merge_tool_path" -f emerge-files-with-ancestor-command "$LOCAL" "$REMOTE" "$BASE" "$(basename "$MERGED")"
 264            else
 265                "$merge_tool_path" -f emerge-files-command "$LOCAL" "$REMOTE" "$(basename "$MERGED")"
 266            fi
 267            status=$?
 268            ;;
 269        *)
 270            if test -n "$merge_tool_cmd"; then
 271                if test "$merge_tool_trust_exit_code" = "false"; then
 272                    touch "$BACKUP"
 273                    ( eval $merge_tool_cmd )
 274                    check_unchanged
 275                else
 276                    ( eval $merge_tool_cmd )
 277                    status=$?
 278                fi
 279            fi
 280            ;;
 281    esac
 282    if test "$status" -ne 0; then
 283        echo "merge of $MERGED failed" 1>&2
 284        mv -- "$BACKUP" "$MERGED"
 285
 286        if test "$merge_keep_temporaries" = "false"; then
 287            cleanup_temp_files
 288        fi
 289
 290        return 1
 291    fi
 292
 293    if test "$merge_keep_backup" = "true"; then
 294        mv -- "$BACKUP" "$MERGED.orig"
 295    else
 296        rm -- "$BACKUP"
 297    fi
 298
 299    git add -- "$MERGED"
 300    cleanup_temp_files
 301    return 0
 302}
 303
 304prompt=$(git config --bool mergetool.prompt || echo true)
 305
 306while test $# != 0
 307do
 308    case "$1" in
 309        -t|--tool*)
 310            case "$#,$1" in
 311                *,*=*)
 312                    merge_tool=`expr "z$1" : 'z-[^=]*=\(.*\)'`
 313                    ;;
 314                1,*)
 315                    usage ;;
 316                *)
 317                    merge_tool="$2"
 318                    shift ;;
 319            esac
 320            ;;
 321        -y|--no-prompt)
 322            prompt=false
 323            ;;
 324        --prompt)
 325            prompt=true
 326            ;;
 327        --)
 328            shift
 329            break
 330            ;;
 331        -*)
 332            usage
 333            ;;
 334        *)
 335            break
 336            ;;
 337    esac
 338    shift
 339done
 340
 341valid_custom_tool()
 342{
 343    merge_tool_cmd="$(git config mergetool.$1.cmd)"
 344    test -n "$merge_tool_cmd"
 345}
 346
 347valid_tool() {
 348        case "$1" in
 349                kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
 350                        ;; # happy
 351                *)
 352                        if ! valid_custom_tool "$1"; then
 353                                return 1
 354                        fi
 355                        ;;
 356        esac
 357}
 358
 359init_merge_tool_path() {
 360        merge_tool_path=`git config mergetool.$1.path`
 361        if test -z "$merge_tool_path" ; then
 362                case "$1" in
 363                        emerge)
 364                                merge_tool_path=emacs
 365                                ;;
 366                        *)
 367                                merge_tool_path=$1
 368                                ;;
 369                esac
 370        fi
 371}
 372
 373prompt_after_failed_merge() {
 374    while true; do
 375        printf "Continue merging other unresolved paths (y/n) ? "
 376        read ans
 377        case "$ans" in
 378
 379            [yY]*)
 380                return 0
 381                ;;
 382
 383            [nN]*)
 384                return 1
 385                ;;
 386        esac
 387    done
 388}
 389
 390if test -z "$merge_tool"; then
 391    merge_tool=`git config merge.tool`
 392    if test -n "$merge_tool" && ! valid_tool "$merge_tool"; then
 393            echo >&2 "git config option merge.tool set to unknown tool: $merge_tool"
 394            echo >&2 "Resetting to default..."
 395            unset merge_tool
 396    fi
 397fi
 398
 399if test -z "$merge_tool" ; then
 400    if test -n "$DISPLAY"; then
 401        merge_tool_candidates="kdiff3 tkdiff xxdiff meld gvimdiff"
 402        if test -n "$GNOME_DESKTOP_SESSION_ID" ; then
 403            merge_tool_candidates="meld $merge_tool_candidates"
 404        fi
 405        if test "$KDE_FULL_SESSION" = "true"; then
 406            merge_tool_candidates="kdiff3 $merge_tool_candidates"
 407        fi
 408    fi
 409    if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
 410        merge_tool_candidates="$merge_tool_candidates emerge"
 411    fi
 412    if echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
 413        merge_tool_candidates="$merge_tool_candidates vimdiff"
 414    fi
 415    merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
 416    echo "merge tool candidates: $merge_tool_candidates"
 417    for i in $merge_tool_candidates; do
 418        init_merge_tool_path $i
 419        if type "$merge_tool_path" > /dev/null 2>&1; then
 420            merge_tool=$i
 421            break
 422        fi
 423    done
 424    if test -z "$merge_tool" ; then
 425        echo "No known merge resolution program available."
 426        exit 1
 427    fi
 428else
 429    if ! valid_tool "$merge_tool"; then
 430        echo >&2 "Unknown merge_tool $merge_tool"
 431        exit 1
 432    fi
 433
 434    init_merge_tool_path "$merge_tool"
 435
 436    merge_keep_backup="$(git config --bool merge.keepBackup || echo true)"
 437    merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
 438
 439    if test -z "$merge_tool_cmd" && ! type "$merge_tool_path" > /dev/null 2>&1; then
 440        echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
 441        exit 1
 442    fi
 443
 444    if ! test -z "$merge_tool_cmd"; then
 445        merge_tool_trust_exit_code="$(git config --bool mergetool.$merge_tool.trustExitCode || echo false)"
 446    fi
 447fi
 448
 449last_status=0
 450rollup_status=0
 451
 452if test $# -eq 0 ; then
 453    files=`git ls-files -u | sed -e 's/^[^      ]*      //' | sort -u`
 454    if test -z "$files" ; then
 455        echo "No files need merging"
 456        exit 0
 457    fi
 458    echo Merging the files: "$files"
 459    git ls-files -u |
 460    sed -e 's/^[^       ]*      //' |
 461    sort -u |
 462    while IFS= read i
 463    do
 464        if test $last_status -ne 0; then
 465            prompt_after_failed_merge < /dev/tty || exit 1
 466        fi
 467        printf "\n"
 468        merge_file "$i" < /dev/tty > /dev/tty
 469        last_status=$?
 470        if test $last_status -ne 0; then
 471            rollup_status=1
 472        fi
 473    done
 474else
 475    while test $# -gt 0; do
 476        if test $last_status -ne 0; then
 477            prompt_after_failed_merge || exit 1
 478        fi
 479        printf "\n"
 480        merge_file "$1"
 481        last_status=$?
 482        if test $last_status -ne 0; then
 483            rollup_status=1
 484        fi
 485        shift
 486    done
 487fi
 488
 489exit $rollup_status