メインコンテンツへスキップ

Claude Codeに教わった `--force-if-includes`

· loading · loading ·
kiitosu
著者
kiitosu
aws community builder. 画像処理やデバイスドライバ、データ基盤構築からWebバックエンドまで、多様な領域に携わってきました。地図解析や地図アプリケーションの仕組みにも経験があり、幅広い技術を活かした開発に取り組んでいます。休日は草野球とランニングを楽しんでいます。
目次

はじめに
#

これまで自分は git push --force しか使ってこなかった。自分が一人で触るブランチでしかforce-pushしないので困ったこともなかったし、警告らしい警告を見た記憶もありません。

きっかけはClaude Codeが force pushの手順を提示するときに --force-with-lease を当たり前のように含めていた こと。「これ何?」から始まり、調べていくと --force-if-includes まで関わってくる、という構図でした。

ちなみに --force-with-lease 自体は2013年、--force-if-includes は2020年と、どちらも古くからあるオプションです。

この記事では:

  1. 3つのオプション(--force / --force-with-lease / --force-if-includes)が何をチェックしているか
  2. なぜ --force-with-lease だけでは足りないのか
  3. 現実的にどう運用すべきか

を整理します。すでに同じトピックの和訳・解説記事は何本も出ている(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 が単独でも効くケースから見ます。

シナリオ

  1. 同僚と自分が feature/xxxx を共有しており、両者の手元は同じcommit X
  2. 同僚が新しいcommit Y を追加して push(fast-forward、--force 不要)
  3. 自分は fetch していない ので、自分の origin/feature/xxxx は古い X のまま
  4. 自分は手元で 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を拒否します。

シナリオ

  1. 同僚と自分が feature/xxxx を共有しており、両者の手元は同じcommit X
  2. 同僚が新しいcommit Y を追加して push
  3. 自分のIDEが裏で git fetch → 自分の origin/feature/xxxx は Y を指すが、ローカルbranchには Y が含まれない
  4. 自分が --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 = truegitconfig 一行に圧縮しておく
  • 結局 --force-with-lease を打つ習慣 に頼るしかない

-f 直打ち自体をブロックする低コストな手段はgitには存在しない、というのが調べてみての結論です。設定派 + alias派 + hook派、と並べて検討しましたが、どれも穴がありました。--force-with-lease を打つ手癖」+ push.useForceIfIncludes = true の組み合わせが現実解、と諦めて受け入れました。

Reply by Email

関連記事

Next.js hydration mismatch 対処法の決定木
· loading · loading
CloudWatch Pipelinesを使ってみた
いまさら AWS AppConfig に触ってみた
· loading · loading