Skip to content

Commit

Permalink
bash/zsh completion: reimplement and decrease runtime by factor 1863
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
rohieb authored and koutcher committed Feb 23, 2020
1 parent bf0cc12 commit 26ab51d
Showing 1 changed file with 69 additions and 255 deletions.
324 changes: 69 additions & 255 deletions contrib/tig-completion.bash
Original file line number Diff line number Diff line change
@@ -1,276 +1,88 @@
##
# 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;;
-*) ;;
*) 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") )
;;
reflog)
__git_complete_command log
;;
"")
__git_complete_command log
__gitcompappend "$(compgen -W "$__tig_options $__tig_commands" -- "$cur")"
;;
*)
__git_complete_command $command
;;
esac
}

Expand All @@ -281,11 +93,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

This comment has been minimized.

Copy link
@darcyparker

darcyparker Dec 2, 2020

Contributor

Using __git_complete is a good idea... but there are cases when it is not defined because git completions are loaded dynamically by bash_completion. See #1011 (comment)


# 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

0 comments on commit 26ab51d

Please sign in to comment.