Dev Study
← 解説「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しない、が第一の対策)