git checkout理解してなかった

# TL;DR

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

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

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

# 準備

## Definition

[Pro git 2nd Edition](https://git-scm.com/book/en/v2)から:

+ 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)[^head]

+ 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)

[^head]: コミットから見るとHEADはポインタのポインタってやつになります。

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

包含関係的には、`local branch` $\supset$ `remote-tracking branch` $\supset$ `upstream branch` です[^supset]。`tracking branch`と`upstream branch`は対の関係ね。[^supplement]

[^supplement]: `tracking branch`と`upstream branch`については、補足の節に書きました。

[^supset]: 厳密には、(`local branch`全体の集合) $\supset$ (`remote-tracking branch`全体の集合) $\supset$ (`upstream branch`全体の集合)です。

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

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

# 本題

## Sample

+ リモートレポジトリ https://github.com/knknkn1162/git_test にmasterとfix-readmeリモートブランチがある状態。( graph: https://github.com/knknkn1162/git_test/network )

## Version

```
% git --version
git version 2.16.2
```

# git cloneと状態の確認

```bash
# 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 origiｎ
* 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も存在しうる

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

```bash
# ブランチ名を(わざと)`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`ブランチを作成したい。


```bash: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`には２つの用法があるということだ！
一つは、HEADの移動のみを伴うコマンドとして。もう一つは、`git checkout -b fix-readme origin/fix-readme`のエイリアスとして。

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

[^tracking]: [Pro git 2nd Edition](https://git-scm.com/book/en/v2)のp.87が詳しい。

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

+ もし、`-b`オプションを付けてしまって、`git checkout -b fix-readme`とすると、全く違ったブランチが切られてしまう：

```bash
# 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-readme`が`tracking branch`に変化するわけではない：

```bash
# 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 commiｔ

% 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`とかの意味がわかったってことなので、`fetch`や`merge`とかもすぐ理解できる(と思われる)。


## まとめ

+ `git checkout <branch>`には２つの異なる意味があるよ

## 補足

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

一つは、`git pull`するときに、`tracking branch`と`upstream branch`が定められている必要がある。[^git_pull_tracking]

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

[^git_pull_tracking]: "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](https://git-scm.com/book/en/v2)のp.87に書いてある。ちなみに、`git pull`は`git fetch; git merge FETCH_HEAD`のショートカット[^git_pull_short]なんだけど、2つのコマンドを順に実行すれば、`Already up to date.`となる。

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

```bash
% 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となっている場合。

```bash
% 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`ってわけ)[^push_default]

[^push_default]: pull.defaultオプションは`pull`の仕様から存在しないです。

```bash
# 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
```