merge: let custom strategies intervene in trivial merges
[gitweb.git] / git-mergetool.sh
index 47a80553ad3283c49cae0fbffcf444b93c8e34b7..b52a7410bcb7b37dce0f4d6213dddedd2c1e42e7 100755 (executable)
@@ -8,9 +8,12 @@
 # at the discretion of Junio C Hamano.
 #
 
-USAGE='[--tool=tool] [file to merge] ...'
+USAGE='[--tool=tool] [-y|--no-prompt|--prompt] [file to merge] ...'
 SUBDIRECTORY_OK=Yes
+OPTIONS_SPEC=
+TOOL_MODE=merge
 . git-sh-setup
+. git-mergetool--lib
 require_work_tree
 
 # Returns true if the mode reflects a symlink
@@ -32,7 +35,7 @@ base_present () {
 
 cleanup_temp_files () {
     if test "$1" = --save-backup ; then
-       mv -- "$BACKUP" "$path.orig"
+       mv -- "$BACKUP" "$MERGED.orig"
        rm -f -- "$LOCAL" "$REMOTE" "$BASE"
     else
        rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
@@ -65,19 +68,19 @@ resolve_symlink_merge () {
        read ans
        case "$ans" in
            [lL]*)
-               git checkout-index -f --stage=2 -- "$path"
-               git add -- "$path"
+               git checkout-index -f --stage=2 -- "$MERGED"
+               git add -- "$MERGED"
                cleanup_temp_files --save-backup
-               return
+               return 0
                ;;
            [rR]*)
-               git checkout-index -f --stage=3 -- "$path"
-               git add -- "$path"
+               git checkout-index -f --stage=3 -- "$MERGED"
+               git add -- "$MERGED"
                cleanup_temp_files --save-backup
-               return
+               return 0
                ;;
            [aA]*)
-               exit 1
+               return 1
                ;;
            esac
        done
@@ -93,81 +96,62 @@ resolve_deleted_merge () {
        read ans
        case "$ans" in
            [mMcC]*)
-               git add -- "$path"
+               git add -- "$MERGED"
                cleanup_temp_files --save-backup
-               return
+               return 0
                ;;
            [dD]*)
-               git rm -- "$path" > /dev/null
+               git rm -- "$MERGED" > /dev/null
                cleanup_temp_files
-               return
+               return 0
                ;;
            [aA]*)
-               exit 1
+               return 1
                ;;
            esac
        done
 }
 
-check_unchanged () {
-    if test "$path" -nt "$BACKUP" ; then
-       status=0;
-    else
-       while true; do
-           echo "$path seems unchanged."
-           printf "Was the merge successful? [y/n] "
-           read answer < /dev/tty
-           case "$answer" in
-               y*|Y*) status=0; break ;;
-               n*|N*) status=1; break ;;
-           esac
-       done
-    fi
-}
-
-save_backup () {
-    if test "$status" -eq 0; then
-       mv -- "$BACKUP" "$path.orig"
-    fi
-}
+checkout_staged_file () {
+    tmpfile=$(expr "$(git checkout-index --temp --stage="$1" "$2")" : '\([^    ]*\)    ')
 
-remove_backup () {
-    if test "$status" -eq 0; then
-       rm "$BACKUP"
+    if test $? -eq 0 -a -n "$tmpfile" ; then
+       mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3"
     fi
 }
 
 merge_file () {
-    path="$1"
+    MERGED="$1"
 
-    f=`git ls-files -u -- "$path"`
+    f=$(git ls-files -u -- "$MERGED")
     if test -z "$f" ; then
-       if test ! -f "$path" ; then
-           echo "$path: file not found"
+       if test ! -f "$MERGED" ; then
+           echo "$MERGED: file not found"
        else
-           echo "$path: file does not need merging"
+           echo "$MERGED: file does not need merging"
        fi
-       exit 1
+       return 1
     fi
 
-    BACKUP="$path.BACKUP.$$"
-    LOCAL="$path.LOCAL.$$"
-    REMOTE="$path.REMOTE.$$"
-    BASE="$path.BASE.$$"
+    ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
+    BACKUP="./$MERGED.BACKUP.$ext"
+    LOCAL="./$MERGED.LOCAL.$ext"
+    REMOTE="./$MERGED.REMOTE.$ext"
+    BASE="./$MERGED.BASE.$ext"
 
-    mv -- "$path" "$BACKUP"
-    cp -- "$BACKUP" "$path"
+    mv -- "$MERGED" "$BACKUP"
+    cp -- "$BACKUP" "$MERGED"
 
-    base_mode=`git ls-files -u -- "$path" | awk '{if ($3==1) print $1;}'`
-    local_mode=`git ls-files -u -- "$path" | awk '{if ($3==2) print $1;}'`
-    remote_mode=`git ls-files -u -- "$path" | awk '{if ($3==3) print $1;}'`
+    base_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}')
+    local_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}')
+    remote_mode=$(git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}')
 
-    base_present   && git cat-file blob ":1:$path" > "$BASE" 2>/dev/null
-    local_present  && git cat-file blob ":2:$path" > "$LOCAL" 2>/dev/null
-    remote_present && git cat-file blob ":3:$path" > "$REMOTE" 2>/dev/null
+    base_present   && checkout_staged_file 1 "$MERGED" "$BASE"
+    local_present  && checkout_staged_file 2 "$MERGED" "$LOCAL"
+    remote_present && checkout_staged_file 3 "$MERGED" "$REMOTE"
 
     if test -z "$local_mode" -o -z "$remote_mode"; then
-       echo "Deleted merge conflict for '$path':"
+       echo "Deleted merge conflict for '$MERGED':"
        describe_file "$local_mode" "local" "$LOCAL"
        describe_file "$remote_mode" "remote" "$REMOTE"
        resolve_deleted_merge
@@ -175,106 +159,58 @@ merge_file () {
     fi
 
     if is_symlink "$local_mode" || is_symlink "$remote_mode"; then
-       echo "Symbolic link merge conflict for '$path':"
+       echo "Symbolic link merge conflict for '$MERGED':"
        describe_file "$local_mode" "local" "$LOCAL"
        describe_file "$remote_mode" "remote" "$REMOTE"
        resolve_symlink_merge
        return
     fi
 
-    echo "Normal merge conflict for '$path':"
+    echo "Normal merge conflict for '$MERGED':"
     describe_file "$local_mode" "local" "$LOCAL"
     describe_file "$remote_mode" "remote" "$REMOTE"
-    printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
-    read ans
-
-    case "$merge_tool" in
-       kdiff3)
-           if base_present ; then
-               (kdiff3 --auto --L1 "$path (Base)" -L2 "$path (Local)" --L3 "$path (Remote)" \
-                   -o "$path" -- "$BASE" "$LOCAL" "$REMOTE" > /dev/null 2>&1)
-           else
-               (kdiff3 --auto -L1 "$path (Local)" --L2 "$path (Remote)" \
-                   -o "$path" -- "$LOCAL" "$REMOTE" > /dev/null 2>&1)
-           fi
-           status=$?
-           remove_backup
-           ;;
-       tkdiff)
-           if base_present ; then
-               tkdiff -a "$BASE" -o "$path" -- "$LOCAL" "$REMOTE"
-           else
-               tkdiff -o "$path" -- "$LOCAL" "$REMOTE"
-           fi
-           status=$?
-           save_backup
-           ;;
-       meld|vimdiff)
-           touch "$BACKUP"
-           $merge_tool -- "$LOCAL" "$path" "$REMOTE"
-           check_unchanged
-           save_backup
-           ;;
-       gvimdiff)
-               touch "$BACKUP"
-               gvimdiff -f -- "$LOCAL" "$path" "$REMOTE"
-               check_unchanged
-               save_backup
-               ;;
-       xxdiff)
-           touch "$BACKUP"
-           if base_present ; then
-               xxdiff -X --show-merged-pane \
-                   -R 'Accel.SaveAsMerged: "Ctrl-S"' \
-                   -R 'Accel.Search: "Ctrl+F"' \
-                   -R 'Accel.SearchForward: "Ctrl-G"' \
-                   --merged-file "$path" -- "$LOCAL" "$BASE" "$REMOTE"
-           else
-               xxdiff -X --show-merged-pane \
-                   -R 'Accel.SaveAsMerged: "Ctrl-S"' \
-                   -R 'Accel.Search: "Ctrl+F"' \
-                   -R 'Accel.SearchForward: "Ctrl-G"' \
-                   --merged-file "$path" -- "$LOCAL" "$REMOTE"
-           fi
-           check_unchanged
-           save_backup
-           ;;
-       opendiff)
-           touch "$BACKUP"
-           if base_present; then
-               opendiff "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$path" | cat
-           else
-               opendiff "$LOCAL" "$REMOTE" -merge "$path" | cat
-           fi
-           check_unchanged
-           save_backup
-           ;;
-       emerge)
-           if base_present ; then
-               emacs -f emerge-files-with-ancestor-command "$LOCAL" "$REMOTE" "$BASE" "$path"
-           else
-               emacs -f emerge-files-command "$LOCAL" "$REMOTE" "$path"
-           fi
-           status=$?
-           save_backup
-           ;;
-    esac
-    if test "$status" -ne 0; then
-       echo "merge of $path failed" 1>&2
-       mv -- "$BACKUP" "$path"
-       exit 1
+    if "$prompt" = true; then
+       printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
+       read ans
+    fi
+
+    if base_present; then
+           present=true
+    else
+           present=false
     fi
-    git add -- "$path"
+
+    if ! run_merge_tool "$merge_tool" "$present"; then
+       echo "merge of $MERGED failed" 1>&2
+       mv -- "$BACKUP" "$MERGED"
+
+       if test "$merge_keep_temporaries" = "false"; then
+           cleanup_temp_files
+       fi
+
+       return 1
+    fi
+
+    if test "$merge_keep_backup" = "true"; then
+       mv -- "$BACKUP" "$MERGED.orig"
+    else
+       rm -- "$BACKUP"
+    fi
+
+    git add -- "$MERGED"
     cleanup_temp_files
+    return 0
 }
 
-while case $# in 0) break ;; esac
+prompt=$(git config --bool mergetool.prompt || echo true)
+
+while test $# != 0
 do
     case "$1" in
        -t|--tool*)
            case "$#,$1" in
                *,*=*)
-                   merge_tool=`expr "z$1" : 'z-[^=]*=\(.*\)'`
+                   merge_tool=$(expr "z$1" : 'z-[^=]*=\(.*\)')
                    ;;
                1,*)
                    usage ;;
@@ -283,7 +219,14 @@ do
                    shift ;;
            esac
            ;;
+       -y|--no-prompt)
+           prompt=false
+           ;;
+       --prompt)
+           prompt=true
+           ;;
        --)
+           shift
            break
            ;;
        -*)
@@ -296,90 +239,67 @@ do
     shift
 done
 
-if test -z "$merge_tool"; then
-    merge_tool=`git config merge.tool`
-    case "$merge_tool" in
-       kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | "")
-           ;; # happy
-       *)
-           echo >&2 "git config option merge.tool set to unknown tool: $merge_tool"
-           echo >&2 "Resetting to default..."
-           unset merge_tool
-           ;;
-    esac
-fi
+prompt_after_failed_merge() {
+    while true; do
+       printf "Continue merging other unresolved paths (y/n) ? "
+       read ans
+       case "$ans" in
 
-if test -z "$merge_tool" ; then
-    if test -n "$DISPLAY"; then
-        merge_tool_candidates="kdiff3 tkdiff xxdiff meld gvimdiff"
-        if test -n "$GNOME_DESKTOP_SESSION_ID" ; then
-            merge_tool_candidates="meld $merge_tool_candidates"
-        fi
-        if test "$KDE_FULL_SESSION" = "true"; then
-            merge_tool_candidates="kdiff3 $merge_tool_candidates"
-        fi
-    fi
-    if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
-        merge_tool_candidates="$merge_tool_candidates emerge"
-    fi
-    if echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
-        merge_tool_candidates="$merge_tool_candidates vimdiff"
-    fi
-    merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
-    echo "merge tool candidates: $merge_tool_candidates"
-    for i in $merge_tool_candidates; do
-        if test $i = emerge ; then
-            cmd=emacs
-        else
-            cmd=$i
-        fi
-        if type $cmd > /dev/null 2>&1; then
-            merge_tool=$i
-            break
-        fi
+           [yY]*)
+               return 0
+               ;;
+
+           [nN]*)
+               return 1
+               ;;
+       esac
     done
-    if test -z "$merge_tool" ; then
-       echo "No available merge resolution programs available."
-       exit 1
-    fi
+}
+
+if test -z "$merge_tool"; then
+    merge_tool=$(get_merge_tool "$merge_tool") || exit
 fi
+merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)"
+merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)"
 
-case "$merge_tool" in
-    kdiff3|tkdiff|meld|xxdiff|vimdiff|gvimdiff|opendiff)
-       if ! type "$merge_tool" > /dev/null 2>&1; then
-           echo "The merge tool $merge_tool is not available"
-           exit 1
-       fi
-       ;;
-    emerge)
-       if ! type "emacs" > /dev/null 2>&1; then
-           echo "Emacs is not available"
-           exit 1
-       fi
-       ;;
-    *)
-       echo "Unknown merge tool: $merge_tool"
-       exit 1
-       ;;
-esac
+last_status=0
+rollup_status=0
 
 if test $# -eq 0 ; then
-       files=`git ls-files -u | sed -e 's/^[^  ]*      //' | sort -u`
-       if test -z "$files" ; then
-               echo "No files need merging"
-               exit 0
+    files=$(git ls-files -u | sed -e 's/^[^    ]*      //' | sort -u)
+    if test -z "$files" ; then
+       echo "No files need merging"
+       exit 0
+    fi
+    echo Merging the files: "$files"
+    git ls-files -u |
+    sed -e 's/^[^      ]*      //' |
+    sort -u |
+    while IFS= read i
+    do
+       if test $last_status -ne 0; then
+           prompt_after_failed_merge < /dev/tty || exit 1
        fi
-       echo Merging the files: $files
-       git ls-files -u | sed -e 's/^[^ ]*      //' | sort -u | while read i
-       do
-               printf "\n"
-               merge_file "$i" < /dev/tty > /dev/tty
-       done
+       printf "\n"
+       merge_file "$i" < /dev/tty > /dev/tty
+       last_status=$?
+       if test $last_status -ne 0; then
+           rollup_status=1
+       fi
+    done
 else
-       while test $# -gt 0; do
-               printf "\n"
-               merge_file "$1"
-               shift
-       done
+    while test $# -gt 0; do
+       if test $last_status -ne 0; then
+           prompt_after_failed_merge || exit 1
+       fi
+       printf "\n"
+       merge_file "$1"
+       last_status=$?
+       if test $last_status -ne 0; then
+           rollup_status=1
+       fi
+       shift
+    done
 fi
-exit 0
+
+exit $rollup_status