Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: Slowness issue with git completion



Nikolai Weibull wrote:
> How did you rewrite it?  I tried implementing it with
> *(e:__git_cached:) (or similar), but that was, in my implementation, a
> lot slower.  And this wasn’t even on Cygwin, where the forking makes
> it even slower.

Below is how I was going about it. As I said, it may be full of bugs and
other short-comings.  It was pretty quick in the linux kernel repo,
though (I had implemented a minimal _git completion that used the
"git-add" completion to test the code).

I was also thinking about a scheme to give the user the option to choose
smart (and slow) and utterly-dumb (and quick) file completion in various
contexts via zstyle, but lost interest because it's just *so* much to
do.

Anyway, here it is:

# helpers...
# If a sub-command completion requires zsh to be in a git repository when
# executing, this function should be run to guard that. Usage:
#     __git-needs_repo || return 0
function __git-needs_repo {
    if [[ -z ${_gitdir} ]]; then
        _message "Not a git repository."
        return 1
    fi
    return 0
}

# Git file listing helpers.
#
# With git, files can be in various states. Files from one state may make
# sense for completion with one sub-command, but may be utterly useless
# with the next one. Therefore, we got a whole battery of helpers, that
# should make the job a lot easier.
#
# Identifying the enemy:
#
#   index files
#       Files git already knows about (aka. "stuff checked in"). The
#       "git ls-tree" command can list those.
#
#   modified files
#       Files git knows about, but which have also changed. These changes
#       could be staged or not. So we end up with the following set of
#       states:
#           mod-staged     - files with staged modifications
#               "git diff-index --cached --diff-filter=M --name-only"
#           mod-unstaged   - files, whose modifications are *not* staged
#               "git ls-files -m"
#           modified       - both.
#               "git diff-index --diff-filter=M --name-only"
#
#   other files
#       Git doesn't know these files. "git ls-files -o"
#
#   ignored files
#       Git doesn't know these, and it was even told to ignore them, when
#       it sees them. "git ls-files --exclude-standard -i -o"
#
#   deleted files
#       Files git knows about, but which will be removed from its knowledge
#       in the next commit (note that the actual file does not have to be
#       deleted - "git rm --cached file" has that effect).
#       "git diff-index --name-only --diff-filter=D"
#
#   killed files
#       Sometimes, in order to call "git checkout-index" successfully,
#       files or directories may need to be removed to resolve conflicts.
#       This may happen rarely, but we can use "git ls-files -k" to catch
#       these, so we probably should. It may come in handy.
#
#   unmerged files
#       When multiple versions of a file exist in the staging area (the
#       index file) that's an unmerged file. The only way I can think
#       of end up having these in git-completion is during the resolution
#       of a merge-conflict. And then, it would probably be most useful
#       to have these available in an editor's completion function. Like
#       "killed files", this sort of file is available from "git ls-files",
#       but it'll need extra filtering, because --unmerged implicitly
#       turns on --stage. "git ls-files -u"
#
# I'll be nameing these `__git-files_<type>', so ie "index files" will be
# completed by `__git-files_index'.

function __git-files_index {
    local expl cw cd
    local -a files

    cw=${words[CURRENT]}
    case $cw in
    (*/) cd=${cw} ;;
    (*/*) cd=${cw:h} ;;
    esac
    files=( ${(ps:\0:)"$(_call_program index-files git ls-tree --name-only -z HEAD${cd:+:$cd})"} )
    _wanted 'index-files' expl 'index file' compadd -f ${expl} -- ${cd:+$cd}"${files[@]}"
}

function __git-files_modified {
    local expl cw cd
    local -a files

    cw=${words[CURRENT]}
    case $cw in
    (*/) cd=${cw} ;;
    (*/*) cd=${cw:h} ;;
    esac
    files=( ${(ps:\0:)"$(_call_program modified-files git diff-index -z --diff-filter=MDA --name-only HEAD ${cd:+$cd})"} )
    files=( ${(u)${${(M)files:#${cd}*}/(#b)(${cd}[^\/]##)*/${match[1]}}} )
    _wanted 'modified-files' expl 'modified file' compadd -f ${expl} -- "${files[@]}"
}

function __git-files_modified_staged {
    local expl cw cd
    local -a files

    cw=${words[CURRENT]}
    case $cw in
    (*/) cd=${cw} ;;
    (*/*) cd=${cw:h} ;;
    esac
    files=( ${(ps:\0:)"$(_call_program staged-modified-files git diff-index -z --cached --diff-filter=MDA --name-only HEAD ${cd:+$cd})"} )
    files=( ${(u)${${(M)files:#${cd}*}/(#b)(${cd}[^\/]##)*/${match[1]}}} )
    _wanted 'staged-modified-files' expl 'staged modified file' compadd -f ${expl} -- "${files[@]}"
}

function __git-files_modified_unstaged {
    local expl cw cd
    local -a files

    cw=${words[CURRENT]}
    case $cw in
    (*/) cd=${cw} ;;
    (*/*) cd=${cw:h} ;;
    esac
    files=( ${(ps:\0:)"$(_call_program unstaged-modified-files git ls-files --exclude-standard -m -z ${cd:+$cd})"} )
    files=( ${(u)${${(M)files:#${cd}*}/(#b)(${cd}[^\/]##)*/${match[1]}}} )
    _wanted 'unstaged-modified-files' expl 'unstaged modified file' compadd -f ${expl} -- "${files[@]}"
}

function __git-files_ignored {
    local expl cw cd
    local -a files

    cw=${words[CURRENT]}
    case $cw in
    (*/) cd=${cw} ;;
    (*/*) cd=${cw:h} ;;
    esac
    files=( ${(ps:\0:)"$(_call_program ignored-files git ls-files --exclude-standard -m -z ${cd:+$cd})"} )
    files=( ${(u)${${(M)files:#${cd}*}/(#b)(${cd}[^\/]##)*/${match[1]}}} )
    _wanted 'ignored-files' expl 'ignored file' compadd -f ${expl} -- "${files[@]}"
}

function __git-files_other {
    local expl cw cd
    local -a files

    cw=${words[CURRENT]}
    case $cw in
    (*/) cd=${cw} ;;
    (*/*) cd=${cw:h} ;;
    esac
    files=( ${(ps:\0:)"$(_call_program other-files git ls-files --exclude-standard -o -z ${cd:+$cd})"} )
    files=( ${(u)${${(M)files:#${cd}*}/(#b)(${cd}[^\/]##)*/${match[1]}}} )
    _wanted 'other-files' expl 'other file' compadd -f ${expl} -- "${files[@]}"
}

function __git-files_deleted {
    local expl cw cd
    local -a files

    cw=${words[CURRENT]}
    case $cw in
    (*/) cd=${cw} ;;
    (*/*) cd=${cw:h} ;;
    esac
    files=( ${(ps:\0:)"$(_call_program deleted-files git diff-index -z --diff-filter=D --name-only HEAD ${cd:+$cd})"} )
    files=( ${(u)${${(M)files:#${cd}*}/(#b)(${cd}[^\/]##)*/${match[1]}}} )
    _wanted 'deleted-files' expl 'deleted file' compadd -f ${expl} -- "${files[@]}"
}

# other helpers
function __git-tags {
    local expl cw
    local -a tags

    cw=${words[CURRENT]}
    tags=( ${${(f)"$(_call_program git-tags git for-each-ref --format='"%(refname)"' refs/tags/${cw:+$cw*})"}#refs/tags/} )
    _wanted 'tags' expl 'git tag' compadd ${expl} -- "${tags[@]}"
}

function __git-branches {
    local expl cw
    local -a branches

    cw=${words[CURRENT]}
    branches=( ${${(f)"$(_call_program git-branches git for-each-ref --format='"%(refname)"' refs/heads/${cw:+$cw*})"}#refs/heads/} )
    _wanted 'branches' expl 'git branch' compadd ${expl} -- "${branches[@]}"
}

function __git-branches_remote {
    local expl cw
    local -a branches

    cw=${words[CURRENT]}
    branches=( ${${(f)"$(_call_program git-branches-remote git for-each-ref --format='"%(refname)"' refs/remotes/${cw:+$cw*})"}#refs/remotes/} )
    _wanted 'remote branches' expl 'git branch (remote)' compadd ${expl} -- "${branches[@]}"
}

function __git-remotes {
    local expl cw
    local -a remotes

    cw=${words[CURRENT]}
    remotes=( ${${${${(f)"$(_call_program git-remotes git config --get-regexp "'remote\..*\.url'")"}%% *}#remote.}%.url} )
    _wanted 'remotes' expl 'remote' compadd ${expl} -- "${remotes[@]}"
}



Messages sorted by: Reverse Date, Date, Thread, Author