vimdiff: new implementation with layout support
[git/debian.git] / mergetools / vimdiff
blob9d1bf4f455650d2deb7b72a41e7ad6d496476c50
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):
7 #       - $BASE
8 #       - $LOCAL
9 #       - $REMOTE
10 #       - $MERGED
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
24 #     variable to be set.
27 ################################################################################
28 ## Internal functions (not meant to be used outside this script)
29 ################################################################################
31 debug_print () {
32         # Send message to stderr if global variable GIT_MERGETOOL_VIMDIFF is set
33         # to "true"
35         if test -n "$GIT_MERGETOOL_VIMDIFF_DEBUG"
36         then
37                 >&2 echo "$@"
38         fi
41 substring () {
42         # Return a substring of $1 containing $3 characters starting at
43         # zero-based offset $2.
44         #
45         # Examples:
46         #
47         #   substring "Hello world" 0 4  --> "Hell"
48         #   substring "Hello world" 3 4  --> "lo w"
49         #   substring "Hello world" 3 10 --> "lo world"
51         STRING=$1
52         START=$2
53         LEN=$3
55         echo "$STRING" | cut -c$(( START + 1 ))-$(( START + $LEN ))
58 gen_cmd_aux () {
59         # Auxiliary function used from "gen_cmd()".
60         # Read that other function documentation for more details.
62         LAYOUT=$1
63         CMD=$2  # This is a second (hidden) argument used for recursion
65         debug_print
66         debug_print "LAYOUT    : $LAYOUT"
67         debug_print "CMD       : $CMD"
69         if test -z "$CMD"
70         then
71                 CMD="echo" # vim "nop" operator
72         fi
74         start=0
75         end=${#LAYOUT}
77         nested=0
78         nested_min=100
81         # Step 1:
82         #
83         # Increase/decrease "start"/"end" indices respectively to get rid of
84         # outer parenthesis.
85         #
86         # Example:
87         #
88         #   - BEFORE: (( LOCAL , BASE ) / MERGED )
89         #   - AFTER :  ( LOCAL , BASE ) / MERGED
91         oldIFS=$IFS
92         IFS=#
93         for c in $(echo "$LAYOUT" | sed 's:.:&#:g')
94         do
95                 if test "$c" = " "
96                 then
97                         continue
98                 fi
100                 if test "$c" = "("
101                 then
102                         nested=$(( nested + 1 ))
103                         continue
104                 fi
106                 if test "$c" = ")"
107                 then
108                         nested=$(( nested - 1 ))
109                         continue
110                 fi
112                 if test "$nested" -lt "$nested_min"
113                 then
114                         nested_min=$nested
115                 fi
116         done
117         IFS=$oldIFS
119         debug_print "NESTED MIN: $nested_min"
121         while test "$nested_min" -gt "0"
122         do
123                 start=$(( start + 1 ))
124                 end=$(( end - 1 ))
126                 start_minus_one=$(( start - 1 ))
128                 while ! test "$(substring "$LAYOUT" "$start_minus_one" 1)" = "("
129                 do
130                         start=$(( start + 1 ))
131                         start_minus_one=$(( start_minus_one + 1 ))
132                 done
134                 while ! test "$(substring "$LAYOUT" "$end" 1)" = ")"
135                 do
136                         end=$(( end - 1 ))
137                 done
139                 nested_min=$(( nested_min - 1 ))
140         done
142         debug_print "CLEAN     : $(substring "$LAYOUT" "$start" "$(( end - start ))")"
145         # Step 2:
146         #
147         # Search for all valid separators ("+", "/" or ",") which are *not*
148         # inside parenthesis. Save the index at which each of them makes the
149         # first appearance.
151         index_new_tab=""
152         index_horizontal_split=""
153         index_vertical_split=""
155         nested=0
156         i=$(( start - 1 ))
158         oldIFS=$IFS
159         IFS=#
160         for c in $(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:.:&#:g');
161         do
162                 i=$(( i + 1 ))
164                 if test "$c" = " "
165                 then
166                         continue
167                 fi
169                 if test "$c" = "("
170                 then
171                         nested=$(( nested + 1 ))
172                         continue
173                 fi
175                 if test "$c" = ")"
176                 then
177                         nested=$(( nested - 1 ))
178                         continue
179                 fi
181                 if test "$nested" = 0
182                 then
183                         current=$c
185                         if test "$current" = "+"
186                         then
187                                 if test -z "$index_new_tab"
188                                 then
189                                         index_new_tab=$i
190                                 fi
192                         elif test "$current" = "/"
193                         then
194                                 if test -z "$index_horizontal_split"
195                                 then
196                                         index_horizontal_split=$i
197                                 fi
199                         elif test "$current" = ","
200                         then
201                                 if test -z "$index_vertical_split"
202                                 then
203                                         index_vertical_split=$i
204                                 fi
205                         fi
206                 fi
207         done
208         IFS=$oldIFS
211         # Step 3:
212         #
213         # Process the separator with the highest order of precedence
214         # (";" has the highest precedence and "|" the lowest one).
215         #
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.
220         terminate="false"
222         if ! test -z "$index_new_tab"
223         then
224                 before="-tabnew"
225                 after="tabnext"
226                 index=$index_new_tab
227                 terminate="true"
229         elif ! test -z "$index_horizontal_split"
230         then
231                 before="split"
232                 after="wincmd j"
233                 index=$index_horizontal_split
234                 terminate="true"
236         elif ! test -z "$index_vertical_split"
237         then
238                 before="vertical split"
239                 after="wincmd l"
240                 index=$index_vertical_split
241                 terminate="true"
242         fi
244         if  test "$terminate" = "true"
245         then
246                 CMD="$CMD | $before"
247                 CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$start" "$(( index - start ))")" "$CMD")
248                 CMD="$CMD | $after"
249                 CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$(( index + 1 ))" "$(( ${#LAYOUT} - index ))")" "$CMD")
250                 echo "$CMD"
251                 return
252         fi
255         # Step 4:
256         #
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"
263         then
264                 CMD="$CMD | 1b"
266         elif test "$target" = "BASE"
267         then
268                 CMD="$CMD | 2b"
270         elif test "$target" = "REMOTE"
271         then
272                 CMD="$CMD | 3b"
274         elif test "$target" = "MERGED"
275         then
276                 CMD="$CMD | 4b"
278         else
279                 CMD="$CMD | ERROR: >$target<"
280         fi
282         echo "$CMD"
283         return
287 gen_cmd () {
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
290         # layout:
291         #
292         #   $ vim -f $FINAL_CMD "$LOCAL" "$BASE" "$REMOTE" "$MERGED"
293         #
294         # It takes one single argument: a string containing the desired layout
295         # definition.
296         #
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...
300         #
301         #   * "+" means "a new vim tab"
302         #   * "/" means "a new vim horizontal split"
303         #   * "," means "a new vim vertical split"
304         #
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.
308         #
309         # Example:
310         #
311         #     gen_cmd "@LOCAL , REMOTE"
312         #     |
313         #     `-> FINAL_CMD    == "-c \"echo | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
314         #         FINAL_TARGET == "LOCAL"
316         LAYOUT=$1
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
324         then
325                 FINAL_TARGET="LOCAL"
326         elif echo "$LAYOUT" | grep @BASE >/dev/null
327         then
328                 FINAL_TARGET="BASE"
329         else
330                 FINAL_TARGET="MERGED"
331         fi
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
343         then
344                 CMD="$CMD | tabdo windo diffthis"
345         else
346                 CMD="$CMD | bufdo diffthis"
347         fi
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 ################################################################################
362 diff_cmd () {
363         "$merge_tool_path" -R -f -d \
364                 -c 'wincmd l' -c 'cd $GIT_PREFIX' "$LOCAL" "$REMOTE"
368 merge_cmd () {
369         layout=$(git config mergetool.vimdiff.layout)
371         case "$1" in
372         *vimdiff)
373                 if test -z "$layout"
374                 then
375                         # Default layout when none is specified
376                         layout="(LOCAL,BASE,REMOTE)/MERGED"
377                 fi
378                 ;;
379         *vimdiff1)
380                 layout="@LOCAL,REMOTE"
381                 ;;
382         *vimdiff2)
383                 layout="LOCAL,MERGED,REMOTE"
384                 ;;
385         *vimdiff3)
386                 layout="MERGED"
387                 ;;
388         esac
390         gen_cmd "$layout"
392         debug_print ""
393         debug_print "FINAL CMD : $FINAL_CMD"
394         debug_print "FINAL TAR : $FINAL_TARGET"
396         if $base_present
397         then
398                 eval "$merge_tool_path" \
399                         -f "$FINAL_CMD" "$LOCAL" "$BASE" "$REMOTE" "$MERGED"
400         else
401                 # If there is no BASE (example: a merge conflict in a new file
402                 # with the same name created in both braches which didn't exist
403                 # before), close all BASE windows using vim's "quit" command
405                 FINAL_CMD=$(echo "$FINAL_CMD" | \
406                         sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g')
408                 eval "$merge_tool_path" \
409                         -f "$FINAL_CMD" "$LOCAL" "$REMOTE" "$MERGED"
410         fi
412         ret="$?"
414         if test "$ret" -eq 0
415         then
416                 case "$FINAL_TARGET" in
417                 LOCAL)
418                         source_path="$LOCAL"
419                         ;;
420                 REMOTE)
421                         source_path="$REMOTE"
422                         ;;
423                 MERGED|*)
424                         # Do nothing
425                         source_path=
426                         ;;
427                 esac
429                 if test -n "$source_path"
430                 then
431                         cp "$source_path" "$MERGED"
432                 fi
433         fi
435         return "$ret"
439 translate_merge_tool_path () {
440         case "$1" in
441         nvimdiff*)
442                 echo nvim
443                 ;;
444         gvimdiff*)
445                 echo gvim
446                 ;;
447         vimdiff*)
448                 echo vim
449                 ;;
450         esac
454 exit_code_trustable () {
455         true
459 list_tool_variants () {
460         if test "$TOOL_MODE" = "diff"
461         then
462                 for prefix in '' g n
463                 do
464                         echo "${prefix}vimdiff"
465                 done
466         else
467                 for prefix in '' g n
468                 do
469                         for suffix in '' 1 2 3
470                         do
471                                 echo "${prefix}vimdiff${suffix}"
472                         done
473                 done
474         fi
478 ################################################################################
479 ## Unit tests (called from scripts inside the "t" folder)
480 ################################################################################
482 run_unit_tests () {
483         # Function to make sure that we don't break anything when modifying this
484         # script.
486         NUMBER_OF_TEST_CASES=16
488         TEST_CASE_01="(LOCAL,BASE,REMOTE)/MERGED"   # default behaviour
489         TEST_CASE_02="@LOCAL,REMOTE"                # when using vimdiff1
490         TEST_CASE_03="LOCAL,MERGED,REMOTE"          # when using vimdiff2
491         TEST_CASE_04="MERGED"                       # when using vimdiff3
492         TEST_CASE_05="LOCAL/MERGED/REMOTE"
493         TEST_CASE_06="(LOCAL/REMOTE),MERGED"
494         TEST_CASE_07="MERGED,(LOCAL/REMOTE)"
495         TEST_CASE_08="(LOCAL,REMOTE)/MERGED"
496         TEST_CASE_09="MERGED/(LOCAL,REMOTE)"
497         TEST_CASE_10="(LOCAL/BASE/REMOTE),MERGED"
498         TEST_CASE_11="(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED"
499         TEST_CASE_12="((LOCAL,REMOTE)/BASE),MERGED"
500         TEST_CASE_13="((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)"
501         TEST_CASE_14="BASE,REMOTE+BASE,LOCAL"
502         TEST_CASE_15="  ((  (LOCAL , BASE , REMOTE) / MERGED))   +(BASE)   , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) ,    MERGED   )  "
503         TEST_CASE_16="LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED"
505         EXPECTED_CMD_01="-c \"echo | split | vertical split | 1b | wincmd l | vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
506         EXPECTED_CMD_02="-c \"echo | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
507         EXPECTED_CMD_03="-c \"echo | vertical split | 1b | wincmd l | vertical split | 4b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
508         EXPECTED_CMD_04="-c \"echo | 4b | bufdo diffthis\" -c \"tabfirst\""
509         EXPECTED_CMD_05="-c \"echo | split | 1b | wincmd j | split | 4b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
510         EXPECTED_CMD_06="-c \"echo | vertical split | split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
511         EXPECTED_CMD_07="-c \"echo | vertical split | 4b | wincmd l | split | 1b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
512         EXPECTED_CMD_08="-c \"echo | split | vertical split | 1b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
513         EXPECTED_CMD_09="-c \"echo | split | 4b | wincmd j | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
514         EXPECTED_CMD_10="-c \"echo | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
515         EXPECTED_CMD_11="-c \"echo | -tabnew | split | vertical split | 1b | wincmd l | vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
516         EXPECTED_CMD_12="-c \"echo | vertical split | split | vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
517         EXPECTED_CMD_13="-c \"echo | vertical split | split | vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | vertical split | split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
518         EXPECTED_CMD_14="-c \"echo | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | 2b | wincmd l | 1b | tabdo windo diffthis\" -c \"tabfirst\""
519         EXPECTED_CMD_15="-c \"echo | -tabnew | split | vertical split | 1b | wincmd l | vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
520         EXPECTED_CMD_16="-c \"echo | -tabnew | split | vertical split | 1b | wincmd l | vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
522         EXPECTED_TARGET_01="MERGED"
523         EXPECTED_TARGET_02="LOCAL"
524         EXPECTED_TARGET_03="MERGED"
525         EXPECTED_TARGET_04="MERGED"
526         EXPECTED_TARGET_05="MERGED"
527         EXPECTED_TARGET_06="MERGED"
528         EXPECTED_TARGET_07="MERGED"
529         EXPECTED_TARGET_08="MERGED"
530         EXPECTED_TARGET_09="MERGED"
531         EXPECTED_TARGET_10="MERGED"
532         EXPECTED_TARGET_11="MERGED"
533         EXPECTED_TARGET_12="MERGED"
534         EXPECTED_TARGET_13="MERGED"
535         EXPECTED_TARGET_14="MERGED"
536         EXPECTED_TARGET_15="MERGED"
537         EXPECTED_TARGET_16="MERGED"
539         at_least_one_ko="false"
541         for i in $(seq -w 1 99)
542         do
543                 if test "$i" -gt $NUMBER_OF_TEST_CASES
544                 then
545                         break
546                 fi
548                 gen_cmd "$(eval echo \${TEST_CASE_"$i"})"
550                 if test "$FINAL_CMD" = "$(eval echo \${EXPECTED_CMD_"$i"})" \
551                         && test "$FINAL_TARGET" = "$(eval echo \${EXPECTED_TARGET_"$i"})"
552                 then
553                         printf "Test Case #%02d: OK\n" "$(echo "$i" | sed 's/^0*//')"
554                 else
555                         printf "Test Case #%02d: KO !!!!\n" "$(echo "$i" | sed 's/^0*//')"
556                         echo "  FINAL_CMD              : $FINAL_CMD"
557                         echo "  FINAL_CMD (expected)   : $(eval echo \${EXPECTED_CMD_"$i"})"
558                         echo "  FINAL_TARGET           : $FINAL_TARGET"
559                         echo "  FINAL_TARGET (expected): $(eval echo \${EXPECTED_TARGET_"$i"})"
560                         at_least_one_ko="true"
561                 fi
562         done
564         if test "$at_least_one_ko" = "true"
565         then
566                 return 255
567         else
568                 return 0
569         fi