| # 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" |
| |
| 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_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_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_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= |
| oldIFS=$IFS |
| IFS=+ |
| for tab in $LAYOUT |
| do |
| if test -z "$CMD" |
| then |
| CMD="echo" # vim "nop" operator |
| else |
| CMD="$CMD | tabnew" |
| fi |
| |
| # If this is a single window diff with all the buffers |
| if ! echo "$tab" | grep ",\|/" >/dev/null |
| then |
| CMD="$CMD | silent execute 'bufdo diffthis'" |
| fi |
| |
| CMD=$(gen_cmd_aux "$tab" "$CMD") |
| done |
| IFS=$oldIFS |
| |
| CMD="$CMD | execute 'tabdo windo diffthis'" |
| |
| FINAL_CMD="-c \"set hidden diffopt-=hiddenoff | $CMD | 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 () { |
| TOOL=$1 |
| |
| layout=$(git config "mergetool.$TOOL.layout") |
| |
| # backward compatibility: |
| if test -z "$layout" |
| then |
| layout=$(git config mergetool.vimdiff.layout) |
| fi |
| |
| case "$TOOL" 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 diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_02="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 1b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_03="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 4b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_04="-c \"set hidden diffopt-=hiddenoff | echo | silent execute 'bufdo diffthis' | 4b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_05="-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | 1b | wincmd j | leftabove split | 4b | wincmd j | 3b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_06="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | leftabove split | 1b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_07="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 4b | wincmd l | leftabove split | 1b | wincmd j | 3b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_08="-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 4b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_09="-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | 4b | wincmd j | leftabove vertical split | 1b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_10="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_11="-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnew | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_12="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_13="-c \"set hidden diffopt-=hiddenoff | 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 | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_14="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 2b | wincmd l | 3b | tabnew | leftabove vertical split | 2b | wincmd l | 1b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_15="-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnew | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"" |
| EXPECTED_CMD_16="-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnew | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | 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 diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | quit | wincmd l | 2b | wincmd j | 3b | execute 'tabdo windo diffthis' | 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 |
| } |