Gitリモート入門:push・pull・fetchと拒否されたpushの直し方

ローカルの bare リポジトリでオフライン再現。fetch と pull の違い、non-fast-forward で拒否された push、upstream、pull.rebase の選び方、clone サイズの実測まで。

シリーズ:Git 講座 3/6(中級)。 コミットとブランチには慣れた(講座1・2)けれど、push と pull はおまじないのように打っている人向けです。origin/main の正体、push が拒否される理由、fetch と pull の違い、そして pull の設定の選び方が分かるようになります。

git push が拒否されて手が止まった人、git pull したら誰も望んでいないマージコミットができた人、「force push は絶対ダメ」と言われたけれど理由は聞いていない人のための記事です。例はすべてオフラインで動きます。「サーバー」はローカルフォルダに置いた bare リポジトリで、2つの clone alice と bob を同じチームの2人に見立てます。表示されるメッセージは、認証の部分を除けば GitHub などのホスティングサービスで見るものと同じです。

実行環境は Git 2.54.0(Apple Git-157)、Apple Silicon Mac(M1 Pro)、デモ用 ID、使い捨てフォルダのみ。出力中のローカルパスは ~/demo/… に短縮しています。

壊しても平気なリモートを作る

リモートとは、自分のリポジトリが名前で知っている別のリポジトリです。普通は origin という名前です。bare リポジトリは作業ツリーを持たず、.git の中身だけでできています。サーバー上では誰もファイルを編集しないので bare を使います。

git init --bare -b main remote.git
git clone remote.git alice
Initialized empty Git repository in ~/demo/rm3/remote.git/
Cloning into 'alice'...
warning: You appear to have cloned an empty repository.
done.

この警告は無害です。Alice がコミットして push します。

cd alice
printf 'orders = []\n' > shop.py; git add shop.py; git commit -qm "Start shop"
git push
git branch -vv
To ~/demo/rm3/remote.git
 * [new branch]      main -> main
* main edd9011 [origin/main] Start shop

[origin/main] は main の upstream(追跡ブランチ)です。push・pull・status が既定で比べる相手のリモートブランチで、git clone が設定してくれました。

なぜ普通の(bare でない)リポジトリに push しないのか。試すとサーバー側が拒否しました。

remote: error: refusing to update checked out branch: refs/heads/main
remote: error: By default, updating the current branch in a non-bare repository
remote: is denied, because it will make the index and work tree inconsistent
...
 ! [remote rejected] main -> main (branch is currently checked out)

upstream と新しいブランチの最初の push

新しいブランチには、設定するまで upstream がありません。

git switch -c feature/cart
# ... コミット ...
git push
fatal: The current branch feature/cart has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin feature/cart

To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.

表示どおり git push -u origin feature/cart とするか、push.autoSetupRemote=true を一度設定します(Git 2.37以降)。設定後は同じ git push が次のように表示しました。

 * [new branch]      feature/cart -> feature/cart
branch 'feature/cart' set up to track 'origin/feature/cart'.

もう1つの定番は、存在しないブランチ名を push することです。古い解説の多くは master と書いていますが、新しいリポジトリは main です。

$ git push origin master
error: src refspec master does not match any
error: failed to push some refs to '~/demo/rm3/remote.git'

「src refspec … does not match any」は、その名前のローカルブランチがないという意味です。git branch で確認しましょう。

fetch と pull:origin/main の正体

origin/main はリモート追跡ブランチです。最後に origin と通信したとき main がどこにあったかを手元に控えた、読み取り専用のメモです。動くのは fetch(または最初に fetch を行う pull)のときだけです。

Alice がコミットを push しました。Bob はまだ fetch していません。

cd ../bob
git status
On branch main
Your branch is up to date with 'origin/main'.

ここが落とし穴です。「up to date」はサーバーではなく、Bob の手元の古い origin/main と比べた結果です。fetch すると変わります。

git fetch
git status -sb
git log --oneline main..origin/main
From ~/demo/rm3/remote
   edd9011..8500a27  main       -> origin/main
## main...origin/main [behind 1]
8500a27 Add TAX

fetch はコミットをダウンロードして origin/main を動かしただけで、Bob の main もファイルも変わっていません。中身を確認してから(git log main..origin/main、git diff main origin/main)git merge で取り込めます。今回は fast-forward でした。git pull は git fetch とこの取り込みを続けて実行するだけのコマンドです。

図:サーバー上のリモートの main は、git fetch によって手元のリポジトリのリモート追跡ブランチ origin/main に取り込まれる。origin/main から git merge または git rebase で自分の main と作業ファイルに反映する。git pull はこの2段階をまとめて行う。git push は自分の main をサーバーの main に送り、サーバーに手元にないコミットがあれば拒否される。
git fetch が更新するのは origin/main(リモートの位置の手元の控え)だけ。git pull は fetch に、自分のブランチへのマージかリベースを足したもの。
やりたいこと コマンド ブランチ・ファイルに触れるか
同僚が push した内容を安全に見る git fetch 触れない
取り込む前に比べる git log main..origin/main、git diff main origin/main 触れない
同僚の作業を自分のブランチへ git pull(fetch + merge/rebase) 触れる
fast-forward のときだけ受け入れる git pull --ff-only 触れる、または失敗
サーバーの今の状態を確認 git ls-remote origin 触れない

拒否された push と取り込み方

今度は2人が同時に作業します。Alice は送料を追加して先に push しました。Bob は config.py を追加してコミットし、push します。

$ git push
To ~/demo/rm3/remote.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '~/demo/rm3/remote.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Bob の push を受け入れると、サーバーの main から Alice のコミットが抜け落ちるので Git は拒否します。pull の設定がない状態で Bob が git pull を実行すると:

From ~/demo/rm3/remote
   8500a27..b2e3cbf  main       -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
...
fatal: Need to specify how to reconcile divergent branches.

最初の2行に注目してください。fetch はもう終わっています。 この状態でもう一度 push すると、理由が (fetch first) から (non-fast-forward) に変わります。Git がリモートのコミットを知ったうえで、Bob のブランチが遅れていると判断したためです。git status -sb は [ahead 1, behind 1] でした。

Bob のリポジトリを2つコピーして、両方の答えを試しました。

マージ(git pull --no-rebase):

*   c70294d Merge branch 'main' of ~/demo/rm3/remote
|\  
| * b2e3cbf Add shipping fee
* | a556d35 Add currency
|/  
* 8500a27 Add TAX

リベース(git pull --rebase):

Successfully rebased and updated refs/heads/main.
* 50a55ed Add currency
* b2e3cbf Add shipping fee
* 8500a27 Add TAX
* edd9011 Start shop

リベースでは Bob のコミットが Alice のコミットの上に積み直され、ID が変わりました(a556d35 → 50a55ed)。履歴は一直線になり、push も通りました(b2e3cbf..50a55ed main -> main)。マージ版でも動きますが、pull のたびに中身のない「Merge branch 'main' of …」コミットが増えていきます。

判断図:fetch first または non-fast-forward で push が拒否された。手元のコミットが未共有なら git pull --rebase(履歴は一直線、コミットIDは新しくなる)。すでに他で共有済みなら git pull --no-rebase(マージコミットが増える)。その後もう一度 push する。共有ブランチに git push --force は使わない。
push が拒否されたら、取り込み方を選ぶ。自分しか持っていないコミットならリベースが定番。
設定 分岐時の git pull の動作 向いている場面
pull.rebase true 手元のコミットを上に積み直す 多くのチーム。未公開の作業をきれいに保つ
pull.rebase false マージコミットを作る 手元のコミットを別の場所ですでに共有している
pull.ff only fast-forward 以外は拒否 自分ではコミットしないブランチ(デプロイ用の checkout など)
未設定(2.54.0) fatal: Need to specify how to reconcile divergent branches. 選択を強制する

リベースが書き換えるのはまだ push していない自分のコミットだけなので安全です。リベースの詳細と、他人がすでに持っているコミットをリベースしてはいけない理由は講座4で扱います。

よくある失敗:「とりあえず force push」

実際に起きること。 Alice の clone は、origin/main がまだ b2e3cbf だと思っています(Bob の push を fetch していない)。Alice は直前のコミットを amend して push します。普通の push は拒否されるので、force に手を伸ばします。

--force-with-lease は、Alice が最後に見た状態がサーバーにまだあるかを先に確かめます。

$ git push --force-with-lease
 ! [rejected]        main -> main (stale info)
error: failed to push some refs to '~/demo/rm3/remote.git'

素の --force には確認がありません。

$ git push --force
 + 50a55ed...56ed907 main -> main (forced update)

Bob のコミット 50a55ed はサーバーの main から消えました。次に Bob が pull すると + 50a55ed...56ed907 main -> origin/main (forced update) と分岐エラーが表示され、Bob の作業は彼の手元の clone にしか残っていません(検証後、サーバーはコピーから復元しました)。

対処: 共有ブランチには force push しない。自分だけの機能ブランチをリベース後に書き換えるときは git push --force-with-lease を使います。直前に git fetch したなら、届いた内容を先に確認します(git log HEAD..origin/<ブランチ>)。fetch すると lease が比べる値が更新され、保護が効かなくなるからです。

実測:通常・shallow・blobless の clone

リモートには「どれだけダウンロードするか」という面もあります。CSV ファイル1つに毎回20行ずつ追記する2,000コミットのリポジトリを生成しました(乱数シード42固定、最終的に40,000行・984,486バイト、git gc 後のサーバー側パックは10.55MiB)。ハードリンクではなく実際のパック転送になるよう file:// 経由で clone し、各3回、Apple Silicon Mac(M1 Pro)、Git 2.54.0 で測りました。

clone の種類 各回(ms) .git のサイズ 履歴のコミット数
通常 1169.4 / 1124.1 / 1221.5 10,936 KB 2,000
--depth 1(shallow) 128.1 / 129.6 / 140.9 608 KB 1
--filter=blob:none(blobless) 180.9 / 190.7 / 184.8 1,108 KB 2,000
棒グラフ:通常の clone 1,169ms、--depth 1 の shallow clone 130ms、--filter=blob:none の blobless clone 185ms。
2,000コミットのリポジトリを file:// で clone した時間(各3回の中央値)。.git のサイズは full 10,936KB、shallow 608KB、blobless 1,108KB。Git 2.54.0、Apple M1 Pro。

小さな clone にも後払いのコストがあります。

  • blobless の clone で git log -p -50 を実行すると、1回目は 6,792.8ms かかりました。古い版のファイル50個をその場で取りに行くためで、.git は25,644KBまで増えました。2回目は300.3msでした。
  • shallow の clone では git log --oneline にコミットが1つしか出ず、git rev-parse --is-shallow-repository は true でした。git fetch --unshallow は1,241.9msかかり、.git は12,104KB、ほぼ通常 clone と同じコストになりました。
  • 新着がないときの git fetch は5回の中央値で41.5msでした。

目安:1つのコミットをビルドするだけの CI は --depth 1、履歴は必要だが古いファイルまでは要らない開発者は blobless、毎日使う作業用 clone は通常の clone。

正直な限界とバージョンの注意

  • すべて Git 2.54.0 とローカルの bare リポジトリで実行しました。ホスティングサービスでは認証、ブランチ保護、独自の拒否メッセージ(保護ブランチが force push を拒否する等)が加わりますが、ここでは試していません。
  • push.autoSetupRemote は Git 2.37 で追加されました。分岐時の pull がエラーになったのは 2.33/2.34 の頃で(それ以前は警告してマージしていた)、文言は 2.54.0 でのみ確認しています。
  • --force-with-lease は Git 1.8.5 からあります。期待値を明示しない場合は手元の origin/main と比べるので、直前の git fetch で保護が弱まります。
  • 部分 clone(--filter)にはサーバー側の対応が必要で、ローカルの bare リポジトリでは uploadpack.allowFilter true を設定しました。file:// での時間にはネットワーク遅延が含まれないので、実際の差は普通もっと大きくなります。

練習問題

  1. git status は「Your branch is up to date with 'origin/main'」と言いますが、同僚は10分前に push したと言っています。自分のブランチを変えずに同僚のコミットを見るには?
  2. push が (non-fast-forward) で拒否されました。手元には一度も push していないコミットが2つあります。履歴を一直線に保ったままリモートの作業を取り込むコマンドは?

解答

  1. git fetch の後に git log --oneline main..origin/main。status は最後に fetch した origin/main としか比べません。
  2. git pull --rebase(または一度だけ git config pull.rebase true)の後に git push。2つのコミットの ID は変わりますが、他の誰も持っていないので問題ありません。

前の講座: Git 講座2:ブランチ・マージ・コンフリクト ・次の講座: Git 講座4:rebase・cherry-pick と履歴 ・シリーズの最初:Git 講座1 ・新人向け学習ロードマップに戻る。

よくある質問

git fetch と git pull の違いは何ですか?

git fetch は新しいコミットをダウンロードして origin/main などのリモート追跡ブランチを更新するだけで、自分のブランチやファイルには触れません。git pull は fetch の後に origin/main を自分のブランチへマージまたはリベースします。検証では、リモートに新しいコミットがあっても fetch 前の git status は up to date と表示し、fetch 後に behind 1 になりました。

push が fetch first や non-fast-forward で拒否されたのはなぜですか?

リモートのブランチに手元にないコミットがあり、そのまま受け入れるとそれが消えてしまうからです。git pull --rebase(マージにしたいなら --no-rebase)で先に取り込んでから、もう一度 push します。共有ブランチで --force は使いません。--force-with-lease はリモートが動いていたため (stale info) で上書きを拒否しました。

fatal: Need to specify how to reconcile divergent branches とは?

自分のブランチとリモートの両方に新しいコミットがあり、マージかリベースかを決める pull.rebase や pull.ff の設定がない状態です。コマンドごとに git pull --rebase/--no-rebase/--ff-only を指定するか、git config pull.rebase true のように一度設定します。このエラーが出た時点で fetch はすでに終わっています。

  • #git
  • #中級
  • #リモート
  • #push
  • #pull
  • #fetch

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑