うならぼ

どうも。アフィリエイトとか広告とか解析とかは/about見てね。

Gitで不要なブランチを列挙する方法いろいろ

マージされたまま放置していたブランチとかその辺を掃除したかった。

不要なブランチを列挙できれば | xargs git branch -D でローカルブランチを削除したり、| xargs git push origin -d でリモートブランチを削除したりできるわけだ。

直接マージされたブランチ(=masterから辿れるコミット)

git branch にはちょうどマージ済みのものを抽出するオプションがある。

git branch --merged master

--format "%(refname:short)" をつければHEADであることを表す * を消せる。

upstreamが消滅しているブランチ

GitHubでPRを送ってマージされてその場でブランチを消した、とか。

そういったブランチは git branch -v だと [gone]git branch -vv だと [origin/hoge: gone] といった形で表示されるが、コミットメッセージにそういった文字列が含まれているかもわからない。

この [gone] は書式文字列では %(upstream:track) で表示できるので、自分で指定した方が安全になる。

git branch --format "%(refname:short) %(upstream:track)" | grep "\[gone\]" | cut -d" " -f1

ローカルでだけ消したブランチ

これを一括で消していいかどうかは運用によるが、ローカルでブランチ作ってPR投げておしまい、みたいなケースならいけると思う。

全てのブランチをpushしてよければ、--prune というオプションがある((まるごと同期というと --mirror なんてオプションもあるが、これはrefsをまるごとミラーする前提なので、リモートのrefをremotes以下に分離する普通のclone元に対して使うものではない。))。

git push --all --prune origin

削除だけしたい場合、そういったコマンドやオプションはなさそうなので、ローカルとリモートのブランチの差分を取ってみる。

comm -13 <(git branch | sed 's|*| |' | sort) <(git branch -r --list origin/* | sed -E 's#origin/|.+ -> .+##' | sort)

ローカルのブランチ一覧からは * 印を消し、リモートのブランチ一覧からは origin/ の部分を消してHEADを除外する。それ以外の symbolic-ref があることを考慮していないが、まあ大抵はないだろう…。

ところでリモートの HEAD が参照していたブランチを消してしまった場合、git remote set-head で変更することができる。手動でブランチを指定したり、-a でリモートの情報に合わせたりできるが、そもそもこれいるんだろうか。-d で消してちょっと様子を見てみよう。

squashマージされたブランチ

こいつが厄介。squashマージによるコミットはマージしたブランチのコミット群と履歴が連続していないので、履歴を辿ってもマージ済みかどうか判断することができない。

そこで見つけたのが、git cherry コマンドを使った方法。

not-an-aardvark/git-delete-squashed: Delete branches that have been squashed and merged into master

ちょっと短くしつつ、やっぱり長いのでここでは \ で行を分割して書く。

git branch --format="%(refname:short)" | \
while read branch; do
    mergeBase=$(git merge-base master $branch) && \
    [[ $(git cherry master $(git commit-tree $(git rev-parse $branch^{tree}) -p $mergeBase -m _)) == "-"* ]] && \
    echo $branch;
done

git cherry はブランチポイントからの各コミットについて、相手のブランチに既に取り込まれているかをファイルの中身で判定してくれるので、squash されていようと問題ない。ただコミット単位での判定になるため、例えば最終的に残らなかった変更のコミットについても評価されてしまうという git cherry-pick 同様の問題があり、これを防ぐために上の例では git commit-tree で squash コミットを作成している。結果が一行になるので判定が楽というのもある。

ただこれまでのパターンと比べるとやはり時間がかかるので、他の可能性を先に調べたり、判定除外を指定できるようにあれこれ足してシェルスクリプトにした。プレビューしてから再評価することなく本番を実行できるように、コピペ用のワンライナーも出力するようにした。

https://gist.github.com/unarist/2f6da154ddb30eb52993713e9d064a83