Skip to contentSkip to conversation

completion: fix bugs with slashes in remote names

PATCH In review David Mandelberg via GitGitGadget 7 participants 44 messages Mar 2, 2025 – Apr 8, 2025

lore
Share & tools
lore
#0 lore raw

cover completion: fix bugs with slashes in remote names

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

In addition, the change to __git_dwim_remote_heads() restricts it to only list remote branches, not HEAD. I think that actually improves the completion where it's used though, since HEAD doesn't seem to be valid anyway:

$ git switch HEAD fatal: a branch is expected, got 'HEAD'

David Mandelberg (3):
completion: add helper to escape strings for fnmatch
completion: add helper to count path components
completion: fix bugs with slashes in remote names

 contrib/completion/git-completion.bash |  40 ++++-
 t/t9902-completion.sh                  | 213 ++++++++++++++++++++++---
 2 files changed, 225 insertions(+), 28 deletions(-)

base-commit: cb0ae672aeabefca9704477ea8018ac94f523970
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1901%2Fdseomn%2Fcompletion-remote-slash-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1901/dseomn/completion-remote-slash-v1
Pull-Request: https://github.com/git/git/pull/1901

Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/3 completion: add helper to escape strings for fnmatch

From: David Mandelberg <[email protected]>

A follow-up commit will use this to escape parts of ref names for for-each-ref.

---
 contrib/completion/git-completion.bash | 12 ++++++++++++
 t/t9902-completion.sh                  |  7 +++++++
 2 files changed, 19 insertions(+)
contrib/completion/git-completion.bash +12 -0
index 413911be3be..cb10f818a81 100644
@@ -234,6 +234,18 @@ __git_dequote ()
 	done
 }
 
+# Escapes special characters in a string to pass to fnmatch(3)
+# 1: String to escape.
+__git_escape_fnmatch ()
+{
+	local s="$1"
+	s=${s//\\/\\\\}
+	s=${s//\?/\\\?}
+	s=${s//\*/\\\*}
+	s=${s//\[/\\\[}
+	printf '%s\n' "$s"
+}
+
 # The following function is based on code from:
 #
 #   bash_completion - programmable completion functions for bash 3.2+
t/t9902-completion.sh +7 -0
index 51bd7508376..c5e91622876 100755
@@ -455,6 +455,13 @@ test_expect_success '__git_dequote - open double quote' '
 '
 
 
+test_expect_success '__git_escape_fnmatch' '
+	echo '\''foo\\\?\*\['\'' >expected &&
+	__git_escape_fnmatch '\''foo\?*['\'' >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+
 test_expect_success '__gitcomp_direct - puts everything into COMPREPLY as-is' '
 	sed -e "s/Z$//g" >expected <<-EOF &&
 	with-trailing-space Z
Signature · 1 line
gitgitgadget
#2 lore raw

PATCH 2/3 completion: add helper to count path components

In reply to David Mandelberg via GitGitGadget

From: David Mandelberg <[email protected]>

A follow-up commit will use this with for-each-ref to strip the right number of path components from refnames.

---
 contrib/completion/git-completion.bash | 11 +++++++++++
 t/t9902-completion.sh                  | 26 ++++++++++++++++++++++++++
 2 files changed, 37 insertions(+)
contrib/completion/git-completion.bash +11 -0
index cb10f818a81..17c044f7d60 100644
@@ -246,6 +246,17 @@ __git_escape_fnmatch ()
 	printf '%s\n' "$s"
 }
 
+# Prints the number of slash-separated components in a path.
+# 1: Path to count components of.
+__git_count_path_components ()
+{
+	local path="$1"
+	local relative="${path#/}"
+	relative="${relative%/}"
+	local slashes="/${relative//[^\/]}"
+	echo "${#slashes}"
+}
+
 # The following function is based on code from:
 #
 #   bash_completion - programmable completion functions for bash 3.2+
t/t9902-completion.sh +26 -0
index c5e91622876..0219408358c 100755
@@ -462,6 +462,32 @@ test_expect_success '__git_escape_fnmatch' '
 '
 
 
+test_expect_success '__git_count_path_components - no slashes' '
+	echo 1 >expected &&
+	__git_count_path_components a >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+test_expect_success '__git_count_path_components - relative' '
+	echo 3 >expected &&
+	__git_count_path_components a/b/c >"$actual" &&
+	test_cmp expected "$actual"
+
+'
+
+test_expect_success '__git_count_path_components - absolute' '
+	echo 3 >expected &&
+	__git_count_path_components /a/b/c >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+test_expect_success '__git_count_path_components - trailing slash' '
+	echo 3 >expected &&
+	__git_count_path_components a/b/c/ >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+
 test_expect_success '__gitcomp_direct - puts everything into COMPREPLY as-is' '
 	sed -e "s/Z$//g" >expected <<-EOF &&
 	with-trailing-space Z
Signature · 1 line
gitgitgadget
#3 lore raw

PATCH 3/3 completion: fix bugs with slashes in remote names

In reply to David Mandelberg via GitGitGadget

From: David Mandelberg <[email protected]>

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

In addition, the change to __git_dwim_remote_heads() restricts it to only list remote branches, not HEAD. I think that actually improves the completion where it's used though, since HEAD doesn't seem to be valid anyway:

$ git switch HEAD
fatal: a branch is expected, got 'HEAD'
---
 contrib/completion/git-completion.bash |  17 ++-
 t/t9902-completion.sh                  | 180 ++++++++++++++++++++++---
 2 files changed, 169 insertions(+), 28 deletions(-)
contrib/completion/git-completion.bash +11 -6
index 17c044f7d60..5f4407be37f 100644
@@ -807,11 +807,15 @@ __git_dwim_remote_heads ()
 	# 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" \
-		${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
-		"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
-	uniq -u
+	local remote
+	for remote in $(__git_remotes); do
+		local base="$(__git_escape_fnmatch "refs/remotes/$remote")"
+		local strip="$(__git_count_path_components "$base")"
+		__git for-each-ref \
+			--format="$fer_pfx%(refname:strip=$strip)$sfx" \
+			${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
+			"$base/$cur_*" "$base/$cur_*/**"
+	done | sort | uniq -u
 }
 
 # Lists refs from the local (by default) or from a remote repository.
@@ -917,7 +921,8 @@ __git_refs ()
 			case "HEAD" in
 			$match*|$umatch*)	echo "${pfx}HEAD$sfx" ;;
 			esac
-			__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
+			local strip="$(__git_count_path_components "refs/remotes/$remote")"
+			__git for-each-ref --format="$fer_pfx%(refname:strip=$strip)$sfx" \
 				${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
 				"refs/remotes/$remote/$match*" \
 				"refs/remotes/$remote/$match*/**"
t/t9902-completion.sh +158 -22

577 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#4 lore raw

Hi David

Quoted 8 lines · David Mandelberg via GitGitGadget

From: David Mandelberg <[email protected]>

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Quoted 7 lines

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

This sounds like a useful improvement and I like the idea, but I think running "git for-each-ref" once for each remote is not going to scale very well for people who have a lot of remotes. I think it would be better to try and strip "refs/remote/$remote/" outside of "git for-each-ref". I've not tested it but I think something like

local sed_cmd=
local remote

# ref names and therefore remote names cannot contain '*?[]^' so we
# only need to escape '.$/'. Using 'sort -r' means that if there is a
# remote called "github" and another called "github/alice" we will try
# and strip "github/alice" first.
for remote in $(__git_remotes | sort -r)
do
	remote="${remote//./\\./}"
	remote="${remote//\$/\\\$/}"
	remote="${remote//\//\\\//}"
	sed_cmd="${sed_cmd} -e s/^refs\/remotes\/$remote\/// -e t"
done
__git for-each-ref --format="$fer_pfx$sfx" \
	${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
	"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" |
sed $sed_cmd | sort -u

should work and means we're only forking three extra processes no matter how many remotes the user has. I'm not sure if it changes the output order when GIT_COMPLETION_IGNORE_CASE is set though.

Best Wishes

Phillip

Quoted 688 lines

In addition, the change to __git_dwim_remote_heads() restricts it to only list remote branches, not HEAD. I think that actually improves the completion where it's used though, since HEAD doesn't seem to be valid anyway:

$ git switch HEAD
fatal: a branch is expected, got 'HEAD'
---
  contrib/completion/git-completion.bash |  17 ++-
  t/t9902-completion.sh                  | 180 ++++++++++++++++++++++---
  2 files changed, 169 insertions(+), 28 deletions(-)
contrib/completion/git-completion.bash +11 -6
index 17c044f7d60..5f4407be37f 100644
@@ -807,11 +807,15 @@ __git_dwim_remote_heads ()
  	# 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" \
-		${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
-		"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
-	uniq -u
+	local remote
+	for remote in $(__git_remotes); do
+		local base="$(__git_escape_fnmatch "refs/remotes/$remote")"
+		local strip="$(__git_count_path_components "$base")"
+		__git for-each-ref \
+			--format="$fer_pfx%(refname:strip=$strip)$sfx" \
+			${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
+			"$base/$cur_*" "$base/$cur_*/**"
+	done | sort | uniq -u
  }
 
  # Lists refs from the local (by default) or from a remote repository.
@@ -917,7 +921,8 @@ __git_refs ()
  			case "HEAD" in
  			$match*|$umatch*)	echo "${pfx}HEAD$sfx" ;;
  			esac
-			__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
+			local strip="$(__git_count_path_components "refs/remotes/$remote")"
+			__git for-each-ref --format="$fer_pfx%(refname:strip=$strip)$sfx" \
  				${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
  				"refs/remotes/$remote/$match*" \
  				"refs/remotes/$remote/$match*/**"
t/t9902-completion.sh +158 -22
index 0219408358c..0c41ef9d5b1 100755
@@ -149,7 +149,8 @@ fi
  test_expect_success 'setup for __git_find_repo_path/__gitdir tests' '
  	mkdir -p subdir/subsubdir &&
  	mkdir -p non-repo &&
-	git init -b main otherrepo
+	git init -b main otherrepo &&
+	git init -b main slashrepo
  '
 
  test_expect_success '__git_find_repo_path - from command line (through $__git_dir)' '
@@ -681,6 +682,13 @@ test_expect_success 'setup for ref completion' '
  	) &&
  	git remote add other "$ROOT/otherrepo/.git" &&
  	git fetch --no-tags other &&
+	(
+		cd slashrepo &&
+		git commit --allow-empty -m initial &&
+		git branch -m main branch/with/slash
+	) &&
+	git remote add remote/with/slash "$ROOT/slashrepo/.git" &&
+	git fetch --no-tags remote/with/slash &&
  	rm -f .git/FETCH_HEAD &&
  	git init thirdrepo
  '
@@ -693,6 +701,8 @@ test_expect_success '__git_refs - simple' '
  	other/HEAD
  	other/branch-in-other
  	other/main-in-other
+	remote/with/slash/HEAD
+	remote/with/slash/branch/with/slash
  	matching-tag
  	EOF
  	(
@@ -709,6 +719,8 @@ test_expect_success '__git_refs - full refs' '
  	refs/remotes/other/HEAD
  	refs/remotes/other/branch-in-other
  	refs/remotes/other/main-in-other
+	refs/remotes/remote/with/slash/HEAD
+	refs/remotes/remote/with/slash/branch/with/slash
  	refs/tags/matching-tag
  	EOF
  	(
@@ -774,6 +786,19 @@ test_expect_success '__git_refs - configured remote' '
  	test_cmp expected "$actual"
  '
 
+test_expect_success '__git_refs - configured remote - with slash' '
+	cat >expected <<-EOF &&
+	HEAD
+	HEAD
+	branch/with/slash
+	EOF
+	(
+		cur= &&
+		__git_refs remote/with/slash >"$actual"
+	) &&
+	test_cmp expected "$actual"
+'
+
  test_expect_success '__git_refs - configured remote - full refs' '
  	cat >expected <<-EOF &&
  	HEAD
@@ -916,17 +941,19 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
  	other/ambiguous
  	other/branch-in-other
  	other/main-in-other
-	remote/ambiguous
-	remote/branch-in-remote
+	remote/with/slash/HEAD
+	remote/with/slash/ambiguous
+	remote/with/slash/branch-in-remote
+	remote/with/slash/branch/with/slash
  	matching-tag
-	HEAD
  	branch-in-other
  	branch-in-remote
+	branch/with/slash
  	main-in-other
  	EOF
  	for remote_ref in refs/remotes/other/ambiguous \
-		refs/remotes/remote/ambiguous \
-		refs/remotes/remote/branch-in-remote
+		refs/remotes/remote/with/slash/ambiguous \
+		refs/remotes/remote/with/slash/branch-in-remote
  	do
  		git update-ref $remote_ref main &&
  		test_when_finished "git update-ref -d $remote_ref" || return 1
@@ -946,6 +973,8 @@ test_expect_success '__git_refs - after --opt=' '
  	other/HEAD
  	other/branch-in-other
  	other/main-in-other
+	remote/with/slash/HEAD
+	remote/with/slash/branch/with/slash
  	matching-tag
  	EOF
  	(
@@ -962,6 +991,8 @@ test_expect_success '__git_refs - after --opt= - full refs' '
  	refs/remotes/other/HEAD
  	refs/remotes/other/branch-in-other
  	refs/remotes/other/main-in-other
+	refs/remotes/remote/with/slash/HEAD
+	refs/remotes/remote/with/slash/branch/with/slash
  	refs/tags/matching-tag
  	EOF
  	(
@@ -979,6 +1010,8 @@ test_expect_success '__git refs - excluding refs' '
  	^other/HEAD
  	^other/branch-in-other
  	^other/main-in-other
+	^remote/with/slash/HEAD
+	^remote/with/slash/branch/with/slash
  	^matching-tag
  	EOF
  	(
@@ -995,6 +1028,8 @@ test_expect_success '__git refs - excluding full refs' '
  	^refs/remotes/other/HEAD
  	^refs/remotes/other/branch-in-other
  	^refs/remotes/other/main-in-other
+	^refs/remotes/remote/with/slash/HEAD
+	^refs/remotes/remote/with/slash/branch/with/slash
  	^refs/tags/matching-tag
  	EOF
  	(
@@ -1022,6 +1057,8 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
  	other/branch-in-other
  	other/main-in-other
  	other/matching/branch-in-other
+	remote/with/slash/HEAD
+	remote/with/slash/branch/with/slash
  	matching-tag
  	matching/tag
  	EOF
@@ -1142,6 +1179,8 @@ test_expect_success '__git_complete_refs - simple' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	matching-tag Z
  	EOF
  	(
@@ -1180,6 +1219,20 @@ test_expect_success '__git_complete_refs - remote' '
  	test_cmp expected out
  '
 
+test_expect_success '__git_complete_refs - remote - with slash' '
+	sed -e "s/Z$//" >expected <<-EOF &&
+	HEAD Z
+	HEAD Z
+	branch/with/slash Z
+	EOF
+	(
+		cur= &&
+		__git_complete_refs --remote=remote/with/slash &&
+		print_comp
+	) &&
+	test_cmp expected out
+'
+
  test_expect_success '__git_complete_refs - track' '
  	sed -e "s/Z$//" >expected <<-EOF &&
  	HEAD Z
@@ -1188,9 +1241,11 @@ test_expect_success '__git_complete_refs - track' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	matching-tag Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main-in-other Z
  	EOF
  	(
@@ -1235,6 +1290,8 @@ test_expect_success '__git_complete_refs - suffix' '
  	other/HEAD.
  	other/branch-in-other.
  	other/main-in-other.
+	remote/with/slash/HEAD.
+	remote/with/slash/branch/with/slash.
  	matching-tag.
  	EOF
  	(
@@ -1260,6 +1317,20 @@ test_expect_success '__git_complete_fetch_refspecs - simple' '
  	test_cmp expected out
  '
 
+test_expect_success '__git_complete_fetch_refspecs - with slash' '
+	sed -e "s/Z$//" >expected <<-EOF &&
+	HEAD:HEAD Z
+	HEAD:HEAD Z
+	branch/with/slash:branch/with/slash Z
+	EOF
+	(
+		cur= &&
+		__git_complete_fetch_refspecs remote/with/slash &&
+		print_comp
+	) &&
+	test_cmp expected out
+'
+
  test_expect_success '__git_complete_fetch_refspecs - matching' '
  	sed -e "s/Z$//" >expected <<-EOF &&
  	branch-in-other:branch-in-other Z
@@ -1340,8 +1411,8 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
  test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
  	test_completion "git switch " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1487,8 +1558,8 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
  test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
  	test_completion "git checkout " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1496,6 +1567,8 @@ test_expect_success 'git checkout - completes refs and unique remote branches fo
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1515,8 +1588,8 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
  test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
  	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1525,8 +1598,8 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
  test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
  	test_completion "git switch --no-guess --guess " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1549,14 +1622,16 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
  test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
  	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1564,6 +1639,8 @@ test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1,
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1576,14 +1653,16 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
  test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
  	test_completion "git checkout --no-guess --guess " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1591,6 +1670,8 @@ test_expect_success 'git checkout - a later --guess overrides previous --no-gues
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1603,6 +1684,8 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1616,6 +1699,8 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1623,8 +1708,8 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
  	test_config checkout.guess true &&
  	test_completion "git checkout " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1632,6 +1717,8 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1639,8 +1726,8 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
  	test_config checkout.guess false &&
  	test_completion "git checkout --guess " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1648,6 +1735,8 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1661,6 +1750,8 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1673,6 +1764,8 @@ test_expect_success 'git switch - with --detach, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1685,6 +1778,8 @@ test_expect_success 'git checkout - with --detach, complete only references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1857,6 +1952,8 @@ test_expect_success 'git switch - with -d, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1869,6 +1966,8 @@ test_expect_success 'git checkout - with -d, complete only references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1877,11 +1976,15 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  	test_completion "git switch -t " <<-\EOF
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1890,11 +1993,15 @@ test_expect_success 'git checkout - with --track, complete only remote branches'
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  	test_completion "git checkout -t " <<-\EOF
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1914,6 +2021,8 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1926,6 +2035,8 @@ test_expect_success 'git switch - with -c, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1938,6 +2049,8 @@ test_expect_success 'git switch - with -C, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1950,6 +2063,8 @@ test_expect_success 'git switch - with -c and --track, complete all references'
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1962,6 +2077,8 @@ test_expect_success 'git switch - with -C and --track, complete all references'
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1974,6 +2091,8 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1986,6 +2105,8 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1998,6 +2119,8 @@ test_expect_success 'git checkout - with -b, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2010,6 +2133,8 @@ test_expect_success 'git checkout - with -B, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2022,6 +2147,8 @@ test_expect_success 'git checkout - with -b and --track, complete all references
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2034,6 +2161,8 @@ test_expect_success 'git checkout - with -B and --track, complete all references
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2046,6 +2175,8 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2058,13 +2189,15 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
  test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
  	test_completion "git switch -c " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2073,8 +2206,8 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
  test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
  	test_completion "git switch -C " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2111,8 +2244,8 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
  test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
  	test_completion "git checkout -b " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2121,8 +2254,8 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
  test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
  	test_completion "git checkout -B " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2159,8 +2292,8 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
  test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
  	test_completion "git switch --orphan " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2175,8 +2308,8 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
  test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
  	test_completion "git checkout --orphan " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2192,6 +2325,8 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2206,7 +2341,8 @@ test_expect_success 'git restore completes modified files' '
  test_expect_success 'teardown after ref completion' '
  	git branch -d matching-branch &&
  	git tag -d matching-tag &&
-	git remote remove other
+	git remote remove other &&
+	git remote remove remote/with/slash
  '
 
 
#5 lore raw

Op 2025-03-02 om 09:17 schreef Phillip Wood:

Quoted 13 lines

Hi David

Quoted 10 lines · David Mandelberg via GitGitGadget

From: David Mandelberg <[email protected]>

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Result in:

Quoted 7 lines
Quoted 5 lines

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

This sounds like a useful improvement and I like the idea, but I think running "git for-each-ref" once for each remote is not going to scale very well for people who have a lot of remotes. I think it would be better to try and strip "refs/remote/$remote/" outside of "git for-each- ref". I've not tested it but I think something like

Good point, I hadn't thought of that. Do you have a rough estimate of what "a lot of remotes" is? 100ish, maybe? I'd like to do some testing to get actual performance numbers before trying to optimize this, because I think the optimization has some drawbacks, see below.

If optimization is needed, another approach is to parallelize the forks:

{
	local fer_pids=
	for ...
	do
		__git for-each-ref ... &
		fer_pids="$fer_pids $!"
	done
	test -z "$fer_pids" || wait $fer_pids
} | sort | uniq -u

That might cause spikes in cpu/memory/disk usage that aren't ideal though.

Quoted 11 lines

local sed_cmd=
local remote

# ref names and therefore remote names cannot contain '*?[]^' so we
# only need to escape '.$/'. Using 'sort -r' means that if there is a
# remote called "github" and another called "github/alice" we will try
# and strip "github/alice" first.
for remote in $(__git_remotes | sort -r)
do
     remote="${remote//./\\./}"
     remote="${remote//\$/\\\$/}"
     remote="${remote//\//\\\//}"

Just FYI since it took me hours to figure this out myself: I think this would break tests on macos because of an old version of bash that handles backslashes weirdly. I think removing the double quotes would work around that issue, and be safe because word splitting doesn't happen in assignments.

Quoted 2 lines

    sed_cmd="${sed_cmd} -e s/^refs\/remotes\/$remote\/// -e t"
done

Mostly just a note to myself if I end up using this idea: I think a space in $remote would break this because bash would split up the arg to sed. There's probably some way to fix that with extra escaping though?

Quoted 3 lines

__git for-each-ref --format="$fer_pfx$sfx" \
    ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
    "refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" |

This would search for $cur_ in the wrong place because * only matches one path component, right? Changing to ** might help, but then it would match in places more it shouldn't.

Quoted 7 lines

sed $sed_cmd | sort -u

should work and means we're only forking three extra processes no matter how many remotes the user has. I'm not sure if it changes the output order when GIT_COMPLETION_IGNORE_CASE is set though.

Best Wishes

Phillip

#6 lore raw

Hi David

Quoted 7 lines · David Mandelberg

Op 2025-03-02 om 09:17 schreef Phillip Wood:

Quoted 4 lines · David Mandelberg via GitGitGadget
Quoted 2 lines

From: David Mandelberg <[email protected]>

This sounds like a useful improvement and I like the idea, but I think running "git for-each-ref" once for each remote is not going to scale very well for people who have a lot of remotes. I think it would be better to try and strip "refs/remote/$remote/" outside of "git for- each- ref". I've not tested it but I think something like

Good point, I hadn't thought of that. Do you have a rough estimate of what "a lot of remotes" is? 100ish, maybe?

I'm not really sure what a "large" number looks like for people who track a lot of repositories. On windows forking is pretty slow (our shell-based test suite is incredibly slow on that platform) so I think it makes sense to try and avoid adding new processes.

Quoted 14 lines

I'd like to do some testing to get actual performance numbers before trying to optimize this, because I think the optimization has some drawbacks, see below.

If optimization is needed, another approach is to parallelize the forks:

{
    local fer_pids=
    for ...
    do
        __git for-each-ref ... &
        fer_pids="$fer_pids $!"
    done
    test -z "$fer_pids" || wait $fer_pids
} | sort | uniq -u

Quoted 1 line

That might cause spikes in cpu/memory/disk usage that aren't ideal though.

Yes, if there were a 100 remotes that's a bit of a fork-bomb.

Quoted 12 lines
Quoted 11 lines

local sed_cmd=
local remote

# ref names and therefore remote names cannot contain '*?[]^' so we
# only need to escape '.$/'. Using 'sort -r' means that if there is a
# remote called "github" and another called "github/alice" we will try
# and strip "github/alice" first.
for remote in $(__git_remotes | sort -r)
do
     remote="${remote//./\\./}"
     remote="${remote//\$/\\\$/}"
     remote="${remote//\//\\\//}"
Quoted 1 line

Just FYI since it took me hours to figure this out myself: I think this would break tests on macos because of an old version of bash that handles backslashes weirdly. I think removing the double quotes would work around that issue, and be safe because word splitting doesn't happen in assignments.

Thanks, I'm not familiar with bash's extensions to parameter substitution. The completions can also but used under zsh (git-completion.zsh is pretty much abandoned I think) but it looks like bash and zsh agree on this expansion.

Quoted 4 lines
Quoted 2 lines

     sed_cmd="${sed_cmd} -e s/^refs\/remotes\/$remote\/// -e t" done

Mostly just a note to myself if I end up using this idea: I think a space in $remote would break this because bash would split up the arg to sed. There's probably some way to fix that with extra escaping though?

I should have said in the comment above that ref names cannot contain space, tab, newline etc. so we don't have to worry about the shell's word splitting splitting the patterns. They cannot contain backslashes either.

Quoted 5 lines
Quoted 3 lines

__git for-each-ref --format="$fer_pfx$sfx" \
     ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
     "refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" |

This would search for $cur_ in the wrong place because * only matches one path component, right? Changing to ** might help, but then it would match in places more it shouldn't.

Oh, good point I'd missed that. Could we change sed to run with '-n' and generate an expression that looks like

    -e s|^refs/remotes/$remote/\($cur_\)|\1|p -e t

with the appropriate escaping of $remote and $cur_ so that we only print the output of for-each-ref that matches $cur_?

Best Wishes

Phillip

Quoted 8 lines
Quoted 7 lines

sed $sed_cmd | sort -u

should work and means we're only forking three extra processes no matter how many remotes the user has. I'm not sure if it changes the output order when GIT_COMPLETION_IGNORE_CASE is set though.

Best Wishes

Phillip

#7 lore raw

Op 2025-03-03 om 11:36 schreef [email protected]:

Quoted 1 line

I'm not really sure what a "large" number looks like for people who track a lot of repositories. On windows forking is pretty slow (our shell-based test suite is incredibly slow on that platform) so I think it makes sense to try and avoid adding new processes.

Thanks, I didn't know about forking being slow on Windows.

Do you know if we need to worry about hitting the max number or length of arguments to a command? https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/limits.h.html under _POSIX_ARG_MAX says that at least 4KiB should be supported, which seems low enough to maybe cause trouble, but it's a minimum. https://www.in-ulm.de/~mascheck/various/argmax/ lists a bunch of values for different platforms, and from a brief scan it looks like all the remotely-modern platforms have values that are probably large enough for this.

Quoted 14 lines
Quoted 14 lines

I'd like to do some testing to get actual performance numbers before trying to optimize this, because I think the optimization has some drawbacks, see below.

If optimization is needed, another approach is to parallelize the forks:

{
     local fer_pids=
     for ...
     do
         __git for-each-ref ... &
         fer_pids="$fer_pids $!"
     done
     test -z "$fer_pids" || wait $fer_pids
} | sort | uniq -u

Quoted 3 lines
Quoted 1 line

That might cause spikes in cpu/memory/disk usage that aren't ideal though.

Yes, if there were a 100 remotes that's a bit of a fork-bomb.

I also realized after sending that email that it could run into issues with the number of available PIDs too. It could be improved by limiting to n child processes at a time and waiting for one to finish before starting the next, but that's more complication. I think your solution below is better.

Quoted 12 lines
Quoted 12 lines
Quoted 11 lines

local sed_cmd=
local remote

# ref names and therefore remote names cannot contain '*?[]^' so we
# only need to escape '.$/'. Using 'sort -r' means that if there is a
# remote called "github" and another called "github/alice" we will try
# and strip "github/alice" first.
for remote in $(__git_remotes | sort -r)
do
     remote="${remote//./\\./}"
     remote="${remote//\$/\\\$/}"
     remote="${remote//\//\\\//}"
Quoted 3 lines
Quoted 1 line

Just FYI since it took me hours to figure this out myself: I think this would break tests on macos because of an old version of bash that handles backslashes weirdly. I think removing the double quotes would work around that issue, and be safe because word splitting doesn't happen in assignments.

Thanks, I'm not familiar with bash's extensions to parameter substitution. The completions can also but used under zsh (git- completion.zsh is pretty much abandoned I think) but it looks like bash and zsh agree on this expansion.

The tests don't automatically run under zsh though, right? Maybe I should try to figure out how to do that in a separate patch...

Quoted 6 lines
Quoted 4 lines
Quoted 2 lines

     sed_cmd="${sed_cmd} -e s/^refs\/remotes\/$remote\/// -e t" done

Mostly just a note to myself if I end up using this idea: I think a space in $remote would break this because bash would split up the arg to sed. There's probably some way to fix that with extra escaping though?

I should have said in the comment above that ref names cannot contain space, tab, newline etc. so we don't have to worry about the shell's word splitting splitting the patterns. They cannot contain backslashes either.

Good to know. Is https://git-scm.com/docs/git-check-ref-format the place I should have read to understand what escaping is needed and what isn't because of the limitations on ref names?

Quoted 11 lines
Quoted 5 lines
Quoted 3 lines

__git for-each-ref --format="$fer_pfx$sfx" \
     ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
     "refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" |

This would search for $cur_ in the wrong place because * only matches one path component, right? Changing to ** might help, but then it would match in places more it shouldn't.

Oh, good point I'd missed that. Could we change sed to run with '-n' and generate an expression that looks like

    -e s|^refs/remotes/$remote/\($cur_\)|\1|p -e t

with the appropriate escaping of $remote and $cur_ so that we only print the output of for-each-ref that matches $cur_?

I think that would work. I was hoping to avoid sed, and I was thinking about using shell's ${parameter#word} to strip and test for matching prefixes. But I can't think of a way to do that with GIT_COMPLETION_IGNORE_CASE. Modern bash has ${parameter@L} to lowercase a string, but I don't think the version of bash on macos has that. With sed it would just be a matter of adding the i flag if needed.

I don't think this is an issue, but do we need to worry about putting remote names and $cur_ on the command line where any other user on the same system can read them? $cur_ was already on the command line before this patch, and remote names appear on other git command lines all the time, so I doubt they're meant to be secret.

Is a Helped-by trailer the right way to credit you for your suggestions on this?

#8 lore raw
Quoted 6 lines · David Mandelberg

I think that would work. I was hoping to avoid sed, and I was thinking about using shell's ${parameter#word} to strip and test for matching prefixes. But I can't think of a way to do that with GIT_COMPLETION_IGNORE_CASE. Modern bash has ${parameter@L} to lowercase a string, but I don't think the version of bash on macos has that. With sed it would just be a matter of adding the i flag if needed.

I haven't followed this discussion closely, but you can also use ${parameter,,} to do parameter case modification.

Sadly, it looks like those were added in bash-4.0, so Apple will have kept their users from benefiting from that to avoid GPLv3 code. :/

I don't have an ancient bash-3.x like MacOS to test, but maybe they've backported some features?

Though if you do end up using this, ${parameter,,} will work in a much wider range of bash version that ${parameter@L} which covers various releases in the OS's we aim to target (e.g. RHEL 8.x, which has bash-4.4).

Signature · 1 line
Todd
#9 lore raw
Quoted 12 lines · Todd Zullinger
Quoted 6 lines · David Mandelberg

I think that would work. I was hoping to avoid sed, and I was thinking about using shell's ${parameter#word} to strip and test for matching prefixes. But I can't think of a way to do that with GIT_COMPLETION_IGNORE_CASE. Modern bash has ${parameter@L} to lowercase a string, but I don't think the version of bash on macos has that. With sed it would just be a matter of adding the i flag if needed.

I haven't followed this discussion closely, but you can also use ${parameter,,} to do parameter case modification.

Quoted 6 lines

Sadly, it looks like those were added in bash-4.0, so Apple will have kept their users from benefiting from that to avoid GPLv3 code. :/

I don't have an ancient bash-3.x like MacOS to test, but maybe they've backported some features?

I can confirm Apple's /bin/bash doesn't have ${x,,}

Quoted 9 lines

Though if you do end up using this, ${parameter,,} will work in a much wider range of bash version that ${parameter@L} which covers various releases in the OS's we aim to target (e.g. RHEL 8.x, which has bash-4.4).

--
Todd

Signature · 1 line
D. Ben Knoble
#10 lore raw
In reply to David Mandelberg
Quoted 24 lines · David Mandelberg

Op 2025-03-03 om 11:36 schreef [email protected]:

Quoted 22 lines
Quoted 12 lines
Quoted 11 lines

local sed_cmd=
local remote

# ref names and therefore remote names cannot contain '*?[]^' so we
# only need to escape '.$/'. Using 'sort -r' means that if there is a
# remote called "github" and another called "github/alice" we will try
# and strip "github/alice" first.
for remote in $(__git_remotes | sort -r)
do
     remote="${remote//./\\./}"
     remote="${remote//\$/\\\$/}"
     remote="${remote//\//\\\//}"
Quoted 5 lines

Just FYI since it took me hours to figure this out myself: I think this would break tests on macos because of an old version of bash that handles backslashes weirdly. I think removing the double quotes would work around that issue, and be safe because word splitting doesn't happen in assignments.

Thanks, I'm not familiar with bash's extensions to parameter substitution. The completions can also but used under zsh (git- completion.zsh is pretty much abandoned I think) but it looks like bash and zsh agree on this expansion.

I don't think "abandoned" is the right characterization—at least, it's the completion script that Homebrew-installed Git users will get for Zsh, and it does re-use the Bash completion scripts (which therefore ought to stay portable between both if possible, otherwise we get stuff like 8776470cf3 (completion: repair config completion for Zsh, 2025-01-06)). The Zsh script has seen some fixups over time (such as my own 3c20acdf46 (completion: zsh: stop leaking local cache variable, 2024-04-30)).

Quoted 3 lines

The tests don't automatically run under zsh though, right? Maybe I should try to figure out how to do that in a separate patch...

Correct—which is how bugs sneak in ;)

I'm willing to manually test the patch if I can understand how to reproduce the issue—it sounds like having a remote name with a slash is sufficient?

I started trying to test Zsh completion a while back when working on one of the patches mentioned above; I got as far as this hack [1], which is to say, not very far at all.

[1]: https://github.com/git/git/commit/d8918195f18a503aa1a42fed3c66a0af8d04131f

--
D. Ben Knoble

#11 lore raw
Quoted 7 lines · D. Ben Knoble
Quoted 5 lines · David Mandelberg
Quoted 5 lines

Thanks, I'm not familiar with bash's extensions to parameter substitution. The completions can also but used under zsh (git- completion.zsh is pretty much abandoned I think) but it looks like bash and zsh agree on this expansion.

Quoted 8 lines

I don't think "abandoned" is the right characterization—at least, it's the completion script that Homebrew-installed Git users will get for Zsh, and it does re-use the Bash completion scripts (which therefore ought to stay portable between both if possible, otherwise we get stuff like 8776470cf3 (completion: repair config completion for Zsh, 2025-01-06)). The Zsh script has seen some fixups over time (such as my own 3c20acdf46 (completion: zsh: stop leaking local cache variable, 2024-04-30)).

Thanks for clarifying.

Quoted 5 lines
Quoted 2 lines

The tests don't automatically run under zsh though, right? Maybe I should try to figure out how to do that in a separate patch...

Correct—which is how bugs sneak in ;)

Quoted 7 lines

I'm willing to manually test the patch if I can understand how to reproduce the issue—it sounds like having a remote name with a slash is sufficient?

I started trying to test Zsh completion a while back when working on one of the patches mentioned above; I got as far as this hack [1], which is to say, not very far at all.

Thanks.

#12 lore raw
In reply to D. Ben Knoble

Hi Ben

Quoted 8 lines · D. Ben Knoble
Quoted 6 lines · David Mandelberg

Op 2025-03-03 om 11:36 schreef [email protected]:

Quoted 4 lines

Thanks, I'm not familiar with bash's extensions to parameter substitution. The completions can also but used under zsh (git- completion.zsh is pretty much abandoned I think) but it looks like bash and zsh agree on this expansion.

Quoted 8 lines

I don't think "abandoned" is the right characterization—at least, it's the completion script that Homebrew-installed Git users will get for Zsh, and it does re-use the Bash completion scripts (which therefore ought to stay portable between both if possible, otherwise we get stuff like 8776470cf3 (completion: repair config completion for Zsh, 2025-01-06)). The Zsh script has seen some fixups over time (such as my own 3c20acdf46 (completion: zsh: stop leaking local cache variable, 2024-04-30)).

Thanks for clarifying that, I'd misunderstood the relationship between git-completion.zsh and git-completion.bash.

Best Wishes

Phillip

#13 lore raw
In reply to D. Ben Knoble

Op 2025-03-06 om 15:24 schreef D. Ben Knoble:

Quoted 3 lines

I'm willing to manually test the patch if I can understand how to reproduce the issue—it sounds like having a remote name with a slash is sufficient?

Yup. I was able to reproduce it with these commands:

/tmp/tmp.zOjfmdMx1i$ git init foo
Initialized empty Git repository in /tmp/tmp.zOjfmdMx1i/foo/.git/ /tmp/tmp.zOjfmdMx1i$ cd foo
/tmp/tmp.zOjfmdMx1i/foo$ git remote add with/slash /path/does/not/matter /tmp/tmp.zOjfmdMx1i/foo$ git commit -m 'test' --allow-empty [main (root-commit) 4b95a99] test
/tmp/tmp.zOjfmdMx1i/foo$ git update-ref refs/remotes/with/slash/main 4b95a99

Then type:

git push with/slash :ma<Tab>

#14 lore raw
Quoted 16 lines · David Mandelberg

Op 2025-03-06 om 15:24 schreef D. Ben Knoble:

Quoted 3 lines

I'm willing to manually test the patch if I can understand how to reproduce the issue—it sounds like having a remote name with a slash is sufficient?

Yup. I was able to reproduce it with these commands:

/tmp/tmp.zOjfmdMx1i$ git init foo
Initialized empty Git repository in /tmp/tmp.zOjfmdMx1i/foo/.git/ /tmp/tmp.zOjfmdMx1i$ cd foo
/tmp/tmp.zOjfmdMx1i/foo$ git remote add with/slash /path/does/not/matter /tmp/tmp.zOjfmdMx1i/foo$ git commit -m 'test' --allow-empty [main (root-commit) 4b95a99] test
/tmp/tmp.zOjfmdMx1i/foo$ git update-ref refs/remotes/with/slash/main 4b95a99

Quoted 3 lines

Then type:

git push with/slash :ma<Tab>

Thanks; in Zsh the results after :<Tab> are HEAD and main. After :ma<Tab>, just main. The results with _no_ colon are HEAD, main, with/slash/main. I think that's all the expected results?

My test procedure was to

git fetch <your PR tip>
mv /usr/local/share/zsh/site-functions/git-completion.bash{,.bak} git show FETCH_HEAD:contrib/completion/git-completion.bash > /usr/local/share/zsh/site-functions/git-completion.bash

# adjust startup files to use `compinit -D` to avoid caches
<your test procedure>
Signature · 1 line
D. Ben Knoble
#15 lore raw

Op 2025-03-13 om 13:40 schreef D. Ben Knoble:

Quoted 17 lines · David Mandelberg
Quoted 16 lines

Op 2025-03-06 om 15:24 schreef D. Ben Knoble:

Quoted 3 lines

I'm willing to manually test the patch if I can understand how to reproduce the issue—it sounds like having a remote name with a slash is sufficient?

Yup. I was able to reproduce it with these commands:

/tmp/tmp.zOjfmdMx1i$ git init foo
Initialized empty Git repository in /tmp/tmp.zOjfmdMx1i/foo/.git/ /tmp/tmp.zOjfmdMx1i$ cd foo
/tmp/tmp.zOjfmdMx1i/foo$ git remote add with/slash /path/does/not/matter /tmp/tmp.zOjfmdMx1i/foo$ git commit -m 'test' --allow-empty [main (root-commit) 4b95a99] test
/tmp/tmp.zOjfmdMx1i/foo$ git update-ref refs/remotes/with/slash/main 4b95a99

Quoted 7 lines
Quoted 3 lines

Then type:

git push with/slash :ma<Tab>

Thanks; in Zsh the results after :<Tab> are HEAD and main. After :ma<Tab>, just main. The results with _no_ colon are HEAD, main, with/slash/main. I think that's all the expected results?

That sounds right to me, thanks for testing it!

#16 lore raw
In reply to [email protected]
Quoted 11 lines · [email protected] writes:
Quoted 10 lines

    local fer_pids=
    for ...
    do
        __git for-each-ref ... &
        fer_pids="$fer_pids $!"
    done
    test -z "$fer_pids" || wait $fer_pids
} | sort | uniq -u
That might cause spikes in cpu/memory/disk usage that aren't ideal though.

Quoted 1 line

Yes, if there were a 100 remotes that's a bit of a fork-bomb.

And they are all competing for a shared common resource for read access.

I wonder if a single invocation of for-each-ref for refs/remotes/ hierarchy would be sufficient? That is, the calling shell script can set up a shell function "foo" and then

    eval $(for-each-ref --format='foo %(refname)' --shell refs/remotes/)

and "foo" can do its thing avoiding as much forks as possible. As you are allowed bash-ism in completion script, you do not have to limit yourself to POSIX parameter substitution but can use substring expansion ${parameter:offset:length} and pattern substitution ${parameter/pattern/string} and other kind of bash exotics, as well as shell arrays (e.g., define an array __git__remote_branches[] for each remote, which may contain the remote-tracking branches for the remote).

#17 lore raw
In reply to David Mandelberg
Quoted 20 lines · David Mandelberg

Op 2025-03-02 om 09:17 schreef Phillip Wood:

Quoted 19 lines

Hi David

Quoted 15 lines · David Mandelberg via GitGitGadget

From: David Mandelberg <[email protected]>

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

Quoted 8 lines
Quoted 5 lines

This sounds like a useful improvement and I like the idea, but I think running "git for-each-ref" once for each remote is not going to scale very well for people who have a lot of remotes. I think it would be better to try and strip "refs/remote/$remote/" outside of "git for-each- ref". I've not tested it but I think something like

Good point, I hadn't thought of that. Do you have a rough estimate of what "a lot of remotes" is? 100ish, maybe?

5.

In Git for Windows fork()-ing subshells and fork()+exec()-ing processes is rather costly, about an order of magnitude slower than on Linux. The rough equivalent of the body of your loop, with two subshells and a git process:

  time { a=$(echo 1) ; b=$(echo 2) ; git for-each-ref >/dev/null ; }

takes on average 0.17s on a windows box I have access to (with fully packed refs, and merely 4 refs in total). So guess at about 4-5 remotes it would take over a second to react to my TABs...

I would rather try to go in the opposite direction to see whether 'git for-each-ref' could be taught to strip the "refs/remote/$remote/" prefix with a format specifier option like '%(refname:strip=remote)'.

That would surely be faster than any shell filtering we might come up with, and would also save us from the trouble of escaping glob and/or regex metacharacters for shell/sed pattern matching.

Quoted 16 lines

I'd like to do some testing to get
actual performance numbers before trying to optimize this, because I think the optimization has some drawbacks, see below.

If optimization is needed, another approach is to parallelize the forks:

{
	local fer_pids=
	for ...
	do
		__git for-each-ref ... &
		fer_pids="$fer_pids $!"
	done
	test -z "$fer_pids" || wait $fer_pids
} | sort | uniq -u
Quoted 1 line

That might cause spikes in cpu/memory/disk usage that aren't ideal though.

Please don't do this, the completion script must not forkbomb the system.

#18 lore raw

Op 2025-03-03 om 14:33 schreef SZEDER Gábor:

Quoted 11 lines · David Mandelberg
Quoted 2 lines

Good point, I hadn't thought of that. Do you have a rough estimate of what "a lot of remotes" is? 100ish, maybe?

5.

In Git for Windows fork()-ing subshells and fork()+exec()-ing processes is rather costly, about an order of magnitude slower than on Linux. The rough equivalent of the body of your loop, with two subshells and a git process:

Quoted 5 lines
   time { a=$(echo 1) ; b=$(echo 2) ; git for-each-ref >/dev/null ; }

takes on average 0.17s on a windows box I have access to (with fully packed refs, and merely 4 refs in total). So guess at about 4-5 remotes it would take over a second to react to my TABs...

Oh wow, that's much slower than I expected, thanks for the numbers!

Quoted 7 lines

I would rather try to go in the opposite direction to see whether 'git for-each-ref' could be taught to strip the "refs/remote/$remote/" prefix with a format specifier option like '%(refname:strip=remote)'.

That would surely be faster than any shell filtering we might come up with, and would also save us from the trouble of escaping glob and/or regex metacharacters for shell/sed pattern matching.

That doesn't fix the issue of needing to anchor the $cur_ part of the pattern after each refs/remote/$remote/ prefix. It might be possible to use something like %(refname:strip=remote) to find all remote branches with the remote stripped, then filter them for $cur_ in the shell or with sed though.

What are the expectations around mixing versions of git itself and files in contrib? Can a single patch series add a feature to for-each-ref and use it in git-completion.bash, or does the feature in for-each-ref need to exist for some time before git-completion.bash can use it?

#19 lore raw
In reply to David Mandelberg via GitGitGadget
Quoted 7 lines · David Mandelberg via GitGitGadget

This made completions like:

git push github/dseomn :com<Tab>

Result in:

git push github/dseomn :dseomn/completion-remote-slash

Apparently I am late to the party, but I have to wonder how prevalent it is to use such a name for remotes.

Back in 2005 when I designed the remote namespaces and remote-tracking branches, the code may have been loose not to complain, but it certainly was not an intended way the remote names and remote-tracking hiearchies were to be used. Being able to say "refs/remote/*/master" and learn where everybody's 'master' branch points at with a single globbing was a powerful thing, but with refs/remotes/github/dseomn/master thrown into the mix, that would break down miserably. As a remote name, we still do use "is that a file on the filesystem?" to see if it is a local file:// URL (i.e. "git fetch github/dseomn" may be fetching from a subdirectory two levels down); in retrospect, we might have been better off if we said "well with a slash it must be a local directory and not remote nickname" to prevent such usage pattern.

Anyway, this is not something we'd do lightly, but we may want to see if we should tighten the naming rules for remote nicknames and the mapping from nicknames to remote-tracking hiearchy, at Git 3.0 version boundary.

#20 lore raw

Op 2025-03-06 om 15:46 schreef Junio C Hamano:

Quoted 2 lines

Apparently I am late to the party, but I have to wonder how prevalent it is to use such a name for remotes.

From a quick search, it looks like I'm not the only one, but I don't know how common it is: https://github.com/desktop/desktop/issues/3618

I'm not sure if I'm remembering right, but I think I got the idea from seeing slashes in branch names to organize a hierarchy of branches, and realizing I could do the same with remotes. Was it intentional to allow slash in branch names, or is that a similar situation?

Quoted 4 lines

As a remote name, we still do use "is that a
file on the filesystem?" to see if it is a local file:// URL (i.e. "git fetch github/dseomn" may be fetching from a subdirectory two levels down);

That's a good reason not to use slashes in remotes that I didn't know about, thanks!

Quoted 4 lines

Anyway, this is not something we'd do lightly, but we may want to see if we should tighten the naming rules for remote nicknames and the mapping from nicknames to remote-tracking hiearchy, at Git 3.0 version boundary.

What does that mean for this patch now? Do you want to fix the completion for the currently (unintentionally) supported remote names, or leave it broken to discourage their use? Fixing the completion might also give anybody reading the completion script the idea that slashes in remote names are somewhat sanctioned.

#21 lore raw
Quoted 4 lines · David Mandelberg

I'm not sure if I'm remembering right, but I think I got the idea from seeing slashes in branch names to organize a hierarchy of branches, and realizing I could do the same with remotes. Was it intentional to allow slash in branch names, or is that a similar situation?

Reference names were hierarchical pretty much from the beginning.

Quoted 10 lines
Quoted 4 lines

Anyway, this is not something we'd do lightly, but we may want to see if we should tighten the naming rules for remote nicknames and the mapping from nicknames to remote-tracking hiearchy, at Git 3.0 version boundary.

What does that mean for this patch now? Do you want to fix the completion for the currently (unintentionally) supported remote names, or leave it broken to discourage their use? Fixing the completion might also give anybody reading the completion script the idea that slashes in remote names are somewhat sanctioned.

It probably would not mean much either way, and certainly not something we should worry about during pre-release feature freeze.

The "fix" may take quite different shapes. Remote nick/name may still be allowed but the default fetch refspec left by "git clone" would start using the refs/remotes/nick-name/ hierarchy instead of the refs/remotes/nick/name hierarchy, for example, but as long as this round of completion fixes properly read the configuration to learn the remote-tracking hierarchy by reading the configured value of "remote.nick/name.fetch", such a change would not break the code in the completion script at all. Or remote nick/name may outright banned and people are encouraged to use nick-name instead, in which case even if the completion code is prepared to accept remote names with slashes in it, as long as it still works correctly with remote names with no slashes in it, it will keep working in such a future.

Or the "fix" may be to declare "there is nothing to fix, if people want to use

[remote "nick/name"] fetch = +refs/heads/*:refs/remotes/nickname/* [remote "nocknyme"] fetch = +refs/heads/*:refs/remotes/nock/nyme/*

that's their choice, and the completion code would do the right thing anyway".

I didn't read your patches, so the situation may be different and with any shape of "fix", or without any "fix", there might still be need for further polishing. But hopefully you got the idea. For this particular patch, there is nothing urgently needed. Well, we are right now in the pre-release feature freeze anyway ;-).

Thanks.

#22 lore raw

Op 2025-03-07 om 15:57 schreef Junio C Hamano:

Quoted 25 lines

It probably would not mean much either way, and certainly not something we should worry about during pre-release feature freeze.

The "fix" may take quite different shapes. Remote nick/name may still be allowed but the default fetch refspec left by "git clone" would start using the refs/remotes/nick-name/ hierarchy instead of the refs/remotes/nick/name hierarchy, for example, but as long as this round of completion fixes properly read the configuration to learn the remote-tracking hierarchy by reading the configured value of "remote.nick/name.fetch", such a change would not break the code in the completion script at all. Or remote nick/name may outright banned and people are encouraged to use nick-name instead, in which case even if the completion code is prepared to accept remote names with slashes in it, as long as it still works correctly with remote names with no slashes in it, it will keep working in such a future.

Or the "fix" may be to declare "there is nothing to fix, if people want to use

[remote "nick/name"] fetch = +refs/heads/*:refs/remotes/nickname/* [remote "nocknyme"] fetch = +refs/heads/*:refs/remotes/nock/nyme/*

that's their choice, and the completion code would do the right thing anyway".

Quoted 7 lines

I didn't read your patches, so the situation may be different and with any shape of "fix", or without any "fix", there might still be need for further polishing. But hopefully you got the idea. For this particular patch, there is nothing urgently needed. Well, we are right now in the pre-release feature freeze anyway ;-).

Thanks.

Thanks for the explanation!

Quoted 4 lines

As a remote name, we still do use "is that a
file on the filesystem?" to see if it is a local file:// URL (i.e. "git fetch github/dseomn" may be fetching from a subdirectory two levels down)

Btw, I just tested it, and I think this is an issue for remote names without slashes too:

/tmp/tmp.sVAhhZuEzA$ git init foo
Initialized empty Git repository in /tmp/tmp.sVAhhZuEzA/foo/.git/ /tmp/tmp.sVAhhZuEzA$ cd foo
/tmp/tmp.sVAhhZuEzA/foo$ git init --bare origin Initialized empty Git repository in /tmp/tmp.sVAhhZuEzA/foo/origin/ /tmp/tmp.sVAhhZuEzA/foo$ git commit --allow-empty -m 'test' [main (root-commit) 24c92a9] test /tmp/tmp.sVAhhZuEzA/foo$ git push origin main Enumerating objects: 2, done.
Counting objects: 100% (2/2), done.
Writing objects: 100% (2/2), 162 bytes | 162.00 KiB/s, done. Total 2 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) To origin
* [new branch] main -> main

#23 lore raw
Quoted 7 lines · David Mandelberg
Quoted 4 lines

As a remote name, we still do use "is that a
file on the filesystem?" to see if it is a local file:// URL (i.e. "git fetch github/dseomn" may be fetching from a subdirectory two levels down)

Btw, I just tested it, and I think this is an issue for remote names without slashes too:

Yes, and that is an intended way local repositories are discovered and used.

The problem with slashes is that the mistake surface becomes a lot larger. "ls<RETURN>" would show you "origin" immediately in the current working directory, but you wouldn't know if origin has origin/foo or origin/bar unless you look. Limiting the local filesystem reference to something like "only the ones that has no slashes, or begin with ../ or /" would make it less likely that you meant to push to somewhere and instead push to a local directory.

#24 lore raw

v2 cover completion: fix bugs with slashes in remote names

In reply to David Mandelberg via GitGitGadget

Thanks everyone for all the reviews! I rewrote __git_dwim_remote_heads to fork a constant number of times instead of a multiple of the number of remotes. There are still a handful of forks, but I think they can mostly be parallelized.

I tried to find a pure shell implementation like a few of you mentioned, but I couldn't think of one that would work in both bash 3.2 (the version on macOS, I think?) and zsh. The only thing I found in
https://www.bashcookbook.com/bashinfo/source/bash-3.2/doc/bash.html that looked like it would work to implement GIT_COMPLETION_IGNORE_CASE was the nocasematch arg to shopt, but
https://zsh.sourceforge.io/Doc/Release/zsh_14.html#index_split-13_fn_letter-S
doesn't list shopt at all.

One unintentional difference with this approach is that $cur_ is no longer treated as a glob pattern. Was it supposed to be in the first place though, or was that just because of missing escaping? I.e., this no longer works to complete unique-remote-branch:

git checkout unique-remote-br*<Tab>

In my original commit message, I mentioned that HEAD was no longer listed, but I think I was wrong about that. I think HEAD went away in a bunch of the tests because I added another remote, so HEAD was no longer unique. Maybe HEAD should be filtered out too, but that seems like a separate issue for a separate patch.

David Mandelberg (2):
completion: add helper to count path components
completion: fix bugs with slashes in remote names

 contrib/completion/git-completion.bash |  49 +++++-
 t/t9902-completion.sh                  | 206 ++++++++++++++++++++++---
 2 files changed, 226 insertions(+), 29 deletions(-)
Signature · 1 line
2.47.2
#25 lore raw

PATCH v2 1/2 completion: add helper to count path components

A follow-up commit will use this with for-each-ref to strip the right number of path components from refnames.

---
 contrib/completion/git-completion.bash | 11 +++++++++++
 t/t9902-completion.sh                  | 26 ++++++++++++++++++++++++++
 2 files changed, 37 insertions(+)
contrib/completion/git-completion.bash +11 -0
index 413911be3b..5fdc71208e 100644
@@ -234,6 +234,17 @@ __git_dequote ()
 	done
 }
 
+# Prints the number of slash-separated components in a path.
+# 1: Path to count components of.
+__git_count_path_components ()
+{
+	local path="$1"
+	local relative="${path#/}"
+	relative="${relative%/}"
+	local slashes="/${relative//[^\/]}"
+	echo "${#slashes}"
+}
+
 # The following function is based on code from:
 #
 #   bash_completion - programmable completion functions for bash 3.2+
t/t9902-completion.sh +26 -0
index 51bd750837..015289c776 100755
@@ -455,6 +455,32 @@ test_expect_success '__git_dequote - open double quote' '
 '
 
 
+test_expect_success '__git_count_path_components - no slashes' '
+	echo 1 >expected &&
+	__git_count_path_components a >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+test_expect_success '__git_count_path_components - relative' '
+	echo 3 >expected &&
+	__git_count_path_components a/b/c >"$actual" &&
+	test_cmp expected "$actual"
+
+'
+
+test_expect_success '__git_count_path_components - absolute' '
+	echo 3 >expected &&
+	__git_count_path_components /a/b/c >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+test_expect_success '__git_count_path_components - trailing slash' '
+	echo 3 >expected &&
+	__git_count_path_components a/b/c/ >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+
 test_expect_success '__gitcomp_direct - puts everything into COMPREPLY as-is' '
 	sed -e "s/Z$//g" >expected <<-EOF &&
 	with-trailing-space Z
Signature · 1 line
2.47.2
#26 lore raw

PATCH v2 2/2 completion: fix bugs with slashes in remote names

In reply to David Mandelberg

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

---
 contrib/completion/git-completion.bash |  38 +++++-
 t/t9902-completion.sh                  | 180 ++++++++++++++++++++++---
 2 files changed, 189 insertions(+), 29 deletions(-)
contrib/completion/git-completion.bash +31 -7
index 5fdc71208e..450fabc901 100644
@@ -790,16 +790,39 @@ __git_tags ()
 __git_dwim_remote_heads ()
 {
 	local pfx="${1-}" cur_="${2-}" sfx="${3-}"
-	local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
 
 	# 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" \
-		${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
-		"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
-	uniq -u
+	local awk_script='
+	function casemap(s) {
+		if (ENVIRON["IGNORE_CASE"])
+			return tolower(s)
+		else
+			return s
+	}
+	BEGIN {
+		split(ENVIRON["REMOTES"], remotes, /\n/)
+		for (i in remotes)
+			remotes[i] = "refs/remotes/" casemap(remotes[i])
+		cur_ = casemap(ENVIRON["CUR_"])
+	}
+	{
+		ref_case = casemap($0)
+		for (i in remotes) {
+			if (index(ref_case, remotes[i] "/" cur_) == 1) {
+				branch = substr($0, length(remotes[i] "/") + 1)
+				print ENVIRON["PFX"] branch ENVIRON["SFX"]
+				break
+			}
+		}
+	}
+	'
+	__git for-each-ref --format='%(refname)' |
+		PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
+			IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
+			REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" |
+		sort | uniq -u
 }
 
 # Lists refs from the local (by default) or from a remote repository.
@@ -905,7 +928,8 @@ __git_refs ()
 			case "HEAD" in
 			$match*|$umatch*)	echo "${pfx}HEAD$sfx" ;;
 			esac
-			__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
+			local strip="$(__git_count_path_components "refs/remotes/$remote")"
+			__git for-each-ref --format="$fer_pfx%(refname:strip=$strip)$sfx" \
 				${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
 				"refs/remotes/$remote/$match*" \
 				"refs/remotes/$remote/$match*/**"
t/t9902-completion.sh +158 -22

577 lines not on this page · raw message

Signature · 1 line
2.47.2
#27 lore raw

David Mandelberg schreef op 2025-03-04 19:09:

Quoted 57 lines

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

---
 contrib/completion/git-completion.bash |  38 +++++-
 t/t9902-completion.sh                  | 180 ++++++++++++++++++++++---
 2 files changed, 189 insertions(+), 29 deletions(-)
contrib/completion/git-completion.bash +19 -6
index 5fdc71208e..450fabc901 100644
@@ -790,16 +790,39 @@ __git_tags ()
 __git_dwim_remote_heads ()
 {
 	local pfx="${1-}" cur_="${2-}" sfx="${3-}"
-	local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
 
 	# 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" \
-		${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
-		"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
-	uniq -u
+	local awk_script='
+	function casemap(s) {
+		if (ENVIRON["IGNORE_CASE"])
+			return tolower(s)
+		else
+			return s
+	}
+	BEGIN {
+		split(ENVIRON["REMOTES"], remotes, /\n/)
+		for (i in remotes)
+			remotes[i] = "refs/remotes/" casemap(remotes[i])
+		cur_ = casemap(ENVIRON["CUR_"])
+	}
+	{
+		ref_case = casemap($0)
+		for (i in remotes) {
+			if (index(ref_case, remotes[i] "/" cur_) == 1) {
+				branch = substr($0, length(remotes[i] "/") + 1)
+				print ENVIRON["PFX"] branch ENVIRON["SFX"]

contrib/completion/git-completion.bash __git_tags () · line 825

Quoted 10 lines
contrib/completion/git-completion.bash +10 -0
+				break
+			}
+		}
+	}
+	'
+	__git for-each-ref --format='%(refname)' |
+		PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
+			IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
+			REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" |
+		sort | uniq -u

I realized that this sends refs to awk that it doesn't need to. I'll apply this diff to the next version of this patch:

contrib/completion/git-completion.bash +1 -1
index 450fabc901..c9d014070c 100644
@@ -818,7 +818,7 @@ __git_dwim_remote_heads ()
                }
        }
        '
-       __git for-each-ref --format='%(refname)' |
+       __git for-each-ref --format='%(refname)' 'refs/remotes/**' |
                PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
                        IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
                        REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" |
Quoted 650 lines
contrib/completion/git-completion.bash +2 -1
 }
 
 # Lists refs from the local (by default) or from a remote repository.
@@ -905,7 +928,8 @@ __git_refs ()
 			case "HEAD" in
 			$match*|$umatch*)	echo "${pfx}HEAD$sfx" ;;
 			esac
-			__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
+			local strip="$(__git_count_path_components "refs/remotes/$remote")"
+			__git for-each-ref --format="$fer_pfx%(refname:strip=$strip)$sfx" \
 				${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
 				"refs/remotes/$remote/$match*" \
 				"refs/remotes/$remote/$match*/**"
t/t9902-completion.sh +158 -22
index 015289c776..343b8cd191 100755
@@ -149,7 +149,8 @@ fi
 test_expect_success 'setup for __git_find_repo_path/__gitdir tests' '
 	mkdir -p subdir/subsubdir &&
 	mkdir -p non-repo &&
-	git init -b main otherrepo
+	git init -b main otherrepo &&
+	git init -b main slashrepo
 '
 
 test_expect_success '__git_find_repo_path - from command line (through $__git_dir)' '
@@ -674,6 +675,13 @@ test_expect_success 'setup for ref completion' '
 	) &&
 	git remote add other "$ROOT/otherrepo/.git" &&
 	git fetch --no-tags other &&
+	(
+		cd slashrepo &&
+		git commit --allow-empty -m initial &&
+		git branch -m main branch/with/slash
+	) &&
+	git remote add remote/with/slash "$ROOT/slashrepo/.git" &&
+	git fetch --no-tags remote/with/slash &&
 	rm -f .git/FETCH_HEAD &&
 	git init thirdrepo
 '
@@ -686,6 +694,8 @@ test_expect_success '__git_refs - simple' '
 	other/HEAD
 	other/branch-in-other
 	other/main-in-other
+	remote/with/slash/HEAD
+	remote/with/slash/branch/with/slash
 	matching-tag
 	EOF
 	(
@@ -702,6 +712,8 @@ test_expect_success '__git_refs - full refs' '
 	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
+	refs/remotes/remote/with/slash/HEAD
+	refs/remotes/remote/with/slash/branch/with/slash
 	refs/tags/matching-tag
 	EOF
 	(
@@ -767,6 +779,19 @@ test_expect_success '__git_refs - configured remote' '
 	test_cmp expected "$actual"
 '
 
+test_expect_success '__git_refs - configured remote - with slash' '
+	cat >expected <<-EOF &&
+	HEAD
+	HEAD
+	branch/with/slash
+	EOF
+	(
+		cur= &&
+		__git_refs remote/with/slash >"$actual"
+	) &&
+	test_cmp expected "$actual"
+'
+
 test_expect_success '__git_refs - configured remote - full refs' '
 	cat >expected <<-EOF &&
 	HEAD
@@ -909,17 +934,19 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
-	remote/ambiguous
-	remote/branch-in-remote
+	remote/with/slash/HEAD
+	remote/with/slash/ambiguous
+	remote/with/slash/branch-in-remote
+	remote/with/slash/branch/with/slash
 	matching-tag
-	HEAD
 	branch-in-other
 	branch-in-remote
+	branch/with/slash
 	main-in-other
 	EOF
 	for remote_ref in refs/remotes/other/ambiguous \
-		refs/remotes/remote/ambiguous \
-		refs/remotes/remote/branch-in-remote
+		refs/remotes/remote/with/slash/ambiguous \
+		refs/remotes/remote/with/slash/branch-in-remote
 	do
 		git update-ref $remote_ref main &&
 		test_when_finished "git update-ref -d $remote_ref" || return 1
@@ -939,6 +966,8 @@ test_expect_success '__git_refs - after --opt=' '
 	other/HEAD
 	other/branch-in-other
 	other/main-in-other
+	remote/with/slash/HEAD
+	remote/with/slash/branch/with/slash
 	matching-tag
 	EOF
 	(
@@ -955,6 +984,8 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
+	refs/remotes/remote/with/slash/HEAD
+	refs/remotes/remote/with/slash/branch/with/slash
 	refs/tags/matching-tag
 	EOF
 	(
@@ -972,6 +1003,8 @@ test_expect_success '__git refs - excluding refs' '
 	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
+	^remote/with/slash/HEAD
+	^remote/with/slash/branch/with/slash
 	^matching-tag
 	EOF
 	(
@@ -988,6 +1021,8 @@ test_expect_success '__git refs - excluding full refs' '
 	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
+	^refs/remotes/remote/with/slash/HEAD
+	^refs/remotes/remote/with/slash/branch/with/slash
 	^refs/tags/matching-tag
 	EOF
 	(
@@ -1015,6 +1050,8 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
+	remote/with/slash/HEAD
+	remote/with/slash/branch/with/slash
 	matching-tag
 	matching/tag
 	EOF
@@ -1135,6 +1172,8 @@ test_expect_success '__git_complete_refs - simple' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	matching-tag Z
 	EOF
 	(
@@ -1173,6 +1212,20 @@ test_expect_success '__git_complete_refs - remote' '
 	test_cmp expected out
 '
 
+test_expect_success '__git_complete_refs - remote - with slash' '
+	sed -e "s/Z$//" >expected <<-EOF &&
+	HEAD Z
+	HEAD Z
+	branch/with/slash Z
+	EOF
+	(
+		cur= &&
+		__git_complete_refs --remote=remote/with/slash &&
+		print_comp
+	) &&
+	test_cmp expected out
+'
+
 test_expect_success '__git_complete_refs - track' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
@@ -1181,9 +1234,11 @@ test_expect_success '__git_complete_refs - track' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	matching-tag Z
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main-in-other Z
 	EOF
 	(
@@ -1228,6 +1283,8 @@ test_expect_success '__git_complete_refs - suffix' '
 	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
+	remote/with/slash/HEAD.
+	remote/with/slash/branch/with/slash.
 	matching-tag.
 	EOF
 	(
@@ -1253,6 +1310,20 @@ test_expect_success '__git_complete_fetch_refspecs - simple' '
 	test_cmp expected out
 '
 
+test_expect_success '__git_complete_fetch_refspecs - with slash' '
+	sed -e "s/Z$//" >expected <<-EOF &&
+	HEAD:HEAD Z
+	HEAD:HEAD Z
+	branch/with/slash:branch/with/slash Z
+	EOF
+	(
+		cur= &&
+		__git_complete_fetch_refspecs remote/with/slash &&
+		print_comp
+	) &&
+	test_cmp expected out
+'
+
 test_expect_success '__git_complete_fetch_refspecs - matching' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	branch-in-other:branch-in-other Z
@@ -1333,8 +1404,8 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -1480,8 +1551,8 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -1489,6 +1560,8 @@ test_expect_success 'git checkout - completes refs and unique remote branches fo
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1508,8 +1581,8 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -1518,8 +1591,8 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -1542,14 +1615,16 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -1557,6 +1632,8 @@ test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1,
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1569,14 +1646,16 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -1584,6 +1663,8 @@ test_expect_success 'git checkout - a later --guess overrides previous --no-gues
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1596,6 +1677,8 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1609,6 +1692,8 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1616,8 +1701,8 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -1625,6 +1710,8 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1632,8 +1719,8 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -1641,6 +1728,8 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1654,6 +1743,8 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1666,6 +1757,8 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1678,6 +1771,8 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1850,6 +1945,8 @@ test_expect_success 'git switch - with -d, complete all references' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1862,6 +1959,8 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1870,11 +1969,15 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1883,11 +1986,15 @@ test_expect_success 'git checkout - with --track, complete only remote branches'
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1907,6 +2014,8 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1919,6 +2028,8 @@ test_expect_success 'git switch - with -c, complete all references' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1931,6 +2042,8 @@ test_expect_success 'git switch - with -C, complete all references' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1943,6 +2056,8 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1955,6 +2070,8 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1967,6 +2084,8 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1979,6 +2098,8 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -1991,6 +2112,8 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -2003,6 +2126,8 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -2015,6 +2140,8 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -2027,6 +2154,8 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -2039,6 +2168,8 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -2051,13 +2182,15 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -2066,8 +2199,8 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -2104,8 +2237,8 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -2114,8 +2247,8 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -2152,8 +2285,8 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -2168,8 +2301,8 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
-	HEAD Z
 	branch-in-other Z
+	branch/with/slash Z
 	main Z
 	main-in-other Z
 	matching-branch Z
@@ -2185,6 +2318,8 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
 	EOF
 '
 
@@ -2199,7 +2334,8 @@ test_expect_success 'git restore completes modified files' '
 test_expect_success 'teardown after ref completion' '
 	git branch -d matching-branch &&
 	git tag -d matching-tag &&
-	git remote remove other
+	git remote remove other &&
+	git remote remove remote/with/slash
 '
#28 lore raw

Hi David

Quoted 60 lines · David Mandelberg

David Mandelberg schreef op 2025-03-04 19:09:

Quoted 59 lines

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

---
  contrib/completion/git-completion.bash |  38 +++++-
  t/t9902-completion.sh                  | 180 ++++++++++++++++++++++---
  2 files changed, 189 insertions(+), 29 deletions(-)
contrib/completion/git-completion.bash +21 -6
index 5fdc71208e..450fabc901 100644
@@ -790,16 +790,39 @@ __git_tags ()
  __git_dwim_remote_heads ()
  {
  	local pfx="${1-}" cur_="${2-}" sfx="${3-}"
-	local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
 
  	# 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" \
-		${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
-		"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
-	uniq -u
+	local awk_script='
+	function casemap(s) {
+		if (ENVIRON["IGNORE_CASE"])
+			return tolower(s)
+		else
+			return s
+	}
+	BEGIN {
+		split(ENVIRON["REMOTES"], remotes, /\n/)
+		for (i in remotes)
+			remotes[i] = "refs/remotes/" casemap(remotes[i])
+		cur_ = casemap(ENVIRON["CUR_"])
+	}
+	{
+		ref_case = casemap($0)
+		for (i in remotes) {
+			if (index(ref_case, remotes[i] "/" cur_) == 1) {
+				branch = substr($0, length(remotes[i] "/") + 1)
+				print ENVIRON["PFX"] branch ENVIRON["SFX"]
+				break
+			}
Quoted 8 lines

contrib/completion/git-completion.bash __git_tags () · line 825

Quoted 8 lines
contrib/completion/git-completion.bash +8 -0
+		}
+	}
+	'
+	__git for-each-ref --format='%(refname)' |
+		PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
+			IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
+			REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" |
+		sort | uniq -u

I wonder if we can use "sort -u" here and drop uniq. It isn't quite the same as it will remove entries that are equal according to the current locale but I don't think that should matter.

Quoted 2 lines

I realized that this sends refs to awk that it doesn't need to. I'll apply this diff to the next version of this patch:

With that tweak this looks good. If there are no glob characters then "git for-each-ref" does a prefix match so strictly speaking you don't need the '**' but I don't think it matters in practice. I had one thought below but if you don't feel like spending more that on this I think what you have here is fine.

The filtering is O(number of remote refs * number of remotes). If we could sort the list of remotes and remote refs in the same order then we can reduce this to O(number of remote refs + number of remotes) by doing (in pseudo code)

    for ($ref in $remote_refs) {
	while (!starts_with($ref, "refs/remotes/$remote[$i]"))
		$i++;
	if (starts_with($ref, "refs/remotes/$remote[$i]/$cur_)
		print $ref;
    }

I think we can get "git for-each-ref --sort=-refname" and "sort -r" to agree on a sorting order by setting LC_COLLATE=C, otherwise "sort" will sort according the the current locale whereas git sorts by bytes.

Best Wishes

Phillip

Quoted 665 lines
contrib/completion/git-completion.bash +1 -1
index 450fabc901..c9d014070c 100644
@@ -818,7 +818,7 @@ __git_dwim_remote_heads ()
                 }
         }
         '
-       __git for-each-ref --format='%(refname)' |
+       __git for-each-ref --format='%(refname)' 'refs/remotes/**' |
                 PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
                         IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
                         REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" |
Quoted 651 lines
contrib/completion/git-completion.bash +2 -1
  }
 
  # Lists refs from the local (by default) or from a remote repository.
@@ -905,7 +928,8 @@ __git_refs ()
  			case "HEAD" in
  			$match*|$umatch*)	echo "${pfx}HEAD$sfx" ;;
  			esac
-			__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
+			local strip="$(__git_count_path_components "refs/remotes/$remote")"
+			__git for-each-ref --format="$fer_pfx%(refname:strip=$strip)$sfx" \
  				${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
  				"refs/remotes/$remote/$match*" \
  				"refs/remotes/$remote/$match*/**"
t/t9902-completion.sh +158 -22
index 015289c776..343b8cd191 100755
@@ -149,7 +149,8 @@ fi
  test_expect_success 'setup for __git_find_repo_path/__gitdir tests' '
  	mkdir -p subdir/subsubdir &&
  	mkdir -p non-repo &&
-	git init -b main otherrepo
+	git init -b main otherrepo &&
+	git init -b main slashrepo
  '
 
  test_expect_success '__git_find_repo_path - from command line (through $__git_dir)' '
@@ -674,6 +675,13 @@ test_expect_success 'setup for ref completion' '
  	) &&
  	git remote add other "$ROOT/otherrepo/.git" &&
  	git fetch --no-tags other &&
+	(
+		cd slashrepo &&
+		git commit --allow-empty -m initial &&
+		git branch -m main branch/with/slash
+	) &&
+	git remote add remote/with/slash "$ROOT/slashrepo/.git" &&
+	git fetch --no-tags remote/with/slash &&
  	rm -f .git/FETCH_HEAD &&
  	git init thirdrepo
  '
@@ -686,6 +694,8 @@ test_expect_success '__git_refs - simple' '
  	other/HEAD
  	other/branch-in-other
  	other/main-in-other
+	remote/with/slash/HEAD
+	remote/with/slash/branch/with/slash
  	matching-tag
  	EOF
  	(
@@ -702,6 +712,8 @@ test_expect_success '__git_refs - full refs' '
  	refs/remotes/other/HEAD
  	refs/remotes/other/branch-in-other
  	refs/remotes/other/main-in-other
+	refs/remotes/remote/with/slash/HEAD
+	refs/remotes/remote/with/slash/branch/with/slash
  	refs/tags/matching-tag
  	EOF
  	(
@@ -767,6 +779,19 @@ test_expect_success '__git_refs - configured remote' '
  	test_cmp expected "$actual"
  '
 
+test_expect_success '__git_refs - configured remote - with slash' '
+	cat >expected <<-EOF &&
+	HEAD
+	HEAD
+	branch/with/slash
+	EOF
+	(
+		cur= &&
+		__git_refs remote/with/slash >"$actual"
+	) &&
+	test_cmp expected "$actual"
+'
+
  test_expect_success '__git_refs - configured remote - full refs' '
  	cat >expected <<-EOF &&
  	HEAD
@@ -909,17 +934,19 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
  	other/ambiguous
  	other/branch-in-other
  	other/main-in-other
-	remote/ambiguous
-	remote/branch-in-remote
+	remote/with/slash/HEAD
+	remote/with/slash/ambiguous
+	remote/with/slash/branch-in-remote
+	remote/with/slash/branch/with/slash
  	matching-tag
-	HEAD
  	branch-in-other
  	branch-in-remote
+	branch/with/slash
  	main-in-other
  	EOF
  	for remote_ref in refs/remotes/other/ambiguous \
-		refs/remotes/remote/ambiguous \
-		refs/remotes/remote/branch-in-remote
+		refs/remotes/remote/with/slash/ambiguous \
+		refs/remotes/remote/with/slash/branch-in-remote
  	do
  		git update-ref $remote_ref main &&
  		test_when_finished "git update-ref -d $remote_ref" || return 1
@@ -939,6 +966,8 @@ test_expect_success '__git_refs - after --opt=' '
  	other/HEAD
  	other/branch-in-other
  	other/main-in-other
+	remote/with/slash/HEAD
+	remote/with/slash/branch/with/slash
  	matching-tag
  	EOF
  	(
@@ -955,6 +984,8 @@ test_expect_success '__git_refs - after --opt= - full refs' '
  	refs/remotes/other/HEAD
  	refs/remotes/other/branch-in-other
  	refs/remotes/other/main-in-other
+	refs/remotes/remote/with/slash/HEAD
+	refs/remotes/remote/with/slash/branch/with/slash
  	refs/tags/matching-tag
  	EOF
  	(
@@ -972,6 +1003,8 @@ test_expect_success '__git refs - excluding refs' '
  	^other/HEAD
  	^other/branch-in-other
  	^other/main-in-other
+	^remote/with/slash/HEAD
+	^remote/with/slash/branch/with/slash
  	^matching-tag
  	EOF
  	(
@@ -988,6 +1021,8 @@ test_expect_success '__git refs - excluding full refs' '
  	^refs/remotes/other/HEAD
  	^refs/remotes/other/branch-in-other
  	^refs/remotes/other/main-in-other
+	^refs/remotes/remote/with/slash/HEAD
+	^refs/remotes/remote/with/slash/branch/with/slash
  	^refs/tags/matching-tag
  	EOF
  	(
@@ -1015,6 +1050,8 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
  	other/branch-in-other
  	other/main-in-other
  	other/matching/branch-in-other
+	remote/with/slash/HEAD
+	remote/with/slash/branch/with/slash
  	matching-tag
  	matching/tag
  	EOF
@@ -1135,6 +1172,8 @@ test_expect_success '__git_complete_refs - simple' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	matching-tag Z
  	EOF
  	(
@@ -1173,6 +1212,20 @@ test_expect_success '__git_complete_refs - remote' '
  	test_cmp expected out
  '
 
+test_expect_success '__git_complete_refs - remote - with slash' '
+	sed -e "s/Z$//" >expected <<-EOF &&
+	HEAD Z
+	HEAD Z
+	branch/with/slash Z
+	EOF
+	(
+		cur= &&
+		__git_complete_refs --remote=remote/with/slash &&
+		print_comp
+	) &&
+	test_cmp expected out
+'
+
  test_expect_success '__git_complete_refs - track' '
  	sed -e "s/Z$//" >expected <<-EOF &&
  	HEAD Z
@@ -1181,9 +1234,11 @@ test_expect_success '__git_complete_refs - track' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	matching-tag Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main-in-other Z
  	EOF
  	(
@@ -1228,6 +1283,8 @@ test_expect_success '__git_complete_refs - suffix' '
  	other/HEAD.
  	other/branch-in-other.
  	other/main-in-other.
+	remote/with/slash/HEAD.
+	remote/with/slash/branch/with/slash.
  	matching-tag.
  	EOF
  	(
@@ -1253,6 +1310,20 @@ test_expect_success '__git_complete_fetch_refspecs - simple' '
  	test_cmp expected out
  '
 
+test_expect_success '__git_complete_fetch_refspecs - with slash' '
+	sed -e "s/Z$//" >expected <<-EOF &&
+	HEAD:HEAD Z
+	HEAD:HEAD Z
+	branch/with/slash:branch/with/slash Z
+	EOF
+	(
+		cur= &&
+		__git_complete_fetch_refspecs remote/with/slash &&
+		print_comp
+	) &&
+	test_cmp expected out
+'
+
  test_expect_success '__git_complete_fetch_refspecs - matching' '
  	sed -e "s/Z$//" >expected <<-EOF &&
  	branch-in-other:branch-in-other Z
@@ -1333,8 +1404,8 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
  test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
  	test_completion "git switch " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1480,8 +1551,8 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
  test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
  	test_completion "git checkout " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1489,6 +1560,8 @@ test_expect_success 'git checkout - completes refs and unique remote branches fo
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1508,8 +1581,8 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
  test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
  	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1518,8 +1591,8 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
  test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
  	test_completion "git switch --no-guess --guess " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1542,14 +1615,16 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
  test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
  	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1557,6 +1632,8 @@ test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1,
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1569,14 +1646,16 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
  test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
  	test_completion "git checkout --no-guess --guess " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1584,6 +1663,8 @@ test_expect_success 'git checkout - a later --guess overrides previous --no-gues
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1596,6 +1677,8 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1609,6 +1692,8 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1616,8 +1701,8 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
  	test_config checkout.guess true &&
  	test_completion "git checkout " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1625,6 +1710,8 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1632,8 +1719,8 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
  	test_config checkout.guess false &&
  	test_completion "git checkout --guess " <<-\EOF
  	HEAD Z
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -1641,6 +1728,8 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1654,6 +1743,8 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1666,6 +1757,8 @@ test_expect_success 'git switch - with --detach, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1678,6 +1771,8 @@ test_expect_success 'git checkout - with --detach, complete only references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1850,6 +1945,8 @@ test_expect_success 'git switch - with -d, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1862,6 +1959,8 @@ test_expect_success 'git checkout - with -d, complete only references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1870,11 +1969,15 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  	test_completion "git switch -t " <<-\EOF
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1883,11 +1986,15 @@ test_expect_success 'git checkout - with --track, complete only remote branches'
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  	test_completion "git checkout -t " <<-\EOF
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1907,6 +2014,8 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1919,6 +2028,8 @@ test_expect_success 'git switch - with -c, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1931,6 +2042,8 @@ test_expect_success 'git switch - with -C, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1943,6 +2056,8 @@ test_expect_success 'git switch - with -c and --track, complete all references'
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1955,6 +2070,8 @@ test_expect_success 'git switch - with -C and --track, complete all references'
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1967,6 +2084,8 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1979,6 +2098,8 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -1991,6 +2112,8 @@ test_expect_success 'git checkout - with -b, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2003,6 +2126,8 @@ test_expect_success 'git checkout - with -B, complete all references' '
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2015,6 +2140,8 @@ test_expect_success 'git checkout - with -b and --track, complete all references
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2027,6 +2154,8 @@ test_expect_success 'git checkout - with -B and --track, complete all references
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2039,6 +2168,8 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2051,13 +2182,15 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
  test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
  	test_completion "git switch -c " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2066,8 +2199,8 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
  test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
  	test_completion "git switch -C " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2104,8 +2237,8 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
  test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
  	test_completion "git checkout -b " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2114,8 +2247,8 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
  test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
  	test_completion "git checkout -B " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2152,8 +2285,8 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
  test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
  	test_completion "git switch --orphan " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2168,8 +2301,8 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
  test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
  	test_completion "git checkout --orphan " <<-\EOF
-	HEAD Z
  	branch-in-other Z
+	branch/with/slash Z
  	main Z
  	main-in-other Z
  	matching-branch Z
@@ -2185,6 +2318,8 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
  	other/HEAD Z
  	other/branch-in-other Z
  	other/main-in-other Z
+	remote/with/slash/HEAD Z
+	remote/with/slash/branch/with/slash Z
  	EOF
  '
 
@@ -2199,7 +2334,8 @@ test_expect_success 'git restore completes modified files' '
  test_expect_success 'teardown after ref completion' '
  	git branch -d matching-branch &&
  	git tag -d matching-tag &&
-	git remote remove other
+	git remote remove other &&
+	git remote remove remote/with/slash
  '
#29 lore raw

Op 2025-03-06 om 11:35 schreef Phillip Wood:

Quoted 65 lines

Hi David

Quoted 62 lines · David Mandelberg

David Mandelberg schreef op 2025-03-04 19:09:

Quoted 61 lines

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

---
  contrib/completion/git-completion.bash |  38 +++++- t/t9902-completion.sh                  | 180 ++++++++++++++++++++++---

  2 files changed, 189 insertions(+), 29 deletions(-)
contrib/completion/git-completion.bash +0 -0
index 5fdc71208e..450fabc901 100644
@@ -790,16 +790,39 @@ __git_tags ()
   __git_dwim_remote_heads ()
   {

      local pfx="${1-}" cur_="${2-}" sfx="${3-}"
-    local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers

      # 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" \
-        ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
-        "refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
-    uniq -u
+    local awk_script='
+    function casemap(s) {
+        if (ENVIRON["IGNORE_CASE"])
+            return tolower(s)
+        else
+            return s
+    }
+    BEGIN {
+        split(ENVIRON["REMOTES"], remotes, /\n/)
+        for (i in remotes)
+            remotes[i] = "refs/remotes/" casemap(remotes[i])
+        cur_ = casemap(ENVIRON["CUR_"])
+    }
+    {
+        ref_case = casemap($0)
+        for (i in remotes) {
+            if (index(ref_case, remotes[i] "/" cur_) == 1) {
+                branch = substr($0, length(remotes[i] "/") + 1)
+                print ENVIRON["PFX"] branch ENVIRON["SFX"]
+                break
+            }
+        }
+    }
Quoted 8 lines
Quoted 6 lines
Quoted 6 lines

+    '
+    __git for-each-ref --format='%(refname)' |
+        PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
+            IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
+            REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" | +        sort | uniq -u

I wonder if we can use "sort -u" here and drop uniq. It isn't quite the same as it will remove entries that are equal according to the current locale but I don't think that should matter.

That's what I thought when I first saw that, but uniq -u removes duplicates:

$ printf 'a\nb\na\n' | sort | uniq -u
b
$ printf 'a\nb\na\n' | sort -u
a
b
Quoted 7 lines
Quoted 2 lines

I realized that this sends refs to awk that it doesn't need to. I'll apply this diff to the next version of this patch:

With that tweak this looks good. If there are no glob characters then "git for-each-ref" does a prefix match so strictly speaking you don't need the '**' but I don't think it matters in practice. I had one thought below but if you don't feel like spending more that on this I think what you have here is fine.

The filtering is O(number of remote refs * number of remotes). If we could sort the list of remotes and remote refs in the same order then we can reduce this to O(number of remote refs + number of remotes) by doing (in pseudo code)

Quoted 8 lines

    for ($ref in $remote_refs) {
    while (!starts_with($ref, "refs/remotes/$remote[$i]")) $i++;
    if (starts_with($ref, "refs/remotes/$remote[$i]/$cur_) print $ref;
    }

I think we can get "git for-each-ref --sort=-refname" and "sort -r" to agree on a sorting order by setting LC_COLLATE=C, otherwise "sort" will sort according the the current locale whereas git sorts by bytes.

On my desktop, a similar-ish loop takes about 0.1s with 1000*1000 iterations. 1000 remote branches seems plausible but on the high side to me, and 1000 remotes seems very high to me. Do you still think it's worth optimizing? I do think your solution would work, but I think it would take a decent amount of testing to avoid the collation issues you mentioned, and off-by-one errors and the like. So I'd rather not do it unless there's a practical performance advantage, not just theoretical. (Which is what I thought about minimizing forks too, before I learned that it was a practical issue.)

$ time seq 1 1000 | LONG="$(seq 1 1000)" awk 'BEGIN { split(ENVIRON["LONG"], long, /\n/); } { for (i in long) { if ($0 == long[i]) { print; }  } }' > /dev/null
real    0m0,092s
user    0m0,067s
sys     0m0,028s
Quoted 683 lines

Best Wishes

Phillip

Quoted 678 lines
contrib/completion/git-completion.bash +0 -0
index 450fabc901..c9d014070c 100644
@@ -818,7 +818,7 @@ __git_dwim_remote_heads ()
                  }
          }

         '
-       __git for-each-ref --format='%(refname)' |
+       __git for-each-ref --format='%(refname)' 'refs/remotes/**' | PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
                         IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
                         REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" |

Quoted 663 lines

  }
  # Lists refs from the local (by default) or from a remote repository.

contrib/completion/git-completion.bash +0 -0
@@ -905,7 +928,8 @@ __git_refs ()

              case "HEAD" in
              $match*|$umatch*)    echo "${pfx}HEAD$sfx" ;; esac
-            __git for-each-ref -- format="$fer_pfx%(refname:strip=3)$sfx" \
+            local strip="$(__git_count_path_components "refs/ remotes/$remote")"
+            __git for-each-ref -- format="$fer_pfx%(refname:strip=$strip)$sfx" \
                  ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
                  "refs/remotes/$remote/$match*" \
                  "refs/remotes/$remote/$match*/**"

t/t9902-completion.sh +0 -0
index 015289c776..343b8cd191 100755
@@ -149,7 +149,8 @@ fi

  test_expect_success 'setup for __git_find_repo_path/__gitdir tests' ' mkdir -p subdir/subsubdir &&
      mkdir -p non-repo &&
-    git init -b main otherrepo
+    git init -b main otherrepo &&
+    git init -b main slashrepo
  '
  test_expect_success '__git_find_repo_path - from command line (through $__git_dir)' '

t/t9902-completion.sh +0 -0
@@ -674,6 +675,13 @@ test_expect_success 'setup for ref completion' '

      ) &&
      git remote add other "$ROOT/otherrepo/.git" && git fetch --no-tags other &&
+    (
+        cd slashrepo &&
+        git commit --allow-empty -m initial &&
+        git branch -m main branch/with/slash
+    ) &&
+    git remote add remote/with/slash "$ROOT/slashrepo/.git" && +    git fetch --no-tags remote/with/slash &&
      rm -f .git/FETCH_HEAD &&
      git init thirdrepo
  '

t/t9902-completion.sh +0 -0
@@ -686,6 +694,8 @@ test_expect_success '__git_refs - simple' '

      other/HEAD
      other/branch-in-other
      other/main-in-other
+    remote/with/slash/HEAD
+    remote/with/slash/branch/with/slash
      matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -702,6 +712,8 @@ test_expect_success '__git_refs - full refs' '

      refs/remotes/other/HEAD refs/remotes/other/branch-in-other refs/remotes/other/main-in-other
+    refs/remotes/remote/with/slash/HEAD
+    refs/remotes/remote/with/slash/branch/with/slash refs/tags/matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -767,6 +779,19 @@ test_expect_success '__git_refs - configured 

remote' '
      test_cmp expected "$actual"
  '

t/t9902-completion.sh +13 -0
+test_expect_success '__git_refs - configured remote - with slash' '
+    cat >expected <<-EOF &&
+    HEAD
+    HEAD
+    branch/with/slash
+    EOF
+    (
+        cur= &&
+        __git_refs remote/with/slash >"$actual"
+    ) &&
+    test_cmp expected "$actual"
+'
+

  test_expect_success '__git_refs - configured remote - full refs' ' cat >expected <<-EOF &&
      HEAD

t/t9902-completion.sh +0 -0
@@ -909,17 +934,19 @@ test_expect_success '__git_refs - unique remote 

branches for git checkout DWIMer
      other/ambiguous
      other/branch-in-other
      other/main-in-other
-    remote/ambiguous
-    remote/branch-in-remote
+    remote/with/slash/HEAD
+    remote/with/slash/ambiguous
+    remote/with/slash/branch-in-remote
+    remote/with/slash/branch/with/slash
      matching-tag
-    HEAD
      branch-in-other
      branch-in-remote
+    branch/with/slash
      main-in-other
      EOF
      for remote_ref in refs/remotes/other/ambiguous \
-        refs/remotes/remote/ambiguous \
-        refs/remotes/remote/branch-in-remote
+        refs/remotes/remote/with/slash/ambiguous \
+        refs/remotes/remote/with/slash/branch-in-remote
      do
          git update-ref $remote_ref main && test_when_finished "git update-ref -d $remote_ref" || return 1

t/t9902-completion.sh +0 -0
@@ -939,6 +966,8 @@ test_expect_success '__git_refs - after --opt=' '

      other/HEAD
      other/branch-in-other
      other/main-in-other
+    remote/with/slash/HEAD
+    remote/with/slash/branch/with/slash
      matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -955,6 +984,8 @@ test_expect_success '__git_refs - after --opt= - 

full refs' '
      refs/remotes/other/HEAD refs/remotes/other/branch-in-other refs/remotes/other/main-in-other
+    refs/remotes/remote/with/slash/HEAD
+    refs/remotes/remote/with/slash/branch/with/slash refs/tags/matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -972,6 +1003,8 @@ test_expect_success '__git refs - excluding refs' '

      ^other/HEAD
      ^other/branch-in-other
      ^other/main-in-other
+    ^remote/with/slash/HEAD
+    ^remote/with/slash/branch/with/slash
      ^matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -988,6 +1021,8 @@ test_expect_success '__git refs - excluding full 

refs' '
      ^refs/remotes/other/HEAD ^refs/remotes/other/branch-in-other ^refs/remotes/other/main-in-other
+    ^refs/remotes/remote/with/slash/HEAD
+    ^refs/remotes/remote/with/slash/branch/with/slash ^refs/tags/matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -1015,6 +1050,8 @@ test_expect_success '__git_refs - do not filter 

refs unless told so' '
      other/branch-in-other
      other/main-in-other
      other/matching/branch-in-other
+    remote/with/slash/HEAD
+    remote/with/slash/branch/with/slash
      matching-tag
      matching/tag
      EOF

t/t9902-completion.sh +0 -0
@@ -1135,6 +1172,8 @@ test_expect_success '__git_complete_refs - 

simple' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      matching-tag Z
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -1173,6 +1212,20 @@ test_expect_success '__git_complete_refs - 

remote' '
      test_cmp expected out
  '

t/t9902-completion.sh +14 -0
+test_expect_success '__git_complete_refs - remote - with slash' '
+    sed -e "s/Z$//" >expected <<-EOF &&
+    HEAD Z
+    HEAD Z
+    branch/with/slash Z
+    EOF
+    (
+        cur= &&
+        __git_complete_refs --remote=remote/with/slash &&
+        print_comp
+    ) &&
+    test_cmp expected out
+'
+

  test_expect_success '__git_complete_refs - track' ' sed -e "s/Z$//" >expected <<-EOF && HEAD Z

t/t9902-completion.sh +0 -0
@@ -1181,9 +1234,11 @@ test_expect_success '__git_complete_refs - 

track' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      matching-tag Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main-in-other Z
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -1228,6 +1283,8 @@ test_expect_success '__git_complete_refs - 

suffix' '
      other/HEAD.
      other/branch-in-other.
      other/main-in-other.
+    remote/with/slash/HEAD.
+    remote/with/slash/branch/with/slash.
      matching-tag.
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -1253,6 +1310,20 @@ test_expect_success 

'__git_complete_fetch_refspecs - simple' '
      test_cmp expected out
  '

t/t9902-completion.sh +14 -0
+test_expect_success '__git_complete_fetch_refspecs - with slash' '
+    sed -e "s/Z$//" >expected <<-EOF &&
+    HEAD:HEAD Z
+    HEAD:HEAD Z
+    branch/with/slash:branch/with/slash Z
+    EOF
+    (
+        cur= &&
+        __git_complete_fetch_refspecs remote/with/slash &&
+        print_comp
+    ) &&
+    test_cmp expected out
+'
+

  test_expect_success '__git_complete_fetch_refspecs - matching' ' sed -e "s/Z$//" >expected <<-EOF && branch-in-other:branch-in-other Z

t/t9902-completion.sh +0 -0
@@ -1333,8 +1404,8 @@ test_expect_success 

'__git_complete_worktree_paths with -C' '
  test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' ' test_completion "git switch " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1480,8 +1551,8 @@ test_expect_success 'git-bisect - existing view 

subcommand is recognized and ena
  test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' ' test_completion "git checkout " <<-\EOF
      HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1489,6 +1560,8 @@ test_expect_success 'git checkout - completes 

refs and unique remote branches fo
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1508,8 +1581,8 @@ test_expect_success 'git switch - with 

GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
  test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' ' GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch
--guess " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1518,8 +1591,8 @@ test_expect_success 'git switch - --guess 

overrides GIT_COMPLETION_CHECKOUT_NO_G
  test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' ' test_completion "git switch --no-guess --guess " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1542,14 +1615,16 @@ test_expect_success 'git checkout - with 

GIT_COMPLETION_NO_GUESS=1 only complete
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '
  test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' ' GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
      HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1557,6 +1632,8 @@ test_expect_success 'git checkout - --guess 
 overrides GIT_COMPLETION_NO_GUESS=1,

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1569,14 +1646,16 @@ test_expect_success 'git checkout - with -- 

no-guess, only completes refs' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '
  test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' ' test_completion "git checkout --no-guess --guess " <<-\EOF
      HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1584,6 +1663,8 @@ test_expect_success 'git checkout - a later -- 

guess overrides previous --no-gues
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1596,6 +1677,8 @@ test_expect_success 'git checkout - a later -- 

no-guess overrides previous --gues
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1609,6 +1692,8 @@ test_expect_success 'git checkout - with 

checkout.guess = false, only completes
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1616,8 +1701,8 @@ test_expect_success 'git checkout - with 

checkout.guess = true, completes refs a
      test_config checkout.guess true &&
      test_completion "git checkout " <<-\EOF
      HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1625,6 +1710,8 @@ test_expect_success 'git checkout - with 

checkout.guess = true, completes refs a
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1632,8 +1719,8 @@ test_expect_success 'git checkout - a later -- 

guess overrides previous checkout. test_config checkout.guess false && test_completion "git checkout --guess " <<-\EOF HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1641,6 +1728,8 @@ test_expect_success 'git checkout - a later -- 

guess overrides previous checkout.
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1654,6 +1743,8 @@ test_expect_success 'git checkout - a later -- 

no-guess overrides previous checko
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1666,6 +1757,8 @@ test_expect_success 'git switch - with -- 

detach, complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1678,6 +1771,8 @@ test_expect_success 'git checkout - with -- 

detach, complete only references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1850,6 +1945,8 @@ test_expect_success 'git switch - with -d, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1862,6 +1959,8 @@ test_expect_success 'git checkout - with -d, 

complete only references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1870,11 +1969,15 @@ test_expect_success 'git switch - with -- 

track, complete only remote branches' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
      test_completion "git switch -t " <<-\EOF
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1883,11 +1986,15 @@ test_expect_success 'git checkout - with -- 

track, complete only remote branches'
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
      test_completion "git checkout -t " <<-\EOF
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1907,6 +2014,8 @@ test_expect_success 'git checkout - with --no- 

track, complete only local referen
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1919,6 +2028,8 @@ test_expect_success 'git switch - with -c, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1931,6 +2042,8 @@ test_expect_success 'git switch - with -C, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -1943,6 +2056,8 @@ test_expect_success 'git switch - with -c and 
--track, complete all references'

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -1955,6 +2070,8 @@ test_expect_success 'git switch - with -C and 
--track, complete all references'

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -1967,6 +2084,8 @@ test_expect_success 'git switch - with -c and 
--no-track, complete all reference

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -1979,6 +2098,8 @@ test_expect_success 'git switch - with -C and 
--no-track, complete all reference

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1991,6 +2112,8 @@ test_expect_success 'git checkout - with -b, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -2003,6 +2126,8 @@ test_expect_success 'git checkout - with -B, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -2015,6 +2140,8 @@ test_expect_success 'git checkout - with -b and 
--track, complete all references

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -2027,6 +2154,8 @@ test_expect_success 'git checkout - with -B and 
--track, complete all references

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -2039,6 +2168,8 @@ test_expect_success 'git checkout - with -b and 
--no-track, complete all referen

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -2051,13 +2182,15 @@ test_expect_success 'git checkout - with -B 

and --no-track, complete all referen
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '
  test_expect_success 'git switch - for -c, complete local branches and unique remote branches' ' test_completion "git switch -c " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -2066,8 +2199,8 @@ test_expect_success 'git switch - for -c, 

complete local branches and unique rem
  test_expect_success 'git switch - for -C, complete local branches and unique remote branches' ' test_completion "git switch -C " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -1
@@ -2104,8 +2237,8 @@ test_expect_success 'git switch - for -C with 
--no-track, complete local branche

  test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' ' test_completion "git checkout -b " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -2114,8 +2247,8 @@ test_expect_success 'git checkout - for -b, 

complete local branches and unique r
  test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' ' test_completion "git checkout -B " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -1
@@ -2152,8 +2285,8 @@ test_expect_success 'git checkout - for -B with 
--no-track, complete local branc

  test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' ' test_completion "git switch --orphan " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -2168,8 +2301,8 @@ test_expect_success 'git switch - --orphan with 

branch already provided complete
  test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' ' test_completion "git checkout --orphan " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -2185,6 +2318,8 @@ test_expect_success 'git checkout - --orphan 

with branch already provided comple
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -2199,7 +2334,8 @@ test_expect_success 'git restore completes 

modified files' '
  test_expect_success 'teardown after ref completion' ' git branch -d matching-branch &&
      git tag -d matching-tag &&
-    git remote remove other
+    git remote remove other &&
+    git remote remove remote/with/slash
  '

#30 lore raw

Hi David

Quoted 10 lines · David Mandelberg

Op 2025-03-06 om 11:35 schreef Phillip Wood:

Quoted 7 lines
Quoted 5 lines
Quoted 5 lines

+    __git for-each-ref --format='%(refname)' |
+        PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
+            IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
+            REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" | +        sort | uniq -u

I wonder if we can use "sort -u" here and drop uniq. It isn't quite the same as it will remove entries that are equal according to the current locale but I don't think that should matter.

Quoted 7 lines

That's what I thought when I first saw that, but uniq -u removes duplicates:

$ printf 'a\nb\na\n' | sort | uniq -u
b
$ printf 'a\nb\na\n' | sort -u
a
b

Oh, thanks for pointing that out

Quoted 14 lines

 >> I realized that this sends refs to awk that it doesn't need to. I'll

Quoted 13 lines
Quoted 1 line

apply this diff to the next version of this patch:

With that tweak this looks good. If there are no glob characters then "git for-each-ref" does a prefix match so strictly speaking you don't need the '**' but I don't think it matters in practice. I had one thought below but if you don't feel like spending more that on this I think what you have here is fine.

The filtering is O(number of remote refs * number of remotes). If we could sort the list of remotes and remote refs in the same order then we can reduce this to O(number of remote refs + number of remotes) by doing (in pseudo code)

     for ($ref in $remote_refs) {
     while (!starts_with($ref, "refs/remotes/$remote[$i]")) $i++;
     if (starts_with($ref, "refs/remotes/$remote[$i]/$cur_) print $ref;
     }

Quoted 3 lines
Quoted 1 line

I think we can get "git for-each-ref --sort=-refname" and "sort -r" to agree on a sorting order by setting LC_COLLATE=C, otherwise "sort" will sort according the the current locale whereas git sorts by bytes.

On my desktop, a similar-ish loop takes about 0.1s with 1000*1000 iterations. 1000 remote branches seems plausible but on the high side to me, and 1000 remotes seems very high to me. Do you still think it's worth optimizing? I do think your solution would work, but I think it would take a decent amount of testing to avoid the collation issues you mentioned, and off-by-one errors and the like. So I'd rather not do it unless there's a practical performance advantage, not just theoretical. (Which is what I thought about minimizing forks too, before I learned that it was a practical issue.)

Thanks for running a benchmark. I'm fine with leaving it as is, it is definitely simpler and easier to understand. We can always revisit the filtering in the future if it turns out to be a too slow for some reason.

Best Wishes

Phillip

Quoted 691 lines
$ time seq 1 1000 | LONG="$(seq 1 1000)" awk 'BEGIN { split(ENVIRON["LONG"], long, /\n/); } { for (i in long) { if ($0 == long[i]) { print; }  } }' > /dev/null

real    0m0,092s
user    0m0,067s
sys     0m0,028s

Quoted 684 lines

Best Wishes

Phillip

Quoted 679 lines
contrib/completion/git-completion.bash +0 -0
index 450fabc901..c9d014070c 100644
@@ -818,7 +818,7 @@ __git_dwim_remote_heads ()
                  }
          }

         '
-       __git for-each-ref --format='%(refname)' |
+       __git for-each-ref --format='%(refname)' 'refs/remotes/**' | PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
                         IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
                         REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" |

Quoted 664 lines

  }
  # Lists refs from the local (by default) or from a remote repository.

(quoted diff) +0 -0
@@ -905,7 +928,8 @@ __git_refs ()

              case "HEAD" in
              $match*|$umatch*)    echo "${pfx}HEAD$sfx" ;; esac
-            __git for-each-ref -- format="$fer_pfx%(refname:strip=3)$sfx" \
+            local strip="$(__git_count_path_components "refs/ remotes/$remote")"
+            __git for-each-ref -- format="$fer_pfx%(refname:strip=$strip)$sfx" \
                  ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
                  "refs/remotes/$remote/$match*" \
                  "refs/remotes/$remote/$match*/**"

t/t9902-completion.sh +0 -0
index 015289c776..343b8cd191 100755
@@ -149,7 +149,8 @@ fi

  test_expect_success 'setup for __git_find_repo_path/__gitdir tests' ' mkdir -p subdir/subsubdir &&
      mkdir -p non-repo &&
-    git init -b main otherrepo
+    git init -b main otherrepo &&
+    git init -b main slashrepo
  '
  test_expect_success '__git_find_repo_path - from command line (through $__git_dir)' '

t/t9902-completion.sh +0 -0
@@ -674,6 +675,13 @@ test_expect_success 'setup for ref completion' '

      ) &&
      git remote add other "$ROOT/otherrepo/.git" && git fetch --no-tags other &&
+    (
+        cd slashrepo &&
+        git commit --allow-empty -m initial &&
+        git branch -m main branch/with/slash
+    ) &&
+    git remote add remote/with/slash "$ROOT/slashrepo/.git" && +    git fetch --no-tags remote/with/slash &&
      rm -f .git/FETCH_HEAD &&
      git init thirdrepo
  '

t/t9902-completion.sh +0 -0
@@ -686,6 +694,8 @@ test_expect_success '__git_refs - simple' '

      other/HEAD
      other/branch-in-other
      other/main-in-other
+    remote/with/slash/HEAD
+    remote/with/slash/branch/with/slash
      matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -702,6 +712,8 @@ test_expect_success '__git_refs - full refs' '

      refs/remotes/other/HEAD refs/remotes/other/branch-in-other refs/remotes/other/main-in-other
+    refs/remotes/remote/with/slash/HEAD
+    refs/remotes/remote/with/slash/branch/with/slash refs/tags/matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -767,6 +779,19 @@ test_expect_success '__git_refs - configured 

remote' '
      test_cmp expected "$actual"
  '

t/t9902-completion.sh +13 -0
+test_expect_success '__git_refs - configured remote - with slash' '
+    cat >expected <<-EOF &&
+    HEAD
+    HEAD
+    branch/with/slash
+    EOF
+    (
+        cur= &&
+        __git_refs remote/with/slash >"$actual"
+    ) &&
+    test_cmp expected "$actual"
+'
+

  test_expect_success '__git_refs - configured remote - full refs' ' cat >expected <<-EOF &&
      HEAD

t/t9902-completion.sh +0 -0
@@ -909,17 +934,19 @@ test_expect_success '__git_refs - unique 

remote branches for git checkout DWIMer
      other/ambiguous
      other/branch-in-other
      other/main-in-other
-    remote/ambiguous
-    remote/branch-in-remote
+    remote/with/slash/HEAD
+    remote/with/slash/ambiguous
+    remote/with/slash/branch-in-remote
+    remote/with/slash/branch/with/slash
      matching-tag
-    HEAD
      branch-in-other
      branch-in-remote
+    branch/with/slash
      main-in-other
      EOF
      for remote_ref in refs/remotes/other/ambiguous \
-        refs/remotes/remote/ambiguous \
-        refs/remotes/remote/branch-in-remote
+        refs/remotes/remote/with/slash/ambiguous \
+        refs/remotes/remote/with/slash/branch-in-remote
      do
          git update-ref $remote_ref main && test_when_finished "git update-ref -d $remote_ref" || return 1

t/t9902-completion.sh +0 -0
@@ -939,6 +966,8 @@ test_expect_success '__git_refs - after --opt=' '

      other/HEAD
      other/branch-in-other
      other/main-in-other
+    remote/with/slash/HEAD
+    remote/with/slash/branch/with/slash
      matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -955,6 +984,8 @@ test_expect_success '__git_refs - after --opt= - 

full refs' '
      refs/remotes/other/HEAD refs/remotes/other/branch-in-other refs/remotes/other/main-in-other
+    refs/remotes/remote/with/slash/HEAD
+    refs/remotes/remote/with/slash/branch/with/slash refs/tags/matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -972,6 +1003,8 @@ test_expect_success '__git refs - excluding 

refs' '
      ^other/HEAD
      ^other/branch-in-other
      ^other/main-in-other
+    ^remote/with/slash/HEAD
+    ^remote/with/slash/branch/with/slash
      ^matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -988,6 +1021,8 @@ test_expect_success '__git refs - excluding 

full refs' '
      ^refs/remotes/other/HEAD ^refs/remotes/other/branch-in-other ^refs/remotes/other/main-in-other
+    ^refs/remotes/remote/with/slash/HEAD
+    ^refs/remotes/remote/with/slash/branch/with/slash ^refs/tags/matching-tag
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -1015,6 +1050,8 @@ test_expect_success '__git_refs - do not 

filter refs unless told so' '
      other/branch-in-other
      other/main-in-other
      other/matching/branch-in-other
+    remote/with/slash/HEAD
+    remote/with/slash/branch/with/slash
      matching-tag
      matching/tag
      EOF

t/t9902-completion.sh +0 -0
@@ -1135,6 +1172,8 @@ test_expect_success '__git_complete_refs - 

simple' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      matching-tag Z
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -1173,6 +1212,20 @@ test_expect_success '__git_complete_refs - 

remote' '
      test_cmp expected out
  '

t/t9902-completion.sh +14 -0
+test_expect_success '__git_complete_refs - remote - with slash' '
+    sed -e "s/Z$//" >expected <<-EOF &&
+    HEAD Z
+    HEAD Z
+    branch/with/slash Z
+    EOF
+    (
+        cur= &&
+        __git_complete_refs --remote=remote/with/slash &&
+        print_comp
+    ) &&
+    test_cmp expected out
+'
+

  test_expect_success '__git_complete_refs - track' ' sed -e "s/Z$//" >expected <<-EOF && HEAD Z

t/t9902-completion.sh +0 -0
@@ -1181,9 +1234,11 @@ test_expect_success '__git_complete_refs - 

track' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      matching-tag Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main-in-other Z
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -1228,6 +1283,8 @@ test_expect_success '__git_complete_refs - 

suffix' '
      other/HEAD.
      other/branch-in-other.
      other/main-in-other.
+    remote/with/slash/HEAD.
+    remote/with/slash/branch/with/slash.
      matching-tag.
      EOF
      (

t/t9902-completion.sh +0 -0
@@ -1253,6 +1310,20 @@ test_expect_success 

'__git_complete_fetch_refspecs - simple' '
      test_cmp expected out
  '

t/t9902-completion.sh +14 -0
+test_expect_success '__git_complete_fetch_refspecs - with slash' '
+    sed -e "s/Z$//" >expected <<-EOF &&
+    HEAD:HEAD Z
+    HEAD:HEAD Z
+    branch/with/slash:branch/with/slash Z
+    EOF
+    (
+        cur= &&
+        __git_complete_fetch_refspecs remote/with/slash &&
+        print_comp
+    ) &&
+    test_cmp expected out
+'
+

  test_expect_success '__git_complete_fetch_refspecs - matching' ' sed -e "s/Z$//" >expected <<-EOF && branch-in-other:branch-in-other Z

t/t9902-completion.sh +0 -0
@@ -1333,8 +1404,8 @@ test_expect_success 

'__git_complete_worktree_paths with -C' '
  test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' ' test_completion "git switch " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1480,8 +1551,8 @@ test_expect_success 'git-bisect - existing 

view subcommand is recognized and ena
  test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' ' test_completion "git checkout " <<-\EOF
      HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1489,6 +1560,8 @@ test_expect_success 'git checkout - completes 

refs and unique remote branches fo
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1508,8 +1581,8 @@ test_expect_success 'git switch - with 

GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
  test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' ' GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch
--guess " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1518,8 +1591,8 @@ test_expect_success 'git switch - --guess 

overrides GIT_COMPLETION_CHECKOUT_NO_G
  test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' ' test_completion "git switch --no-guess --guess " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1542,14 +1615,16 @@ test_expect_success 'git checkout - with 

GIT_COMPLETION_NO_GUESS=1 only complete
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '
  test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' ' GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
      HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1557,6 +1632,8 @@ test_expect_success 'git checkout - --guess 
 overrides GIT_COMPLETION_NO_GUESS=1,

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1569,14 +1646,16 @@ test_expect_success 'git checkout - with -- 

no-guess, only completes refs' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '
  test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' ' test_completion "git checkout --no-guess --guess " <<-\EOF
      HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1584,6 +1663,8 @@ test_expect_success 'git checkout - a later -- 

guess overrides previous --no-gues
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1596,6 +1677,8 @@ test_expect_success 'git checkout - a later -- 

no-guess overrides previous --gues
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1609,6 +1692,8 @@ test_expect_success 'git checkout - with 

checkout.guess = false, only completes
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1616,8 +1701,8 @@ test_expect_success 'git checkout - with 

checkout.guess = true, completes refs a
      test_config checkout.guess true &&
      test_completion "git checkout " <<-\EOF
      HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1625,6 +1710,8 @@ test_expect_success 'git checkout - with 

checkout.guess = true, completes refs a
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1632,8 +1719,8 @@ test_expect_success 'git checkout - a later -- 

guess overrides previous checkout. test_config checkout.guess false && test_completion "git checkout --guess " <<-\EOF HEAD Z
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -1641,6 +1728,8 @@ test_expect_success 'git checkout - a later -- 

guess overrides previous checkout.
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1654,6 +1743,8 @@ test_expect_success 'git checkout - a later -- 

no-guess overrides previous checko
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1666,6 +1757,8 @@ test_expect_success 'git switch - with -- 

detach, complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1678,6 +1771,8 @@ test_expect_success 'git checkout - with -- 

detach, complete only references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1850,6 +1945,8 @@ test_expect_success 'git switch - with -d, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1862,6 +1959,8 @@ test_expect_success 'git checkout - with -d, 

complete only references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1870,11 +1969,15 @@ test_expect_success 'git switch - with -- 

track, complete only remote branches' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
      test_completion "git switch -t " <<-\EOF
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1883,11 +1986,15 @@ test_expect_success 'git checkout - with -- 

track, complete only remote branches'
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
      test_completion "git checkout -t " <<-\EOF
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1907,6 +2014,8 @@ test_expect_success 'git checkout - with --no- 

track, complete only local referen
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1919,6 +2028,8 @@ test_expect_success 'git switch - with -c, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1931,6 +2042,8 @@ test_expect_success 'git switch - with -C, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -1943,6 +2056,8 @@ test_expect_success 'git switch - with -c and 
--track, complete all references'

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -1955,6 +2070,8 @@ test_expect_success 'git switch - with -C and 
--track, complete all references'

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -1967,6 +2084,8 @@ test_expect_success 'git switch - with -c and 
--no-track, complete all reference

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -1
@@ -1979,6 +2098,8 @@ test_expect_success 'git switch - with -C and 
--no-track, complete all reference

      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -1991,6 +2112,8 @@ test_expect_success 'git checkout - with -b, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -2003,6 +2126,8 @@ test_expect_success 'git checkout - with -B, 

complete all references' '
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -2015,6 +2140,8 @@ test_expect_success 'git checkout - with -b 

and --track, complete all references
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -2027,6 +2154,8 @@ test_expect_success 'git checkout - with -B 

and --track, complete all references
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -2039,6 +2168,8 @@ test_expect_success 'git checkout - with -b 

and --no-track, complete all referen
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -2051,13 +2182,15 @@ test_expect_success 'git checkout - with -B 

and --no-track, complete all referen
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '
  test_expect_success 'git switch - for -c, complete local branches and unique remote branches' ' test_completion "git switch -c " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -2066,8 +2199,8 @@ test_expect_success 'git switch - for -c, 

complete local branches and unique rem
  test_expect_success 'git switch - for -C, complete local branches and unique remote branches' ' test_completion "git switch -C " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -1
@@ -2104,8 +2237,8 @@ test_expect_success 'git switch - for -C with 
--no-track, complete local branche

  test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' ' test_completion "git checkout -b " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -2114,8 +2247,8 @@ test_expect_success 'git checkout - for -b, 

complete local branches and unique r
  test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' ' test_completion "git checkout -B " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -2152,8 +2285,8 @@ test_expect_success 'git checkout - for -B 

with --no-track, complete local branc
  test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' ' test_completion "git switch --orphan " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -2168,8 +2301,8 @@ test_expect_success 'git switch - --orphan 

with branch already provided complete
  test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' ' test_completion "git checkout --orphan " <<-\EOF
-    HEAD Z
      branch-in-other Z
+    branch/with/slash Z
      main Z
      main-in-other Z
      matching-branch Z

t/t9902-completion.sh +0 -0
@@ -2185,6 +2318,8 @@ test_expect_success 'git checkout - --orphan 

with branch already provided comple
      other/HEAD Z
      other/branch-in-other Z
      other/main-in-other Z
+    remote/with/slash/HEAD Z
+    remote/with/slash/branch/with/slash Z
      EOF
  '

t/t9902-completion.sh +0 -0
@@ -2199,7 +2334,8 @@ test_expect_success 'git restore completes 

modified files' '
  test_expect_success 'teardown after ref completion' ' git branch -d matching-branch &&
      git tag -d matching-tag &&
-    git remote remove other
+    git remote remove other &&
+    git remote remove remote/with/slash
  '

#31 lore raw

v3 cover completion: fix bugs with slashes in remote names

In reply to David Mandelberg via GitGitGadget

It's been a bit over a week since the last version of this, with some discussion, but I don't think any requests for changes. So here's an updated version with the minor performance change I mentioned in
https://lore.kernel.org/git/[email protected]/

David Mandelberg (2):
completion: add helper to count path components
completion: fix bugs with slashes in remote names

 contrib/completion/git-completion.bash |  49 +++++-
 t/t9902-completion.sh                  | 206 ++++++++++++++++++++++---
 2 files changed, 226 insertions(+), 29 deletions(-)
Signature · 1 line
2.47.2
#32 lore raw

PATCH v3 1/2 completion: add helper to count path components

A follow-up commit will use this with for-each-ref to strip the right number of path components from refnames.

---
 contrib/completion/git-completion.bash | 11 +++++++++++
 t/t9902-completion.sh                  | 26 ++++++++++++++++++++++++++
 2 files changed, 37 insertions(+)
contrib/completion/git-completion.bash +11 -0
index 413911be3b..5fdc71208e 100644
@@ -234,6 +234,17 @@ __git_dequote ()
 	done
 }
 
+# Prints the number of slash-separated components in a path.
+# 1: Path to count components of.
+__git_count_path_components ()
+{
+	local path="$1"
+	local relative="${path#/}"
+	relative="${relative%/}"
+	local slashes="/${relative//[^\/]}"
+	echo "${#slashes}"
+}
+
 # The following function is based on code from:
 #
 #   bash_completion - programmable completion functions for bash 3.2+
t/t9902-completion.sh +26 -0
index 51bd750837..015289c776 100755
@@ -455,6 +455,32 @@ test_expect_success '__git_dequote - open double quote' '
 '
 
 
+test_expect_success '__git_count_path_components - no slashes' '
+	echo 1 >expected &&
+	__git_count_path_components a >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+test_expect_success '__git_count_path_components - relative' '
+	echo 3 >expected &&
+	__git_count_path_components a/b/c >"$actual" &&
+	test_cmp expected "$actual"
+
+'
+
+test_expect_success '__git_count_path_components - absolute' '
+	echo 3 >expected &&
+	__git_count_path_components /a/b/c >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+test_expect_success '__git_count_path_components - trailing slash' '
+	echo 3 >expected &&
+	__git_count_path_components a/b/c/ >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+
 test_expect_success '__gitcomp_direct - puts everything into COMPREPLY as-is' '
 	sed -e "s/Z$//g" >expected <<-EOF &&
 	with-trailing-space Z
Signature · 1 line
2.47.2
#33 lore raw

PATCH v3 2/2 completion: fix bugs with slashes in remote names

In reply to David Mandelberg

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

---
 contrib/completion/git-completion.bash |  38 +++++-
 t/t9902-completion.sh                  | 180 ++++++++++++++++++++++---
 2 files changed, 189 insertions(+), 29 deletions(-)
contrib/completion/git-completion.bash +31 -7
index 5fdc71208e..c9d014070c 100644
@@ -790,16 +790,39 @@ __git_tags ()
 __git_dwim_remote_heads ()
 {
 	local pfx="${1-}" cur_="${2-}" sfx="${3-}"
-	local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
 
 	# 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" \
-		${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
-		"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
-	uniq -u
+	local awk_script='
+	function casemap(s) {
+		if (ENVIRON["IGNORE_CASE"])
+			return tolower(s)
+		else
+			return s
+	}
+	BEGIN {
+		split(ENVIRON["REMOTES"], remotes, /\n/)
+		for (i in remotes)
+			remotes[i] = "refs/remotes/" casemap(remotes[i])
+		cur_ = casemap(ENVIRON["CUR_"])
+	}
+	{
+		ref_case = casemap($0)
+		for (i in remotes) {
+			if (index(ref_case, remotes[i] "/" cur_) == 1) {
+				branch = substr($0, length(remotes[i] "/") + 1)
+				print ENVIRON["PFX"] branch ENVIRON["SFX"]
+				break
+			}
+		}
+	}
+	'
+	__git for-each-ref --format='%(refname)' 'refs/remotes/**' |
+		PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
+			IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
+			REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" |
+		sort | uniq -u
 }
 
 # Lists refs from the local (by default) or from a remote repository.
@@ -905,7 +928,8 @@ __git_refs ()
 			case "HEAD" in
 			$match*|$umatch*)	echo "${pfx}HEAD$sfx" ;;
 			esac
-			__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
+			local strip="$(__git_count_path_components "refs/remotes/$remote")"
+			__git for-each-ref --format="$fer_pfx%(refname:strip=$strip)$sfx" \
 				${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
 				"refs/remotes/$remote/$match*" \
 				"refs/remotes/$remote/$match*/**"
t/t9902-completion.sh +158 -22

577 lines not on this page · raw message

Signature · 1 line
2.47.2
#34 lore raw

contrib/completion/git-completion.bash __git_tags () · line 821

Quoted 1 line · David Mandelberg
contrib/completion/git-completion.bash +1 -0
+	__git for-each-ref --format='%(refname)' 'refs/remotes/**' |

Just a bit of curiosity, but how would this be different from using

	__git for-each-ref --format='%(refname)' refs/remotes/

i.e. the prefix match, here? Does the wildmatcher clever enough to optimize "**" away and make it just to iterate over refs/remotes/ hierarchy without filtering anything?

#35 lore raw

Op 2025-03-14 om 18:18 schreef Junio C Hamano:

Quoted 11 lines

contrib/completion/git-completion.bash __git_tags () · line 821

Quoted 1 line · David Mandelberg
contrib/completion/git-completion.bash +1 -0
+	__git for-each-ref --format='%(refname)' 'refs/remotes/**' |

Just a bit of curiosity, but how would this be different from using

	__git for-each-ref --format='%(refname)' refs/remotes/

i.e. the prefix match, here? Does the wildmatcher clever enough to optimize "**" away and make it just to iterate over refs/remotes/ hierarchy without filtering anything?

Oh right, I lost track of that bit. When I wrote that, I didn't know that for-each-ref did prefix matching. Phillip Wood pointed it out:

Quoted 1 line

If there are no glob characters then "git for-each-ref" does a prefix match so strictly speaking you don't need the '**' but I don't think it matters in practice.

But I forgot about it when I sent v3.

Do you want me to change it? Should I send a v4 now, or just make a fixup commit locally so I don't forget it when I send a v4 in the future?

#36 lore raw
Quoted 13 lines · David Mandelberg

Op 2025-03-14 om 18:18 schreef Junio C Hamano:

Quoted 8 lines

contrib/completion/git-completion.bash __git_tags () · line 821

Quoted 1 line · David Mandelberg
contrib/completion/git-completion.bash +1 -0
+	__git for-each-ref --format='%(refname)' 'refs/remotes/**' |

Just a bit of curiosity, but how would this be different from using

	__git for-each-ref --format='%(refname)' refs/remotes/

i.e. the prefix match, here? Does the wildmatcher clever enough to optimize "**" away and make it just to iterate over refs/remotes/ hierarchy without filtering anything?

Oh right, I lost track of that bit. When I wrote that, I didn't know that for-each-ref did prefix matching. Phillip Wood pointed it out:

Quoted 7 lines
Quoted 1 line

If there are no glob characters then "git for-each-ref" does a prefix match so strictly speaking you don't need the '**' but I don't think it matters in practice.

But I forgot about it when I sent v3.

Do you want me to change it? Should I send a v4 now, or just make a fixup commit locally so I don't forget it when I send a v4 in the future?

Entirely up to you. I do not plan to perform an in-detail review on the topic (I do not consider myself quite qualified), but others who saw earlier rounds of the patch may want to say "now this is perfect" or "now this is perfect with this and that changes", and if you do not think the differenes between v3 and your upcoming v4 are small enough, it may be more efficient to wait for a bit to collect more comments.

Thanks.

#37 lore raw
Quoted 7 lines · Junio C Hamano

Entirely up to you. I do not plan to perform an in-detail review on the topic (I do not consider myself quite qualified), but others who saw earlier rounds of the patch may want to say "now this is perfect" or "now this is perfect with this and that changes", and if you do not think the differenes between v3 and your upcoming v4 are small enough, it may be more efficient to wait for a bit to collect more comments.

I'm not an expert on our completion script but from a general shell scripting perspective this version looks fine to me.

Best Wishes

Phillip

#38 lore raw
Quoted 10 lines · Phillip Wood
Quoted 6 lines · Junio C Hamano

Entirely up to you. I do not plan to perform an in-detail review on the topic (I do not consider myself quite qualified), but others who saw earlier rounds of the patch may want to say "now this is perfect" or "now this is perfect with this and that changes", and if you do not think the differenes between v3 and your upcoming v4 are small enough, it may be more efficient to wait for a bit to collect more comments.

I'm not an expert on our completion script but from a general shell scripting perspective this version looks fine to me.

OK, then let me see if I can squash in before merging the topic to 'next'.

#39 lore raw

v4 cover completion: fix bugs with slashes in remote names

In reply to David Mandelberg via GitGitGadget

Another minor change, as suggested by
https://lore.kernel.org/git/[email protected]/

I'm not sure what the expectations are around updating patches that are scheduled to be merged to next but haven't been merged yet. If it's too late, I'm happy with the previous v3, or I could send a separate patch.

David Mandelberg (2):
completion: add helper to count path components
completion: fix bugs with slashes in remote names

 contrib/completion/git-completion.bash |  49 +++++-
 t/t9902-completion.sh                  | 206 ++++++++++++++++++++++---
 2 files changed, 226 insertions(+), 29 deletions(-)
Signature · 1 line
2.47.2
#40 lore raw

PATCH v4 1/2 completion: add helper to count path components

A follow-up commit will use this with for-each-ref to strip the right number of path components from refnames.

---
 contrib/completion/git-completion.bash | 11 +++++++++++
 t/t9902-completion.sh                  | 26 ++++++++++++++++++++++++++
 2 files changed, 37 insertions(+)
contrib/completion/git-completion.bash +11 -0
index 413911be3b..5fdc71208e 100644
@@ -234,6 +234,17 @@ __git_dequote ()
 	done
 }
 
+# Prints the number of slash-separated components in a path.
+# 1: Path to count components of.
+__git_count_path_components ()
+{
+	local path="$1"
+	local relative="${path#/}"
+	relative="${relative%/}"
+	local slashes="/${relative//[^\/]}"
+	echo "${#slashes}"
+}
+
 # The following function is based on code from:
 #
 #   bash_completion - programmable completion functions for bash 3.2+
t/t9902-completion.sh +26 -0
index 51bd750837..015289c776 100755
@@ -455,6 +455,32 @@ test_expect_success '__git_dequote - open double quote' '
 '
 
 
+test_expect_success '__git_count_path_components - no slashes' '
+	echo 1 >expected &&
+	__git_count_path_components a >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+test_expect_success '__git_count_path_components - relative' '
+	echo 3 >expected &&
+	__git_count_path_components a/b/c >"$actual" &&
+	test_cmp expected "$actual"
+
+'
+
+test_expect_success '__git_count_path_components - absolute' '
+	echo 3 >expected &&
+	__git_count_path_components /a/b/c >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+test_expect_success '__git_count_path_components - trailing slash' '
+	echo 3 >expected &&
+	__git_count_path_components a/b/c/ >"$actual" &&
+	test_cmp expected "$actual"
+'
+
+
 test_expect_success '__gitcomp_direct - puts everything into COMPREPLY as-is' '
 	sed -e "s/Z$//g" >expected <<-EOF &&
 	with-trailing-space Z
Signature · 1 line
2.47.2
#41 lore raw

PATCH v4 2/2 completion: fix bugs with slashes in remote names

In reply to David Mandelberg

Previously, some calls to for-each-ref passed fixed numbers of path components to strip from refs, assuming that remote names had no slashes in them. This made completions like:

git push github/dseomn :com<Tab>

Result in:

git push github/dseomn :dseomn/completion-remote-slash

With this patch, it instead results in:

git push github/dseomn :completion-remote-slash

---
 contrib/completion/git-completion.bash |  38 +++++-
 t/t9902-completion.sh                  | 180 ++++++++++++++++++++++---
 2 files changed, 189 insertions(+), 29 deletions(-)
contrib/completion/git-completion.bash +31 -7
index 5fdc71208e..e3d88b0672 100644
@@ -790,16 +790,39 @@ __git_tags ()
 __git_dwim_remote_heads ()
 {
 	local pfx="${1-}" cur_="${2-}" sfx="${3-}"
-	local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
 
 	# 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" \
-		${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
-		"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
-	uniq -u
+	local awk_script='
+	function casemap(s) {
+		if (ENVIRON["IGNORE_CASE"])
+			return tolower(s)
+		else
+			return s
+	}
+	BEGIN {
+		split(ENVIRON["REMOTES"], remotes, /\n/)
+		for (i in remotes)
+			remotes[i] = "refs/remotes/" casemap(remotes[i])
+		cur_ = casemap(ENVIRON["CUR_"])
+	}
+	{
+		ref_case = casemap($0)
+		for (i in remotes) {
+			if (index(ref_case, remotes[i] "/" cur_) == 1) {
+				branch = substr($0, length(remotes[i] "/") + 1)
+				print ENVIRON["PFX"] branch ENVIRON["SFX"]
+				break
+			}
+		}
+	}
+	'
+	__git for-each-ref --format='%(refname)' refs/remotes/ |
+		PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
+			IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
+			REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" |
+		sort | uniq -u
 }
 
 # Lists refs from the local (by default) or from a remote repository.
@@ -905,7 +928,8 @@ __git_refs ()
 			case "HEAD" in
 			$match*|$umatch*)	echo "${pfx}HEAD$sfx" ;;
 			esac
-			__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
+			local strip="$(__git_count_path_components "refs/remotes/$remote")"
+			__git for-each-ref --format="$fer_pfx%(refname:strip=$strip)$sfx" \
 				${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \
 				"refs/remotes/$remote/$match*" \
 				"refs/remotes/$remote/$match*/**"
t/t9902-completion.sh +158 -22

577 lines not on this page · raw message

Signature · 1 line
2.47.2
#42 lore raw
In reply to David Mandelberg

Hi David

Quoted 6 lines · David Mandelberg

Another minor change, as suggested by
https://lore.kernel.org/git/[email protected]/

I'm not sure what the expectations are around updating patches that are scheduled to be merged to next but haven't been merged yet. If it's too late, I'm happy with the previous v3, or I could send a separate patch.

Unless a patch series has already been merged to next which in not the case here sending a new round of patches like this is the right thing to do. The range-diff (which you can add to your cover letter by passing "--range-diff" to "git format-patch") below looks good to me

Sorry for the slow response and thanks for working on this

Phillip

1:  5f4f1eb105 = 1:  58d036a048 completion: add helper to count path components
2:  28c0f80345 ! 2:  0383e1c608 completion: fix bugs with slashes in remote names

@@ contrib/completion/git-completion.bash: __git_tags ()
+ }
+ }
+ '
-+ __git for-each-ref --format='%(refname)' 'refs/remotes/**' | ++ __git for-each-ref --format='%(refname)' refs/remotes/ |
+ PFX="$pfx" SFX="$sfx" CUR_="$cur_" \
+ IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \
+ REMOTES="$(__git_remotes | sort -r)" awk
"$awk_script" |

Quoted 8 lines

David Mandelberg (2):
completion: add helper to count path components
completion: fix bugs with slashes in remote names

  contrib/completion/git-completion.bash |  49 +++++-
  t/t9902-completion.sh                  | 206 ++++++++++++++++++++++---
  2 files changed, 226 insertions(+), 29 deletions(-)
#43 lore raw

Op 2025-04-08 om 10:18 schreef Phillip Wood:

Quoted 1 line

Unless a patch series has already been merged to next which in not the case here sending a new round of patches like this is the right thing to do.

Makes sense, thanks!

Quoted 1 line

The range-diff (which you can add to your cover letter by passing "--range-diff" to "git format-patch") below looks good to me

Oh, nice. I only learned about range-diff recently and it's my new favorite git command. I'll use git format-patch --range-diff in the future.