git-mergetool.shon commit mergetool: Add prompt to continue after failing to merge a file (b0169d8)
   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
 130merge_file () {
 131    MERGED="$1"
 132
 133    f=`git ls-files -u -- "$MERGED"`
 134    if test -z "$f" ; then
 135        if test ! -f "$MERGED" ; then
 136            echo "$MERGED: file not found"
 137        else
 138            echo "$MERGED: file does not need merging"
 139        fi
 140        return 1
 141    fi
 142
 143    ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
 144    BACKUP="./$MERGED.BACKUP.$ext"
 145    LOCAL="./$MERGED.LOCAL.$ext"
 146    REMOTE="./$MERGED.REMOTE.$ext"
 147    BASE="./$MERGED.BASE.$ext"
 148
 149    mv -- "$MERGED" "$BACKUP"
 150    cp -- "$BACKUP" "$MERGED"
 151
 152    base_mode=`git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}'`
 153    local_mode=`git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}'`
 154    remote_mode=`git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}'`
 155
 156    base_present   && git cat-file blob ":1:$prefix$MERGED" >"$BASE" 2>/dev/null
 157    local_present  && git cat-file blob ":2:$prefix$MERGED" >"$LOCAL" 2>/dev/null
 158    remote_present && git cat-file blob ":3:$prefix$MERGED" >"$REMOTE" 2>/dev/null
 159
 160    if test -z "$local_mode" -o -z "$remote_mode"; then
 161        echo "Deleted merge conflict for '$MERGED':"
 162        describe_file "$local_mode" "local" "$LOCAL"
 163        describe_file "$remote_mode" "remote" "$REMOTE"
 164        resolve_deleted_merge
 165        return
 166    fi
 167
 168    if is_symlink "$local_mode" || is_symlink "$remote_mode"; then
 169        echo "Symbolic link merge conflict for '$MERGED':"
 170        describe_file "$local_mode" "local" "$LOCAL"
 171        describe_file "$remote_mode" "remote" "$REMOTE"
 172        resolve_symlink_merge
 173        return
 174    fi
 175
 176    echo "Normal merge conflict for '$MERGED':"
 177    describe_file "$local_mode" "local" "$LOCAL"
 178    describe_file "$remote_mode" "remote" "$REMOTE"
 179    if "$prompt" = true; then
 180        printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
 181        read ans
 182    fi
 183
 184    case "$merge_tool" in
 185        kdiff3)
 186            if base_present ; then
 187                ("$merge_tool_path" --auto --L1 "$MERGED (Base)" --L2 "$MERGED (Local)" --L3 "$MERGED (Remote)" \
 188                    -o "$MERGED" "$BASE" "$LOCAL" "$REMOTE" > /dev/null 2>&1)
 189            else
 190                ("$merge_tool_path" --auto --L1 "$MERGED (Local)" --L2 "$MERGED (Remote)" \
 191                    -o "$MERGED" "$LOCAL" "$REMOTE" > /dev/null 2>&1)
 192            fi
 193            status=$?
 194            ;;
 195        tkdiff)
 196            if base_present ; then
 197                "$merge_tool_path" -a "$BASE" -o "$MERGED" "$LOCAL" "$REMOTE"
 198            else
 199                "$merge_tool_path" -o "$MERGED" "$LOCAL" "$REMOTE"
 200            fi
 201            status=$?
 202            ;;
 203        meld|vimdiff)
 204            touch "$BACKUP"
 205            "$merge_tool_path" "$LOCAL" "$MERGED" "$REMOTE"
 206            check_unchanged
 207            ;;
 208        gvimdiff)
 209            touch "$BACKUP"
 210            "$merge_tool_path" -f "$LOCAL" "$MERGED" "$REMOTE"
 211            check_unchanged
 212            ;;
 213        xxdiff)
 214            touch "$BACKUP"
 215            if base_present ; then
 216                "$merge_tool_path" -X --show-merged-pane \
 217                    -R 'Accel.SaveAsMerged: "Ctrl-S"' \
 218                    -R 'Accel.Search: "Ctrl+F"' \
 219                    -R 'Accel.SearchForward: "Ctrl-G"' \
 220                    --merged-file "$MERGED" "$LOCAL" "$BASE" "$REMOTE"
 221            else
 222                "$merge_tool_path" -X --show-merged-pane \
 223                    -R 'Accel.SaveAsMerged: "Ctrl-S"' \
 224                    -R 'Accel.Search: "Ctrl+F"' \
 225                    -R 'Accel.SearchForward: "Ctrl-G"' \
 226                    --merged-file "$MERGED" "$LOCAL" "$REMOTE"
 227            fi
 228            check_unchanged
 229            ;;
 230        opendiff)
 231            touch "$BACKUP"
 232            if base_present; then
 233                "$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$MERGED" | cat
 234            else
 235                "$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$MERGED" | cat
 236            fi
 237            check_unchanged
 238            ;;
 239        ecmerge)
 240            touch "$BACKUP"
 241            if base_present; then
 242                "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --default --mode=merge3 --to="$MERGED"
 243            else
 244                "$merge_tool_path" "$LOCAL" "$REMOTE" --default --mode=merge2 --to="$MERGED"
 245            fi
 246            check_unchanged
 247            ;;
 248        emerge)
 249            if base_present ; then
 250                "$merge_tool_path" -f emerge-files-with-ancestor-command "$LOCAL" "$REMOTE" "$BASE" "$(basename "$MERGED")"
 251            else
 252                "$merge_tool_path" -f emerge-files-command "$LOCAL" "$REMOTE" "$(basename "$MERGED")"
 253            fi
 254            status=$?
 255            ;;
 256        *)
 257            if test -n "$merge_tool_cmd"; then
 258                if test "$merge_tool_trust_exit_code" = "false"; then
 259                    touch "$BACKUP"
 260                    ( eval $merge_tool_cmd )
 261                    check_unchanged
 262                else
 263                    ( eval $merge_tool_cmd )
 264                    status=$?
 265                fi
 266            fi
 267            ;;
 268    esac
 269    if test "$status" -ne 0; then
 270        echo "merge of $MERGED failed" 1>&2
 271        mv -- "$BACKUP" "$MERGED"
 272        return 1
 273    fi
 274
 275    if test "$merge_keep_backup" = "true"; then
 276        mv -- "$BACKUP" "$MERGED.orig"
 277    else
 278        rm -- "$BACKUP"
 279    fi
 280
 281    git add -- "$MERGED"
 282    cleanup_temp_files
 283    return 0
 284}
 285
 286prompt=$(git config --bool mergetool.prompt || echo true)
 287
 288while test $# != 0
 289do
 290    case "$1" in
 291        -t|--tool*)
 292            case "$#,$1" in
 293                *,*=*)
 294                    merge_tool=`expr "z$1" : 'z-[^=]*=\(.*\)'`
 295                    ;;
 296                1,*)
 297                    usage ;;
 298                *)
 299                    merge_tool="$2"
 300                    shift ;;
 301            esac
 302            ;;
 303        -y|--no-prompt)
 304            prompt=false
 305            ;;
 306        --prompt)
 307            prompt=true
 308            ;;
 309        --)
 310            break
 311            ;;
 312        -*)
 313            usage
 314            ;;
 315        *)
 316            break
 317            ;;
 318    esac
 319    shift
 320done
 321
 322valid_custom_tool()
 323{
 324    merge_tool_cmd="$(git config mergetool.$1.cmd)"
 325    test -n "$merge_tool_cmd"
 326}
 327
 328valid_tool() {
 329        case "$1" in
 330                kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
 331                        ;; # happy
 332                *)
 333                        if ! valid_custom_tool "$1"; then
 334                                return 1
 335                        fi
 336                        ;;
 337        esac
 338}
 339
 340init_merge_tool_path() {
 341        merge_tool_path=`git config mergetool.$1.path`
 342        if test -z "$merge_tool_path" ; then
 343                case "$1" in
 344                        emerge)
 345                                merge_tool_path=emacs
 346                                ;;
 347                        *)
 348                                merge_tool_path=$1
 349                                ;;
 350                esac
 351        fi
 352}
 353
 354prompt_after_failed_merge() {
 355    while true; do
 356        printf "Continue merging other unresolved paths (y/n) ? "
 357        read ans
 358        case "$ans" in
 359
 360            [yY]*)
 361                return 0
 362                ;;
 363
 364            [nN]*)
 365                return 1
 366                ;;
 367        esac
 368    done
 369}
 370
 371if test -z "$merge_tool"; then
 372    merge_tool=`git config merge.tool`
 373    if test -n "$merge_tool" && ! valid_tool "$merge_tool"; then
 374            echo >&2 "git config option merge.tool set to unknown tool: $merge_tool"
 375            echo >&2 "Resetting to default..."
 376            unset merge_tool
 377    fi
 378fi
 379
 380if test -z "$merge_tool" ; then
 381    if test -n "$DISPLAY"; then
 382        merge_tool_candidates="kdiff3 tkdiff xxdiff meld gvimdiff"
 383        if test -n "$GNOME_DESKTOP_SESSION_ID" ; then
 384            merge_tool_candidates="meld $merge_tool_candidates"
 385        fi
 386        if test "$KDE_FULL_SESSION" = "true"; then
 387            merge_tool_candidates="kdiff3 $merge_tool_candidates"
 388        fi
 389    fi
 390    if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
 391        merge_tool_candidates="$merge_tool_candidates emerge"
 392    fi
 393    if echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
 394        merge_tool_candidates="$merge_tool_candidates vimdiff"
 395    fi
 396    merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
 397    echo "merge tool candidates: $merge_tool_candidates"
 398    for i in $merge_tool_candidates; do
 399        init_merge_tool_path $i
 400        if type "$merge_tool_path" > /dev/null 2>&1; then
 401            merge_tool=$i
 402            break
 403        fi
 404    done
 405    if test -z "$merge_tool" ; then
 406        echo "No known merge resolution program available."
 407        exit 1
 408    fi
 409else
 410    if ! valid_tool "$merge_tool"; then
 411        echo >&2 "Unknown merge_tool $merge_tool"
 412        exit 1
 413    fi
 414
 415    init_merge_tool_path "$merge_tool"
 416
 417    merge_keep_backup="$(git config --bool merge.keepBackup || echo true)"
 418
 419    if test -z "$merge_tool_cmd" && ! type "$merge_tool_path" > /dev/null 2>&1; then
 420        echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
 421        exit 1
 422    fi
 423
 424    if ! test -z "$merge_tool_cmd"; then
 425        merge_tool_trust_exit_code="$(git config --bool mergetool.$merge_tool.trustExitCode || echo false)"
 426    fi
 427fi
 428
 429last_status=0
 430rollup_status=0
 431
 432if test $# -eq 0 ; then
 433    files=`git ls-files -u | sed -e 's/^[^      ]*      //' | sort -u`
 434    if test -z "$files" ; then
 435        echo "No files need merging"
 436        exit 0
 437    fi
 438    echo Merging the files: "$files"
 439    git ls-files -u |
 440    sed -e 's/^[^       ]*      //' |
 441    sort -u |
 442    while IFS= read i
 443    do
 444        if test $last_status -ne 0; then
 445            prompt_after_failed_merge < /dev/tty || exit 1
 446        fi
 447        printf "\n"
 448        merge_file "$i" < /dev/tty > /dev/tty
 449        last_status=$?
 450        if test $last_status -ne 0; then
 451            rollup_status=1
 452        fi
 453    done
 454else
 455    while test $# -gt 0; do
 456        if test $last_status -ne 0; then
 457            prompt_after_failed_merge || exit 1
 458        fi
 459        printf "\n"
 460        merge_file "$1"
 461        last_status=$?
 462        if test $last_status -ne 0; then
 463            rollup_status=1
 464        fi
 465        shift
 466    done
 467fi
 468
 469exit $rollup_status