マージ済みのローカルブランチが溜まっていく悩みを、Git 2.56の--delete-mergedで片付ける方法。消える条件と消えない条件、2.55以前のgrep + xargsの落とし穴、掃除の前後で事故を防ぐコマンドまで解説します。

本記事はプロモーションを含みます。
PR がマージされるたびに、手元には用済みのブランチが 1 本ずつ残ります。git branch の一覧が画面に収まらなくなり、どれを消してよいのか分からない。そんな状態になっていませんか?
結論から書くと、Git 2.56 以降なら次の 2 行で片付きます。
git branch --dry-run --delete-merged origin # 消えるブランチを確かめる
git branch --delete-merged origin # 消す
ただし、この機能は 「main にマージされたか」ではなく「そのブランチの upstream (追跡先) から届くか」 で判断します。ブランチの作り方によっては 1 本も消えないので、その条件を先に押さえておきます。
2.56 のリリースノートには、git branch に --delete-merged が加わったと書かれています。あわせて、upstream で絞り込んで一覧を出す --forked も入りました。
| オプション | 何をするか |
|---|---|
--forked <pattern> | upstream が <pattern> に一致するブランチを一覧する |
--delete-merged <pattern> | upstream が <pattern> に一致し、その upstream から先端のコミットが届くブランチを消す |
--dry-run | --delete-merged と組み合わせ、消す予定のブランチを表示するだけで何も消さない |
<pattern> には origin/main のような ref、origin のようなリモート名、'origin/*' のようなワイルドカードを書けます。リモート名を書くと、origin/HEAD が指すブランチ (普通は origin/main) として扱われます。
これまでは git branch --merged main の結果を grep と xargs で git branch -d に流すのが定番でした。GitLab の解説記事は、この方法では「main から切って origin/main にマージされたブランチ」を拾えないことを、新機能の動機として挙げています。
--delete-merged が働くのは、ブランチが 統合先のブランチ (origin/main など) を追跡している ときです。リモート追跡ブランチを起点にブランチを作ると、upstream が自動でそこに設定されます。
git switch --create feature-a origin/main
# => branch 'feature-a' set up to track 'origin/main'.
git branch -vv で、各ブランチの upstream と進み具合を確かめられます (出力例は GitLab の解説記事から)。
feature-a b3ebf58 [origin/main: behind 1] Add feature A
feature-b 34c0403 [origin/main: ahead 1, behind 1] Add feature B
* main 230a3b0 [origin/main] Document the parser
behind だけのブランチは、自分のコミットがすべて origin/main に入っている (マージ済み) という意味です。ahead が付いているブランチは、まだ入っていないコミットを持っています。
いつもローカルの main からブランチを切っているなら、branch.autoSetupMerge を inherit にすると、起点のブランチの upstream (main なら origin/main) が新しいブランチにも引き継がれます。
git config --global branch.autoSetupMerge inherit
git fetch origin # origin/main を最新にする
git branch --forked origin # origin/main を追跡しているブランチの一覧
git branch --dry-run --delete-merged origin # 消える予定のブランチ
git branch --delete-merged origin # 実行
特に 'origin/*' のような広いパターンを使うときは、公式ドキュメントも dry-run での確認を勧めています。
公式ドキュメントには、次のブランチは消さないと書かれています。
branch.<name>.deleteMerged が false3 つ目の条件から読むと、git push -u origin feature-a で 同じ名前のリモートブランチ (origin/feature-a) を upstream にしたブランチは対象外 になります。GitHub で PR を出すときによくある形なので、この運用だと 1 本も消えないはずです (2.56 での実行は未検証)。その場合は後で書く --merged の方法を使います。
マージ後も続きを作業するブランチは、設定で除外できます。
git config branch.feature-b.deleteMerged false
GitHub の「Squash and merge」は、ブランチのコミットをまとめた 新しいコミット を main に作ります。ブランチの先端のコミット自体は main から届かないので、--delete-merged でも git branch --merged でも「未マージ」と判断されます。git branch -d も error: the branch '...' is not fully merged で止まります。
スカッシュマージの運用では、PR がマージされたことを GitHub 上で確かめてから git branch -D で消すことになります。
--delete-merged origin の origin は origin/HEAD が指すブランチです。この origin/HEAD は clone した時点の既定ブランチを指したままで、リモート側で既定ブランチを変えても git fetch では追従しません (remote.origin.followRemoteHEAD の既定が「無ければ作る」だけのため)。
既定ブランチを master から main に変えたリポジトリなどでは、古い名前を基準に判定することになります。リモート名で指定する前に確かめておきます。
git symbolic-ref refs/remotes/origin/HEAD # 今の値
git remote set-head origin --auto # リモートの既定ブランチに揃える
迷うなら origin/main のように ref で直接書けば、この問題は起きません。
2.56 より前の Git や、upstream を統合先にしていない運用では、従来の方法を使います。よく見かける形はこれです。
git branch --merged main | grep -v main | xargs git branch -d
この形には穴があります。grep -v main は「main を含む行」を除くので、fix-main-page のように 名前に main を含むブランチまで一覧から外れ、消えずに残ります。手元の Git 2.55.0 で、feat-a と fix-main-page を main にマージしてから実行すると、消えたのは feat-a だけでした。
名前だけを取り出して、完全一致で除きます。
git branch --merged main --format='%(refname:short)' | grep -vx main | xargs -r git branch -d
--format='%(refname:short)' で、* や行頭の空白が付かない名前だけにするgrep -vx で、行全体が main のときだけ除くxargs -r で、消すものが無いときに git branch -d を空で呼ばない同じ条件で試すと、fix-main-page も消えました。-d (小文字) は未マージのブランチを拒否するので、一覧が多少ずれても未マージの作業は消えません。-D にはしないでください。
スクリプトに組み込むなら、|| true や if の中で失敗が見えなくならないように書きます。set -e が効かない場面は bashのset -eが効かない5つの場面と対処法 にまとめています。
--merged main はローカルの main を基準にするので、main が古いとマージ済みのブランチを拾えません。作業中のブランチにいたまま、main だけを fast-forward できます。
git fetch origin main:main
main とリモートが分岐していれば non-fast-forward で拒否されるので、ローカルだけのコミットが消えることはありません。今チェックアウトしているブランチには使えない (拒否される) 点に注意します。
毎回打つのは手間なので、alias にしておきます。
# Git 2.56 以降
git config --global alias.sweep '!git fetch --prune origin && git branch --delete-merged origin'
# 2.55 以前
git config --global alias.sweep '!git fetch origin main:main && git branch --merged main --format="%(refname:short)" | grep -vx main | xargs -r git branch -d'
git sweep の 1 回で、一覧から 1 本ずつマージ済みかを確かめて -d していた作業が無くなります。一覧が短くなると、git switch の補完や git branch を目で追う時間も減ります。
掃除のついでによく起きるのが、ブランチを切り忘れて main に直接コミットしてしまう事故です。git reset --keep なら、関係の無いローカル変更を残したまま main だけを戻せます。
git branch feature main # 先にコミットを新しいブランチに残す
git reset --keep origin/main # main を origin/main に戻す
git switch feature
--keep は、戻すと消えてしまう変更があるときは何も変えずに中止します。--hard より安全です。
git sweep」と決めておくと、新しく入った人が古いブランチを起点に作業を始める事故を減らせます。スカッシュマージかどうかで使えるコマンドが変わるので、リポジトリのマージ方式と一緒に書いておくのがおすすめです--merged、-d、reset --keep のどれにも共通する考え方です。ブランチとコミットの関係を一度体系的に押さえておくと、新しいオプションが出たときも挙動を読み違えませんブランチの設計やチーム開発での運用まで含めて Git を一通り学ぶなら、コマンドの意味を手を動かしながら確かめられる本が手元にあると便利です。
独習Git (リック・ウマリ 著、吉川邦夫 訳)
--delete-merged / --forked / --dry-run)--keep)