mizdra's blog

ぽよぐらみんぐ

zsh + fzf で「あの時作業していたあのブランチ」を快適に探す

今まで id:mizdra はターミナルで Git ブランチを切り替えるときに、zsh + peco を使った Git ブランチ検索用のキーバインドを使用していた。

# .zshrc

function select-git-branch() {
  selected_branch=$(git branch | cut -c 3- | peco)
  BUFFER="${LBUFFER}${selected_branch}${RBUFFER}"
  CURSOR=$#LBUFFER+$#selected_branch
  zle redisplay
}
zle -N select-git-branch
bindkey '^b' select-git-branch

zsh + peco で Git ブランチを切り替える様子

便利っちゃ便利なのだけど...沢山のブランチの中から「あの時作業していたあのブランチ」を探すのが、どうも難しかった。ブランチの名前を覚えていれば良いけど、時々忘れてしまうこともある。もしそうなったら、以下のような手段でブランチを探すことになる。

  • ブランチ一覧から、それっぽいブランチを1つずつ git log して中身を見ていく
  • GitHub の Pull Request ページからブランチ名を探す (Pull Request が作成されている時限定)
  • git reflog で操作履歴を見て、それっぽいブランチを探す

最終的に目的のブランチは見つけられるが、どれも多少の手間が掛かる。そこで、もっと使い勝手の良い Git ブランチ検索機能が作れないのかなと思ったのだった。

どうやったら使い勝手が良くなるのか

まず第一に、ブランチを並べる順序を committer date の降順にするべきだと思う。経験上、最近作業したブランチほど切り替える確率が高いので、それが上にきて欲しい。ついでに committer date もブランチ名に並べて表示しておくと良いと思う。ブランチの名前は覚えていなくても作業した日付は案外覚えているもので (「大体3日前/1週間前/3ヶ月前に作業してたよな〜」とか)、ブランチを探すときのヒントになるはず。

あと自分のブランチなのか、他人のブランチなのかが人目で区別がつくと良いと思う。というのも、普段の作業では基本的に自分のブランチの中から探す傾向があり、一方レビューするときは他人のブランチの中から探す傾向がある。ブランチの author の区別がつけば、それがやりやすくなる。

ブランチ名、committer date、author さえあれば大抵は十分だろうが、それでも探したいブランチを特定できない時というのはあると思う。そうした時のために、ブランチの詳細を掘り下げて調べられるようにしたい。例えば commit message を出すとか。欲をいえば git log --oneline --graph --decorate 相当の情報が見れたら良い。

select-git-branch-friendly

という訳で欲しいやつを作った。

# .zshrc

# fzf のデフォルトのオプション。お好みで。ここでは peco っぽくなるよう調整してる。
export FZF_DEFAULT_OPTS="--reverse --no-sort --no-hscroll --preview-window=down"

user_name=$(git config user.name)
fmt="\
%(if:equals=$user_name)%(authorname)%(then)%(color:default)%(else)%(color:brightred)%(end)%(refname:short)|\
%(committerdate:relative)|\
%(subject)"
function select-git-branch-friendly() {
  selected_branch=$(
    git branch --sort=-committerdate --format=$fmt --color=always \
    | column -ts'|' \
    | fzf --ansi --exact --preview='git log --oneline --graph --decorate --color=always -50 {+1}' \
    | awk '{print $1}' \
  )
  BUFFER="${LBUFFER}${selected_branch}${RBUFFER}"
  CURSOR=$#LBUFFER+$#selected_branch
  zle redisplay
}
zle -N select-git-branch-friendly
bindkey '^b' select-git-branch-friendly

実際に動かしている様子がこれ。

select-git-branch-friendly を試している様子。

ブランチは committer date の降順で並べられていて、ブランチ名の横には committer date と commit message を表示している。本当は author も出したかったけど、あんまり出す項目が多すぎるとターミナルの横幅が小さい時に、表示内容の多くが見切れてしまうので断念した。代わりに、他の人が author のブランチは赤色にして区別できるようにしてる *1

また下部には git log --oneline --graph --decorate の出力を表示している。これで各々のブランチの詳細を掘り下げて調べられる。

実装解説

折角なので実装について解説する。といってもほとんど zsh + peco の時と同じで、違うところは主に以下の部分。

selected_branch=$(
  git branch --sort=-committerdate --format=$fmt --color=always \
  | column -ts'|' \
  | fzf --ansi --exact --preview='git log --oneline --graph --decorate --color=always -50 {+1}' \
  | awk '{print $1}' \
)

git branch --sort=-committerdate --format=$fmt --color=always

まず git branch --sort=-committerdate --format=$fmt --color=always でブランチの一覧を出力している。--sort=-committerdate で committerdate の降順に表示できる。各々のブランチをどう整形して出力するかは --format=$fmt で指定している。

user_name=$(git config user.name)
fmt="\
%(if:equals=$user_name)%(authorname)%(then)%(color:default)%(else)%(color:brightred)%(end)%(refname:short)|\
%(committerdate:relative)|\
%(subject)"

ちょっとややこしいけど、ブランチ名、現在時刻からの committer date の相対日時、commit message を | 区切りでくっつけてるだけ。| は後のフェーズで column コマンドで出力を整えるために使われる。

面白いのは authorname が自分以外の時に赤色にするのを、%(if:equals=$user_name)%(authorname)%(then)%...%(else)...%(end) という構文で実現しているところ。そんな構文あるの!!!って感じだけど Git の公式ドキュメントにもちゃんと書いてある。

あとパイプ使っている関係でそのままだと色が付かないので、--color=always をつけている。

column -ts'|'

| 区切りで垂直方向を揃えているだけ。

$ git branch --format=$fmt
main|4 weeks ago|Update dependency @mizdra/eslint-config-mizdra to v4
remove-workaround-for-source-map|4 weeks ago|remove workaround for source-map
tsserver-handle-css-modules|4 weeks ago|WIP

$ git branch --format=$fmt | column -ts'|'
main                              4 weeks ago  Update dependency @mizdra/eslint-config-mizdra to v4
remove-workaround-for-source-map  4 weeks ago  remove workaround for source-map
tsserver-handle-css-modules       4 weeks ago  WIP

ところで前のステップで %(authorname) が自分自身の時に %(color:default) を出力していたのには理由がある。もし %(color:default) がなくて、main が他の人のブランチだった場合、column -ts'|' は以下を出力する。

$ git branch --format=$fmt | column -ts'|'
main                          4 weeks ago  Update dependency @mizdra/eslint-config-mizdra to v4
remove-workaround-for-source-map  4 weeks ago  remove workaround for source-map
tsserver-handle-css-modules       4 weeks ago  WIP

main ブランチの行が 4 文字分左に寄ってしまっているのが分かると思う。これは %(color:brightred)ANSI escape sequence\e[91m を意味し、これが 4 文字としてカウントされてしまっているため *2

そして %(color:default) は ANSI escape sequence の \e[39m を意味する。そのため、これを含めておけば \e[91m\e[39m で文字数が揃って column -ts'|' の出力が整う。

なんだか不安になるハックだが...動けばオッケーということで。もっと賢い方法知ってる人がいたら教えて下さい。

fzf --ansi --exact --preview='git log --oneline --graph --decorate --color=always -50 {+1}'

見て分かる通り、peco の代わりに fzf を使っている。個人的には peco のほうが好きなのだけど、Preview window なる機能が使いたくて、仕方なく fzf にしてる。

--preview='git log --oneline --graph --decorate --color=always -50 {+1}' の部分がその Preview window というやつで、これで下部に git log を出してる。たったこれだけで格好良い UI 出せるの便利。git log-50 を渡しているのは、出力する commit 履歴を絞るため。これがないと全履歴を出そうとして、巨大リポジトリで大破滅する。まあ 50 件あったら十分だろうと思って -50 にしてる。

--ansi は前段からやってくる色付き (ANSI Escape sequence 込み) の入力を fzf が解釈できるようにするために必要。あと fuzzy search あんまり便利じゃないなと思ったので、--exact 付けてる。typo してもマッチする便利さよりも、厳密に絞り込めないことで検索結果が noizy になる不便さのほうが勝るなと感じたので。ここは好みによると思う。

awk '{print $1}'

ブランチ名、committer date、author が含まれる行からブランチ名を取り出してるだけ。これで $selected_branch に選択したブランチが代入される。

感想

個人的にはかなり使いやすいものができて満足してる。ブランチを探す時の傾向やパターンから、使い勝手の良い UI を設計するというのも面白かった。あと fzfgitcolumnawk だけでこれだけリッチな UI が作れるんだなという驚きもあった。

皆さんも使ってみてください。

追記 (2024-10-19 18:12)

今気づいたけど、fzf 公式のプラグインが似たようなキーバインドを実装しているようだった。

github.com

# 引用元: https://github.com/junegunn/fzf-git.sh/blob/6a5d4a923b86908abd9545c8646ae5dd44dff607/fzf-git.sh#L59-L62
# 見やすいように一部整形している。
$fmt='\
%(HEAD) %(color:yellow)%(refname:short) %(color:green)(%(committerdate:relative))\t\
%(color:blue)%(subject)%(color:reset)\
'
if [[ $# -eq 1 ]]; then
  branches() {
    git branch "$@" --sort=-committerdate --sort=-HEAD --format=$fmt --color=$(__fzf_git_color) | column -ts$'\t'
  }
  # ...
}

僕が作ったのと同じように、committer date でソートし、committer と commit message を出している。よくみると column コマンドも同じように使ってる。author で色分けするのはやってない模様。

Preview window で git log --oneline --graph を出すのも同じだった。けどこちらは多少 format が異なるようだった。

# 引用元: https://github.com/junegunn/fzf-git.sh/blob/6a5d4a923b86908abd9545c8646ae5dd44dff607/fzf-git.sh#L185-L200
# 見やすいように一部整形している。
_fzf_git_branches() {
  _fzf_git_check || return
  bash "$__fzf_git" branches |
  _fzf_git_fzf --ansi \
    # ...
    --preview "git log --oneline --graph --date=short --color=$(__fzf_git_color .) --pretty='format:%C(auto)%cd %h%d %s' \$(sed s/^..// <<< {} | cut -d' ' -f1) --" "$@" |
  sed 's/^..//' | cut -d' ' -f1
}

まあでも大体僕のやつと同じっぽい。まさか車輪の再発明をしていたとはね...。

*1:最初は自分のブランチの色も緑色に変えていたけど、fzf がクエリにマッチする文字のハイライトに緑色を使っていて、それと競合するのでやめた。そもそもあんまりカラフルにしてもゴチャゴチャした見た目になるので、色数は少なく保ったほうが良いはず。

*2:6文字じゃないのかと思うかもしれないが、\e は 2 文字で 1 つのエスケープ文字を表すので、5文字とみなすのが正しい。それでも1文字多くて不思議だけど、多分 column コマンドがエスケープ文字を無視して整形しているのではないか... なんかこれ column コマンドの実装によって挙動変わってきそうで怖いな...

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.