git-mergetool.shon commit git-svn: platform auth providers are working only on 1.6.15 or newer (c26ddce)
   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 \
 185            "$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" \
 186            : '\([^     ]*\)    ')
 187
 188    if test $? -eq 0 -a -n "$tmpfile" ; then
 189        mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3"
 190    else
 191        >"$3"
 192    fi
 193}
 194
 195merge_file () {
 196    MERGED="$1"
 197
 198    f=$(git ls-files -u -- "$MERGED")
 199    if test -z "$f" ; then
 200        if test ! -f "$MERGED" ; then
 201            echo "$MERGED: file not found"
 202        else
 203            echo "$MERGED: file does not need merging"
 204        fi
 205        return 1
 206    fi
 207
 208    ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
 209    BACKUP="./$MERGED.BACKUP.$ext"
 210    LOCAL="./$MERGED.LOCAL.$ext"
 211    REMOTE="./$MERGED.REMOTE.$ext"
 212    BASE="./$MERGED.BASE.$ext"
 213
 214    base_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}')
 215    local_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}')
 216    remote_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}')
 217
 218    if is_submodule "$local_mode" || is_submodule "$remote_mode"; then
 219        echo "Submodule merge conflict for '$MERGED':"
 220        local_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $2;}')
 221        remote_sha1=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $2;}')
 222        describe_file "$local_mode" "local" "$local_sha1"
 223        describe_file "$remote_mode" "remote" "$remote_sha1"
 224        resolve_submodule_merge
 225        return
 226    fi
 227
 228    mv -- "$MERGED" "$BACKUP"
 229    cp -- "$BACKUP" "$MERGED"
 230
 231    checkout_staged_file 1 "$MERGED" "$BASE"
 232    checkout_staged_file 2 "$MERGED" "$LOCAL"
 233    checkout_staged_file 3 "$MERGED" "$REMOTE"
 234
 235    if test -z "$local_mode" -o -z "$remote_mode"; then
 236        echo "Deleted merge conflict for '$MERGED':"
 237        describe_file "$local_mode" "local" "$LOCAL"
 238        describe_file "$remote_mode" "remote" "$REMOTE"
 239        resolve_deleted_merge
 240        return
 241    fi
 242
 243    if is_symlink "$local_mode" || is_symlink "$remote_mode"; then
 244        echo "Symbolic link merge conflict for '$MERGED':"
 245        describe_file "$local_mode" "local" "$LOCAL"
 246        describe_file "$remote_mode" "remote" "$REMOTE"
 247        resolve_symlink_merge
 248        return
 249    fi
 250
 251    echo "Normal merge conflict for '$MERGED':"
 252    describe_file "$local_mode" "local" "$LOCAL"
 253    describe_file "$remote_mode" "remote" "$REMOTE"
 254    if "$prompt" = true; then
 255        printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
 256        read ans || return 1
 257    fi
 258
 259    if base_present; then
 260            present=true
 261    else
 262            present=false
 263    fi
 264
 265    if ! run_merge_tool "$merge_tool" "$present"; then
 266        echo "merge of $MERGED failed" 1>&2
 267        mv -- "$BACKUP" "$MERGED"
 268
 269        if test "$merge_keep_temporaries" = "false"; then
 270            cleanup_temp_files
 271        fi
 272
 273        return 1
 274    fi
 275
 276    if test "$merge_keep_backup" = "true"; then
 277        mv -- "$BACKUP" "$MERGED.orig"
 278    else
 279        rm -- "$BACKUP"
 280    fi
 281
 282    git add -- "$MERGED"
 283    cleanup_temp_files
 284    return 0
 285}
 286
 287prompt=$(git config --bool mergetool.prompt || echo true)
 288
 289while test $# != 0
 290do
 291    case "$1" in
 292        -t|--tool*)
 293            case "$#,$1" in
 294                *,*=*)
 295                    merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
 296                    ;;
 297                1,*)
 298                    usage ;;
 299                *)
 300                    merge_tool="$2"
 301                    shift ;;
 302            esac
 303            ;;
 304        -y|--no-prompt)
 305            prompt=false
 306            ;;
 307        --prompt)
 308            prompt=true
 309            ;;
 310        --)
 311            shift
 312            break
 313            ;;
 314        -*)
 315            usage
 316            ;;
 317        *)
 318            break
 319            ;;
 320    esac
 321    shift
 322done
 323
 324prompt_after_failed_merge() {
 325    while true; do
 326        printf "Continue merging other unresolved paths (y/n) ? "
 327        read ans || return 1
 328        case "$ans" in
 329
 330            [yY]*)
 331                return 0
 332                ;;
 333
 334            [nN]*)
 335                return 1
 336                ;;
 337        esac
 338    done
 339}
 340
 341if test -z "$merge_tool"; then
 342    merge_tool=$(get_merge_tool "$merge_tool") || exit
 343fi
 344merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
 345merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
 346
 347last_status=0
 348rollup_status=0
 349files=
 350
 351if test $# -eq 0 ; then
 352    cd_to_toplevel
 353
 354    if test -e "$GIT_DIR/MERGE_RR"
 355    then
 356        files=$(git rerere remaining)
 357    else
 358        files=$(git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u)
 359    fi
 360else
 361    files=$(git ls-files -u -- "$@" | sed -e 's/^[^     ]*      //' | sort -u)
 362fi
 363
 364if test -z "$files" ; then
 365    echo "No files need merging"
 366    exit 0
 367fi
 368
 369printf "Merging:\n"
 370printf "$files\n"
 371
 372IFS='
 373'
 374for i in $files
 375do
 376    if test $last_status -ne 0; then
 377        prompt_after_failed_merge || exit 1
 378    fi
 379    printf "\n"
 380    merge_file "$i"
 381    last_status=$?
 382    if test $last_status -ne 0; then
 383        rollup_status=1
 384    fi
 385done
 386
 387exit $rollup_status