Git で Git をハックする

# はじめに
みなさん、Git 使っていますか？　Git、便利ですよね。
弊社の新人教育では、バージョン管理のツールとして Git ではなく SVN を教わりましたが、
やっぱり Git 使いたいですよね。
みんな使っているのに以外と最初はとっつきにくい Git。

この記事は、Git を使い始めて、なんとなく言われた通りに`git add`とか`git commit`とかしてるけど、
何が起きているか分からないブラックボックスな Git を、Git を使ってその動きを確認していきます。

# 座学
まずは、Git について簡単に書きます。 あとで、触りながら確認をします。

## Git の他のバージョン管理システムとの違い
Git はそもそも、Linux カーネルを開発した、リーナス・トーバルズ氏によって、大規模な開発でもバージョン管理ができるように開発されました。
それまでにもバージョン管理システムはあったそうですが、Git がそれらのシステムと違うのは、
コミットごとにデータを圧縮したデータを保持しているという点です。
**コミットごとにデータを圧縮したデータ**というのが大事で、それまでのバージョン管理システムでは、前のコミットとの**変更差分**をデータとして保持していました。

> 何が違うかというと、変更差分だけを保持していると、例えば 1000 世代前のバージョンに戻りたい時に、
> システムは 1000 回分の変更差分を計算しなければいけないということです。
> これはとても処理効率が悪いですよね。
> （もし 200 世代くらい前のデータだけが消えてしまったらどうなるんだろう ？）

> Git はコミットごとにデータを圧縮して保持しているため、たとえ １０００ 世代前に戻るとしても、システム的には １ 回の計算で済みます。

## git add と git commit
ローカルの Git のリポジトリには、３ つの領域が存在します。

```
1. ワーキングツリー
2. ステージ
3. ローカルリポジトリ
```
この ３ つの領域をドラクエで例えると、（イメージだけ伝わってほしい）

```
1. ワーキングツリー
   ⇨ プレイ中のデータ 
　　　（まさに編集中のファイル）

2. ステージ
   ⇨ ボス戦の前とか、キリのいいとこでセーブしたデータ 
　　　（とりあえず動くまで作ったソースコードを今の状態で保存）

3. ローカルリポジトリ
   ⇨ 大きな分岐の前で、しくったらいやだから別の名前で保存しておいたセーブデータ
　　　（過去の状態に戻すことができるデータの集合）
``` 
※ちなみに僕はドラクエしたことありません

ワーキングツリーからステージにあげるのが `git add`
ステージに保存しておいたデータをリポジトリに格納するのが `git commit`
です。

## git add
git add をすると、ファイル毎にその圧縮ファイルが生成され、Git のリポジトリに格納されます。
その圧縮ファイルを、Git では blob オブジェクトと呼びます。
(２ つのファイルを add したら、２ つの blob が生成されます。)
その時、blob のファイル名は、**ファイルの中身**にヘッダーを付け加えたものを SHA-1 というハッシュ関数で生成した 16 進数 40 文字がつけられます。

ここで大事なのは、blob オブジェクトは元のファイル名の情報を一切持たないということです。
git add するともうひとつ、インデックスファイルというバイナリーデータが生成されます。
この中に、blob オブジェクトと、実ファイル（git add したファイル）の名前の関連付けが記録されます。

## git commit
git commit をすると、インデックスファイルを元に生成される ツリーオブジェクト とコミットメッセージ、親コミットなどを記録した コミットオブジェクト が生成されます。
コミットオブジェクト は ツリーオブジェクト と関連づけられます。

これが Git が行なっている動作ですが、意味が分からないと思います。
ここから、本題の Git を Git でハックしていきます。

# ハンズオン
ここからは実際に触りながら確認をします。

## Git リポジトリ生成
### 環境

```
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.6
BuildVersion:	18G103

$ git --version
git version 2.23.0
```

まず、適当な空のディレクトリを作成し、それをリポジトリにします。

```shell-session:~repository
$ cd ~
$ mkdir repository
$ cd repository
$ git init
```
これで、`~/repository`が Git リポジトリになりました。

```shell-session:~repository
$ ls -a
./    ../   .git/
```
この、`.git`ディレクトリがローカルリポジトリの正体です。
Git でのバージョンの記録は全てこのディレクトリ配下に保存されます。
中身を確認します。

```shell-session:~repository
$ tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

8 directories, 15 files
```

様々なディレクトリ、ファイルが保存されていることがわかります。
今回はこの .git ディレクトリの変更を探りたいため、.git ディレクトリをリポジトリとして Git で管理します。

```shell-session:.git
$ cd .git
$ git init
```
.git ディレクトリの変更を検出するため、今の状態をコミットしておきます。

```shell-session:.git
$ git add .
$ git commit -m 'initial commit'
[master (root-commit) 5ebac92] initial commit
 15 files changed, 655 insertions(+)
 create mode 100644 HEAD
 create mode 100644 config
 create mode 100644 description
 create mode 100755 hooks/applypatch-msg.sample
 create mode 100755 hooks/commit-msg.sample
 create mode 100755 hooks/fsmonitor-watchman.sample
 create mode 100755 hooks/post-update.sample
 create mode 100755 hooks/pre-applypatch.sample
 create mode 100755 hooks/pre-commit.sample
 create mode 100755 hooks/pre-push.sample
 create mode 100755 hooks/pre-rebase.sample
 create mode 100755 hooks/pre-receive.sample
 create mode 100755 hooks/prepare-commit-msg.sample
 create mode 100755 hooks/update.sample
 create mode 100644 info/exclude
```

## git add
次に、~repository で適当なファイルを作成し、git add してみます。

```shell-session:~repository
$ echo "hello world" > hello.txt
$ git add hello.txt
```
`hello world`と記述された、　hello.txt を作成し、git add しました。
では、.git の変更を確認します。

```shell-session:.git
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	index
	objects/

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

$ tree objects/
objects/
├── 3b
│   └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── info
└── pack
```
`index`と`objects/`が生成され、objects/ の中には、`3b/18e512dba79e4c8300dd08aeb37f8e728b8dad`が生成されたことが確認できます。
それぞれが、インデックスファイルと、blob オブジェクトです。
中身を確認します。

```shell-session:~repository
$ git hash-object hello.txt
3b18e512dba79e4c8300dd08aeb37f8e728b8dad

$ git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
hello world
```
`git hash-object`コマンドで、hello.txt の blob オブジェクトの名前を調べます。
`git cat-file -p`コマンドで、その中身を調べます。
確かに、hello world だということがわかります。

```shell-session:~repository
$ git ls-files --stage
100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0	hello.txt
```
index ファイルの中身です。
`3b18e512dba79e4c8300dd08aeb37f8e728b8dad` という blob オブジェクトと、`hello.txt`というファイル名が関連づけられていることがわかります。

一旦、今の状態で .git をコミットしておきます。

```shell-session:.git
$ git add .
$ git commit -m 'add hello.txt'
[master dfd8190] add hello.txt
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 index
 create mode 100644 objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
```

## git commit
次に、git add した hello.txt を git commit します。

```shell-session:~repository
$ git commit -m 'create hello worild'
[master (root-commit) d482b93] create hello worild
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt
```

.git の変更を確認します。

```shell-session:.git
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   index

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	COMMIT_EDITMSG
	logs/
	objects/68/
	objects/d4/
	refs/

no changes added to commit (use "git add" and/or "git commit -a")

$ tree objects/
objects/
├── 3b
│   └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── 68
│   └── aba62e560c0ebc3396e8ae9335232cd93a3f60
├── d4
│   └── 82b930517d4f9575a45ee33ee70d9eae454c27
├── info
└── pack
```
色々、変更されていることがわかります。

まず、先ほどのコミットIDを調べます。

```shell-session:~repository
$ git log
commit d482b930517d4f9575a45ee33ee70d9eae454c27 (HEAD -> master)
Author: *************************
Date:   *************************

    create hello worild
```
コミットID は、`d482b930517d4f9575a45ee33ee70d9eae454c27`です。
中身を確認します。

```shell-session:~repository
$ git cat-file -p d482b930517d4f9575a45ee33ee70d9eae454c27
tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60
author ************************* 1573050645 +0900
committer ************************* 1573050645 +0900

create hello worild
```
ツリーオブジェクト が、`68aba62e560c0ebc3396e8ae9335232cd93a3f60`だということがわかります。
中身を確認します。

```
$ git cat-file -p 68aba62e560c0ebc3396e8ae9335232cd93a3f60
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad	hello.txt
```
ツリーオブジェクト には、コミットした hello.txt とその blob オブジェクトが関連づけられていることがわかりました。


## おわり
長くなりそうなので、一旦ここまでにしておきます。
ほんとは図があったほうがわかりやすいと思いますが、作るのがめんどくさかったです。
ごめんなさい。

Git って、何がおきているかよくわかりませんが、中身を確認するとイメージがしやすくなりますね。
「Git で Git をハックする」 思ったよりも面白いです、ぜひ。