Git入門:バージョン管理のきほん
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-dateEverything up-to-date と言われて戸惑うのは、この段階の人がよく通る道です。編集は残っていますが、コミットしていないので Git から見れば送るものが無いのです。まず git add と git 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 push と git 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 つの操作を続けて実行しています。
git fetch— リモートの最新の状態を取ってくる。手元の作業には触らないgit merge— 取ってきた内容を、いま自分がいるブランチに合流させる
この境界を覚えておいてください。fetch は取ってくるだけ、pull は取ってきて合流までやる、という違いです。
実際に手で分解してみると分かります。次の 2 行は git pull とほぼ同じ意味です。
ターミナル
$ git fetch origin
$ git merge origin/mainorigin/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 truerebase は自分のコミットを作り直す操作なので、ハッシュが変わります。すでに 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 pushpull --rebase を使う場合も同じ流れです。
ターミナル
$ git pull --rebase
$ # 競合が出たら解決して git add と git rebase --continue
$ git pushpull で競合が出たときの解決手順は コンフリクトを解決する で扱った通りです。
やってはいけない直し方
検索すると、こういう答えが見つかります。
ターミナル
$ git push --forceこれは確かにエラーを消します。そして相手のコミットも消します。
--force は「リモートの履歴を、手元の履歴でまるごと置き換えろ」という命令です。リモートにあってあなたが持っていないコミットは、単純に無くなります。相手の半日分の作業が消えたという事故は、ほぼ例外なくこのコマンドから起きます。
エラーメッセージが rejected だからといって、押し通す方向へ進んではいけません。エラーの原因は「相手の変更を取り込んでいないこと」なので、直すべきは取り込むことです。
force が必要なときは --force-with-lease
とはいえ、force push が正当な場面もあります。
自分だけが使っている個人ブランチで git rebase や git 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を使う