git pullの詳細な挙動を追ってみる
git push/pullは何気なく使ってるけど実はよくわかってなかった。ことのきっかけはこういう質問。
$ git pull origin hoge:hoge
でもこれは間違えで、なぜか今いるブランチ(master)にhogeがmergeされるし、期待してる動作じゃない。正解はこう。
$ git branch hoge origin/hoge
もしくはチェックアウトも同時にするなら
$ git checkout -b hoge origin/hoge
こう。自分は普段後者のやり方でやってたけど、なんで上のはダメで下のが正解なのか説明できなかったのでちゃんと調べてみた。
ブランチ名の解決
まず、git pullを知る前に、いくつか知っとかないといけないことがある。まずはブランチ名の解決から。
通常ローカルブランチはこんな感じでつくる。
$ git branch hoge
そうするとhogeというローカルブランチができて
$ git show hoge
とかするとhogeのブランチを参照できる。でもこのshowで使ってるhogeという名前は実は省略形式で、正式な名称は refs/heads/hoge になる。
なのでこの二つのコマンドを実行してみると同じ結果になるはず。
$ git show master $ git show refs/heads/master
基本的にブランチとタグの正式な形式は次のようになる。
- refs/heads/
# ローカルブランチ - refs/remotes/
/ # 追跡ブランチ - refs/tags/
# タグ
- refs/
- refs/tags/
- refs/heads/
- refs/remotes/
- refs/remotes/
/HEAD
の順番でそのブランチ(もしくはタグ)があるか探す。これは
$ git help rev-parse
を見ると確認できる。なので単にmasterと指定したら
- refs/master
- refs/tags/master
- refs/heads/master
の順番で探して、refs/heads/masterがマッチするので無事期待通りの挙動になる。ので仮にtagにmasterという名前をつけると大変なことになる。(一応警告はでるみたい)
ちなみにこの情報は .git/refs/* においてある。
traking branch
追跡ブランチともいう。これは何かというと、リモートリポジトリのブランチを追跡するためだけのブランチ。
基本的にこのブランチにコミットとかマージはしちゃダメ。pushとかpull(正確にはpullじゃなくてfetchのとき)したときに勝手に最新の情報に更新されるようになってるという特殊なブランチ。
特に何も設定してなくても、どっかからcloneしたリポジトリのブランチを見ると
$ git branch -a * master remotes/origin/master
とかって出るんじゃなかろうかと思う。masterってのが普通のローカルブランチ、remotes/origin/masterってのが追跡ブランチ。わかりにくいけど、追跡ブランチもローカルのブランチになる。remotes/origin/master ってのはつまり、「originっていうリモートリポジトリのmasterブランチを追跡しているローカルブランチ」ってこと。正式名称は refs/remotes/origin/master。git branchコマンドでは refs/ が省略された形式で出力されてるだけ。
単にpush、pullするだけの簡単なお仕事ならこの追跡ブランチを意識することはほとんどないけど、今回明らかにしたい挙動ではこれが重要になる。言葉を整理するために、今後は次のように使う。
- トピックブランチ (ローカルにある通常のブランチ)
- 追跡ブランチ(ローカルにある追跡ブランチ)
- リモートブランチ(リモートリポジトリにあるブランチ)
追跡ブランチというのはリモートブランチの状態を同期するだけのブランチなので、コミットもマージもすべきではない。その証拠?に、追跡ブランチをチェックアウトしようとすると、一応チェックアウトはできるものの、detached HEADという「切り離されたHEAD」というものになってしまう。
$ git checkout origin/master Note: checking out 'origin/master'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b new_branch_name HEAD is now at 403264d... Add
detached HEADについてはこのあたりを参照のこと。
gitのHEADがブランチから外れてしまう現象とその直し方 - 西尾泰和のはてなダイアリー
git fetch
git fetchで追跡ブランチにリモートブランチのデータを反映させて、追跡ブランチをトピックブランチにmergeするってのがgit pullの流れになる。ので次はgit fetchについて説明する。
git fetchのhelpを見るとgit fetchは次のようなコマンドライン引数を受け取ることができることがわかる。
git fetch [<options>] [<repository> [<refspec>...]]
[+]<source>:<destinations>
実際に書くとこんな感じ。
+refs/heads/hoge:refs/remotes/origin/hoge
先頭の+は省略可能であってもなくてもいい(+をつけた場合はfast-forwardのチェックをしない。詳しくは入門Gitに書いてある)。
この参照の指定はワイルドカード(*)が指定できる。なのでgit fetchはこのように書ける。
$ git fetch origin '+refs/heads/*:/remotes/origin/*'
また、この「+refs/heads/*:/remotes/origin/*」ってのは通常、fetchのrefspecのデフォルト値になってる。設定ファイルを見ると
$ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true [remote "origin"] fetch = +refs/heads/*:refs/remotes/origin/* url = /Users/hokamura/tmp/git/repo [branch "master"] remote = origin merge = refs/heads/master
この「fetch = +refs/heads/*:refs/remotes/origin/*」っていうのが、originをfetchするときのrefspecのデフォルト値になる。なので
$ git fetch
ってやると
$ git fetch origin '+refs/heads/*:refs/remotes/origin/*'
ってやったのと同じことになる(
例えば、リモートに新しくhogeというブランチができててfetchしたら次のようになる。
$ git fetch remote: Counting objects: 3, done. remote: Compressing objects: 100% (2/2), done. remote: Total 2 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (2/2), done. From /Users/hokamura/tmp/git/repo * [new branch] hoge -> origin/hoge
「hoge -> origin/hoge 」ってのがリモートのhogeっていうブランチをローカルのorigin/hogeっていう追跡ブランチに落としてきたよって意味。なので
$ git branch -a * master remotes/origin/hoge remotes/origin/master
と、追跡ブランチが増えていることがわかる。
ここまでで、最初のコマンド
$ git branch hoge origin/hoge
が正しいというのがわかる。これは origin/hoge という追跡ブランチを元にして hoge というトピックブランチをつくるということ。これが最初の例で望んでいた挙動。ただし、最初の例ではこれが正しいと言ったけど、fetchやpull、cloneなどでhogeリモートブランチの追跡ブランチ(origin/hoge)がローカルに作成されているという前提条件が抜けていたのに注意。
git pull
fetchだけでは追跡ブランチを同期させるだけなので、トピックブランチには反映されない点に注意が必要。トピックブランチにも反映させるには追跡ブランチをトピックブランチにmergeなりrebaseなりをして反映させる必要がある。
git pull は git fetch の後に git merge も続けてやってくれるコマンド。git pull も git fetch と同じように、
git pull [options] [<repository> [<refspec>...]]
という引数を取る。また、fetchと同じく、refspecに何も指定しなければ次の設定を使ってくれる。
[remote "origin"] fetch = +refs/heads/*:refs/remotes/origin/*
つまり
$ git pull
ってやると、
$ git fetch origin '+refs/heads/*:refs/remotes/origin/*'
という処理がまず走る。つぎにマージ。
$ git pull $ git pull origin '+refs/heads/*:refs/remotes/origin/*'
fetchのときはこれは同じ意味だったけど、pullのときはmergeの段階で上の二つは別の挙動になる。refspecを明示的に指定した場合は、refspecの左側(リモートブランチ)を全て今チェックアウトしてるブランチ(カレントブランチ)にマージする。
$ git pull origin '+refs/heads/hoge:refs/remotes/origin/hoge'
とかするとリモートのhogeブランチを追跡ブランチにfetchした後、カレントブランチにマージするということになる。
refspec を指定しない場合は何がマージされるのかは、設定ファイルに書いてある。
[branch "master"] remote = origin merge = refs/heads/master
というのが設定ファイルに書かれてるはず。これは refspec を指定しないで git pull したときに、カレントブランチにリモートブランチのmasterをマージするという意味。(refs/heads/master はリモートブランチを指している)。つまり、masterがカレントブランチのときに
$ git pull
ってやると、
$ git fetch origin '+refs/heads/*:refs/remotes/origin/*' $ git merge origin/master
ってやったのと同じことになるということ。やっと git pull の流れが見えてきた。
refspecの省略表記
refspecの指定で補足。refspecは
[+]<source>:<destinations>
って書いたけど、:
A parameter without a colon is equivalent to : when pulling/fetching, so it merges into the current branch without storing the remote branch anywhere locally
つまりコロンなしで指定すると : って指定したのと同じ意味になり、 をカレントブランチにマージしてリモートブランチをローカルに保存しないってことらしい(追跡ブランチもつくらない)。
これがどういうときにいいかというというのは入門Gitに書いてあって、Pull Requestをマージするときに役に立つ。たとえばPull Requestを受け取った時、その人のリモートリポジトリをfetchして追跡ブランチ追加してそこからマージしてもいいんだけど、Pull Request大量に受け取る人とかはゴミブランチがいっぱい増えて大変になる(消せばいいんだけど)。そんなとき、
$ git pull <repository> <branch>
ってやるだけでカレントブランチにPull Requestのブランチがマージされて、しかもローカルにそのブランチの情報な残らないので嬉しいということ。
ちなみにpushのときのrefspceをって書くと:ではなく
:として扱われるらしく、コマンドによってそのあたりの挙動が変わるので難しい。
つまり何が言いたいかというとこの二つは等価でないということ。
$ git pull origin hoge $ git pull origin hoge:hoge
pushのときはこの二つは等価。
$ git push origin hoge $ git push origin hoge:hoge
まとめ
最初のやつは何がダメだったか。
# 今のブランチどんな感じか $ git branch -a * master remotes/origin/master # pull実行 $ git pull origin hoge:hoge remote: Counting objects: 3, done. remote: Compressing objects: 100% (2/2), done. remote: Total 2 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (2/2), done. From /Users/hokamura/tmp/git/repo * [new branch] hoge -> hoge Merge made by recursive. 0 files changed, 0 insertions(+), 0 deletions(-)
これで何が起きてるか。
まず refspec が hoge:hoge になってることに注目。コロンの左がリモートブランチで右がローカルの追跡ブランチを指定するんだった。まず左は hoge なんで解決する順番にそってリモートブランチは(おそらく) refs/heads/hoge を見つける。これは特に問題ない。次に右もhogeだけど
* [new branch] hoge -> hoge
を見てわかるとおり、ローカルブランチは refs/heads/hoge として解決されたっぽい(ここの挙動がよくわからんのだけどブランチ名を探してもなかったら refs/heads につくるのかな?)。ホントはここで追跡ブランチ、origin/hogeを作ってほしかったところ。実行後のブランチは次のようになってる。
$ git br -a hoge * master remotes/origin/master
ここまでがfetchの段階。さらに続いてmergeが始まる。mergeはリモートのhogeブランチがカレントブランチ(master)にマージされるので、期待していない動作になる。のでこのコマンドは間違えだろうと思われる。というところで調査終わり。
gitが push と pull を何も指定しなくても簡単に使えるように裏で色々設定とか勝手にやってくれてるのでいざちょっと違うことをしようとしたり、細かい挙動を把握しようと思った時に深みにはまるというのがわかった。