Merge branch 'cb/mergetool'

* cb/mergetool:
  Add a very basic test script for git mergetool
  Teach git mergetool to use custom commands defined at config time
  Changed an internal variable of mergetool to support custom commands
  Tidy up git mergetool's backup file behaviour
This commit is contained in:
Junio C Hamano
2008-03-07 22:30:07 -08:00
3 changed files with 162 additions and 76 deletions

View File

@ -749,8 +749,10 @@ merge.summary::
merge.tool:: merge.tool::
Controls which merge resolution program is used by Controls which merge resolution program is used by
linkgit:git-mergetool[1]. Valid values are: "kdiff3", "tkdiff", linkgit:git-mergetool[1]. Valid built-in values are: "kdiff3",
"meld", "xxdiff", "emerge", "vimdiff", "gvimdiff", and "opendiff". "tkdiff", "meld", "xxdiff", "emerge", "vimdiff", "gvimdiff", and
"opendiff". Any other value is treated is custom merge tool
and there must be a corresponing mergetool.<tool>.cmd option.
merge.verbosity:: merge.verbosity::
Controls the amount of output shown by the recursive merge Controls the amount of output shown by the recursive merge
@ -777,6 +779,31 @@ mergetool.<tool>.path::
Override the path for the given tool. This is useful in case Override the path for the given tool. This is useful in case
your tool is not in the PATH. your tool is not in the PATH.
mergetool.<tool>.cmd::
Specify the command to invoke the specified merge tool. The
specified command is evaluated in shell with the following
variables available: 'BASE' is the name of a temporary file
containing the common base of the files to be merged, if available;
'LOCAL' is the name of a temporary file containing the contents of
the file on the current branch; 'REMOTE' is the name of a temporary
file containing the contents of the file from the branch being
merged; 'MERGED' contains the name of the file to which the merge
tool should write the results of a successful merge.
mergetool.<tool>.trustExitCode::
For a custom merge command, specify whether the exit code of
the merge command can be used to determine whether the merge was
successful. If this is not set to true then the merge target file
timestamp is checked and the merge assumed to have been successful
if the file has been updated, otherwise the user is prompted to
indicate the success of the merge.
mergetool.keepBackup::
After performing a merge, the original file with conflict markers
can be saved as a file with a `.orig` extension. If this variable
is set to `false` then this file is not preserved. Defaults to
`true` (i.e. keep the backup files).
pack.window:: pack.window::
The size of the window used by linkgit:git-pack-objects[1] when no The size of the window used by linkgit:git-pack-objects[1] when no
window size is given on the command line. Defaults to 10. window size is given on the command line. Defaults to 10.

View File

@ -34,7 +34,7 @@ base_present () {
cleanup_temp_files () { cleanup_temp_files () {
if test "$1" = --save-backup ; then if test "$1" = --save-backup ; then
mv -- "$BACKUP" "$path.orig" mv -- "$BACKUP" "$MERGED.orig"
rm -f -- "$LOCAL" "$REMOTE" "$BASE" rm -f -- "$LOCAL" "$REMOTE" "$BASE"
else else
rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP" rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
@ -67,14 +67,14 @@ resolve_symlink_merge () {
read ans read ans
case "$ans" in case "$ans" in
[lL]*) [lL]*)
git checkout-index -f --stage=2 -- "$path" git checkout-index -f --stage=2 -- "$MERGED"
git add -- "$path" git add -- "$MERGED"
cleanup_temp_files --save-backup cleanup_temp_files --save-backup
return return
;; ;;
[rR]*) [rR]*)
git checkout-index -f --stage=3 -- "$path" git checkout-index -f --stage=3 -- "$MERGED"
git add -- "$path" git add -- "$MERGED"
cleanup_temp_files --save-backup cleanup_temp_files --save-backup
return return
;; ;;
@ -95,12 +95,12 @@ resolve_deleted_merge () {
read ans read ans
case "$ans" in case "$ans" in
[mMcC]*) [mMcC]*)
git add -- "$path" git add -- "$MERGED"
cleanup_temp_files --save-backup cleanup_temp_files --save-backup
return return
;; ;;
[dD]*) [dD]*)
git rm -- "$path" > /dev/null git rm -- "$MERGED" > /dev/null
cleanup_temp_files cleanup_temp_files
return return
;; ;;
@ -112,11 +112,11 @@ resolve_deleted_merge () {
} }
check_unchanged () { check_unchanged () {
if test "$path" -nt "$BACKUP" ; then if test "$MERGED" -nt "$BACKUP" ; then
status=0; status=0;
else else
while true; do while true; do
echo "$path seems unchanged." echo "$MERGED seems unchanged."
printf "Was the merge successful? [y/n] " printf "Was the merge successful? [y/n] "
read answer < /dev/tty read answer < /dev/tty
case "$answer" in case "$answer" in
@ -127,50 +127,38 @@ check_unchanged () {
fi fi
} }
save_backup () {
if test "$status" -eq 0; then
mv -- "$BACKUP" "$path.orig"
fi
}
remove_backup () {
if test "$status" -eq 0; then
rm "$BACKUP"
fi
}
merge_file () { 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 -z "$f" ; then
if test ! -f "$path" ; then if test ! -f "$MERGED" ; then
echo "$path: file not found" echo "$MERGED: file not found"
else else
echo "$path: file does not need merging" echo "$MERGED: file does not need merging"
fi fi
exit 1 exit 1
fi fi
ext="$$$(expr "$path" : '.*\(\.[^/]*\)$')" ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
BACKUP="$path.BACKUP.$ext" BACKUP="$MERGED.BACKUP.$ext"
LOCAL="$path.LOCAL.$ext" LOCAL="$MERGED.LOCAL.$ext"
REMOTE="$path.REMOTE.$ext" REMOTE="$MERGED.REMOTE.$ext"
BASE="$path.BASE.$ext" BASE="$MERGED.BASE.$ext"
mv -- "$path" "$BACKUP" mv -- "$MERGED" "$BACKUP"
cp -- "$BACKUP" "$path" cp -- "$BACKUP" "$MERGED"
base_mode=`git ls-files -u -- "$path" | awk '{if ($3==1) print $1;}'` base_mode=`git ls-files -u -- "$MERGED" | awk '{if ($3==1) print $1;}'`
local_mode=`git ls-files -u -- "$path" | awk '{if ($3==2) print $1;}'` local_mode=`git ls-files -u -- "$MERGED" | awk '{if ($3==2) print $1;}'`
remote_mode=`git ls-files -u -- "$path" | awk '{if ($3==3) print $1;}'` remote_mode=`git ls-files -u -- "$MERGED" | awk '{if ($3==3) print $1;}'`
base_present && git cat-file blob ":1:$prefix$path" >"$BASE" 2>/dev/null base_present && git cat-file blob ":1:$prefix$MERGED" >"$BASE" 2>/dev/null
local_present && git cat-file blob ":2:$prefix$path" >"$LOCAL" 2>/dev/null local_present && git cat-file blob ":2:$prefix$MERGED" >"$LOCAL" 2>/dev/null
remote_present && git cat-file blob ":3:$prefix$path" >"$REMOTE" 2>/dev/null remote_present && git cat-file blob ":3:$prefix$MERGED" >"$REMOTE" 2>/dev/null
if test -z "$local_mode" -o -z "$remote_mode"; then 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 "$local_mode" "local" "$LOCAL"
describe_file "$remote_mode" "remote" "$REMOTE" describe_file "$remote_mode" "remote" "$REMOTE"
resolve_deleted_merge resolve_deleted_merge
@ -178,14 +166,14 @@ merge_file () {
fi fi
if is_symlink "$local_mode" || is_symlink "$remote_mode"; then 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 "$local_mode" "local" "$LOCAL"
describe_file "$remote_mode" "remote" "$REMOTE" describe_file "$remote_mode" "remote" "$REMOTE"
resolve_symlink_merge resolve_symlink_merge
return return
fi fi
echo "Normal merge conflict for '$path':" echo "Normal merge conflict for '$MERGED':"
describe_file "$local_mode" "local" "$LOCAL" describe_file "$local_mode" "local" "$LOCAL"
describe_file "$remote_mode" "remote" "$REMOTE" describe_file "$remote_mode" "remote" "$REMOTE"
printf "Hit return to start merge resolution tool (%s): " "$merge_tool" printf "Hit return to start merge resolution tool (%s): " "$merge_tool"
@ -194,36 +182,32 @@ merge_file () {
case "$merge_tool" in case "$merge_tool" in
kdiff3) kdiff3)
if base_present ; then if base_present ; then
("$merge_tool_path" --auto --L1 "$path (Base)" --L2 "$path (Local)" --L3 "$path (Remote)" \ ("$merge_tool_path" --auto --L1 "$MERGED (Base)" --L2 "$MERGED (Local)" --L3 "$MERGED (Remote)" \
-o "$path" -- "$BASE" "$LOCAL" "$REMOTE" > /dev/null 2>&1) -o "$MERGED" -- "$BASE" "$LOCAL" "$REMOTE" > /dev/null 2>&1)
else else
("$merge_tool_path" --auto --L1 "$path (Local)" --L2 "$path (Remote)" \ ("$merge_tool_path" --auto --L1 "$MERGED (Local)" --L2 "$MERGED (Remote)" \
-o "$path" -- "$LOCAL" "$REMOTE" > /dev/null 2>&1) -o "$MERGED" -- "$LOCAL" "$REMOTE" > /dev/null 2>&1)
fi fi
status=$? status=$?
remove_backup
;; ;;
tkdiff) tkdiff)
if base_present ; then if base_present ; then
"$merge_tool_path" -a "$BASE" -o "$path" -- "$LOCAL" "$REMOTE" "$merge_tool_path" -a "$BASE" -o "$MERGED" -- "$LOCAL" "$REMOTE"
else else
"$merge_tool_path" -o "$path" -- "$LOCAL" "$REMOTE" "$merge_tool_path" -o "$MERGED" -- "$LOCAL" "$REMOTE"
fi fi
status=$? status=$?
save_backup
;; ;;
meld|vimdiff) meld|vimdiff)
touch "$BACKUP" touch "$BACKUP"
"$merge_tool_path" -- "$LOCAL" "$path" "$REMOTE" "$merge_tool_path" -- "$LOCAL" "$MERGED" "$REMOTE"
check_unchanged check_unchanged
save_backup
;; ;;
gvimdiff) gvimdiff)
touch "$BACKUP" touch "$BACKUP"
"$merge_tool_path" -f -- "$LOCAL" "$path" "$REMOTE" "$merge_tool_path" -f -- "$LOCAL" "$MERGED" "$REMOTE"
check_unchanged check_unchanged
save_backup ;;
;;
xxdiff) xxdiff)
touch "$BACKUP" touch "$BACKUP"
if base_present ; then if base_present ; then
@ -231,53 +215,68 @@ merge_file () {
-R 'Accel.SaveAsMerged: "Ctrl-S"' \ -R 'Accel.SaveAsMerged: "Ctrl-S"' \
-R 'Accel.Search: "Ctrl+F"' \ -R 'Accel.Search: "Ctrl+F"' \
-R 'Accel.SearchForward: "Ctrl-G"' \ -R 'Accel.SearchForward: "Ctrl-G"' \
--merged-file "$path" -- "$LOCAL" "$BASE" "$REMOTE" --merged-file "$MERGED" -- "$LOCAL" "$BASE" "$REMOTE"
else else
"$merge_tool_path" -X --show-merged-pane \ "$merge_tool_path" -X --show-merged-pane \
-R 'Accel.SaveAsMerged: "Ctrl-S"' \ -R 'Accel.SaveAsMerged: "Ctrl-S"' \
-R 'Accel.Search: "Ctrl+F"' \ -R 'Accel.Search: "Ctrl+F"' \
-R 'Accel.SearchForward: "Ctrl-G"' \ -R 'Accel.SearchForward: "Ctrl-G"' \
--merged-file "$path" -- "$LOCAL" "$REMOTE" --merged-file "$MERGED" -- "$LOCAL" "$REMOTE"
fi fi
check_unchanged check_unchanged
save_backup
;; ;;
opendiff) opendiff)
touch "$BACKUP" touch "$BACKUP"
if base_present; then if base_present; then
"$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$path" | cat "$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$MERGED" | cat
else else
"$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$path" | cat "$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$MERGED" | cat
fi fi
check_unchanged check_unchanged
save_backup
;; ;;
ecmerge) ecmerge)
touch "$BACKUP" touch "$BACKUP"
if base_present; then if base_present; then
"$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --mode=merge3 --to="$path" "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --mode=merge3 --to="$MERGED"
else else
"$merge_tool_path" "$LOCAL" "$REMOTE" --mode=merge2 --to="$path" "$merge_tool_path" "$LOCAL" "$REMOTE" --mode=merge2 --to="$MERGED"
fi fi
check_unchanged check_unchanged
save_backup
;; ;;
emerge) emerge)
if base_present ; then if base_present ; then
"$merge_tool_path" -f emerge-files-with-ancestor-command "$LOCAL" "$REMOTE" "$BASE" "$(basename "$path")" "$merge_tool_path" -f emerge-files-with-ancestor-command "$LOCAL" "$REMOTE" "$BASE" "$(basename "$MERGED")"
else else
"$merge_tool_path" -f emerge-files-command "$LOCAL" "$REMOTE" "$(basename "$path")" "$merge_tool_path" -f emerge-files-command "$LOCAL" "$REMOTE" "$(basename "$MERGED")"
fi fi
status=$? status=$?
save_backup ;;
*)
if test -n "$merge_tool_cmd"; then
if test "$merge_tool_trust_exit_code" = "false"; then
touch "$BACKUP"
( eval $merge_tool_cmd )
check_unchanged
else
( eval $merge_tool_cmd )
status=$?
fi
fi
;; ;;
esac esac
if test "$status" -ne 0; then if test "$status" -ne 0; then
echo "merge of $path failed" 1>&2 echo "merge of $MERGED failed" 1>&2
mv -- "$BACKUP" "$path" mv -- "$BACKUP" "$MERGED"
exit 1 exit 1
fi fi
git add -- "$path"
if test "$merge_keep_backup" = "true"; then
mv -- "$BACKUP" "$MERGED.orig"
else
rm -- "$BACKUP"
fi
git add -- "$MERGED"
cleanup_temp_files cleanup_temp_files
} }
@ -309,12 +308,20 @@ do
shift shift
done done
valid_custom_tool()
{
merge_tool_cmd="$(git config mergetool.$1.cmd)"
test -n "$merge_tool_cmd"
}
valid_tool() { valid_tool() {
case "$1" in case "$1" in
kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge) kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
;; # happy ;; # happy
*) *)
return 1 if ! valid_custom_tool "$1"; then
return 1
fi
;; ;;
esac esac
} }
@ -380,10 +387,16 @@ else
init_merge_tool_path "$merge_tool" init_merge_tool_path "$merge_tool"
if ! type "$merge_tool_path" > /dev/null 2>&1; then merge_keep_backup="$(git config --bool merge.keepBackup || echo true)"
if test -z "$merge_tool_cmd" && ! type "$merge_tool_path" > /dev/null 2>&1; then
echo "The merge tool $merge_tool is not available as '$merge_tool_path'" echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
exit 1 exit 1
fi fi
if ! test -z "$merge_tool_cmd"; then
merge_tool_trust_exit_code="$(git config --bool mergetool.$merge_tool.trustExitCode || echo false)"
fi
fi fi

46
t/t7610-mergetool.sh Normal file
View File

@ -0,0 +1,46 @@
#!/bin/sh
#
# Copyright (c) 2008 Charles Bailey
#
test_description='git-mergetool
Testing basic merge tool invocation'
. ./test-lib.sh
test_expect_success 'setup' '
echo master >file1 &&
git add file1 &&
git commit -m "added file1" &&
git checkout -b branch1 master &&
echo branch1 change >file1 &&
echo branch1 newfile >file2 &&
git add file1 file2 &&
git commit -m "branch1 changes" &&
git checkout -b branch2 master &&
echo branch2 change >file1 &&
echo branch2 newfile >file2 &&
git add file1 file2 &&
git commit -m "branch2 changes" &&
git checkout master &&
echo master updated >file1 &&
echo master new >file2 &&
git add file1 file2 &&
git commit -m "master updates"
'
test_expect_success 'custom mergetool' '
git config merge.tool mytool &&
git config mergetool.mytool.cmd "cat \"\$REMOTE\" >\"\$MERGED\"" &&
git config mergetool.mytool.trustExitCode true &&
git checkout branch1 &&
! git merge master >/dev/null 2>&1 &&
( yes "" | git mergetool file1>/dev/null 2>&1 ) &&
( yes "" | git mergetool file2>/dev/null 2>&1 ) &&
test "$(cat file1)" = "master updated" &&
test "$(cat file2)" = "master new" &&
git commit -m "branch1 resolved with mergetool"
'
test_done