Subject: [PATCH v2 0/2] add case insensitivity option to bash completion

From: "Alison Winters via GitGitGadget" <[email-masked]>
Date: Mon, 21 Nov 2022 00:26:57 +0000

In 3bb16a8bf2 (tag, branch, for-each-ref: add --ignore-case for sorting and
filtering, 2016-12-04), support was added for filtering and sorting refs in
a case insensitive way. This is a behavior that seems appropriate to enable
with shell completion. Many shells provide case insensitive completion as an
option, even on filesystems that remain case sensitive.

This patch adds a new variable that, when set, will allow Bash completion to
use the --ignore-case option to match refs. Additionally, some basic support
is implemented to match pseudorefs like HEAD.

Changes since v1:

 * Improved comments and commit messages to clarify behavior on case
   sensitive filesystems
 * Replaced some lengthy if blocks with inline substitution
 * As a result of the above change, GIT_COMPLETION_IGNORE_CASE no longer
   needs to be set to "1" and now just needs to be present in the
   environment to work
 * Removed unnecessary exports in tests

Alison Winters (2):
  completion: add optional ignore-case when matching refs
  completion: add case-insensitive match of pseudorefs

 contrib/completion/git-completion.bash | 26 +++++++++++++++++++---
 t/t9902-completion.sh                  | 30 ++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 3 deletions(-)

base-commit: a0789512c5a4ae7da935cd2e419f253cb3cb4ce7
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1374%2Falisonatwork%2Fbash-insensitive-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1374/alisonatwork/bash-insensitive-v2
Pull-Request: https://github.com/git/git/pull/1374

Range-diff vs v1:

 1:  cef9a12b575 ! 1:  a261a94877a completion: add optional ignore-case when matching refs
     @@ Metadata
       ## Commit message ##
          completion: add optional ignore-case when matching refs
      
     -    If GIT_COMPLETION_IGNORE_CASE=1 is set, --ignore-case will be added to
     -    git for-each-ref calls so that branches and tags can be matched case
     -    insensitively.
     +    If GIT_COMPLETION_IGNORE_CASE is set, --ignore-case will be added to
     +    git for-each-ref calls so that refs can be matched case insensitively,
     +    even when running on case sensitive filesystems.
      
          Signed-off-by: Alison Winters <[email-masked]>
      
     @@ contrib/completion/git-completion.bash
      +#
      +#   GIT_COMPLETION_IGNORE_CASE
      +#
     -+#     When set to "1", suggest refs that match case insensitively (e.g.,
     -+#     completing "FOO" on "git checkout f<TAB>").
     ++#     When set, uses for-each-ref '--ignore-case' to find refs that match
     ++#     case insensitively, even on systems with case sensitive file systems
     ++#     (e.g., completing tag name "FOO" on "git checkout f<TAB>").
       
       case "$COMP_WORDBREAKS" in
       *:*) : great ;;
     -@@ contrib/completion/git-completion.bash: __git_complete_index_file ()
     - __git_heads ()
     - {
     +@@ contrib/completion/git-completion.bash: __git_heads ()
       	local pfx="${1-}" cur_="${2-}" sfx="${3-}"
     -+	local ignore_case=""
     -+
     -+	if test "${GIT_COMPLETION_IGNORE_CASE-}" = "1"
     -+	then
     -+		ignore_case="--ignore-case"
     -+	fi
       
       	__git for-each-ref --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \
     -+			$ignore_case \
     ++			${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
       			"refs/heads/$cur_*" "refs/heads/$cur_*/**"
       }
       
     -@@ contrib/completion/git-completion.bash: __git_heads ()
     - __git_remote_heads ()
     - {
     +@@ contrib/completion/git-completion.bash: __git_remote_heads ()
       	local pfx="${1-}" cur_="${2-}" sfx="${3-}"
     -+	local ignore_case=""
     -+
     -+	if test "${GIT_COMPLETION_IGNORE_CASE-}" = "1"
     -+	then
     -+		ignore_case="--ignore-case"
     -+	fi
       
       	__git for-each-ref --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \
     -+			$ignore_case \
     ++			${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
       			"refs/remotes/$cur_*" "refs/remotes/$cur_*/**"
       }
       
     -@@ contrib/completion/git-completion.bash: __git_remote_heads ()
     - __git_tags ()
     - {
     +@@ contrib/completion/git-completion.bash: __git_tags ()
       	local pfx="${1-}" cur_="${2-}" sfx="${3-}"
     -+	local ignore_case=""
     -+
     -+	if test "${GIT_COMPLETION_IGNORE_CASE-}" = "1"
     -+	then
     -+		ignore_case="--ignore-case"
     -+	fi
       
       	__git for-each-ref --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \
     -+			$ignore_case \
     ++			${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
       			"refs/tags/$cur_*" "refs/tags/$cur_*/**"
       }
       
      @@ contrib/completion/git-completion.bash: __git_dwim_remote_heads ()
     - {
     - 	local pfx="${1-}" cur_="${2-}" sfx="${3-}"
     - 	local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
     -+	local ignore_case=""
     -+
     -+	if test "${GIT_COMPLETION_IGNORE_CASE-}" = "1"
     -+	then
     -+		ignore_case="--ignore-case"
     -+	fi
     - 
     - 	# employ the heuristic used by git checkout and git switch
     - 	# Try to find a remote branch that cur_es the completion word
       	# but only output if the branch name is unique
       	__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
       		--sort="refname:strip=3" \
     -+		$ignore_case \
     ++		${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
       		"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
       	uniq -u
       }
     -@@ contrib/completion/git-completion.bash: __git_refs ()
     - 	local pfx="${3-}" cur_="${4-$cur}" sfx="${5-}"
     - 	local match="${4-}"
     - 	local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
     -+	local ignore_case=""
     - 
     - 	__git_find_repo_path
     - 	dir="$__git_repo_path"
     -@@ contrib/completion/git-completion.bash: __git_refs ()
     - 		fi
     - 	fi
     - 
     -+	if test "${GIT_COMPLETION_IGNORE_CASE-}" = "1"
     -+	then
     -+		ignore_case="--ignore-case"
     -+	fi
     -+
     - 	if [ "$list_refs_from" = path ]; then
     - 		if [[ "$cur_" == ^* ]]; then
     - 			pfx="$pfx^"
      @@ contrib/completion/git-completion.bash: __git_refs ()
       			;;
       		esac
       		__git_dir="$dir" __git for-each-ref --format="$fer_pfx%($format)$sfx" \
     -+			$ignore_case \
     ++			${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
       			"${refs[@]}"
       		if [ -n "$track" ]; then
       			__git_dwim_remote_heads "$pfx" "$match" "$sfx"
     @@ contrib/completion/git-completion.bash: __git_refs ()
       			$match*)	echo "${pfx}HEAD$sfx" ;;
       			esac
       			__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
     -+				$ignore_case \
     ++				${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
       				"refs/remotes/$remote/$match*" \
       				"refs/remotes/$remote/$match*/**"
       		else
     @@ t/t9902-completion.sh: test_expect_success 'checkout completes ref names' '
      +
      +test_expect_success 'checkout matches case insensitively with GIT_COMPLETION_IGNORE_CASE' '
      +	(
     -+		. "$GIT_BUILD_DIR/contrib/completion/git-completion.bash" &&
     -+		GIT_COMPLETION_IGNORE_CASE=1 && export GIT_COMPLETION_IGNORE_CASE &&
     ++		GIT_COMPLETION_IGNORE_CASE=1 &&
      +		test_completion "git checkout M" <<-\EOF
      +		main Z
      +		mybranch Z
 2:  c455e855395 ! 2:  480f6554c93 completion: add case-insensitive match of pseudorefs
     @@ Metadata
       ## Commit message ##
          completion: add case-insensitive match of pseudorefs
      
     -    When GIT_COMPLETION_IGNORE_CASE=1, also allow lowercase completion text
     -    like "head" to match HEAD and other pseudorefs.
     +    When GIT_COMPLETION_IGNORE_CASE is set, also allow lowercase completion
     +    text like "head" to match uppercase HEAD and other pseudorefs.
      
          Signed-off-by: Alison Winters <[email-masked]>
      
     @@ contrib/completion/git-completion.bash: __git_refs ()
       	local match="${4-}"
      +	local umatch="${4-}"
       	local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
     - 	local ignore_case=""
       
     + 	__git_find_repo_path
      @@ contrib/completion/git-completion.bash: __git_refs ()
     - 	if test "${GIT_COMPLETION_IGNORE_CASE-}" = "1"
     - 	then
     - 		ignore_case="--ignore-case"
     -+		# use tr instead of ${match,^^} to preserve bash 3.2 compatibility
     -+		umatch=$(echo "$match" | tr a-z A-Z 2> /dev/null || echo "$match")
     + 		fi
       	fi
       
     ++	if test "${GIT_COMPLETION_IGNORE_CASE:+1}" = "1"
     ++	then
     ++		# uppercase with tr instead of ${match,^^} for bash 3.2 compatibility
     ++		umatch=$(echo "$match" | tr a-z A-Z 2>/dev/null || echo "$match")
     ++	fi
     ++
       	if [ "$list_refs_from" = path ]; then
     -@@ contrib/completion/git-completion.bash: __git_refs ()
     + 		if [[ "$cur_" == ^* ]]; then
     + 			pfx="$pfx^"
       			fer_pfx="$fer_pfx^"
       			cur_=${cur_#^}
       			match=${match#^}
     @@ contrib/completion/git-completion.bash: __git_refs ()
      +			$match*|$umatch*)	echo "${pfx}HEAD$sfx" ;;
       			esac
       			__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
     - 				$ignore_case \
     + 				${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
      @@ contrib/completion/git-completion.bash: __git_refs ()
       		else
       			local query_symref
     @@ t/t9902-completion.sh: test_expect_success 'checkout matches case insensitively
      +
      +test_expect_success 'checkout completes pseudo refs case insensitively with GIT_COMPLETION_IGNORE_CASE' '
      +	(
     -+		. "$GIT_BUILD_DIR/contrib/completion/git-completion.bash" &&
     -+		GIT_COMPLETION_IGNORE_CASE=1 && export GIT_COMPLETION_IGNORE_CASE &&
     ++		GIT_COMPLETION_IGNORE_CASE=1 &&
      +		test_completion "git checkout h" <<-\EOF
      +		HEAD Z
      +		EOF

-- 
gitgitgadget