git-completion.bash: add support for path completion

The git-completion.bash script did not implemented full, git aware, support to complete paths, for git commands that operate on files within the current working directory or the index. As an example: git add <TAB> will suggest all files in the current working directory, including ignored files and files that have not been modified. Support path completion, for git commands where the non-option arguments always refer to paths within the current working directory or the index, as follows: * the path completion for the "git rm" and "git ls-files" commands will suggest all cached files. * the path completion for the "git add" command will suggest all untracked and modified files. Ignored files are excluded. * the path completion for the "git clean" command will suggest all untracked files. Ignored files are excluded. * the path completion for the "git mv" command will suggest all cached files when expanding the first argument, and all untracked and cached files for subsequent arguments. In the latter case, empty directories are included and ignored files are excluded. * the path completion for the "git commit" command will suggest all files that have been modified from the HEAD, if HEAD exists, otherwise it will suggest all cached files. For all affected commands, completion will always stop at directory boundary. Only standard ignored files are excluded, using the --exclude-standard option of the ls-files command. When using a recent Bash version, Git path completion will be the same as builtin file completion, e.g. git add contrib/ will suggest relative file names. Signed-off-by: Manlio Perillo <manlio.perillo@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Manlio Perillo committed Jan 11, 2013 at 19:48 UTC fea16b47b603e7e4fa7fca198bd49229c0e5da3d
1 file changed +240 -15
contrib/completion/git-completion.bash
+240 -15
@@ -13,6 +13,7 @@
13 # *) .git/remotes file names
14 # *) git 'subcommands'
15 # *) tree paths within 'ref:path/to/file' expressions
16 +# *) file paths within current working directory and index
17 # *) common --long-options
18 #
19 # To use these routines:
@@ -233,6 +234,118 @@ __gitcomp_nl ()
234 COMPREPLY=($(compgen -P "${2-}" -S "${4- }" -W "$1" -- "${3-$cur}"))
235 }
236
237 +# Generates completion reply with compgen from newline-separated possible
238 +# completion filenames.
239 +# It accepts 1 to 3 arguments:
240 +# 1: List of possible completion filenames, separated by a single newline.
241 +# 2: A directory prefix to be added to each possible completion filename
242 +# (optional).
243 +# 3: Generate possible completion matches for this word (optional).
244 +__gitcomp_file ()
245 +{
246 + local IFS=$'\n'
247 +
248 + # XXX does not work when the directory prefix contains a tilde,
249 + # since tilde expansion is not applied.
250 + # This means that COMPREPLY will be empty and Bash default
251 + # completion will be used.
252 + COMPREPLY=($(compgen -P "${2-}" -W "$1" -- "${3-$cur}"))
253 +
254 + # Tell Bash that compspec generates filenames.
255 + compopt -o filenames 2>/dev/null
256 +}
257 +
258 +__git_index_file_list_filter_compat ()
259 +{
260 + local path
261 +
262 + while read -r path; do
263 + case "$path" in
264 + ?*/*) echo "${path%%/*}/" ;;
265 + *) echo "$path" ;;
266 + esac
267 + done
268 +}
269 +
270 +__git_index_file_list_filter_bash ()
271 +{
272 + local path
273 +
274 + while read -r path; do
275 + case "$path" in
276 + ?*/*)
277 + # XXX if we append a slash to directory names when using
278 + # `compopt -o filenames`, Bash will append another slash.
279 + # This is pretty stupid, and this the reason why we have to
280 + # define a compatible version for this function.
281 + echo "${path%%/*}" ;;
282 + *)
283 + echo "$path" ;;
284 + esac
285 + done
286 +}
287 +
288 +# Process path list returned by "ls-files" and "diff-index --name-only"
289 +# commands, in order to list only file names relative to a specified
290 +# directory, and append a slash to directory names.
291 +__git_index_file_list_filter ()
292 +{
293 + # Default to Bash >= 4.x
294 + __git_index_file_list_filter_bash
295 +}
296 +
297 +# Execute git ls-files, returning paths relative to the directory
298 +# specified in the first argument, and using the options specified in
299 +# the second argument.
300 +__git_ls_files_helper ()
301 +{
302 + # NOTE: $2 is not quoted in order to support multiple options
303 + cd "$1" && git ls-files --exclude-standard $2
304 +} 2>/dev/null
305 +
306 +
307 +# Execute git diff-index, returning paths relative to the directory
308 +# specified in the first argument, and using the tree object id
309 +# specified in the second argument.
310 +__git_diff_index_helper ()
311 +{
312 + cd "$1" && git diff-index --name-only --relative "$2"
313 +} 2>/dev/null
314 +
315 +# __git_index_files accepts 1 or 2 arguments:
316 +# 1: Options to pass to ls-files (required).
317 +# Supported options are --cached, --modified, --deleted, --others,
318 +# and --directory.
319 +# 2: A directory path (optional).
320 +# If provided, only files within the specified directory are listed.
321 +# Sub directories are never recursed. Path must have a trailing
322 +# slash.
323 +__git_index_files ()
324 +{
325 + local dir="$(__gitdir)" root="${2-.}"
326 +
327 + if [ -d "$dir" ]; then
328 + __git_ls_files_helper "$root" "$1" | __git_index_file_list_filter |
329 + sort | uniq
330 + fi
331 +}
332 +
333 +# __git_diff_index_files accepts 1 or 2 arguments:
334 +# 1) The id of a tree object.
335 +# 2) A directory path (optional).
336 +# If provided, only files within the specified directory are listed.
337 +# Sub directories are never recursed. Path must have a trailing
338 +# slash.
339 +__git_diff_index_files ()
340 +{
341 + local dir="$(__gitdir)" root="${2-.}"
342 +
343 + if [ -d "$dir" ]; then
344 + __git_diff_index_helper "$root" "$1" | __git_index_file_list_filter |
345 + sort | uniq
346 + fi
347 +}
348 +
349 __git_heads ()
350 {
351 local dir="$(__gitdir)"
@@ -430,6 +543,46 @@ __git_complete_revlist_file ()
543 }
544
545
546 +# __git_complete_index_file requires 1 argument: the options to pass to
547 +# ls-file
548 +__git_complete_index_file ()
549 +{
550 + local pfx cur_="$cur"
551 +
552 + case "$cur_" in
553 + ?*/*)
554 + pfx="${cur_%/*}"
555 + cur_="${cur_##*/}"
556 + pfx="${pfx}/"
557 +
558 + __gitcomp_file "$(__git_index_files "$1" "$pfx")" "$pfx" "$cur_"
559 + ;;
560 + *)
561 + __gitcomp_file "$(__git_index_files "$1")" "" "$cur_"
562 + ;;
563 + esac
564 +}
565 +
566 +# __git_complete_diff_index_file requires 1 argument: the id of a tree
567 +# object
568 +__git_complete_diff_index_file ()
569 +{
570 + local pfx cur_="$cur"
571 +
572 + case "$cur_" in
573 + ?*/*)
574 + pfx="${cur_%/*}"
575 + cur_="${cur_##*/}"
576 + pfx="${pfx}/"
577 +
578 + __gitcomp_file "$(__git_diff_index_files "$1" "$pfx")" "$pfx" "$cur_"
579 + ;;
580 + *)
581 + __gitcomp_file "$(__git_diff_index_files "$1")" "" "$cur_"
582 + ;;
583 + esac
584 +}
585 +
586 __git_complete_file ()
587 {
588 __git_complete_revlist_file
@@ -722,6 +875,43 @@ __git_has_doubledash ()
875 return 1
876 }
877
878 +# Try to count non option arguments passed on the command line for the
879 +# specified git command.
880 +# When options are used, it is necessary to use the special -- option to
881 +# tell the implementation were non option arguments begin.
882 +# XXX this can not be improved, since options can appear everywhere, as
883 +# an example:
884 +# git mv x -n y
885 +#
886 +# __git_count_arguments requires 1 argument: the git command executed.
887 +__git_count_arguments ()
888 +{
889 + local word i c=0
890 +
891 + # Skip "git" (first argument)
892 + for ((i=1; i < ${#words[@]}; i++)); do
893 + word="${words[i]}"
894 +
895 + case "$word" in
896 + --)
897 + # Good; we can assume that the following are only non
898 + # option arguments.
899 + ((c = 0))
900 + ;;
901 + "$1")
902 + # Skip the specified git command and discard git
903 + # main options
904 + ((c = 0))
905 + ;;
906 + ?*)
907 + ((c++))
908 + ;;
909 + esac
910 + done
911 +
912 + printf "%d" $c
913 +}
914 +
915 __git_whitespacelist="nowarn warn error error-all fix"
916
917 _git_am ()
@@ -770,8 +960,6 @@ _git_apply ()
960
961 _git_add ()
962 {
773 - __git_has_doubledash && return
774 -
963 case "$cur" in
964 --*)
965 __gitcomp "
@@ -780,7 +968,9 @@ _git_add ()
968 "
969 return
970 esac
783 - COMPREPLY=()
971 +
972 + # XXX should we check for --update and --all options ?
973 + __git_complete_index_file "--others --modified"
974 }
975
976 _git_archive ()
@@ -930,15 +1120,15 @@ _git_cherry_pick ()
1120
1121 _git_clean ()
1122 {
933 - __git_has_doubledash && return
934 -
1123 case "$cur" in
1124 --*)
1125 __gitcomp "--dry-run --quiet"
1126 return
1127 ;;
1128 esac
941 - COMPREPLY=()
1129 +
1130 + # XXX should we check for -x option ?
1131 + __git_complete_index_file "--others"
1132 }
1133
1134 _git_clone ()
@@ -969,7 +1159,12 @@ _git_clone ()
1159
1160 _git_commit ()
1161 {
972 - __git_has_doubledash && return
1162 + case "$prev" in
1163 + -c|-C)
1164 + __gitcomp_nl "$(__git_refs)" "" "${cur}"
1165 + return
1166 + ;;
1167 + esac
1168
1169 case "$cur" in
1170 --cleanup=*)
@@ -998,7 +1193,13 @@ _git_commit ()
1193 "
1194 return
1195 esac
1001 - COMPREPLY=()
1196 +
1197 + if git rev-parse --verify --quiet HEAD >/dev/null; then
1198 + __git_complete_diff_index_file "HEAD"
1199 + else
1200 + # This is the first commit
1201 + __git_complete_index_file "--cached"
1202 + fi
1203 }
1204
1205 _git_describe ()
@@ -1216,8 +1417,6 @@ _git_init ()
1417
1418 _git_ls_files ()
1419 {
1219 - __git_has_doubledash && return
1220 -
1420 case "$cur" in
1421 --*)
1422 __gitcomp "--cached --deleted --modified --others --ignored
@@ -1230,7 +1429,10 @@ _git_ls_files ()
1429 return
1430 ;;
1431 esac
1233 - COMPREPLY=()
1432 +
1433 + # XXX ignore options like --modified and always suggest all cached
1434 + # files.
1435 + __git_complete_index_file "--cached"
1436 }
1437
1438 _git_ls_remote ()
@@ -1362,7 +1564,14 @@ _git_mv ()
1564 return
1565 ;;
1566 esac
1365 - COMPREPLY=()
1567 +
1568 + if [ $(__git_count_arguments "mv") -gt 0 ]; then
1569 + # We need to show both cached and untracked files (including
1570 + # empty directories) since this may not be the last argument.
1571 + __git_complete_index_file "--cached --others --directory"
1572 + else
1573 + __git_complete_index_file "--cached"
1574 + fi
1575 }
1576
1577 _git_name_rev ()
@@ -2068,15 +2277,14 @@ _git_revert ()
2277
2278 _git_rm ()
2279 {
2071 - __git_has_doubledash && return
2072 -
2280 case "$cur" in
2281 --*)
2282 __gitcomp "--cached --dry-run --ignore-unmatch --quiet"
2283 return
2284 ;;
2285 esac
2079 - COMPREPLY=()
2286 +
2287 + __git_complete_index_file "--cached"
2288 }
2289
2290 _git_shortlog ()
@@ -2441,6 +2649,15 @@ if [[ -n ${ZSH_VERSION-} ]]; then
2649 compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
2650 }
2651
2652 + __gitcomp_file ()
2653 + {
2654 + emulate -L zsh
2655 +
2656 + local IFS=$'\n'
2657 + compset -P '*[=:]'
2658 + compadd -Q -p "${2-}" -f -- ${=1} && _ret=0
2659 + }
2660 +
2661 __git_zsh_helper ()
2662 {
2663 emulate -L ksh
@@ -2462,6 +2679,14 @@ if [[ -n ${ZSH_VERSION-} ]]; then
2679
2680 compdef _git git gitk
2681 return
2682 +elif [[ -n ${BASH_VERSION-} ]]; then
2683 + if ((${BASH_VERSINFO[0]} < 4)); then
2684 + # compopt is not supported
2685 + __git_index_file_list_filter ()
2686 + {
2687 + __git_index_file_list_filter_compat
2688 + }
2689 + fi
2690 fi
2691
2692 __git_func_wrap ()