Skip to content

Commit fea16b4

Browse files
perillogitster
authored andcommitted
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 2e90029 commit fea16b4

File tree

1 file changed

+240
-15
lines changed

1 file changed

+240
-15
lines changed

contrib/completion/git-completion.bash

Lines changed: 240 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# *) .git/remotes file names
1414
# *) git 'subcommands'
1515
# *) tree paths within 'ref:path/to/file' expressions
16+
# *) file paths within current working directory and index
1617
# *) common --long-options
1718
#
1819
# To use these routines:
@@ -233,6 +234,118 @@ __gitcomp_nl ()
233234
COMPREPLY=($(compgen -P "${2-}" -S "${4- }" -W "$1" -- "${3-$cur}"))
234235
}
235236

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+
236349
__git_heads ()
237350
{
238351
local dir="$(__gitdir)"
@@ -430,6 +543,46 @@ __git_complete_revlist_file ()
430543
}
431544

432545

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+
433586
__git_complete_file ()
434587
{
435588
__git_complete_revlist_file
@@ -722,6 +875,43 @@ __git_has_doubledash ()
722875
return 1
723876
}
724877

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+
725915
__git_whitespacelist="nowarn warn error error-all fix"
726916

727917
_git_am ()
@@ -770,8 +960,6 @@ _git_apply ()
770960

771961
_git_add ()
772962
{
773-
__git_has_doubledash && return
774-
775963
case "$cur" in
776964
--*)
777965
__gitcomp "
@@ -780,7 +968,9 @@ _git_add ()
780968
"
781969
return
782970
esac
783-
COMPREPLY=()
971+
972+
# XXX should we check for --update and --all options ?
973+
__git_complete_index_file "--others --modified"
784974
}
785975

786976
_git_archive ()
@@ -930,15 +1120,15 @@ _git_cherry_pick ()
9301120

9311121
_git_clean ()
9321122
{
933-
__git_has_doubledash && return
934-
9351123
case "$cur" in
9361124
--*)
9371125
__gitcomp "--dry-run --quiet"
9381126
return
9391127
;;
9401128
esac
941-
COMPREPLY=()
1129+
1130+
# XXX should we check for -x option ?
1131+
__git_complete_index_file "--others"
9421132
}
9431133

9441134
_git_clone ()
@@ -969,7 +1159,12 @@ _git_clone ()
9691159

9701160
_git_commit ()
9711161
{
972-
__git_has_doubledash && return
1162+
case "$prev" in
1163+
-c|-C)
1164+
__gitcomp_nl "$(__git_refs)" "" "${cur}"
1165+
return
1166+
;;
1167+
esac
9731168

9741169
case "$cur" in
9751170
--cleanup=*)
@@ -998,7 +1193,13 @@ _git_commit ()
9981193
"
9991194
return
10001195
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
10021203
}
10031204

10041205
_git_describe ()
@@ -1216,8 +1417,6 @@ _git_init ()
12161417

12171418
_git_ls_files ()
12181419
{
1219-
__git_has_doubledash && return
1220-
12211420
case "$cur" in
12221421
--*)
12231422
__gitcomp "--cached --deleted --modified --others --ignored
@@ -1230,7 +1429,10 @@ _git_ls_files ()
12301429
return
12311430
;;
12321431
esac
1233-
COMPREPLY=()
1432+
1433+
# XXX ignore options like --modified and always suggest all cached
1434+
# files.
1435+
__git_complete_index_file "--cached"
12341436
}
12351437

12361438
_git_ls_remote ()
@@ -1362,7 +1564,14 @@ _git_mv ()
13621564
return
13631565
;;
13641566
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
13661575
}
13671576

13681577
_git_name_rev ()
@@ -2068,15 +2277,14 @@ _git_revert ()
20682277

20692278
_git_rm ()
20702279
{
2071-
__git_has_doubledash && return
2072-
20732280
case "$cur" in
20742281
--*)
20752282
__gitcomp "--cached --dry-run --ignore-unmatch --quiet"
20762283
return
20772284
;;
20782285
esac
2079-
COMPREPLY=()
2286+
2287+
__git_complete_index_file "--cached"
20802288
}
20812289

20822290
_git_shortlog ()
@@ -2441,6 +2649,15 @@ if [[ -n ${ZSH_VERSION-} ]]; then
24412649
compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
24422650
}
24432651

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+
24442661
__git_zsh_helper ()
24452662
{
24462663
emulate -L ksh
@@ -2462,6 +2679,14 @@ if [[ -n ${ZSH_VERSION-} ]]; then
24622679

24632680
compdef _git git gitk
24642681
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
24652690
fi
24662691

24672692
__git_func_wrap ()

0 commit comments

Comments
 (0)