Skip to content

Latest commit

 

History

History
312 lines (258 loc) · 13.2 KB

git.org

File metadata and controls

312 lines (258 loc) · 13.2 KB

Elvish completions for git

Completions for git, including automatically generated completions for both subcommands and command-line options.

Some original inspiration from occivink’s git completer.

Table of Contents

Usage

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

Implementation

Libraries and global variables

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 = { }

Configuration variables

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

Utility functions

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 &regex='--(\w[\w-]*)' &regex-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] } ) }

Initialization of completion definitions

$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

Test suite

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 })
  )
)