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