【検証】Git 2.56 の git branch --delete-merged、いつもの push -u 運用では1本も消えなかった
投稿 最終更新
環境(先に書く)
この記事の実測は、次の環境でだけ確認した。
- OS: Linux(Debian 系)
- Git: 2.56.0。kernel.org の公式 tarball(
git-2.56.0.tar.xz)からビルドした。sha256 はミラーのsha256sums.ascと一致 - リポジトリ: ローカルの bare リポジトリを
originにして、作業用クローンworkとマージ担当のクローンmaintainerの2つを用意 - 設定:
HOMEを隔離し、GIT_CONFIG_NOSYSTEM=1。ユーザー設定もシステム設定も読んでいない。push.defaultも未設定(既定値のまま)
GitHub などのホスティング側の挙動は見ていない。ローカルの Git の判定だけを測った記事だ。
触れ込み
GitHub Blog の「Highlights from Git 2.56」では、こう紹介されている。
$ git branch --delete-merged 'origin/*' 'topic-*' --dry-run
マージ済みのローカル枝を、上流と比べてまとめて消す。--dry-run を付ければ一覧を出すだけ。聞くかぎり、git branch --merged main | xargs git branch -d の置き換えに見える。
実際に8通りの枝を作って試した。
同梱ドキュメントに書いてあること
Documentation/git-branch.adoc の要点はこうだ(原文は英語)。
- 対象は、設定された上流が
<pattern>に一致するローカル枝。そのうち、枝の先端が上流から到達可能なものだけを消す <pattern>には ref 名、リモート名(そのHEADが指す枝)、シェル風のグロブを書ける- 後ろに
<branch-pattern>を書くと、候補のローカル枝を絞れる
次の場合は消さない。
- 設定された上流の ref がもう存在しない
- どこかの worktree でチェックアウトされている
branch.<name>.remoteへ push すると上流そのものを更新してしまう(push と fetch の refspec で判定)- 消さずに残る枝の、ローカル上流になっている
branch.<name>.deleteMergedがfalse
未マージの枝は「黙ってスキップ」するとも書いてある。
3つめの条件は、読んだだけでは効き方が分からなかった。実測したら、これが一番効いた。
用意した枝
すべて main から生やし、1コミットずつ積んで push した。マージは別クローンで行い、main を push している。
| 記号 | 枝 | やったこと |
|---|---|---|
| A | topic-merge | --no-ff でマージ。リモート枝は残す |
| B | topic-ff | fast-forward でマージ。リモート枝は残す |
| C | topic-squash-keep | squash マージ。リモート枝は残す |
| D | topic-squash-del | squash マージ後にリモート枝を削除し、作業側で git fetch --prune |
| E | topic-unpushed-work | push した分はマージ済み。その後、未 push のコミットを1つ追加 |
| F | topic-merged-then-del | 通常マージ後にリモート枝を削除し、fetch --prune |
| G | topic-keep-config | 通常マージ。branch.topic-keep-config.deleteMerged=false を設定 |
| H | topic-worktree | 通常マージ。別 worktree でチェックアウト中 |
| I | topic-never-merged | 対照用。push しただけで、マージしない |
これを、上流の張り方だけ変えて2通り作った。
- パターン1: いつもの
git push -u origin topic-X。上流はorigin/topic-Xになる - パターン2:
git switch -c topic-X --track origin/mainで作り、git push origin topic-X(-uなし)。上流はorigin/mainのまま
パターン1:push -u 運用では、1本も消えない
作業側の状態はこうだ。
$ git branch -vv
* main ec88ac9 [origin/main] Merge topic-worktree
topic-ff f75151d [origin/topic-ff] topic-ff work
topic-keep-config 5174cb5 [origin/topic-keep-config] topic-keep-config work
topic-merge 73b2701 [origin/topic-merge] topic-merge work
topic-merged-then-del 535e5f8 [origin/topic-merged-then-del: gone] topic-merged-then-del work
topic-never-merged 5d9018a [origin/topic-never-merged] topic-never-merged work
topic-squash-del ced93ef [origin/topic-squash-del: gone] topic-squash-del work
topic-squash-keep 2d3d3a4 [origin/topic-squash-keep] topic-squash-keep work
topic-unpushed-work 3abeb8f [origin/topic-unpushed-work: ahead 1] unpushed local commit
+ topic-worktree 0663fa9 (../wt-topic-worktree) [origin/topic-worktree] topic-worktree work
従来の --merged は、素直に拾う。
$ git branch --merged main
* main
topic-ff
topic-keep-config
topic-merge
topic-merged-then-del
+ topic-worktree
--delete-merged は、何を渡しても無言だった。
$ git branch --dry-run --delete-merged 'origin/*'
$ git branch --dry-run --delete-merged 'origin/*' 'topic-*'
$ git branch --dry-run --delete-merged origin
$ git branch --dry-run --delete-merged '*/*'
$ git branch --delete-merged 'origin/*'
--dry-run を外した本番も、終了コードは 0 で、何も消えていない。確かにマージ済みの A と B も残った。
理由:自分の push 先を上流にしている枝は、判定のしようがない
これは不具合ではなく、先の3つめの条件だ。builtin/branch.c の branch_pushes_to_upstream() を読むと、判定はこうなっている。
remote.origin.pushがあれば、それで push 先を求める。無ければ同名(refs/heads/topic-X)- push 先を fetch refspec に通すと
refs/remotes/origin/topic-Xになる - それが上流と同じなら、スキップ
push -u した枝は、上流が「自分を push したもの」だ。push した直後なら、先端は当然上流から到達できる。マージされたのか、push しただけなのか、区別がつかない。だから最初から候補にしない。
ちなみに push.default は判定に使っていない。push.default=nothing にしても、結果は同じ無言だった。
$ git -c push.default=nothing branch --dry-run --delete-merged 'origin/*'
ガードを外すと、何が起きるか
push refspec を別の名前へ向けると、このガードは外れる。試しに一時的に付けて dry-run した。
$ git -c remote.origin.push='refs/heads/*:refs/heads/wip/*' branch --dry-run --delete-merged 'origin/*'
Skipping 'topic-keep-config' (branch.topic-keep-config.deleteMerged is false)
Would delete branch topic-ff (was f75151d).
Would delete branch topic-merge (was 73b2701).
Would delete branch topic-never-merged (was 5d9018a).
Would delete branch topic-squash-keep (was 2d3d3a4).
一度もマージしていない topic-never-merged が、削除候補に入った。ガードが防いでいるのは、まさにこれだ。
パターン2:origin/main を追跡している枝なら、効く
同じ9本を、上流 origin/main で作り直した。
$ git branch --dry-run --delete-merged 'origin/*'
Skipping 'topic-keep-config' (branch.topic-keep-config.deleteMerged is false)
Would delete branch topic-ff (was f75151d).
Would delete branch topic-merge (was 73b2701).
Would delete branch topic-merged-then-del (was 535e5f8).
'origin/*' 'topic-*'、origin、origin/main、'*/*' も、同じ一覧になった。'topic-*' を上流側のパターンに渡すと、何も一致しない(上流を絞る引数なので当然だ)。
本番を打つと、dry-run の一覧どおりに消えた。
$ git branch --delete-merged 'origin/*'
Skipping 'topic-keep-config' (branch.topic-keep-config.deleteMerged is false)
Deleted branch topic-ff (was f75151d).
Deleted branch topic-merge (was 73b2701).
Deleted branch topic-merged-then-del (was 535e5f8).
後ろに 'topic-m*' を付けた回は、topic-merge と topic-merged-then-del だけが消えた。消えた枝の branch.<name>.* 設定も一緒に消えていた。
ただし、この運用には代償がある。上流と名前が違うので、既定の push.default=simple のもとでは引数なしの git push が拒否される。
$ git push --dry-run
fatal: The upstream branch of your current branch does not match
the name of your current branch. To push to the upstream branch
on the remote, use
git push origin HEAD:main
(以下、案内文が続く。全文は検証ログにある。)
結果の一覧
| 記号 | 枝 | パターン1(上流=同名) | パターン2(上流=origin/main) | --merged main |
|---|---|---|---|---|
| A | topic-merge | 残る | 消える | 出る |
| B | topic-ff | 残る | 消える | 出る |
| C | topic-squash-keep | 残る | 残る | 出ない |
| D | topic-squash-del | 残る | 残る | 出ない |
| E | topic-unpushed-work | 残る | 残る | 出ない |
| F | topic-merged-then-del | 残る | 消える | 出る |
| G | topic-keep-config | 残る | 残る(Skipping と表示) | 出る |
| H | topic-worktree | 残る | 残る | 出る |
| I | topic-never-merged | 残る | 残る | 出ない |
引っかかった点
squash マージは拾わない。C も D も、パターン2でさえ残った。main にある squash コミットは枝の先端を含まないので、到達可能の判定に乗らない。これは --merged main も同じだ。GitHub の「Squash and merge」で運用しているリポジトリでは、この機能で枝は減らない。
上流が消えた枝も拾わない。F は本当にマージ済みだが、パターン1では fetch --prune で上流が gone になった時点で対象から外れる。ドキュメントの「上流の ref が存在しない」だ。GitHub の「マージ後に枝を自動削除」と組み合わせると、まさにこの形になる。
スキップ理由は、ほぼ表示されない。表示されたのは deleteMerged=false の1行だけだった。worktree 使用中、上流消滅、push ガード、未マージは、どれも無言で飛ばす。パターン1のように dry-run が空だと、なぜ空なのかは自分で調べるしかない。
まとめ
git branch --delete-merged は「main にマージ済みか」ではなく、「その枝が追跡している上流に取り込まれたか」を見る。
git push -u origin topicの運用では、ガードにより1本も消えない- 効くのは、ローカル枝が
origin/mainなどの統合先を直接追跡している運用(Git 本体のテストt/t3200-branch.shもこの形) - squash マージの枝と、上流が消えた枝は、どちらの運用でも残る
手元がいつもの push -u 運用なら、当面は git branch --merged main を見て git branch -d で消すほうが早い。数字と挙動は Git 2.56.0 のローカル実測で、ホスティングの設定や将来の版では変わり得る。
出典
- GitHub Blog — Highlights from Git 2.56 — https://github.blog/open-source/git/highlights-from-git-2-56/
- Git 2.56.0 ソース同梱
Documentation/git-branch.adoc、Documentation/config/branch.adoc、Documentation/RelNotes/2.56.0.adoc - Git 2.56.0 ソース
builtin/branch.c(branch_pushes_to_upstream()、delete_merged_branches())、t/t3200-branch.sh - 実測: 令和8年9月29日(JST)。再現スクリプトと全ログを手元に保存(
repro.shを2回実行し、ログが完全一致することを確認)