зеркало из https://github.com/microsoft/git.git
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
This commit is contained in:
Коммит
4ce72180ab
|
@ -94,6 +94,70 @@ __git ()
|
|||
${__git_dir:+--git-dir="$__git_dir"} "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
# Removes backslash escaping, single quotes and double quotes from a word,
|
||||
# stores the result in the variable $dequoted_word.
|
||||
# 1: The word to dequote.
|
||||
__git_dequote ()
|
||||
{
|
||||
local rest="$1" len ch
|
||||
|
||||
dequoted_word=""
|
||||
|
||||
while test -n "$rest"; do
|
||||
len=${#dequoted_word}
|
||||
dequoted_word="$dequoted_word${rest%%[\\\'\"]*}"
|
||||
rest="${rest:$((${#dequoted_word}-$len))}"
|
||||
|
||||
case "${rest:0:1}" in
|
||||
\\)
|
||||
ch="${rest:1:1}"
|
||||
case "$ch" in
|
||||
$'\n')
|
||||
;;
|
||||
*)
|
||||
dequoted_word="$dequoted_word$ch"
|
||||
;;
|
||||
esac
|
||||
rest="${rest:2}"
|
||||
;;
|
||||
\')
|
||||
rest="${rest:1}"
|
||||
len=${#dequoted_word}
|
||||
dequoted_word="$dequoted_word${rest%%\'*}"
|
||||
rest="${rest:$((${#dequoted_word}-$len+1))}"
|
||||
;;
|
||||
\")
|
||||
rest="${rest:1}"
|
||||
while test -n "$rest" ; do
|
||||
len=${#dequoted_word}
|
||||
dequoted_word="$dequoted_word${rest%%[\\\"]*}"
|
||||
rest="${rest:$((${#dequoted_word}-$len))}"
|
||||
case "${rest:0:1}" in
|
||||
\\)
|
||||
ch="${rest:1:1}"
|
||||
case "$ch" in
|
||||
\"|\\|\$|\`)
|
||||
dequoted_word="$dequoted_word$ch"
|
||||
;;
|
||||
$'\n')
|
||||
;;
|
||||
*)
|
||||
dequoted_word="$dequoted_word\\$ch"
|
||||
;;
|
||||
esac
|
||||
rest="${rest:2}"
|
||||
;;
|
||||
\")
|
||||
rest="${rest:1}"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# The following function is based on code from:
|
||||
#
|
||||
# bash_completion - programmable completion functions for bash 3.2+
|
||||
|
@ -346,6 +410,24 @@ __gitcomp_nl ()
|
|||
__gitcomp_nl_append "$@"
|
||||
}
|
||||
|
||||
# Fills the COMPREPLY array with prefiltered paths without any additional
|
||||
# processing.
|
||||
# Callers must take care of providing only paths that match the current path
|
||||
# to be completed and adding any prefix path components, if necessary.
|
||||
# 1: List of newline-separated matching paths, complete with all prefix
|
||||
# path componens.
|
||||
__gitcomp_file_direct ()
|
||||
{
|
||||
local IFS=$'\n'
|
||||
|
||||
COMPREPLY=($1)
|
||||
|
||||
# use a hack to enable file mode in bash < 4
|
||||
compopt -o filenames +o nospace 2>/dev/null ||
|
||||
compgen -f /non-existing-dir/ >/dev/null ||
|
||||
true
|
||||
}
|
||||
|
||||
# Generates completion reply with compgen from newline-separated possible
|
||||
# completion filenames.
|
||||
# It accepts 1 to 3 arguments:
|
||||
|
@ -365,7 +447,8 @@ __gitcomp_file ()
|
|||
|
||||
# use a hack to enable file mode in bash < 4
|
||||
compopt -o filenames +o nospace 2>/dev/null ||
|
||||
compgen -f /non-existing-dir/ > /dev/null
|
||||
compgen -f /non-existing-dir/ >/dev/null ||
|
||||
true
|
||||
}
|
||||
|
||||
# Execute 'git ls-files', unless the --committable option is specified, in
|
||||
|
@ -375,10 +458,12 @@ __gitcomp_file ()
|
|||
__git_ls_files_helper ()
|
||||
{
|
||||
if [ "$2" == "--committable" ]; then
|
||||
__git -C "$1" diff-index --name-only --relative HEAD
|
||||
__git -C "$1" -c core.quotePath=false diff-index \
|
||||
--name-only --relative HEAD -- "${3//\\/\\\\}*"
|
||||
else
|
||||
# NOTE: $2 is not quoted in order to support multiple options
|
||||
__git -C "$1" ls-files --exclude-standard $2
|
||||
__git -C "$1" -c core.quotePath=false ls-files \
|
||||
--exclude-standard $2 -- "${3//\\/\\\\}*"
|
||||
fi
|
||||
}
|
||||
|
||||
|
@ -389,12 +474,103 @@ __git_ls_files_helper ()
|
|||
# If provided, only files within the specified directory are listed.
|
||||
# Sub directories are never recursed. Path must have a trailing
|
||||
# slash.
|
||||
# 3: List only paths matching this path component (optional).
|
||||
__git_index_files ()
|
||||
{
|
||||
local root="${2-.}" file
|
||||
local root="$2" match="$3"
|
||||
|
||||
__git_ls_files_helper "$root" "$1" |
|
||||
cut -f1 -d/ | sort | uniq
|
||||
__git_ls_files_helper "$root" "$1" "$match" |
|
||||
awk -F / -v pfx="${2//\\/\\\\}" '{
|
||||
paths[$1] = 1
|
||||
}
|
||||
END {
|
||||
for (p in paths) {
|
||||
if (substr(p, 1, 1) != "\"") {
|
||||
# No special characters, easy!
|
||||
print pfx p
|
||||
continue
|
||||
}
|
||||
|
||||
# The path is quoted.
|
||||
p = dequote(p)
|
||||
if (p == "")
|
||||
continue
|
||||
|
||||
# Even when a directory name itself does not contain
|
||||
# any special characters, it will still be quoted if
|
||||
# any of its (stripped) trailing path components do.
|
||||
# Because of this we may have seen the same direcory
|
||||
# both quoted and unquoted.
|
||||
if (p in paths)
|
||||
# We have seen the same directory unquoted,
|
||||
# skip it.
|
||||
continue
|
||||
else
|
||||
print pfx p
|
||||
}
|
||||
}
|
||||
function dequote(p, bs_idx, out, esc, esc_idx, dec) {
|
||||
# Skip opening double quote.
|
||||
p = substr(p, 2)
|
||||
|
||||
# Interpret backslash escape sequences.
|
||||
while ((bs_idx = index(p, "\\")) != 0) {
|
||||
out = out substr(p, 1, bs_idx - 1)
|
||||
esc = substr(p, bs_idx + 1, 1)
|
||||
p = substr(p, bs_idx + 2)
|
||||
|
||||
if ((esc_idx = index("abtvfr\"\\", esc)) != 0) {
|
||||
# C-style one-character escape sequence.
|
||||
out = out substr("\a\b\t\v\f\r\"\\",
|
||||
esc_idx, 1)
|
||||
} else if (esc == "n") {
|
||||
# Uh-oh, a newline character.
|
||||
# We cant reliably put a pathname
|
||||
# containing a newline into COMPREPLY,
|
||||
# and the newline would create a mess.
|
||||
# Skip this path.
|
||||
return ""
|
||||
} else {
|
||||
# Must be a \nnn octal value, then.
|
||||
dec = esc * 64 + \
|
||||
substr(p, 1, 1) * 8 + \
|
||||
substr(p, 2, 1)
|
||||
out = out sprintf("%c", dec)
|
||||
p = substr(p, 3)
|
||||
}
|
||||
}
|
||||
# Drop closing double quote, if there is one.
|
||||
# (There isnt any if this is a directory, as it was
|
||||
# already stripped with the trailing path components.)
|
||||
if (substr(p, length(p), 1) == "\"")
|
||||
out = out substr(p, 1, length(p) - 1)
|
||||
else
|
||||
out = out p
|
||||
|
||||
return out
|
||||
}'
|
||||
}
|
||||
|
||||
# __git_complete_index_file requires 1 argument:
|
||||
# 1: the options to pass to ls-file
|
||||
#
|
||||
# The exception is --committable, which finds the files appropriate commit.
|
||||
__git_complete_index_file ()
|
||||
{
|
||||
local dequoted_word pfx="" cur_
|
||||
|
||||
__git_dequote "$cur"
|
||||
|
||||
case "$dequoted_word" in
|
||||
?*/*)
|
||||
pfx="${dequoted_word%/*}/"
|
||||
cur_="${dequoted_word##*/}"
|
||||
;;
|
||||
*)
|
||||
cur_="$dequoted_word"
|
||||
esac
|
||||
|
||||
__gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_")"
|
||||
}
|
||||
|
||||
# Lists branches from the local repository.
|
||||
|
@ -713,26 +889,6 @@ __git_complete_revlist_file ()
|
|||
esac
|
||||
}
|
||||
|
||||
|
||||
# __git_complete_index_file requires 1 argument:
|
||||
# 1: the options to pass to ls-file
|
||||
#
|
||||
# The exception is --committable, which finds the files appropriate commit.
|
||||
__git_complete_index_file ()
|
||||
{
|
||||
local pfx="" cur_="$cur"
|
||||
|
||||
case "$cur_" in
|
||||
?*/*)
|
||||
pfx="${cur_%/*}"
|
||||
cur_="${cur_##*/}"
|
||||
pfx="${pfx}/"
|
||||
;;
|
||||
esac
|
||||
|
||||
__gitcomp_file "$(__git_index_files "$1" ${pfx:+"$pfx"})" "$pfx" "$cur_"
|
||||
}
|
||||
|
||||
__git_complete_file ()
|
||||
{
|
||||
__git_complete_revlist_file
|
||||
|
@ -3232,6 +3388,15 @@ if [[ -n ${ZSH_VERSION-} ]]; then
|
|||
compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
|
||||
}
|
||||
|
||||
__gitcomp_file_direct ()
|
||||
{
|
||||
emulate -L zsh
|
||||
|
||||
local IFS=$'\n'
|
||||
compset -P '*[=:]'
|
||||
compadd -Q -f -- ${=1} && _ret=0
|
||||
}
|
||||
|
||||
__gitcomp_file ()
|
||||
{
|
||||
emulate -L zsh
|
||||
|
|
|
@ -93,6 +93,15 @@ __gitcomp_nl_append ()
|
|||
compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
|
||||
}
|
||||
|
||||
__gitcomp_file_direct ()
|
||||
{
|
||||
emulate -L zsh
|
||||
|
||||
local IFS=$'\n'
|
||||
compset -P '*[=:]'
|
||||
compadd -Q -f -- ${=1} && _ret=0
|
||||
}
|
||||
|
||||
__gitcomp_file ()
|
||||
{
|
||||
emulate -L zsh
|
||||
|
|
|
@ -84,10 +84,11 @@ test_completion ()
|
|||
then
|
||||
printf '%s\n' "$2" >expected
|
||||
else
|
||||
sed -e 's/Z$//' >expected
|
||||
sed -e 's/Z$//' |sort >expected
|
||||
fi &&
|
||||
run_completion "$1" &&
|
||||
test_cmp expected out
|
||||
sort out >out_sorted &&
|
||||
test_cmp expected out_sorted
|
||||
}
|
||||
|
||||
# Test __gitcomp.
|
||||
|
@ -400,6 +401,46 @@ test_expect_success '__gitdir - remote as argument' '
|
|||
test_cmp expected "$actual"
|
||||
'
|
||||
|
||||
|
||||
test_expect_success '__git_dequote - plain unquoted word' '
|
||||
__git_dequote unquoted-word &&
|
||||
verbose test unquoted-word = "$dequoted_word"
|
||||
'
|
||||
|
||||
# input: b\a\c\k\'\\\"s\l\a\s\h\es
|
||||
# expected: back'\"slashes
|
||||
test_expect_success '__git_dequote - backslash escaped' '
|
||||
__git_dequote "b\a\c\k\\'\''\\\\\\\"s\l\a\s\h\es" &&
|
||||
verbose test "back'\''\\\"slashes" = "$dequoted_word"
|
||||
'
|
||||
|
||||
# input: sin'gle\' '"quo'ted
|
||||
# expected: single\ "quoted
|
||||
test_expect_success '__git_dequote - single quoted' '
|
||||
__git_dequote "'"sin'gle\\\\' '\\\"quo'ted"'" &&
|
||||
verbose test '\''single\ "quoted'\'' = "$dequoted_word"
|
||||
'
|
||||
|
||||
# input: dou"ble\\" "\"\quot"ed
|
||||
# expected: double\ "\quoted
|
||||
test_expect_success '__git_dequote - double quoted' '
|
||||
__git_dequote '\''dou"ble\\" "\"\quot"ed'\'' &&
|
||||
verbose test '\''double\ "\quoted'\'' = "$dequoted_word"
|
||||
'
|
||||
|
||||
# input: 'open single quote
|
||||
test_expect_success '__git_dequote - open single quote' '
|
||||
__git_dequote "'\''open single quote" &&
|
||||
verbose test "open single quote" = "$dequoted_word"
|
||||
'
|
||||
|
||||
# input: "open double quote
|
||||
test_expect_success '__git_dequote - open double quote' '
|
||||
__git_dequote "\"open double quote" &&
|
||||
verbose test "open double quote" = "$dequoted_word"
|
||||
'
|
||||
|
||||
|
||||
test_expect_success '__gitcomp_direct - puts everything into COMPREPLY as-is' '
|
||||
sed -e "s/Z$//g" >expected <<-EOF &&
|
||||
with-trailing-space Z
|
||||
|
@ -1168,6 +1209,124 @@ test_expect_success 'teardown after ref completion' '
|
|||
git remote remove other
|
||||
'
|
||||
|
||||
|
||||
test_path_completion ()
|
||||
{
|
||||
test $# = 2 || error "bug in the test script: not 2 parameters to test_path_completion"
|
||||
|
||||
local cur="$1" expected="$2"
|
||||
echo "$expected" >expected &&
|
||||
(
|
||||
# In the following tests calling this function we only
|
||||
# care about how __git_complete_index_file() deals with
|
||||
# unusual characters in path names. By requesting only
|
||||
# untracked files we dont have to bother adding any
|
||||
# paths to the index in those tests.
|
||||
__git_complete_index_file --others &&
|
||||
print_comp
|
||||
) &&
|
||||
test_cmp expected out
|
||||
}
|
||||
|
||||
test_expect_success 'setup for path completion tests' '
|
||||
mkdir simple-dir \
|
||||
"spaces in dir" \
|
||||
árvíztűrő &&
|
||||
touch simple-dir/simple-file \
|
||||
"spaces in dir/spaces in file" \
|
||||
"árvíztűrő/Сайн яваарай" &&
|
||||
if test_have_prereq !MINGW &&
|
||||
mkdir BS\\dir \
|
||||
'$'separators\034in\035dir'' &&
|
||||
touch BS\\dir/DQ\"file \
|
||||
'$'separators\034in\035dir/sep\036in\037file''
|
||||
then
|
||||
test_set_prereq FUNNYNAMES
|
||||
else
|
||||
rm -rf BS\\dir '$'separators\034in\035dir''
|
||||
fi
|
||||
'
|
||||
|
||||
test_expect_success '__git_complete_index_file - simple' '
|
||||
test_path_completion simple simple-dir && # Bash is supposed to
|
||||
# add the trailing /.
|
||||
test_path_completion simple-dir/simple simple-dir/simple-file
|
||||
'
|
||||
|
||||
test_expect_success \
|
||||
'__git_complete_index_file - escaped characters on cmdline' '
|
||||
test_path_completion spac "spaces in dir" && # Bash will turn this
|
||||
# into "spaces\ in\ dir"
|
||||
test_path_completion "spaces\\ i" \
|
||||
"spaces in dir" &&
|
||||
test_path_completion "spaces\\ in\\ dir/s" \
|
||||
"spaces in dir/spaces in file" &&
|
||||
test_path_completion "spaces\\ in\\ dir/spaces\\ i" \
|
||||
"spaces in dir/spaces in file"
|
||||
'
|
||||
|
||||
test_expect_success \
|
||||
'__git_complete_index_file - quoted characters on cmdline' '
|
||||
# Testing with an opening but without a corresponding closing
|
||||
# double quote is important.
|
||||
test_path_completion \"spac "spaces in dir" &&
|
||||
test_path_completion "\"spaces i" \
|
||||
"spaces in dir" &&
|
||||
test_path_completion "\"spaces in dir/s" \
|
||||
"spaces in dir/spaces in file" &&
|
||||
test_path_completion "\"spaces in dir/spaces i" \
|
||||
"spaces in dir/spaces in file"
|
||||
'
|
||||
|
||||
test_expect_success '__git_complete_index_file - UTF-8 in ls-files output' '
|
||||
test_path_completion á árvíztűrő &&
|
||||
test_path_completion árvíztűrő/С "árvíztűrő/Сайн яваарай"
|
||||
'
|
||||
|
||||
test_expect_success FUNNYNAMES \
|
||||
'__git_complete_index_file - C-style escapes in ls-files output' '
|
||||
test_path_completion BS \
|
||||
BS\\dir &&
|
||||
test_path_completion BS\\\\d \
|
||||
BS\\dir &&
|
||||
test_path_completion BS\\\\dir/DQ \
|
||||
BS\\dir/DQ\"file &&
|
||||
test_path_completion BS\\\\dir/DQ\\\"f \
|
||||
BS\\dir/DQ\"file
|
||||
'
|
||||
|
||||
test_expect_success FUNNYNAMES \
|
||||
'__git_complete_index_file - \nnn-escaped characters in ls-files output' '
|
||||
test_path_completion sep '$'separators\034in\035dir'' &&
|
||||
test_path_completion '$'separators\034i'' \
|
||||
'$'separators\034in\035dir'' &&
|
||||
test_path_completion '$'separators\034in\035dir/sep'' \
|
||||
'$'separators\034in\035dir/sep\036in\037file'' &&
|
||||
test_path_completion '$'separators\034in\035dir/sep\036i'' \
|
||||
'$'separators\034in\035dir/sep\036in\037file''
|
||||
'
|
||||
|
||||
test_expect_success FUNNYNAMES \
|
||||
'__git_complete_index_file - removing repeated quoted path components' '
|
||||
test_when_finished rm -r repeated-quoted &&
|
||||
mkdir repeated-quoted && # A directory whose name in itself
|
||||
# would not be quoted ...
|
||||
>repeated-quoted/0-file &&
|
||||
>repeated-quoted/1\"file && # ... but here the file makes the
|
||||
# dirname quoted ...
|
||||
>repeated-quoted/2-file &&
|
||||
>repeated-quoted/3\"file && # ... and here, too.
|
||||
|
||||
# Still, we shold only list the directory name only once.
|
||||
test_path_completion repeated repeated-quoted
|
||||
'
|
||||
|
||||
test_expect_success 'teardown after path completion tests' '
|
||||
rm -rf simple-dir "spaces in dir" árvíztűrő \
|
||||
BS\\dir '$'separators\034in\035dir''
|
||||
'
|
||||
|
||||
|
||||
test_expect_success '__git_get_config_variables' '
|
||||
cat >expect <<-EOF &&
|
||||
name-1
|
||||
|
@ -1365,6 +1524,7 @@ test_expect_success 'complete files' '
|
|||
|
||||
echo "expected" > .gitignore &&
|
||||
echo "out" >> .gitignore &&
|
||||
echo "out_sorted" >> .gitignore &&
|
||||
|
||||
git add .gitignore &&
|
||||
test_completion "git commit " ".gitignore" &&
|
||||
|
|
Загрузка…
Ссылка в новой задаче