completion: hide dotfiles for selected path completion

The completion helper for index paths uses git ls-files rather than shell filename completion. As a result, leading-dot paths such as a tracked .gitignore were offered even when the user had not started the path with ".". Hide leading-dot path components for git rm, git mv, and git ls-files when completing an empty path component. Explicit dot completion is still preserved, so git rm . can still complete .gitignore. This matches standard shell filename completion behavior, where dotfiles are hidden by default unless the user starts their input with a dot. This also resolves four TODO comments in t/9902-completion.sh which have been present since 2013 (commit ddf07bddef9a, "completion: add file completion tests", 2013-04-27), expecting that .gitignore would not be shown when completing on an empty path component. Signed-off-by: Zakariyah Ali <zakariyahali100@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Zakariyah Ali committed Jun 20, 2026 at 17:55 UTC 7f5550445099f501dd16299c49f59a0dd949d237
2 files changed +26 -20
contrib/completion/git-completion.bash
+24 -12
@@ -638,25 +638,33 @@ __git_ls_files_helper ()
638 }
639
640
641 -# __git_index_files accepts 1 or 2 arguments:
641 +# __git_index_files accepts 1 to 4 arguments:
642 # 1: Options to pass to ls-files (required).
643 # 2: A directory path (optional).
644 # If provided, only files within the specified directory are listed.
645 # Sub directories are never recursed. Path must have a trailing
646 # slash.
647 # 3: List only paths matching this path component (optional).
648 +# 4: Hide paths whose first component starts with a dot if this is
649 +# "hide-dotfiles" and the third argument is empty (optional).
650 __git_index_files ()
651 {
650 - local root="$2" match="$3"
652 + local root="$2" match="$3" hide_dotfiles="${4-}"
653 + local hide_dotfiles_awk=0
654 + if [ "$hide_dotfiles" = "hide-dotfiles" ] && [ -z "$match" ]; then
655 + hide_dotfiles_awk=1
656 + fi
657
658 __git_ls_files_helper "$root" "$1" "${match:-?}" |
653 - awk -F / -v pfx="${2//\\/\\\\}" '{
659 + awk -F / -v pfx="${2//\\/\\\\}" -v hide_dotfiles="$hide_dotfiles_awk" '{
660 paths[$1] = 1
661 }
662 END {
663 for (p in paths) {
664 if (substr(p, 1, 1) != "\"") {
665 # No special characters, easy!
666 + if (hide_dotfiles == 1 && substr(p, 1, 1) == ".")
667 + continue
668 print pfx p
669 continue
670 }
@@ -675,8 +683,10 @@ __git_index_files ()
683 # We have seen the same directory unquoted,
684 # skip it.
685 continue
678 - else
679 - print pfx p
686 +
687 + if (hide_dotfiles == 1 && substr(p, 1, 1) == ".")
688 + continue
689 + print pfx p
690 }
691 }
692 function dequote(p, bs_idx, out, esc, esc_idx, dec) {
@@ -721,13 +731,15 @@ __git_index_files ()
731 }'
732 }
733
724 -# __git_complete_index_file requires 1 argument:
734 +# __git_complete_index_file accepts 1 or 2 arguments:
735 # 1: the options to pass to ls-file
736 +# 2: Hide paths whose first component starts with a dot if this is
737 +# "hide-dotfiles" and the current word is empty (optional).
738 #
739 # The exception is --committable, which finds the files appropriate commit.
740 __git_complete_index_file ()
741 {
730 - local dequoted_word pfx="" cur_
742 + local dequoted_word pfx="" cur_ hide_dotfiles="${2-}"
743
744 __git_dequote "$cur"
745
@@ -740,7 +752,7 @@ __git_complete_index_file ()
752 cur_="$dequoted_word"
753 esac
754
743 - __gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_")"
755 + __gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_" "$hide_dotfiles")"
756 }
757
758 # Lists branches from the local repository.
@@ -2164,7 +2176,7 @@ _git_ls_files ()
2176
2177 # XXX ignore options like --modified and always suggest all cached
2178 # files.
2167 - __git_complete_index_file "--cached"
2179 + __git_complete_index_file "--cached" hide-dotfiles
2180 }
2181
2182 _git_ls_remote ()
@@ -2397,9 +2409,9 @@ _git_mv ()
2409 if [ $(__git_count_arguments "mv") -gt 0 ]; then
2410 # We need to show both cached and untracked files (including
2411 # empty directories) since this may not be the last argument.
2400 - __git_complete_index_file "--cached --others --directory"
2412 + __git_complete_index_file "--cached --others --directory" hide-dotfiles
2413 else
2402 - __git_complete_index_file "--cached"
2414 + __git_complete_index_file "--cached" hide-dotfiles
2415 fi
2416 }
2417
@@ -3219,7 +3231,7 @@ _git_rm ()
3231 ;;
3232 esac
3233
3222 - __git_complete_index_file "--cached"
3234 + __git_complete_index_file "--cached" hide-dotfiles
3235 }
3236
3237 _git_shortlog ()
t/t9902-completion.sh
+2 -8
@@ -2811,17 +2811,15 @@ test_expect_success 'complete files' '
2811
2812 touch untracked &&
2813
2814 - : TODO .gitignore should not be here &&
2814 test_completion "git rm " <<-\EOF &&
2816 - .gitignore
2815 modified
2816 EOF
2817
2818 + test_completion "git rm ." ".gitignore" &&
2819 +
2820 test_completion "git clean " "untracked" &&
2821
2822 - : TODO .gitignore should not be here &&
2822 test_completion "git mv " <<-\EOF &&
2824 - .gitignore
2823 modified
2824 EOF
2825
@@ -2832,9 +2830,7 @@ test_expect_success 'complete files' '
2830
2831 mkdir untracked-dir &&
2832
2835 - : TODO .gitignore should not be here &&
2833 test_completion "git mv modified " <<-\EOF &&
2837 - .gitignore
2834 dir
2835 modified
2836 untracked
@@ -2843,9 +2839,7 @@ test_expect_success 'complete files' '
2839
2840 test_completion "git commit " "modified" &&
2841
2846 - : TODO .gitignore should not be here &&
2842 test_completion "git ls-files " <<-\EOF &&
2848 - .gitignore
2843 dir
2844 modified
2845 EOF