Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

配管を通ってGitを理解してみる

f:id:tbpg:20161006221700p:plain

Gitを理解するにはGitの中身の知るのが良い、
と天の声が聞こえてきたので学習がてらまとめることにしました。

この記事は個人メモ的な記事です。
基本的に既出情報なのでタイトルをみてピンと来ているかたは読む必要がありません。

※この記事を読むタイミングとしてはGitの基本的な操作と概念をある程度理解した
あとが良いと思います。曖昧な基準ですが。

Gitの2種類のコマンド

f:id:tbpg:20161006221709p:plain

Gitの中身はキーバリュー型のデータストアになっています。

GitにはVCS(Version Control System)としてのUIがありますが、
もともとはファイルシステムとしてのUIを重視していました。

前者を 磁器( Porcelain )コマンド
後者を 配管( Plumbing )コマンド

と呼びます。

洗面器やトイレなどユーザーが直接使うものを磁器、
水道管など通常直接使わないものを配管と呼ぶことで区別しようとしたのかな?

StackOverflowでもそのあたりに関するやりとりがありました。

Greg Hewgill answer

"Porcelain" is the material from which toilets are usually made (and sometimes other fixtures such as washbasins). This is distinct from "plumbing" (the actual pipes and drains), where the porcelain provides a more user-friendly interface to the plumbing.

Git uses this terminology in analogy, to separate the low-level commands that users don't usually need to use directly (the "plumbing") from the more user-friendly high level commands (the "porcelain").

Evan Plaice answer

Makes you wonder if Linus was imagining the potential streams of excrement his plumbing would be used to transport. Plumbing for open source code is a dirty job but someone's got to do it.

つまり、こういうことですね!

f:id:tbpg:20161006221724p:plain

・・・え?

配管( Plumbing ) コマンド

f:id:tbpg:20161006221731p:plain

Plumbing [plˈʌmɪŋ]

ファイルシステムとしてのコマンド。
low level のコマンド。
Gitの内部動作にアクセスします。
一般の git ユーザーがコマンドラインから実行するのではなく、
新たに作られるツールの処理の一部として利用されることを想定しています。

  • cat-file
  • commit-tree
  • count-objects

などがあります。

Plumbing コマンドについては公式 Reference にまとめて記載してあります。
下記ページの Plumbing Commands にコマンドリストがあります。

Git - Reference

磁器( Porcelain ) コマンド

f:id:tbpg:20161006221755p:plain

Porcelain [pˈɔɚs(ə)lɪn]

VCSとしてのコマンド。
high level のコマンド。
一般のgitユーザーが通常のオペレーションを行うときに利用するコマンドです。

  • checkout
  • branch
  • remote

などがあります。

Gitの中身

Gitの保管データは .git ディレクトリに保存されます。

f:id:tbpg:20161006221816p:plain

git init 直後の内容を確認すると以下のようになっています。

$ git init
Initialized empty Git repository in /path/to/project/.git/
$ tree -a
.
└── .git
    ├── HEAD
    ├── config
    ├── description
    ├── hooks
    │   ├── applypatch-msg.sample
    │   ├── commit-msg.sample
    │   ├── post-update.sample
    │   ├── pre-applypatch.sample
    │   ├── pre-commit.sample
    │   ├── pre-push.sample
    │   ├── pre-rebase.sample
    │   ├── prepare-commit-msg.sample
    │   └── update.sample
    ├── info
    │   └── exclude
    ├── objects
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags

Gitオブジェクト

Git はSHA1のハッシュキーとGitオブジェクトからなるキーバリューストアです。
Git オブジェクトは 4 種類あります。

f:id:tbpg:20161006221823p:plain

  1. blob
  2. tree
  3. commit
  4. tag

です。

blob オブジェクト

blob オブジェクトはファイルを保持するGitオブジェクトです。

f:id:tbpg:20161006221830p:plain

配管コマンドで blob オブジェクトを作成してみます。
まず git hash-object コマンドでファイルをGitオブジェクトにします。

$ echo hoge > hoge.txt
$ git hash-object -w hoge.txt
2262de0c121f22df8e78f5a37d6e114fd322c0b0
$ cd .git/objects
$ tree
.
├── 22
│   └── 62de0c121f22df8e78f5a37d6e114fd322c0b0
├── info
└── pack

オブジェクトを git cat-file コマンドで確認します。

# blob オブジェクトの中身を表示
$ git cat-file -p 2262de0c121f22df8e78f5a37d6e114fd322c0b0
hoge

# blob オブジェクトの種類を表示
$ git cat-file -t 2262de0c121f22df8e78f5a37d6e114fd322c0b0
blob

blob オブジェクトを作成できていることを確認できました。
ちなみに blob オブジェクトの実体は元ファイルを zlib 圧縮 したファイルです。

tree オブジェクト

tree オブジェクトはディレクトリを保持するGitオブジェクトです。
内部でディレクトリが格納している blob オブジェクトと tree オブジェクトの情報を持ちます。

f:id:tbpg:20161006221844p:plain

配管コマンドで tree オブジェクト作成してみます。
まず git update-index でインデックスを更新します。

# ファイル、ディレクトリを追加
$ echo hige > hige.txt
$ mkdir dir
$ echo in_dir > in_dir.txt

# 追加したファイルの blob オブジェクトを作成
$ git hash-object -w hoge.txt hige.txt dir/in_dir.txt
2262de0c121f22df8e78f5a37d6e114fd322c0b0
9e4f51055b0950ef098e9e2705f2c4d4e9662857
3d2a2dbf2845e04e2f5c87792f6b8b0ebec54846

# インデックスの更新
$ git update-index --add --cacheinfo 100644 2262de0c121f22df8e78f5a37d6e114fd322c0b0 hoge.txt
$ git update-index --add --cacheinfo 100644 9e4f51055b0950ef098e9e2705f2c4d4e9662857 hige.txt
$ git update-index --add --cacheinfo 100644 3d2a2dbf2845e04e2f5c87792f6b8b0ebec54846 dir/in_dir.txt

id:koseki2 さん作のツール git-object-browser gem で確認すると以下のようにステージングされています

f:id:tbpg:20161006221852p:plain

次に git write-tree で tree オブジェクトを作成します。

$ git write-tree

# 作成した tree オブジェクトの内容を表示
$ git cat-file -p 34cfd122c532d9c704f94d2445b76f1ec5e65f1a
040000 tree 62a8ac5b69c8a18c8401fc65422279368b66a7b7    dir
100644 blob 9e4f51055b0950ef098e9e2705f2c4d4e9662857    hige.txt
100644 blob 2262de0c121f22df8e78f5a37d6e114fd322c0b0    hoge.txt

# 作成した tree オブジェクトの種類を表示
$ git cat-file -t 34cfd122c532d9c704f94d2445b76f1ec5e65f1a
tree

# 作成した tree オブジェクトの内容を表示
$ git cat-file -p 62a8ac5b69c8a18c8401fc65422279368b66a7b7
100644 blob 3d2a2dbf2845e04e2f5c87792f6b8b0ebec54846    in_dir.txt

# 作成した tree オブジェクトの種類を表示
$ git cat-file -t 62a8ac5b69c8a18c8401fc65422279368b66a7b7
tree

tree オブジェクトが作成されていることを確認できました。

commit オブジェクト

commit オブジェクトは commit 情報を保持するGitオブジェクトです。

f:id:tbpg:20161006221903p:plain

配管コマンドで commit オブジェクトを作成してみます。
tree オブジェクトを作成した後の状態からスタートします。

# commit オブジェクトの作成
$ echo 'first commit' | git commit-tree 34cfd122c532d9c704f94d2445b76f1ec5e65f1a

# 作成した commit オブジェクトの内容を表示
$ git cat-file -p 25557e6f6472f90b8962374bce49f764594cb5d9
tree 34cfd122c532d9c704f94d2445b76f1ec5e65f1a
author name <email> 1475720628 +0900
committer name <email> 1475720628 +0900

first commit

# 作成した commit オブジェクトの種類を表示
$ git cat-file -t 25557e6f6472f90b8962374bce49f764594cb5d9
commit

name はコミットした人の git user.name です。
email はコミットした人の git user.email です。

hageファイルを追加して commit オブジェクトを追加してみます

$ echo hage > hage.txt
$ git hash-object -w hage.txt
6b096d6a1258bf34c76f9ff789b13082bbcb432d
$ git update-index --add --cacheinfo 100644 6b096d6a1258bf34c76f9ff789b13082bbcb432d hage.txt
$ git cat-file -p 6b096d6a1258bf34c76f9ff789b13082bbcb432d
hage
$ git cat-file -t 6b096d6a1258bf34c76f9ff789b13082bbcb432d
blob
$ git write-tree
7726c104e638b81fc13bb8eb18f1374913804f8b
$ echo 'second commit' | git commit-tree 7726c104e638b81fc13bb8eb18f1374913804f8b -p 25557e6f6472f90b8962374bce49f764594cb5d9
1601ae1c2217cce390a57721e73d8aaa3d25af31

$ git cat-file -p 1601ae1c2217cce390a57721e73d8aaa3d25af31
tree 7726c104e638b81fc13bb8eb18f1374913804f8b
parent 25557e6f6472f90b8962374bce49f764594cb5d9
author name <email> 1475721658 +0900
committer name <email> 1475721658 +0900

second commit
$ git cat-file -t 1601ae1c2217cce390a57721e73d8aaa3d25af31
commit

履歴を確認します

$ git log --stat 1601ae1c2217cce390a57721e73d8aaa3d25af31
commit 1601ae1c2217cce390a57721e73d8aaa3d25af31
Author: name <email>
Date:   Thu Oct 6 21:40:58 2016 +0900

    second commit

 hage.txt | 1 +
 1 file changed, 1 insertion(+)

commit 25557e6f6472f90b8962374bce49f764594cb5d9
Author: name <email>
Date:   Thu Oct 6 21:23:48 2016 +0900

    first commit

 dir/in_dir.txt | 1 +
 hige.txt       | 1 +
 hoge.txt       | 1 +
 3 files changed, 3 insertions(+)

そして最後のコミットを .git/refs/heads/master に設定することで git log で履歴を確認できるようになります。

# master ブランチの refs の設定前
$ git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   dir/in_dir.txt
    new file:   hage.txt
    new file:   hige.txt
    new file:   hoge.txt

# まだ master ブランチの refs の設定がない
$ git log
fatal: your current branch 'master' does not have any commits yet

# master ブランチの参照に最終コミットの SHA1 を設定する
$ echo "1601ae1c2217cce390a57721e73d8aaa3d25af31" > .git/refs/heads/master

# master ブランチの refs の設定後
$ git status
On branch master
nothing to commit, working tree clean

$ git log --pretty=oneline master
1601ae1c2217cce390a57721e73d8aaa3d25af31 second commit
25557e6f6472f90b8962374bce49f764594cb5d9 first commit

ここまでで、 磁器コマンドgit addgit commit 相当のことをしました。

tag オブジェクト

tag オブジェクトは tag 情報を保持するGitオブジェクトです。

f:id:tbpg:20161006221922p:plain

tag には 軽量タグアノテートタグ があります。
tag オブジェクトは アノテートタグ の作成時のみ作成されます。

  • 軽量タグの作成
# オプションなしでタグを作成
$ git tag test_tag
# tagの参照先はHEADが指している commit の SHA1 になる
$ cat .git/refs/tags/test_tag
1601ae1c2217cce390a57721e73d8aaa3d25af31
  • アノテートタグの作成
# -a オプションでタグを作成
$ git tag -a test_tag2 -m "version tag2"
# tagの参照先はHEADが指している tag オブジェクトの SHA1 になる
$ cat .git/refs/tags/test_tag2
6e7a983fb7b48d28632bbe93fee7e02c773d2fc5

# 作成した tag オブジェクトの内容を表示
$ git cat-file -p 6e7a983fb7b48d28632bbe93fee7e02c773d2fc5
object 25557e6f6472f90b8962374bce49f764594cb5d9
type commit
tag test_tag2
tagger name 1475730743 +0900

version tag2

# 作成した tag オブジェクトの種類を表示
$ git cat-file -t 6e7a983fb7b48d28632bbe93fee7e02c773d2fc5
tag

tag オブジェクトを作成できていることを確認できました。

参照

GitオブジェクトはSHA1ハッシュで識別されますが、
SHA1は人に優しくないので、人にとって分かりやすい名前をつけることができます。
これが 参照 です。

ブランチの内部では参照が作成されています。

$ git checkout -b hoge
$ cat .git/refs/heads/hoge
3b629a6ccc1951695c039a465b336ee7dd9a913e

# 参照の本体は hoge ブランチの最新のコミットオブジェクトです
$ git cat-file -p 3b629a6ccc1951695c039a465b336ee7dd9a913e                ⮂
tree 6a73751cba5a86b1ebe10ab2856b68404aab50fd
author name <email> 1475745009 +0900
committer name <email> 1475745009 +0900

test

参照を参照するケースもあります。
例えば以下のような場合

$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
$ cat .git/refs/remotes/origin/HEAD
ref: refs/remotes/origin/master

.git/refs/remotes/origin/HEAD に設定されている ref: refs/remotes/origin/master
参照の参照 = シンボリック参照です。

.git配下のファイル、ディレクトリの説明

HEAD ファイル

HEADは現在使用しているブランチの先頭です。
HEAD ファイルは branch名, commit SHA1 が設定されています。
つまり、参照とシンボリック参照双方を扱います。

# log で commit SHA1 を確認
$ git log --oneline
1601ae1 second commit
25557e6 first commit

# HEAD ファイルを確認。 master ブランチを参照している
$ cat .git/HEAD    
ref: refs/heads/master

# master ブランチの参照を確認。最後のコミットを指している
$ cat .git/refs/heads/master
1601ae1c2217cce390a57721e73d8aaa3d25af31

# 最初のコミットにHEADを移動する
$ git checkout 25557e6
Note: checking out '25557e6'.

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 25557e6... first commit

# HEADへのリファレンスが最初のコミットへの参照になっている
$ cat .git/HEAD
25557e6f6472f90b8962374bce49f764594cb5d9

index ファイル

.git/index ファイルはある時点でのプロジェクトのディレクトリツリー全体の情報を持つバイナリファイルです。
git ls-files --stage で参照可能です。

$ git ls-files --stage
100644 3d2a2dbf2845e04e2f5c87792f6b8b0ebec54846 0   dir/in_dir.txt
100644 6b096d6a1258bf34c76f9ff789b13082bbcb432d 0   hage.txt
100644 9e4f51055b0950ef098e9e2705f2c4d4e9662857 0   hige.txt
100644 2262de0c121f22df8e78f5a37d6e114fd322c0b0 0   hoge.txt

objects ディレクト

.git/objects ディレクトリには blob, tree, commit, tag のGitオブジェクトが保存されています。
Gitオブジェクトは40文字から成るSHA1ハッシュの2文字目までのディレクトリと残り38文字をファイル名の形式で保存されています。

Gitオブジェクトは git cat-file -p <SHA1> で参照可能です。

refs ディレクト

.git/refs ディレクトリ配下にはブランチ、タグへの参照が保存されています

$ cd .git/refs
$ tree
.
├── heads
│   └── master
├── remotes
│   └── origin
│       ├── HEAD
│       └── master
└── tags
    ├── test_tag
    └── test_tag2

.git/refs/heads/master を確認すると master ブランチの最後の commit オブジェクトへの参照を表す SHA1が設定されています。

$ cat heads/master
1601ae1c2217cce390a57721e73d8aaa3d25af31

このように、ブランチやタグの最後の commit を参照できるようになっています

関連資料