]> git.ipfire.org Git - thirdparty/git.git/blame - mergetools/vimdiff
vimdiff: make layout engine more robust against user vim settings
[thirdparty/git.git] / mergetools / vimdiff
CommitLineData
00417974
FR
1# This script can be run in two different contexts:
2#
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):
6#
7# - $BASE
8# - $LOCAL
9# - $REMOTE
10# - $MERGED
11#
12# In this mode, all this script does is to run the next command:
13#
14# vim -f -c ... $LOCAL $BASE $REMOTE $MERGED
15#
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.
19#
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.
25
26
27################################################################################
28## Internal functions (not meant to be used outside this script)
29################################################################################
30
31debug_print () {
32 # Send message to stderr if global variable GIT_MERGETOOL_VIMDIFF is set
33 # to "true"
34
35 if test -n "$GIT_MERGETOOL_VIMDIFF_DEBUG"
36 then
37 >&2 echo "$@"
38 fi
39}
40
41substring () {
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"
50
51 STRING=$1
52 START=$2
53 LEN=$3
54
55 echo "$STRING" | cut -c$(( START + 1 ))-$(( START + $LEN ))
56}
57
58gen_cmd_aux () {
59 # Auxiliary function used from "gen_cmd()".
60 # Read that other function documentation for more details.
61
62 LAYOUT=$1
63 CMD=$2 # This is a second (hidden) argument used for recursion
64
65 debug_print
66 debug_print "LAYOUT : $LAYOUT"
67 debug_print "CMD : $CMD"
68
69 if test -z "$CMD"
70 then
71 CMD="echo" # vim "nop" operator
72 fi
73
74 start=0
75 end=${#LAYOUT}
76
77 nested=0
78 nested_min=100
79
80
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
90
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
99
100 if test "$c" = "("
101 then
102 nested=$(( nested + 1 ))
103 continue
104 fi
105
106 if test "$c" = ")"
107 then
108 nested=$(( nested - 1 ))
109 continue
110 fi
111
112 if test "$nested" -lt "$nested_min"
113 then
114 nested_min=$nested
115 fi
116 done
117 IFS=$oldIFS
118
119 debug_print "NESTED MIN: $nested_min"
120
121 while test "$nested_min" -gt "0"
122 do
123 start=$(( start + 1 ))
124 end=$(( end - 1 ))
125
126 start_minus_one=$(( start - 1 ))
127
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
133
134 while ! test "$(substring "$LAYOUT" "$end" 1)" = ")"
135 do
136 end=$(( end - 1 ))
137 done
138
139 nested_min=$(( nested_min - 1 ))
140 done
141
142 debug_print "CLEAN : $(substring "$LAYOUT" "$start" "$(( end - start ))")"
143
144
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.
150
151 index_new_tab=""
152 index_horizontal_split=""
153 index_vertical_split=""
154
155 nested=0
156 i=$(( start - 1 ))
157
158 oldIFS=$IFS
159 IFS=#
160 for c in $(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:.:&#:g');
161 do
162 i=$(( i + 1 ))
163
164 if test "$c" = " "
165 then
166 continue
167 fi
168
169 if test "$c" = "("
170 then
171 nested=$(( nested + 1 ))
172 continue
173 fi
174
175 if test "$c" = ")"
176 then
177 nested=$(( nested - 1 ))
178 continue
179 fi
180
181 if test "$nested" = 0
182 then
183 current=$c
184
185 if test "$current" = "+"
186 then
187 if test -z "$index_new_tab"
188 then
189 index_new_tab=$i
190 fi
191
192 elif test "$current" = "/"
193 then
194 if test -z "$index_horizontal_split"
195 then
196 index_horizontal_split=$i
197 fi
198
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
209
210
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.
219
220 terminate="false"
221
222 if ! test -z "$index_new_tab"
223 then
224 before="-tabnew"
225 after="tabnext"
226 index=$index_new_tab
227 terminate="true"
228
229 elif ! test -z "$index_horizontal_split"
230 then
f3d7623a 231 before="leftabove split"
00417974
FR
232 after="wincmd j"
233 index=$index_horizontal_split
234 terminate="true"
235
236 elif ! test -z "$index_vertical_split"
237 then
f3d7623a 238 before="leftabove vertical split"
00417974
FR
239 after="wincmd l"
240 index=$index_vertical_split
241 terminate="true"
242 fi
243
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
253
254
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
259
260 target=$(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:[ @();|-]::g')
261
262 if test "$target" = "LOCAL"
263 then
264 CMD="$CMD | 1b"
265
266 elif test "$target" = "BASE"
267 then
268 CMD="$CMD | 2b"
269
270 elif test "$target" = "REMOTE"
271 then
272 CMD="$CMD | 3b"
273
274 elif test "$target" = "MERGED"
275 then
276 CMD="$CMD | 4b"
277
278 else
279 CMD="$CMD | ERROR: >$target<"
280 fi
281
282 echo "$CMD"
283 return
284}
285
286
287gen_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 # |
f3d7623a 313 # `-> FINAL_CMD == "-c \"echo | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
00417974
FR
314 # FINAL_TARGET == "LOCAL"
315
316 LAYOUT=$1
317
318
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.
322
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
332
333
334 # Obtain the first part of vim "-c" option to obtain the desired layout
335
336 CMD=$(gen_cmd_aux "$LAYOUT")
337
338
339 # Adjust the just obtained script depending on whether more than one
340 # windows are visible or not
341
342 if echo "$LAYOUT" | grep ",\|/" >/dev/null
343 then
344 CMD="$CMD | tabdo windo diffthis"
345 else
346 CMD="$CMD | bufdo diffthis"
347 fi
348
349
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
353
354 FINAL_CMD="-c \"$CMD\" -c \"tabfirst\""
355}
356
357
358################################################################################
359## API functions (called from "git-mergetool--lib.sh")
360################################################################################
361
bc7a96a8 362diff_cmd () {
b2a6b712
DA
363 "$merge_tool_path" -R -f -d \
364 -c 'wincmd l' -c 'cd $GIT_PREFIX' "$LOCAL" "$REMOTE"
bc7a96a8
DA
365}
366
00417974 367
7b5cf8be
FR
368diff_cmd_help () {
369 TOOL=$1
370
371 case "$TOOL" in
372 nvimdiff*)
373 printf "Use Neovim"
374 ;;
375 gvimdiff*)
376 printf "Use gVim (requires a graphical session)"
377 ;;
378 vimdiff*)
379 printf "Use Vim"
380 ;;
381 esac
382
383 return 0
384}
385
386
bc7a96a8 387merge_cmd () {
00417974
FR
388 layout=$(git config mergetool.vimdiff.layout)
389
bc7a96a8 390 case "$1" in
11868978 391 *vimdiff)
00417974 392 if test -z "$layout"
bc7a96a8 393 then
00417974
FR
394 # Default layout when none is specified
395 layout="(LOCAL,BASE,REMOTE)/MERGED"
bc7a96a8
DA
396 fi
397 ;;
30bb8088 398 *vimdiff1)
00417974 399 layout="@LOCAL,REMOTE"
30bb8088 400 ;;
11868978 401 *vimdiff2)
00417974 402 layout="LOCAL,MERGED,REMOTE"
bc7a96a8 403 ;;
11868978 404 *vimdiff3)
00417974 405 layout="MERGED"
7c147b77 406 ;;
bc7a96a8 407 esac
00417974
FR
408
409 gen_cmd "$layout"
410
411 debug_print ""
412 debug_print "FINAL CMD : $FINAL_CMD"
413 debug_print "FINAL TAR : $FINAL_TARGET"
414
415 if $base_present
416 then
417 eval "$merge_tool_path" \
418 -f "$FINAL_CMD" "$LOCAL" "$BASE" "$REMOTE" "$MERGED"
419 else
420 # If there is no BASE (example: a merge conflict in a new file
421 # with the same name created in both braches which didn't exist
422 # before), close all BASE windows using vim's "quit" command
423
424 FINAL_CMD=$(echo "$FINAL_CMD" | \
425 sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g')
426
427 eval "$merge_tool_path" \
428 -f "$FINAL_CMD" "$LOCAL" "$REMOTE" "$MERGED"
429 fi
430
431 ret="$?"
432
433 if test "$ret" -eq 0
434 then
435 case "$FINAL_TARGET" in
436 LOCAL)
437 source_path="$LOCAL"
438 ;;
439 REMOTE)
440 source_path="$REMOTE"
441 ;;
442 MERGED|*)
443 # Do nothing
444 source_path=
445 ;;
446 esac
447
448 if test -n "$source_path"
449 then
450 cp "$source_path" "$MERGED"
451 fi
452 fi
453
454 return "$ret"
bc7a96a8
DA
455}
456
00417974 457
7b5cf8be
FR
458merge_cmd_help () {
459 TOOL=$1
460
461 case "$TOOL" in
462 nvimdiff*)
463 printf "Use Neovim "
464 ;;
465 gvimdiff*)
466 printf "Use gVim (requires a graphical session) "
467 ;;
468 vimdiff*)
469 printf "Use Vim "
470 ;;
471 esac
472
473 case "$TOOL" in
474 *1)
475 echo "with a 2 panes layout (LOCAL and REMOTE)"
476 ;;
477 *2)
478 echo "with a 3 panes layout (LOCAL, MERGED and REMOTE)"
479 ;;
480 *3)
481 echo "where only the MERGED file is shown"
482 ;;
483 *)
484 echo "with a custom layout (see \`git help mergetool\`'s \`BACKEND SPECIFIC HINTS\` section)"
485 ;;
486 esac
487
488 return 0
489}
490
491
00417974 492translate_merge_tool_path () {
bc7a96a8 493 case "$1" in
11868978 494 nvimdiff*)
495 echo nvim
496 ;;
497 gvimdiff*)
bc7a96a8
DA
498 echo gvim
499 ;;
11868978 500 vimdiff*)
bc7a96a8
DA
501 echo vim
502 ;;
503 esac
504}
29672844 505
00417974 506
29672844
DA
507exit_code_trustable () {
508 true
509}
83bbf9b9 510
00417974 511
83bbf9b9 512list_tool_variants () {
00417974
FR
513 if test "$TOOL_MODE" = "diff"
514 then
515 for prefix in '' g n
516 do
517 echo "${prefix}vimdiff"
83bbf9b9 518 done
00417974
FR
519 else
520 for prefix in '' g n
521 do
522 for suffix in '' 1 2 3
523 do
524 echo "${prefix}vimdiff${suffix}"
525 done
526 done
527 fi
528}
529
530
531################################################################################
532## Unit tests (called from scripts inside the "t" folder)
533################################################################################
534
535run_unit_tests () {
536 # Function to make sure that we don't break anything when modifying this
537 # script.
538
539 NUMBER_OF_TEST_CASES=16
540
541 TEST_CASE_01="(LOCAL,BASE,REMOTE)/MERGED" # default behaviour
542 TEST_CASE_02="@LOCAL,REMOTE" # when using vimdiff1
543 TEST_CASE_03="LOCAL,MERGED,REMOTE" # when using vimdiff2
544 TEST_CASE_04="MERGED" # when using vimdiff3
545 TEST_CASE_05="LOCAL/MERGED/REMOTE"
546 TEST_CASE_06="(LOCAL/REMOTE),MERGED"
547 TEST_CASE_07="MERGED,(LOCAL/REMOTE)"
548 TEST_CASE_08="(LOCAL,REMOTE)/MERGED"
549 TEST_CASE_09="MERGED/(LOCAL,REMOTE)"
550 TEST_CASE_10="(LOCAL/BASE/REMOTE),MERGED"
551 TEST_CASE_11="(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED"
552 TEST_CASE_12="((LOCAL,REMOTE)/BASE),MERGED"
553 TEST_CASE_13="((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)"
554 TEST_CASE_14="BASE,REMOTE+BASE,LOCAL"
555 TEST_CASE_15=" (( (LOCAL , BASE , REMOTE) / MERGED)) +(BASE) , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) , MERGED ) "
556 TEST_CASE_16="LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED"
557
f3d7623a
FR
558 EXPECTED_CMD_01="-c \"echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
559 EXPECTED_CMD_02="-c \"echo | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
560 EXPECTED_CMD_03="-c \"echo | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 4b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
00417974 561 EXPECTED_CMD_04="-c \"echo | 4b | bufdo diffthis\" -c \"tabfirst\""
f3d7623a
FR
562 EXPECTED_CMD_05="-c \"echo | leftabove split | 1b | wincmd j | leftabove split | 4b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
563 EXPECTED_CMD_06="-c \"echo | leftabove vertical split | leftabove split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
564 EXPECTED_CMD_07="-c \"echo | leftabove vertical split | 4b | wincmd l | leftabove split | 1b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
565 EXPECTED_CMD_08="-c \"echo | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
566 EXPECTED_CMD_09="-c \"echo | leftabove split | 4b | wincmd j | leftabove vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
567 EXPECTED_CMD_10="-c \"echo | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
568 EXPECTED_CMD_11="-c \"echo | -tabnew | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
569 EXPECTED_CMD_12="-c \"echo | leftabove vertical split | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
570 EXPECTED_CMD_13="-c \"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 | tabdo windo diffthis\" -c \"tabfirst\""
571 EXPECTED_CMD_14="-c \"echo | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | 2b | wincmd l | 1b | tabdo windo diffthis\" -c \"tabfirst\""
572 EXPECTED_CMD_15="-c \"echo | -tabnew | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
573 EXPECTED_CMD_16="-c \"echo | -tabnew | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
00417974
FR
574
575 EXPECTED_TARGET_01="MERGED"
576 EXPECTED_TARGET_02="LOCAL"
577 EXPECTED_TARGET_03="MERGED"
578 EXPECTED_TARGET_04="MERGED"
579 EXPECTED_TARGET_05="MERGED"
580 EXPECTED_TARGET_06="MERGED"
581 EXPECTED_TARGET_07="MERGED"
582 EXPECTED_TARGET_08="MERGED"
583 EXPECTED_TARGET_09="MERGED"
584 EXPECTED_TARGET_10="MERGED"
585 EXPECTED_TARGET_11="MERGED"
586 EXPECTED_TARGET_12="MERGED"
587 EXPECTED_TARGET_13="MERGED"
588 EXPECTED_TARGET_14="MERGED"
589 EXPECTED_TARGET_15="MERGED"
590 EXPECTED_TARGET_16="MERGED"
591
592 at_least_one_ko="false"
593
594 for i in $(seq -w 1 99)
595 do
596 if test "$i" -gt $NUMBER_OF_TEST_CASES
597 then
598 break
599 fi
600
601 gen_cmd "$(eval echo \${TEST_CASE_"$i"})"
602
603 if test "$FINAL_CMD" = "$(eval echo \${EXPECTED_CMD_"$i"})" \
604 && test "$FINAL_TARGET" = "$(eval echo \${EXPECTED_TARGET_"$i"})"
605 then
606 printf "Test Case #%02d: OK\n" "$(echo "$i" | sed 's/^0*//')"
607 else
608 printf "Test Case #%02d: KO !!!!\n" "$(echo "$i" | sed 's/^0*//')"
609 echo " FINAL_CMD : $FINAL_CMD"
610 echo " FINAL_CMD (expected) : $(eval echo \${EXPECTED_CMD_"$i"})"
611 echo " FINAL_TARGET : $FINAL_TARGET"
612 echo " FINAL_TARGET (expected): $(eval echo \${EXPECTED_TARGET_"$i"})"
613 at_least_one_ko="true"
614 fi
83bbf9b9 615 done
00417974
FR
616
617 if test "$at_least_one_ko" = "true"
618 then
619 return 255
620 else
621 return 0
622 fi
83bbf9b9 623}