git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
From: Fernando Ramos <greenfoo@u92.eu>
To: git@vger.kernel.org
Cc: gitster@pobox.com, davvid@gmail.com, seth@eseth.com,
	levraiphilippeblain@gmail.com, rogi@skylittlesystem.org
Subject: [PATCH 1/3] vimdiff: new implementation with layout support
Date: Thu,  4 Nov 2021 17:09:57 +0100	[thread overview]
Message-ID: <20211104160959.183402-2-greenfoo@u92.eu> (raw)
In-Reply-To: <20211104160959.183402-1-greenfoo@u92.eu>

When running 'git mergetool -t vimdiff', a new configuration option
('mergetool.vimdiff.layout') can now be used to select how the user
wants the different windows, tabs and buffers to be displayed.

If the option is not provided, the layout will be the same one that was
being used before this commit (ie. two rows with LOCAL, BASE and COMMIT
in the top one and MERGED in the bottom one).

The 'vimdiff' variants ('vimdiff{1,2,3}') still work but, because they
represented nothing else than different layouts, are now internally
implemented as a subcase of 'vimdiff' with the corresponding
pre-configured 'layout'.

Signed-off-by: Fernando Ramos <greenfoo@u92.eu>
---
 mergetools/vimdiff | 530 ++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 504 insertions(+), 26 deletions(-)

diff --git a/mergetools/vimdiff b/mergetools/vimdiff
index 96f6209a04..1f2e88777e 100644
--- a/mergetools/vimdiff
+++ b/mergetools/vimdiff
@@ -1,48 +1,509 @@
+#!/bin/bash
+
+# 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 the shell, manually. This is only expected to be done by developers
+#     who are editing this script. When this happens, the script runs 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.
+#
+
+
+# Set to "true" to print debug messages to stderr
+DEBUG=false
+#DEBUG=true
+
+
+################################################################################
+## Internal functions (not meant to be used outside this script)
+################################################################################
+
+debug_print() { 
+	# Send message to stderr if global variable DEBUG is set to "true"
+        
+	if test "$DEBUG" = "true"
+	then
+		>&2 echo "$@";
+	fi
+}
+
+
+gen_cmd_aux() {
+	# Auxiliary function used from "gen_cmd()".
+	# Read that other function documentation for more details.
+
+	local LAYOUT=$1 
+	local 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
+
+	local start=0
+	local end=${#LAYOUT}
+
+	local nested=0
+	local 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 
+
+	for (( i=$start; i<$end; i++ )); do
+		if test "${LAYOUT:$i:1}" = " "
+		then
+			continue
+		fi
+
+		if test "${LAYOUT:$i:1}" = "("
+		then
+			nested=$(( nested + 1 ))
+			continue
+		fi
+
+		if test "${LAYOUT:$i:1}" = ")"
+		then
+			nested=$(( nested - 1 ))
+			continue
+		fi
+
+		if test "$nested" -lt "$nested_min"
+		then
+			nested_min=$nested
+		fi
+	done
+
+	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 "${LAYOUT:$start_minus_one:1}" = "("
+		do
+			start=$(( start + 1 ))
+			start_minus_one=$(( start_minus_one + 1 ))
+		done
+
+		while ! test "${LAYOUT:$end:1}" = ")"
+		do
+			end=$(( end - 1 ))
+		done
+
+		nested_min=$(( nested_min - 1 ))
+	done
+
+        debug_print "CLEAN     : ${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.
+
+	local index_semicolon=""
+	local index_minus=""
+	local index_pipe=""
+
+	nested=0
+	for (( i=$start; i<$end; i++ )); do
+		if test "${LAYOUT:$i:1}" = " "
+		then
+			continue
+		fi
+
+		if test "${LAYOUT:$i:1}" = "("
+		then
+			nested=$(( nested + 1 ))
+			continue
+		fi
+
+		if test "${LAYOUT:$i:1}" = ")"
+		then
+			nested=$(( nested - 1 ))
+			continue
+		fi
+
+		if test "$nested" -eq "0"
+		then
+			current=${LAYOUT:$i:1}
+			
+			if test "$current" = ";"
+			then
+				if test -z "$index_semicolon"
+				then
+					index_semicolon=$i
+				fi
+
+			elif test "$current" = "-"
+			then
+				if test -z "$index_minus"
+				then
+					index_minus=$i
+				fi
+
+			elif test "$current" = "|"
+			then
+				if test -z "$index_pipe"
+				then
+					index_pipe=$i
+				fi
+			fi
+		fi
+	done
+
+
+	# 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.
+
+	local terminate="false"
+
+	if ! test -z "$index_semicolon"
+	then
+		before="-tabnew"
+		after="tabnext"
+		index=$index_semicolon
+		terminate="true"
+
+	elif ! test -z "$index_minus"
+	then
+		before="split"
+		after="wincmd j"
+		index=$index_minus
+		terminate="true"
+
+	elif ! test -z "$index_pipe"
+	then
+		before="vertical split"
+		after="wincmd l"
+		index=$index_pipe
+		terminate="true"
+	fi
+
+	if  test "$terminate" = "true"
+	then
+		CMD="$CMD | $before"
+		CMD=$(gen_cmd_aux "${LAYOUT:$start:$(( index - start ))}" "$CMD")
+		CMD="$CMD | $after"
+		CMD=$(gen_cmd_aux "${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
+
+	local target=$(echo "${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 ... (TODO)...
+	# 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"
+
+	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.
+
+	AUX=$(echo "$LAYOUT" | grep -oe "[A-Z]\+\*")
+
+	if ! test -z "$AUX"
+	then
+	        FINAL_TARGET="${AUX:0:-1}"
+	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 test $(echo $LAYOUT | wc -w) == "1"
+	then
+		CMD="$CMD | bufdo diffthis"
+        else
+		CMD="$CMD | tabdo windo 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\""
+}
+
+
+run_unit_tests() {
+	# Function to make sure that we don't break anything when modifying this
+	# script.
+	#
+	# This function is automatically executed when you execute this script
+	# from the shell with environment variable "DEBUG_GIT_VIMDIFF" set (to
+	# any value).
+
+	local test_cases=(
+		`#Test case 00` "LOCAL | MERGED | REMOTE"
+		`#Test case 01` "LOCAL - MERGED - REMOTE"
+		`#Test case 02` "(LOCAL - REMOTE) | MERGED"
+		`#Test case 03` "MERGED | (LOCAL - REMOTE)"
+		`#Test case 04` "(LOCAL | REMOTE) - MERGED"
+		`#Test case 05` "MERGED - (LOCAL | REMOTE)"
+		`#Test case 06` "(LOCAL | BASE | REMOTE) - MERGED"
+		`#Test case 07` "(LOCAL - BASE - REMOTE) | MERGED"
+		`#Test case 08` "LOCAL* | REMOTE"
+		`#Test case 09` "MERGED"
+		`#Test case 10` "(LOCAL | BASE | REMOTE) - MERGED; BASE | LOCAL; BASE | REMOTE; (LOCAL - BASE - REMOTE) | MERGED"
+		`#Test case 11` "((LOCAL | REMOTE) - BASE) | MERGED"
+		`#Test case 12` "((LOCAL | REMOTE) - BASE) | ((LOCAL - REMOTE) | MERGED)"
+		`#Test case 13` "BASE | REMOTE ; BASE | LOCAL"
+	)
+
+	local expected_cmd=(
+		`#Test case 00` "-c \"echo | vertical split | 1b | wincmd l | vertical split | 4b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 01` "-c \"echo | split | 1b | wincmd j | split | 4b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 02` "-c \"echo | vertical split | split | 1b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 03` "-c \"echo | vertical split | 4b | wincmd l | split | 1b | wincmd j | 3b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 04` "-c \"echo | split | vertical split | 1b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 05` "-c \"echo | split | 4b | wincmd j | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 06` "-c \"echo | split | vertical split | 1b | wincmd l | vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 07` "-c \"echo | vertical split | split | 1b | wincmd j | split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 08` "-c \"echo | vertical split | 1b | wincmd l | 3b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 09` "-c \"echo | 4b | bufdo diffthis\" -c \"tabfirst\""
+		`#Test case 10` "-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\""
+		`#Test case 11` "-c \"echo | vertical split | split | vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | 4b | tabdo windo diffthis\" -c \"tabfirst\""
+		`#Test case 12` "-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\""
+		`#Test case 13` "-c \"echo | -tabnew | vertical split | 2b | wincmd l | 3b | tabnext | vertical split | 2b | wincmd l | 1b | tabdo windo diffthis\" -c \"tabfirst\""
+	)
+
+	local expected_target=(
+		`#Test case 00` "MERGED"
+		`#Test case 01` "MERGED"
+		`#Test case 02` "MERGED"
+		`#Test case 03` "MERGED"
+		`#Test case 04` "MERGED"
+		`#Test case 05` "MERGED"
+		`#Test case 06` "MERGED"
+		`#Test case 07` "MERGED"
+		`#Test case 08` "LOCAL"
+		`#Test case 09` "MERGED"
+		`#Test case 10` "MERGED"
+		`#Test case 11` "MERGED"
+		`#Test case 12` "MERGED"
+		`#Test case 13` "MERGED"
+	)
+
+	local at_least_one_ko="false"
+
+	for i in ${!test_cases[@]}; do
+		gen_cmd "${test_cases[$i]}"
+	
+		if test "$FINAL_CMD" = "${expected_cmd[$i]}" && test "$FINAL_TARGET" = "${expected_target[$i]}"
+		then
+			printf "Test Case #%02d: OK\n" $i
+		else
+			printf "Test Case #%02d: KO !!!!\n" $i
+			echo "  FINAL_CMD              : $FINAL_CMD"
+                        echo "  FINAL_CMD (expected)   : ${expected_cmd[$i]}"
+			echo "  FINAL_TARGET           : $FINAL_TARGET"
+                        echo "  FINAL_TARGET (expected): ${expected_target[$i]}"
+			at_least_one_ko="true"
+		fi
+	done
+	
+	if test "$at_least_one_ko" = "true"
+	then
+		return -1
+	else
+		return 0
+	fi
+}
+
+
+################################################################################
+## 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"
 }
 
+
 merge_cmd () {
+	layout=$(git config mergetool.$merge_tool.layout)
+	print_warning="false"
+
 	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* | MERGED"
+		print_warning="true"
 		;;
 	*vimdiff2)
-		"$merge_tool_path" -f -d -c 'wincmd l' \
-			"$LOCAL" "$MERGED" "$REMOTE"
+		layout="LOCAL | MERGED | REMOTE"
+		print_warning="true"
 		;;
 	*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"
+		print_warning="true"
 		;;
 	esac
+
+	if test "$print_warning" = "true"
+	then
+		echo "WARNING:"
+		echo "WARNING: '$1' is going to be removed in a future version. You will be"
+		echo "WARNING: able to obtain the same result by selecting 'vimdiff' as the merge"
+		echo "WARNING: tool and setting configuration variable 'mergetool.vimdiff.layout'"
+		echo "WARNING: to the following value:"
+		echo "WARNING:"
+		echo "WARNING:     layout = \"$layout\""
+		echo "WARNING:"
+		echo "Press ENTER to continue..."
+		read
+	fi
+
+	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
+		if test "$FINAL_TARGET" != "MERGED"
+		then
+			eval cp -- \$"$FINAL_TARGET" "$MERGED"
+		fi
+	fi
+	return "$ret"
 }
 
+
 translate_merge_tool_path() {
 	case "$1" in
 	nvimdiff*)
@@ -57,14 +518,31 @@ 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
+		for suffix in '' 1 2 3
+		do
 			echo "${prefix}vimdiff${suffix}"
 		done
 	done
 }
+
+
+################################################################################
+## Run unit tests when calling this script from a shell
+################################################################################
+
+if test $(ps -o stat= -p $PPID) = "Ss" && test $(ps -o stat= -p $$) = "S+"
+then
+	# Script is being manually run from command line (see
+	# https://stackoverflow.com/questions/4261876/check-if-bash-script-was-invoked-from-a-shell-or-another-script-application)
+
+	run_unit_tests
+fi
+
-- 
2.33.1


  reply	other threads:[~2021-11-04 16:10 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-04 16:09 [PATCH 0/3] vimdiff: new layout option + docs Fernando Ramos
2021-11-04 16:09 ` Fernando Ramos [this message]
2021-11-07 22:41   ` [PATCH 1/3] vimdiff: new implementation with layout support David Aguilar
2021-11-08  0:47     ` Eric Sunshine
2021-11-04 16:09 ` [PATCH 2/3] vimdiff: add tool documentation Fernando Ramos
2021-11-07 21:24   ` David Aguilar
2021-11-08  1:02     ` Eric Sunshine
2021-11-08 19:08     ` Junio C Hamano
2021-11-04 16:09 ` [PATCH 3/3] vimdiff: remove deprecated {,g,n}vimdiff{1,2,3} variants Fernando Ramos

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://vger.kernel.org/majordomo-info.html

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20211104160959.183402-2-greenfoo@u92.eu \
    --to=greenfoo@u92.eu \
    --cc=davvid@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=levraiphilippeblain@gmail.com \
    --cc=rogi@skylittlesystem.org \
    --cc=seth@eseth.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).