1 # This script can be run in two different contexts:
3 # - From git, when the user invokes the "vimdiff" merge tool. In this context
4 # this script expects the following environment variables (among others) to
5 # be defined (which is something "git" takes care of):
12 # In this mode, all this script does is to run the next command:
14 # vim -f -c ... $LOCAL $BASE $REMOTE $MERGED
16 # ...where the "..." string depends on the value of the
17 # "mergetool.vimdiff.layout" configuration variable and is used to open vim
18 # with a certain layout of buffers, windows and tabs.
20 # - From a script inside the unit tests framework folder ("t" folder) by
21 # sourcing this script and then manually calling "run_unit_tests", which
22 # will run a battery of unit tests to make sure nothing breaks.
23 # In this context this script does not expect any particular environment
27 ################################################################################
28 ## Internal functions (not meant to be used outside this script)
29 ################################################################################
32 # Send message to stderr if global variable GIT_MERGETOOL_VIMDIFF is set
35 if test -n "$GIT_MERGETOOL_VIMDIFF_DEBUG"
42 # Return a substring of $1 containing $3 characters starting at
43 # zero-based offset $2.
47 # substring "Hello world" 0 4 --> "Hell"
48 # substring "Hello world" 3 4 --> "lo w"
49 # substring "Hello world" 3 10 --> "lo world"
55 echo "$STRING" | cut -c$(( START + 1 ))-$(( START + $LEN ))
59 # Auxiliary function used from "gen_cmd()".
60 # Read that other function documentation for more details.
63 CMD=$2 # This is a second (hidden) argument used for recursion
66 debug_print "LAYOUT : $LAYOUT"
67 debug_print "CMD : $CMD"
71 CMD="echo" # vim "nop" operator
83 # Increase/decrease "start"/"end" indices respectively to get rid of
88 # - BEFORE: (( LOCAL , BASE ) / MERGED )
89 # - AFTER : ( LOCAL , BASE ) / MERGED
93 for c in $(echo "$LAYOUT" | sed 's:.:&#:g')
102 nested=$(( nested + 1 ))
108 nested=$(( nested - 1 ))
112 if test "$nested" -lt "$nested_min"
119 debug_print "NESTED MIN: $nested_min"
121 while test "$nested_min" -gt "0"
123 start=$(( start + 1 ))
126 start_minus_one=$(( start - 1 ))
128 while ! test "$(substring "$LAYOUT" "$start_minus_one" 1)" = "("
130 start=$(( start + 1 ))
131 start_minus_one=$(( start_minus_one + 1 ))
134 while ! test "$(substring "$LAYOUT" "$end" 1)" = ")"
139 nested_min=$(( nested_min - 1 ))
142 debug_print "CLEAN : $(substring "$LAYOUT" "$start" "$(( end - start ))")"
147 # Search for all valid separators ("+", "/" or ",") which are *not*
148 # inside parenthesis. Save the index at which each of them makes the
152 index_horizontal_split=""
153 index_vertical_split=""
160 for c in $(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:.:&#:g');
171 nested=$(( nested + 1 ))
177 nested=$(( nested - 1 ))
181 if test "$nested" = 0
185 if test "$current" = "+"
187 if test -z "$index_new_tab"
192 elif test "$current" = "/"
194 if test -z "$index_horizontal_split"
196 index_horizontal_split=$i
199 elif test "$current" = ","
201 if test -z "$index_vertical_split"
203 index_vertical_split=$i
213 # Process the separator with the highest order of precedence
214 # (";" has the highest precedence and "|" the lowest one).
216 # By "process" I mean recursively call this function twice: the first
217 # one with the substring at the left of the separator and the second one
218 # with the one at its right.
222 if ! test -z "$index_new_tab"
229 elif ! test -z "$index_horizontal_split"
231 before="leftabove split"
233 index=$index_horizontal_split
236 elif ! test -z "$index_vertical_split"
238 before="leftabove vertical split"
240 index=$index_vertical_split
244 if test "$terminate" = "true"
247 CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$start" "$(( index - start ))")" "$CMD")
249 CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$(( index + 1 ))" "$(( ${#LAYOUT} - index ))")" "$CMD")
257 # If we reach this point, it means there are no separators and we just
258 # need to print the command to display the specified buffer
260 target=$(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:[ @();|-]::g')
262 if test "$target" = "LOCAL"
266 elif test "$target" = "BASE"
270 elif test "$target" = "REMOTE"
274 elif test "$target" = "MERGED"
279 CMD="$CMD | ERROR: >$target<"
288 # This function returns (in global variable FINAL_CMD) the string that
289 # you can use when invoking "vim" (as shown next) to obtain a given
292 # $ vim -f $FINAL_CMD "$LOCAL" "$BASE" "$REMOTE" "$MERGED"
294 # It takes one single argument: a string containing the desired layout
297 # The syntax of the "layout definitions" is explained in "Documentation/
298 # mergetools/vimdiff.txt" but you can already intuitively understand how
299 # it works by knowing that...
301 # * "+" means "a new vim tab"
302 # * "/" means "a new vim horizontal split"
303 # * "," means "a new vim vertical split"
305 # It also returns (in global variable FINAL_TARGET) the name ("LOCAL",
306 # "BASE", "REMOTE" or "MERGED") of the file that is marked with an "@",
307 # or "MERGED" if none of them is.
311 # gen_cmd "@LOCAL , REMOTE"
313 # `-> FINAL_CMD == "-c \"echo | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
314 # FINAL_TARGET == "LOCAL"
319 # Search for a "@" in one of the files identifiers ("LOCAL", "BASE",
320 # "REMOTE", "MERGED"). If not found, use "MERGE" as the default file
321 # where changes will be saved.
323 if echo "$LAYOUT" | grep @LOCAL >/dev/null
326 elif echo "$LAYOUT" | grep @BASE >/dev/null
330 FINAL_TARGET="MERGED"
334 # Obtain the first part of vim "-c" option to obtain the desired layout
336 CMD=$(gen_cmd_aux "$LAYOUT")
339 # Adjust the just obtained script depending on whether more than one
340 # windows are visible or not
342 if echo "$LAYOUT" | grep ",\|/" >/dev/null
344 CMD="$CMD | tabdo windo diffthis"
346 CMD="$CMD | bufdo diffthis"
350 # Add an extra "-c" option to move to the first tab (notice that we
351 # can't simply append the command to the previous "-c" string as
352 # explained here: https://github.com/vim/vim/issues/9076
354 FINAL_CMD="-c \"$CMD\" -c \"tabfirst\""
358 ################################################################################
359 ## API functions (called from "git-mergetool--lib.sh")
360 ################################################################################
363 "$merge_tool_path" -R -f -d \
364 -c 'wincmd l' -c 'cd $GIT_PREFIX' "$LOCAL" "$REMOTE"
376 printf "Use gVim (requires a graphical session)"
388 layout=$(git config mergetool.vimdiff.layout)
394 # Default layout when none is specified
395 layout="(LOCAL,BASE,REMOTE)/MERGED"
399 layout="@LOCAL,REMOTE"
402 layout="LOCAL,MERGED,REMOTE"
412 debug_print "FINAL CMD : $FINAL_CMD"
413 debug_print "FINAL TAR : $FINAL_TARGET"
417 eval '"$merge_tool_path"' \
418 -f "$FINAL_CMD" '"$LOCAL"' '"$BASE"' '"$REMOTE"' '"$MERGED"'
420 # If there is no BASE (example: a merge conflict in a new file
421 # with the same name created in both braches which didn't exist
422 # before), close all BASE windows using vim's "quit" command
424 FINAL_CMD=$(echo "$FINAL_CMD" | \
425 sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g')
427 eval '"$merge_tool_path"' \
428 -f "$FINAL_CMD" '"$LOCAL"' '"$REMOTE"' '"$MERGED"'
435 case "$FINAL_TARGET" in
440 source_path="$REMOTE"
448 if test -n "$source_path"
450 cp "$source_path" "$MERGED"
466 printf "Use gVim (requires a graphical session) "
475 echo "with a 2 panes layout (LOCAL and REMOTE)"
478 echo "with a 3 panes layout (LOCAL, MERGED and REMOTE)"
481 echo "where only the MERGED file is shown"
484 echo "with a custom layout (see \`git help mergetool\`'s \`BACKEND SPECIFIC HINTS\` section)"
492 translate_merge_tool_path () {
507 exit_code_trustable () {
512 list_tool_variants () {
513 if test "$TOOL_MODE" = "diff"
517 echo "${prefix}vimdiff"
522 for suffix in '' 1 2 3
524 echo "${prefix}vimdiff${suffix}"
531 ################################################################################
532 ## Unit tests (called from scripts inside the "t" folder)
533 ################################################################################
536 # Function to make sure that we don't break anything when modifying this
539 NUMBER_OF_TEST_CASES=16
541 TEST_CASE_01="(LOCAL,BASE,REMOTE)/MERGED" # default behaviour
542 TEST_CASE_02="@LOCAL,REMOTE" # when using vimdiff1
543 TEST_CASE_03="LOCAL,MERGED,REMOTE" # when using vimdiff2
544 TEST_CASE_04="MERGED" # when using vimdiff3
545 TEST_CASE_05="LOCAL/MERGED/REMOTE"
546 TEST_CASE_06="(LOCAL/REMOTE),MERGED"
547 TEST_CASE_07="MERGED,(LOCAL/REMOTE)"
548 TEST_CASE_08="(LOCAL,REMOTE)/MERGED"
549 TEST_CASE_09="MERGED/(LOCAL,REMOTE)"
550 TEST_CASE_10="(LOCAL/BASE/REMOTE),MERGED"
551 TEST_CASE_11="(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED"
552 TEST_CASE_12="((LOCAL,REMOTE)/BASE),MERGED"
553 TEST_CASE_13="((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)"
554 TEST_CASE_14="BASE,REMOTE+BASE,LOCAL"
555 TEST_CASE_15=" (( (LOCAL , BASE , REMOTE) / MERGED)) +(BASE) , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) , MERGED ) "
556 TEST_CASE_16="LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED"
558 EXPECTED_CMD_01="-c \"echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
559 EXPECTED_CMD_02="-c \"echo | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
560 EXPECTED_CMD_03="-c \"echo | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 4b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
561 EXPECTED_CMD_04="-c \"echo | 4b | bufdo diffthis\" -c \"tabfirst\""
562 EXPECTED_CMD_05="-c \"echo | leftabove split | 1b | wincmd j | leftabove split | 4b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
563 EXPECTED_CMD_06="-c \"echo | leftabove vertical split | leftabove split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
564 EXPECTED_CMD_07="-c \"echo | leftabove vertical split | 4b | wincmd l | leftabove split | 1b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
565 EXPECTED_CMD_08="-c \"echo | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
566 EXPECTED_CMD_09="-c \"echo | leftabove split | 4b | wincmd j | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
567 EXPECTED_CMD_10="-c \"echo | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
568 EXPECTED_CMD_11="-c \"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\""
569 EXPECTED_CMD_12="-c \"echo | leftabove vertical split | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
570 EXPECTED_CMD_13="-c \"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\""
571 EXPECTED_CMD_14="-c \"echo | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | 2b | wincmd l | 1b | tabdo windo diffthis\" -c \"tabfirst\""
572 EXPECTED_CMD_15="-c \"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\""
573 EXPECTED_CMD_16="-c \"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\""
575 EXPECTED_TARGET_01="MERGED"
576 EXPECTED_TARGET_02="LOCAL"
577 EXPECTED_TARGET_03="MERGED"
578 EXPECTED_TARGET_04="MERGED"
579 EXPECTED_TARGET_05="MERGED"
580 EXPECTED_TARGET_06="MERGED"
581 EXPECTED_TARGET_07="MERGED"
582 EXPECTED_TARGET_08="MERGED"
583 EXPECTED_TARGET_09="MERGED"
584 EXPECTED_TARGET_10="MERGED"
585 EXPECTED_TARGET_11="MERGED"
586 EXPECTED_TARGET_12="MERGED"
587 EXPECTED_TARGET_13="MERGED"
588 EXPECTED_TARGET_14="MERGED"
589 EXPECTED_TARGET_15="MERGED"
590 EXPECTED_TARGET_16="MERGED"
592 at_least_one_ko="false"
594 for i in $(seq -w 1 99)
596 if test "$i" -gt $NUMBER_OF_TEST_CASES
601 gen_cmd "$(eval echo \${TEST_CASE_"$i"})"
603 if test "$FINAL_CMD" = "$(eval echo \${EXPECTED_CMD_"$i"})" \
604 && test "$FINAL_TARGET" = "$(eval echo \${EXPECTED_TARGET_"$i"})"
606 printf "Test Case #%02d: OK\n" "$(echo "$i" | sed 's/^0*//')"
608 printf "Test Case #%02d: KO !!!!\n" "$(echo "$i" | sed 's/^0*//')"
609 echo " FINAL_CMD : $FINAL_CMD"
610 echo " FINAL_CMD (expected) : $(eval echo \${EXPECTED_CMD_"$i"})"
611 echo " FINAL_TARGET : $FINAL_TARGET"
612 echo " FINAL_TARGET (expected): $(eval echo \${EXPECTED_TARGET_"$i"})"
613 at_least_one_ko="true"
617 # verify that `merge_cmd` handles paths with spaces
618 record_parameters () {
631 merge_tool_path=record_parameters
633 merge_cmd vimdiff || at_least_one_ko=true
638 echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | quit | wincmd l | 2b | wincmd j | 3b | tabdo windo diffthis
646 diff -u expect actual || at_least_one_ko=true
648 if test "$at_least_one_ko" = "true"