Skip to content

Commit 4ce7218

Browse files
committed
Merge branch 'sg/complete-paths'
Command line completion (in contrib/) learned to complete pathnames for various commands better. * sg/complete-paths: t9902-completion: exercise __git_complete_index_file() directly completion: don't return with error from __gitcomp_file_direct() completion: fill COMPREPLY directly when completing paths completion: improve handling quoted paths in 'git ls-files's output completion: remove repeated dirnames with 'awk' during path completion t9902-completion: ignore COMPREPLY element order in some tests completion: use 'awk' to strip trailing path components completion: let 'ls-files' and 'diff-index' filter matching paths completion: improve handling quoted paths on the command line completion: support completing non-ASCII pathnames completion: simplify prefix path component handling during path completion completion: move __git_complete_index_file() next to its helpers t9902-completion: add tests demonstrating issues with quoted pathnames
2 parents 6105fee + 7d31407 commit 4ce7218

File tree

3 files changed

+362
-28
lines changed

3 files changed

+362
-28
lines changed

contrib/completion/git-completion.bash

Lines changed: 191 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,70 @@ __git ()
9494
${__git_dir:+--git-dir="$__git_dir"} "$@" 2>/dev/null
9595
}
9696

97+
# Removes backslash escaping, single quotes and double quotes from a word,
98+
# stores the result in the variable $dequoted_word.
99+
# 1: The word to dequote.
100+
__git_dequote ()
101+
{
102+
local rest="$1" len ch
103+
104+
dequoted_word=""
105+
106+
while test -n "$rest"; do
107+
len=${#dequoted_word}
108+
dequoted_word="$dequoted_word${rest%%[\\\'\"]*}"
109+
rest="${rest:$((${#dequoted_word}-$len))}"
110+
111+
case "${rest:0:1}" in
112+
\\)
113+
ch="${rest:1:1}"
114+
case "$ch" in
115+
$'\n')
116+
;;
117+
*)
118+
dequoted_word="$dequoted_word$ch"
119+
;;
120+
esac
121+
rest="${rest:2}"
122+
;;
123+
\')
124+
rest="${rest:1}"
125+
len=${#dequoted_word}
126+
dequoted_word="$dequoted_word${rest%%\'*}"
127+
rest="${rest:$((${#dequoted_word}-$len+1))}"
128+
;;
129+
\")
130+
rest="${rest:1}"
131+
while test -n "$rest" ; do
132+
len=${#dequoted_word}
133+
dequoted_word="$dequoted_word${rest%%[\\\"]*}"
134+
rest="${rest:$((${#dequoted_word}-$len))}"
135+
case "${rest:0:1}" in
136+
\\)
137+
ch="${rest:1:1}"
138+
case "$ch" in
139+
\"|\\|\$|\`)
140+
dequoted_word="$dequoted_word$ch"
141+
;;
142+
$'\n')
143+
;;
144+
*)
145+
dequoted_word="$dequoted_word\\$ch"
146+
;;
147+
esac
148+
rest="${rest:2}"
149+
;;
150+
\")
151+
rest="${rest:1}"
152+
break
153+
;;
154+
esac
155+
done
156+
;;
157+
esac
158+
done
159+
}
160+
97161
# The following function is based on code from:
98162
#
99163
# bash_completion - programmable completion functions for bash 3.2+
@@ -346,6 +410,24 @@ __gitcomp_nl ()
346410
__gitcomp_nl_append "$@"
347411
}
348412

413+
# Fills the COMPREPLY array with prefiltered paths without any additional
414+
# processing.
415+
# Callers must take care of providing only paths that match the current path
416+
# to be completed and adding any prefix path components, if necessary.
417+
# 1: List of newline-separated matching paths, complete with all prefix
418+
# path componens.
419+
__gitcomp_file_direct ()
420+
{
421+
local IFS=$'\n'
422+
423+
COMPREPLY=($1)
424+
425+
# use a hack to enable file mode in bash < 4
426+
compopt -o filenames +o nospace 2>/dev/null ||
427+
compgen -f /non-existing-dir/ >/dev/null ||
428+
true
429+
}
430+
349431
# Generates completion reply with compgen from newline-separated possible
350432
# completion filenames.
351433
# It accepts 1 to 3 arguments:
@@ -365,7 +447,8 @@ __gitcomp_file ()
365447

366448
# use a hack to enable file mode in bash < 4
367449
compopt -o filenames +o nospace 2>/dev/null ||
368-
compgen -f /non-existing-dir/ > /dev/null
450+
compgen -f /non-existing-dir/ >/dev/null ||
451+
true
369452
}
370453

371454
# Execute 'git ls-files', unless the --committable option is specified, in
@@ -375,10 +458,12 @@ __gitcomp_file ()
375458
__git_ls_files_helper ()
376459
{
377460
if [ "$2" == "--committable" ]; then
378-
__git -C "$1" diff-index --name-only --relative HEAD
461+
__git -C "$1" -c core.quotePath=false diff-index \
462+
--name-only --relative HEAD -- "${3//\\/\\\\}*"
379463
else
380464
# NOTE: $2 is not quoted in order to support multiple options
381-
__git -C "$1" ls-files --exclude-standard $2
465+
__git -C "$1" -c core.quotePath=false ls-files \
466+
--exclude-standard $2 -- "${3//\\/\\\\}*"
382467
fi
383468
}
384469

@@ -389,12 +474,103 @@ __git_ls_files_helper ()
389474
# If provided, only files within the specified directory are listed.
390475
# Sub directories are never recursed. Path must have a trailing
391476
# slash.
477+
# 3: List only paths matching this path component (optional).
392478
__git_index_files ()
393479
{
394-
local root="${2-.}" file
480+
local root="$2" match="$3"
395481

396-
__git_ls_files_helper "$root" "$1" |
397-
cut -f1 -d/ | sort | uniq
482+
__git_ls_files_helper "$root" "$1" "$match" |
483+
awk -F / -v pfx="${2//\\/\\\\}" '{
484+
paths[$1] = 1
485+
}
486+
END {
487+
for (p in paths) {
488+
if (substr(p, 1, 1) != "\"") {
489+
# No special characters, easy!
490+
print pfx p
491+
continue
492+
}
493+
494+
# The path is quoted.
495+
p = dequote(p)
496+
if (p == "")
497+
continue
498+
499+
# Even when a directory name itself does not contain
500+
# any special characters, it will still be quoted if
501+
# any of its (stripped) trailing path components do.
502+
# Because of this we may have seen the same direcory
503+
# both quoted and unquoted.
504+
if (p in paths)
505+
# We have seen the same directory unquoted,
506+
# skip it.
507+
continue
508+
else
509+
print pfx p
510+
}
511+
}
512+
function dequote(p, bs_idx, out, esc, esc_idx, dec) {
513+
# Skip opening double quote.
514+
p = substr(p, 2)
515+
516+
# Interpret backslash escape sequences.
517+
while ((bs_idx = index(p, "\\")) != 0) {
518+
out = out substr(p, 1, bs_idx - 1)
519+
esc = substr(p, bs_idx + 1, 1)
520+
p = substr(p, bs_idx + 2)
521+
522+
if ((esc_idx = index("abtvfr\"\\", esc)) != 0) {
523+
# C-style one-character escape sequence.
524+
out = out substr("\a\b\t\v\f\r\"\\",
525+
esc_idx, 1)
526+
} else if (esc == "n") {
527+
# Uh-oh, a newline character.
528+
# We cant reliably put a pathname
529+
# containing a newline into COMPREPLY,
530+
# and the newline would create a mess.
531+
# Skip this path.
532+
return ""
533+
} else {
534+
# Must be a \nnn octal value, then.
535+
dec = esc * 64 + \
536+
substr(p, 1, 1) * 8 + \
537+
substr(p, 2, 1)
538+
out = out sprintf("%c", dec)
539+
p = substr(p, 3)
540+
}
541+
}
542+
# Drop closing double quote, if there is one.
543+
# (There isnt any if this is a directory, as it was
544+
# already stripped with the trailing path components.)
545+
if (substr(p, length(p), 1) == "\"")
546+
out = out substr(p, 1, length(p) - 1)
547+
else
548+
out = out p
549+
550+
return out
551+
}'
552+
}
553+
554+
# __git_complete_index_file requires 1 argument:
555+
# 1: the options to pass to ls-file
556+
#
557+
# The exception is --committable, which finds the files appropriate commit.
558+
__git_complete_index_file ()
559+
{
560+
local dequoted_word pfx="" cur_
561+
562+
__git_dequote "$cur"
563+
564+
case "$dequoted_word" in
565+
?*/*)
566+
pfx="${dequoted_word%/*}/"
567+
cur_="${dequoted_word##*/}"
568+
;;
569+
*)
570+
cur_="$dequoted_word"
571+
esac
572+
573+
__gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_")"
398574
}
399575

400576
# Lists branches from the local repository.
@@ -713,26 +889,6 @@ __git_complete_revlist_file ()
713889
esac
714890
}
715891

716-
717-
# __git_complete_index_file requires 1 argument:
718-
# 1: the options to pass to ls-file
719-
#
720-
# The exception is --committable, which finds the files appropriate commit.
721-
__git_complete_index_file ()
722-
{
723-
local pfx="" cur_="$cur"
724-
725-
case "$cur_" in
726-
?*/*)
727-
pfx="${cur_%/*}"
728-
cur_="${cur_##*/}"
729-
pfx="${pfx}/"
730-
;;
731-
esac
732-
733-
__gitcomp_file "$(__git_index_files "$1" ${pfx:+"$pfx"})" "$pfx" "$cur_"
734-
}
735-
736892
__git_complete_file ()
737893
{
738894
__git_complete_revlist_file
@@ -3232,6 +3388,15 @@ if [[ -n ${ZSH_VERSION-} ]]; then
32323388
compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
32333389
}
32343390

3391+
__gitcomp_file_direct ()
3392+
{
3393+
emulate -L zsh
3394+
3395+
local IFS=$'\n'
3396+
compset -P '*[=:]'
3397+
compadd -Q -f -- ${=1} && _ret=0
3398+
}
3399+
32353400
__gitcomp_file ()
32363401
{
32373402
emulate -L zsh

contrib/completion/git-completion.zsh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ __gitcomp_nl_append ()
9393
compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
9494
}
9595

96+
__gitcomp_file_direct ()
97+
{
98+
emulate -L zsh
99+
100+
local IFS=$'\n'
101+
compset -P '*[=:]'
102+
compadd -Q -f -- ${=1} && _ret=0
103+
}
104+
96105
__gitcomp_file ()
97106
{
98107
emulate -L zsh

0 commit comments

Comments
 (0)