Zsh
Git
zshDay 11

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

More than 3 years have passed since last update.

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。

# 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


表示の例

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


普通の状態

普通の状態


ステージしていない修正がある状態

ステージしていない修正がある状態


ステージした修正がある状態

ステージした修正がある状態


untracked なファイルがある状態

untracked なファイルがある状態


リモートに pushしていないコミットが1件ある状態

pushしていないコミットが1件ある状態


stash が1件ある状態

stash が1件ある状態


いろいろいっぱいある状態

いろいろいっぱいある状態


fix-bug ブランチから master にマージしていないコミットが1件ある状態

master にマージしていないコミットが1件ある状態


マージでコンフリクトした状態

マージでコンフリクトした状態

いい感じ。


カスタマイズする

自分で見た目を変えたい人もいると思うので説明する。


色を変える

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。

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」だけでたいてい足りると思う。

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

function +vi-git-nomerge-master() {

# この中に処理を書く
}

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


hook 関数の中身を書く

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

# とりあえず実装

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つ出てくる。

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

# 完成版

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)
    基本的に今回言ったことは全部 man に書いてある。


  • Misc/vcs_info-examples
    ソースコードに添付されてる vcs_info の例。今回紹介した Hooks の例も載ってる。