squash マージの後にブランチへ足したコミットは、既定ブランチに入らない
PR を出す。レビュー中に気づいて、同じブランチへ 1 コミット足す。
その間にマージされる。足した分だけが消える。
squash マージ運用のリポジトリで、2 日間に 4 回これを踏んだ。
同じ形の事故を数えると、半月で 7 件になった。
なぜ気づけないのか
squash はブランチのコミット群を 1 つに潰して既定ブランチへ載せる。
潰したあとのブランチ先端は既定ブランチの祖先にならない。
だから git merge-base --is-ancestor は「未マージ」と言うし、
PR の画面は MERGED と言う。どちらも内容が入ったことの証明になっていない。
実際に踏んだ形を並べると、壊れ方の幅が分かる。
| 形 | 何が起きたか |
|---|---|
| マージ直後の追補 | 撤回したはずの設計だけが残り、作り直した本命が丸ごと落ちた |
| push 自体が失敗 | リモート先端は 1 つ前のまま。ローカルでは完了して見えた |
| 積んだ PR | ベース側だけが入り、上に積んだ側は 1 文字も届かなかった |
| 空のマージコミット | MERGED 表示なのに変更ファイル 0 件。件名は別 PR のものだった |
| 自動コミット | 10 分ごとの自動バックアップが、マージ済みブランチへ積み続けた |
PR の上に PR を積まない
衝突を避けるつもりで、2 本目の PR のベースを 1 本目のブランチに向けたことがある。
いわゆる stacked PR だ。
結果はこうなった。2 本目はそのベースブランチへマージされ、
1 本目は既定ブランチへ squash で入った。
squash は 2 本目のコミットを祖先として持たないので、2 本目の内容はどこへも届かない。
それでいて両方 MERGED と表示される。
発覚したのは、既定ブランチを grep して関数名が 0 件だったからだ。
デプロイの直前だった。気づかなければ半分だけの版を本番へ出していた。
- ベースを別 PR のブランチに向けない。共通ファイルが衝突しても、衝突を手で直すほうが安全
- 同じファイルを触る 2 本なら、そもそも 1 つの PR にまとめるのが正解
- 「ベース側を先にマージすれば大丈夫」は成り立たない。squash だと積んだ側は取り残される
MERGED は状態表示であって、内容の保証ではない
ある PR は画面上 MERGED なのに、既定ブランチに 1 文字も入っていなかった。
マージコミットは変更ファイル 0 件の空コミットで、しかもメッセージが別 PR のものだった。
近い時刻に連続でマージしたときの取り違えと見ている。
同じセッションで出した他の 2 本は正しく着地していた。
つまりこれはPR 単位で起きる。「1 本確認したから大丈夫」にはならない。
確かめるのは状態ではなく内容だ。
git fetch origin
git show origin/main:path/to/file.ts | grep 'theFunctionYouAdded'
人が push しないリポジトリほど危ない
エディタの自動バックアップ機能が 10 分ごとにコミットするリポジトリで、
squash マージ済みのブランチに HEAD が残ったまま自動コミットが積み続けた。
1 日分・9 コミットが既定ブランチに入っていなかった。
自動コミットは「今の HEAD」へ積むので、一度外れると人が戻すまで永遠に外れたままになる。
PR も出ないので通知もない。
- 兆候は
git branch -vvに出る。main ... [origin/main: behind N]なのに別ブランチだけ進んでいる - 根治はHEAD を既定ブランチへ戻すこと。積み先が正しければ以後は自動で追随する
危険なのは「ブランチを整理して」と言われた瞬間だ。
マージ済みに見えるブランチを消すと、そこにしか無い更新が消える。
実務ルール
- PR を出したら、そのブランチに追加コミットを載せない。 別ブランチ・別 PR にする
- 「まだマージされていないから大丈夫」は通用しない。書いている最中にマージされる
- ただし別 PR を前の PR の上に積むのも同じ穴。必ず既定ブランチから切る
- デプロイ前に「本当に入っているか」を grep で実測する。 半分だけ本番へ出すのが最悪の結末
- ブランチを消す前に
git log origin/main..<branch>を見る。git branch -dが「merged to ... but not yet merged to HEAD」と警告したら、それが取りこぼしのサイン - 検算は
git show --statのファイル一覧を突き合わせる。件数が違えば落ちている - push 直後に
git ls-remote --heads origin <branch>でリモート先端のハッシュを照合する。静かに失敗することがあり、パイプで繋いだ成功判定は終了コードを取り違えて嘘をつく
落としたときの復旧
慌てなくていい。ブランチを消してもコミットオブジェクトは残る。
git branch --contains <sha>で所在を探す。reflog より確実- 救出用のブランチを生やす(
git branch <救出名> <sha>) - 既定ブランチの先端へ cherry-pick して、別 PR として出し直す
- 作業ツリーを汚したくなければ
git checkout <旧branch> -- <paths>で内容だけ移す
まとめ
- squash 運用では、PR を出した後の追加コミットは落ちる前提で動く
- stacked PR は同じ穴。ベースは常に既定ブランチ
MERGED・PR 番号・マージコミットの件名は、どれも当てにならない。内容を grep する- 自動コミットは積み先が外れたことを教えてくれない。兆候は
git branch -vv - 消す前に
git log origin/main..<branch>。消してしまっても sha から回収できる