Git基礎 #4 リモートリポジトリ — clone・fetch・pull・pushとoriginの正体

読了 7分

「Git基礎 #3 ブランチとマージ — fast-forwardと3-way」までのすべての作業は、自分のコンピュータの.gitディレクトリの中で完結していました。コミットもブランチもマージもネットワークなしで動きます。しかしこの状態では、コンピュータが壊れると履歴全体を失い、ほかの人と一緒に作業する方法もありません。2つの問題を一度に解決するのがリモートリポジトリです。

全6回で構成します。

  • #1 Gitとは — スナップショットモデルと3つの領域
  • #2 add・commit・status — ステージングエリアの意味
  • #3 ブランチとマージ — fast-forwardと3-way
  • #4 リモートリポジトリ — clone・fetch・pull・pushとoriginの正体 ← この記事
  • #5 取り消し — restore・reset・revertの使い分け
  • #6 GitHubと初めてのPull Request

この記事では、リモートリポジトリを複製するcloneから、リモートとやり取りするfetch、pull、pushまでの流れを整理します。その過程で、多くの人が名前だけ知っていて正体は知らないまま使っているoriginとorigin/mainが実際には何なのかを確認します。

git clone — 履歴全体を複製する #

リモートリポジトリでの作業の出発点は、ほとんどの場合cloneです。

リポジトリの複製
$ git clone https://github.com/schoolofweb/sample-project.git
Cloning into 'sample-project'...
remote: Enumerating objects: 127, done.
Receiving objects: 100% (127/127), done.

cloneが持ってくるのは最新のファイルだけではありません。すべてのコミット、すべてのブランチを含む履歴全体が複製されます。だからcloneが終わった瞬間から、git logもブランチ作成もネットワークなしで動きます。複製だからといって機能が制限されたコピーではなく、リモートにあるものと対等な、完全なリポジトリがもう1つできるのです。

originの正体 — リモートのデフォルト名にすぎない #

clone直後のリポジトリの中で、リモートの一覧を確認してみます。

リモートの確認
$ cd sample-project
$ git remote -v
origin  https://github.com/schoolofweb/sample-project.git (fetch)
origin  https://github.com/schoolofweb/sample-project.git (push)

originは特別な機能を持つ予約語ではありません。cloneするときに元のリポジトリのURLへ自動で付く、デフォルトの別名にすぎません。毎回長いURLを入力する代わりに、originという短い名前で呼んでいるだけです。名前が気に入らなければgit remote rename origin upstreamのように変えることもでき、リモートを複数登録することもできます。originという名前そのものには何の魔法もないという点だけ覚えておけば十分です。

origin/main — リモートを記憶するローカルのポインタ #

ブランチをすべて表示すると、見慣れない名前が1つ現れます。

リモート追跡ブランチの確認
$ git branch -a
* main
  remotes/origin/main

origin/mainはリモートサーバーにあるブランチではありません。最後にリモートと通信したとき、リモートのmainがどこを指していたかを記録した、自分のリポジトリの中にある読み取り専用のポインタです。リモート追跡ブランチ(remote-tracking branch)と呼びます。

自分の main と origin/main は別のポインタ
[リモートリポジトリ (GitHub)]
A --- B --- C            ← main

[自分のリポジトリ]
A --- B --- C            ← origin/main (最後に通信した時点の記録)
              \
                D        ← main (HEAD, 自分が追加したコミット)

自分がローカルでコミットDを作ると、自分のmainだけが前進し、origin/mainはCに留まります。逆にほかの人がリモートへコミットを上げても、自分が通信するまで自分のorigin/mainはその事実を知りません。この構造を理解すると、fetchとpullの違いが明確になります。

git fetch — 取得するが、触らない #

fetchはリモートの新しいコミットをダウンロードして、リモート追跡ブランチだけを更新します。

fetch でリモートの状態を取得
$ git fetch origin
remote: Enumerating objects: 5, done.
From https://github.com/schoolofweb/sample-project
   1a2b3c4..9f8e7d6  main       -> origin/main

出力の最後の行が核心です。更新されたのはorigin/mainだけで、自分のmainと作業ディレクトリのファイルはそのままです。だからfetchはいつ実行しても安全です。取得したあとで何が変わったのかを確認してから、合流させるかどうかを決められます。

fetch 後の差分確認とマージ
$ git log main..origin/main --oneline   # リモートにだけあるコミットを確認
9f8e7d6 ログインのバグを修正

$ git merge origin/main                  # 確認してから自分の main に合流

git pull — fetchとmergeを一度に #

pullは新しいコマンドではありません。fetchを実行した直後にmergeまで実行する短縮コマンドです。

pull は fetch + merge
$ git pull origin main
# 下の2つのコマンドと同じです
# git fetch origin
# git merge origin/main

日常の作業ではpullが便利です。ただし、何が下りてきて何が合流するのかを知らないまま実行することになるため、リモートで大きな変更があったらしいときや状態が気になるときは、fetchで先に確認してからmergeする2段階が安全です。pullがよく分からなくなったら、fetchとmergeに分解して考えると混乱のほとんどが整理されます。

git push — 自分のコミットをリモートへ上げる #

方向を逆にして、自分のコミットをリモートへ上げるときはpushを使います。新しいブランチを初めて上げるときだけ、形が少し違います。

初めてプッシュするとき
$ git push -u origin feature-login
To https://github.com/schoolofweb/sample-project.git
 * [new branch]      feature-login -> feature-login
branch 'feature-login' set up to track 'origin/feature-login'.

-uはupstream設定のオプションです。自分のfeature-loginがリモートのorigin/feature-loginとペアだという事実を記録しておきます。このペアを一度記録すれば、次からはブランチとリモートの名前を省略してgit pushgit pullだけで十分です。どこへ送り、どこから受け取るかをGitがすでに知っているからです。

pushが拒否されるとき #

pushは常に成功するわけではありません。自分が最後にfetchしたあとで、ほかの人が先にリモートへコミットを上げていたら、リモートが自分より先行している状態なのでGitはpushを拒否します。

push の拒否
$ git push
To https://github.com/schoolofweb/sample-project.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs
hint: Updates were rejected because the remote contains work that you do not
hint: have locally.

自分のpushがリモートのコミットを上書きしないように守ってくれる、正常な保護装置です。解決の順序も決まっています。リモートのコミットを先に受け取って合流させ、それからもう一度上げることです。

拒否されたときの解決の流れ
$ git pull      # リモートのコミットを受け取り自分のコミットとマージ
$ git push      # 合流した結果を上げる

pullの途中でコンフリクトが起きたら、前回の記事で扱ったコンフリクト解決の手順をそのまま踏めば大丈夫です。

認証 — HTTPSとSSH #

pushはリポジトリへ書き込む行為なので認証が必要です。方式は2つです。

  • HTTPS — URLがhttps://で始まります。最初にpushするとき、OSのcredential managerがGitHubへのログインを求め、以降は保存された認証情報を再利用します。始めるのが簡単なので入門段階に向いています。
  • SSH — URLがgit@github.com:で始まります。公開鍵をGitHubアカウントに登録しておけば、以降は認証手続きなしで通信します。初期設定が必要な代わりに、一度済ませれば楽です。
注記
どちらでも機能に差はないので、今はHTTPSで始めても十分です。SSHキーの登録手順は、GitHub公式ドキュメントの「Connecting to GitHub with SSH」に段階ごとにまとまっています。

まとめ #

この記事のポイントは4つです。

  • cloneは最新のファイルではなく履歴全体を複製し、originはその元のURLに付くデフォルトの別名にすぎません。
  • origin/mainは最後に通信した時点のリモートの状態を記録した、自分のリポジトリの中にある読み取り専用のポインタです。
  • fetchはリモート追跡ブランチだけを更新する安全な確認の段階で、pullはfetchとmergeを一度に実行する短縮です。
  • pushが拒否されたらリモートが先行しているという意味なので、pullで合流させてからもう一度上げます。

取り消しはまだ扱っていません。間違えてaddしたファイル、たった今作ったコミットの誤字、すでにpushまで終えたコミットのバグは、それぞれ取り消し方が違います。次の記事「Git基礎 #5 取り消し — restore・reset・revertの使い分け」で、3つのコマンドがそれぞれいつ必要なのかを整理します。

X