Sync with 'maint'
[git/gitster.git] / mergetools / vimdiff
blobffc9be86c83da1f3e342a98b8dcb20de83469a44
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_DEBUG
33         # is set.
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         start=0
70         end=${#LAYOUT}
72         nested=0
73         nested_min=100
75         # Step 1:
76         #
77         # Increase/decrease "start"/"end" indices respectively to get rid of
78         # outer parenthesis.
79         #
80         # Example:
81         #
82         #   - BEFORE: (( LOCAL , BASE ) / MERGED )
83         #   - AFTER :  ( LOCAL , BASE ) / MERGED
85         oldIFS=$IFS
86         IFS=#
87         for c in $(echo "$LAYOUT" | sed 's:.:&#:g')
88         do
89                 if test -z "$c" || test "$c" = " "
90                 then
91                         continue
92                 fi
94                 if test "$c" = "("
95                 then
96                         nested=$(( nested + 1 ))
97                         continue
98                 fi
100                 if test "$c" = ")"
101                 then
102                         nested=$(( nested - 1 ))
103                         continue
104                 fi
106                 if test "$nested" -lt "$nested_min"
107                 then
108                         nested_min=$nested
109                 fi
110         done
111         IFS=$oldIFS
113         debug_print "NESTED MIN: $nested_min"
115         while test "$nested_min" -gt "0"
116         do
117                 start=$(( start + 1 ))
118                 end=$(( end - 1 ))
120                 start_minus_one=$(( start - 1 ))
122                 while ! test "$(substring "$LAYOUT" "$start_minus_one" 1)" = "("
123                 do
124                         start=$(( start + 1 ))
125                         start_minus_one=$(( start_minus_one + 1 ))
126                 done
128                 while ! test "$(substring "$LAYOUT" "$end" 1)" = ")"
129                 do
130                         end=$(( end - 1 ))
131                 done
133                 nested_min=$(( nested_min - 1 ))
134         done
136         debug_print "CLEAN     : $(substring "$LAYOUT" "$start" "$(( end - start ))")"
139         # Step 2:
140         #
141         # Search for all valid separators ("/" or ",") which are *not*
142         # inside parenthesis. Save the index at which each of them makes the
143         # first appearance.
145         index_horizontal_split=""
146         index_vertical_split=""
148         nested=0
149         i=$(( start - 1 ))
151         oldIFS=$IFS
152         IFS=#
153         for c in $(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:.:&#:g');
154         do
155                 i=$(( i + 1 ))
157                 if test "$c" = " "
158                 then
159                         continue
160                 fi
162                 if test "$c" = "("
163                 then
164                         nested=$(( nested + 1 ))
165                         continue
166                 fi
168                 if test "$c" = ")"
169                 then
170                         nested=$(( nested - 1 ))
171                         continue
172                 fi
174                 if test "$nested" = 0
175                 then
176                         current=$c
178                         if test "$current" = "/"
179                         then
180                                 if test -z "$index_horizontal_split"
181                                 then
182                                         index_horizontal_split=$i
183                                 fi
185                         elif test "$current" = ","
186                         then
187                                 if test -z "$index_vertical_split"
188                                 then
189                                         index_vertical_split=$i
190                                 fi
191                         fi
192                 fi
193         done
194         IFS=$oldIFS
197         # Step 3:
198         #
199         # Process the separator with the highest order of precedence
200         # (";" has the highest precedence and "|" the lowest one).
201         #
202         # By "process" I mean recursively call this function twice: the first
203         # one with the substring at the left of the separator and the second one
204         # with the one at its right.
206         terminate="false"
208         if ! test -z "$index_horizontal_split"
209         then
210                 before="leftabove split"
211                 after="wincmd j"
212                 index=$index_horizontal_split
213                 terminate="true"
215         elif ! test -z "$index_vertical_split"
216         then
217                 before="leftabove vertical split"
218                 after="wincmd l"
219                 index=$index_vertical_split
220                 terminate="true"
221         fi
223         if  test "$terminate" = "true"
224         then
225                 CMD="$CMD | $before"
226                 CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$start" "$(( index - start ))")" "$CMD")
227                 CMD="$CMD | $after"
228                 CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$(( index + 1 ))" "$(( ${#LAYOUT} - index ))")" "$CMD")
229                 echo "$CMD"
230                 return
231         fi
234         # Step 4:
235         #
236         # If we reach this point, it means there are no separators and we just
237         # need to print the command to display the specified buffer
239         target=$(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:[ @();|-]::g')
241         if test "$target" = "LOCAL"
242         then
243                 CMD="$CMD | 1b"
245         elif test "$target" = "BASE"
246         then
247                 CMD="$CMD | 2b"
249         elif test "$target" = "REMOTE"
250         then
251                 CMD="$CMD | 3b"
253         elif test "$target" = "MERGED"
254         then
255                 CMD="$CMD | 4b"
257         else
258                 CMD="$CMD | ERROR: >$target<"
259         fi
261         echo "$CMD"
262         return
266 gen_cmd () {
267         # This function returns (in global variable FINAL_CMD) the string that
268         # you can use when invoking "vim" (as shown next) to obtain a given
269         # layout:
270         #
271         #   $ vim -f $FINAL_CMD "$LOCAL" "$BASE" "$REMOTE" "$MERGED"
272         #
273         # It takes one single argument: a string containing the desired layout
274         # definition.
275         #
276         # The syntax of the "layout definitions" is explained in "Documentation/
277         # mergetools/vimdiff.txt" but you can already intuitively understand how
278         # it works by knowing that...
279         #
280         #   * "+" means "a new vim tab"
281         #   * "/" means "a new vim horizontal split"
282         #   * "," means "a new vim vertical split"
283         #
284         # It also returns (in global variable FINAL_TARGET) the name ("LOCAL",
285         # "BASE", "REMOTE" or "MERGED") of the file that is marked with an "@",
286         # or "MERGED" if none of them is.
287         #
288         # Example:
289         #
290         #     gen_cmd "@LOCAL , REMOTE"
291         #     |
292         #     `-> FINAL_CMD    == "-c \"echo | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
293         #         FINAL_TARGET == "LOCAL"
295         LAYOUT=$1
298         # Search for a "@" in one of the files identifiers ("LOCAL", "BASE",
299         # "REMOTE", "MERGED"). If not found, use "MERGE" as the default file
300         # where changes will be saved.
302         if echo "$LAYOUT" | grep @LOCAL >/dev/null
303         then
304                 FINAL_TARGET="LOCAL"
305         elif echo "$LAYOUT" | grep @BASE >/dev/null
306         then
307                 FINAL_TARGET="BASE"
308         else
309                 FINAL_TARGET="MERGED"
310         fi
313         # Obtain the first part of vim "-c" option to obtain the desired layout
315         CMD=
316         oldIFS=$IFS
317         IFS=+
318         for tab in $LAYOUT
319         do
320                 if test -z "$CMD"
321                 then
322                         CMD="echo" # vim "nop" operator
323                 else
324                         CMD="$CMD | tabnew"
325                 fi
327                 # If this is a single window diff with all the buffers
328                 if ! echo "$tab" | grep -E ",|/" >/dev/null
329                 then
330                         CMD="$CMD | silent execute 'bufdo diffthis'"
331                 fi
333                 CMD=$(gen_cmd_aux "$tab" "$CMD")
334         done
335         IFS=$oldIFS
337         CMD="$CMD | execute 'tabdo windo diffthis'"
339         FINAL_CMD="-c \"set hidden diffopt-=hiddenoff | $CMD | tabfirst\""
343 ################################################################################
344 ## API functions (called from "git-mergetool--lib.sh")
345 ################################################################################
347 diff_cmd () {
348         "$merge_tool_path" -R -f -d \
349                 -c 'wincmd l' -c 'cd $GIT_PREFIX' "$LOCAL" "$REMOTE"
353 diff_cmd_help () {
354         TOOL=$1
356         case "$TOOL" in
357         nvimdiff*)
358                 printf "Use Neovim"
359                 ;;
360         gvimdiff*)
361                 printf "Use gVim (requires a graphical session)"
362                 ;;
363         vimdiff*)
364                 printf "Use Vim"
365                 ;;
366         esac
368         return 0
372 merge_cmd () {
373         TOOL=$1
375         layout=$(git config "mergetool.$TOOL.layout")
377         # backward compatibility:
378         if test -z "$layout"
379         then
380                 layout=$(git config mergetool.vimdiff.layout)
381         fi
383         case "$TOOL" in
384         *vimdiff)
385                 if test -z "$layout"
386                 then
387                         # Default layout when none is specified
388                         layout="(LOCAL,BASE,REMOTE)/MERGED"
389                 fi
390                 ;;
391         *vimdiff1)
392                 layout="@LOCAL,REMOTE"
393                 ;;
394         *vimdiff2)
395                 layout="LOCAL,MERGED,REMOTE"
396                 ;;
397         *vimdiff3)
398                 layout="MERGED"
399                 ;;
400         esac
402         gen_cmd "$layout"
404         debug_print ""
405         debug_print "FINAL CMD : $FINAL_CMD"
406         debug_print "FINAL TAR : $FINAL_TARGET"
408         if $base_present
409         then
410                 eval '"$merge_tool_path"' \
411                         -f "$FINAL_CMD" '"$LOCAL"' '"$BASE"' '"$REMOTE"' '"$MERGED"'
412         else
413                 # If there is no BASE (example: a merge conflict in a new file
414                 # with the same name created in both branches which didn't exist
415                 # before), close all BASE windows using vim's "quit" command
417                 FINAL_CMD=$(echo "$FINAL_CMD" | \
418                         sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g')
420                 eval '"$merge_tool_path"' \
421                         -f "$FINAL_CMD" '"$LOCAL"' '"$REMOTE"' '"$MERGED"'
422         fi
424         ret="$?"
426         if test "$ret" -eq 0
427         then
428                 case "$FINAL_TARGET" in
429                 LOCAL)
430                         source_path="$LOCAL"
431                         ;;
432                 REMOTE)
433                         source_path="$REMOTE"
434                         ;;
435                 MERGED|*)
436                         # Do nothing
437                         source_path=
438                         ;;
439                 esac
441                 if test -n "$source_path"
442                 then
443                         cp "$source_path" "$MERGED"
444                 fi
445         fi
447         return "$ret"
451 merge_cmd_help () {
452         TOOL=$1
454         case "$TOOL" in
455         nvimdiff*)
456                 printf "Use Neovim "
457                 ;;
458         gvimdiff*)
459                 printf "Use gVim (requires a graphical session) "
460                 ;;
461         vimdiff*)
462                 printf "Use Vim "
463                 ;;
464         esac
466         case "$TOOL" in
467         *1)
468                 echo "with a 2 panes layout (LOCAL and REMOTE)"
469                 ;;
470         *2)
471                 echo "with a 3 panes layout (LOCAL, MERGED and REMOTE)"
472                 ;;
473         *3)
474                 echo "where only the MERGED file is shown"
475                 ;;
476         *)
477                 echo "with a custom layout (see \`git help mergetool\`'s \`BACKEND SPECIFIC HINTS\` section)"
478                 ;;
479         esac
481         return 0
485 translate_merge_tool_path () {
486         case "$1" in
487         nvimdiff*)
488                 echo nvim
489                 ;;
490         gvimdiff*)
491                 echo gvim
492                 ;;
493         vimdiff*)
494                 echo vim
495                 ;;
496         esac
500 exit_code_trustable () {
501         true
505 list_tool_variants () {
506         if test "$TOOL_MODE" = "diff"
507         then
508                 for prefix in '' g n
509                 do
510                         echo "${prefix}vimdiff"
511                 done
512         else
513                 for prefix in '' g n
514                 do
515                         for suffix in '' 1 2 3
516                         do
517                                 echo "${prefix}vimdiff${suffix}"
518                         done
519                 done
520         fi
524 ################################################################################
525 ## Unit tests (called from scripts inside the "t" folder)
526 ################################################################################
528 run_unit_tests () {
529         # Function to make sure that we don't break anything when modifying this
530         # script.
532         NUMBER_OF_TEST_CASES=16
534         TEST_CASE_01="(LOCAL,BASE,REMOTE)/MERGED"   # default behaviour
535         TEST_CASE_02="@LOCAL,REMOTE"                # when using vimdiff1
536         TEST_CASE_03="LOCAL,MERGED,REMOTE"          # when using vimdiff2
537         TEST_CASE_04="MERGED"                       # when using vimdiff3
538         TEST_CASE_05="LOCAL/MERGED/REMOTE"
539         TEST_CASE_06="(LOCAL/REMOTE),MERGED"
540         TEST_CASE_07="MERGED,(LOCAL/REMOTE)"
541         TEST_CASE_08="(LOCAL,REMOTE)/MERGED"
542         TEST_CASE_09="MERGED/(LOCAL,REMOTE)"
543         TEST_CASE_10="(LOCAL/BASE/REMOTE),MERGED"
544         TEST_CASE_11="(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED"
545         TEST_CASE_12="((LOCAL,REMOTE)/BASE),MERGED"
546         TEST_CASE_13="((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)"
547         TEST_CASE_14="BASE,REMOTE+BASE,LOCAL"
548         TEST_CASE_15="  ((  (LOCAL , BASE , REMOTE) / MERGED))   +(BASE)   , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) ,    MERGED   )  "
549         TEST_CASE_16="LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED"
551         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\""
552         EXPECTED_CMD_02="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 1b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\""
553         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\""
554         EXPECTED_CMD_04="-c \"set hidden diffopt-=hiddenoff | echo | silent execute 'bufdo diffthis' | 4b | execute 'tabdo windo diffthis' | tabfirst\""
555         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\""
556         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\""
557         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\""
558         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\""
559         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\""
560         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\""
561         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\""
562         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\""
563         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\""
564         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\""
565         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\""
566         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\""
568         EXPECTED_TARGET_01="MERGED"
569         EXPECTED_TARGET_02="LOCAL"
570         EXPECTED_TARGET_03="MERGED"
571         EXPECTED_TARGET_04="MERGED"
572         EXPECTED_TARGET_05="MERGED"
573         EXPECTED_TARGET_06="MERGED"
574         EXPECTED_TARGET_07="MERGED"
575         EXPECTED_TARGET_08="MERGED"
576         EXPECTED_TARGET_09="MERGED"
577         EXPECTED_TARGET_10="MERGED"
578         EXPECTED_TARGET_11="MERGED"
579         EXPECTED_TARGET_12="MERGED"
580         EXPECTED_TARGET_13="MERGED"
581         EXPECTED_TARGET_14="MERGED"
582         EXPECTED_TARGET_15="MERGED"
583         EXPECTED_TARGET_16="MERGED"
585         at_least_one_ko="false"
587         for i in $(seq -w 1 99)
588         do
589                 if test "$i" -gt $NUMBER_OF_TEST_CASES
590                 then
591                         break
592                 fi
594                 gen_cmd "$(eval echo \${TEST_CASE_"$i"})"
596                 if test "$FINAL_CMD" = "$(eval echo \${EXPECTED_CMD_"$i"})" \
597                         && test "$FINAL_TARGET" = "$(eval echo \${EXPECTED_TARGET_"$i"})"
598                 then
599                         printf "Test Case #%02d: OK\n" "$(echo "$i" | sed 's/^0*//')"
600                 else
601                         printf "Test Case #%02d: KO !!!!\n" "$(echo "$i" | sed 's/^0*//')"
602                         echo "  FINAL_CMD              : $FINAL_CMD"
603                         echo "  FINAL_CMD (expected)   : $(eval echo \${EXPECTED_CMD_"$i"})"
604                         echo "  FINAL_TARGET           : $FINAL_TARGET"
605                         echo "  FINAL_TARGET (expected): $(eval echo \${EXPECTED_TARGET_"$i"})"
606                         at_least_one_ko="true"
607                 fi
608         done
610         # verify that `merge_cmd` handles paths with spaces
611         record_parameters () {
612                 >actual
613                 for arg
614                 do
615                         echo "$arg" >>actual
616                 done
617         }
619         base_present=false
620         LOCAL='lo cal'
621         BASE='ba se'
622         REMOTE="' '"
623         MERGED='mer ged'
624         merge_tool_path=record_parameters
626         merge_cmd vimdiff || at_least_one_ko=true
628         cat >expect <<-\EOF
629         -f
630         -c
631         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
632         lo cal
633         ' '
634         mer ged
635         EOF
637         diff -u expect actual || at_least_one_ko=true
639         if test "$at_least_one_ko" = "true"
640         then
641                 return 255
642         else
643                 return 0
644         fi