diff --git a/Documentation/Makefile b/Documentation/Makefile index 44c080e3e5..adb2f1b50a 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -302,12 +302,12 @@ $(mergetools_txt): mergetools-list.made mergetools-list.made: ../git-mergetool--lib.sh $(wildcard ../mergetools/*) $(QUIET_GEN) \ - $(SHELL_PATH) -c 'MERGE_TOOLS_DIR=../mergetools && \ + $(SHELL_PATH) -c 'MERGE_TOOLS_DIR=../mergetools && TOOL_MODE=diff && \ . ../git-mergetool--lib.sh && \ - show_tool_names can_diff "* " || :' >mergetools-diff.txt && \ - $(SHELL_PATH) -c 'MERGE_TOOLS_DIR=../mergetools && \ + show_tool_names can_diff' | sed -e "s/\([a-z0-9]*\)/\`\1\`;;/" >mergetools-diff.txt && \ + $(SHELL_PATH) -c 'MERGE_TOOLS_DIR=../mergetools && TOOL_MODE=merge && \ . ../git-mergetool--lib.sh && \ - show_tool_names can_merge "* " || :' >mergetools-merge.txt && \ + show_tool_names can_merge' | sed -e "s/\([a-z0-9]*\)/\`\1\`;;/" >mergetools-merge.txt && \ date >$@ TRACK_ASCIIDOCFLAGS = $(subst ','\'',$(ASCIIDOC_COMMON):$(ASCIIDOC_HTML):$(ASCIIDOC_DOCBOOK)) diff --git a/Documentation/config/mergetool.txt b/Documentation/config/mergetool.txt index cafbbef46a..90b3809700 100644 --- a/Documentation/config/mergetool.txt +++ b/Documentation/config/mergetool.txt @@ -45,6 +45,15 @@ mergetool.meld.useAutoMerge:: value of `false` avoids using `--auto-merge` altogether, and is the default value. +mergetool.vimdiff.layout:: + The vimdiff backend uses this variable to control how its split + windows look like. Applies even if you are using Neovim (`nvim`) or + gVim (`gvim`) as the merge tool. See BACKEND SPECIFIC HINTS section +ifndef::git-mergetool[] + in linkgit:git-mergetool[1]. +endif::[] + for details. + mergetool.hideResolved:: During a merge Git will automatically resolve as many conflicts as possible and write the 'MERGED' file containing conflict markers around diff --git a/Documentation/git-mergetool.txt b/Documentation/git-mergetool.txt index e587c7763a..f784027bc1 100644 --- a/Documentation/git-mergetool.txt +++ b/Documentation/git-mergetool.txt @@ -101,6 +101,7 @@ success of the resolution after the custom tool has exited. CONFIGURATION ------------- +:git-mergetool: 1 include::config/mergetool.txt[] TEMPORARY FILES @@ -113,6 +114,13 @@ Setting the `mergetool.keepBackup` configuration variable to `false` causes `git mergetool` to automatically remove the backup as files are successfully merged. +BACKEND SPECIFIC HINTS +---------------------- + +vimdiff +~~~~~~~ +include::mergetools/vimdiff.txt[] + GIT --- Part of the linkgit:git[1] suite diff --git a/Documentation/mergetools/vimdiff.txt b/Documentation/mergetools/vimdiff.txt new file mode 100644 index 0000000000..2d631e9b1f --- /dev/null +++ b/Documentation/mergetools/vimdiff.txt @@ -0,0 +1,194 @@ +Description +^^^^^^^^^^^ + +When specifying `--tool=vimdiff` in `git mergetool` Git will open Vim with a 4 +windows layout distributed in the following way: +.... +------------------------------------------ +| | | | +| LOCAL | BASE | REMOTE | +| | | | +------------------------------------------ +| | +| MERGED | +| | +------------------------------------------ +.... +`LOCAL`, `BASE` and `REMOTE` are read-only buffers showing the contents of the +conflicting file in specific commits ("commit you are merging into", "common +ancestor commit" and "commit you are merging from" respectively) + +`MERGED` is a writable buffer where you have to resolve the conflicts (using the +other read-only buffers as a reference). Once you are done, save and exit Vim as +usual (`:wq`) or, if you want to abort, exit using `:cq`. + +Layout configuration +^^^^^^^^^^^^^^^^^^^^ + +You can change the windows layout used by Vim by setting configuration variable +`mergetool.vimdiff.layout` which accepts a string where the following separators +have special meaning: + + - `+` is used to "open a new tab" + - `,` is used to "open a new vertical split" + - `/` is used to "open a new horizontal split" + - `@` is used to indicate which is the file containing the final version after + solving the conflicts. If not present, `MERGED` will be used by default. + +The precedence of the operators is this one (you can use parentheses to change +it): + + `@` > `+` > `/` > `,` + +Let's see some examples to understand how it works: + +* `layout = "(LOCAL,BASE,REMOTE)/MERGED"` ++ +-- +This is exactly the same as the default layout we have already seen. + +Note that `/` has precedence over `,` and thus the parenthesis are not +needed in this case. The next layout definition is equivalent: + + layout = "LOCAL,BASE,REMOTE / MERGED" +-- +* `layout = "LOCAL,MERGED,REMOTE"` ++ +-- +If, for some reason, we are not interested in the `BASE` buffer. +.... +------------------------------------------ +| | | | +| | | | +| LOCAL | MERGED | REMOTE | +| | | | +| | | | +------------------------------------------ +.... +-- +* `layout = "MERGED"` ++ +-- +Only the `MERGED` buffer will be shown. Note, however, that all the other +ones are still loaded in vim, and you can access them with the "buffers" +command. +.... +------------------------------------------ +| | +| | +| MERGED | +| | +| | +------------------------------------------ +.... +-- +* `layout = "@LOCAL,REMOTE"` ++ +-- +When `MERGED` is not present in the layout, you must "mark" one of the +buffers with an asterisk. That will become the buffer you need to edit and +save after resolving the conflicts. +.... +------------------------------------------ +| | | +| | | +| | | +| LOCAL | REMOTE | +| | | +| | | +| | | +------------------------------------------ +.... +-- +* `layout = "LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE"` ++ +-- +Three tabs will open: the first one is a copy of the default layout, while +the other two only show the differences between (`BASE` and `LOCAL`) and +(`BASE` and `REMOTE`) respectively. +.... +------------------------------------------ +| | TAB #2 | TAB #3 | | +------------------------------------------ +| | | | +| LOCAL | BASE | REMOTE | +| | | | +------------------------------------------ +| | +| MERGED | +| | +------------------------------------------ +.... +.... +------------------------------------------ +| TAB #1 | | TAB #3 | | +------------------------------------------ +| | | +| | | +| | | +| BASE | LOCAL | +| | | +| | | +| | | +------------------------------------------ +.... +.... +------------------------------------------ +| TAB #1 | TAB #2 | | | +------------------------------------------ +| | | +| | | +| | | +| BASE | REMOTE | +| | | +| | | +| | | +------------------------------------------ +.... +-- +* `layout = "LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL/BASE/REMOTE),MERGED"` ++ +-- +Same as the previous example, but adds a fourth tab with the same +information as the first tab, with a different layout. +.... +--------------------------------------------- +| TAB #1 | TAB #2 | TAB #3 | | +--------------------------------------------- +| LOCAL | | +|---------------------| | +| BASE | MERGED | +|---------------------| | +| REMOTE | | +--------------------------------------------- +.... +Note how in the third tab definition we need to use parenthesis to make `,` +have precedence over `/`. +-- + +Variants +^^^^^^^^ + +Instead of `--tool=vimdiff`, you can also use one of these other variants: + + * `--tool=gvimdiff`, to open gVim instead of Vim. + + * `--tool=nvimdiff`, to open Neovim instead of Vim. + +When using these variants, in order to specify a custom layout you will have to +set configuration variables `mergetool.gvimdiff.layout` and +`mergetool.nvimdiff.layout` instead of `mergetool.vimdiff.layout` + +In addition, for backwards compatibility with previous Git versions, you can +also append `1`, `2` or `3` to either `vimdiff` or any of the variants (ex: +`vimdiff3`, `nvimdiff1`, etc...) to use a predefined layout. +In other words, using `--tool=[g,n,]vimdiffx` is the same as using +`--tool=[g,n,]vimdiff` and setting configuration variable +`mergetool.[g,n,]vimdiff.layout` to... + + * `x=1`: `"@LOCAL, REMOTE"` + * `x=2`: `"LOCAL, MERGED, REMOTE"` + * `x=3`: `"MERGED"` + +Example: using `--tool=gvimdiff2` will open `gvim` with three columns (LOCAL, +MERGED and REMOTE). diff --git a/git-mergetool--lib.sh b/git-mergetool--lib.sh index 542a6a75eb..9f99201bcc 100644 --- a/git-mergetool--lib.sh +++ b/git-mergetool--lib.sh @@ -63,7 +63,7 @@ $(list_tool_variants)" preamble= fi shown_any=yes - printf "%s%s\n" "$per_line_prefix" "$toolname" + printf "%s%-15s %s\n" "$per_line_prefix" "$toolname" $(diff_mode && diff_cmd_help "$toolname" || merge_cmd_help "$toolname") fi done @@ -162,10 +162,18 @@ setup_tool () { return 1 } + diff_cmd_help () { + return 0 + } + merge_cmd () { return 1 } + merge_cmd_help () { + return 0 + } + hide_resolved_enabled () { return 0 } diff --git a/mergetools/araxis b/mergetools/araxis index e2407b65b7..eb32a7da95 100644 --- a/mergetools/araxis +++ b/mergetools/araxis @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" -wait -2 "$LOCAL" "$REMOTE" >/dev/null 2>&1 } +diff_cmd_help () { + echo "Use Araxis Merge (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -13,6 +17,10 @@ merge_cmd () { fi } +merge_cmd_help () { + echo "Use Araxis Merge (requires a graphical session)" +} + translate_merge_tool_path() { echo compare } diff --git a/mergetools/bc b/mergetools/bc index 26c19d46a5..2922667ddd 100644 --- a/mergetools/bc +++ b/mergetools/bc @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" } +diff_cmd_help () { + echo "Use Beyond Compare (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -13,6 +17,10 @@ merge_cmd () { fi } +merge_cmd_help () { + echo "Use Beyond Compare (requires a graphical session)" +} + translate_merge_tool_path() { if type bcomp >/dev/null 2>/dev/null then diff --git a/mergetools/codecompare b/mergetools/codecompare index 9f60e8da65..610963d377 100644 --- a/mergetools/codecompare +++ b/mergetools/codecompare @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" } +diff_cmd_help () { + echo "Use Code Compare (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -13,6 +17,10 @@ merge_cmd () { fi } +merge_cmd_help () { + echo "Use Code Compare (requires a graphical session)" +} + translate_merge_tool_path() { if merge_mode then diff --git a/mergetools/deltawalker b/mergetools/deltawalker index ee6f374bce..efae4c285c 100644 --- a/mergetools/deltawalker +++ b/mergetools/deltawalker @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" >/dev/null 2>&1 } +diff_cmd_help () { + echo "Use DeltaWalker (requires a graphical session)" +} + merge_cmd () { # Adding $(pwd)/ in front of $MERGED should not be necessary. # However without it, DeltaWalker (at least v1.9.8 on Windows) @@ -16,6 +20,10 @@ merge_cmd () { fi >/dev/null 2>&1 } +merge_cmd_help () { + echo "Use DeltaWalker (requires a graphical session)" +} + translate_merge_tool_path () { echo DeltaWalker } diff --git a/mergetools/diffmerge b/mergetools/diffmerge index 9b6355b98a..9b5b62d1ca 100644 --- a/mergetools/diffmerge +++ b/mergetools/diffmerge @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" >/dev/null 2>&1 } +diff_cmd_help () { + echo "Use DiffMerge (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -13,6 +17,10 @@ merge_cmd () { fi } +merge_cmd_help () { + echo "Use DiffMerge (requires a graphical session)" +} + exit_code_trustable () { true } diff --git a/mergetools/diffuse b/mergetools/diffuse index 5a3ae8b569..ebfaba5172 100644 --- a/mergetools/diffuse +++ b/mergetools/diffuse @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" | cat } +diff_cmd_help () { + echo "Use Diffuse (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -13,3 +17,7 @@ merge_cmd () { "$LOCAL" "$MERGED" "$REMOTE" | cat fi } + +merge_cmd_help () { + echo "Use Diffuse (requires a graphical session)" +} diff --git a/mergetools/ecmerge b/mergetools/ecmerge index 6c5101c4f7..0d4d609874 100644 --- a/mergetools/ecmerge +++ b/mergetools/ecmerge @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" --default --mode=diff2 "$LOCAL" "$REMOTE" } +diff_cmd_help () { + echo "Use ECMerge (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -12,3 +16,7 @@ merge_cmd () { --default --mode=merge2 --to="$MERGED" fi } + +merge_cmd_help () { + echo "Use ECMerge (requires a graphical session)" +} diff --git a/mergetools/emerge b/mergetools/emerge index d1ce513ff5..fc6892cc95 100644 --- a/mergetools/emerge +++ b/mergetools/emerge @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" -f emerge-files-command "$LOCAL" "$REMOTE" } +diff_cmd_help () { + echo "Use Emacs' Emerge" +} + merge_cmd () { if $base_present then @@ -17,6 +21,10 @@ merge_cmd () { fi } +merge_cmd_help () { + echo "Use Emacs' Emerge" +} + translate_merge_tool_path() { echo emacs } diff --git a/mergetools/examdiff b/mergetools/examdiff index e72b06fc4d..6f53ca9161 100644 --- a/mergetools/examdiff +++ b/mergetools/examdiff @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" -nh } +diff_cmd_help () { + echo "Use ExamDiff Pro (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -11,6 +15,10 @@ merge_cmd () { fi } +merge_cmd_help () { + echo "Use ExamDiff Pro (requires a graphical session)" +} + translate_merge_tool_path() { mergetool_find_win32_cmd "ExamDiff.com" "ExamDiff Pro" } diff --git a/mergetools/guiffy b/mergetools/guiffy index 8b23a13c41..3ed07efd16 100644 --- a/mergetools/guiffy +++ b/mergetools/guiffy @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" } +diff_cmd_help () { + echo "Use Guiffy's Diff Tool (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -13,6 +17,10 @@ merge_cmd () { fi } +merge_cmd_help () { + echo "Use Guiffy's Diff Tool (requires a graphical session)" +} + exit_code_trustable () { true } diff --git a/mergetools/kdiff3 b/mergetools/kdiff3 index 520cb914a1..ee8b3a0570 100644 --- a/mergetools/kdiff3 +++ b/mergetools/kdiff3 @@ -4,6 +4,10 @@ diff_cmd () { "$LOCAL" "$REMOTE" >/dev/null 2>&1 } +diff_cmd_help () { + echo "Use KDiff3 (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -22,6 +26,10 @@ merge_cmd () { fi } +merge_cmd_help () { + echo "Use KDiff3 (requires a graphical session)" +} + exit_code_trustable () { true } diff --git a/mergetools/kompare b/mergetools/kompare index e8c0bfa678..4ce23dbe8b 100644 --- a/mergetools/kompare +++ b/mergetools/kompare @@ -2,10 +2,18 @@ can_merge () { return 1 } +diff_cmd_help () { + echo "Use Kompare (requires a graphical session)" +} + diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" } +merge_cmd_help () { + echo "Use Kompare (requires a graphical session)" +} + exit_code_trustable () { true } diff --git a/mergetools/meld b/mergetools/meld index aab4ebb935..8ec0867e03 100644 --- a/mergetools/meld +++ b/mergetools/meld @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" } +diff_cmd_help () { + echo "Use Meld (requires a graphical session)" +} + merge_cmd () { check_meld_for_features @@ -20,6 +24,10 @@ merge_cmd () { fi } +merge_cmd_help () { + echo "Use Meld (requires a graphical session) with optional \`auto merge\` (see \`git help mergetool\`'s \`CONFIGURATION\` section)" +} + # Get meld help message init_meld_help_msg () { if test -z "$meld_help_msg" diff --git a/mergetools/opendiff b/mergetools/opendiff index b608dd6de3..44adf8f951 100644 --- a/mergetools/opendiff +++ b/mergetools/opendiff @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" | cat } +diff_cmd_help () { + echo "Use FileMerge (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -12,3 +16,7 @@ merge_cmd () { -merge "$MERGED" | cat fi } + +merge_cmd_help () { + echo "Use FileMerge (requires a graphical session)" +} diff --git a/mergetools/p4merge b/mergetools/p4merge index 7a5b291dd2..f3cb197e58 100644 --- a/mergetools/p4merge +++ b/mergetools/p4merge @@ -19,6 +19,10 @@ diff_cmd () { fi } +diff_cmd_help () { + echo "Use HelixCore P4Merge (requires a graphical session)" +} + merge_cmd () { if ! $base_present then @@ -34,3 +38,7 @@ create_empty_file () { printf "%s" "$empty_file" } + +merge_cmd_help () { + echo "Use HelixCore P4Merge (requires a graphical session)" +} diff --git a/mergetools/smerge b/mergetools/smerge index 9c2e6f6fd7..5410835a6b 100644 --- a/mergetools/smerge +++ b/mergetools/smerge @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" mergetool "$LOCAL" "$REMOTE" -o "$MERGED" } +diff_cmd_help () { + echo "Use Sublime Merge (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -10,3 +14,7 @@ merge_cmd () { "$merge_tool_path" mergetool "$LOCAL" "$REMOTE" -o "$MERGED" fi } + +merge_cmd_help () { + echo "Use Sublime Merge (requires a graphical session)" +} diff --git a/mergetools/tkdiff b/mergetools/tkdiff index eee5cb57e3..66906a720d 100644 --- a/mergetools/tkdiff +++ b/mergetools/tkdiff @@ -2,6 +2,10 @@ diff_cmd () { "$merge_tool_path" "$LOCAL" "$REMOTE" } +diff_cmd_help () { + echo "Use TkDiff (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -14,3 +18,7 @@ merge_cmd () { exit_code_trustable () { true } + +merge_cmd_help () { + echo "Use TkDiff (requires a graphical session)" +} diff --git a/mergetools/tortoisemerge b/mergetools/tortoisemerge index d7ab666a59..507edcd444 100644 --- a/mergetools/tortoisemerge +++ b/mergetools/tortoisemerge @@ -2,6 +2,10 @@ can_diff () { return 1 } +diff_cmd_help () { + echo "Use TortoiseMerge (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -30,3 +34,7 @@ translate_merge_tool_path() { echo tortoisemerge fi } + +merge_cmd_help () { + echo "Use TortoiseMerge (requires a graphical session)" +} diff --git a/mergetools/vimdiff b/mergetools/vimdiff index 96f6209a04..461a89b6f9 100644 --- a/mergetools/vimdiff +++ b/mergetools/vimdiff @@ -1,49 +1,495 @@ +# This script can be run in two different contexts: +# +# - From git, when the user invokes the "vimdiff" merge tool. In this context +# this script expects the following environment variables (among others) to +# be defined (which is something "git" takes care of): +# +# - $BASE +# - $LOCAL +# - $REMOTE +# - $MERGED +# +# In this mode, all this script does is to run the next command: +# +# vim -f -c ... $LOCAL $BASE $REMOTE $MERGED +# +# ...where the "..." string depends on the value of the +# "mergetool.vimdiff.layout" configuration variable and is used to open vim +# with a certain layout of buffers, windows and tabs. +# +# - From a script inside the unit tests framework folder ("t" folder) by +# sourcing this script and then manually calling "run_unit_tests", which +# will run a battery of unit tests to make sure nothing breaks. +# In this context this script does not expect any particular environment +# variable to be set. + + +################################################################################ +## Internal functions (not meant to be used outside this script) +################################################################################ + +debug_print () { + # Send message to stderr if global variable GIT_MERGETOOL_VIMDIFF is set + # to "true" + + if test -n "$GIT_MERGETOOL_VIMDIFF_DEBUG" + then + >&2 echo "$@" + fi +} + +substring () { + # Return a substring of $1 containing $3 characters starting at + # zero-based offset $2. + # + # Examples: + # + # substring "Hello world" 0 4 --> "Hell" + # substring "Hello world" 3 4 --> "lo w" + # substring "Hello world" 3 10 --> "lo world" + + STRING=$1 + START=$2 + LEN=$3 + + echo "$STRING" | cut -c$(( START + 1 ))-$(( START + $LEN )) +} + +gen_cmd_aux () { + # Auxiliary function used from "gen_cmd()". + # Read that other function documentation for more details. + + LAYOUT=$1 + CMD=$2 # This is a second (hidden) argument used for recursion + + debug_print + debug_print "LAYOUT : $LAYOUT" + debug_print "CMD : $CMD" + + if test -z "$CMD" + then + CMD="echo" # vim "nop" operator + fi + + start=0 + end=${#LAYOUT} + + nested=0 + nested_min=100 + + + # Step 1: + # + # Increase/decrease "start"/"end" indices respectively to get rid of + # outer parenthesis. + # + # Example: + # + # - BEFORE: (( LOCAL , BASE ) / MERGED ) + # - AFTER : ( LOCAL , BASE ) / MERGED + + oldIFS=$IFS + IFS=# + for c in $(echo "$LAYOUT" | sed 's:.:&#:g') + do + if test "$c" = " " + then + continue + fi + + if test "$c" = "(" + then + nested=$(( nested + 1 )) + continue + fi + + if test "$c" = ")" + then + nested=$(( nested - 1 )) + continue + fi + + if test "$nested" -lt "$nested_min" + then + nested_min=$nested + fi + done + IFS=$oldIFS + + debug_print "NESTED MIN: $nested_min" + + while test "$nested_min" -gt "0" + do + start=$(( start + 1 )) + end=$(( end - 1 )) + + start_minus_one=$(( start - 1 )) + + while ! test "$(substring "$LAYOUT" "$start_minus_one" 1)" = "(" + do + start=$(( start + 1 )) + start_minus_one=$(( start_minus_one + 1 )) + done + + while ! test "$(substring "$LAYOUT" "$end" 1)" = ")" + do + end=$(( end - 1 )) + done + + nested_min=$(( nested_min - 1 )) + done + + debug_print "CLEAN : $(substring "$LAYOUT" "$start" "$(( end - start ))")" + + + # Step 2: + # + # Search for all valid separators ("+", "/" or ",") which are *not* + # inside parenthesis. Save the index at which each of them makes the + # first appearance. + + index_new_tab="" + index_horizontal_split="" + index_vertical_split="" + + nested=0 + i=$(( start - 1 )) + + oldIFS=$IFS + IFS=# + for c in $(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:.:&#:g'); + do + i=$(( i + 1 )) + + if test "$c" = " " + then + continue + fi + + if test "$c" = "(" + then + nested=$(( nested + 1 )) + continue + fi + + if test "$c" = ")" + then + nested=$(( nested - 1 )) + continue + fi + + if test "$nested" = 0 + then + current=$c + + if test "$current" = "+" + then + if test -z "$index_new_tab" + then + index_new_tab=$i + fi + + elif test "$current" = "/" + then + if test -z "$index_horizontal_split" + then + index_horizontal_split=$i + fi + + elif test "$current" = "," + then + if test -z "$index_vertical_split" + then + index_vertical_split=$i + fi + fi + fi + done + IFS=$oldIFS + + + # Step 3: + # + # Process the separator with the highest order of precedence + # (";" has the highest precedence and "|" the lowest one). + # + # By "process" I mean recursively call this function twice: the first + # one with the substring at the left of the separator and the second one + # with the one at its right. + + terminate="false" + + if ! test -z "$index_new_tab" + then + before="-tabnew" + after="tabnext" + index=$index_new_tab + terminate="true" + + elif ! test -z "$index_horizontal_split" + then + before="split" + after="wincmd j" + index=$index_horizontal_split + terminate="true" + + elif ! test -z "$index_vertical_split" + then + before="vertical split" + after="wincmd l" + index=$index_vertical_split + terminate="true" + fi + + if test "$terminate" = "true" + then + CMD="$CMD | $before" + CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$start" "$(( index - start ))")" "$CMD") + CMD="$CMD | $after" + CMD=$(gen_cmd_aux "$(substring "$LAYOUT" "$(( index + 1 ))" "$(( ${#LAYOUT} - index ))")" "$CMD") + echo "$CMD" + return + fi + + + # Step 4: + # + # If we reach this point, it means there are no separators and we just + # need to print the command to display the specified buffer + + target=$(substring "$LAYOUT" "$start" "$(( end - start ))" | sed 's:[ @();|-]::g') + + if test "$target" = "LOCAL" + then + CMD="$CMD | 1b" + + elif test "$target" = "BASE" + then + CMD="$CMD | 2b" + + elif test "$target" = "REMOTE" + then + CMD="$CMD | 3b" + + elif test "$target" = "MERGED" + then + CMD="$CMD | 4b" + + else + CMD="$CMD | ERROR: >$target<" + fi + + echo "$CMD" + return +} + + +gen_cmd () { + # This function returns (in global variable FINAL_CMD) the string that + # you can use when invoking "vim" (as shown next) to obtain a given + # layout: + # + # $ vim -f $FINAL_CMD "$LOCAL" "$BASE" "$REMOTE" "$MERGED" + # + # It takes one single argument: a string containing the desired layout + # definition. + # + # The syntax of the "layout definitions" is explained in "Documentation/ + # mergetools/vimdiff.txt" but you can already intuitively understand how + # it works by knowing that... + # + # * "+" means "a new vim tab" + # * "/" means "a new vim horizontal split" + # * "," means "a new vim vertical split" + # + # It also returns (in global variable FINAL_TARGET) the name ("LOCAL", + # "BASE", "REMOTE" or "MERGED") of the file that is marked with an "@", + # or "MERGED" if none of them is. + # + # Example: + # + # gen_cmd "@LOCAL , REMOTE" + # | + # `-> FINAL_CMD == "-c \"echo | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\"" + # FINAL_TARGET == "LOCAL" + + LAYOUT=$1 + + + # Search for a "@" in one of the files identifiers ("LOCAL", "BASE", + # "REMOTE", "MERGED"). If not found, use "MERGE" as the default file + # where changes will be saved. + + if echo "$LAYOUT" | grep @LOCAL >/dev/null + then + FINAL_TARGET="LOCAL" + elif echo "$LAYOUT" | grep @BASE >/dev/null + then + FINAL_TARGET="BASE" + else + FINAL_TARGET="MERGED" + fi + + + # Obtain the first part of vim "-c" option to obtain the desired layout + + CMD=$(gen_cmd_aux "$LAYOUT") + + + # Adjust the just obtained script depending on whether more than one + # windows are visible or not + + if echo "$LAYOUT" | grep ",\|/" >/dev/null + then + CMD="$CMD | tabdo windo diffthis" + else + CMD="$CMD | bufdo diffthis" + fi + + + # Add an extra "-c" option to move to the first tab (notice that we + # can't simply append the command to the previous "-c" string as + # explained here: https://github.com/vim/vim/issues/9076 + + FINAL_CMD="-c \"$CMD\" -c \"tabfirst\"" +} + + +################################################################################ +## API functions (called from "git-mergetool--lib.sh") +################################################################################ + diff_cmd () { "$merge_tool_path" -R -f -d \ -c 'wincmd l' -c 'cd $GIT_PREFIX' "$LOCAL" "$REMOTE" } + +diff_cmd_help () { + TOOL=$1 + + case "$TOOL" in + nvimdiff*) + printf "Use Neovim" + ;; + gvimdiff*) + printf "Use gVim (requires a graphical session)" + ;; + vimdiff*) + printf "Use Vim" + ;; + esac + + return 0 +} + + merge_cmd () { + layout=$(git config mergetool.vimdiff.layout) + case "$1" in *vimdiff) - if $base_present + if test -z "$layout" then - "$merge_tool_path" -f -d -c '4wincmd w | wincmd J' \ - "$LOCAL" "$BASE" "$REMOTE" "$MERGED" - else - "$merge_tool_path" -f -d -c 'wincmd l' \ - "$LOCAL" "$MERGED" "$REMOTE" + # Default layout when none is specified + layout="(LOCAL,BASE,REMOTE)/MERGED" fi ;; *vimdiff1) - "$merge_tool_path" -f -d \ - -c 'echon "Resolve conflicts leftward then save. Use :cq to abort."' \ - "$LOCAL" "$REMOTE" - ret="$?" - if test "$ret" -eq 0 - then - cp -- "$LOCAL" "$MERGED" - fi - return "$ret" + layout="@LOCAL,REMOTE" ;; *vimdiff2) - "$merge_tool_path" -f -d -c 'wincmd l' \ - "$LOCAL" "$MERGED" "$REMOTE" + layout="LOCAL,MERGED,REMOTE" ;; *vimdiff3) - if $base_present - then - "$merge_tool_path" -f -d -c 'hid | hid | hid' \ - "$LOCAL" "$REMOTE" "$BASE" "$MERGED" - else - "$merge_tool_path" -f -d -c 'hid | hid' \ - "$LOCAL" "$REMOTE" "$MERGED" - fi + layout="MERGED" ;; esac + + gen_cmd "$layout" + + debug_print "" + debug_print "FINAL CMD : $FINAL_CMD" + debug_print "FINAL TAR : $FINAL_TARGET" + + if $base_present + then + eval "$merge_tool_path" \ + -f "$FINAL_CMD" "$LOCAL" "$BASE" "$REMOTE" "$MERGED" + else + # If there is no BASE (example: a merge conflict in a new file + # with the same name created in both braches which didn't exist + # before), close all BASE windows using vim's "quit" command + + FINAL_CMD=$(echo "$FINAL_CMD" | \ + sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g') + + eval "$merge_tool_path" \ + -f "$FINAL_CMD" "$LOCAL" "$REMOTE" "$MERGED" + fi + + ret="$?" + + if test "$ret" -eq 0 + then + case "$FINAL_TARGET" in + LOCAL) + source_path="$LOCAL" + ;; + REMOTE) + source_path="$REMOTE" + ;; + MERGED|*) + # Do nothing + source_path= + ;; + esac + + if test -n "$source_path" + then + cp "$source_path" "$MERGED" + fi + fi + + return "$ret" } -translate_merge_tool_path() { + +merge_cmd_help () { + TOOL=$1 + + case "$TOOL" in + nvimdiff*) + printf "Use Neovim " + ;; + gvimdiff*) + printf "Use gVim (requires a graphical session) " + ;; + vimdiff*) + printf "Use Vim " + ;; + esac + + case "$TOOL" in + *1) + echo "with a 2 panes layout (LOCAL and REMOTE)" + ;; + *2) + echo "with a 3 panes layout (LOCAL, MERGED and REMOTE)" + ;; + *3) + echo "where only the MERGED file is shown" + ;; + *) + echo "with a custom layout (see \`git help mergetool\`'s \`BACKEND SPECIFIC HINTS\` section)" + ;; + esac + + return 0 +} + + +translate_merge_tool_path () { case "$1" in nvimdiff*) echo nvim @@ -57,14 +503,121 @@ translate_merge_tool_path() { esac } + exit_code_trustable () { true } + list_tool_variants () { - for prefix in '' g n; do - for suffix in '' 1 2 3; do - echo "${prefix}vimdiff${suffix}" + if test "$TOOL_MODE" = "diff" + then + for prefix in '' g n + do + echo "${prefix}vimdiff" done - done + else + for prefix in '' g n + do + for suffix in '' 1 2 3 + do + echo "${prefix}vimdiff${suffix}" + done + done + fi +} + + +################################################################################ +## Unit tests (called from scripts inside the "t" folder) +################################################################################ + +run_unit_tests () { + # Function to make sure that we don't break anything when modifying this + # script. + + NUMBER_OF_TEST_CASES=16 + + TEST_CASE_01="(LOCAL,BASE,REMOTE)/MERGED" # default behaviour + TEST_CASE_02="@LOCAL,REMOTE" # when using vimdiff1 + TEST_CASE_03="LOCAL,MERGED,REMOTE" # when using vimdiff2 + TEST_CASE_04="MERGED" # when using vimdiff3 + TEST_CASE_05="LOCAL/MERGED/REMOTE" + TEST_CASE_06="(LOCAL/REMOTE),MERGED" + TEST_CASE_07="MERGED,(LOCAL/REMOTE)" + TEST_CASE_08="(LOCAL,REMOTE)/MERGED" + TEST_CASE_09="MERGED/(LOCAL,REMOTE)" + TEST_CASE_10="(LOCAL/BASE/REMOTE),MERGED" + TEST_CASE_11="(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED" + TEST_CASE_12="((LOCAL,REMOTE)/BASE),MERGED" + TEST_CASE_13="((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)" + TEST_CASE_14="BASE,REMOTE+BASE,LOCAL" + TEST_CASE_15=" (( (LOCAL , BASE , REMOTE) / MERGED)) +(BASE) , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) , MERGED ) " + TEST_CASE_16="LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED" + + 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\"" + EXPECTED_CMD_02="-c \"echo | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\"" + EXPECTED_CMD_03="-c \"echo | vertical split | 1b | wincmd l | vertical split | 4b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\"" + EXPECTED_CMD_04="-c \"echo | 4b | bufdo diffthis\" -c \"tabfirst\"" + EXPECTED_CMD_05="-c \"echo | split | 1b | wincmd j | split | 4b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\"" + EXPECTED_CMD_06="-c \"echo | vertical split | split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\"" + EXPECTED_CMD_07="-c \"echo | vertical split | 4b | wincmd l | split | 1b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\"" + EXPECTED_CMD_08="-c \"echo | split | vertical split | 1b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\"" + EXPECTED_CMD_09="-c \"echo | split | 4b | wincmd j | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\"" + EXPECTED_CMD_10="-c \"echo | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\"" + 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\"" + 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\"" + 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\"" + EXPECTED_CMD_14="-c \"echo | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | 2b | wincmd l | 1b | tabdo windo diffthis\" -c \"tabfirst\"" + 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\"" + 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\"" + + EXPECTED_TARGET_01="MERGED" + EXPECTED_TARGET_02="LOCAL" + EXPECTED_TARGET_03="MERGED" + EXPECTED_TARGET_04="MERGED" + EXPECTED_TARGET_05="MERGED" + EXPECTED_TARGET_06="MERGED" + EXPECTED_TARGET_07="MERGED" + EXPECTED_TARGET_08="MERGED" + EXPECTED_TARGET_09="MERGED" + EXPECTED_TARGET_10="MERGED" + EXPECTED_TARGET_11="MERGED" + EXPECTED_TARGET_12="MERGED" + EXPECTED_TARGET_13="MERGED" + EXPECTED_TARGET_14="MERGED" + EXPECTED_TARGET_15="MERGED" + EXPECTED_TARGET_16="MERGED" + + at_least_one_ko="false" + + for i in $(seq -w 1 99) + do + if test "$i" -gt $NUMBER_OF_TEST_CASES + then + break + fi + + gen_cmd "$(eval echo \${TEST_CASE_"$i"})" + + if test "$FINAL_CMD" = "$(eval echo \${EXPECTED_CMD_"$i"})" \ + && test "$FINAL_TARGET" = "$(eval echo \${EXPECTED_TARGET_"$i"})" + then + printf "Test Case #%02d: OK\n" "$(echo "$i" | sed 's/^0*//')" + else + printf "Test Case #%02d: KO !!!!\n" "$(echo "$i" | sed 's/^0*//')" + echo " FINAL_CMD : $FINAL_CMD" + echo " FINAL_CMD (expected) : $(eval echo \${EXPECTED_CMD_"$i"})" + echo " FINAL_TARGET : $FINAL_TARGET" + echo " FINAL_TARGET (expected): $(eval echo \${EXPECTED_TARGET_"$i"})" + at_least_one_ko="true" + fi + done + + if test "$at_least_one_ko" = "true" + then + return 255 + else + return 0 + fi } diff --git a/mergetools/winmerge b/mergetools/winmerge index 74d03259fd..36c72dde6e 100644 --- a/mergetools/winmerge +++ b/mergetools/winmerge @@ -3,6 +3,10 @@ diff_cmd () { return 0 } +diff_cmd_help () { + echo "Use WinMerge (requires a graphical session)" +} + merge_cmd () { # mergetool.winmerge.trustExitCode is implicitly false. # touch $BACKUP so that we can check_unchanged. @@ -13,3 +17,7 @@ merge_cmd () { translate_merge_tool_path() { mergetool_find_win32_cmd "WinMergeU.exe" "WinMerge" } + +merge_cmd_help () { + echo "Use WinMerge (requires a graphical session)" +} diff --git a/mergetools/xxdiff b/mergetools/xxdiff index d5ce467995..cd205f9842 100644 --- a/mergetools/xxdiff +++ b/mergetools/xxdiff @@ -12,6 +12,10 @@ diff_cmd () { fi } +diff_cmd_help () { + echo "Use xxdiff (requires a graphical session)" +} + merge_cmd () { if $base_present then @@ -28,3 +32,7 @@ merge_cmd () { --merged-file "$MERGED" "$LOCAL" "$REMOTE" fi } + +merge_cmd_help () { + echo "Use xxdiff (requires a graphical session)" +} diff --git a/t/t7609-mergetool--lib.sh b/t/t7609-mergetool--lib.sh new file mode 100755 index 0000000000..d848fe6442 --- /dev/null +++ b/t/t7609-mergetool--lib.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +test_description='git mergetool + +Testing basic merge tools options' + +. ./test-lib.sh + +test_expect_success 'mergetool --tool=vimdiff creates the expected layout' ' + . $GIT_BUILD_DIR/mergetools/vimdiff && + run_unit_tests +' + +test_done