parse-options: factor out register_abbrev() and struct parsed_option
[git.git] / mergetools / vimdiff
blob06937acbf5497115c9ba4d9a2377634b1885db7e
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         layout=$(git config mergetool.vimdiff.layout)
376         case "$1" in
377         *vimdiff)
378                 if test -z "$layout"
379                 then
380                         # Default layout when none is specified
381                         layout="(LOCAL,BASE,REMOTE)/MERGED"
382                 fi
383                 ;;
384         *vimdiff1)
385                 layout="@LOCAL,REMOTE"
386                 ;;
387         *vimdiff2)
388                 layout="LOCAL,MERGED,REMOTE"
389                 ;;
390         *vimdiff3)
391                 layout="MERGED"
392                 ;;
393         esac
395         gen_cmd "$layout"
397         debug_print ""
398         debug_print "FINAL CMD : $FINAL_CMD"
399         debug_print "FINAL TAR : $FINAL_TARGET"
401         if $base_present
402         then
403                 eval '"$merge_tool_path"' \
404                         -f "$FINAL_CMD" '"$LOCAL"' '"$BASE"' '"$REMOTE"' '"$MERGED"'
405         else
406                 # If there is no BASE (example: a merge conflict in a new file
407                 # with the same name created in both braches which didn't exist
408                 # before), close all BASE windows using vim's "quit" command
410                 FINAL_CMD=$(echo "$FINAL_CMD" | \
411                         sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g')
413                 eval '"$merge_tool_path"' \
414                         -f "$FINAL_CMD" '"$LOCAL"' '"$REMOTE"' '"$MERGED"'
415         fi
417         ret="$?"
419         if test "$ret" -eq 0
420         then
421                 case "$FINAL_TARGET" in
422                 LOCAL)
423                         source_path="$LOCAL"
424                         ;;
425                 REMOTE)
426                         source_path="$REMOTE"
427                         ;;
428                 MERGED|*)
429                         # Do nothing
430                         source_path=
431                         ;;
432                 esac
434                 if test -n "$source_path"
435                 then
436                         cp "$source_path" "$MERGED"
437                 fi
438         fi
440         return "$ret"
444 merge_cmd_help () {
445         TOOL=$1
447         case "$TOOL" in
448         nvimdiff*)
449                 printf "Use Neovim "
450                 ;;
451         gvimdiff*)
452                 printf "Use gVim (requires a graphical session) "
453                 ;;
454         vimdiff*)
455                 printf "Use Vim "
456                 ;;
457         esac
459         case "$TOOL" in
460         *1)
461                 echo "with a 2 panes layout (LOCAL and REMOTE)"
462                 ;;
463         *2)
464                 echo "with a 3 panes layout (LOCAL, MERGED and REMOTE)"
465                 ;;
466         *3)
467                 echo "where only the MERGED file is shown"
468                 ;;
469         *)
470                 echo "with a custom layout (see \`git help mergetool\`'s \`BACKEND SPECIFIC HINTS\` section)"
471                 ;;
472         esac
474         return 0
478 translate_merge_tool_path () {
479         case "$1" in
480         nvimdiff*)
481                 echo nvim
482                 ;;
483         gvimdiff*)
484                 echo gvim
485                 ;;
486         vimdiff*)
487                 echo vim
488                 ;;
489         esac
493 exit_code_trustable () {
494         true
498 list_tool_variants () {
499         if test "$TOOL_MODE" = "diff"
500         then
501                 for prefix in '' g n
502                 do
503                         echo "${prefix}vimdiff"
504                 done
505         else
506                 for prefix in '' g n
507                 do
508                         for suffix in '' 1 2 3
509                         do
510                                 echo "${prefix}vimdiff${suffix}"
511                         done
512                 done
513         fi
517 ################################################################################
518 ## Unit tests (called from scripts inside the "t" folder)
519 ################################################################################
521 run_unit_tests () {
522         # Function to make sure that we don't break anything when modifying this
523         # script.
525         NUMBER_OF_TEST_CASES=16
527         TEST_CASE_01="(LOCAL,BASE,REMOTE)/MERGED"   # default behaviour
528         TEST_CASE_02="@LOCAL,REMOTE"                # when using vimdiff1
529         TEST_CASE_03="LOCAL,MERGED,REMOTE"          # when using vimdiff2
530         TEST_CASE_04="MERGED"                       # when using vimdiff3
531         TEST_CASE_05="LOCAL/MERGED/REMOTE"
532         TEST_CASE_06="(LOCAL/REMOTE),MERGED"
533         TEST_CASE_07="MERGED,(LOCAL/REMOTE)"
534         TEST_CASE_08="(LOCAL,REMOTE)/MERGED"
535         TEST_CASE_09="MERGED/(LOCAL,REMOTE)"
536         TEST_CASE_10="(LOCAL/BASE/REMOTE),MERGED"
537         TEST_CASE_11="(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED"
538         TEST_CASE_12="((LOCAL,REMOTE)/BASE),MERGED"
539         TEST_CASE_13="((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)"
540         TEST_CASE_14="BASE,REMOTE+BASE,LOCAL"
541         TEST_CASE_15="  ((  (LOCAL , BASE , REMOTE) / MERGED))   +(BASE)   , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) ,    MERGED   )  "
542         TEST_CASE_16="LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED"
544         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\""
545         EXPECTED_CMD_02="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 1b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\""
546         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\""
547         EXPECTED_CMD_04="-c \"set hidden diffopt-=hiddenoff | echo | silent execute 'bufdo diffthis' | 4b | execute 'tabdo windo diffthis' | tabfirst\""
548         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\""
549         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\""
550         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\""
551         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\""
552         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\""
553         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\""
554         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\""
555         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\""
556         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\""
557         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\""
558         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\""
559         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\""
561         EXPECTED_TARGET_01="MERGED"
562         EXPECTED_TARGET_02="LOCAL"
563         EXPECTED_TARGET_03="MERGED"
564         EXPECTED_TARGET_04="MERGED"
565         EXPECTED_TARGET_05="MERGED"
566         EXPECTED_TARGET_06="MERGED"
567         EXPECTED_TARGET_07="MERGED"
568         EXPECTED_TARGET_08="MERGED"
569         EXPECTED_TARGET_09="MERGED"
570         EXPECTED_TARGET_10="MERGED"
571         EXPECTED_TARGET_11="MERGED"
572         EXPECTED_TARGET_12="MERGED"
573         EXPECTED_TARGET_13="MERGED"
574         EXPECTED_TARGET_14="MERGED"
575         EXPECTED_TARGET_15="MERGED"
576         EXPECTED_TARGET_16="MERGED"
578         at_least_one_ko="false"
580         for i in $(seq -w 1 99)
581         do
582                 if test "$i" -gt $NUMBER_OF_TEST_CASES
583                 then
584                         break
585                 fi
587                 gen_cmd "$(eval echo \${TEST_CASE_"$i"})"
589                 if test "$FINAL_CMD" = "$(eval echo \${EXPECTED_CMD_"$i"})" \
590                         && test "$FINAL_TARGET" = "$(eval echo \${EXPECTED_TARGET_"$i"})"
591                 then
592                         printf "Test Case #%02d: OK\n" "$(echo "$i" | sed 's/^0*//')"
593                 else
594                         printf "Test Case #%02d: KO !!!!\n" "$(echo "$i" | sed 's/^0*//')"
595                         echo "  FINAL_CMD              : $FINAL_CMD"
596                         echo "  FINAL_CMD (expected)   : $(eval echo \${EXPECTED_CMD_"$i"})"
597                         echo "  FINAL_TARGET           : $FINAL_TARGET"
598                         echo "  FINAL_TARGET (expected): $(eval echo \${EXPECTED_TARGET_"$i"})"
599                         at_least_one_ko="true"
600                 fi
601         done
603         # verify that `merge_cmd` handles paths with spaces
604         record_parameters () {
605                 >actual
606                 for arg
607                 do
608                         echo "$arg" >>actual
609                 done
610         }
612         base_present=false
613         LOCAL='lo cal'
614         BASE='ba se'
615         REMOTE="' '"
616         MERGED='mer ged'
617         merge_tool_path=record_parameters
619         merge_cmd vimdiff || at_least_one_ko=true
621         cat >expect <<-\EOF
622         -f
623         -c
624         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
625         lo cal
626         ' '
627         mer ged
628         EOF
630         diff -u expect actual || at_least_one_ko=true
632         if test "$at_least_one_ko" = "true"
633         then
634                 return 255
635         else
636                 return 0
637         fi