Merge branch 'sg/complete-paths'
authorJunio C Hamano <gitster@pobox.com>
Wed, 30 May 2018 05:04:08 +0000 (14:04 +0900)
committerJunio C Hamano <gitster@pobox.com>
Wed, 30 May 2018 05:04:08 +0000 (14:04 +0900)
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

1  2 
contrib/completion/git-completion.bash
index 961a0ed76f89133dc01fc86a07be37eb5728651a,8bc79a522688299347a96e649702acb190a1cd9f..1491b7239be11cd04c257aabc42944569b806f70
@@@ -94,6 -94,70 +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+
@@@ -284,11 -348,7 +348,11 @@@ __gitcomp (
  
  # Clear the variables caching builtins' options when (re-)sourcing
  # the completion script.
 -unset $(set |sed -ne 's/^\(__gitcomp_builtin_[a-zA-Z0-9_][a-zA-Z0-9_]*\)=.*/\1/p') 2>/dev/null
 +if [[ -n ${ZSH_VERSION-} ]]; then
 +      unset $(set |sed -ne 's/^\(__gitcomp_builtin_[a-zA-Z0-9_][a-zA-Z0-9_]*\)=.*/\1/p') 2>/dev/null
 +else
 +      unset $(compgen -v __gitcomp_builtin_)
 +fi
  
  # This function is equivalent to
  #
@@@ -346,6 -406,24 +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 -443,8 +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
  __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
  }
  
  #    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 -885,6 +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
@@@ -879,7 -1031,6 +1035,7 @@@ __git_list_porcelain_commands (
                check-ref-format) : plumbing;;
                checkout-index)   : plumbing;;
                column)           : internal helper;;
 +              commit-graph)     : plumbing;;
                commit-tree)      : plumbing;;
                count-objects)    : infrequent;;
                credential)       : credentials;;
@@@ -1949,7 -2100,7 +2105,7 @@@ _git_rebase (
        --*)
                __gitcomp "
                        --onto --merge --strategy --interactive
 -                      --preserve-merges --stat --no-stat
 +                      --rebase-merges --preserve-merges --stat --no-stat
                        --committer-date-is-author-date --ignore-date
                        --ignore-whitespace --whitespace=
                        --autosquash --no-autosquash
@@@ -2120,7 -2271,7 +2276,7 @@@ _git_config (
                return
                ;;
        branch.*.rebase)
 -              __gitcomp "false true preserve interactive"
 +              __gitcomp "false true merges preserve interactive"
                return
                ;;
        remote.pushdefault)
                __gitcomp "$__git_log_date_formats"
                return
                ;;
 -      sendemail.aliasesfiletype)
 +      sendemail.aliasfiletype)
                __gitcomp "mutt mailrc pine elm gnus"
                return
                ;;
                core.bigFileThreshold
                core.checkStat
                core.commentChar
 +              core.commitGraph
                core.compression
                core.createObject
                core.deltaBaseCacheLimit
@@@ -2775,21 -2925,13 +2931,21 @@@ _git_show_branch (
  _git_stash ()
  {
        local save_opts='--all --keep-index --no-keep-index --quiet --patch --include-untracked'
 -      local subcommands='push save list show apply clear drop pop create branch'
 -      local subcommand="$(__git_find_on_cmdline "$subcommands")"
 +      local subcommands='push list show apply clear drop pop create branch'
 +      local subcommand="$(__git_find_on_cmdline "$subcommands save")"
 +      if [ -n "$(__git_find_on_cmdline "-p")" ]; then
 +              subcommand="push"
 +      fi
        if [ -z "$subcommand" ]; then
                case "$cur" in
                --*)
                        __gitcomp "$save_opts"
                        ;;
 +              sa*)
 +                      if [ -z "$(__git_find_on_cmdline "$save_opts")" ]; then
 +                              __gitcomp "save"
 +                      fi
 +                      ;;
                *)
                        if [ -z "$(__git_find_on_cmdline "$save_opts")" ]; then
                                __gitcomp "$subcommands"
@@@ -3073,17 -3215,10 +3229,17 @@@ __git_support_parseopt_helper () 
  __git_complete_command () {
        local command="$1"
        local completion_func="_git_${command//-/_}"
 -      if declare -f $completion_func >/dev/null 2>/dev/null; then
 +      if ! declare -f $completion_func >/dev/null 2>/dev/null &&
 +              declare -f _completion_loader >/dev/null 2>/dev/null
 +      then
 +              _completion_loader "git-$command"
 +      fi
 +      if declare -f $completion_func >/dev/null 2>/dev/null
 +      then
                $completion_func
                return 0
 -      elif __git_support_parseopt_helper "$command"; then
 +      elif __git_support_parseopt_helper "$command"
 +      then
                __git_complete_common "$command"
                return 0
        else
@@@ -3232,6 -3367,15 +3388,15 @@@ if [[ -n ${ZSH_VERSION-} ]]; the
                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