檜山正幸のキマイラ飼育記 このページをアンテナに追加 RSSフィード Twitter

キマイラ・サイトは http://www.chimaira.org/です。
トラックバック/コメントは日付を気にせずにどうぞ。
連絡は hiyama{at}chimaira{dot}org へ。
蒸し返し歓迎!
このブログの更新は、Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama
ところで、アーカイブってけっこう便利ですよ。

2015-09-28 (月)

もうGitは怖くない: 自信を持って使いたいあなたへ

| 08:43 | もうGitは怖くない: 自信を持って使いたいあなたへを含むブックマーク

2014初頭に書いた「WindowsにおけるGit利用環境は整った: Git for Windows と SourceTree for Windows」の最後の文:

ブランチは、Gitのなかで最も重要でありながら最も分かりにくい概念でしょう。表面的な言葉に騙されず、先入観を持たず、SourceTreeの視覚的表示(樹形図)の力を借りながら学習するのが、理解への一番の近道です。

そんへんの詳しいことはまたの機会に述べるかも知れません。

1年半以上たってしまいましたが、「またの機会」がやって来ましたよ。ええ、Gitの説明をします、ブランチを中心に詳しく。

「基礎編」と「ブランチ編」で2回に分けようかと思ったけど、長大な記事として一挙公開。これからGitを使う人が対象ではありません。Gitが何をやっているのか、自分が何をやっているのかイマイチ自信が持てない方向けです。

ブランチやマージって、なんだかよく分からないな、不安だな、Git怖いなぁ、と思っている人、大丈夫、もう怖くない。ブランチ大好きになると思うよ。

基礎編:

この記事の序文にあたるのは、

基礎編では、Gitのオブジェクトデータベースと参照システムの内部構造を理解します。

  1. Gitって怖いの?
  2. Gitは何も隠さない
  3. 横着者のためのGit実験室
  4. これが肝だ、Gitオブジェクト
  5. Gitオブジェクトデータベース
  6. 杞憂はやめよう、Gitは信頼できる
  7. オブジェクトデータベースを探る準備
  8. オブジェクトデータベースは絶対追記式
  9. オブジェクトデータベースはグラフデータベース
  10. オブジェクトグラフの中を散策してみる
  11. Gitオブジェクトの種類とGitリポジトリ内ファイルシステム

ブランチ編:

ブランチ編では、Gitのブランチの様々な側面を完全に理解します。ある程度ご存知の方は、ここから読み始めてもかまいません。必要なときに基礎編を参照すればいいでしょう。

  1. ブランチとHEAD、ラベルとしての理解
  2. ラベルの追加と削除は、どうでもいい程に軽い作業
  3. 変態リポジトリ登場
  4. AOS集合は理解しておけ
  5. AOS集合をGitに表示させる
  6. AOS集合の共通部分集合
  7. Gitが使っている集合演算:差集合と対称差集合
  8. その集合演算、答はGitに聞け
  9. やっと「枝」の概念にたどり着いたぜ
  10. 成長点としてのブランチ
  11. 有向グラフのクイズ
  12. 言い残した事など

[追記]

[/追記]

Gitって怖いの?

Gitをはじめとするバージョン管理システムは、安心とそれを背景とした勇気を手に入れるための道具です。なのに、それが「怖い」ってどういうことでしょう?

  • 「ブランチを削除」、「コミットを捨てる」、「変更を取り消す」ためのコマンドが在るってことは、保存したデータがやっぱり無くなってしまうのでは?
  • なんかヘマをしたらニッチもサッチも行かなくなって、リポジトリごと捨てるしかないのでは?
  • コンフリクトをどう処理していいか分からないから、ブランチや分散リポジトリは使いたくないよ。
  • Subversionのマージで酷い思いをした、もうコリゴリだ。

熟達した技術者は苦笑(または嘲笑)を浮かべるかも知れませんが、分散バージョン管理システムを理解することは容易じゃないです。十分なオリエンテーションなしで分散バージョン管理システムを与えられると、人はとてもトンチンカンな行動をします(「リビジョン管理システムを使える技術者はイケテいる」参照。)

得体の知れないモノは怖いんですよね。だから、Gitの正体を見極めないと、不安感・恐怖感はなかなか抜けないでしょう。愛することは知ることから始めるもんです。

Gitは何も隠さない

Gitのサブコマンドとオプションは膨大にあり、理解しがたく覚えにくく、ほんとにウンザリします。しかし、Gitコアの内部構造は唖然とするほどにシンプルです。嫌気がさすグチャグチャ・ゴタゴタを掻き分けた奥に、明快で美しい構造が立ち現われる様は感動ものです。

「この美しさを皆んなに伝えたい」とか「このシンプルさをベースに理解すべき」という気持ちはよく分かります。例えば「【翻訳】Gitをボトムアップから理解する」の最初の段落に(強調は檜山):

ボトムアップ視点で見る Git がこんなにも美しくシンプルであるなら、私が調べたことを他の人も興味を持って読んでくれるのではないか、そうして私が経験した苦労を避けられるのではと考えた。

また、id:propellaさんは2011年の時点で次のように指摘しています。「ファイルシステムとしての Git」より(強調は檜山):

Git のコマンド体系は全く歴史に学ばず後世に禍根を残す酷いデザインだが、どういうわけか内部構造は大変素晴らしい

Gitの賞賛すべき美徳のひとつは、その内部構造を一切隠してないことです。マル裸の状態、いや「イチジクの葉っぱ*1」か「貝殻*2」くらいは身にまとっているかな、という程度です。

[*3]

.git/ディレクトリの中を調べれば、Gitリポジトリの構造を覗き見できます。イチジクの葉っぱで覆っていると思えるのは、バイナリファイルがあるからです。しかし、ごく少数のGitコマンドだけでバイナリの解読もできます。

横着者のためのGit実験室

Gitを理解するには、実験のためのリポジトリを用意していじり倒すのが近道です。だけどね、実験環境を作るのが割とめんどくさいんですよね。説明や練習のためには変態なリポジトリがいいのですが、初心者がイキナリ変態にはなれません。

そこで、横着者(つまり僕自身)のために実験用リポジトリ(変態含む)を作り出すスクリプトを書きました。http://www.chimaira.org/misc/git-quest-kit.shar にシェルアーカイブが置いてあるので次のようにします。(ソースは https://github.com/m-hiyama/git-quest-kit )

$ curl http://www.chimaira.org/misc/git-quest-kit.shar > git-quest-kit.shar
$ sh ./git-quest-kit.shar

Windowsを使っている人もPOSIXシェル環境を準備してください。Git for Windowsbashコンソールが付属しているので大丈夫ですよね。

上記の2行のコマンドを実行すると、次のシェルスクリプト達が生成されます。

  1. git-inspect-functions.sh -- Gitの内部を調べるための関数(g_objectsだけ使う)
  2. setup-git-sample-1.sh -- 1番目のサンプルリポジトリを作る
  3. setup-git-sample-2.sh -- 2番目のサンプルリポジトリを作る(今回は使わない)
  4. setup-git-sample-3.sh -- 3番目のサンプルリポジトリを作る
  5. clean-samples.sh -- サンプルリポジトリを消去する

このなかの setup-git-sample-1.sh を実行(sh ./setup-git-sample-1.sh)すると、1番目の実験用のリポジトリ 1-git-sample/ が作られます。

リポジトリを視覚的に確認するためにSourceTreeもあったほうがいいでしょう。gitkや git log --graph でもいいですけど。

これが肝だ、Gitオブジェクト

Gitコアの、さらに中核(コアのコア)となる概念はなんでしょうか? それは間違いなくGitオブジェクトです。オブジェクト指向とは関係ないです。Gitにおけるオブジェクトとは単なるバイト列データのことです。ですが、オブジェクトにはID(一意識別子)が付いています。

「IDが付いたバイト列」がGitオブジェクトです。Gitオブジェクトに特徴的なことは、オブジェクトIDオブジェクト名とも言います)が、オブジェクトの中身を完全に表していることです。これは次のことを意味します。

  • 中身が同じ2つのオブジェクトは、同じオブジェクトIDを持つ。
  • オブジェクトIDが同じ2つのオブジェクトは、中身も同じ。

言い方を変えると:

  • オブジェクトIDが同じオブジェクトは、同一のオブジェクトとみなす!

名前(オブジェクトID)によってデータ内容を完全に識別するので、十分に長い名前が必要です。40桁の16進数文字列が使われています。

GitのオブジェクトIDは40桁

6ed796a189da0d6f756dd31e82755b36690e33a2
---------|---------|---------|---------|

Gitオブジェクトにおいては、「名が体を表す」が成立しています。この単純にして強力なアイディアが、分散バージョン管理システムGitを支えているのです。

Gitオブジェクトデータベース

Gitオブジェクトはどこにあるのでしょうか? 「Gitは何も隠さない」ことを思い出してください。.git/ディレクトリの配下に、そのリポジトリのすべてのオブジェクトが格納されています。

$ alias ls # 最初に、僕の ls はこんなです
alias ls='ls -aF --color=auto --show-control-chars'

$ sh ./setup-git-sample-1.sh # 実行済みなら不要です
なにやらかにやら

$ cd 1-git-sample/ # 1番のサンプルリポジトリを見に行く

$ ls .git
./   COMMIT_EDITMSG  ORIG_HEAD  description  index  logs/     refs/
../  HEAD            config     hooks/       info/  objects/

$ ls .git/objects/
./   07/  3d/  8c/  9c/  a7/  b9/  f5/    pack/
../  35/  85/  96/  a4/  ab/  be/  info/

$

サンプルリポジトリ 1-git-sample/ の下の、.git/objects/ に下には2文字の名前のディレクトリが幾つかあります。上記の例があなたの環境と同じとは限りませんが、be/ はあるはずです。

$ ls .git/objects/be
./  ../  3cf97185b993e42df63f601b8de2aefd046be2

$ cat .git/objects/be/3cf97185b993e42df63f601b8de2aefd046be2
バイナリファイルなのでグジャグジャ

$

リポジトリのすべてのオブジェクトは、.git/objects/ の下に通常ファイルとして格納されています。40桁のオブジェクトIDのうち2桁はサブディレクトリ名、残り38桁がファイル名です。オブジェクトの中身は対応するファイルの内容を圧縮したバイナリデータ*4です。いまcatしたオブジェクトは、a.txtというファイル内容の圧縮バイナリでした。

リポジトリのすべてのオブジェクトが格納されているディレクトリ .git/objects/ を、Gitのオブジェクトデータベースと呼びます。あなたが保存したあらゆるモノと、あなたは気付かないかも知れない関連するモノが、オブジェクトデータベースに入っています。

杞憂はやめよう、Gitは信頼できる

Gitの説明のなかで、あたかもGitオブジェクトを削除(消去)するような表現が出てきます。それは(一部の例外を除いて)言葉のあやです。通常の操作、あるいはヘマやドジでGitオブジェクトを消すのは極めて困難です。

もちろん、オブジェクトをほんとに削除する手段はあります。機密情報を間違ってリポジトリに入れてしまった時など、その情報を消せないと困りますからね。でも、リアルにデータを削除することはメチャクチャ面倒です。Gitが自発的にゴミ集め(garbage collection)することもあります。が、これは無視してもいいもんです。

つまり、事実上オブジェクトは消えないのです。

保存したオブジェクト(データ)が消えたように見えても、実際には探しにくくなっているだけです。安心して下さい、入ってますよ。

[*5]

オブジェクトは消せないばかりか変更もできません。もしオブジェクトの中身(バイト列データ)が変われば、名前(オブジェクトID)も変わります。名前が違えば別なオブジェクトです。名前を変えずに中身を変えることが原理的に不可能なのです。

厳密に言えば、違う中身で同じ名前のオブジェクトが出現する可能性はあります。しかしそれは、超々チョー稀で、オブジェクトIDの衝突を心配するのは、太陽が膨張して地球を飲み込むことを毎日心配するようなものです。

オブジェクトデータベースを探る準備

オブジェクトデータベースのディレクトリ .git/objects/ は、単なるオブジェクトの集合というわけではなく、もっと抽象的なデータ構造と捉えるべきです。どんな構造なのでしょうか? それを探っていきましょう。

探索・調査を助けるためのシェル関数が git-inspect-functions.sh(http://www.chimaira.org/misc/git-quest-kit.shar から展開)に入っています。次のようにして利用可能にします。

$ . ./git-inspect-functions.sh

日本語メッセージがutf-8エンコーディングで入っています。コンソールがutf-8を表示できないときは環境変数CONSOLE_ENCODINGをセットしてください(例: export CONSOLE_ENCODING=sjis

探索・調査用のシェル関数は名前は「g_」で始まっていて、g_help とすると全ての関数のヘルプが表示されます。これらの関数はgitコマンドをラップしていて便利だと思いますが、説明のなかではなるべく生のgitコマンドを使用します。しかし、とありえず次の「g_関数」は使います。

  • g_objects -- リポジトリ(のオブジェクトデータベース)に含まれる全てのオブジェクトを列挙します。

g_objects関数は、(cd .git/objects/; find -type f) | sed -e 's@^\.@@' -e 's@/@@g' を行うと思ってください。

なお、g_objects関数は、小規模なサンプルを調べるためのもので、大きなリポジトリでは正しく動かないかも知れません*6。git-quest-kit.shar内のスクリプトはあくまで学習目的と心得てくださいね。

オブジェクトデータベースは絶対追記式

では、ちょっと小手調べを。

$ sh ./setup-git-sample-1.sh # 実行済みなら不要です
なにやらかにやら

$ . ./git-inspect-functions.sh # 実行済みなら不要です

$ cd 1-git-sample/ # 1番のサンプルリポジトリを見に行く

$ g_objects
0718ba5d0a618675379e644c0bfa663c6234de22
358462e3ad874af2e3fd06cf0e806058b3179393
3d80f31c7e1ff1405291ab7aeb88d4e943aee689
853e2b3e200f8c3d33badb87f225f7b46169fe2f
8cbb43e0bda4053373e0efeea4fb50e03e8f1f50
9632a673ddb364c9f46427b2e7f6fcd1548334f0
9c08c2d14ddce27ca14ae6f4b78861cfa8d5caf6
a47fb14178e7a18ddabbf52f497813206ee184fb
a7cf72c7a33bee6b555087c27d8298bde1791d7c
ab6dbf626e0622caead0d2782204b39fba7faef2
b945b3c51935ccd856124fb2548a87e0f55b3ee5
be3cf97185b993e42df63f601b8de2aefd046be2
f5888fcb7e0112d8d14b80d25e34d3a2fe99e84c

$

ウギャーッ、なんじゃこりゃ。

40桁の16進数文字列の羅列を見せられてもサッパリ分からないですよね。でも、これでオブジェクトの個数を調べられます。

$ g_objects | wc -l
13

$

13個のオブジェクトがあります。「オブジェクトは消えない」ことを確認しましょう。

$ echo x> x.txt

$ git add x.txt

$ g_objects | wc -l
14

$

git add しただけではファイルは登録されないと習ったでしょうが、実際にはオブジェクトデータベースに登録されます。オブジェクトが1個増えて14個です。git addは名前の通り、オブジェクトをホントに追加します。

git addを取り消してみましょう。仮登録されたオブジェクトが消えると思いますか?

$ git status # ステータスを確認
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   x.txt


$ git reset HEAD x.txt # 仮登録を取り消す

$ git status # ステータスを確認
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        x.txt

nothing added to commit but untracked files present (use "git add" to track)

$ g_objects | wc -l # オブジェクトの個数
14

$

14個のままです。仮登録であれ、一度addしたらもう消すことは出来ません。オブジェクトデータベースにシッカリと入っているのです*7

次に、「ひとつ前のコミットを取り消すコマンド」と言われている git commit --amend やってみます。

$ g_objects | wc -l # オブジェクトの個数
14

$ git log -s | grep commit
commit f5888fcb7e0112d8d14b80d25e34d3a2fe99e84c # ← 注目
commit a47fb14178e7a18ddabbf52f497813206ee184fb
commit 9c08c2d14ddce27ca14ae6f4b78861cfa8d5caf6
commit 358462e3ad874af2e3fd06cf0e806058b3179393

$ git commit --amend -m amended # ひとつ前のコミットを取り消す
[master 2cd9a51] amended
 Date: Fri Sep 25 14:33:26 2015 +0900

$ git log -s | grep commit
commit 2cd9a511ca077d9613cf30308d30a265b06f25e0 # ← 注目
commit a47fb14178e7a18ddabbf52f497813206ee184fb
commit 9c08c2d14ddce27ca14ae6f4b78861cfa8d5caf6
commit 358462e3ad874af2e3fd06cf0e806058b3179393

$ g_objects  | wc -l # オブジェクトの個数
15

$

確かに最新のコミットが入れ替わっています。しかし、オブジェクト数は、14個から15個に増えています。コミットは消えてなんていないんです。見えなくなっただけです -- いざとなったら見つけて取り出すことも出来ます*8

「削除する」「取り消す」「修正する」といった表現がされていても、それは言葉のあや(厳密には)です。Gitのオブジェクトデータベースは絶対追記式のストレージで、削除も変更も出来ません。

オブジェクトデータベースはグラフデータベース

Gitのオブジェクトデータベースの構造をもっと詳しく見ましょう。先ほどの実験の効果を取り消すために次のようにします。

$ cd ..

$ sh ./clean-samples.sh  # サンプルを消去
removing 1-git-sample

$ sh ./setup-git-sample-1.sh # もう一度サンプルリポジトリを作る
なにやらかにやら

$

コミットの親子関係をグラフィカルに見るためにSourceTreeを使うことにします。サンプルリポジトリの樹形図は次のようになります。

4個のコミットオブジェクトが描かれています。しかし、このサンプルリポジトリ(のオブジェクトデータベース)には13個のオブジェクトがあるのでした。残りの9個はどうなっているの?

はい、僕(檜山)が頑張って13個全部のオブジェクトと相互関係まで描きましたよ。(もう、大変だったよ。)

オブジェクト(グラフのノード)の数を勘定してね。ちゃんと13個あるでしょ、これで全部。

Gitのオブジェクトデータベースがどんな種類のデータベースかと言うと、関係データベースとは違い、ドキュメントデータベースとも言えず、そうグラフデータベースに近いですね。オブジェクト間の関連性を示すグラフ辺がデータベース内部で行き交っています。そして、データベースの外部からオブジェクトを指し示すための参照のグラフ辺も飛び込んでいる、そういう構造をしてるわけ。

オブジェクトグラフの中を散策してみる

Gitオブジェクト達が作る有向グラフ(directed graph)をオブジェクトグラフと呼ぶことにします。オブジェクトグラフを貯えているデータベースがGitオブジェクトデータベースということになります。

ここから先は、オブジェクトグラフの絵を眺めながら読んでください(僕も絵を見ながら書いています)。http://www.chimaira.org/img3/git-sample-1-whole-graph.png をブラウザや画像ビューアの別ウィンドウに表示するか、印刷するといいですよ。

オブジェクトグラフのノードであるオブジェクトは、40桁のIDで識別できます。しかし、40桁のIDを扱える人なんていないわけで、人間可読な名前による名指しが必要です。その名指しの手段を提供するソフトウェア部分を、Gitの参照システムと呼びましょう。絵ではピンクと赤のところです。

参照システムがあるので、オブジェクトIDを使わずにオブジェクトグラフを扱えるようになり、バージョン管理システムとしての様々な機能を実装できるのです。

絵のピンクの囲いのなかにある人間可読な名前を、ラベルと呼ぶことにします。これらのラベルの実体はどこにあるのでしょう。「Gitは何も隠さない」のでした。もちろん、.git/ディレクトリの下にあります。

ラベル 対応するファイル
HEAD .git/HEAD
master .git/refs/heads/master
topic .git/refs/heads/topic
tag_second .git/refs/tags/tag_second

ラベルに対応するファイルを覗き見してみます。

$ cat .git/HEAD
ref: refs/heads/master

$ cat .git/refs/heads/master
8eedab38d83c5085058f3ce153fe6d466df61157

$ cat .git/refs/heads/topic
e4ff254f976a7202a3b88ff3aef3e63aa225124d

$ cat .git/refs/tags/tag_second
b1edd575f4e21096623bec4d2cf06ff4684d4342

$

テキストファイルの内容としてオブジェクトIDを記録することによりラベルを実現しているのです、素朴・簡単でしょ。HEADは、いったん refs/heads/master(ファイルの相対パス名)を経由してからオブジェクトIDによりオブジェクトを参照しています。

オブジェクトの中身はバイナリでしたが、それを解読して人間可読型式で表示してくれるコマンドが git cat-file です。git cat-file の引数はラベルでいいのですが、ここは生々しく禍々しい雰囲気を味わうために、オブジェクトIDをコピペして使いましょう。

$ git cat-file -t 8eedab38d83c5085058f3ce153fe6d466df61157 # このオブジェクトの型
commit # このオブジェクトはcommit型オブジェクト

$ git cat-file -p 8eedab38d83c5085058f3ce153fe6d466df61157 # このオブジェクトの中身
tree 8cbb43e0bda4053373e0efeea4fb50e03e8f1f50
parent b1edd575f4e21096623bec4d2cf06ff4684d4342
parent e4ff254f976a7202a3b88ff3aef3e63aa225124d
author m_hiyama <m.hiyama@gmail.com> 1443162786 +0900
committer m_hiyama <m.hiyama@gmail.com> 1443162786 +0900

merged

$

コンソール表示の結果を分かりやすく表にまとめると(オブジェクトIDは先頭部のみ*9):

このオブジェクトのID(既知)8eedab3...
このオブジェクトの型 commit
tree参照先のオブジェクトID 8cbb43e...
parent参照先のオブジェクトIDb1edd57...
parent参照先のオブジェクトIDe4ff254...
コミットメッセージ merged

オブジェクトの型にはcommit型(絵では二重枠楕円)以外にtree型(絵ではフォルダー形)、blob型(絵では楕円)があります。型については後で説明しますが、tree参照(絵では鎖線矢印)の先はtree型オブジェクト、parent参照(絵では青矢印)の先はcommit型オブジェクトだと決まっています。よって、ここまでの情報からオブジェクトグラフの一部(↓)を描けます。

同じようにして、オブジェクトIDで参照される先をドンドンたどっていけば、データベース内のオブジェクト達とその相互関連のグラフを描くことが出来ます。人間にとってこの作業は辛いですが、コンピューターにはいともたやすい事です。

Gitオブジェクトの種類とGitリポジトリファイルシステム

Gitオブジェクトは次の3種に大分類できます。

「コミット」という言葉には動詞と名詞がありますが、動詞(行為)としてのコミットにより、名詞(オブジェクト)としてのコミットが1個作られます。マージによってもコミットオブジェクトが作られますが、あれはGitが自動的にコミット(行為)してるだけです。

コミット(行為)は、記録・保存したい一群のファイルをGitオブジェクトデータベースに格納することです。1個のファイルは1個のblob型オブジェクトになり、1個のディレクトリは1個のtree型オブジェクトになります。そして全体としてはツリー構造を形成します。そのツリー構造が1個のコミットオブジェクトに紐付いてオブジェクトデータベースに格納されます。ファイルシステムツリーが、オブジェクトグラフの一部として埋め込まれて永久凍結されるわけ。

ここまでの話で、Gitは各時点でのファイル達を丸々抱えていることが分かるでしょう。絵 http://www.chimaira.org/img3/git-sample-1-whole-graph.png でa.txtが2つあるのは、変更前と変更後のa.txtを2つとも持っているからです。しかし、同じ内容のファイルを重複して持つことはありません。「中身が同じ2つのオブジェクトは、同じオブジェクトIDを持つ」ので、同じデータを重複しては持てないのです。

Gitオブジェクトデータベース内のファイルシステムは、Unixファイルシステムとほとんど同じです。中身(データ内容)が同じファイル(blob型オブジェクト)やディレクトリ(tree型オブジェクト)は、ハードリンク(同一オブジェクトへの参照)によって共有されます。共有はディスクの節約にも役立ちます。

ディレクトリに相当するtree型オブジェクトを覗いてみましょうか。

$ git cat-file -t 8cbb43e0bda4053373e0efeea4fb50e03e8f1f50 # このオブジェクトの型
tree # tree型オブジェクト

$ git cat-file -p 8cbb43e0bda4053373e0efeea4fb50e03e8f1f50 # このオブジェクトの中身
100644 blob be3cf97185b993e42df63f601b8de2aefd046be2    a.txt
100644 blob 0718ba5d0a618675379e644c0bfa663c6234de22    b.txt
040000 tree ab6dbf626e0622caead0d2782204b39fba7faef2    subdir

$

tree型オブジェクトの中身は、「blob型オブジェクト(ファイル相当)か他のtree型オブジェクト(サブディレクトリ相当)のオブジェクトIDと、ファイル名」の並びです。左端の100644などの数値は、オブジェクトの型とファイルパーミッション(644など)を組み合わせたフラグです*10。以下の絵では、オブジェクトIDによる参照をitemとラベルされたグラフ辺(矢印)で表現してます。

絵では、ファイル名をblob型オブジェクト側にラベルしてますが、ファイル名を含めたメタデータはすべてtree型オブジェクト側で保持しています。blob型オブジェクトには無垢なデータだけが詰め込まれているのです。


さて、ここからいよいよ「ブランチ編」です。

「基礎編」で読み疲れた方は、ひと休みして続きは明日にしましょう。

  1. ブランチとHEAD、ラベルとしての理解
  2. ラベルの追加と削除は、どうでもいい程に軽い作業
  3. 変態リポジトリ登場
  4. AOS集合は理解しておけ
  5. AOS集合をGitに表示させる
  6. AOS集合の共通部分集合
  7. Gitが使っている集合演算:差集合と対称差集合
  8. その集合演算、答はGitに聞け
  9. やっと「枝」の概念にたどり着いたぜ
  10. 成長点としてのブランチ
  11. 有向グラフのクイズ
  12. 言い残した事など

ブランチとHEAD、ラベルとしての理解

次のような説明を見たことがあります。

  • ソフトウェア開発のメインライン部分をトランク(幹)と呼び、そこから派生した枝をブランチと呼ぶ。

Gitのブランチはこのようなものではありません。このように理解しようとしても無理で、誤解と混乱に陥るだけです。

今更言ってもしょうがないけど、「ブランチ」という伝統的用語を流用しないで、「カーソル」とか「ポインタ」とか呼べばよかったのに、という気もします。

僕の知る範囲で、「ブランチ」という言葉は次の5つくらいの意味で使われています。

  1. コミットオブジェクトを指す(参照する)ラベル
  2. ラベルから指される(参照される)コミットオブジェクト
  3. 特定のコミットオブジェクトに対する、自分を含む祖先コミット集合
  4. 2つのコミット集合の差集合
  5. グラフの分岐ノード

このなかでも難しい概念である「2つのコミット集合の差集合」の特殊な場合が、幹に対する「枝」に対応します。難しい特殊なところから出発して分かるわけないでしょうよ。

簡単な話から始めましょう。Gitの参照システムについては既に述べました先の絵のように、幾つかのラベルがあり、ラベルがコミットオブジェクトを名指し(参照)しているのでした。ラベルには固定ラベルと変動ラベルがあります。固定ラベルの参照先オブジェクトは変えられません。一方、変動ラベルの参照先オブジェクトはどんどん変わります。

固定ラベルをタグと呼び、変動ラベルをブランチと呼ぶ、そう思ってください。ただし、HEADラベルは変動しますが特殊なのでブランチとは呼びません。

HEADは通常、他のラベルを指します。「HEAD → 他のラベル → オブジェクト」と参照を2回たどってオブジェクトに到達します。HEADが指すコミットオブジェクトがカレントのコミットです。カレントのコミットに紐付くリポジトリ内のファイルシステムツリーが、ワークツリー(作業ディレクトリのツリー)と比較されます。

「ブランチ」が「枝」を意味しないのと同様に、「ヘッド」(HEAD)に「先頭」の意味はありません。HEADは必ずしも他のラベルを経由する必要もなく、直接にコミットオブジェクトを参照できます。HEADは、どのコミットオブジェクトを参照してもかまいません

サンプルリポジトリで実験してみましょう。

$ cat .git/HEAD # HEADの参照先
ref: refs/heads/master

$ git checkout HEAD^ # 現在のHEADの親コミットをカレントにする
Note: checking out 'HEAD^'.

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 b1edd57... second

$ cat .git/HEAD # 変更後のHEAD、オブジェクトIDによる直接参照になっているはず
b1edd575f4e21096623bec4d2cf06ff4684d4342

$ git cat-file -p b1edd575f4e21096623bec4d2cf06ff4684d4342 # 参照先オブジェクトの中身
tree 853e2b3e200f8c3d33badb87f225f7b46169fe2f
parent 0f6f8dc6c0445b22bdc109ea6f25efc3eea84353
author m_hiyama <m.hiyama@gmail.com> 1443162784 +0900
committer m_hiyama <m.hiyama@gmail.com> 1443162784 +0900

second

$

HEAD^ という書き方は、現在のHEADが指しているコミットの親コミット(親が複数あるならの1番目の親)を表します。何やら警告が出ますが、HEADは直接的に、オブジェクトID b1edd575f4e21096623bec4d2cf06ff4684d4342 を使ってオブジェクトを参照するようになりました。参照先のオブジェクトは、second とコミットメッセージされたコミットオブジェクトです(絵 http://www.chimaira.org/img3/git-sample-1-whole-graph.pngを見てください)。git checkout masterで元の状態に戻ります。

直接参照であれ間接参照であれ、HEADの指すものは:

  • 我々がそれをもとに作業したいコミットオブジェクト

です。

ラベルの追加と削除は、どうでもいい程に軽い作業

変動可能なラベル、つまりブランチを作ってみます。

$ git branch b1 HEAD # ラベルb1はHEADと同じオブジェクトを指す

$ git branch b2 HEAD^ # ラベルb2はHEADの親オブジェクトを指す

$ git branch b3 topic # ラベルb3はtopicと同じオブジェクトを指す

# 以下省略
# ...

$ ls .git/refs/heads/ # ラベルの実体であるファイルを列挙
./   b1   b11  b13  b15  b17  b19  b20  b4  b6  b8  master
../  b10  b12  b14  b16  b18  b2   b3   b5  b7  b9  topic

$

20個のブランチ(ラベル)を作ってみました。SourceTreeで見てみます。

4個のコミットオブジェクトにラベルがウジャウジャ付いてます。別に問題ありません。

鬱陶しかったら削除しちゃいましょう。git branch -d b1 のようにします。rm .git/refs/heads/b* なんてお行儀の悪いことはしないでね。

これから分かるように、ブランチ(ラベル)を作っても消しても、オブジェクトデータベースには何の影響もありません。ただし、HEADから指されているカレントブランチは消せません。その他、ブランチ削除で「いいのかよ」と警告されるケースがあります。

固定ラベルであるタグに関しても同じことで、いくら作っても消してもかまいません。タグにはブランチのような削除制限はありません。

消すべきでないラベルを間違って消すミスは起こり得るでしょう。しかし、それはGitのメカニズムの問題ではなくて、人間の注意力の問題です。注意は必要ですが、ブランチ/タグは極めてお手軽な道具です。

ブランチやタグが難しくなるのは、プロジェクトや組織としての運用ルールを作ったり守ったりする段階においてです。Gitのブランチ/タグは単純軽量なメカニズムですが、良い使い方が難しいのです。

変態リポジトリ登場

ブランチの概念をさらに探るために、変態なリポジトリを作ります。ここで変態と言っているのは、コミットグラフが、自然にはなかなか現れないトポロジー(繋がり具合)をしているからです。

git-quest-kit.sharを展開したなかの、setup-git-sample-3.sh を使います。スクリプトの番号は3番です、setup-git-sample-2.sh ではないのでご注意。

$ cd .. # 1-git-sample/ から上に戻る

$ sh ./setup-git-sample-3.sh
なにやらかにやら

$

2番のスクリプト setup-git-sample-2.sh は、リモートリポジトリ、フェッチ、リモートとのマージなどの説明をするために準備したものです。記事が長くなってしまったので、この部分の説明は今回割愛します。これは、またの機会に述べるかも知れません(1年半前と同じセリフ)。

出来上がったリポジトリをSourceTreeで見てみます。

このリポジトリを作るために、僕が事前に描いた“設計書”が次の絵です。

こんなリポジトリを例題にしたのは、幹だの枝だのという先入観を破壊したかったからです。

コミットの番号は、コミットメッセージを使って入れています。説明にはこの番号を使います。でも、Gitは番号を理解しないので、番号とGitの参照式(正確には、revision expressionと言います)の対応を示しておきます。

コミットの番号(説明用) Gitの参照式
1 master^^
2 master^
3 master
4 HEAD^
5 HEAD
6 tag_master2
7 other^
8 other

例えば、4番のコミットを見たいなら、git show -s HEAD^ とします。

$ cd 3-git-sample/

$ git show -s HEAD^
commit d293eb0cfb3e979c9d3bf83ff8d38e192de75d0f
Author: m_hiyama <m.hiyama@gmail.com>
Date:   Sat Sep 26 13:06:45 2015 +0900

    4

$

AOS集合は理解しておけ

ここからは、手描きのグラフの絵 http://www.chimaira.org/img3/git-hentai-repo-plan.png を見ながら、そして練習問題を解くつもりで読んでください。

手描きのコミットグラフは、下方向に祖先、上方向に子孫となるように描いています。特定のコミットを指定したとき、そのコミットの祖先コミット全体からなるノード集合を、祖先集合(ancestor node-set)と呼びます。自分自身(当該コミット)を含めた集合は、自分を含む祖先集合(ancestor-or-self node-set)です。ancestor-or-selfはXPathの用語ですが、長たらしいのでAOS集合と略称しましょう。

特定のコミットのAOS集合が、ブランチ(ラベル以外の意味)を理解するうえで極めて重要です。AOS集合を使わずに、曖昧な喩え話のような事ばかり言っていてもラチがあきません。AOS集合をちゃんと理解しましょう!

例題の変態コミットグラフにおいて、1番のノード(コミットオブジェクト)のAOS集合を AOS(1) と書くことにします。AOS(2), AOS(3), ... なども同じです。1番から8番までのすべてのノード(コミットオブジェクト)に関して AOS(n) を求めてください。

やってくださいよ。

やりましたか?

最初の3つの AOS(n) だけを書いておけば:

  1. AOS(1) = {1}
  2. AOS(2) = {1, 2}
  3. AOS(3) = {1, 2, 3, 4}

「ブランチ」という言葉が“コミットオブジェクトの集合”の意味で使われるときは、特定コミットのAOS集合のことです。

例えば、masterブランチは、ラベルmasterが指すコミットが3番なのでAOS(3)、つまり4個のコミットからなる集合 {1, 2, 3, 4} のことです。otherブランチなら AOS(8) = {7, 8} となります。

AOS集合をGitに表示させる

特定コミットに対するAOS集合を表示するコマンドが git log です。git log は詳しい情報を表示しますが、今回は番号だけを表示させるために、git log --format=%s という形を使います。

$ git log --format=%s master^^ # AOS(1)
1

$ git log --format=%s master # AOS(3)
3
2
4
1

$ git log --format=%s HEAD # AOS(5)
5
4

$ git log --format=%s tag_master2 # AOS(6)
6
5
2
4
1

$ git log --format=%s other # AOS(8)
8
7

$

AOS集合の共通部分集合

2つの集合の共通部分集合というのがあります(Wikipedia項目)。次の図(ベン図)で表現されます。

2つのAOS集合の共通部分は、2つのコミットの共通祖先集合です。共通祖先集合は直線状だと思っている人がいますが、そんなことはないですよ。

  • 3番コミットと6番コミットの共通祖先集合は {1, 2, 4} で、直線状ではありません。
  • 2番コミットと5番コミット、あるいは6番コミットと8番コミットの共通祖先集合は空集合です。

Gitのマージアルゴリズムは、共通祖先コミット(マージベースと呼ぶ)の存在を仮定してますが、なくても動きます*11。一番若い共通祖先コミットが一意に決まったほうが望ましい、とは言えますけどね*12

Gitが使っている集合演算:差集合と対称差集合

集合の共通部分をとる演算はよく知られた集合演算ですが、Gitは、共通部分より知名度が低い集合演算である差集合対称差集合を使います。

差集合と対称差集合をベン図で描けば次のようです。知らなかった人は、直前の段落内からリンクを張ったWikipedia項目を調べてください。

実は、「枝」という概念を表現するには、差集合と対称差集合が必要なのです。

差集合の演算記号を「\」、対称差集合の演算記号を「△」と約束します。例題の変態リポジトリに関して、次の集合を求めてください。(絵→ http://www.chimaira.org/img3/git-hentai-repo-plan.png

  1. AOS(3)\AOS(6)
  2. AOS(3)△AOS(6)
  3. AOS(6)\AOS(2)
  4. AOS(6)△AOS(2)
  5. AOS(6)\AOS(8)
  6. AOS(6)△AOS(8)

今、答あわせはしません。正解は、Gitに教えてもらいましょう。

その集合演算、答はGitに聞け

Gitでは、指定した2つのコミットに対して:

  • 2つのAOS集合の差集合を求める演算を「..」(ドット2つ)、ただし逆順
  • 2つのAOS集合の対称差集合を求める演算を「...」(ドット3つ)

で表すことになっています。「逆順」とは、AOS(3)\AOS(6) を求めるのに 6..3 と書くということです。対称差集合は順番に無関係(可換演算)です。

ですから、前セクションの練習問題をGit記法に書き換えれば次のようになります。

  1. 6..3 (逆順)
  2. 3...6
  3. 2..6 (逆順)
  4. 6...2
  5. 8..6 (逆順)
  6. 6...8

Gitはコミットの番号を理解しませんから、先ほどの「番号←→参照式」変換表の必要な部分を再掲します。

コミットの番号(説明用) Gitの参照式
2 master^
3 master
6 tag_master2
8 other

これで準備OK。集合演算の答をGitに教えてもらいましょう。

$ git log --format=%s tag_master2..master # 6..3
3

$ git log --format=%s master...tag_master2  # 3...6
3
6
5

$ git log --format=%s master^..tag_master2 # 2..6
6
5
4

$ git log --format=%s tag_master2...master^ # 6...2
6
5
4

$ git log --format=%s other..tag_master2 # 8..6
6
5
2
4
1

$ git log --format=%s tag_master2...other # 6...8
6
5
8
2
4
7
1

$

もう一度、http://www.chimaira.org/img3/git-hentai-repo-plan.png の絵と見比べてください。Gitはちゃんと集合演算をやってるでしょ。

やっと「枝」の概念にたどり着いたぜ

一見分かりにくい演算記号「..」「...」ですが、これを利用すればコミットグラフの一部をうまく指定できます。特に、古典的・伝統的な「枝」概念も表現できるのです。

開発のメインラインである幹から枝分かれした「枝」をコミット集合(コミットグラフのノードセット)として表現することを考えます。幹の先頭のコミットをM、枝の先頭のコミットをBとしましょう。

このコミットグラフにおいて、差集合 M..B = AOS(B)\AOS(M) と、対称差集合 B...M = M...B = AOS(B)△AOS(M) を求めれば次のようになります。

得られたコミット集合は、確かに「枝」と呼ぶにふさわしいものです。しかし、ここまで頑張って読んでこられた皆さんには、ラベルとしての「ブランチ」から枝としての「ブランチ」までの道のりは、随分と遠いことが理解できたはずです。

そんなにも難しい概念を、まともな説明もしないで「ブランチは枝です」とか言ってしまう杜撰さって、イカガナモノカ? と僕は腹立たしいのですよ。

Gitを自信を持って使うために、「ブランチ」をちゃんと理解しましょうね。

成長点としてのブランチ

「ブランチ編」も大詰めです。疲れた方は一息入れましょう。

ラベルとしてのブランチは、変動するラベルだと言いました。では、どのように変動するのでしょうか。その変動のルールを知らないと、ブランチ(ラベル)の挙動を理解・予測できません。

ブランチラベルの挙動を理解する前に、HEADラベルについて知っておく必要があります。HEADは、直接的または間接的にカレントコミットを参照します。HEADがどんなコミットを指すかは自由です。「HEADは最新のコミットを指す」とかは嘘(穏やかに言って「言葉のあや」)ですから信用しないように*13

オブジェクトグラフの部分グラフであるコミットグラフにノード(コミットオブジェクト)を追加するとき、新しいノードの親は必ずHEADが指すコミットになります。言い方を変えると、追加されるノードはHEADの子ノードとなります。グラフは、HEADが指すコミットの所から成長する、と言えます。

コミットグラフに新しいノード(コミットオブジェクト)が追加されると、HEADラベルは新しいノードを指すように書き換えられます。

HEADに関する原則:

  • 追加されるノードは、現在の「HEADが指すノード」の子ノードとなる。
  • 追加が完了すると、HEADは新しいノードを指すように書き換えられる。

以上は、HEADラベルが直接にノード(コミットオブジェクト)を参照する状況を想定してますが、「HEADラベル → ブランチラベル → コミットオブジェクト」の場合でも、同じ原則が適用されます。

  • 追加されるノードは、現在の「HEADが指すブランチが指すノード」の子ノードとなる。
  • 追加が完了すると、「HEADが指すブランチ」は新しいノードを指すように書き換えられる。

これで説明は終わりです。これだけ理解すれば十分です。つまらない喩え話や曖昧な説明は忘れましょう。

現実のケースでは、HEADラベルはブランチラベルを指しています。このため、新しいノード(コミットオブジェクト)の追加の際に、HEAD自体を書き換える必要はなくて、ブランチラベルが更新されます。それでも、HEADに関する原則はキチンと守られます。

HEADが指すコミットがコミットグラフの成長点でした。HEADからの参照がブランチを経由するときは、「HEADが指すブランチが指すコミット」が成長点となります。つまり、カレントブランチがコミットグラフの成長点、と言ってもいいわけです。

「HEADが指すブランチ」を書き換えることを「ブランチを切り替える」と言ったりします。ブランチの切り替えは、ラベルの書き換えだけでは済まなくて*14、checkoutによる作業ディレクトリ(ワークツリー)の更新も生じますが、参照システムとしての挙動は今述べたとおりです。

有向グラフのクイズ

ここで練習問題 -- つうより、Gitを使ったお遊びです。今回説明しなかったフェッチやマージも使います。フェッチ/マージをご存知の方はチャレンジしてみてください。[追記]フェッチは使わなくても出来ますね。その方法は、文書using-orphan.mdを参照して下さい。[/追記]

有向グラフにおいて、有向辺(矢印)を“流れ”と解釈するとき、次のような用語を使います。

ノードの分類 入る辺の数 出る辺の数
湧き出しノード 0 任意
吸い込みノード 任意 0
合流ノード 2以上 任意
分流ノード 任意 2以上

入る辺の数も出る辺の数も 0, 1, 2 に制限して、すべての組み合わせを列挙してみます。

入↓出→ 0 1 2
0 湧き出し&吸い込み 湧き出し 湧き出し&分流
1 吸い込み 分流
2 合流&吸い込み 合流 合流&分流

9種類のノードになりますね。

9個のノードを持つ有向グラフで、そのぞれのノードが上の表のそれぞれの種類のノードになるようなものを描いてみてください。しばらく試行錯誤すれば出来ると思いますよ。

次に、この有向グラフをGitのコミットグラフとして実現してみてましょう。どんなgitコマンドを使ったらいいか分からない人は、変態リポジトリを作るスクリプトsetup-git-sample-3.shを見ればヒントになるでしょう。

言い残した事など

Gitは自由度の高いソフトウェアです。自由度が高過ぎるとも言えます。ですから、「普通、そんなことしねーよ」とか「それ、あぶないぞ」という操作・機能は多々あります。Git自身も意外と親切に警告してくれます。しかしながら、運用上の推奨事項・注意事項とメカニズム・機能性の境界を曖昧にするのは良くないと思います。この記事から始めて、変態・外道なGit使いにもなれるでしょうが、そこは皆さんの良識で判断できるはずです。

今回は、Gitのオブジェクトデータベースと参照システムについてだけ説明しました。他に、リポジトリへの入出力を司るサブシステムがあり、コマンドラインインターフェイスを構成するたくさんのサブコマンド/オプションがあります。ワークツリー、インデックス(ステージングエリア)、リモートリポジトリ、サブモジュールなどについては、「詳しいことはまたの機会に述べるかも知れません」という言葉を再び書いておきます。

この記事から垣間見えるように、有向非巡回グラフ(directed acyclic graph)の諸概念は、オブジェクトグラフの議論に効果的に適用できます。グラフ理論の観点から見たGitというのも面白い話題かも知れません。

グラフデータベースシステムとしてのGitとはどんなもんだろう? とか、パッチ(差分)とGitとの関係とかも僕には興味深い話です。色々と話題は尽きませんが、ここらへんにしておきます。

*1:アダムとイブと禁断の果実のお話に、イチジクの葉っぱが出てきます。

*2:年寄りしか知らない話。『マイディア ステファニー―武田久美子写真集』ワニブックス (1989/09)参照。

*3:ティントレット(Jacopo Tintoretto)によるふくよかなイブ(絵画の一部)。画像の取得元: http://transact.seesaa.net/article/104243331.html

*4:「バイナリ」って何だよ? って話は毎度出るんですが、人間可読ではないフォーマットのデータという程度です。

*5:とにかく明るい安村さん。画像の取得元: http://earth38moon.blog115.fc2.com/blog-entry-3159.html

*6:パックされたオブジェクトを考慮してません。

*7:しかし、x.txtはどこからも参照されない迷子オブジェクトになっています。git fsck で確認できます。

*8:git reflog で見つかります。

*9[追記]この表の長いオブジェクトIDが小画面デバイスでのレイアウトを邪魔しているようなので修正。[/追記]

*10[追記]644や755はパーミッションに見えますが、パーミッションそのものというわけではなくて、6桁全体でオブジェクトの分類をしているようです。[/追記]

*11:git merge-base は、どんな場合でも何かしらのコミットオブジェクトを探し出します。

*12[追記]2つのコミットに対して、一番若い共通祖先コミットが一意に決まるとき、コミットグラフは順序集合として(あるいは代数構造として)半束(semilattice)と呼びます。常識的なリポジトリは半束になってますし、半束ではないリポジトリは扱いにくいです。半束リポジトリでは、最初のコミット(おおもとの祖先、始祖)は1個だけです。したがって、コミットグラフの辺の向きを反転させたグラフでは、任意のノードが「始祖コミットから可達(reachable)」となります。[/追記]

*13:そもそも、「最新」をどう定義するかハッキリしないですよね。タイムスタンプで比較するのか、有向グラフのトポロジーから判断するのか?

*14git symbolic-refコマンドで、間接参照の書き換えだけができます。