Gitのマージ済みブランチを一括削除する方法【2.56対応】

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

Gitのマージ済みブランチを一括削除する方法【2.56対応】

本記事はプロモーションを含みます。

はじめに:ローカルブランチが溜まり続ける

--delete-merged は main にマージされたかではなく、upstream から届くかで判定する

PR がマージされるたびに、手元には用済みのブランチが 1 本ずつ残る。git branch の一覧が画面に収まらなくなり、どれを消してよいのか分からない状態になりやすい。

結論から書くと、Git 2.56 以降なら次の 2 行で片付く。

git branch --dry-run --delete-merged origin   # 消えるブランチを確かめる
git branch --delete-merged origin             # 消す

ただし、この機能は 「main にマージされたか」ではなく「そのブランチの upstream (追跡先) から届くか」 で判断する。ブランチの作り方によっては 1 本も消えないので、その条件を先に押さえておく。

Git 2.56 で何が増えたか

<pattern> には ref、リモート名、ワイルドカードを書け、リモート名は origin/HEAD の指すブランチになる

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 にマージされたブランチ」を拾えないことを、新機能の動機として挙げている。

実際の使い方

前提:ブランチの upstream を origin/main にしておく

origin/main から辿ると behind だけの feature-a には届き、ahead のある feature-b のコミットには届かない

--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

消す前に dry-run で確かめる

origin/main から届く feature-a だけが消え、未マージのコミットを持つ feature-b は残る
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 での確認を勧めている。

消えないブランチ

push -u で upstream が origin/feature-a に変わると、--delete-merged origin の対象から外れる

公式ドキュメントには、次のブランチは消さないと書かれている。

  • upstream の ref がもう無い
  • どこかの worktree でチェックアウトしている
  • リモートへ push すると upstream が更新される (pull した直後の「マージ済みに見えるだけ」のブランチと区別できないため)
  • 消さない別のブランチの upstream になっている
  • branch.<name>.deleteMerged が false
  • upstream にまだ入っていないコミットがある (警告なしに飛ばす)

3 つ目の条件から読むと、git push -u origin feature-a で 同じ名前のリモートブランチ (origin/feature-a) を upstream にしたブランチは対象外 になる。GitHub で PR を出すときによくある形なので、この運用だと 1 本も消えないはずだ (2.56 での実行は未検証)。その場合は後で書く --merged の方法を使う。

マージ後も続きを作業するブランチは、設定で除外できる。

git config branch.feature-b.deleteMerged false

スカッシュマージしたブランチは消えない

スカッシュマージは main に新しいコミットを作るので、main からブランチの先端に届かず未マージ扱いになる

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 で消すことになる。

origin の指す先が古いまま、という落とし穴

origin/HEAD は clone 時の既定ブランチのままなので、set-head --auto でリモートの既定ブランチに揃える

--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 で直接書けば、この問題は起きない。fetch のたびに追従させる設定と、スクリプトから既定ブランチや派生元を判定するときの落とし穴は Gitの既定ブランチと派生元をスクリプトで判定する落とし穴 にまとめた。

Git 2.55 以前:–merged と xargs で消す

grep -v main は名前に main を含む fix-main-page まで残し、grep -vx main なら main だけを外す

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つの場面と対処法 にまとめている。

時短になる使い方

掃除の前に、チェックアウトせずに main を最新にする

feature をチェックアウトしたまま、git fetch origin main:main で main だけを origin/main まで進める

--merged main はローカルの main を基準にするので、main が古いとマージ済みのブランチを拾えない。作業中のブランチにいたまま、main だけを fast-forward できる。

git fetch origin main:main

main とリモートが分岐していれば non-fast-forward で拒否されるので、ローカルだけのコミットが消えることはない。今チェックアウトしているブランチには使えない (拒否される) 点に注意する。

alias にして週 1 回回す

毎回打つのは手間なので、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 に積んだコミットを逃がす

コミットを feature に残してから、reset --keep で main だけを origin/main に戻す

掃除のついでによく起きるのが、ブランチを切り忘れて main に直接コミットしてしまう事故だ。git reset --keep なら、関係の無いローカル変更を残したまま main だけを戻せる。

git branch feature main          # 先にコミットを新しいブランチに残す
git reset --keep origin/main     # main を origin/main に戻す
git switch feature

--keep は、戻すと消えてしまう変更があるときは何も変えずに中止する。--hard より安全だ。

ブランチを消しすぎた、消したブランチがリモートに戻ってきた、といった事故の戻し方は Gitの事故から復旧する手順 にまとめた。

チームで使うときは

  • 手順書に載せる: 「PR がマージされたら git sweep」と決めておくと、新しく入った人が古いブランチを起点に作業を始める事故を減らせる。スカッシュマージかどうかで使えるコマンドが変わるので、リポジトリのマージ方式と一緒に書いておくのがよい
  • 判定の考え方を共有する: 今回の「upstream から届くか」「reachable か」という判定は、--merged、-d、reset --keep のどれにも共通する考え方だ。ここを押さえておくと、新しいオプションが出たときも挙動を読み違えない

ブランチとコミットの関係を一度体系的に押さえておくなら、コマンドの意味を手を動かしながら確かめられる本が手元にあると便利だ。

独習Git (リック・ウマリ 著、吉川邦夫 訳) 画像: 楽天市場

独習Git (リック・ウマリ 著、吉川邦夫 訳)

参考