Git入門:バージョン管理のきほん
clone と fetch の違い
このレッスンで分かること
- clone が「まるごと複製 + リモート登録 + 初回チェックアウト」をまとめてやっていること
origin/mainというリモート追跡ブランチが何を記録しているか- fetch は取ってくるだけで手元に触らない、という境界
clone がやっている 3 つのこと
他人のリポジトリや、GitHub 上に作った自分のリポジトリを手元に持ってくるとき、最初に打つのが git clone です。
ターミナル
$ git clone git@github.com:example/myapp.git
Cloning into 'myapp'...
remote: Enumerating objects: 142, done.
remote: Counting objects: 100% (142/142), done.
remote: Compressing objects: 100% (89/89), done.
Receiving objects: 100% (142/142), 24.31 KiB | 4.86 MiB/s, done.
Resolving deltas: 100% (48/48), done.1 行のコマンドですが、内部では 3 つのことが順に行われています。
myappというディレクトリを作り、Git リポジトリとして初期化する- リモートの全履歴をダウンロードし、そのリモートに
originという別名を登録する - 既定のブランチ (多くの場合
main) をチェックアウトして、作業ファイルを並べる
3 番まで済んでいるので、clone した直後からファイルが見える状態になっています。
確認してみます。
ターミナル
$ cd myapp
$ git remote -v
origin git@github.com:example/myapp.git (fetch)
origin git@github.com:example/myapp.git (push)
$ git log --oneline -3
8a2f4e6 (HEAD -> main, origin/main, origin/HEAD) 検索 API のレスポンスを整形
3b9c0d1 ユーザー一覧のページング
1a2b3c4 プロジェクトの初期設定origin が自動で登録されている点に注目してください。前のレッスンで git remote add を手で打ったのは、git init で自分で作ったリポジトリの場合です。clone した場合は不要です。
clone は最初の 1 回だけです。 「リポジトリを更新したい」と思って再び clone すると、別のディレクトリにまったく別のコピーができます。手元の変更も設定も引き継がれません。2 回目以降は fetch か pull を使います。
clone のときに指定できること
そのままでは、URL の末尾がディレクトリ名になります。別の名前にしたければ、URL のあとに書きます。
ターミナル
$ git clone git@github.com:example/myapp.git my-work
Cloning into 'my-work'...既定以外のブランチをチェックアウトした状態で始めたいときは -b を使います。
ターミナル
$ git clone -b develop git@github.com:example/myapp.git履歴が長いリポジトリで、最新の状態だけあれば足りる場合は --depth 1 をつけると、ダウンロード量が大きく減ります。
ターミナル
$ git clone --depth 1 git@github.com:example/myapp.gitただしこれは過去のコミットを持ってこないので、git log はほぼ何も表示せず、古いコミットとの比較もできません。自動テストの実行環境など、履歴が要らない用途に限って使ってください。
origin/main は何者か
さきほどの git log の出力に、見慣れない名前がありました。
プレーンテキスト
8a2f4e6 (HEAD -> main, origin/main, origin/HEAD) 検索 API のレスポンスを整形main と origin/main の 2 つが同じコミットを指しています。この 2 つは別物です。
origin/main はリモート追跡ブランチと呼ばれます。役割は 1 つで、最後にリモートと通信したとき、リモートの main がどのコミットを指していたかを記録することです。
| 名前 | 何を指すか | 誰が動かすか |
|---|---|---|
main | 手元の作業ブランチ | 自分がコミットしたとき進む |
origin/main | 最後に見たリモートの main の位置 | fetch や pull をしたときだけ進む |
大事なのは、origin/main がリモートを見にいっているわけではないという点です。ネットワークの向こうを覗く窓ではありません。手元のディスクに保存された、前回の通信結果のメモです。
だから、あなたがオフラインでも origin/main は読めます。そして、他の人が GitHub に push しても、あなたが fetch するまで origin/main は動きません。表示されている情報が古い可能性は常にあります。
一覧してみると、手元のブランチとリモート追跡ブランチが分かれて並びます。
ターミナル
$ git branch
* main
feature-login
$ git branch -r
origin/HEAD -> origin/main
origin/main
origin/feature-search
$ git branch -a
* main
feature-login
remotes/origin/HEAD -> origin/main
remotes/origin/main
remotes/origin/feature-search-r がリモート追跡ブランチだけ、-a が両方です。ここで origin/feature-search が見えているのは、誰かが GitHub 上でそのブランチを作り、あなたがそれを fetch したからです。
他の人が作ったブランチで作業したくなったら、その名前でブランチを作ります。
ターミナル
$ git switch feature-search
branch 'feature-search' set up to track 'origin/feature-search'.
Switched to a new branch 'feature-search'手元に無いブランチ名を git switch に渡すと、同じ名前のリモート追跡ブランチを探して、それを元にした作業ブランチを自動で作ってくれます。上流ブランチの設定まで済むので、以後は引数なしの push と pull が使えます。
リモート追跡ブランチは読み取り専用です。
origin/mainに直接コミットすることはできません。切り替えようとすると detached HEAD になります。あくまで記録用の目印だと考えてください。
fetch は取ってくるだけ
ここがこのレッスンの核心です。
ターミナル
$ git fetch
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
Unpacking objects: 100% (5/5), 638 bytes | 638.00 KiB/s, done.
From github.com:example/myapp
8a2f4e6..c7d1b93 main -> origin/main最後の行が起きたことのすべてです。origin/main が 8a2f4e6 から c7d1b93 に進みました。
そして、それ以外は何も変わっていません。
- 手元の
mainは8a2f4e6のままです - 作業中のファイルは 1 文字も書き換わっていません
- 編集途中のファイルがあっても、fetch は文句を言いません
確認します。
ターミナル
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree cleanGit が「リモートのほうが 1 コミット進んでいます。取り込むなら pull してください」と教えてくれています。取り込むかどうかは、まだあなたが決めていない状態です。
前のレッスンで見た通り、git pull はこの fetch に続けて merge まで実行します。境界はこうです。
| コマンド | リモートの情報を取ってくる | 手元のブランチを進める | 作業ファイルを書き換える |
|---|---|---|---|
git fetch | する | しない | しない |
git pull | する | する | する |
fetch は取ってくるだけ、pull は取ってきて合流まで。 この一文を覚えておけば、どちらを打つべきか迷いません。
fetch してから中身を見る
fetch の価値は、取り込む前に相手の変更を確認できることにあります。
まず、差を見ます。
ターミナル
$ git log --oneline main..origin/main
c7d1b93 パスワードの最小文字数を 12 に変更main..origin/main は「main には無くて origin/main にあるコミット」という指定です。これから降ってくるものが分かります。
中身も見られます。
ターミナル
$ git diff main origin/main
diff --git a/src/validators/password.js b/src/validators/password.js
index 4a8c1e2..9f3b7d0 100644
--- a/src/validators/password.js
+++ b/src/validators/password.js
@@ -1,5 +1,5 @@
export function validatePassword(value) {
- if (value.length < 8) {
+ if (value.length < 12) {
return "パスワードが短すぎます";
}自分がいまその同じファイルを編集していると分かれば、pull する前に手を打てます。コミットしてから pull する、あるいは stash で退避してから pull する、という判断ができます。
いきなり pull すると、この判断の機会がありません。コンフリクトの解決作業に突然放り込まれます。
長く作業していない日の朝は、いきなり
git pullするよりgit fetchを打ってgit log main..origin/mainを眺めるほうが安全です。何が変わったかを知ってから取り込むだけで、事故はかなり減ります。
リモートで消えたブランチを掃除する
しばらく開発を続けると、git branch -r の一覧がどんどん増えていきます。マージ済みで GitHub 上からは削除されたブランチが、手元には残り続けるからです。
fetch は新しい情報を追加しますが、消えたものを勝手には消しません。掃除するには明示します。
ターミナル
$ git fetch --prune
From github.com:example/myapp
- [deleted] (none) -> origin/feature-search毎回つけたければ設定できます。
ターミナル
$ git config --global fetch.prune true--prune が消すのは origin/feature-search というリモート追跡ブランチだけです。手元に feature-search という作業ブランチを持っていれば、それはそのまま残ります。安全な操作です。
やりがちな失敗
いちばん多いのは、fetch だけして取り込んだつもりになることです。
ターミナル
$ git fetch
From github.com:example/myapp
8a2f4e6..c7d1b93 main -> origin/mainこの出力を見て「取ってきた」と思い、そのまま作業を続ける。しかし手元の main は動いていないので、他の人の修正は反映されていません。数時間後に push しようとして弾かれ、大きくなった差分を解決するはめになります。
fetch の後に、必ず git status を打つ習慣をつけてください。behind と出ていれば、まだ取り込んでいません。
もうひとつは、clone を更新手段だと思う失敗です。「最新にしたいから clone し直す」というやり方は、手元のコミットしていない作業を丸ごと置き去りにします。しかも古いディレクトリが残るので、どちらで作業していたのか分からなくなります。手元にリポジトリがあるなら、更新は必ず fetch か pull です。
| やりたいこと | 使うもの |
|---|---|
| リポジトリを初めて手元に持ってくる | git clone |
| リモートに何があるか知りたい | git fetch |
| リモートの変更を自分のブランチに入れたい | git pull |
| 消えたリモートブランチの記録を掃除する | git fetch --prune |
- clone は複製とリモート登録と初回チェックアウトをまとめてやる。使うのは最初の 1 回だけ
origin/mainは最後に通信したときのリモートの位置を記録した読み取り専用の目印で、リアルタイムのリモートではない- fetch は取ってくるだけで、手元のブランチにも作業ファイルにも触らない。pull は合流までやる
- fetch のあとは
git statusとgit log main..origin/mainで中身を確かめてから取り込む