diff options
Diffstat (limited to 'contrib/completion/git-completion.bash')
-rw-r--r-- | contrib/completion/git-completion.bash | 423 |
1 files changed, 348 insertions, 75 deletions
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 13a39eb..75193de 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -122,6 +122,40 @@ __git () ${__git_dir:+--git-dir="$__git_dir"} "$@" 2>/dev/null } +# Helper function to read the first line of a file into a variable. +# __git_eread requires 2 arguments, the file path and the name of the +# variable, in that order. +# +# This is taken from git-prompt.sh. +__git_eread () +{ + test -r "$1" && IFS=$'\r\n' read -r "$2" <"$1" +} + +# Runs git in $__git_repo_path to determine whether a pseudoref exists. +# 1: The pseudo-ref to search +__git_pseudoref_exists () +{ + local ref=$1 + local head + + __git_find_repo_path + + # If the reftable is in use, we have to shell out to 'git rev-parse' + # to determine whether the ref exists instead of looking directly in + # the filesystem to determine whether the ref exists. Otherwise, use + # Bash builtins since executing Git commands are expensive on some + # platforms. + if __git_eread "$__git_repo_path/HEAD" head; then + if [ "$head" == "ref: refs/heads/.invalid" ]; then + __git show-ref --exists "$ref" + return $? + fi + fi + + [ -f "$__git_repo_path/$ref" ] +} + # Removes backslash escaping, single quotes and double quotes from a word, # stores the result in the variable $dequoted_word. # 1: The word to dequote. @@ -420,16 +454,18 @@ fi # This function is equivalent to # -# __gitcomp "$(git xxx --git-completion-helper) ..." +# ___git_resolved_builtins=$(git xxx --git-completion-helper) # -# except that the output is cached. Accept 1-3 arguments: +# except that the result of the execution is cached. +# +# Accept 1-3 arguments: # 1: the git command to execute, this is also the cache key +# (use "_" when the command contains spaces, e.g. "remote add" +# becomes "remote_add") # 2: extra options to be added on top (e.g. negative forms) # 3: options to be excluded -__gitcomp_builtin () +__git_resolve_builtins () { - # spaces must be replaced with underscore for multi-word - # commands, e.g. "git remote add" becomes remote_add. local cmd="$1" local incl="${2-}" local excl="${3-}" @@ -455,7 +491,24 @@ __gitcomp_builtin () eval "$var=\"$options\"" fi - __gitcomp "$options" + ___git_resolved_builtins="$options" +} + +# This function is equivalent to +# +# __gitcomp "$(git xxx --git-completion-helper) ..." +# +# except that the output is cached. Accept 1-3 arguments: +# 1: the git command to execute, this is also the cache key +# (use "_" when the command contains spaces, e.g. "remote add" +# becomes "remote_add") +# 2: extra options to be added on top (e.g. negative forms) +# 3: options to be excluded +__gitcomp_builtin () +{ + __git_resolve_builtins "$1" "$2" "$3" + + __gitcomp "$___git_resolved_builtins" } # Variation of __gitcomp_nl () that appends to the existing list of @@ -522,6 +575,26 @@ __gitcomp_file () true } +# Find the current subcommand for commands that follow the syntax: +# +# git <command> <subcommand> +# +# 1: List of possible subcommands. +# 2: Optional subcommand to return when none is found. +__git_find_subcommand () +{ + local subcommand subcommands="$1" default_subcommand="$2" + + for subcommand in $subcommands; do + if [ "$subcommand" = "${words[__git_cmd_idx+1]}" ]; then + echo $subcommand + return + fi + done + + echo $default_subcommand +} + # Execute 'git ls-files', unless the --committable option is specified, in # which case it runs 'git diff-index' to find out the files that can be # committed. It return paths relative to the directory specified in the first @@ -1449,12 +1522,32 @@ _git_bisect () { __git_has_doubledash && return - local subcommands="start bad good skip reset visualize replay log run" - local subcommand="$(__git_find_on_cmdline "$subcommands")" + __git_find_repo_path + + # If a bisection is in progress get the terms being used. + local term_bad term_good + if [ -f "$__git_repo_path"/BISECT_TERMS ]; then + term_bad=$(__git bisect terms --term-bad) + term_good=$(__git bisect terms --term-good) + fi + + # We will complete any custom terms, but still always complete the + # more usual bad/new/good/old because git bisect gives a good error + # message if these are given when not in use, and that's better than + # silent refusal to complete if the user is confused. + # + # We want to recognize 'view' but not complete it, because it overlaps + # with 'visualize' too much and is just an alias for it. + # + local completable_subcommands="start bad new $term_bad good old $term_good terms skip reset visualize replay log run help" + local all_subcommands="$completable_subcommands view" + + local subcommand="$(__git_find_on_cmdline "$all_subcommands")" + if [ -z "$subcommand" ]; then __git_find_repo_path if [ -f "$__git_repo_path"/BISECT_START ]; then - __gitcomp "$subcommands" + __gitcomp "$completable_subcommands" else __gitcomp "replay start" fi @@ -1462,7 +1555,26 @@ _git_bisect () fi case "$subcommand" in - bad|good|reset|skip|start) + start) + case "$cur" in + --*) + __gitcomp "--first-parent --no-checkout --term-new --term-bad --term-old --term-good" + return + ;; + *) + __git_complete_refs + ;; + esac + ;; + terms) + __gitcomp "--term-good --term-old --term-bad --term-new" + return + ;; + visualize|view) + __git_complete_log_opts + return + ;; + bad|new|"$term_bad"|good|old|"$term_good"|reset|skip) __git_complete_refs ;; *) @@ -1624,8 +1736,7 @@ __git_cherry_pick_inprogress_options=$__git_sequencer_inprogress_options _git_cherry_pick () { - __git_find_repo_path - if [ -f "$__git_repo_path"/CHERRY_PICK_HEAD ]; then + if __git_pseudoref_exists CHERRY_PICK_HEAD; then __gitcomp "$__git_cherry_pick_inprogress_options" return fi @@ -1775,7 +1886,7 @@ __git_diff_common_options="--stat --numstat --shortstat --summary --output= --output-indicator-context= --output-indicator-new= --output-indicator-old= --ws-error-highlight= - --pickaxe-all --pickaxe-regex + --pickaxe-all --pickaxe-regex --patch-with-raw " # Options for diff/difftool @@ -2039,6 +2150,16 @@ __git_log_common_options=" --min-age= --until= --before= --min-parents= --max-parents= --no-min-parents --no-max-parents + --alternate-refs --ancestry-path + --author-date-order --basic-regexp + --bisect --boundary --exclude-first-parent-only + --exclude-hidden --extended-regexp + --fixed-strings --grep-reflog + --ignore-missing --left-only --perl-regexp + --reflog --regexp-ignore-case --remove-empty + --right-only --show-linear-break + --show-notes-by-default --show-pulls + --since-as-filter --single-worktree " # Options that go well for log and gitk (not shortlog) __git_log_gitk_options=" @@ -2054,6 +2175,7 @@ __git_log_shortlog_options=" # Options accepted by log and show __git_log_show_options=" --diff-merges --diff-merges= --no-diff-merges --dd --remerge-diff + --encoding= " __git_diff_merges_opts="off none on first-parent 1 separate m combined c dense-combined cc remerge r" @@ -2061,13 +2183,15 @@ __git_diff_merges_opts="off none on first-parent 1 separate m combined c dense-c __git_log_pretty_formats="oneline short medium full fuller reference email raw format: tformat: mboxrd" __git_log_date_formats="relative iso8601 iso8601-strict rfc2822 short local default human raw unix auto: format:" -_git_log () +# Complete porcelain (i.e. not git-rev-list) options and at least some +# option arguments accepted by git-log. Note that this same set of options +# are also accepted by some other git commands besides git-log. +__git_complete_log_opts () { - __git_has_doubledash && return - __git_find_repo_path + COMPREPLY=() local merge="" - if [ -f "$__git_repo_path/MERGE_HEAD" ]; then + if __git_pseudoref_exists MERGE_HEAD; then merge="--merge" fi case "$prev,$cur" in @@ -2137,6 +2261,8 @@ _git_log () --no-walk --no-walk= --do-walk --parents --children --expand-tabs --expand-tabs= --no-expand-tabs + --clear-decorations --decorate-refs= + --decorate-refs-exclude= $merge $__git_diff_common_options " @@ -2158,6 +2284,16 @@ _git_log () return ;; esac +} + +_git_log () +{ + __git_has_doubledash && return + __git_find_repo_path + + __git_complete_log_opts + [ ${#COMPREPLY[@]} -eq 0 ] || return + __git_complete_revlist } @@ -2374,13 +2510,30 @@ _git_rebase () _git_reflog () { - local subcommands="show delete expire" - local subcommand="$(__git_find_on_cmdline "$subcommands")" + local subcommands subcommand - if [ -z "$subcommand" ]; then - __gitcomp "$subcommands" - else - __git_complete_refs + __git_resolve_builtins "reflog" + + subcommands="$___git_resolved_builtins" + subcommand="$(__git_find_subcommand "$subcommands" "show")" + + case "$subcommand,$cur" in + show,--*) + __gitcomp " + $__git_log_common_options + " + return + ;; + $subcommand,--*) + __gitcomp_builtin "reflog_$subcommand" + return + ;; + esac + + __git_complete_refs + + if [ $((cword - __git_cmd_idx)) -eq 1 ]; then + __gitcompappend "$subcommands" "" "$cur" " " fi } @@ -2563,6 +2716,33 @@ __git_compute_config_vars () __git_config_vars="$(git help --config-for-completion)" } +__git_config_vars_all= +__git_compute_config_vars_all () +{ + test -n "$__git_config_vars_all" || + __git_config_vars_all="$(git --no-pager help --config)" +} + +__git_compute_first_level_config_vars_for_section () +{ + local section="$1" + __git_compute_config_vars + local this_section="__git_first_level_config_vars_for_section_${section}" + test -n "${!this_section}" || + printf -v "__git_first_level_config_vars_for_section_${section}" %s \ + "$(echo "$__git_config_vars" | awk -F. "/^${section}\.[a-z]/ { print \$2 }")" +} + +__git_compute_second_level_config_vars_for_section () +{ + local section="$1" + __git_compute_config_vars_all + local this_section="__git_second_level_config_vars_for_section_${section}" + test -n "${!this_section}" || + printf -v "__git_second_level_config_vars_for_section_${section}" %s \ + "$(echo "$__git_config_vars_all" | awk -F. "/^${section}\.</ { print \$3 }")" +} + __git_config_sections= __git_compute_config_sections () { @@ -2707,73 +2887,50 @@ __git_complete_config_variable_name () done case "$cur_" in - branch.*.*) + branch.*.*|guitool.*.*|difftool.*.*|man.*.*|mergetool.*.*|remote.*.*|submodule.*.*|url.*.*) local pfx="${cur_%.*}." cur_="${cur_##*.}" - __gitcomp "remote pushRemote merge mergeOptions rebase" "$pfx" "$cur_" "$sfx" + local section="${pfx%.*.}" + __git_compute_second_level_config_vars_for_section "${section}" + local this_section="__git_second_level_config_vars_for_section_${section}" + __gitcomp "${!this_section}" "$pfx" "$cur_" "$sfx" return ;; branch.*) local pfx="${cur_%.*}." cur_="${cur_#*.}" + local section="${pfx%.}" __gitcomp_direct "$(__git_heads "$pfx" "$cur_" ".")" - __gitcomp_nl_append $'autoSetupMerge\nautoSetupRebase\n' "$pfx" "$cur_" "${sfx- }" - return - ;; - guitool.*.*) - local pfx="${cur_%.*}." - cur_="${cur_##*.}" - __gitcomp " - argPrompt cmd confirm needsFile noConsole noRescan - prompt revPrompt revUnmerged title - " "$pfx" "$cur_" "$sfx" - return - ;; - difftool.*.*) - local pfx="${cur_%.*}." - cur_="${cur_##*.}" - __gitcomp "cmd path" "$pfx" "$cur_" "$sfx" - return - ;; - man.*.*) - local pfx="${cur_%.*}." - cur_="${cur_##*.}" - __gitcomp "cmd path" "$pfx" "$cur_" "$sfx" - return - ;; - mergetool.*.*) - local pfx="${cur_%.*}." - cur_="${cur_##*.}" - __gitcomp "cmd path trustExitCode" "$pfx" "$cur_" "$sfx" + __git_compute_first_level_config_vars_for_section "${section}" + local this_section="__git_first_level_config_vars_for_section_${section}" + __gitcomp_nl_append "${!this_section}" "$pfx" "$cur_" "${sfx:- }" return ;; pager.*) local pfx="${cur_%.*}." cur_="${cur_#*.}" __git_compute_all_commands - __gitcomp_nl "$__git_all_commands" "$pfx" "$cur_" "${sfx- }" - return - ;; - remote.*.*) - local pfx="${cur_%.*}." - cur_="${cur_##*.}" - __gitcomp " - url proxy fetch push mirror skipDefaultUpdate - receivepack uploadpack tagOpt pushurl - " "$pfx" "$cur_" "$sfx" + __gitcomp_nl "$__git_all_commands" "$pfx" "$cur_" "${sfx:- }" return ;; remote.*) local pfx="${cur_%.*}." cur_="${cur_#*.}" + local section="${pfx%.}" __gitcomp_nl "$(__git_remotes)" "$pfx" "$cur_" "." - __gitcomp_nl_append "pushDefault" "$pfx" "$cur_" "${sfx- }" + __git_compute_first_level_config_vars_for_section "${section}" + local this_section="__git_first_level_config_vars_for_section_${section}" + __gitcomp_nl_append "${!this_section}" "$pfx" "$cur_" "${sfx:- }" return ;; - url.*.*) + submodule.*) local pfx="${cur_%.*}." - cur_="${cur_##*.}" - __gitcomp "insteadOf pushInsteadOf" "$pfx" "$cur_" "$sfx" + cur_="${cur_#*.}" + local section="${pfx%.}" + __gitcomp_nl "$(__git config -f "$(__git rev-parse --show-toplevel)/.gitmodules" --get-regexp 'submodule.*.path' | awk -F. '{print $2}')" "$pfx" "$cur_" "." + __git_compute_first_level_config_vars_for_section "${section}" + local this_section="__git_first_level_config_vars_for_section_${section}" + __gitcomp_nl_append "${!this_section}" "$pfx" "$cur_" "${sfx:- }" return ;; *.*) @@ -2952,7 +3109,7 @@ _git_restore () __gitcomp_builtin restore ;; *) - if __git rev-parse --verify --quiet HEAD >/dev/null; then + if __git_pseudoref_exists HEAD; then __git_complete_index_file "--modified" fi esac @@ -2962,8 +3119,7 @@ __git_revert_inprogress_options=$__git_sequencer_inprogress_options _git_revert () { - __git_find_repo_path - if [ -f "$__git_repo_path"/REVERT_HEAD ]; then + if __git_pseudoref_exists REVERT_HEAD; then __gitcomp "$__git_revert_inprogress_options" return fi @@ -3084,12 +3240,119 @@ __gitcomp_directories () COMPREPLY+=("$c/") _found=1 fi - done < <(git ls-tree -z -d --name-only HEAD $_tmp_dir) + done < <(__git ls-tree -z -d --name-only HEAD $_tmp_dir) if [[ $_found == 0 ]] && [[ "$cur" =~ /$ ]]; then # No possible further completions any deeper, so assume we're at # a leaf directory and just consider it complete __gitcomp_direct_append "$cur " + elif [[ $_found == 0 ]]; then + # No possible completions found. Avoid falling back to + # bash's default file and directory completion, because all + # valid completions have already been searched and the + # fallbacks can do nothing but mislead. In fact, they can + # mislead in three different ways: + # 1) Fallback file completion makes no sense when asking + # for directory completions, as this function does. + # 2) Fallback directory completion is bad because + # e.g. "/pro" is invalid and should NOT complete to + # "/proc". + # 3) Fallback file/directory completion only completes + # on paths that exist in the current working tree, + # i.e. which are *already* part of their + # sparse-checkout. Thus, normal file and directory + # completion is always useless for "git + # sparse-checkout add" and is also probelmatic for + # "git sparse-checkout set" unless using it to + # strictly narrow the checkout. + COMPREPLY=( "" ) + fi +} + +# In non-cone mode, the arguments to {set,add} are supposed to be +# patterns, relative to the toplevel directory. These can be any kind +# of general pattern, like 'subdir/*.c' and we can't complete on all +# of those. However, if the user presses Tab to get tab completion, we +# presume that they are trying to provide a pattern that names a specific +# path. +__gitcomp_slash_leading_paths () +{ + local dequoted_word pfx="" cur_ toplevel + + # Since we are dealing with a sparse-checkout, subdirectories may not + # exist in the local working copy. Therefore, we want to run all + # ls-files commands relative to the repository toplevel. + toplevel="$(git rev-parse --show-toplevel)/" + + __git_dequote "$cur" + + # If the paths provided by the user already start with '/', then + # they are considered relative to the toplevel of the repository + # already. If they do not start with /, then we need to adjust + # them to start with the appropriate prefix. + case "$cur" in + /*) + cur="${cur:1}" + ;; + *) + pfx="$(__git rev-parse --show-prefix)" + esac + + # Since sparse-index is limited to cone-mode, in non-cone-mode the + # list of valid paths is precisely the cached files in the index. + # + # NEEDSWORK: + # 1) We probably need to take care of cases where ls-files + # responds with special quoting. + # 2) We probably need to take care of cases where ${cur} has + # some kind of special quoting. + # 3) On top of any quoting from 1 & 2, we have to provide an extra + # level of quoting for any paths that contain a '*', '?', '\', + # '[', ']', or leading '#' or '!' since those will be + # interpreted by sparse-checkout as something other than a + # literal path character. + # Since there are two types of quoting here, this might get really + # complex. For now, just punt on all of this... + completions="$(__git -C "${toplevel}" -c core.quotePath=false \ + ls-files --cached -- "${pfx}${cur}*" \ + | sed -e s%^%/% -e 's%$% %')" + # Note, above, though that we needed all of the completions to be + # prefixed with a '/', and we want to add a space so that bash + # completion will actually complete an entry and let us move on to + # the next one. + + # Return what we've found. + if test -n "$completions"; then + # We found some completions; return them + local IFS=$'\n' + COMPREPLY=($completions) + else + # Do NOT fall back to bash-style all-local-files-and-dirs + # when we find no match. Such options are worse than + # useless: + # 1. "git sparse-checkout add" needs paths that are NOT + # currently in the working copy. "git + # sparse-checkout set" does as well, except in the + # special cases when users are only trying to narrow + # their sparse checkout to a subset of what they + # already have. + # + # 2. A path like '.config' is ambiguous as to whether + # the user wants all '.config' files throughout the + # tree, or just the one under the current directory. + # It would result in a warning from the + # sparse-checkout command due to this. As such, all + # completions of paths should be prefixed with a + # '/'. + # + # 3. We don't want paths prefixed with a '/' to + # complete files in the system root directory, we + # want it to complete on files relative to the + # repository root. + # + # As such, make sure that NO completions are offered rather + # than falling back to bash's default completions. + COMPREPLY=( "" ) fi } @@ -3097,6 +3360,7 @@ _git_sparse_checkout () { local subcommands="list init set disable add reapply" local subcommand="$(__git_find_on_cmdline "$subcommands")" + local using_cone=true if [ -z "$subcommand" ]; then __gitcomp "$subcommands" return @@ -3107,9 +3371,18 @@ _git_sparse_checkout () __gitcomp_builtin sparse-checkout_$subcommand "" "--" ;; set,*|add,*) - if [ "$(__git config core.sparseCheckoutCone)" == "true" ] || - [ -n "$(__git_find_on_cmdline --cone)" ]; then + if [[ "$(__git config core.sparseCheckout)" == "true" && + "$(__git config core.sparseCheckoutCone)" == "false" && + -z "$(__git_find_on_cmdline --cone)" ]]; then + using_cone=false + fi + if [[ -n "$(__git_find_on_cmdline --no-cone)" ]]; then + using_cone=false + fi + if [[ "$using_cone" == "true" ]]; then __gitcomp_directories + else + __gitcomp_slash_leading_paths fi esac } @@ -3356,7 +3629,7 @@ __git_complete_worktree_paths () # Generate completion reply from worktree list skipping the first # entry: it's the path of the main worktree, which can't be moved, # removed, locked, etc. - __gitcomp_nl "$(git worktree list --porcelain | + __gitcomp_nl "$(__git worktree list --porcelain | sed -n -e '2,$ s/^worktree //p')" } @@ -3592,7 +3865,7 @@ __gitk_main () __git_find_repo_path local merge="" - if [ -f "$__git_repo_path/MERGE_HEAD" ]; then + if __git_pseudoref_exists MERGE_HEAD; then merge="--merge" fi case "$cur" in |