はじめに #
これまで自分は git push --force しか使ってこなかった。自分が一人で触るブランチでしかforce-pushしないので困ったこともなかったし、警告らしい警告を見た記憶もありません。
きっかけはClaude Codeが force pushの手順を提示するときに --force-with-lease を当たり前のように含めていた こと。「これ何?」から始まり、調べていくと --force-if-includes まで関わってくる、という構図でした。
ちなみに --force-with-lease 自体は2013年、--force-if-includes は2020年と、どちらも古くからあるオプションです。
この記事では:
- 3つのオプション(
--force/--force-with-lease/--force-if-includes)が何をチェックしているか - なぜ
--force-with-leaseだけでは足りないのか - 現実的にどう運用すべきか
を整理します。すでに同じトピックの和訳・解説記事は何本も出ている(onkさんの記事 など)ので、新規性で書くのではなく 自分のための整理 として書きます。
1. 3つのオプションは何を見ているか #
用語:「ref」とは
branchやtagなど commitを指す名前 のこと。--force-with-lease の文脈では2種類のrefを区別する必要があります。
- サーバ側のref:origin リポジトリ上の
refs/heads/feature/xxxx— リモートが今この瞬間に指しているcommit - ローカルのリモート追跡ref:自分のローカルにある
refs/remotes/origin/feature/xxxx(通称origin/feature/xxxx)— 最後にfetchしたときのスナップショット
3つとも「pushしてよいか」の判定基準が違うだけです。
| オプション | 判定基準 | 守れる事故 |
|---|---|---|
--force / -f |
(何もチェックしない) | なし |
--force-with-lease |
サーバ上の実際のbranch が、ローカルの リモート追跡ref(origin/<branch>) と一致するか |
A |
--force-with-lease + --force-if-includes |
上に加え、自分のローカルブランチの履歴が、リモート追跡refの指すcommitを含むか | A + B |
- 事故A:他人がpushしたコミットを、自分が知らないまま上書きする
- 事故B:他人のpushを fetch だけしてしまい、ローカルにマージ/rebaseしないままforce-pushして消す
2. --force-with-lease が効果を発揮するケース(事故Aを防ぐ)
#
まず --force-with-lease が単独でも効くケースから見ます。
シナリオ:
- 同僚と自分が
feature/xxxxを共有しており、両者の手元は同じcommit X - 同僚が新しいcommit Y を追加して push(fast-forward、
--force不要) - 自分は fetch していない ので、自分の
origin/feature/xxxxは古い X のまま - 自分は手元で X を amend した状態で
--force-with-leaseで push しようとする
%%{init: {'themeVariables': {'actorLineColor': '#888', 'signalColor': '#888', 'signalTextColor': '#cccccc', 'noteBorderColor': '#888'}}}%%
sequenceDiagram
participant Other as 同僚
participant Me as 自分
participant Remote as origin
Note over Me,Other: スタート:両者の手元 = X
自分の origin/feature/xxxx = X
Other->>Remote: 新しいcommit Y を追加して push
Note over Remote: remote は Y に進んだ
Note over Me: 自分はfetchしていない
→ origin/feature/xxxx は古い X のまま
Me->>Remote: push --force-with-lease
Note over Me,Remote: チェック:実ref(Y) ≠ ローカルorigin/...(X)
Remote-->>Me: rejected (stale info)
Note over Me,Remote: 同僚のYは守られた
実際に手元で再現してみると:
$ git log --oneline feature/xxxx ← 自分のローカル
f4a0c26 work (自分 fix)
352173e init
$ git log --oneline origin/feature/xxxx ← 自分の追跡ref(fetch前)
eaa4505 work
352173e init
$ git push --force-with-lease origin feature/xxxx
! [rejected] feature/xxxx -> feature/xxxx (stale info)
error: failed to push some refs to ...
(stale info) というメッセージで弾かれます。理由:
- サーバ上の実 ref(同僚のpush済み Y)≠ 自分の追跡ref(古い X)
- 「自分は古い情報しか持っていない」と git が判断 → push を拒否
これがないと、自分の amend が同僚のYを 気づかずに上書き してしまうところでした。--force-with-lease は 「自分が知らないリモート更新」を検出して止める のが本来の効能です。
--forceだったらどうなるか同じ状況で
git push --forceを打つと、警告も拒否もなく 同僚のYを上書きしてしまいます。これが事故A。
3. --force-with-lease だけでは足りない理由(事故B)と --force-if-includes の出番
#
--force-with-lease は 「サーバ上の実際のbranch = ローカルのリモート追跡ref」 を確認するチェックでした。一見万全ですが、自分のIDEやツールが裏で git fetch を回していると、追跡refがいつの間にか最新に更新されてしまう という穴があります。
この穴を踏むとどうなるか:
- 同僚が新しいcommit Y を push
- 自分は手元で amend/rebase 中
- IDEが裏で勝手に fetch → 自分の追跡refも Y を指す
- 自分が
--force-with-leaseで push → チェックが通って Y が上書きされる(事故B)
これを塞ぐのが --force-if-includes です。
push対象のローカルブランチが、最後にfetchしたリモート側の commit を含んでいる(merge/rebase済み) ことも確認する。
つまり「追跡refは更新されたが、自分のbranchにはまだ取り込んでいない」状態を検知してpushを拒否します。
シナリオ:
- 同僚と自分が
feature/xxxxを共有しており、両者の手元は同じcommit X - 同僚が新しいcommit Y を追加して push
- 自分のIDEが裏で
git fetch→ 自分のorigin/feature/xxxxは Y を指すが、ローカルbranchには Y が含まれない - 自分が
--force-with-lease --force-if-includesで push しようとする
%%{init: {'themeVariables': {'actorLineColor': '#888', 'signalColor': '#888', 'signalTextColor': '#cccccc', 'noteBorderColor': '#888'}}}%%
sequenceDiagram
participant Other as 同僚
participant Me as 自分
participant Remote as origin
Note over Me: 自分のbranchを amend / rebase 中
Other->>Remote: 新しいcommit Y を追加して push
Me->>Remote: fetch(IDE等が裏で自動実行)
Remote-->>Me: 同僚の新コミット Y
Note over Me: 追跡refはYだが、
自分のlocal feature/xxxx に Y は含まれていない
Me->>Remote: push --force-with-lease --force-if-includes
Note over Me,Remote: 追加チェック:自分のbranchに Y は含まれているか?
→ 含まれていない
Remote-->>Me: rejected (remote ref updated since checkout)
Note over Me,Remote: 同僚のYは守られた
--force-with-lease 単体ならスルーされていた状況が、--force-if-includes の追加チェックで止まる、というのがポイントです。
4. 運用への落とし方 #
毎回 git push --force-with-lease --force-if-includes と打つのは現実的ではないので、日常運用に溶かしたい。
ここで自然と「そもそも -f を打てなくできないのか」という疑問が湧きますが、結論から言うと できません。一通り見てから、現実的な落とし所に着地します。
-f を打てなくする手段はあるか — どれもハマる
#
| 手段 | なぜハマるか |
|---|---|
push.useForceWithLease のような設定 |
そんな設定はgitに存在しない。push.useForceIfIncludes は --force-with-lease の挙動を拡張するだけで、--force には関与しない |
シェル alias(alias gpf=...) |
安全な代替を提供するだけで -f 直打ちは止まらない。加えてbashrc/zshrcを全マシンで揃える運用は現実的でない |
git alias(alias.pushf) |
gitconfig経由なので多少撒きやすいが、結局 -f 直打ちは素通り |
| client-side pre-push hook | hook自身は -f フラグの情報を受け取れない(標準入力にはref情報しか来ない)。判定可能なのは「force pushかどうか」だけで、これだと --force-with-lease も同時にブロックしてしまう ので本末転倒 |
シェルで git() をラップして -f を書き換え |
移植性がなく、各マシン設定が必要。エディタ起動時のターミナル等で漏れる |
| サーバ側 protected branches(GitHub/GitLab) | これは -f を止める手段ではなく、サーバ側で受け付けないようにする 手段。protect されていないブランチには無力 |
整理すると:
- クライアント側:
-fを確実にブロックする手段は実質ない(gitの設計上、そもそも止めにくい) - サーバ側:protected branches で守れるのは protect されたブランチだけ
結局、現実的な落とし所はこれだけ #
git config --global push.useForceIfIncludes true
# 以後
git push --force-with-lease # = with-lease + if-includes
つまり:
push.useForceIfIncludes = trueをgitconfigに入れる--force-with-leaseを打つ習慣を維持 する-f直打ちのリスクは残るが、それは諦める(共有ブランチに対しては protected branches でサーバ側が守ってくれることを期待する)
「-f を完全に打てなくしたい」と思っても、それを満たす低コストな手段がgitには存在しません。ある程度は習慣でカバーするしかない、というのが正直な結論です。
なお --force-if-includes は --force-with-lease 併用が前提 です。単体で渡しても force としては効かず、普通の non-fast-forward push として拒否されるだけです。
5. 何を覚えておけば良いか #
--forceは使わない。--force-with-leaseが基本--force-with-leaseだけでは「裏で fetch されてた」事故を防げない →--force-if-includesを併用push.useForceIfIncludes = trueを設定すれば、追加の引数は不要
ワンライナーでまとめると:
git config --global push.useForceIfIncludes true
# 以後
git push --force-with-lease # = with-lease + if-includes
最後に #
「自分が一人で触るブランチでしか force-push しないから困らない」と思ってきました。実際、自分が単独で触るブランチでは --force でも事故にならない(自分の作業しか上書きしないから)。
ただ、共有ブランチを触るタイミングは突然来ます。来てから慌てて調べるよりは、
--force-with-leaseで 事故A(他人のpushを知らずに上書き)を防ぎ--force-if-includesで 事故B(自動fetch経由で「知っているつもり」になって上書き)も防ぐ- これを
push.useForceIfIncludes = trueの gitconfig 一行に圧縮しておく - 結局
--force-with-leaseを打つ習慣 に頼るしかない
-f 直打ち自体をブロックする低コストな手段はgitには存在しない、というのが調べてみての結論です。設定派 + alias派 + hook派、と並べて検討しましたが、どれも穴がありました。「--force-with-lease を打つ手癖」+ push.useForceIfIncludes = true の組み合わせが現実解、と諦めて受け入れました。