From edfdbab507236157eaf9eece88a450e1790abcfb Mon Sep 17 00:00:00 2001 From: Roland Hieber Date: Thu, 17 Oct 2019 16:40:01 +0200 Subject: [PATCH] bash/zsh completion: reimplement and decrease runtime by factor 1863 Fixes: #795 ("tab completion is really slow") When run on a Git repository with a large amount of files, like the linux repository, __tig_complete_file() can take a very long time to complete. This is unfortunate because when accidentally pressing Tab in such a repo, bash gets stuck until the completion function has finished, and does not even allow the user to cancel the operation with Ctrl-C. In contrast, the current git completion does not have these problems, and since tig's command line parameters are mostly parameters to git-log or git-diff, we can use git's native completion for those cases instead. This also has the advantage that we do not need to care about updating the tig completion when new parameters are added to git-log or git-diff. I have tested this only in bash, not in zsh. For comparison, here is an exemplary runtime measurement of the old and the new completion in bash, showing an improvement of factor 1863. Admittedly, this was on a fairly loaded system (a build server), but still then the new completion runs in unnoticable time. I'm also getting similar results on an idle system in the same repo with runtime improvements of about factor 1000. linux (master) $ echo $BASH_VERSION 5.0.3(1)-release linux (master) $ git describe v5.4-rc3-38-gbc88f85c6c09 linux (master) $ uptime 16:45:52 up 36 days, 3:33, 224 users, load average: 24.17, 38.87, 31.21 # The new completion: linux (master) $ . ../tig/contrib/tig-completion.bash linux (master) $ time COMP_WORDS=("tig log") COMP_CWORD=2 __git_wrap_tig real 0m0.127s user 0m0.085s sys 0m0.024s # The old completion: linux (master) $ . /usr/share/bash-completion/completions/tig linux (master) $ time COMP_WORDS=("tig log") COMP_CWORD=2 _tig real 2m1.145s user 1m40.379s sys 0m1.347s With this change, almost nothing of the old completion remains, so change the copyright header accordingly. I'm also now adding a GPL-2.0-or-later dedication, which is the same license as most other code in this repository; and which, I presume, was also the author's intent since the first incarnation of this file. While at it, fix some typos in old comments, and update installation instructions. Signed-off-by: Roland Hieber --- contrib/tig-completion.bash | 321 ++++++++---------------------------- 1 file changed, 66 insertions(+), 255 deletions(-) diff --git a/contrib/tig-completion.bash b/contrib/tig-completion.bash index 78c86d53f..e7756d949 100755 --- a/contrib/tig-completion.bash +++ b/contrib/tig-completion.bash @@ -1,276 +1,85 @@ -## -# bash completion support for tig -# +# bash/zsh completion for tig +# +# Copyright (C) 2019 Roland Hieber, Pengutronix # Copyright (C) 2007-2010 Jonas fonseca -# Copyright (C) 2006,2007 Shawn Pearce # -# Based git's git-completion.sh: http://repo.or.cz/w/git/fastimport.git +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. # -# The contained completion routines provide support for completing: +# This completion builds upon the git completion (>= git 1.17.11), +# which most tig users should already have available at this point. +# To use these routines: # -# *) local and remote branch names -# *) local and remote tag names -# *) tig 'subcommands' -# *) tree paths within 'ref:path/to/file' expressions +# 1) Copy this file to somewhere (e.g. ~/.bash_completion.d/tig). # -# To use these routines: +# 2) Add the following line to your .bashrc: # -# 1) Copy this file to somewhere (e.g. ~/.tig-completion.sh). -# 2) Added the following line to your .bashrc: -# source ~/.tig-completion.sh +# source ~/.bash_completion.d/tig +# +# Note that most Linux distributions source everything in +# ~/.bash_completion.d/ automatically at bash startup, so you +# have to source this script manually only in shells that were +# already running before. # # 3) You may want to make sure the git executable is available # in your PATH before this script is sourced, as some caching # is performed while the script loads. If git isn't found # at source time then all lookups will be done on demand, # which may be slightly slower. -# - -__tigdir () -{ - if [ -z "$1" ]; then - if [ -n "$__git_dir" ]; then - echo "$__git_dir" - elif [ -d .git ]; then - echo .git - else - git rev-parse --git-dir 2>/dev/null - fi - elif [ -d "$1/.git" ]; then - echo "$1/.git" - else - echo "$1" - fi -} - -_tigcomp () -{ - local all c s=$'\n' IFS=' '$'\t'$'\n' - local cur="${COMP_WORDS[COMP_CWORD]}" - if [ $# -gt 2 ]; then - cur="$3" - fi - for c in $1; do - case "$c$4" in - --*=*) all="$all$c$4$s" ;; - *.) all="$all$c$4$s" ;; - *) all="$all$c$4 $s" ;; - esac - done - IFS=$s - COMPREPLY=($(compgen -P "$2" -W "$all" -- "$cur")) - return -} -__tig_refs () -{ - local cmd i is_hash=y dir="$(__tigdir "$1")" - if [ -d "$dir" ]; then - for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD; do - if [ -e "$dir/$i" ]; then echo $i; fi - done - for i in $(git --git-dir="$dir" \ - for-each-ref --format='%(refname)' \ - refs/tags refs/heads refs/remotes); do - case "$i" in - refs/tags/*) echo "${i#refs/tags/}" ;; - refs/heads/*) echo "${i#refs/heads/}" ;; - refs/remotes/*) echo "${i#refs/remotes/}" ;; - *) echo "$i" ;; - esac - done - return - fi - for i in $(git-ls-remote "$dir" 2>/dev/null); do - case "$is_hash,$i" in - y,*) is_hash=n ;; - n,*^{}) is_hash=y ;; - n,refs/tags/*) is_hash=y; echo "${i#refs/tags/}" ;; - n,refs/heads/*) is_hash=y; echo "${i#refs/heads/}" ;; - n,refs/remotes/*) is_hash=y; echo "${i#refs/remotes/}" ;; - n,*) is_hash=y; echo "$i" ;; - esac - done -} - -__tig_complete_file () -{ - local pfx ls ref cur="${COMP_WORDS[COMP_CWORD]}" - case "$cur" in - ?*:*) - ref="${cur%%:*}" - cur="${cur#*:}" - case "$cur" in - ?*/*) - pfx="${cur%/*}" - cur="${cur##*/}" - ls="$ref:$pfx" - pfx="$pfx/" - ;; - *) - ls="$ref" - ;; - esac - COMPREPLY=($(compgen -P "$pfx" \ - -W "$(git --git-dir="$(__tigdir)" ls-tree "$ls" \ - | sed '/^100... blob /s,^.* ,, - /^040000 tree /{ - s,^.* ,, - s,$,/, - } - s/^.* //')" \ - -- "$cur")) - ;; - *) - _tigcomp "$(__tig_refs)" - ;; - esac -} - -__tig_complete_revlist () -{ - local pfx cur="${COMP_WORDS[COMP_CWORD]}" - case "$cur" in - *...*) - pfx="${cur%...*}..." - cur="${cur#*...}" - _tigcomp "$(__tig_refs)" "$pfx" "$cur" - ;; - *..*) - pfx="${cur%..*}.." - cur="${cur#*..}" - _tigcomp "$(__tig_refs)" "$pfx" "$cur" - ;; - *.) - _tigcomp "$cur." - ;; - *) - _tigcomp "$(__tig_refs)" - ;; - esac -} - -_tig_options () -{ - local cur="${COMP_WORDS[COMP_CWORD]}" - case "$cur" in - --pretty=*) - _tigcomp " - oneline short medium full fuller email raw - " "" "${cur##--pretty=}" - return - ;; - --*) - _tigcomp " - --max-count= --max-age= --since= --after= - --min-age= --before= --until= - --root --not --topo-order --date-order - --no-merges - --abbrev-commit --abbrev= - --relative-date - --author= --committer= --grep= - --all-match - --pretty= --name-status --name-only - --not --all - --help --version - " - return - ;; - -*) - _tigcomp "-v -h" - return - ;; - esac - __tig_complete_revlist -} - -_tig_blame () -{ - local reply="" ref=HEAD cur="${COMP_WORDS[COMP_CWORD]}" p="" - local pfx=$(git rev-parse --show-prefix 2>/dev/null) - - if test "$COMP_CWORD" -lt 3; then - _tigcomp "$(__tig_refs)" - else - ref="${COMP_WORDS[2]}" - fi - - case "$cur" in - */) p=${cur%/} ;; - */*) p=${cur%/*} ;; - *) p= ;; - esac - - i=${#COMPREPLY[@]} - local IFS=$'\n' - for c in $(git --git-dir="$(__tigdir)" ls-tree "$ref:$pfx$p" 2>/dev/null | - sed -n '/^100... blob /{ - s,^.* ,, - s,$, , - p - } - /^040000 tree /{ - s,^.* ,, - s,$,/, - p - }') - do - c="${p:+$p/}$c" - if [[ "$c" == "$cur"* ]]; then - COMPREPLY[i++]=$c - fi - done -} - -_tig_show () -{ - local cur="${COMP_WORDS[COMP_CWORD]}" - case "$cur" in - --pretty=*) - _tigcomp " - oneline short medium full fuller email raw - " "" "${cur##--pretty=}" - return - ;; - --*) - _tigcomp "--pretty=" - return - ;; - esac - __tig_complete_file -} - -_tig () -{ - local i c=1 command __tig_dir - - while [ $c -lt $COMP_CWORD ]; do - i="${COMP_WORDS[c]}" +__tig_options=" + -v --version + -h --help + -C + -- + + +" +__tig_commands=" + blame + grep + log + reflog + refs + stash + status + show +" + +_tig() { + # parse already existing parameters + local i c=1 command + while [ $c -lt $cword ]; do + i="${words[c]}" case "$i" in - --) command="log"; break;; - -*) ;; - *) command="$i"; break ;; + --) command="log"; break;; + -*) continue;; + *) command="$i"; break ;; esac c=$((++c)) done - if [ $c -eq $COMP_CWORD -a -z "$command" ]; then - case "${COMP_WORDS[COMP_CWORD]}" in - --*=*) COMPREPLY=() ;; - -*) _tig_options ;; - *) _tigcomp "blame status show log stash grep $(__tig_refs)" ;; - esac - return - fi + # options -- only before command + case "$command$cur" in + -C*) + COMPREPLY=( $(compgen -d -P '-C' -- ${cur##-C}) ) + return + ;; + esac + # commands case "$command" in - blame) _tig_blame ;; - show) _tig_show ;; - status) ;; - *) _tigcomp " - $(__tig_complete_file) - $(__tig_refs) - " ;; + refs|status|stash) + COMPREPLY=( $(compgen -W "$__tig_options" -- "$cur") ) + ;; + "") + __git_complete_command log + __gitcompappend "$(compgen -W "$__tig_options $__tig_commands" -- "$cur")" + ;; + *) + __git_complete_command $command + ;; esac } @@ -281,11 +90,13 @@ if [ -n "$ZSH_VERSION" ]; then bashcompinit fi -complete -o default -o nospace -F _tig tig +# we use internal git-completion functions, so wrap _tig for all necessary +# variables (like cword and prev) to be defined +__git_complete tig _tig # The following are necessary only for Cygwin, and only are needed # when the user has tab-completed the executable name and consequently # included the '.exe' suffix. if [ Cygwin = "$(uname -o 2>/dev/null)" ]; then - complete -o default -o nospace -F _tig tig.exe + __git_complete tig.exe _tig fi