Completions for git
, including automatically generated completions for both subcommands and command-line options.
Some original inspiration from occivink’s git completer.
Install the elvish-completions
package using epm:
use epm
epm:install github.com/zzamboni/elvish-completions
In your rc.elv
, load this module:
use github.com/zzamboni/elvish-completions/git
Note: This package depends on @muesli’s git library. If you use epm
as described above, this package will be installed automatically, but if you clone this repository by hand, you need to install it by hand.
Now you can type git<space>
, press Tab
and see the corresponding completions. All git
commands are automatically completed with their options (automatically extracted from their help messages). Some commands get more specific completions, including add
, push
, checkout
, diff
and a few others. Git aliases are automatically detected as well. Aliases which point to a single git
command are automatically completed like the original command.
Several components are colorized, you can configure the styles by setting these variables (default values shown):
var modified-style = yellow
var untracked-style = red
var tracked-style = $nil
var branch-style = blue
var remote-style = cyan
var unmerged-style = magenta
You can change which command is used instead of git
by assigning it to $git:git-command
. The command assigned needs to understand at least the same commands and options as git
. One example of such command is hub. You can also assign functions (for example, a wrapper function around git
).
var git-command = git
We first load a number of libraries, including comp
, the Elvish completion framework.
use ./comp
use re
use str
use github.com/muesli/elvish-libs/git
use github.com/zzamboni/elvish-modules/util
This is where the big completion-definition map will get progressively built.
var completions = [&]
We store the output of git:status
in a global variable to make it easier to access by the different completion functions.
var status = [&]
Here we will store the completer function (for easier access and for testing).
var git-arg-completer = { }
The git-command
variable contains the command or function to run instead of git
(can be assigned to hub
, for example, or any wrapper function you want to use, as long as it accepts the same options and commands as git
).
<<git-command>>
The $*-style
variables contains the style (as per styled
) to use for different completion components the completion menu. Set to ''
(an empty string) to show in the normal style.
var modified-style = yellow
var untracked-style = red
var tracked-style = $nil
var branch-style = blue
var remote-style = cyan
var unmerged-style = magenta
The -run-git
function executes a git-like command, with the given arguments. $git-command
can be a single command, a multi-word command or a function and still be executed correctly. We cannot simply run $gitcmd $@rest
because Elvish always interprets the first token (the head) to be the command. One example of a multi-word $gitcmd
is "vcsh <repo>"
, after which any git subcommand is valid.
(please note: $git-command
is only used for command executed from within this module, not from the muesli/git
module which is used for some pieces of information, so the integration is not yet perfect. But for most commands it should work - for example if you use hub
instead of git
.
fn -run-git {|@rest|
var gitcmds = [$git-command]
if (eq (kind-of $git-command) string) {
set gitcmds = [(re:split " " $git-command)]
}
var cmd = $gitcmds[0]
if (eq (kind-of $cmd) string) {
set cmd = (external $cmd)
}
$cmd (all $gitcmds[1..]) $@rest
}
The -git-opts
function receives an optional git command, runs git [command] -h
and parses the output to extract the command line options. The parsing is done with comp:extract-opts
, but we pre-process the output to join options whose descriptions appear in the next line.
fn -git-opts {|@cmd|
set _ = ?(-run-git $@cmd -h 2>&1) | drop 1 | if (eq $cmd []) {
comp:extract-opts &fold=$true ®ex='--(\w[\w-]*)' ®ex-map=[&long=1]
} else {
comp:extract-opts &fold=$true
}
}
We define the functions that return different possible values used in the completions. Some of these functions assume that $status
contains already the output from git:status
, which gets executed as the pre-hook of the git completer function below.
fn MODIFIED { all $status[local-modified] | comp:decorate &style=$modified-style }
fn UNTRACKED { all $status[untracked] | comp:decorate &style=$untracked-style }
fn UNMERGED { all $status[unmerged] | comp:decorate &style=$unmerged-style }
fn MOD-UNTRACKED { MODIFIED; UNTRACKED }
fn TRACKED { set _ = ?(-run-git ls-files 2>&-) | comp:decorate &style=$tracked-style }
fn BRANCHES {|&all=$false &branch=$true|
var -allarg = []
var -branch = ''
if $all { set -allarg = ['--all'] }
if $branch { set -branch = ' (branch)' }
set _ = ?(-run-git branch --list (all $-allarg) --format '%(refname:short)' 2>&- |
comp:decorate &display-suffix=$-branch &style=$branch-style)
}
fn REMOTE-BRANCHES {
set _ = ?(-run-git branch --list --remote --format '%(refname:short)' 2>&- |
grep -v HEAD |
each {|branch| re:replace 'origin/' '' $branch } |
comp:decorate &display-suffix=' (remote branch)' &style=$branch-style)
}
fn REMOTES { set _ = ?(-run-git remote 2>&- | comp:decorate &display-suffix=' (remote)' &style=$remote-style ) }
fn STASHES { set _ = ?(-run-git stash list 2>&- | each {|l| put [(re:split : $l)][0] } ) }
$git:git-completions
contains the specialized completions for some git commands. Each sequence is a list of functions which return the possible completions at that point in the command. The ...
as a last element in some of them indicates that the last completion function is repeated for all further argument positions. The completion can also be a string, in which case it means an alias for some other command.
var git-completions = [
&add= [ {|stem| MOD-UNTRACKED; UNMERGED; comp:dirs $stem } ... ]
&stage= add
&checkout= [ { MODIFIED; BRANCHES } ... ]
&switch= [ { $BRANCHES~ &branch=$false; REMOTE-BRANCHES } ]
&mv= [ {|stem| TRACKED; comp:dirs $stem } ... ]
&rm= [ {|stem| TRACKED; comp:dirs $stem } ... ]
&diff= [ { MODIFIED; BRANCHES } ... ]
&push= [ $REMOTES~ $BRANCHES~ ]
&pull= [ $REMOTES~ { BRANCHES &all } ]
&merge= [ $BRANCHES~ ... ]
&init= [ {|stem| put "."; comp:dirs $stem } ]
&branch= [ $BRANCHES~ ... ]
&rebase= [ { $BRANCHES~ &all } ... ]
&cherry= [ { $BRANCHES~ &all } $BRANCHES~ $BRANCHES~ ]
&cherry-pick= [ { $BRANCHES~ &all } ... ]
&stash= [
&list= (comp:sequence [])
&clear= (comp:sequence [])
&show= (comp:sequence [ $STASHES~ ])
&drop= (comp:sequence &opts=[[&short=q &long=quiet]] [ $STASHES~ ])
&pop= (comp:sequence &opts=[[&short=q &long=quiet] [&long=index]] [ $STASHES~ ])
&apply= pop
&branch= (comp:sequence [ [] $STASHES~ ])
&push= (comp:sequence [ $comp:files~ ... ] &opts=[
[&short=p &long=patch]
[&short=k &long=keep-index] [&long=no-keep-index]
[&short=q &long=quiet]
[&short=u &long=include-untracked]
[&short=a &long=all]
[&short=m &long=message &arg-required]
])
&create= (comp:sequence [])
&store= (comp:sequence [ $BRANCHES~ ] &opts=[
[&short=m &long=message &arg-required]
[&short=q &long=quiet]
])
]
]
In the git:init
function we initialize the $completions
map with the necessary data structure for comp:subcommands
to provide the completions. We extract as much information as possible automatically from git
itself.
fn init {
set completions = [&]
-run-git help -a --no-verbose | eawk {|line @f| if (re:match '^ [a-z]' $line) { put $@f } } | each {|c|
var seq = [ $comp:files~ ... ]
if (has-key $git-completions $c) {
set seq = $git-completions[$c]
}
if (eq (kind-of $seq) string) {
set completions[$c] = $seq
} elif (eq (kind-of $seq) map) {
set completions[$c] = (comp:subcommands $seq)
} else {
set completions[$c] = (comp:sequence $seq &opts={ -git-opts $c })
}
}
-run-git config --list | each {|l| re:find '^alias\.([^=]+)=(.*)$' $l } | each {|m|
var alias target = $m[groups][1 2][text]
if (has-key $completions $target) {
set completions[$alias] = $target
} else {
set completions[$alias] = (comp:sequence [])
}
}
set git-arg-completer = (comp:subcommands $completions ^
&pre-hook={|@_| set status = (git:status) } &opts={ -git-opts }
)
set edit:completion:arg-completer[git] = $git-arg-completer
}
Next , we fetch the list of valid git commands from the output of git help -a
, and store the corresponding completion sequences in $completions
. All of them are configured to produce completions for their options, as extracted by the -git-opts
function. Commands that have corresponding definitions in $git-completions
get them, otherwise they get the generic filename completer.
-run-git help -a --no-verbose | eawk [line @f]{ if (re:match '^ [a-z]' $line) { put $@f } } | each [c]{
seq = [ $comp:files~ ... ]
if (has-key $git-completions $c) {
seq = $git-completions[$c]
}
if (eq (kind-of $seq) string) {
completions[$c] = $seq
} elif (eq (kind-of $seq) map) {
completions[$c] = (comp:subcommands $seq)
} else {
completions[$c] = (comp:sequence $seq &opts={ -git-opts $c })
}
}
Next, we parse the defined aliases from the output of git config --list
. We store the aliases in completions
as well, but we check if an alias points to another valid command. In this case, we store the name of the target command as its value, which comp:expand
interprets as “use the completions from the target command”. If an alias does not expand to another existing command, we set up its completions as empty.
-run-git config --list | each [l]{ re:find '^alias\.([^=]+)=(.*)$' $l } | each [m]{
alias target = $m[groups][1 2][text]
if (has-key $completions $target) {
completions[$alias] = $target
} else {
completions[$alias] = (comp:sequence [])
}
}
We setup the completer by assigning the function to the corresponding element of $edit:completion:arg-completer
.
git-arg-completer = (comp:subcommands $completions ^
&pre-hook=[@_]{ status = (git:status) } &opts={ -git-opts }
)
edit:completion:arg-completer[git] = $git-arg-completer
We run init
by default on load, although it can be re-run if you change any configuration variables (most notably git:git-command
).
init
use github.com/zzamboni/elvish-completions/git
use github.com/zzamboni/elvish-modules/test
var cmds = ($git:git-arg-completer git '')
(test:set github.com/zzamboni/elvish-completions/git
(test:set "common top-level commands"
(test:check { has-value $cmds add })
(test:check { has-value $cmds checkout })
(test:check { has-value $cmds commit })
)
)