8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

zsh の git ブランチ名補完でローカルブランチだけ補完する

Last updated at Posted at 2017-12-26

なにをやったの

zsh には簡単に使い始められて強力な補完システムがあります

autoload -U compinit && compinit

この補完システムは、入力中のコマンドによってよしなに補完対象を選んでくれます

$ # cd ならディレクトリ名
$ cd ~/[.emacs.d]
[.emacs.d]
 .ssh
 .zshrc
 Documents
 Downloads
$ # git checkout ならブランチ名
$ git checkout [my-awesome-branch]
[my-awesome-branch]
 another-awesome-branch
 master
 devel

が、この補完システム、 git checkout 時にリモートブランチまですべて補完しようとしてしまうので、リモートブランチが大量にある(いちいちリモートブランチを削除する運用をしていない)リポジトリで TAB を押すと固まります。

これが残念だったので、「 git checkout ではローカルブランチしか補完しない」設定をしてみました。

設定が結構面倒だった(というかもっといい方法ありそう)だったので、記事にしてみました。こうすれば一撃だよ!みたいなツッコミあればぜひ教えてください👀

zsh の git 補完の実装

zsh の git ブランチ名補完をいじるために、まずは元の実装を当たってみます。

zsh には PATH のほかに FPATH という環境変数があって、 zsh 内で使われる関数類の定義はこのディレクトリから探索されます。この中のどこかでgit checkout の補完に使われる関数が定義されているはずです。

環境にもよると思うのですが、自分の環境では /usr/local/share/zsh/site-functions/git-complete.bash にいました (バージョンによって多少実装は違うかもしれません)。

_git_checkout ()
{
    __git_has_doubledash && return
    case "$cur" in

    (諸々のオプション名の補完... 省略)

    *)
        # check if --track, --no-track, or --no-guess was specified
        # if so, disable DWIM mode
        local flags="--track --no-track --no-guess" track=1
        if [ -n "$(__git_find_on_cmdline "$flags")" ]; then
            track=''
        fi
        __gitcomp_nl "$(__git_refs '' $track)"
        ;;
    esac
}

細かいことはよくわからないけど、 __git_refs がブランチ名の一覧を取ってきていそうな雰囲気があります。

一方、同じファイルにある、 git branch の補完の実装 (こちらはデフォルトでローカルブランチだけが補完されます) を見てみると、

_git_branch ()
{

    (... 中略)

    *)
        if [ $only_local_ref = "y" -a $has_r = "n" ]; then
            __gitcomp_nl "$(__git_heads)"
        else
            __gitcomp_nl "$(__git_refs)"
        fi
        ;;
    esac
}

どうやら、 __git_refs の代わりに __git_heads を呼んでやればローカルブランチだけが補完されそうな雰囲気があります。

最初に試したこと

方針は立ったので、まずは組み込みの _git_checkout の実装を直接書き換えてみます。

 _git_checkout ()
 {
     __git_has_doubledash && return
     case "$cur" in

     (諸々のオプション名の補完... 省略)

     *)
-        # check if --track, --no-track, or --no-guess was specified
-        # if so, disable DWIM mode
-        local flags="--track --no-track --no-guess" track=1
-        if [ -n "$(__git_find_on_cmdline "$flags")" ]; then
-            track=''
-        fi
-        __gitcomp_nl "$(__git_refs '' $track)"
+        __gitcomp_nl "$(__git_heads)"
         ;;
     esac
 }

zsh を再起動して確かめてみると、確かにローカルブランチだけが補完されます。

ひとまず目的は達成できました。

設定ファイルの中にねじ込みたい

一応の目的は達成できたわけですが、 zsh に一緒についてきたスクリプトを直接変更したので、このままだと zsh のアップデートのタイミングとかで元に戻ってしまったりしそうだし、端末間で設定を共有するのも難しいので、できれば設定ファイルの中で完結したいです。

最初に考えたのは、 .zshrc の中に上の _git_checkout の定義をベタ書きして、元の定義を上書きすることでした。が、これはうまくいきません。

なぜなら、 zsh には autoload という仕組みがあって、関数の実装は実際に使われるタイミングまでロードされないからです。

$ which _git_checkout
_git_checkout ()
{
    (僕らがカスタムした定義... 省略)
}

$ git checkout [my-branch] # ここで TAB 補完
[my-branch]
 another-branch
 yet-another-branch

$ which _git_checkout # 元の定義に戻っている
_git_checkout ()
{
    (オリジナルの定義... 省略)
}

むしろ僕らの定義した _git_checkout が、 autoload の走るタイミングでオリジナルの _git_checkout の定義に上書きされてしまう格好になってしまいました。

どうやら、 git-complete.bash の読み込まれた直後に定義の上書をしなければならなそうです。

こんな時に僕がよく使うのは、次のような方法です:ダミーの定義を用意して、その中で本家の定義を読み込みつつ、読み込み完了時にやりたい処理を一緒にやる。

_git のダミーを作る

さて、ダミーを作らなければいけないのは、実は git-complete.bash 本体ではありません。

僕らが zsh の補完を有効にするときに呼び出す、 compinit 関数の実装を見てみます。

(... 省略)

  for _i_dir in $fpath; do
    [[ $_i_dir = . ]] && continue
    (( $_i_wdirs[(I)$_i_dir] )) && continue
    for _i_file in $_i_dir/^([^_]*|*~|*.zwc)(N); do
      _i_name="${_i_file:t}"
      (( $+_i_test[$_i_name] + $_i_wfiles[(I)$_i_file] )) && continue
      _i_test[$_i_name]=yes
      IFS=$' \t' read -rA _i_line < $_i_file
      _i_tag=$_i_line[1]
      shift _i_line
      case $_i_tag in
      (\#compdef)
        if [[ $_i_line[1] = -[pPkK](n|) ]]; then
          compdef ${_i_line[1]}na "${_i_name}" "${(@)_i_line[2,-1]}"
        else
          compdef -na "${_i_name}" "${_i_line[@]}"
        fi
        ;;
      (\#autoload)
        autoload -Uz "$_i_line[@]" ${_i_name}
        [[ "$_i_line" != \ # ]] && _compautos[${_i_name}]="$_i_line"
        ;;
      esac
    done
  done

(... 省略)

ちょっとややこしいのですが、 FPATH にある、 #compdef という行から始まるファイルに片っ端から compdef 関数をかけています。 compdef 関数の実装も同じファイルにあって、冒頭のコメントを見てみると、

#   `#compdef <names ...>'
#     If the first line looks like this, the file is autoloaded as a
#     function and that function will be called to generate the matches
#     when completing for one of the commands whose <names> are given.

autoload しつつ、補完エンジンとして登録するような処理をする関数なのが見て取れます。

立ち返って git-complete.bash を見てみると、このファイルには #compdef キーワードがありません。したがって、このファイルは compinit で autoload されているファイルではありません。

同じディレクトリ内をあさってみると、 _git という別のファイルが見つかります。このファイルの冒頭には #compdef キーワードがあり、

#compdef git gitk
(... 省略)

どうやらこのファイルの中で、本命の git-complete.bash も読み込まれているっぽいことがわかります。

(... 省略)

zstyle -s ":completion:*:*:git:*" script script
if [ -z "$script" ]; then
    local -a locations
    local e
    locations=(
        $(dirname ${funcsourcetrace[1]%:*})/git-completion.bash
        '/etc/bash_completion.d/git' # fedora, old debian
        '/usr/share/bash-completion/completions/git' # arch, ubuntu, new debian
        '/usr/share/bash-completion/git' # gentoo
        )
    for e in $locations; do
        test -f $e && script="$e" && break
    done
fi

(... 省略)

そんなこんなで、僕らがニセモノを作ってオーバーライドしなければいけないのは、 _git だということがわかりました。

満を持して、こんなファイルを _git という名前で作って:

#compdef git gitk

# 本家 _git を読み込む
source /usr/local/share/zsh/site-functions/_git

# _git_checkout の定義を差し替えて、ローカルブランチだけを補完対象にする
_git_checkout () {
    __git_has_doubledash && return
    case "$cur" in
        --conflict=*)
            __gitcomp "diff3 merge" "" "${cur##--conflict=}"
            ;;
        --*)
            __gitcomp "
            --quiet --ours --theirs --track --no-track --merge
            --conflict= --orphan --patch
            "
            ;;
        *)
            __gitcomp_nl "$(__git_heads)"
            ;;
    esac
}

_git

適当なフォルダ (たとえば、 ~/.zsh.d/functions) に置き、 FPATH を通して本家より先に読まれるようにします。

export FPATH="$HOME/.zsh.d/functions:$FPATH"

これでめでたく git checkout でローカルブランチだけが補完されるようになります。

本家のファイルを直接編集したわけでもないので、設定ファイルとダミーの _git をまとめてコピーしてあげれば他の環境でも同じ設定が使えるし、アップデートなどで上書きされてしまうこともなさそうです。

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?