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
+
31
+debug_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
+
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"
50
+
51
+ STRING=$1
52
+ START=$2
53
+ LEN=$3
54
+
55
+ echo "$STRING" | cut -c$(( START + 1 ))-$(( START + $LEN ))
56
+}
57
+
58
+gen_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
231
+ before="split"
232
+ after="wincmd j"
233
+ index=$index_horizontal_split
234
+ terminate="true"
235
+
236
+ elif ! test -z "$index_vertical_split"
237
+ then
238
+ before="vertical split"
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
+
287
+gen_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
+ # |
313
+ # `-> FINAL_CMD == "-c \"echo | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
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
+
362
diff_cmd () {
363
"$merge_tool_path" -R -f -d \
364
-c 'wincmd l' -c 'cd $GIT_PREFIX' "$LOCAL" "$REMOTE"
365
}
366
367
+
368
merge_cmd () {
369
+ layout=$(git config mergetool.vimdiff.layout)
370
+
371
case "$1" in
372
*vimdiff)
9
- if $base_present
373
+ if test -z "$layout"
374
then
11
- "$merge_tool_path" -f -d -c '4wincmd w | wincmd J' \
12
- "$LOCAL" "$BASE" "$REMOTE" "$MERGED"
13
- else
14
- "$merge_tool_path" -f -d -c 'wincmd l' \
15
- "$LOCAL" "$MERGED" "$REMOTE"
375
+ # Default layout when none is specified
376
+ layout="(LOCAL,BASE,REMOTE)/MERGED"
377
fi
378
;;
379
*vimdiff1)
19
- "$merge_tool_path" -f -d \
20
- -c 'echon "Resolve conflicts leftward then save. Use :cq to abort."' \
21
- "$LOCAL" "$REMOTE"
22
- ret="$?"
23
- if test "$ret" -eq 0
24
- then
25
- cp -- "$LOCAL" "$MERGED"
26
- fi
27
- return "$ret"
380
+ layout="@LOCAL,REMOTE"
381
;;
382
*vimdiff2)
30
- "$merge_tool_path" -f -d -c 'wincmd l' \
31
- "$LOCAL" "$MERGED" "$REMOTE"
383
+ layout="LOCAL,MERGED,REMOTE"
384
;;
385
*vimdiff3)
34
- if $base_present
35
- then
36
- "$merge_tool_path" -f -d -c 'hid | hid | hid' \
37
- "$LOCAL" "$REMOTE" "$BASE" "$MERGED"
38
- else
39
- "$merge_tool_path" -f -d -c 'hid | hid' \
40
- "$LOCAL" "$REMOTE" "$MERGED"
41
- fi
386
+ layout="MERGED"
387
;;
388
esac
389
+
390
+ gen_cmd "$layout"
391
+
392
+ debug_print ""
393
+ debug_print "FINAL CMD : $FINAL_CMD"
394
+ debug_print "FINAL TAR : $FINAL_TARGET"
395
+
396
+ if $base_present
397
+ then
398
+ eval "$merge_tool_path" \
399
+ -f "$FINAL_CMD" "$LOCAL" "$BASE" "$REMOTE" "$MERGED"
400
+ else
401
+ # If there is no BASE (example: a merge conflict in a new file
402
+ # with the same name created in both braches which didn't exist
403
+ # before), close all BASE windows using vim's "quit" command
404
+
405
+ FINAL_CMD=$(echo "$FINAL_CMD" | \
406
+ sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g')
407
+
408
+ eval "$merge_tool_path" \
409
+ -f "$FINAL_CMD" "$LOCAL" "$REMOTE" "$MERGED"
410
+ fi
411
+
412
+ ret="$?"
413
+
414
+ if test "$ret" -eq 0
415
+ then
416
+ case "$FINAL_TARGET" in
417
+ LOCAL)
418
+ source_path="$LOCAL"
419
+ ;;
420
+ REMOTE)
421
+ source_path="$REMOTE"
422
+ ;;
423
+ MERGED|*)
424
+ # Do nothing
425
+ source_path=
426
+ ;;
427
+ esac
428
+
429
+ if test -n "$source_path"
430
+ then
431
+ cp "$source_path" "$MERGED"
432
+ fi
433
+ fi
434
+
435
+ return "$ret"
436
}
437
46
-translate_merge_tool_path() {
438
+
439
+translate_merge_tool_path () {
440
case "$1" in
441
nvimdiff*)
442
echo nvim
450
esac
451
}
452
453
+
454
exit_code_trustable () {
455
true
456
}
457
458
+
459
list_tool_variants () {
65
- for prefix in '' g n; do
66
- for suffix in '' 1 2 3; do
67
- echo "${prefix}vimdiff${suffix}"
460
+ if test "$TOOL_MODE" = "diff"
461
+ then
462
+ for prefix in '' g n
463
+ do
464
+ echo "${prefix}vimdiff"
465
done
466
+ else
467
+ for prefix in '' g n
468
+ do
469
+ for suffix in '' 1 2 3
470
+ do
471
+ echo "${prefix}vimdiff${suffix}"
472
+ done
473
+ done
474
+ fi
475
+}
476
+
477
+
478
+################################################################################
479
+## Unit tests (called from scripts inside the "t" folder)
480
+################################################################################
481
+
482
+run_unit_tests () {
483
+ # Function to make sure that we don't break anything when modifying this
484
+ # script.
485
+
486
+ NUMBER_OF_TEST_CASES=16
487
+
488
+ TEST_CASE_01="(LOCAL,BASE,REMOTE)/MERGED" # default behaviour
489
+ TEST_CASE_02="@LOCAL,REMOTE" # when using vimdiff1
490
+ TEST_CASE_03="LOCAL,MERGED,REMOTE" # when using vimdiff2
491
+ TEST_CASE_04="MERGED" # when using vimdiff3
492
+ TEST_CASE_05="LOCAL/MERGED/REMOTE"
493
+ TEST_CASE_06="(LOCAL/REMOTE),MERGED"
494
+ TEST_CASE_07="MERGED,(LOCAL/REMOTE)"
495
+ TEST_CASE_08="(LOCAL,REMOTE)/MERGED"
496
+ TEST_CASE_09="MERGED/(LOCAL,REMOTE)"
497
+ TEST_CASE_10="(LOCAL/BASE/REMOTE),MERGED"
498
+ TEST_CASE_11="(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED"
499
+ TEST_CASE_12="((LOCAL,REMOTE)/BASE),MERGED"
500
+ TEST_CASE_13="((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)"
501
+ TEST_CASE_14="BASE,REMOTE+BASE,LOCAL"
502
+ TEST_CASE_15=" (( (LOCAL , BASE , REMOTE) / MERGED)) +(BASE) , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) , MERGED ) "
503
+ TEST_CASE_16="LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED"
504
+
505
+ EXPECTED_CMD_01="-c \"echo | split | vertical split | 1b | wincmd l | vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
506
+ EXPECTED_CMD_02="-c \"echo | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
507
+ EXPECTED_CMD_03="-c \"echo | vertical split | 1b | wincmd l | vertical split | 4b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
508
+ EXPECTED_CMD_04="-c \"echo | 4b | bufdo diffthis\" -c \"tabfirst\""
509
+ EXPECTED_CMD_05="-c \"echo | split | 1b | wincmd j | split | 4b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
510
+ EXPECTED_CMD_06="-c \"echo | vertical split | split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
511
+ EXPECTED_CMD_07="-c \"echo | vertical split | 4b | wincmd l | split | 1b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
512
+ EXPECTED_CMD_08="-c \"echo | split | vertical split | 1b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
513
+ EXPECTED_CMD_09="-c \"echo | split | 4b | wincmd j | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
514
+ EXPECTED_CMD_10="-c \"echo | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
515
+ EXPECTED_CMD_11="-c \"echo | -tabnew | split | vertical split | 1b | wincmd l | vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
516
+ EXPECTED_CMD_12="-c \"echo | vertical split | split | vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
517
+ EXPECTED_CMD_13="-c \"echo | vertical split | split | vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | vertical split | split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
518
+ EXPECTED_CMD_14="-c \"echo | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | 2b | wincmd l | 1b | tabdo windo diffthis\" -c \"tabfirst\""
519
+ EXPECTED_CMD_15="-c \"echo | -tabnew | split | vertical split | 1b | wincmd l | vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
520
+ EXPECTED_CMD_16="-c \"echo | -tabnew | split | vertical split | 1b | wincmd l | vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
521
+
522
+ EXPECTED_TARGET_01="MERGED"
523
+ EXPECTED_TARGET_02="LOCAL"
524
+ EXPECTED_TARGET_03="MERGED"
525
+ EXPECTED_TARGET_04="MERGED"
526
+ EXPECTED_TARGET_05="MERGED"
527
+ EXPECTED_TARGET_06="MERGED"
528
+ EXPECTED_TARGET_07="MERGED"
529
+ EXPECTED_TARGET_08="MERGED"
530
+ EXPECTED_TARGET_09="MERGED"
531
+ EXPECTED_TARGET_10="MERGED"
532
+ EXPECTED_TARGET_11="MERGED"
533
+ EXPECTED_TARGET_12="MERGED"
534
+ EXPECTED_TARGET_13="MERGED"
535
+ EXPECTED_TARGET_14="MERGED"
536
+ EXPECTED_TARGET_15="MERGED"
537
+ EXPECTED_TARGET_16="MERGED"
538
+
539
+ at_least_one_ko="false"
540
+
541
+ for i in $(seq -w 1 99)
542
+ do
543
+ if test "$i" -gt $NUMBER_OF_TEST_CASES
544
+ then
545
+ break
546
+ fi
547
+
548
+ gen_cmd "$(eval echo \${TEST_CASE_"$i"})"
549
+
550
+ if test "$FINAL_CMD" = "$(eval echo \${EXPECTED_CMD_"$i"})" \
551
+ && test "$FINAL_TARGET" = "$(eval echo \${EXPECTED_TARGET_"$i"})"
552
+ then
553
+ printf "Test Case #%02d: OK\n" "$(echo "$i" | sed 's/^0*//')"
554
+ else
555
+ printf "Test Case #%02d: KO !!!!\n" "$(echo "$i" | sed 's/^0*//')"
556
+ echo " FINAL_CMD : $FINAL_CMD"
557
+ echo " FINAL_CMD (expected) : $(eval echo \${EXPECTED_CMD_"$i"})"
558
+ echo " FINAL_TARGET : $FINAL_TARGET"
559
+ echo " FINAL_TARGET (expected): $(eval echo \${EXPECTED_TARGET_"$i"})"
560
+ at_least_one_ko="true"
561
+ fi
562
done
563
+
564
+ if test "$at_least_one_ko" = "true"
565
+ then
566
+ return 255
567
+ else
568
+ return 0
569
+ fi
570
}