← 解説「push と fetch — リモートとの間で何が運ばれるのか」に戻る
サンプルコードで身につける: push と fetch — リモートとの間で何が運ばれるのか
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1pushで何が送られているかを見る
pushの出力に出る数字は、実際に転送されたオブジェクトの数です。相手が既に持っているものは送られません。2回目以降のpushが軽い理由が数字で分かります。
$ git push origin main
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 512 bytes | 512.00 KiB/s, done.
↑ 実際に送ったのは5個だけ
To github.com:example/tool.git
dd2818b..0361ab2 main -> main
↑ リモートのブランチファイルがこの範囲で更新された
# 送る前に、何が送られるか確認できる
$ git log origin/main..main --oneline
0361ab2 ログアウト処理を追加
# 実際に転送されるオブジェクトを数える
$ git rev-list --objects origin/main..main | wc -l
5
2「相手が持っているか」をハッシュで判定する
内容ベース管理なので、送るべきものの判定はハッシュの集合演算で済みます。差分パッチを作る必要がありません。その仕組みを集合として再現します。
Python
3pushが弾かれるのは fast-forward できないとき
拒否されるのは事故防止です。ローカルのマージ判定と全く同じ理屈がリモート側で行われているだけだと分かれば、対処に迷いません。エラー文の読み方も押さえます。
$ git push origin main
To github.com:example/tool.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:example/tool.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.
# 何が起きているか確認する
$ git fetch origin
$ git log --oneline --graph main origin/main
* 7f2a91c (main) 自分の変更
| * b3d8e40 (origin/main) 誰かの変更 ← 自分が持っていない
|/
* dd2818b 共通の祖先
# 相手の変更を取り込んでから送り直す
$ git pull --rebase origin main # または git pull origin main
$ git push origin main
4fetch と pull の違いを状態で理解する
fetchはリモート追跡ブランチを更新するだけで、作業ツリーもローカルブランチも変えません。pullはそこにmerge/rebaseが加わります。「fetchは安全」と言われる理由がこれです。
# fetch する前
$ git log --oneline -1 main
dd2818b READMEに使い方を追加
$ git log --oneline -1 origin/main
dd2818b READMEに使い方を追加
$ git fetch origin
# origin/main だけが進む。main と作業ツリーは無傷
$ git log --oneline -1 main
dd2818b READMEに使い方を追加 ← 変わっていない
$ git log --oneline -1 origin/main
b3d8e40 誰かの変更 ← こちらだけ更新
# 差分を確認してから、取り込むか決められる
$ git diff main origin/main
$ git merge origin/main # ここで初めてmainが動く
# git pull = git fetch + git merge(または --rebase)
5--force が危険な理由と、安全な代替
--force はリモートのブランチファイルを無条件に書き換えるため、他人のコミットが参照から外れることがあります。--force-with-lease なら、想定外の更新があるときは止まってくれます。
# 危険: 相手が何をpushしていても上書きする
$ git push --force origin main
# 自分がfetchした後に同僚がpushしていた場合、
# そのコミットはどのブランチからも辿れなくなる
# 安全: 「自分が最後に見た状態から変わっていなければ」上書きする
$ git push --force-with-lease origin main
To github.com:example/tool.git
! [rejected] main -> main (stale info)
# → 誰かがpushしていたので止まった。事故を防げた
# 内容を確認してから改めて対応する
$ git fetch origin
$ git log origin/main --oneline -3
# rebase後のpushでforceが要るのは、履歴を作り直したから
# (共有ブランチでrebaseしない、が第一の対策)