Git入門:バージョン管理のきほん

push と pull で同期する

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部
push と pull のズレ

このレッスンで分かること

  • push と pull が何を運んでいるのか、上流ブランチという結びつきの意味
  • git pull が fetch と merge の 2 段構えであることと、--rebase という選択肢
  • push が弾かれたときの正しい直し方と、--force-with-lease を使う理由

push と pull は何を運んでいるか

前のレッスンで、リモートリポジトリが「ネットワークの向こう側にある同じ形の Git リポジトリ」であることを見ました。origin はその URL につけた別名でした。

push と pull は、その 2 つのリポジトリの間でコミットをやりとりする操作です。

コマンド向き運ぶもの
git push手元 → リモート手元にあってリモートに無いコミット
git pullリモート → 手元リモートにあって手元に無いコミット

大事なのは、運ばれるのがコミットだという点です。ファイルの中身が同期されているのではありません。まだコミットしていない編集中のファイルは、どれだけ push してもリモートには行きません。

ターミナル

$ git status On branch main Changes not staged for commit: modified: src/app.js $ git push origin main Everything up-to-date

Everything up-to-date と言われて戸惑うのは、この段階の人がよく通る道です。編集は残っていますが、コミットしていないので Git から見れば送るものが無いのです。まず git addgit commit をしてください。

上流ブランチを設定する

作ったばかりのブランチを push すると、次のように怒られます。

ターミナル

$ git push fatal: The current branch feature-login has no upstream branch. To push the current branch and set the remote as upstream, use git push --set-upstream origin feature-login

上流ブランチというのは、「手元のこのブランチは、リモートのあのブランチと対応している」という結びつきのことです。この対応が決まっていないと、Git はどこへ送ればいいか判断できません。

メッセージが指示している通りに打ちます。-u--set-upstream の短い形です。

ターミナル

$ git push -u origin feature-login Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Writing objects: 100% (3/3), 312 bytes | 312.00 KiB/s, done. To github.com:example/myapp.git * [new branch] feature-login -> feature-login branch 'feature-login' set up to track 'origin/feature-login'.

最後の行が結びつきができたという報告です。以後このブランチでは、引数なしの git pushgit pull が使えます。

ターミナル

$ git push Everything up-to-date

-u が必要なのは最初の 1 回だけです。2 回目以降につけても害はありませんが、意味はありません。

上流ブランチが決まっていると、git status が手元とリモートのずれを教えてくれるようになります。

ターミナル

$ git status On branch feature-login Your branch is ahead of 'origin/feature-login' by 2 commits. (use "git push" to publish your local commits)

ahead by 2 は「手元が 2 コミット先に進んでいる」つまり push すべきものが 2 つあるという意味です。逆に behind と出たら、リモートのほうが進んでいて pull が必要な状態です。両方同時に出ることもあり、その場合は次で説明する非 fast-forward になります。

git pull は fetch と merge の 2 段構え

ここが、このレッスンでいちばん理解しておくべきところです。

git pull は単独のコマンドに見えますが、実際には 2 つの操作を続けて実行しています。

  1. git fetch — リモートの最新の状態を取ってくる。手元の作業には触らない
  2. git merge — 取ってきた内容を、いま自分がいるブランチに合流させる

この境界を覚えておいてください。fetch は取ってくるだけ、pull は取ってきて合流までやる、という違いです。

実際に手で分解してみると分かります。次の 2 行は git pull とほぼ同じ意味です。

ターミナル

$ git fetch origin $ git merge origin/main

origin/main については次のレッスン clone と fetch の違い で詳しく扱います。ここでは「リモートの main が最後に確認した時点でどこにいたかを記録している目印」とだけ押さえてください。

pull の実行結果はこう出ます。

ターミナル

$ git pull remote: Enumerating objects: 7, done. remote: Counting objects: 100% (7/7), done. Unpacking objects: 100% (4/4), 421 bytes | 421.00 KiB/s, done. From github.com:example/myapp 3b9c0d1..8a2f4e6 main -> origin/main Updating 3b9c0d1..8a2f4e6 Fast-forward src/api/user.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-)

前半が fetch の出力、Updating から後ろが merge の出力です。2 段構えであることが、出力からも読み取れます。

作業中に「いま何が起きているか分からない」状態になったら、pull を使わず git fetch だけを打ってください。手元は一切変わらないので、状況を落ち着いて確認してから次を決められます。

--rebase という選択肢

pull の後半が merge だということは、合流のしかたを変えられるということでもあります。

--rebase をつけると、merge の代わりに rebase が実行されます。

ターミナル

$ git pull --rebase

違いは履歴の形に出ます。merge の場合、相手の変更と自分の変更が合流した地点にマージコミットが 1 つ作られます。自分の作業が 1 コミットしかなくても、Merge branch 'main' of github.com:example/myapp という中身の無いコミットが履歴に残ります。これが積み重なると、履歴がひどく読みにくくなります。

rebase の場合はマージコミットができません。相手の変更をまず取り込み、その上に自分のコミットを積み直します。履歴が一本の線になります。

方法履歴の形マージコミット
git pull (既定)枝分かれと合流が残るできる
git pull --rebase一本の線になるできない

毎回つけるのが面倒なら、設定に入れておけます。

ターミナル

$ git config --global pull.rebase true

rebase は自分のコミットを作り直す操作なので、ハッシュが変わります。すでに push 済みのコミットに対して pull --rebase を使うと、後述の非 fast-forward 状態を自分で作り出すことになります。まだ push していない手元のコミットに対して使うのが基本です。

push が弾かれたとき

チーム開発で必ず一度は出会うのがこれです。

ターミナル

$ git push To github.com:example/myapp.git ! [rejected] main -> main (fetch first) error: failed to push some refs to 'github.com:example/myapp.git' hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing hint: to the same ref. You may want to first integrate the remote changes hint: (e.g., 'git pull ...') before pushing again.

rejected という強い言葉が出ますが、これは Git があなたを守っている状態です。

何が起きているかを説明します。あなたが作業している間に、別の人が同じブランチに push しました。リモートには、あなたがまだ持っていないコミットがあります。ここであなたの push を通すと、相手のコミットが履歴から消えます。だから Git は断っています。

この状態を非 fast-forward と呼びます。fast-forward は「手元の履歴の先端に、そのまま継ぎ足せる」状態のことで、それができないから拒否されている、という意味です。

正しい直し方は 3 手です。

ターミナル

$ git pull $ # 競合が出たら解決して git add と git commit $ git push

pull --rebase を使う場合も同じ流れです。

ターミナル

$ git pull --rebase $ # 競合が出たら解決して git add と git rebase --continue $ git push

pull で競合が出たときの解決手順は コンフリクトを解決する で扱った通りです。

やってはいけない直し方

検索すると、こういう答えが見つかります。

ターミナル

$ git push --force

これは確かにエラーを消します。そして相手のコミットも消します。

--force は「リモートの履歴を、手元の履歴でまるごと置き換えろ」という命令です。リモートにあってあなたが持っていないコミットは、単純に無くなります。相手の半日分の作業が消えたという事故は、ほぼ例外なくこのコマンドから起きます。

エラーメッセージが rejected だからといって、押し通す方向へ進んではいけません。エラーの原因は「相手の変更を取り込んでいないこと」なので、直すべきは取り込むことです。

force が必要なときは --force-with-lease

とはいえ、force push が正当な場面もあります。

自分だけが使っている個人ブランチで git rebasegit commit --amend をすると、コミットのハッシュが変わります。リモートにある古いハッシュのコミットとは繋がらないので、普通の push は弾かれます。この場合は上書きが正しい操作です。

そのときも --force ではなく --force-with-lease を使ってください。

ターミナル

$ git push --force-with-lease

このオプションは、上書きする前にひとつ確認をします。「リモートの現在の先端は、自分が最後に fetch したときの位置と同じか」を見ます。

同じなら、自分が知らないコミットは無いということなので、上書きします。違っていたら、自分が見ていない間に誰かが push したということなので、止めます。

ターミナル

$ git push --force-with-lease To github.com:example/myapp.git ! [rejected] feature-login -> feature-login (stale info) error: failed to push some refs to 'github.com:example/myapp.git'

stale info は「あなたの持っている情報が古い」という意味です。ここで止まってくれるおかげで、相手の作業を消さずに済みます。

コマンド相手のコミットがあったら
git push拒否する
git push --force-with-lease拒否する
git push --force消して上書きする

--force-with-lease を使う前に git fetch を打たないでください。fetch すると「最後に確認した位置」が最新に更新されてしまい、相手のコミットを知らないまま知っているつもりになります。安全装置が働かなくなります。

main のような共有ブランチに対しては、--force-with-lease であっても打たないのが原則です。GitHub 側の設定でブランチを保護し、force push そのものを禁止しておくのが確実です。

この章のポイント
  • push と pull が運ぶのはコミット。コミットしていない編集は送られない
  • git pull は fetch と merge の 2 段構え。fetch は取ってくるだけ、pull は合流までやる
  • push が弾かれたら git pull で相手の変更を取り込んでから push し直す。押し通してはいけない
  • 上書きが必要な個人ブランチでは --force ではなく --force-with-lease を使う