私の頭の中のGit

「Gitは概念だ」そんなことを弊社のが言ってました。

じゃあ概念理解しようってのがこの記事の目的なんですが、なんか世間に出回ってるGitの説明ってむずかしくないですか?
僕の頭なんてメロンパンみたいなのしか入ってないのでもっと簡単に言ってもらわないと困ります(逆切れ)

ということで、私がGitで一番大事だとおもうリファレンス(参照)について書いてみようと思います。
Gitはリファレンスがわかっていることを前提にした動作をするのが多々あるので、理解しておきましょう。

リビジョンとかブランチとかそういった簡単な単語は知ってる前提で進んでいくのでご了承ください。

概要

リファレンス(ref)とはリビジョンを指し示すものでラベルだとかポインタという言い方のほうがしっくりくるかもしれませんね(ここではリファレンスと言い続けますが)。
リファレンスは一つのリビジョンに紐づきます。とったりつけたりは自由自在です。

そして、Gitはこのリファレンスから遡れるリビジョンしか見せてくれません。

$ git log --all --oneline --graph --decorate=full
* f762a9f (tag: refs/tags/tag5) 5
| * 1b364d7 (refs/heads/c) 4
|/
| * e2377a8 (refs/heads/b) 3
|/
| * 7e229a6 (refs/heads/a) 2
|/
* cfe5cc5 (HEAD -> refs/heads/master) 1

言い換えるとリファレンスから遡れないリビジョンは存在しても見えません。
Gitはこれを利用して(リファレンスを差し替えることで)リビジョンの履歴改変を実現しているんですね(以下イメージ図)。

git-reference-image

でもディスク領域とるし邪魔なので辿れないリビジョンは定期的に消してくれます。(ガベージコレクション)
なんらかの操作をして、すでにあったリビジョンが消えたように見えてもガベージコレクションが行われる前であればローカルに残っているので救出できるという訳です。
で、救出するためのコマンドが「git reflog」と呼ばれるコマンドで、「リファレンスのログ」を閲覧するコマンドです。

$ git reflog
cfe5cc5 HEAD@{0}: checkout: moving from f762a9fa3b708ef7ca14a38d17be5df19dbb5811 to master
f762a9f HEAD@{1}: checkout: moving from master to tag5
cfe5cc5 HEAD@{2}: reset: moving to HEAD^
f762a9f HEAD@{3}: commit: 5
cfe5cc5 HEAD@{4}: checkout: moving from c to master
1b364d7 HEAD@{5}: commit: 4
cfe5cc5 HEAD@{6}: checkout: moving from master to c
cfe5cc5 HEAD@{7}: checkout: moving from b to master
e2377a8 HEAD@{8}: commit: 3
cfe5cc5 HEAD@{9}: checkout: moving from master to b
cfe5cc5 HEAD@{10}: checkout: moving from a to master
7e229a6 HEAD@{11}: commit: 2
cfe5cc5 HEAD@{12}: checkout: moving from master to a
cfe5cc5 HEAD@{13}: commit (initial): 1

じゃあリファレンスって具体的に何なのかといえばそれはタグブランチです。
何が違うのか見ていきましょう。

タグ

タグは特定リビジョンにつける永続的なリファレンスです。
特定のリビジョンに対してリリースバージョン等、目印として利用するのが主目的となります。

ブランチ

バージョン管理システムにはブランチという欠かせない概念があり、もちろんGitにもブランチはあります。
Gitは前述したとおりブランチをリファレンスによって表現します。
ブランチというと分岐したすべてのリビジョンと捉えがちですが、Gitにおけるブランチはリファレンスがつけられた一点(リビジョン)だけです。
一連のリビジョン全てをブランチと指すのは、個人的には変だとは思いませんが人によってはおかしいと思う人がいるかもしれませんね。

そして、一つのリビジョンがもつリファレンスの数に制限はないため、一つのリビジョンに複数のブランチが同居するという事態はよく発生します。

リモート追跡ブランチ

ブランチの種類は「リモートブランチ」「リモート追跡ブランチ」「ローカル」に分けられます。いずれもリファレンスであることに変わりはありません。
で、ローカルは説明不要でしょう。
リモートブランチは、例えばGithub等のリモートに存在するブランチで手元のコミットグラフには表示されません。

私たちが見ているのはリモートブランチ(リファレンス)の写しということになります。これがリモート追跡ブランチです。
ただし常にリモートサーバに接続しているわけじゃないので時間の経過とともにどんどんずれていきます。
同期するためのコマンドがfetchやpullです。

ちなみにリモート追跡ブランチはローカルブランチと同居することが正常な状態といえます。
例えば、ローカルブランチがリモート追跡ブランチよりも進んでいる場合「1 ahead」のように表示され、遅れている場合は「2 behind」と表示されます。(数字部分は差分なので可変)
ちなみにリモート追跡ブランチの名称は私たちが自由に設定することはできず「リモート名/ブランチ名」という書式になります。
でもブランチ名にスラッシュ使えるんですよねー。スラッシュ区切りにするとディレクトリで区切られます。でも混乱するのであんまりやらないほうがいいかと。

$ git checkout -b origin/slash
Switched to a new branch 'origin/slash'

$ git commit --allow-empty -m 'create origin/slash'
[origin/slash 7ca4242] create origin/slash

$ git push origin origin/slash
Counting objects: 1, done.
Writing objects: 100% (1/1), 183 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
remote:
remote: Create pull request for origin/slash:
remote:
 * [new branch]      origin/slash -> origin/slash

$ git branch --all
  master
* origin/slash
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/origin/slash

$ git checkout -b slash
Switched to a new branch 'slash'

$ git commit --allow-empty -m 'create slash'
[slash 9d1fe10] create slash


$ git push origin slash
Counting objects: 1, done.
Writing objects: 100% (1/1), 178 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
remote:
remote: Create pull request for slash:
remote:
 * [new branch]      slash -> slash

$ git branch --all
 master
 origin/slash
* slash
 remotes/origin/HEAD -> origin/master
 remotes/origin/master
 remotes/origin/origin/slash
 remotes/origin/slash

$ git checkout origin/slash
warning: refname 'origin/slash' is ambiguous.
Switched to branch 'origin/slash'

$ git branch --all
 master
* origin/slash
 slash
 remotes/origin/HEAD -> origin/master
 remotes/origin/master
 remotes/origin/origin/slash
 remotes/origin/slash

ローカルブランチとリモート追跡ブランチの名称が重複した場合、ローカルブランチが優先されるようですね。

で、リファレンスとしてのブランチとタグの違いは一言でいうと永続化を目的とするかどうかという点にあります。
私が言う永続化は以下の2つの意味を指しています。

リファレンスの追加削除

タグもブランチも追加削除可能ですし運用ルールによりますが、基本的にタグは一度つけたらそのままで頻繁に取り外しされるようなものではありません。
逆にブランチは一部の永続ブランチを除いて、目的ごとにブランチを作成、目的を達したら削除するのが一般的です。
一部の永続ブランチというのも特に決まっているわけではなく、運用ルールによりますが、一番基本的なものはGitのデフォルトブランチであるmasterでしょう。

リファレンスの移動

Gitでは常に何らかのブランチに属していますが、その状態からコミットするとブランチの位置は最新のリビジョンに移ります。
内部的にはローカルブランチはheads/ディレクトリに入っています。先端を意味するので新しいリビジョンができたら移動するということですね。
対する「タグ」はコミットで動きません。外さない限りは同じリビジョンを見続けます。リベースしても動きません。

–force

PUSH/PULL/FETCH(リモートリポジトリへの変更送受信)はリモートリポジトリ、かつ、ブランチ単位で行います。(–allオプションですべて)
そしてリビジョンは親(前)リビジョンへのポインタを持っています。
リモートリポジトリがローカルからリビジョンを受け取るとき、そのリビジョンがリモート側のリビジョンのブランチとしてポインタをたどれる必要があります。

rebaseを行うとリファレンスも移動されるため、別のリビジョンとして判断されPUSHの際にエラーが発生します。
PUSHの際に「–force」を設定することで受け取ったリビジョンを新たなブランチとして判断してくれます。

fast-forward(ファストフォワード)

単にリファレンスを進めることで変更差分を取り込むマージ方法をfast-forwardといいます。
この方法だとマージコミットが発生しないためコミットグラフがきれいになりますが、
逆にマージしたという事実をコミットとして残したい場合は–no-ff(non fast-forward)オプションをマージの際に指定する必要があります。

fast-forwardでマージできる条件は決まっています。
コミットグラフ的に言えば、マージ対象コミットから過去に遡った直線状に現コミットがいる場合です。

これがわかりにくい場合はリビジョンを集合として見れば現コミットの祖先リビジョン集合がマージ対象の祖先リビジョン集合にすべて含まれていると考えてみましょう。

git-revision-set

上記の例だとmasterブランチはtopicブランチにもhotfixブランチにもfast-forward可能です。要素を取り込んで同じ集合になるイメージですね。
逆にこの場合hotfixブランチからtopicブランチに、またはtopicブランチからhotfixブランチにfast-forwardマージはできません。

操作については以下の書籍を見れば大抵のことはなんとかなると思います。

とりあえずこんなところで。
間違ってるところがあったら指摘ください。

参考リンク
Gitのリファレンス(ref)がちょっと理解できてきたという話