summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar Junio C Hamano <gitster@pobox.com>2018-05-30 14:04:08 +0900
committerLibravatar Junio C Hamano <gitster@pobox.com>2018-05-30 14:04:08 +0900
commit4ce72180abe72dbb40f5e6a517deea814014e005 (patch)
tree412bd776cc9fb037860dc5a8c2a6ee3aed7312da
parentMerge branch 'nd/travis-gcc-8' (diff)
parentt9902-completion: exercise __git_complete_index_file() directly (diff)
downloadtgif-4ce72180abe72dbb40f5e6a517deea814014e005.tar.xz
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
-rw-r--r--contrib/completion/git-completion.bash217
-rw-r--r--contrib/completion/git-completion.zsh9
-rwxr-xr-xt/t9902-completion.sh164
3 files changed, 362 insertions, 28 deletions
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 961a0ed76f..1491b7239b 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -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
diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
index c3521fbfc4..53cb0f934f 100644
--- a/contrib/completion/git-completion.zsh
+++ b/contrib/completion/git-completion.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
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index 1b34caa1e1..1b6d275254 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -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" &&