仕組みがわかったらGitが全然怖くなくなったよ、という話

##はじめに
Gitは仕組みが分かると苦手意識がなくなります。少なくとも私はそうでした。そもそも、私の場合なぜ苦手だったかというと「操作を間違えたらファイルを壊すんじゃない？」と思っていたからです。実は、Gitはファイルを圧縮してレポジトリに格納しているだけです。「ファイルを壊しようがない」事が分かり苦手意識がなくなりました。現在Gitを食わず嫌いしている人もこの記事でGitを使った頂けたら幸いです。

#Gitの仕組み
Gitは管理対象のファイルを１つ１つ圧縮し、圧縮ファイルをレポジトリに格納します。2回目以降のコミットでは、以前のコミットから変更があったファイルだけをレポジトリに追加します。ファイルに変更があったかどうかはファイルのハッシュ値で判定しています。
（「ハッシュ値なにそれおいしいの？」という人はとりあえずファイルの電話番号だと思ってください。詳しくはGoogle先生に聞いてください。）

以上

と、これだけでは納得し難いと思いますので本記事ではGitの仕組みがわかる実験してみます。手を動かすのと動かさないのとでは納得感が全然違いますので、ぜひ実際に手を動かして実験して頂きたいです。

#実験に必要なコマンド解説（Gitをまったく知らない人むけ）
Gitをまったく知らない人むけに、今回の実験に必要な最低限の操作を説明します。

1. `git init`をGit管理ディレクトリ下で実行し、空レポジトリを作成
2. `git add ファイル名`で、Git管理したいファイルを指定
3. `git commit`で、コミット

今回の実験ではコミットまでしかしないので、以上の知識で十分です。
他のバージョン管理システムに比べ、`add`というひと手間あるのが特徴です。

>この`add`という手間増えた、というだけでGitを嫌いにならないでください。
>この操作が残ったのにはちゃんと理由があります。詳しくは
>[git add ってなんのためにやるの？ Gitの「ステージング」をイラストで解説します！](https://kray.jp/blog/expound-git-add/)
>を読むと良いです。ネコかわいいですし。

なお、右クリックでコンテキストメニューから"Git Bash Here"を選択するとコマンドプロンプトが表示されるので、ここからGitコマンドを実行すると便利です。
（"Git Bash Here"コンテキストメニューはGitインストール時に追加されます）

![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292064/a0873e47-be9e-6cb7-7dda-14d2152469cc.png)


#実験に必要なコマンド解説（Gitを知っている人むけ）
普段からGitを使う人でも`cat-file`コマンドは使った人は少ないと思います。
`cat-file`はレポジトリに格納されたファイルの中身を見るコマンドです。レポジトリのファイルは圧縮されているためエディタ等で直接覗いても訳が分からないです。ですので、このコマンドで中身を表示します。`git cat-file -p ファイルのハッシュ値`というように使います。（ハッシュ値は最初の4文字だけでOKです）

以下はコミットオブジェクトの中身を`cat-file`で見た例です。　

```shell
$ git cat-file -p 73a4
tree 07e50f691efd71440ab479e9e410ac316699a84d
author WOZ <woz@xxx.com> 1604591870 +0900
committer WOZ <woz@xxx.com> 1604591870 +0900

initial commit
```


#initしてみる
それでは実験してみます。
`git init`コマンドを実行すると、カレントディレクトリに`.git`という隠しフォルダが作成されます。この.gitフォルダがgitレポジトリの実態です。
.git以下のフォルダ構成は次のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292064/7183c4c9-5d7e-61ce-cbab-f5399586f43c.png)
この中で一番大事なのは`objects`フォルダです。実データはここに保存されます。ですので、初期状態で`objects`フォルダには空フォルダしかありません。

#init直後に1ファイルだけaddしてみる
`init`直後に1ファイルだけ`add`すれば、`add`による影響しかないので`add`コマンドが何をしているか分かるはずです。

Gitのことが分からないあなたは"Whats?Git"というファイルを作ったとします。この"Whats?Git"と書かれたファイルを`add`するとどうなるか？やってみます。

```shell
$ echo 'Whats?Git' > test.txt

$ git add .
```

すると、objectsの下のフォルダが追加され、その中に暗号のようなファイル名でファイルができます。

```shell
$ ls -R .git/objects/
.git/objects/:
19/  info/  pack/  ←　"19"というフォルダが増えた

.git/objects/19:
6cb509dbf49301f36e616c55f6cb161ddce3d2　←　このファイルができた

.git/objects/info:　←　初期状態からある空フォルダ

.git/objects/pack:　←　初期状態からある空フォルダ
```

この、2文字のフォルダ名＋長い暗号のようなファイル名、がファイルのハッシュ値です。先ほどaddした"Whats?Git"のハッシュ値を計算すると```196cb509dbf49301f36e616c55f6cb161ddce3d2```になる、というわけです。
（ファイル名＝ハッシュ値なら分かりやすいと思うのですが、きっと格納効率を考えてこうなっているのでしょう）
ハッシュ値が分かれば`cat-file`コマンドが使えるので、さっそく中身を確認します。

```shell
$ git cat-file -p 196c
Whats?Git
```

確かに先ほどのファイルでした。


#ファイルの中身を変えてaddしてみる
変更したファイルがGitでどのように扱われているかを実験するために、先ほどのファイルを1文字書き換えてみます。
当初はチンプンカンプンだったあなたもGitの素晴らしさに感激し"Whats!Git"に変えた、とします。（?が!に変わった）

```shell
$ echo 'Whats!Git' > test.txt

$ git add .
```

"Whats!Git"をaddした直後のレポジトリは次のようになりました。

```shell
$ ls -R .git/objects/
.git/objects/:
13/  19/  info/  pack/  ←　"13"というフォルダが増えた

.git/objects/13:
99fbe8aa852bda75cfb66b196d78fb5908c1d9　←　このファイルができた

.git/objects/19:
6cb509dbf49301f36e616c55f6cb161ddce3d2

.git/objects/info:

.git/objects/pack:
```

1文字変えただけですがハッシュ値はまったく変わってしまいました。（ハッシュ値とはそういうものなので）
新しく増えたファイルの中身も確認してみます。

```bash
$ git cat-file -p 1399
Whats!Git
```

確かに、"!"の方のファイルでした。


#ファイルをもとに戻してaddしてみる
またファイルの中身を"?"に戻してaddしてみます。

```shell
$ echo 'Whats?Git' > test.txt

$ git add .

$ ls -R .git/objects/
.git/objects/:
13/  19/  info/  pack/

.git/objects/13:
99fbe8aa852bda75cfb66b196d78fb5908c1d9

.git/objects/19:
6cb509dbf49301f36e616c55f6cb161ddce3d2

.git/objects/info:

.git/objects/pack:
```

今回、レポジトリの中身は全く変わりませんでした。"Whats?Git"のハッシュ値を計算すると``1399～``になりますが、その名前でファイルを作成しようとしたところ先約がいたのでファイルが作成されなかなった、という訳です。

#コミットしてみる
以上の実験で、`add`コマンドでファイルがレポジトリにバックアップされることが分かりました。なので、`add`さえすれば、ファイルは原理的には復元可能です。ですが、そのファイルがどのバージョンのものか？はまだ管理されていませんし、そもそもレポジトリから復元したいファイルが100個あったら`cat-file`コマンドを100回打たなくてはならなくなり面倒です。

頭のいい方は「ハッシュ値一覧も覚えておけば、そこから特定バージョンのファイルセットを全て復元でできるんでない？」と思ったはずです。実はその「ハッシュ値一覧」を覚える処理が、Gitではコミットに当たります。

それでは、commitの実験をしてレポジトリを見てみましょう。


```shell
$ git commit -m '最初のコミット'
[main (root-commit) 75033d9] 最初のコミット
 1 file changed, 1 insertion(+)
 create mode 100644 test.txt

$ ls -R .git/objects/
.git/objects/:
13/  19/  1c/  75/  info/  pack/

.git/objects/13:
99fbe8aa852bda75cfb66b196d78fb5908c1d9

.git/objects/19:
6cb509dbf49301f36e616c55f6cb161ddce3d2

.git/objects/1c:
e0a15ac967c4c7f4200cb5a27b96898c20c32a　←　このファイルができた

.git/objects/75:
033d99534b359c6d388be13363f1fb8c9ed360　←　このファイルができた

.git/objects/info:

.git/objects/pack:
```

ファイルが2つ増えました。2つのうちどちらがハッシュ値一覧なのでしょうか？答えを先に言ってしまってものいいのですがここは落ち着いて順番に調べていきます。
注目してほしいのは、コミットしたときの以下のメッセージです。

```shell
[main (root-commit) 75033d9] 最初のコミット
```

ここに`75033d9`というハッシュ値っぽいものが表示されています。今回増えた2ファイル`1c/e0a15a...`と`75/033d99...`のうち後者と一致します。というわけで、`7503...`の中身を見てみます。


```shell
$ git cat-file -p 7503
tree 1ce0a15ac967c4c7f4200cb5a27b96898c20c32a
author WOZ <woz@xxx.com> 1611998271 +0900
committer WOZ <woz@xxx.com> 1611998271 +0900

最初のコミット
```

ハッシュ値一覧ではありませんでした。惜しい！

ですが、この中に答えがあります。`tree`から始まる行にハッシュ値があり、これが今回増えたもう一つのファイルのハッシュ値と同じです。なんだか宝探しゲームで地図に示された場所にいくと、次の場所が描かれた別の地図が出てきたような感じですね。

ではこの中身がハッシュ値一覧がなのか？見てみましょう。

```shell
$ git cat-file -p 1ce0
100644 blob 196cb509dbf49301f36e616c55f6cb161ddce3d2    test.txt
```

"Whats?Git"ファイルのハッシュ値がありました！お宝Getです。

まとめると
コミットのハッシュ値　→　treeというハッシュ値　→　ファイルのハッシュ値
と辿ることで、目的のファイルにたどり着くことができました。

Gitが過去バージョンを復元するとき（つまりcheckoutのとき）上記の手順でファイルをレポジトリから見つけ、作業フォルダに復元している、というわけです。

#バイナリファイルも試してみる
「ファイルを圧縮してレポジトリに格納しているだけ」ならば、バイナリファイルも難なく扱えるはずです。今回はGitのロゴのpngファイルをコミットしてみました。
![before.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292064/95966489-6816-d351-8599-ead0cd60445c.png)

これまでと同様`cat-file`コマンドでレポジトリから復元できるはずです。ただ、バイナリファイルを`git cat-file -p ～`とやっても訳わからん文字が出力されるだけなので、出力結果はリダイレクトで受け取ります。

```shell
$ git cat-file -p 18c5 > a.png
```

以下が結果です。まったく同じファイルが復元されました！
![after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292064/c1a3393e-7096-4d9e-b2f1-9ce10d2a0ac8.png)

「Gitはソースコード管理のためのもの」と思っている方もいるかと思いますが、バイナリファイルも全然いけます。

#ちょっとモヤモヤした人むけ話
**"Whats!Git"ファイルはどうなっちゃたの？**
実体はレポジトリに存在するのに復元はできない、かわいそうなファイルになります。生まれたものの戸籍に登録されていないからです。さっき「addしたら原理的に復元可能」と言いましたが、レポジトリの膨大なファイルの中からお目当てのファイルを見つけ出すのは現実的ではありません。
その代わり、commitしたファイルはどうにかなります。表面上消えたとしても復活させる仕組みがGitにはあります（詳しくは`git reflog`でググってください）。
逆に、「要らないものがレポジトリに残り続けて気持ち悪い」と思われるかもしれませんが、ある一定期間過ぎるとガーベージコレクトで消えることになっています。今すぐ消したいときは`git gc --prune=now`というコマンドを使います。

**レポジトリに格納されるのは差分じゃないの?**
Subversionとか一般的なバージョン管理システムはバージョン間の差分をレポジトリに記録する方式ですが、Gitは根本的に違います。Gitはコミットのスナップショットを記録する方式です。仕組み上「1文字でも変えたら違うファイルとして保存される」ためレポジトリの容量が大きくなるというデメリットがありますが、メリットもあります。
一番のメリットはレポジトリの変更履歴を自由に編集できることだと思います。一般的なバージョン管理システムの場合、レポジトリの編集は基本できません。コミットの順番を変えたら復元できなくなる恐れがあるからです。ですが、Gitではコミットがどのような経緯できたは関係ないので、コミットの順番を変えても難なく復元できます。
一般的なバージョン管理システムでは履歴が汚くなるのを恐れるあまり気安くコミットできませんが、Gitは「後からコミットの履歴を変更できる」ため気軽にコミットする事ができます。（ただし、ローカルレポジトリに限った話です）

#最後に
Gitを使ってない頃、私はバックアップ方法としてフォルダを圧縮して保存していました。そして、Gitを使っている今も「ファイルを圧縮して保存」するという点では同じ事をやっていたわけです。同じことをやるなら便利なGitの方がいいですよね。
同じようにフォルダを圧縮したりコピーしたりしてバックアップをとっている人がこの記事を読んでGitに移行してもらえたなら、本記事を書いた甲斐があったというものです。一人でも多くの方がGitに移行して頂けると大変嬉しく思います。