git
645 строк · 17.2 Кб
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_DEBUG
33# is set.
34
35if test -n "$GIT_MERGETOOL_VIMDIFF_DEBUG"
36then
37>&2 echo "$@"
38fi
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
51STRING=$1
52START=$2
53LEN=$3
54
55echo "$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
62LAYOUT=$1
63CMD=$2 # This is a second (hidden) argument used for recursion
64
65debug_print
66debug_print "LAYOUT : $LAYOUT"
67debug_print "CMD : $CMD"
68
69start=0
70end=${#LAYOUT}
71
72nested=0
73nested_min=100
74
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
84
85oldIFS=$IFS
86IFS=#
87for c in $(echo "$LAYOUT" | sed 's:.:&#:g')
88do
89if test -z "$c" || test "$c" = " "
90then
91continue
92fi
93
94if test "$c" = "("
95then
96nested=$(( nested + 1 ))
97continue
98fi
99
100if test "$c" = ")"
101then
102nested=$(( nested - 1 ))
103continue
104fi
105
106if test "$nested" -lt "$nested_min"
107then
108nested_min=$nested
109fi
110done
111IFS=$oldIFS
112
113debug_print "NESTED MIN: $nested_min"
114
115while test "$nested_min" -gt "0"
116do
117start=$(( start + 1 ))
118end=$(( end - 1 ))
119
120start_minus_one=$(( start - 1 ))
121
122while ! test "$(substring "$LAYOUT" "$start_minus_one" 1)" = "("
123do
124start=$(( start + 1 ))
125start_minus_one=$(( start_minus_one + 1 ))
126done
127
128while ! test "$(substring "$LAYOUT" "$end" 1)" = ")"
129do
130end=$(( end - 1 ))
131done
132
133nested_min=$(( nested_min - 1 ))
134done
135
136debug_print "CLEAN : $(substring "$LAYOUT" "$start" "$(( end - start ))")"
137
138
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.
144
145index_horizontal_split=""
146index_vertical_split=""
147
148nested=0
149i=$(( start - 1 ))
150
151oldIFS=$IFS
152IFS=#
153for c in $(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:.:&#:g');
154do
155i=$(( i + 1 ))
156
157if test "$c" = " "
158then
159continue
160fi
161
162if test "$c" = "("
163then
164nested=$(( nested + 1 ))
165continue
166fi
167
168if test "$c" = ")"
169then
170nested=$(( nested - 1 ))
171continue
172fi
173
174if test "$nested" = 0
175then
176current=$c
177
178if test "$current" = "/"
179then
180if test -z "$index_horizontal_split"
181then
182index_horizontal_split=$i
183fi
184
185elif test "$current" = ","
186then
187if test -z "$index_vertical_split"
188then
189index_vertical_split=$i
190fi
191fi
192fi
193done
194IFS=$oldIFS
195
196
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.
205
206terminate="false"
207
208if ! test -z "$index_horizontal_split"
209then
210before="leftabove split"
211after="wincmd j"
212index=$index_horizontal_split
213terminate="true"
214
215elif ! test -z "$index_vertical_split"
216then
217before="leftabove vertical split"
218after="wincmd l"
219index=$index_vertical_split
220terminate="true"
221fi
222
223if test "$terminate" = "true"
224then
225CMD="$CMD | $before"
226CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$start" "$(( index - start ))")" "$CMD")
227CMD="$CMD | $after"
228CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$(( index + 1 ))" "$(( ${#LAYOUT} - index ))")" "$CMD")
229echo "$CMD"
230return
231fi
232
233
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
238
239target=$(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:[ @();|-]::g')
240
241if test "$target" = "LOCAL"
242then
243CMD="$CMD | 1b"
244
245elif test "$target" = "BASE"
246then
247CMD="$CMD | 2b"
248
249elif test "$target" = "REMOTE"
250then
251CMD="$CMD | 3b"
252
253elif test "$target" = "MERGED"
254then
255CMD="$CMD | 4b"
256
257else
258CMD="$CMD | ERROR: >$target<"
259fi
260
261echo "$CMD"
262return
263}
264
265
266gen_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"
294
295LAYOUT=$1
296
297
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.
301
302if echo "$LAYOUT" | grep @LOCAL >/dev/null
303then
304FINAL_TARGET="LOCAL"
305elif echo "$LAYOUT" | grep @BASE >/dev/null
306then
307FINAL_TARGET="BASE"
308else
309FINAL_TARGET="MERGED"
310fi
311
312
313# Obtain the first part of vim "-c" option to obtain the desired layout
314
315CMD=
316oldIFS=$IFS
317IFS=+
318for tab in $LAYOUT
319do
320if test -z "$CMD"
321then
322CMD="echo" # vim "nop" operator
323else
324CMD="$CMD | tabnew"
325fi
326
327# If this is a single window diff with all the buffers
328if ! echo "$tab" | grep -E ",|/" >/dev/null
329then
330CMD="$CMD | silent execute 'bufdo diffthis'"
331fi
332
333CMD=$(gen_cmd_aux "$tab" "$CMD")
334done
335IFS=$oldIFS
336
337CMD="$CMD | execute 'tabdo windo diffthis'"
338
339FINAL_CMD="-c \"set hidden diffopt-=hiddenoff | $CMD | tabfirst\""
340}
341
342
343################################################################################
344## API functions (called from "git-mergetool--lib.sh")
345################################################################################
346
347diff_cmd () {
348"$merge_tool_path" -R -f -d \
349-c 'wincmd l' -c 'cd $GIT_PREFIX' "$LOCAL" "$REMOTE"
350}
351
352
353diff_cmd_help () {
354TOOL=$1
355
356case "$TOOL" in
357nvimdiff*)
358printf "Use Neovim"
359;;
360gvimdiff*)
361printf "Use gVim (requires a graphical session)"
362;;
363vimdiff*)
364printf "Use Vim"
365;;
366esac
367
368return 0
369}
370
371
372merge_cmd () {
373TOOL=$1
374
375layout=$(git config "mergetool.$TOOL.layout")
376
377# backward compatibility:
378if test -z "$layout"
379then
380layout=$(git config mergetool.vimdiff.layout)
381fi
382
383case "$TOOL" in
384*vimdiff)
385if test -z "$layout"
386then
387# Default layout when none is specified
388layout="(LOCAL,BASE,REMOTE)/MERGED"
389fi
390;;
391*vimdiff1)
392layout="@LOCAL,REMOTE"
393;;
394*vimdiff2)
395layout="LOCAL,MERGED,REMOTE"
396;;
397*vimdiff3)
398layout="MERGED"
399;;
400esac
401
402gen_cmd "$layout"
403
404debug_print ""
405debug_print "FINAL CMD : $FINAL_CMD"
406debug_print "FINAL TAR : $FINAL_TARGET"
407
408if $base_present
409then
410eval '"$merge_tool_path"' \
411-f "$FINAL_CMD" '"$LOCAL"' '"$BASE"' '"$REMOTE"' '"$MERGED"'
412else
413# If there is no BASE (example: a merge conflict in a new file
414# with the same name created in both braches which didn't exist
415# before), close all BASE windows using vim's "quit" command
416
417FINAL_CMD=$(echo "$FINAL_CMD" | \
418sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g')
419
420eval '"$merge_tool_path"' \
421-f "$FINAL_CMD" '"$LOCAL"' '"$REMOTE"' '"$MERGED"'
422fi
423
424ret="$?"
425
426if test "$ret" -eq 0
427then
428case "$FINAL_TARGET" in
429LOCAL)
430source_path="$LOCAL"
431;;
432REMOTE)
433source_path="$REMOTE"
434;;
435MERGED|*)
436# Do nothing
437source_path=
438;;
439esac
440
441if test -n "$source_path"
442then
443cp "$source_path" "$MERGED"
444fi
445fi
446
447return "$ret"
448}
449
450
451merge_cmd_help () {
452TOOL=$1
453
454case "$TOOL" in
455nvimdiff*)
456printf "Use Neovim "
457;;
458gvimdiff*)
459printf "Use gVim (requires a graphical session) "
460;;
461vimdiff*)
462printf "Use Vim "
463;;
464esac
465
466case "$TOOL" in
467*1)
468echo "with a 2 panes layout (LOCAL and REMOTE)"
469;;
470*2)
471echo "with a 3 panes layout (LOCAL, MERGED and REMOTE)"
472;;
473*3)
474echo "where only the MERGED file is shown"
475;;
476*)
477echo "with a custom layout (see \`git help mergetool\`'s \`BACKEND SPECIFIC HINTS\` section)"
478;;
479esac
480
481return 0
482}
483
484
485translate_merge_tool_path () {
486case "$1" in
487nvimdiff*)
488echo nvim
489;;
490gvimdiff*)
491echo gvim
492;;
493vimdiff*)
494echo vim
495;;
496esac
497}
498
499
500exit_code_trustable () {
501true
502}
503
504
505list_tool_variants () {
506if test "$TOOL_MODE" = "diff"
507then
508for prefix in '' g n
509do
510echo "${prefix}vimdiff"
511done
512else
513for prefix in '' g n
514do
515for suffix in '' 1 2 3
516do
517echo "${prefix}vimdiff${suffix}"
518done
519done
520fi
521}
522
523
524################################################################################
525## Unit tests (called from scripts inside the "t" folder)
526################################################################################
527
528run_unit_tests () {
529# Function to make sure that we don't break anything when modifying this
530# script.
531
532NUMBER_OF_TEST_CASES=16
533
534TEST_CASE_01="(LOCAL,BASE,REMOTE)/MERGED" # default behaviour
535TEST_CASE_02="@LOCAL,REMOTE" # when using vimdiff1
536TEST_CASE_03="LOCAL,MERGED,REMOTE" # when using vimdiff2
537TEST_CASE_04="MERGED" # when using vimdiff3
538TEST_CASE_05="LOCAL/MERGED/REMOTE"
539TEST_CASE_06="(LOCAL/REMOTE),MERGED"
540TEST_CASE_07="MERGED,(LOCAL/REMOTE)"
541TEST_CASE_08="(LOCAL,REMOTE)/MERGED"
542TEST_CASE_09="MERGED/(LOCAL,REMOTE)"
543TEST_CASE_10="(LOCAL/BASE/REMOTE),MERGED"
544TEST_CASE_11="(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED"
545TEST_CASE_12="((LOCAL,REMOTE)/BASE),MERGED"
546TEST_CASE_13="((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)"
547TEST_CASE_14="BASE,REMOTE+BASE,LOCAL"
548TEST_CASE_15=" (( (LOCAL , BASE , REMOTE) / MERGED)) +(BASE) , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) , MERGED ) "
549TEST_CASE_16="LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED"
550
551EXPECTED_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\""
552EXPECTED_CMD_02="-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 1b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\""
553EXPECTED_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\""
554EXPECTED_CMD_04="-c \"set hidden diffopt-=hiddenoff | echo | silent execute 'bufdo diffthis' | 4b | execute 'tabdo windo diffthis' | tabfirst\""
555EXPECTED_CMD_05="-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | 1b | wincmd j | leftabove split | 4b | wincmd j | 3b | execute 'tabdo windo diffthis' | tabfirst\""
556EXPECTED_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\""
557EXPECTED_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\""
558EXPECTED_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\""
559EXPECTED_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\""
560EXPECTED_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\""
561EXPECTED_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\""
562EXPECTED_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\""
563EXPECTED_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\""
564EXPECTED_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\""
565EXPECTED_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\""
566EXPECTED_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\""
567
568EXPECTED_TARGET_01="MERGED"
569EXPECTED_TARGET_02="LOCAL"
570EXPECTED_TARGET_03="MERGED"
571EXPECTED_TARGET_04="MERGED"
572EXPECTED_TARGET_05="MERGED"
573EXPECTED_TARGET_06="MERGED"
574EXPECTED_TARGET_07="MERGED"
575EXPECTED_TARGET_08="MERGED"
576EXPECTED_TARGET_09="MERGED"
577EXPECTED_TARGET_10="MERGED"
578EXPECTED_TARGET_11="MERGED"
579EXPECTED_TARGET_12="MERGED"
580EXPECTED_TARGET_13="MERGED"
581EXPECTED_TARGET_14="MERGED"
582EXPECTED_TARGET_15="MERGED"
583EXPECTED_TARGET_16="MERGED"
584
585at_least_one_ko="false"
586
587for i in $(seq -w 1 99)
588do
589if test "$i" -gt $NUMBER_OF_TEST_CASES
590then
591break
592fi
593
594gen_cmd "$(eval echo \${TEST_CASE_"$i"})"
595
596if test "$FINAL_CMD" = "$(eval echo \${EXPECTED_CMD_"$i"})" \
597&& test "$FINAL_TARGET" = "$(eval echo \${EXPECTED_TARGET_"$i"})"
598then
599printf "Test Case #%02d: OK\n" "$(echo "$i" | sed 's/^0*//')"
600else
601printf "Test Case #%02d: KO !!!!\n" "$(echo "$i" | sed 's/^0*//')"
602echo " FINAL_CMD : $FINAL_CMD"
603echo " FINAL_CMD (expected) : $(eval echo \${EXPECTED_CMD_"$i"})"
604echo " FINAL_TARGET : $FINAL_TARGET"
605echo " FINAL_TARGET (expected): $(eval echo \${EXPECTED_TARGET_"$i"})"
606at_least_one_ko="true"
607fi
608done
609
610# verify that `merge_cmd` handles paths with spaces
611record_parameters () {
612>actual
613for arg
614do
615echo "$arg" >>actual
616done
617}
618
619base_present=false
620LOCAL='lo cal'
621BASE='ba se'
622REMOTE="' '"
623MERGED='mer ged'
624merge_tool_path=record_parameters
625
626merge_cmd vimdiff || at_least_one_ko=true
627
628cat >expect <<-\EOF
629-f
630-c
631set 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
632lo cal
633' '
634mer ged
635EOF
636
637diff -u expect actual || at_least_one_ko=true
638
639if test "$at_least_one_ko" = "true"
640then
641return 255
642else
643return 0
644fi
645}
646