git-commit-vandalism/mergetools/vimdiff
Felipe Contreras 79db50d821 mergetools: vimdiff: make vimdiff3 actually work
When vimdiff3 was added in 7c147b77d3 (mergetools: add vimdiff3 mode,
2014-04-20), the description made clear the intention:

    It's similar to the default, except that the other windows are
    hidden.  This ensures that removed/added colors are still visible on
    the main merge window, but the other windows not visible.

However, in 0041797449 (vimdiff: new implementation with layout support,
2022-03-30) this was broken by generating a command that never creates
windows, and therefore vim never shows the diff.

The layout support implementation broke the whole purpose of vimdiff3,
and simply shows MERGED, which is no different from simply opening the
file with vim.

In order to show the diff, the windows need to be created first, and
then when they are hidden the diff remains (if hidenoff isn't set), but
by setting the `hidden` option the initial buffers are marked as hidden
thus making the feature work.

Suggested-by: Fernando Ramos <greenfoo@u92.eu>
Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
Reviewed-by: Fernando Ramos <greenfoo@u92.eu>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-08-10 12:39:17 -07:00

655 lines
17 KiB
Plaintext

# This script can be run in two different contexts:
#
# - From git, when the user invokes the "vimdiff" merge tool. In this context
# this script expects the following environment variables (among others) to
# be defined (which is something "git" takes care of):
#
# - $BASE
# - $LOCAL
# - $REMOTE
# - $MERGED
#
# In this mode, all this script does is to run the next command:
#
# vim -f -c ... $LOCAL $BASE $REMOTE $MERGED
#
# ...where the "..." string depends on the value of the
# "mergetool.vimdiff.layout" configuration variable and is used to open vim
# with a certain layout of buffers, windows and tabs.
#
# - From a script inside the unit tests framework folder ("t" folder) by
# sourcing this script and then manually calling "run_unit_tests", which
# will run a battery of unit tests to make sure nothing breaks.
# In this context this script does not expect any particular environment
# variable to be set.
################################################################################
## Internal functions (not meant to be used outside this script)
################################################################################
debug_print () {
# Send message to stderr if global variable GIT_MERGETOOL_VIMDIFF_DEBUG
# is set.
if test -n "$GIT_MERGETOOL_VIMDIFF_DEBUG"
then
>&2 echo "$@"
fi
}
substring () {
# Return a substring of $1 containing $3 characters starting at
# zero-based offset $2.
#
# Examples:
#
# substring "Hello world" 0 4 --> "Hell"
# substring "Hello world" 3 4 --> "lo w"
# substring "Hello world" 3 10 --> "lo world"
STRING=$1
START=$2
LEN=$3
echo "$STRING" | cut -c$(( START + 1 ))-$(( START + $LEN ))
}
gen_cmd_aux () {
# Auxiliary function used from "gen_cmd()".
# Read that other function documentation for more details.
LAYOUT=$1
CMD=$2 # This is a second (hidden) argument used for recursion
debug_print
debug_print "LAYOUT : $LAYOUT"
debug_print "CMD : $CMD"
if test -z "$CMD"
then
CMD="echo" # vim "nop" operator
fi
start=0
end=${#LAYOUT}
nested=0
nested_min=100
# Step 1:
#
# Increase/decrease "start"/"end" indices respectively to get rid of
# outer parenthesis.
#
# Example:
#
# - BEFORE: (( LOCAL , BASE ) / MERGED )
# - AFTER : ( LOCAL , BASE ) / MERGED
oldIFS=$IFS
IFS=#
for c in $(echo "$LAYOUT" | sed 's:.:&#:g')
do
if test "$c" = " "
then
continue
fi
if test "$c" = "("
then
nested=$(( nested + 1 ))
continue
fi
if test "$c" = ")"
then
nested=$(( nested - 1 ))
continue
fi
if test "$nested" -lt "$nested_min"
then
nested_min=$nested
fi
done
IFS=$oldIFS
debug_print "NESTED MIN: $nested_min"
while test "$nested_min" -gt "0"
do
start=$(( start + 1 ))
end=$(( end - 1 ))
start_minus_one=$(( start - 1 ))
while ! test "$(substring "$LAYOUT" "$start_minus_one" 1)" = "("
do
start=$(( start + 1 ))
start_minus_one=$(( start_minus_one + 1 ))
done
while ! test "$(substring "$LAYOUT" "$end" 1)" = ")"
do
end=$(( end - 1 ))
done
nested_min=$(( nested_min - 1 ))
done
debug_print "CLEAN : $(substring "$LAYOUT" "$start" "$(( end - start ))")"
# Step 2:
#
# Search for all valid separators ("+", "/" or ",") which are *not*
# inside parenthesis. Save the index at which each of them makes the
# first appearance.
index_new_tab=""
index_horizontal_split=""
index_vertical_split=""
nested=0
i=$(( start - 1 ))
oldIFS=$IFS
IFS=#
for c in $(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:.:&#:g');
do
i=$(( i + 1 ))
if test "$c" = " "
then
continue
fi
if test "$c" = "("
then
nested=$(( nested + 1 ))
continue
fi
if test "$c" = ")"
then
nested=$(( nested - 1 ))
continue
fi
if test "$nested" = 0
then
current=$c
if test "$current" = "+"
then
if test -z "$index_new_tab"
then
index_new_tab=$i
fi
elif test "$current" = "/"
then
if test -z "$index_horizontal_split"
then
index_horizontal_split=$i
fi
elif test "$current" = ","
then
if test -z "$index_vertical_split"
then
index_vertical_split=$i
fi
fi
fi
done
IFS=$oldIFS
# Step 3:
#
# Process the separator with the highest order of precedence
# (";" has the highest precedence and "|" the lowest one).
#
# By "process" I mean recursively call this function twice: the first
# one with the substring at the left of the separator and the second one
# with the one at its right.
terminate="false"
if ! test -z "$index_new_tab"
then
before="-tabnew"
after="tabnext"
index=$index_new_tab
terminate="true"
elif ! test -z "$index_horizontal_split"
then
before="leftabove split"
after="wincmd j"
index=$index_horizontal_split
terminate="true"
elif ! test -z "$index_vertical_split"
then
before="leftabove vertical split"
after="wincmd l"
index=$index_vertical_split
terminate="true"
fi
if test "$terminate" = "true"
then
CMD="$CMD | $before"
CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$start" "$(( index - start ))")" "$CMD")
CMD="$CMD | $after"
CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$(( index + 1 ))" "$(( ${#LAYOUT} - index ))")" "$CMD")
echo "$CMD"
return
fi
# Step 4:
#
# If we reach this point, it means there are no separators and we just
# need to print the command to display the specified buffer
target=$(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:[ @();|-]::g')
if test "$target" = "LOCAL"
then
CMD="$CMD | 1b"
elif test "$target" = "BASE"
then
CMD="$CMD | 2b"
elif test "$target" = "REMOTE"
then
CMD="$CMD | 3b"
elif test "$target" = "MERGED"
then
CMD="$CMD | 4b"
else
CMD="$CMD | ERROR: >$target<"
fi
echo "$CMD"
return
}
gen_cmd () {
# This function returns (in global variable FINAL_CMD) the string that
# you can use when invoking "vim" (as shown next) to obtain a given
# layout:
#
# $ vim -f $FINAL_CMD "$LOCAL" "$BASE" "$REMOTE" "$MERGED"
#
# It takes one single argument: a string containing the desired layout
# definition.
#
# The syntax of the "layout definitions" is explained in "Documentation/
# mergetools/vimdiff.txt" but you can already intuitively understand how
# it works by knowing that...
#
# * "+" means "a new vim tab"
# * "/" means "a new vim horizontal split"
# * "," means "a new vim vertical split"
#
# It also returns (in global variable FINAL_TARGET) the name ("LOCAL",
# "BASE", "REMOTE" or "MERGED") of the file that is marked with an "@",
# or "MERGED" if none of them is.
#
# Example:
#
# gen_cmd "@LOCAL , REMOTE"
# |
# `-> FINAL_CMD == "-c \"echo | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
# FINAL_TARGET == "LOCAL"
LAYOUT=$1
# Search for a "@" in one of the files identifiers ("LOCAL", "BASE",
# "REMOTE", "MERGED"). If not found, use "MERGE" as the default file
# where changes will be saved.
if echo "$LAYOUT" | grep @LOCAL >/dev/null
then
FINAL_TARGET="LOCAL"
elif echo "$LAYOUT" | grep @BASE >/dev/null
then
FINAL_TARGET="BASE"
else
FINAL_TARGET="MERGED"
fi
# Obtain the first part of vim "-c" option to obtain the desired layout
CMD=$(gen_cmd_aux "$LAYOUT")
# Adjust the just obtained script depending on whether more than one
# windows are visible or not
if echo "$LAYOUT" | grep ",\|/" >/dev/null
then
CMD="$CMD | tabdo windo diffthis"
else
CMD="$CMD | bufdo diffthis"
fi
# Add an extra "-c" option to move to the first tab (notice that we
# can't simply append the command to the previous "-c" string as
# explained here: https://github.com/vim/vim/issues/9076
FINAL_CMD="-c \"set hidden | $CMD\" -c \"tabfirst\""
}
################################################################################
## API functions (called from "git-mergetool--lib.sh")
################################################################################
diff_cmd () {
"$merge_tool_path" -R -f -d \
-c 'wincmd l' -c 'cd $GIT_PREFIX' "$LOCAL" "$REMOTE"
}
diff_cmd_help () {
TOOL=$1
case "$TOOL" in
nvimdiff*)
printf "Use Neovim"
;;
gvimdiff*)
printf "Use gVim (requires a graphical session)"
;;
vimdiff*)
printf "Use Vim"
;;
esac
return 0
}
merge_cmd () {
layout=$(git config mergetool.vimdiff.layout)
case "$1" in
*vimdiff)
if test -z "$layout"
then
# Default layout when none is specified
layout="(LOCAL,BASE,REMOTE)/MERGED"
fi
;;
*vimdiff1)
layout="@LOCAL,REMOTE"
;;
*vimdiff2)
layout="LOCAL,MERGED,REMOTE"
;;
*vimdiff3)
layout="MERGED"
;;
esac
gen_cmd "$layout"
debug_print ""
debug_print "FINAL CMD : $FINAL_CMD"
debug_print "FINAL TAR : $FINAL_TARGET"
if $base_present
then
eval '"$merge_tool_path"' \
-f "$FINAL_CMD" '"$LOCAL"' '"$BASE"' '"$REMOTE"' '"$MERGED"'
else
# If there is no BASE (example: a merge conflict in a new file
# with the same name created in both braches which didn't exist
# before), close all BASE windows using vim's "quit" command
FINAL_CMD=$(echo "$FINAL_CMD" | \
sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g')
eval '"$merge_tool_path"' \
-f "$FINAL_CMD" '"$LOCAL"' '"$REMOTE"' '"$MERGED"'
fi
ret="$?"
if test "$ret" -eq 0
then
case "$FINAL_TARGET" in
LOCAL)
source_path="$LOCAL"
;;
REMOTE)
source_path="$REMOTE"
;;
MERGED|*)
# Do nothing
source_path=
;;
esac
if test -n "$source_path"
then
cp "$source_path" "$MERGED"
fi
fi
return "$ret"
}
merge_cmd_help () {
TOOL=$1
case "$TOOL" in
nvimdiff*)
printf "Use Neovim "
;;
gvimdiff*)
printf "Use gVim (requires a graphical session) "
;;
vimdiff*)
printf "Use Vim "
;;
esac
case "$TOOL" in
*1)
echo "with a 2 panes layout (LOCAL and REMOTE)"
;;
*2)
echo "with a 3 panes layout (LOCAL, MERGED and REMOTE)"
;;
*3)
echo "where only the MERGED file is shown"
;;
*)
echo "with a custom layout (see \`git help mergetool\`'s \`BACKEND SPECIFIC HINTS\` section)"
;;
esac
return 0
}
translate_merge_tool_path () {
case "$1" in
nvimdiff*)
echo nvim
;;
gvimdiff*)
echo gvim
;;
vimdiff*)
echo vim
;;
esac
}
exit_code_trustable () {
true
}
list_tool_variants () {
if test "$TOOL_MODE" = "diff"
then
for prefix in '' g n
do
echo "${prefix}vimdiff"
done
else
for prefix in '' g n
do
for suffix in '' 1 2 3
do
echo "${prefix}vimdiff${suffix}"
done
done
fi
}
################################################################################
## Unit tests (called from scripts inside the "t" folder)
################################################################################
run_unit_tests () {
# Function to make sure that we don't break anything when modifying this
# script.
NUMBER_OF_TEST_CASES=16
TEST_CASE_01="(LOCAL,BASE,REMOTE)/MERGED" # default behaviour
TEST_CASE_02="@LOCAL,REMOTE" # when using vimdiff1
TEST_CASE_03="LOCAL,MERGED,REMOTE" # when using vimdiff2
TEST_CASE_04="MERGED" # when using vimdiff3
TEST_CASE_05="LOCAL/MERGED/REMOTE"
TEST_CASE_06="(LOCAL/REMOTE),MERGED"
TEST_CASE_07="MERGED,(LOCAL/REMOTE)"
TEST_CASE_08="(LOCAL,REMOTE)/MERGED"
TEST_CASE_09="MERGED/(LOCAL,REMOTE)"
TEST_CASE_10="(LOCAL/BASE/REMOTE),MERGED"
TEST_CASE_11="(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED"
TEST_CASE_12="((LOCAL,REMOTE)/BASE),MERGED"
TEST_CASE_13="((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)"
TEST_CASE_14="BASE,REMOTE+BASE,LOCAL"
TEST_CASE_15=" (( (LOCAL , BASE , REMOTE) / MERGED)) +(BASE) , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) , MERGED ) "
TEST_CASE_16="LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED"
EXPECTED_CMD_01="-c \"set hidden | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_02="-c \"set hidden | echo | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_03="-c \"set hidden | echo | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 4b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_04="-c \"set hidden | echo | 4b | bufdo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_05="-c \"set hidden | echo | leftabove split | 1b | wincmd j | leftabove split | 4b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_06="-c \"set hidden | echo | leftabove vertical split | leftabove split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_07="-c \"set hidden | echo | leftabove vertical split | 4b | wincmd l | leftabove split | 1b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_08="-c \"set hidden | echo | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_09="-c \"set hidden | echo | leftabove split | 4b | wincmd j | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_10="-c \"set hidden | echo | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_11="-c \"set hidden | echo | -tabnew | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_12="-c \"set hidden | echo | leftabove vertical split | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_13="-c \"set hidden | echo | leftabove vertical split | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | leftabove vertical split | leftabove split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_14="-c \"set hidden | echo | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | 2b | wincmd l | 1b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_15="-c \"set hidden | echo | -tabnew | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_CMD_16="-c \"set hidden | echo | -tabnew | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
EXPECTED_TARGET_01="MERGED"
EXPECTED_TARGET_02="LOCAL"
EXPECTED_TARGET_03="MERGED"
EXPECTED_TARGET_04="MERGED"
EXPECTED_TARGET_05="MERGED"
EXPECTED_TARGET_06="MERGED"
EXPECTED_TARGET_07="MERGED"
EXPECTED_TARGET_08="MERGED"
EXPECTED_TARGET_09="MERGED"
EXPECTED_TARGET_10="MERGED"
EXPECTED_TARGET_11="MERGED"
EXPECTED_TARGET_12="MERGED"
EXPECTED_TARGET_13="MERGED"
EXPECTED_TARGET_14="MERGED"
EXPECTED_TARGET_15="MERGED"
EXPECTED_TARGET_16="MERGED"
at_least_one_ko="false"
for i in $(seq -w 1 99)
do
if test "$i" -gt $NUMBER_OF_TEST_CASES
then
break
fi
gen_cmd "$(eval echo \${TEST_CASE_"$i"})"
if test "$FINAL_CMD" = "$(eval echo \${EXPECTED_CMD_"$i"})" \
&& test "$FINAL_TARGET" = "$(eval echo \${EXPECTED_TARGET_"$i"})"
then
printf "Test Case #%02d: OK\n" "$(echo "$i" | sed 's/^0*//')"
else
printf "Test Case #%02d: KO !!!!\n" "$(echo "$i" | sed 's/^0*//')"
echo " FINAL_CMD : $FINAL_CMD"
echo " FINAL_CMD (expected) : $(eval echo \${EXPECTED_CMD_"$i"})"
echo " FINAL_TARGET : $FINAL_TARGET"
echo " FINAL_TARGET (expected): $(eval echo \${EXPECTED_TARGET_"$i"})"
at_least_one_ko="true"
fi
done
# verify that `merge_cmd` handles paths with spaces
record_parameters () {
>actual
for arg
do
echo "$arg" >>actual
done
}
base_present=false
LOCAL='lo cal'
BASE='ba se'
REMOTE="' '"
MERGED='mer ged'
merge_tool_path=record_parameters
merge_cmd vimdiff || at_least_one_ko=true
cat >expect <<-\EOF
-f
-c
set hidden | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | quit | wincmd l | 2b | wincmd j | 3b | tabdo windo diffthis
-c
tabfirst
lo cal
' '
mer ged
EOF
diff -u expect actual || at_least_one_ko=true
if test "$at_least_one_ko" = "true"
then
return 255
else
return 0
fi
}