ZshでプロンプトにGitリポジトリの情報を表示する

commits7 min read読了の目安(約7000字 4

概要

ZshでGitリポジトリの状態を表示するZshスクリプトを作ります。具体的には

  • ブランチ名の表示
  • 状態をブランチ名の色で表現
  • 上流ブランチの有無と状態を色で表現

を実現します。ただ上記を実現したいだけの人は、vcs_infoかgit-promptを使った方が早いと思います。

プロンプトをいじる

GitやSubversionのようなリポジトリを使っていると、そのリポジトリの状態を常に確認したくなります。Gitなら、ブランチ名や未コミット、未プッシュの状態をプロンプトに表示したいですね。他にも自分がrootかどうかをわかりやすくしたりなど、プロンプトをいろいろカスタマイズしている人は多いと思います。

で、当然のことながらそういうニーズのためにいろいろあるわけですが、せっかくなので一つ一つ機能を確認しながらそういうZshスクリプトを組んでみましょう。

最終的に、git-promptという関数を作り、それが現在のGitリポジトリの状態を表示するようにして、その値をPROMPTなりRPROMPTなり好きなところに放り込む形とします。

コードは以下に公開しています。様々な状態のリポジトリを作るMakeファイルがあるので、デバッグ目的にも使えるます。

https://github.com/kaityo256/zsh_vcs

関数を作る

まずは関数を作って実行しましょう。functionで作れます。以下のファイルを例えばzshrc_git.zshという名前で作ります。

function git-prompt{
  echo "main"
}

git-prompt

git-promptという関数を作り、それを呼び出しているだけです。実行してみます。

$ source zshrc_git.zsh
main

単にechoが実行されて、mainと表示されただけです。

色をつける

リポジトリの状態を色で表現したいので、色をつけてみましょう。そのためには、colorsというシェル関数を読み込みます。

autoload -U colors; colors
function git-prompt{
  echo "${fg[green]}main${reset_color}"
}

git-prompt

これを実行すると、緑色でmainと表示されたはずです。

最初のautoload -U colors; colorsは、シェル関数を読み込むイディオムです。autoload funcは、単にfunc読み込むだけで実行しません。なので、すぐに使いたい場合はautoload func;funcとします。autoload -X funcと、-Xオプションをつけるとすぐに実行してくれますが、このオプションでは、現在設定されているaliasを引き継ぎます。lscpなどはユーザ側でaliasが設定されていることが多く、それをシェル関数の中に引き継ぐと誤動作する可能性が高いです。そこで、通常はaliasを引き継がないで読み込むオプション-Uをつけて、autoload -U funcとします。-U-Xは同時に指定できないため、すぐに使いたければautoload -U func; funcとします。

colorsが実行されると、fg[色]が使えるようになります。例えば${fg[green]}は色を緑色にします。そのままだと他の表示も緑色のままになってしまうため、${reset_color}で色をもとに戻します。

カレントブランチ名を取得する

カレントブランチはgit symbolic-ref --short HEADで取れます。

$ git symbolic-ref --short HEAD
main

ちなみに--shortオプションは昔はありませんでした。このオプション無しだとこんな表示になります。

$ git symbolic-ref HEAD
refs/heads/main

ここからブランチ名を取得するにはsedとかが必要でしたが、今は--shortで取れるので不要になりました。

また、Gitリポジトリでは無い場合は以下のような表示が標準エラー出力に出ます。

$ git symbolic-ref --short HEAD
fatal: not a git repository (or any of the parent directories): .git

したがって、標準エラー出力を/dev/nullに飛ばして、標準出力になんか出たらそれがブランチ名、なにもなければGitリポジトリではない、ということになります。ここではHEADが取れていることは想定しないことにしましょう。

以上をまとめるとこんなスクリプトになります。

autoload -U colors; colors

function git-prompt {
  local branchname
  branchname=`git symbolic-ref --short HEAD 2> /dev/null`
  if [ -z $branchname ]; then
    return
  fi
  echo "${fg[green]}${branchname}${reset_color}"
}

git-prompt

ここで、if文の条件式の [ -z $branchname ]は、testコマンドの略記です。-zオプションは、続く文字列の長さが0の時に真となります。こんな感じです。

test -z "";echo $?      # => 0
test -z "hoge";echo $?  # => 1

なお、testは真の時に0を返すので注意が必要です。これで

if [ -z $branchname ]; then
  return
fi

は、$branchnameが空文字、つまりGitのリポジトリでなければ真となり、その時は何もせずにreturnしろ、という意味になります。

状態を調べる

ブランチ名の色で状態を表現します。とりあえず未コミット無し、Untracked filesも無しの状態で緑、未コミットなし、Untracked filesありで黄色、それ以外は赤にしましょう。

まず、git statusを実行して、nothing to commit, working tree cleanと表示されたら緑です。それ以外で、nothing added to commit but untracked files present (use "git add" to track)と表示されたら黄色、それ以外は赤ですね。以上を素直に実装するとこんな感じになるでしょう。

autoload -U colors; colors

function git-prompt {
  local branchname branch st
  branchname=`git symbolic-ref --short HEAD 2> /dev/null`
  if [ -z $branchname ]; then
    return
  fi
  st=`git status 2> /dev/null`
  if [[ -n `echo "$st" | grep "^nothing to"` ]]; then
    branch="${fg[green]}($branchname)$reset_color"
  elif [[ -n `echo "$st" | grep "^nothing added"` ]]; then
    branch="${fg[yellow]}($branchname)$reset_color"
  else
    branch="${fg[red]}($branchname)$reset_color"
  fi

  echo "$branch"
}

git-prompt

新たにbranchstという変数を導入しました。git stutusの実行結果をstに受け、nothing toという文字列を含んでいたら緑、nothing addedを含んでいたら黄色、それ以外は赤です。また、ブランチ名を丸括弧で囲みました。

リポジトリで試してみましょう。kaityo256/zsh_vcsリポジトリの中のsample_dirmakeすると、いくつかディレクトリができます[1]

git clone https://github.com/kaityo256/zsh_vcs.git 
cd zsh_vcs
cd sample_dir
make

それぞれ

  • 00 git initした直後(黄色になるはず)
  • 01 git init;git addした直後(赤色になるはず)
  • 02 git init;git add;git commitした直後(緑色になるはず)

です。実際に、

cd sample_dir
make
cd 00
source ../../zshrc_git.zsh 

とかやって、想定どおりの色でブランチ名が出てくるか確認すると良いでしょう。また、どこかのディレクトリで

git branch -m hoge
source ../../zshrc_git.zsh 

として、ちゃんとブランチ名が取れているか確認してみるのも良いかもしれません。

上流ブランチ

次に、上流ブランチの取得と、上流ブランチとの差分を調べましょう。上流ブランチの取得方法はいろいろありますが、git configで取るのが楽です。git config branch.ブランチ名.remoteで、上流ブランチが設定されていれば取れます。上流ブランチがなければ何も表示されません。

リモートブランチを表示しても良いのですが、僕はどうせoriginしか使わないので、上流ブランチがあればブランチ名の横に[up]を表示し、その色で未プッシュかどうかを判定しましょう。

上流ブランチがある場合、未プッシュのコミットがあるかどうかはgit log 上流ブランチ..現在のブランチで調べることができます。何も表示されなければ差分はありません。

以上をまとめると、こんな感じになるでしょう。

  remote=`git config branch.${branchname}.remote 2> /dev/null`

  if [ -z $remote ]; then
    pushed=''
  else
    upstream="${remote}/${branchname}"
    if [[ -z `git log ${upstream}..${branchname}` ]]; then
      pushed="${fg[green]}[up]$reset_color"
    else
      pushed="${fg[red]}[up]$reset_color"
    fi
  fi

  echo "$branch$pushed"

難しいことは何もありません。pushedという変数を作り、

  • 上流ブランチがなければ空文字列
  • 上流ブランチがあれば[up]
    • ローカルと差分がなければ緑色
    • ローカルと差分があれば赤色

にしているだけです。またsample_dirでチェックしましょう。たとえば03なら

cd 03
source ../../zshrc_git.zsh 

とすると、緑色で(main)[up]と表示されるはずです。

それぞれのディレクトリの状態は、

  • 03 リモートリポジトリを設定し、未プッシュなし (緑色で[up]が表示されるはず)
  • 04 リモートリポジトリを設定し、未プッシュなし (赤色で[up]が表示されるはず)
  • 05 リモートリポジトリを設定し、未コミットあり、未プッシュなし (赤色でブランチ名が、緑で[up]が表示されるはず)

となっています。

RPROMPTに設定

さて、後は出てきた文字列をPROMPTなりRPROMPTなりに突っ込めばよいのですが、そのまま設定すると表示がおかしくなります。これは、色を変えているエスケープシーケンスの文字列を、文字としてカウントしてしまっているためです。これを防ぐには、色を変えるところを%{%}で囲む必要があります。以上の修正をしたものが以下です。

autoload -U colors; colors

function git-prompt {
  local branchname branch st remote pushed upstream
  branchname=`git symbolic-ref --short HEAD 2> /dev/null`
  if [ -z $branchname ]; then
    return
  fi
  st=`git status 2> /dev/null`
  if [[ -n `echo "$st" | grep "^nothing to"` ]]; then
    branch="%{${fg[green]}%}($branchname)%{$reset_color%}"
  elif [[ -n `echo "$st" | grep "^nothing added"` ]]; then
    branch="%{${fg[yellow]}%}($branchname)%{$reset_color%}"
  else
    branch="%{${fg[red]}%}($branchname)%{$reset_color%}"
  fi

  remote=`git config branch.${branchname}.remote 2> /dev/null`

  if [ -z $remote ]; then
    pushed=''
  else
    upstream="${remote}/${branchname}"
    if [[ -z `git log ${upstream}..${branchname}` ]]; then
      pushed="%{${fg[green]}%}[up]%{$reset_color%}"
    else
      pushed="%{${fg[red]}%}[up]%{$reset_color%}"
    fi
  fi

  echo "$branch$pushed"
}

RPROMPT='`git-prompt`%{$fg_bold[cyan]%}[%~]%{$reset_color%}'
setopt prompt_subst

私の趣味でカレントディレクトリを右にシアンで表示していますが、これを左にするなり行をわけるなり、好きに設定すれば良いと思います。ちなみに最後のsetopt prompt_substは、プロンプトの中の変数を表示時に展開するものです。

まとめ

ZshでGitリポジトリの状態を表示するスクリプトを作ってみました。必要最低限の状態表示なら30行ちょっとで書けちゃうので、シェルスクリプトに慣れていない人は、この機会にいろいろ遊んで見ると面白いんじゃないでしょうか。

参考文献

脚注
  1. Gitリポジトリの中に別のリポジトリ作るのは良くないけど、まぁ許して。 ↩︎