zsh の vcs_info に独自の処理を追加して stash 数とか push していない件数とか何でも表示する

zsh で Git 使ってる人はプロンプトにブランチ名とかを表示してる人も多いと思う。
zsh に標準で入ってる vcs\_info っていうのを使うとだいたいいい感じにできるんだけど、できないことも当然ある。
例えば stash した数の表示には対応していないので、自分で無理矢理な感じで Git コマンドを呼び出してプロンプトに表示してる人もいると思う。

でも zsh 4.3.11 ぐらいから vcs\_info に Hooks というのが追加されて、元の機能に自分で処理を追加できるようになってる。これを使うと好きなようにカスタマイズできるようになるので紹介する。


## この記事でできるようになること

こんなことがプロンプトに表示できるようになる。

- 使用しているバージョン管理システムの名前(svn, git, hg, ...)
- 現在のブランチ名
- マージ失敗のエラー表示

さらに Git の場合は以下が表示できる

- ステージしていない修正があるか
- ステージした修正があるか
- untracked なファイル(バージョン管理に含まれていないファイル)があるか
- masterからリモートに push していないコミットが何件あるか
- master にマージしていないコミットが何件あるか
- stash が何件あるか


## zshrc の例

こんな風に zshrc に書いておけば OK。

```sh
# vcs_info 設定

RPROMPT=""

autoload -Uz vcs_info
autoload -Uz add-zsh-hook
autoload -Uz is-at-least
autoload -Uz colors

# 以下の3つのメッセージをエクスポートする
#   $vcs_info_msg_0_ : 通常メッセージ用 (緑)
#   $vcs_info_msg_1_ : 警告メッセージ用 (黄色)
#   $vcs_info_msg_2_ : エラーメッセージ用 (赤)
zstyle ':vcs_info:*' max-exports 3

zstyle ':vcs_info:*' enable git svn hg bzr
# 標準のフォーマット(git 以外で使用)
# misc(%m) は通常は空文字列に置き換えられる
zstyle ':vcs_info:*' formats '(%s)-[%b]'
zstyle ':vcs_info:*' actionformats '(%s)-[%b]' '%m' '<!%a>'
zstyle ':vcs_info:(svn|bzr):*' branchformat '%b:r%r'
zstyle ':vcs_info:bzr:*' use-simple true


if is-at-least 4.3.10; then
    # git 用のフォーマット
    # git のときはステージしているかどうかを表示
    zstyle ':vcs_info:git:*' formats '(%s)-[%b]' '%c%u %m'
    zstyle ':vcs_info:git:*' actionformats '(%s)-[%b]' '%c%u %m' '<!%a>'
    zstyle ':vcs_info:git:*' check-for-changes true
    zstyle ':vcs_info:git:*' stagedstr "+"    # %c で表示する文字列
    zstyle ':vcs_info:git:*' unstagedstr "-"  # %u で表示する文字列
fi

# hooks 設定
if is-at-least 4.3.11; then
    # git のときはフック関数を設定する
    
    # formats '(%s)-[%b]' '%c%u %m' , actionformats '(%s)-[%b]' '%c%u %m' '<!%a>'
    # のメッセージを設定する直前のフック関数
    # 今回の設定の場合はformat の時は2つ, actionformats の時は3つメッセージがあるので
    # 各関数が最大3回呼び出される。
    zstyle ':vcs_info:git+set-message:*' hooks \
                                            git-hook-begin \
                                            git-untracked \
                                            git-push-status \
                                            git-nomerge-branch \
                                            git-stash-count

    # フックの最初の関数
    # git の作業コピーのあるディレクトリのみフック関数を呼び出すようにする
    # (.git ディレクトリ内にいるときは呼び出さない)
    # .git ディレクトリ内では git status --porcelain などがエラーになるため
    function +vi-git-hook-begin() {
        if [[ $(command git rev-parse --is-inside-work-tree 2> /dev/null) != 'true' ]]; then
            # 0以外を返すとそれ以降のフック関数は呼び出されない
            return 1
        fi

        return 0
    }
    
    # untracked ファイル表示
    #
    # untracked ファイル(バージョン管理されていないファイル)がある場合は
    # unstaged (%u) に ? を表示
    function +vi-git-untracked() {
        # zstyle formats, actionformats の2番目のメッセージのみ対象にする
        if [[ "$1" != "1" ]]; then
            return 0
        fi

        if command git status --porcelain 2> /dev/null \
            | awk '{print $1}' \
            | command grep -F '??' > /dev/null 2>&1 ; then

            # unstaged (%u) に追加
            hook_com[unstaged]+='?'
        fi
    }

    # push していないコミットの件数表示
    #
    # リモートリポジトリに push していないコミットの件数を
    # pN という形式で misc (%m) に表示する
    function +vi-git-push-status() {
        # zstyle formats, actionformats の2番目のメッセージのみ対象にする
        if [[ "$1" != "1" ]]; then
            return 0
        fi

        if [[ "${hook_com[branch]}" != "master" ]]; then
            # master ブランチでない場合は何もしない
            return 0
        fi

        # push していないコミット数を取得する
        local ahead
        ahead=$(command git rev-list origin/master..master 2>/dev/null \
            | wc -l \
            | tr -d ' ')

        if [[ "$ahead" -gt 0 ]]; then
            # misc (%m) に追加
            hook_com[misc]+="(p${ahead})"
        fi
    }

    # マージしていない件数表示
    #
    # master 以外のブランチにいる場合に、
    # 現在のブランチ上でまだ master にマージしていないコミットの件数を
    # (mN) という形式で misc (%m) に表示
    function +vi-git-nomerge-branch() {
        # zstyle formats, actionformats の2番目のメッセージのみ対象にする
        if [[ "$1" != "1" ]]; then
            return 0
        fi

        if [[ "${hook_com[branch]}" == "master" ]]; then
            # master ブランチの場合は何もしない
            return 0
        fi

        local nomerged
        nomerged=$(command git rev-list master..${hook_com[branch]} 2>/dev/null | wc -l | tr -d ' ')

        if [[ "$nomerged" -gt 0 ]] ; then
            # misc (%m) に追加
            hook_com[misc]+="(m${nomerged})"
        fi
    }

    
    # stash 件数表示
    #
    # stash している場合は :SN という形式で misc (%m) に表示
    function +vi-git-stash-count() {
        # zstyle formats, actionformats の2番目のメッセージのみ対象にする
        if [[ "$1" != "1" ]]; then
            return 0
        fi

        local stash
        stash=$(command git stash list 2>/dev/null | wc -l | tr -d ' ')
        if [[ "${stash}" -gt 0 ]]; then
            # misc (%m) に追加
            hook_com[misc]+=":S${stash}"
        fi
    }
    
fi

function _update_vcs_info_msg() {
    local -a messages
    local prompt
    
    LANG=en_US.UTF-8 vcs_info

    if [[ -z ${vcs_info_msg_0_} ]]; then
        # vcs_info で何も取得していない場合はプロンプトを表示しない
        prompt=""
    else
        # vcs_info で情報を取得した場合
        # $vcs_info_msg_0_ , $vcs_info_msg_1_ , $vcs_info_msg_2_ を
        # それぞれ緑、黄色、赤で表示する
        [[ -n "$vcs_info_msg_0_" ]] && messages+=( "%F{green}${vcs_info_msg_0_}%f" )
        [[ -n "$vcs_info_msg_1_" ]] && messages+=( "%F{yellow}${vcs_info_msg_1_}%f" )
        [[ -n "$vcs_info_msg_2_" ]] && messages+=( "%F{red}${vcs_info_msg_2_}%f" )
    
        # 間にスペースを入れて連結する
        prompt="${(j: :)messages}"
    fi

    RPROMPT="$prompt"
}
add-zsh-hook precmd _update_vcs_info_msg
```


## 表示の例
こんな感じに表示されるっていうスクリーンショットを貼っておく。

### 普通の状態
![普通の状態](https://lh4.googleusercontent.com/-aKJIA6vIIXE/UMSTpkNhvVI/AAAAAAAAAJo/Yl7LvP4y1ug/s469/zsh_vcs_info_1.png)

### ステージしていない修正がある状態
![ステージしていない修正がある状態](https://lh6.googleusercontent.com/-uHQjyhqSYYk/UMSTpxOSXzI/AAAAAAAAAJs/b6e59zhQB48/s469/zsh_vcs_info_2.png)

### ステージした修正がある状態
![ステージした修正がある状態](https://lh4.googleusercontent.com/-LwUMwgeE9iQ/UMSTp6noemI/AAAAAAAAAJw/OLkGYZDIS04/s466/zsh_vcs_info_3.png)

### untracked なファイルがある状態
![untracked なファイルがある状態](https://lh3.googleusercontent.com/-M_OyxSKTMj8/UMSTqTPxkJI/AAAAAAAAAJ0/mJVvVmG-xs4/s469/zsh_vcs_info_4.png)

### リモートに pushしていないコミットが1件ある状態
![pushしていないコミットが1件ある状態](https://lh3.googleusercontent.com/-2zIV8dvcBKo/UMSTqVPV2mI/AAAAAAAAAJ8/epnSDgG0PpI/s466/zsh_vcs_info_5.png)

### stash が1件ある状態
![stash が1件ある状態](https://lh3.googleusercontent.com/-3IJ00mnDIxc/UMSTqov4BKI/AAAAAAAAAKA/64sJVuIJgW4/s465/zsh_vcs_info_6.png)

### いろいろいっぱいある状態
![いろいろいっぱいある状態](https://lh5.googleusercontent.com/-Dnv76kpvYeY/UMSTq7WJ3nI/AAAAAAAAAKI/dB-S_1ZF4Qw/s506/zsh_vcs_info_7.png)

### fix-bug ブランチから master にマージしていないコミットが1件ある状態
![master にマージしていないコミットが1件ある状態](https://lh4.googleusercontent.com/-AH33CASXsAs/UMSTrD-_XrI/AAAAAAAAAKM/UeYKZRFU5Fw/s460/zsh_vcs_info_8.png)

### マージでコンフリクトした状態
![マージでコンフリクトした状態](https://lh3.googleusercontent.com/-skfyQ_p3_04/UMSTrhi05cI/AAAAAAAAAKc/CNXbfo1uD4k/s462/zsh_vcs_info_9.png)

いい感じ。


## カスタマイズする
自分で見た目を変えたい人もいると思うので説明する。

### 色を変える
`function _update_vcs_info_msg()` の中で `%F{green}` とかやってるので、それを変えればOK。

### 書式、表示する文言を変える
`zstyle ':vcs_info:git:*' formats '(%s)-[%b]' '%c%u %m'`
`zstyle ':vcs_info:git:*' actionformats '(%s)-[%b]' '%c%u %m' '<!%a>'`
というところで書式を指定してるので好きなように変えよう。

こんなふうに `formats` の後ろに文字列を2つ渡しておけば、`vcs_info` を実行すると1つ目が `$vcs_info_msg_0_` 2つ目が `$vcs_info_msg_1_` に代入されるようになる。
`actionformats` の方も同じで、今回は3つ指定してるので `$vcs_info_msg_0_` から `$vcs_info_msg_2_` までに代入される。

`':vcs_info:git:*'` は Git 専用の設定で、`':vcs_info:*'` はそれ以外のデフォルト設定という感じになるので、 `':vcs_info:hg:*'` とかやって「hg はもっと別のを出したい」とかもできる。

書式で使う主な「%なんとか」の意味は以下のとおり。

| フォーマット| 内容 | set-message hooks 内で参照する変数名(後述) |
|:---|:---------------------------------------------------|:----------------------|
| %s | バージョン管理システム名(git, svn, hg, ...)        | $hook\_com[vcs]       |
| %b | ブランチ情報                                       | $hook\_com[branch]    |
| %i | リビジョン番号またはリビジョンID                   | $hook\_com[revision]  |
| %r | リポジトリ名                                       | $hook\_com[base-name] |
| %R | リポジトリのルートディレクトリのパス               | $hook\_com[base]      |
| %S | リポジトリルートから見た今のディレクトリの相対パス | $hook\_com[subdir]    |
| %a | アクション名(mergeなど) actionformats のみで指定可 | $hook\_com[action]    |
| %c | stagedstr 文字列                                   | $hook\_com[staged]    |
| %u | unstagedstr 文字列                                 | $hook\_com[unstaged]  |
| %m | その他の情報                                       | $hook\_com[misc]      |

詳細は man zshcontrib(1) を参照。


### 表示する情報を追加
stash の件数とかを表示するところは vcs\_info の Hooks という機能を使って実装している。これを使うと元の vcs\_info の中に自分で処理を追加することができる。

上で書いたの以外にも自分で独自の処理を追加したい人のために方法を紹介する。

ちなみに、ここまでけっこう書いたけど、ここから先も普通のエントリ1つ分ぐらいの量があるので注意。

今回は「現在のブランチにまだマージしていないコミットが master 上にあるかどうか」を表示することにしよう。もしマージ済でないなら misc (%m) に (R) と表示することにする。
もしこれが出ていたら、master にマージするときは fast forward ではマージできない、という意味になる。

### hook 関数登録

hook 関数の登録は `zstyle ':vcs_info:git+set-message:*' hooks ...` でやってるので、こんな感じで追加すればOK。

```sh
zstyle ':vcs_info:git+set-message:*' hooks \
                                        git-hook-begin \
                                        git-untracked \
                                        git-push-status \
                                        git-nomerge-branch \
                                        git-nomerge-master \  # <= 追加
                                        git-stash-count
```

`git-nomerge-master` という名前で Hook を追加することにした。これで「git の set-message hook に対して関数を追加する」という意味になる。

「set-message」というのは、`vcs_info_msg_N_` 変数の値を設定する直前の処理のこと。
ここに登録した関数はメッセージを作成する回数分呼ばれる。
今回は `zstyle ':vcs_info:git:*' formats '(%s)-[%b]' '%c%u %m'` と指定してるので、'(%s)-[%b]'、 '%c%u %m' と2つメッセージが作成される。なのでこの hook 関数も全部2回ずつ呼びだされる。
「set-message」以外にもいっぱい hook はあるんだけど、とりあえず「set-message」だけでたいてい足りると思う。

関数本体はこんな感じで書く。

```sh
function +vi-git-nomerge-master() {
    # この中に処理を書く
}
```

さっき名前を `git-nomerge-master` として登録したけど、関数名としては頭に `+vi-` を付けるってとこに注意。

### hook 関数の中身を書く

この関数の中身はこんな感じに書く。

```sh
# とりあえず実装
function +vi-git-nomerge-master() {
    # master ブランチにいる時はなにもしない
    if [[ "${hook_com[branch]}" == "master" ]]; then
        return 0
    fi

    # 現在のブランチにまだマージしていないブランチ一覧を取得する。
    # その中に master が含まれていた場合、 master は現在のブランチにマージ済みでないとみなす
    if command git branch --no-merged 2>/dev/null | command grep 'master' > /dev/null 2>&1 ; then
        hook_com[misc]+="(R)"
    fi
}
```

今回は「master ブランチは今のブランチにマージ済みか」を表示したいので、今 master にいるときは何も表示するものがない(master から master にマージするものは無い)。 なので何もせずに終了してる。
このときに 0 以外を返すとこれ以降の hook 関数が実行されなくなってしまうので、0 を返すようにする。

その次がメインの処理。普通にシェルスクリプトとして処理してる。
`hook_com[misc]` 変数に代入すれば misc の値が上書きされて、結果としてそれが %m に表示される。
書式がどの変数名で参照できるかは上の表にまとめておいた。
もし他の関数でも misc を設定していたらそれはそのまま残しておきたいので、 += で後ろに文字列を連結してる。

基本的にこれでいいんだけど1つハメがあって、これをそのまま実行すると (R)(R) みたいに2つ出てしまう。さっきhook 関数はメッセージを作成する回数分呼び出されるって書いたけど、それが原因。
+= で misc の後ろに追加していて、今回の場合は2回呼び出されるので (R) が2つ出てくる。

これを避けるのはこんな感じ。

```sh
# 完成版
function +vi-git-nomerge-master() {
    # vcs_info_msg_1_ を設定する場合のみ処理の対象とする
    if [[ "$1" != "1" ]]; then
        return 0
    fi

    if [[ "${hook_com[branch]}" == "master" ]]; then
        return 0
    fi

    if command git branch --no-merged 2>/dev/null | command grep 'master' > /dev/null 2>&1 ; then
        hook_com[misc]+="(R)"
    fi
}
```

set-message の hook 関数には引数が2つある。1つめは何番目のメッセージかを表す数値で、0 から始まって1, 2, と増えていく。2つ目は formats, actionformats で指定した書式文字列。今回の場合は '(%s)-[%b]' とか '%c%u %m' とかが渡ってくる。

%m は2つ目のメッセージにあるので、第1引数を使って判定して2番目のときだけ処理するようにした。こうやっておけばさっきの2重になるやつは解決する。
第2引数は必要ないので特に使ってない。

これで「今のブランチにまだマージしていないコミットが master 上にあるかどうか」が表示できるようになった。rebase する派の人は、(R) が出たときは rebase が必要って分かるようになる。

こんな感じでどんどん自分で処理を追加できるので、元々無い機能でも好きなようにカスタマイズできて便利。色々試してみると良いと思う。


## 参考ドキュメント
もっと色々やってみたい人はこういうのを読めばOK。

- [man zshcontrib(1)](http://zsh.sourceforge.net/Doc/Release/User-Contributions.html#Version-Control-Information)
基本的に今回言ったことは全部 man に書いてある。
- [Misc/vcs_info-examples](http://zsh.git.sourceforge.net/git/gitweb.cgi?p=zsh/zsh;a=blob_plain;f=Misc/vcs_info-examples;hb=HEAD)
ソースコードに添付されてる vcs\_info の例。今回紹介した Hooks の例も載ってる。