git-mergetool.shon commit send-email: validate & reconfirm interactive responses (51bbccf)
   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=
  14TOOL_MODE=merge
  15. git-sh-setup
  16. git-mergetool--lib
  17require_work_tree
  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
  40cleanup_temp_files () {
  41    if test "$1" = --save-backup ; then
  42        rm -rf -- "$MERGED.orig"
  43        test -e "$BACKUP" && mv -- "$BACKUP" "$MERGED.orig"
  44        rm -f -- "$LOCAL" "$REMOTE" "$BASE"
  45    else
  46        rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
  47    fi
  48}
  49
  50describe_file () {
  51    mode="$1"
  52    branch="$2"
  53    file="$3"
  54
  55    printf "  {%s}: " "$branch"
  56    if test -z "$mode"; then
  57        echo "deleted"
  58    elif is_symlink "$mode" ; then
  59        echo "a symbolic link -> '$(cat "$file")'"
  60    elif is_submodule "$mode" ; then
  61        echo "submodule commit $file"
  62    else
  63        if base_present; then
  64            echo "modified file"
  65        else
  66            echo "created file"
  67        fi
  68    fi
  69}
  70
  71
  72resolve_symlink_merge () {
  73    while true; do
  74        printf "Use (l)ocal or (r)emote, or (a)bort? "
  75        read ans || return 1
  76        case "$ans" in
  77            [lL]*)
  78                git checkout-index -f --stage=2 -- "$MERGED"
  79                git add -- "$MERGED"
  80                cleanup_temp_files --save-backup
  81                return 0
  82                ;;
  83            [rR]*)
  84                git checkout-index -f --stage=3 -- "$MERGED"
  85                git add -- "$MERGED"
  86                cleanup_temp_files --save-backup
  87                return 0
  88                ;;
  89            [aA]*)
  90                return 1
  91                ;;
  92            esac
  93        done
  94}
  95
  96resolve_deleted_merge () {
  97    while true; do
  98        if base_present; then
  99            printf "Use (m)odified or (d)eleted file, or (a)bort? "
 100        else
 101            printf "Use (c)reated or (d)eleted file, or (a)bort? "
 102        fi
 103        read ans || return 1
 104        case "$ans" in
 105            [mMcC]*)
 106                git add -- "$MERGED"
 107                cleanup_temp_files --save-backup
 108                return 0
 109                ;;
 110            [dD]*)
 111                git rm -- "$MERGED" > /dev/null
 112                cleanup_temp_files
 113                return 0
 114                ;;
 115            [aA]*)
 116                return 1
 117                ;;
 118            esac
 119        done
 120}
 121
 122resolve_submodule_merge () {
 123    while true; do
 124        printf "Use (l)ocal or (r)emote, or (a)bort? "
 125        read ans || return 1
 126        case "$ans" in
 127            [lL]*)
 128                if ! local_present; then
 129                    if test -n "$(git ls-tree HEAD -- "$MERGED")"; then
 130                        # Local isn't present, but it's a subdirectory
 131                        git ls-tree --full-name -r HEAD -- "$MERGED" | git update-index --index-info || exit $?
 132                    else
 133                        test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
 134                        git update-index --force-remove "$MERGED"
 135                        cleanup_temp_files --save-backup
 136                    fi
 137                elif is_submodule "$local_mode"; then
 138                    stage_submodule "$MERGED" "$local_sha1"
 139                else
 140                    git checkout-index -f --stage=2 -- "$MERGED"
 141                    git add -- "$MERGED"
 142                fi
 143                return 0
 144                ;;
 145            [rR]*)
 146                if ! remote_present; then
 147                    if test -n "$(git ls-tree MERGE_HEAD -- "$MERGED")"; then
 148                        # Remote isn't present, but it's a subdirectory
 149                        git ls-tree --full-name -r MERGE_HEAD -- "$MERGED" | git update-index --index-info || exit $?
 150                    else
 151                        test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
 152                        git update-index --force-remove "$MERGED"
 153                    fi
 154                elif is_submodule "$remote_mode"; then
 155                    ! is_submodule "$local_mode" && test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
 156                    stage_submodule "$MERGED" "$remote_sha1"
 157                else
 158                    test -e "$MERGED" && mv -- "$MERGED" "$BACKUP"
 159                    git checkout-index -f --stage=3 -- "$MERGED"
 160                    git add -- "$MERGED"
 161                fi
 162                cleanup_temp_files --save-backup
 163                return 0
 164                ;;
 165            [aA]*)
 166                return 1
 167                ;;
 168            esac
 169        done
 170}
 171
 172stage_submodule () {
 173    path="$1"
 174    submodule_sha1="$2"
 175    mkdir -p "$path" || die "fatal: unable to create directory for module at $path"
 176    # Find $path relative to work tree
 177    work_tree_root=$(cd_to_toplevel && pwd)
 178    work_rel_path=$(cd "$path" && GIT_WORK_TREE="${work_tree_root}" git rev-parse --show-prefix)
 179    test -n "$work_rel_path" || die "fatal: unable to get path of module $path relative to work tree"
 180    git update-index --add --replace --cacheinfo 160000 "$submodule_sha1" "${work_rel_path%/}" || die
 181}
 182
 183checkout_staged_file () {
 184    tmpfile=$(expr "$(git checkout-index --temp --stage="$1" "$2")" : '\([^     ]*\)    ')
 185
 186    if test $? -eq 0 -a -n "$tmpfile" ; then
 187        mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3"
 188    fi
 189}
 190
 191merge_file () {
 192    MERGED="$1"
 193
 194    f=$(git ls-files -u -- "$MERGED")
 195    if test -z "$f" ; then
 196        if test ! -f "$MERGED" ; then
 197            echo "$MERGED: file not found"
 198        else
 199            echo "$MERGED: file does not need merging"
 200        fi
 201        return 1
 202    fi
 203
 204    ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
 205    BACKUP="./$MERGED.BACKUP.$ext"
 206    LOCAL="./$MERGED.LOCAL.$ext"
 207    REMOTE="./$MERGED.REMOTE.$ext"
 208    BASE="./$MERGED.BASE.$ext"
 209
 210    base_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}')
 211    local_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}')
 212    remote_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}')
 213
 214    if is_submodule "$local_mode" || is_submodule "$remote_mode"; then
 215        echo "Submodule merge conflict for '$MERGED':"
 216        local_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $2;}')
 217        remote_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $2;}')
 218        describe_file "$local_mode" "local" "$local_sha1"
 219        describe_file "$remote_mode" "remote" "$remote_sha1"
 220        resolve_submodule_merge
 221        return
 222    fi
 223
 224    mv -- "$MERGED" "$BACKUP"
 225    cp -- "$BACKUP" "$MERGED"
 226
 227    base_present   && checkout_staged_file 1 "$MERGED" "$BASE"
 228    local_present  && checkout_staged_file 2 "$MERGED" "$LOCAL"
 229    remote_present && checkout_staged_file 3 "$MERGED" "$REMOTE"
 230
 231    if test -z "$local_mode" -o -z "$remote_mode"; then
 232        echo "Deleted merge conflict for '$MERGED':"
 233        describe_file "$local_mode" "local" "$LOCAL"
 234        describe_file "$remote_mode" "remote" "$REMOTE"
 235        resolve_deleted_merge
 236        return
 237    fi
 238
 239    if is_symlink "$local_mode" || is_symlink "$remote_mode"; then
 240        echo "Symbolic link merge conflict for '$MERGED':"
 241        describe_file "$local_mode" "local" "$LOCAL"
 242        describe_file "$remote_mode" "remote" "$REMOTE"
 243        resolve_symlink_merge
 244        return
 245    fi
 246
 247    echo "Normal merge conflict for '$MERGED':"
 248    describe_file "$local_mode" "local" "$LOCAL"
 249    describe_file "$remote_mode" "remote" "$REMOTE"
 250    if "$prompt" = true; then
 251        printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
 252        read ans || return 1
 253    fi
 254
 255    if base_present; then
 256            present=true
 257    else
 258            present=false
 259    fi
 260
 261    if ! run_merge_tool "$merge_tool" "$present"; then
 262        echo "merge of $MERGED failed" 1>&2
 263        mv -- "$BACKUP" "$MERGED"
 264
 265        if test "$merge_keep_temporaries" = "false"; then
 266            cleanup_temp_files
 267        fi
 268
 269        return 1
 270    fi
 271
 272    if test "$merge_keep_backup" = "true"; then
 273        mv -- "$BACKUP" "$MERGED.orig"
 274    else
 275        rm -- "$BACKUP"
 276    fi
 277
 278    git add -- "$MERGED"
 279    cleanup_temp_files
 280    return 0
 281}
 282
 283prompt=$(git config --bool mergetool.prompt || echo true)
 284
 285while test $# != 0
 286do
 287    case "$1" in
 288        -t|--tool*)
 289            case "$#,$1" in
 290                *,*=*)
 291                    merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
 292                    ;;
 293                1,*)
 294                    usage ;;
 295                *)
 296                    merge_tool="$2"
 297                    shift ;;
 298            esac
 299            ;;
 300        -y|--no-prompt)
 301            prompt=false
 302            ;;
 303        --prompt)
 304            prompt=true
 305            ;;
 306        --)
 307            shift
 308            break
 309            ;;
 310        -*)
 311            usage
 312            ;;
 313        *)
 314            break
 315            ;;
 316    esac
 317    shift
 318done
 319
 320prompt_after_failed_merge() {
 321    while true; do
 322        printf "Continue merging other unresolved paths (y/n) ? "
 323        read ans || return 1
 324        case "$ans" in
 325
 326            [yY]*)
 327                return 0
 328                ;;
 329
 330            [nN]*)
 331                return 1
 332                ;;
 333        esac
 334    done
 335}
 336
 337if test -z "$merge_tool"; then
 338    merge_tool=$(get_merge_tool "$merge_tool") || exit
 339fi
 340merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
 341merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
 342
 343last_status=0
 344rollup_status=0
 345files=
 346
 347if test $# -eq 0 ; then
 348    cd_to_toplevel
 349
 350    if test -e "$GIT_DIR/MERGE_RR"
 351    then
 352        files=$(git rerere remaining)
 353    else
 354        files=$(git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u)
 355    fi
 356else
 357    files=$(git ls-files -u -- "$@" | sed -e 's/^[^     ]*      //' | sort -u)
 358fi
 359
 360if test -z "$files" ; then
 361    echo "No files need merging"
 362    exit 0
 363fi
 364
 365printf "Merging:\n"
 366printf "$files\n"
 367
 368IFS='
 369'
 370for i in $files
 371do
 372    if test $last_status -ne 0; then
 373        prompt_after_failed_merge || exit 1
 374    fi
 375    printf "\n"
 376    merge_file "$i"
 377    last_status=$?
 378    if test $last_status -ne 0; then
 379        rollup_status=1
 380    fi
 381done
 382
 383exit $rollup_status