Git
checkout

git checkout理解してなかった

TL;DR

git checkout <branch>には2つの意味があるよ。

  1. HEADを<branch>ブランチに移動するだけ。<branch>ローカルブランチが存在しているときにこの振る舞いをする。

  2. git checkout -u <branch> origin/<branch>のショートカットとして。<branch>ローカルブランチが存在せず、かつorigin/<branch>というremote-tracking branchが存在するときに、この振る舞いをする。この時、<branch>は、tracking branch、origin/<branch>はupstream branchとなる。

準備

Definition

Pro git 2nd Editionから:

  • branch .. simply a lightweight movable pointer to one of commits. (p.60)

  • HEAD .. The pointer to the local branch you’re currently on. (p.61)1

  • remote branch .. The pointer in your remote repositories (for instance, repository on GitHub), including branches, tags. (p.79)

  • remote-tracking branch .. Reference to the state of remote branch. Remote-tracking branch takes the form <remote>/<branch>. It's local reference that you can’t move.(p.80) (it's also called track remote branch)

  • tracking branch .. The local branch that is checking out from a remote-tracking branch.(p.86-87)

  • upstream branch .. The remote-tracking branch that the tracking branch tracks.(p.87)

この本にはlocal branchの定義が明示されていませんでしたが、remote branchとの対義語と捉えるのが自然でしょう。つまり、The pointer in your local repositoriesとします。

包含関係的には、local branch $\supset$ remote-tracking branch $\supset$ upstream branch です2tracking branchupstream branchは対の関係ね。3

で、ちょっとややこしいんですが、local branchの意味を(local branch全体の集合 - remote-tracking branch全体の集合)と捉えてたりもします。4
ここでは厳密に区別したいので、広義のローカルブランチ、狭義のローカルブランチと名付けましょう。それで、単にlocal branchというときには、狭義のローカルブランチの意味で使うことにし、広義のローカルブランチを単にbranchということにします。

本題

Sample

Version

% git --version
git version 2.16.2

git cloneと状態の確認

# git fetch .. 最新のリモートリポジトリの情報を取得してremote-tracking branchに反映する(update)
% git clone git@github.com:knknkn1162/git_test.git # .. リモートレポジトリをローカルのディレクトリ内に複製して remote-tracking branchを作成する。(create)
Cloning into 'git_test'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 11 (delta 0), reused 11 (delta 0), pack-reused 0
Receiving objects: 100% (11/11), done

# 確認
% git branch -vv
# `master`と`origin/master`は`tracking branch`と`upstream branch`の関係。                                                                                  
* master dcfa179 [origin/master] add sample.txt
% git remote show origin
* remote origin
  Fetch URL: git@github.com:knknkn1162/git_test.git
  Push  URL: git@github.com:knknkn1162/git_test.git
  HEAD branch: master
  Remote branches:
    fix-readme tracked
    master     tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

# List both remote-tracking branches and local branches with `-a` option. (See https://git-scm.com/docs/git-branch#git-branch--a)
% git branch -a
# `local branch`は`master`, `remote-tracking branch`は`origin/fix-readme`と`origin/master`。
# `remotes/`がprefixで付いているのは、論理的には、`origin/fix-readme`という名のlocal branchも存在しうるので、
# `local branch`と`remote-tracking branch`を峻別するため。
* master
  remotes/origin/HEAD -> origin/master # 現在のリモート追跡ブランチのHEADは`origin/master`リモート追跡ブランチ
  remotes/origin/fix-readme
  remotes/origin/master

% git remote show origin
* remote origin
  Fetch URL: git@github.com:knknkn1162/git_test.git
  Push  URL: git@github.com:knknkn1162/git_test.git
  HEAD branch: master
  Remote branches:
    fix-readme tracked # origin/fix-readme というremote-tracking branchがローカルにある
    master     tracked # origin/master というremote-tracking branchがローカルにある
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

# 各ブランチの位置関係はこんな感じになっています。
% git log --graph --all
* commit 31458c1bec7bb6dcc420ffe6ca9a296a106737a7 (origin/fix-readme)
| Author: knknkn <knknkn1162@gmail.com>
| Date:   Sat Apr 14 11:27:26 2018 +0900
|
|     insert item in readme
|
| * commit dcfa17936dc786159288eb4668d4e49499d0d378 (HEAD -> master, origin/master, origin/HEAD)
| | Author: knknkn <knknkn1162@gmail.com>
| | Date:   Sat Apr 14 09:52:09 2018 +0900
| |
| |     add sample.txt
| |
| * commit 731dbcfb14fa0c7f24a9efc0d469bceb885eccbe
|/  Author: knknkn <knknkn1162@gmail.com>
|   Date:   Sat Apr 14 09:51:45 2018 +0900
|
|       fix readme
|
* commit 0cf6aa39477d9a09b2656b1f1ab86b91c17e1485
| Author: knknkn <knknkn1162@gmail.com>
| Date:   Sat Apr 14 09:49:01 2018 +0900
|
|     add README.md
|
* commit fbd0bed4c2e6f3d4dad87049f7bc0ffd61b63a26
  Author: knknkn <knknkn1162@gmail.com>
  Date:   Fri Apr 13 10:14:47 2018 +0900

      first commit

NOTE)

論理的には、origin/fix-readmeという名のlocal branchも存在しうる

っていうのは、つまりこういうことです。

# ブランチ名を(わざと)`origin/aaaaa`で作成してみる
% git branch origin/aaaaa
# 問題なく`origin/aaaaa`ローカルブランチを作成できる
% git branch -vv
* master       dcfa179 [origin/master] add sample.txt
  origin/aaaaa dcfa179 add sample.txt

% git branch -a
* master
  origin/aaaaa #<= `origin/`がついてるけど、これは、remote-tracking branchでない!
  remotes/origin/HEAD -> origin/master
  remotes/origin/fix-readme
  remotes/origin/master

checkout

このあと、remote-tracking branch(origin/fix-readme)を元に、fix-readmeブランチを作成したい。

checkout
# 現時点でfix-readmeローカルブランチは存在しないことに留意する
# HEAD の移動を伴う
% git checkout fix-readme # same as `git checkout -b fix-readme origin/fix-readme` or `git checkout --track origin/fix-readme`
Branch 'fix-readme' set up to track remote branch 'fix-readme' from 'origin'.
Switched to a new branch 'fix-readme'

# ローカルブランチfix-readme を、追跡ブランチである origin/fix-readmeからつくる
% git branch fix-readme origin/fix-readme
Branch 'fix-readme' set up to track remote branch 'fix-readme' from 'origin'.
# この後、`git checkout fix-readme`とすれば、上のコマンドと同義になる

なんとなく使ってた。


ここで、注意すべきは、git checkout local-branchには2つの用法があるということだ!
一つは、HEADの移動のみを伴うコマンドとして。もう一つは、git checkout -b fix-readme origin/fix-readmeのエイリアスとして。

前者は、すでにブランチが存在しているときにHEADを移動させるために用いるコマンド。(コチラがcheckout本来の用法)
後者の場合は、git branch fix-readme origin/fix-readme + git checkout fix-readme(HEADを移動させるだけ)のショートカットの意味で使われる。5

記法は全く同じだが、動作がぜんぜん違う。
以下、理解していないと、間違えやすいところ。

  • もし、-bオプションを付けてしまって、git checkout -b fix-readmeとすると、全く違ったブランチが切られてしまう:
# fix-readme作ったので、一旦消す
% git checkout master
% git branch --delete fix-readme
Deleted branch fix-readme (was 0cf6aa3).

% git checkout -b fix-readme # masterからローカルブランチを切ることに注意する。 
# 一個前の例はリモート追跡ブランチ origin/fix-readmeからローカルブランチを作成している
Switched to a new branch 'fix-readme'

% git branch -vv
* fix-readme dcfa179 add sample.txt # <= masterブランチから切られたブランチなので、fix-readmeブランチはmasterブランチと現時点で同じ
  master     dcfa179 [origin/master] add sample.txt

# fix-readme ローカルブランチと origin/fix-readme リモート追跡ブランチが離れていることに注意する
% git log --graph
* commit 31458c1bec7bb6dcc420ffe6ca9a296a106737a7 (origin/fix-readme)
| Author: knknkn <knknkn1162@gmail.com>
| Date:   Sat Apr 14 11:27:26 2018 +0900
|
|     insert item in readme
|
| * commit dcfa17936dc786159288eb4668d4e49499d0d378 (HEAD -> fix-readme, origin/master, origin/HEAD, master)
| | Author: knknkn <knknkn1162@gmail.com>
| | Date:   Sat Apr 14 09:52:09 2018 +0900
| |
| |     add sample.txt
| |
| * commit 731dbcfb14fa0c7f24a9efc0d469bceb885eccbe
|/  Author: knknkn <knknkn1162@gmail.com>
|   Date:   Sat Apr 14 09:51:45 2018 +0900
|
|       fix readme
|
* commit 0cf6aa39477d9a09b2656b1f1ab86b91c17e1485
| Author: knknkn <knknkn1162@gmail.com>
| Date:   Sat Apr 14 09:49:01 2018 +0900
|
|     add README.md
|
* commit fbd0bed4c2e6f3d4dad87049f7bc0ffd61b63a26
  Author: knknkn <knknkn1162@gmail.com>
  Date:   Fri Apr 13 10:14:47 2018 +0900

      first commit
  • もし、fix-readmeブランチがすでに存在していたら、git checkout fix-readmeとしても、HEADが切り替わるだけでfix-readmetracking branchに変化するわけではない:
# fix-readme作ったので、一旦消す
% git checkout master
% git branch --delete fix-readme
Deleted branch fix-readme (was 0cf6aa3).
% git branch fix-readme
MOB18006:git_test % git branch -vv                                                                                   (git)-[master]
  fix-readme dcfa179 add sample.txt # fix-readmeは、tracking branchでなく、ただのbranch
* master     dcfa179 [origin/master] add sample.txt # 対してmasterはtracking branch

# HEADがmasterにあることの確認
% git log --graph --all --oneline
* 31458c1 (origin/fix-readme) insert item in readme
| * dcfa179 (HEAD -> master, origin/master, origin/HEAD, fix-readme) add sample.txt
| * 731dbcf fix readme
|/
* 0cf6aa3 add README.md
* fbd0bed first commit

% git checkout fix-readme
Switched to branch 'fix-readme'
# HEADがfix-readmeに移動したことの確認
% git log --graph --all --oneline
* 31458c1 (origin/fix-readme) insert item in readme
| * dcfa179 (HEAD -> fix-readme, origin/master, origin/HEAD, master) add sample.txt #<= HEADが移動しただけ
| * 731dbcf fix readme
|/
* 0cf6aa3 add README.md
* fbd0bed first commit

% git branch -vv
* fix-readme dcfa179 add sample.txt # fix-readmeは、tracking branchでなく、ただのbranch
  master     dcfa179 [origin/master] add sample.txt

どちらの用法でgit checkout fix-readmeが使われているのかを把握しておかないと、各ブランチの位置関係が想定とは全く異なってしまう。


mergeとかrebaseとか難しいなぁ、って思ってたんだけど、そもそも、ちゃんとcheckout理解してなかった。
逆に言うと、checkout理解できたってことは、remote-tracking branchとかlocal branchとかtracking branchとかupstream branchとかの意味がわかったってことなので、fetchmergeとかもすぐ理解できる(と思われる)。

まとめ

  • git checkout <branch>には2つの異なる意味があるよ

補足

tracking branchupstream branchについて、言葉の定義だけ述べて存在意義を確認していなかったので、改めて補足。

一つは、git pullするときに、tracking branchupstream branchが定められている必要がある。6

これを確かめるために、fix-readmeがただのローカルブランチの場合(つまり、git checkout -b fix-readme-bオプションをつけてチェックアウトする)、pullが失敗することを確認しよう。

% git branch
* master
* 
# fix-readmeがtracking branchにならないように、新規に`fix-readme`ブランチをmasterブランチから作成する
% git checkout -b fix-readme
Switched to a new branch 'fix-readme'

# 現状の確認
% git branch -vv
* fix-readme dcfa179 add sample.txt
  master     dcfa179 [origin/master] add sample.txt
% git log --graph --all
* commit 31458c1bec7bb6dcc420ffe6ca9a296a106737a7 (origin/fix-readme)
| Author: knknkn <knknkn1162@gmail.com>
| Date:   Sat Apr 14 11:27:26 2018 +0900
|
|     insert item in readme
|
| * commit dcfa17936dc786159288eb4668d4e49499d0d378 (HEAD -> fix-readme, origin/master, origin/HEAD, master)
| | Author: knknkn <knknkn1162@gmail.com>
| | Date:   Sat Apr 14 09:52:09 2018 +0900
| |
| |     add sample.txt
| |
| * commit 731dbcfb14fa0c7f24a9efc0d469bceb885eccbe
|/  Author: knknkn <knknkn1162@gmail.com>
|   Date:   Sat Apr 14 09:51:45 2018 +0900
|
|       fix readme
|
* commit 0cf6aa39477d9a09b2656b1f1ab86b91c17e1485
| Author: knknkn <knknkn1162@gmail.com>
| Date:   Sat Apr 14 09:49:01 2018 +0900
|
|     add README.md
|
* commit fbd0bed4c2e6f3d4dad87049f7bc0ffd61b63a26
  Author: knknkn <knknkn1162@gmail.com>
  Date:   Fri Apr 13 10:14:47 2018 +0900

      first commit

% git pull
There is no tracking information for the current branch. # <= fix-readmeはtracking branchでないため、pullできない
Please specify which branch you want to merge with.
See git-pull(1) for details.

    git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=origin/<branch> fix-readme

対して、fix-readmeがtracking branchとなっている場合。

% git branch --delete fix-readme
Deleted branch fix-readme (was dcfa179).

% git checkout fix-readme
Branch 'fix-readme' set up to track remote branch 'fix-readme' from 'origin'.
Switched to a new branch 'fix-readme'

% git branch -vv
# fix-readmeとorigin/fix-readmeがtracking branchとupstream branchの関係で対応している
* fix-readme 31458c1 [origin/fix-readme] insert item in readme
  master     dcfa179 [origin/master] add sample.txt

% git pull
Already up to date. # 今回は、更新されたorigin/fix-readmeとfix-readmeが同じなので。

Note) ちなみに、pushに関しても同様のことが起こる。詳しくは、https://git-scm.com/docs/git-config#git-config-pushdefault を読んでください。(要するに、fix-readmeをtracking branchに、origin/fix-readmeをupstream branchにしなきゃpushできないってオプションがpush.default = simpleってわけ)7

# git clone git@github.com:knknkn1162/git_test.git
% git checkout -b fix-readme
Switched to a new branch 'fix-readme'
% git branch -vv
* fix-readme dcfa179 add sample.txt # fix-readmeはただのlocal-branchであって、tracking-branchではない。
  master     dcfa179 [origin/master] add sample.txt
% git push
fatal: The current branch fix-readme has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin fix-readme
# remote branch(origin)を指定しても同じ。
% git push origin
fatal: The current branch fix-readme has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin fix-readme



# ちなみに、この話は`git push`のローカルブランチの引数がない場合の挙動のことを言っている。
# `git push origin fix-readme`と明示的に書けば、下記のようにrejectされる:

# `fix-readme`は`origin/fix-readme`の子孫でないので、pushがrejectされる
% git log --graph --all
* commit 31458c1bec7bb6dcc420ffe6ca9a296a106737a7 (origin/fix-readme)
| Author: knknkn <knknkn1162@gmail.com>
| Date:   Sat Apr 14 11:27:26 2018 +0900
|
|     insert item in readme
|
| * commit dcfa17936dc786159288eb4668d4e49499d0d378 (HEAD -> fix-readme, origin/master, origin/HEAD, master)
| | Author: knknkn <knknkn1162@gmail.com>
| | Date:   Sat Apr 14 09:52:09 2018 +0900
| |
| |     add sample.txt
| |
| * commit 731dbcfb14fa0c7f24a9efc0d469bceb885eccbe
|/  Author: knknkn <knknkn1162@gmail.com>
|   Date:   Sat Apr 14 09:51:45 2018 +0900
|
|       fix readme
|
* commit 0cf6aa39477d9a09b2656b1f1ab86b91c17e1485
| Author: knknkn <knknkn1162@gmail.com>
| Date:   Sat Apr 14 09:49:01 2018 +0900
|
|     add README.md
|
* commit fbd0bed4c2e6f3d4dad87049f7bc0ffd61b63a26
  Author: knknkn <knknkn1162@gmail.com>
  Date:   Fri Apr 13 10:14:47 2018 +0900

      first commit

% git push origin fix-readme
To github.com:knknkn1162/git_test.git
 ! [rejected]        fix-readme -> fix-readme (non-fast-forward)
error: failed to push some refs to 'git@github.com:knknkn1162/git_test.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details

  1. コミットから見るとHEADはポインタのポインタってやつになります。 

  2. 厳密には、(local branch全体の集合) $\supset$ (remote-tracking branch全体の集合) $\supset$ (upstream branch全体の集合)です。 

  3. tracking branchupstream branchについては、補足の節に書きました。 

  4. 例えば、https://git-scm.com/docs/git-branch#git-branch--a ではローカルブランチを本記事の「狭義のローカルブランチ」の意味で用いています。 

  5. Pro git 2nd Editionのp.87が詳しい。 

  6. "If you’re on a tracking branch and type git pull, Git automatically knows which server to fetch from and which branch to merge in."と,Pro git 2nd Editionのp.87に書いてある。ちなみに、git pullgit fetch; git merge FETCH_HEADのショートカット8なんだけど、2つのコマンドを順に実行すれば、Already up to date.となる。 

  7. pull.defaultオプションはpullの仕様から存在しないです。 

  8. https://git-scm.com/docs/git-pull#_description に記載されている