🕊️

[Inside Git #1] Gitはどのようにバージョン管理をしているのか?

2022/04/10に公開

会社の同期の方と一緒に勉強会をしていて、せっかくなので発表内容を記事としてまとめることにしました。
ある時、ふと、Gitはどのように過去のファイルの状態を記憶しているんだろう?と疑問に感じました。今回はその疑問について調べた内容、Gitオブジェクトについてまとめていこうと思います!!

環境

  • MacOS Monterey 12.3.1
  • git version 2.35.1

対象読者

  • Gitを多少触ったことがあれば理解できると思います。

.gitの誕生

まず、Git管理下に置きたいディレクトリの中でgit initを実行すると.gitという隠しディレクトリが作成されます。Gitはこの.gitディレクトリの中のファイルを操作することで、Git管理下にあるファイル群のバージョン管理をしています。

試しにrepoというディレクトリを作成して、git initしてみましょう!

% mkdir repo; cd repo
% git init
% ls -a  # .gitディレクトリが作成されている
.    ..   .git

GitにはGitオブジェクトという超重要概念があります。Gitでのバージョン管理とは、このGitオブジェクトの管理と言っても過言ではありません。
このGitオブジェクトは.gitディレクトリの中のobjectsというディレクトリの中に保存されていきます。今回は、このGitオブジェクトがどんなものなのかを観察していき、Gitがどのようにバージョン管理をしているのかを簡単に説明したいと思います!

.gitディレクトリの中には他にも、HEADが現在何を参照しているかが記載されているHEADファイルや、各ブランチがどのコミットを参照しているかなどの様々な参照情報が記載されているrefsディレクトリなどがこの.gitの中に保存されています。.gitの中身について深追いすることはしませんが、これらHEADrefsについても必要最低限、軽く解説しようと思っています!

% ls -F1 .git
HEAD
config
description
hooks/
info/
objects/
refs/

コミットの正体

まずはいくつか適当なコミットを積んで、その過程で出来上がったコミットを観察していくことにしましょう。

% touch README.md
% git add -A && git commit -m 'README.mdの作成'

% touch curry-ingredients.md
% git add -A && git commit -m 'curry-ingredients.mdの作成'

% echo '# カレーのレシピ' > README.md
% git commit -am 'README.mdの更新'

% echo '- にんじん' >> curry-ingredients.md
% echo '- じゃがいも' >> curry-ingredients.md
% echo '- カレールー' >> curry-ingredients.md
% git commit -am 'curry-ingredients.mdの更新'

% tree
.
├── README.md
└── curry-ingredients.md

0 directories, 2 files

まずは最新のコミットの情報をgit logで調べてみます。

% git log -1
commit bf734d3ec625afdec625acfd010f084501ba6305 (HEAD -> main)
Author: Sayama <sayama@example.co.jp>
Date:   Sun Jan 15 15:10:39 2023 +0900

    curry-ingredients.mdの更新

このbf734d3ec625afdec625acfd010f084501ba6305という意味の分からない文字列[1]は、あるGitオブジェクトのハッシュ値[2]になっています。実際のGitオブジェクトは

.git/objects/bf/734d3ec625afdec625acfd010f084501ba6305

というファイルに記されています[3]。しかしこのファイルはバイナリ形式になっているため、直接覗くことは難しいです。しかしgit cat-fileというコマンドが用意されていて、これを使うことで中身を詳しく見ることができるので、早速覗いてみましょう!

% git cat-file -t bf734d3  # -tでGitオブジェクトのタイプが見れます
commit

% git cat-file -p bf734d3  # -pでGitオブジェクトの中身が見れます
tree 433babe388be6759d8e6d51e2b8c388854e51324
parent bb25ab1539bfcd5f80ff7d37f60b43a643306300
author Sayama <sayama@example.co.jp> 1673763039 +0900
committer Sayama <sayama@example.co.jp> 1673763039 +0900

curry-ingredients.mdの更新

このGitオブジェクトはcommitというタイプで、authorcommitterの情報、コミットメッセージなどが記載されていることがわかります。他に、2つの重要な情報が記載されています。
parentという項目にあるGitオブジェクト:bb25ab1539bfcd5f80ff7d37f60b43a643306300
treeという項目にあるGitオブジェクト:433babe388be6759d8e6d51e2b8c388854e51324

一旦、tree項目に書かれた謎のGitオブジェクトは無視しましょう。parentという項目にあるGitオブジェクトを再度調べてみます。

% git cat-file -t bb25ab1
commit

% git cat-file -p bb25ab1
tree d13fd5f50504dfa30597a297bb65d1581c4623dd
parent a4c662bed612919fac1f6171ffbb65f783f85fc6
author Sayama <sayama@example.co.jp> 1673763016 +0900
committer Sayama <sayama@example.co.jp> 1673763016 +0900

README.mdの更新

またしてもcommitというタイプのGitオブジェクトでした。このタイプのGitオブジェクトのことを我々はコミットと呼んでいます。コミットはコミットメッセージなどの情報の他に一つ前のコミットである親コミットのハッシュ値を持っています。
このように最新のコミットは、ひとつ前の親コミットを参照し、そのコミットはまたひとつ前の親コミットを参照し、と言った具合に参照の列を作っています。
この参照の列によりバージョンの時間的経過を表すことができます。コミット、すなわちcommitタイプのGitオブジェクト全体のことを、ヒストリーまたはタイムラインと言います。

一旦、分かったことを簡単にまとめると、bf734d3というコミットはbb25ab1という親コミットを参照していて、bb25ab1というコミットはa4c662bという親コミットを参照していて、というような参照の列があるという事がわかりました。

% git log --oneline
bf734d3 (HEAD -> main) curry-ingredients.mdの更新
bb25ab1 README.mdの更新
a4c662b curry-ingredients.mdの作成
38180f9 README.mdの作成

この節のまとめ

  • コミットの正体はcommitというタイプのGitオブジェクトのことだった。
  • コミットはひとつ前の親コミットを参照していて、その親コミットはそのまた親コミットを参照していて、と参照の列が作られている。
  • 出来上がった参照の列がバージョンの時間的経過を表す。

 

ブランチの正体

前節では、コミットとは何かということをみました。
続いてはブランチとは何かについて観察していこうと思います。

後で比較するために、まずは.git/refs/headsディレクトリの中身を見ておこうと思います。

% ls .git/refs/heads
main

% cat .git/refs/heads/main
bf734d3ec625afdec625acfd010f084501ba6305

カレーの材料に鶏肉を入れ忘れていたので、meatブランチを作成してコミットを積んでおきましょう。

% git switch -c meat
% echo '- 鶏肉' >> curry-ingredients.md
% git commit -am '材料に鶏肉を追加'
% git log --oneline
f7986e8 (HEAD -> meat) 材料に鶏肉を追加
bf734d3 curry-ingredients.mdの更新
bb25ab1 README.mdの更新
a4c662b curry-ingredients.mdの作成
38180f9 README.mdの作成

% git switch main
% echo '美味しいカレーを作ろう!' >> README.md
% git commit -am 'README.mdの更新'
% git log --oneline
8add7f0 (HEAD -> meat) README.mdの更新
bf734d3 curry-ingredients.mdの更新
bb25ab1 README.mdの更新
a4c662b curry-ingredients.mdの作成
38180f9 README.mdの作成

ブランチの実態を見るために.git/refs/headsの中を覗いてみると、新たにmeatというファイルが作成されていることがわかります。

% ls .git/refs/heads
main meat

% cat .git/refs/heads/main
96dcb8ee86c2c6f10c913f4d4b64e442cb2b3d33
% cat .git/refs/heads/meat
f7986e8e9bd2011d4433cd81ff383b137c538523

これらのmainmeatファイルの中身からも分かるように、ブランチとはあるコミットへの参照 (リファレンス) のことです。ブランチというと、根本から枝先までの一連のコミット群をイメージしてしまいますが、そうではなくただの参照値のことで、もっと雑な言い方をすればただの文字列です。従って、1000個くらいブランチを作ったとしても、データのサイズは大して増えません。

もうひとつ重要なリファレンスがあります。それがHEADです。HEADは現在のバージョンを指し示すリファレンスのことです。HEADが指す参照値は.git/HEADというファイルに記載されています。

% cat .git/HEAD
ref: refs/heads/main

どうやらHEADはmainブランチを参照しているようです。

色々とスイッチしていくとHEADファイルの中身も変化していきます。

% git switch meat  # meatブランチへスイッチ
% cat .git/HEAD
ref: refs/heads/meat

% git switch bb25ab1 --detach  # bb25ab1コミットへスイッチ
% cat .git/HEAD
bb25ab1539bfcd5f80ff7d37f60b43a643306300

このようにHEADはブランチを参照することもあれば、コミットを直接参照することもあります。

HEADが直接コミットを参照している状態のことをdetached HEADというふうに言います。
detached HEADの状態でコミットを積むこともできますが、特に何もせずに既存のブランチにスイッチしてしまうと、誰からも参照されないコミットが出来上がってしまいます。そして、参照の向きから考えれば、あなたが数日後にこのコミットを見つけようと思っても、かなり厳しいんじゃないでしょうか。このようなごみコミットは時間が経てばGitのガベージコレクションが掃除してくれますが、あまり気持ちのいい状態とは言えません。detached HEADの状態にする時は、このようなことを分かった上で行うべきですね。

この節のまとめ

  • ブランチはあるコミットを参照するだけのリファレンス。
  • HEADはブランチやコミットを参照するだけのリファレンスで、現在の状態を表す。

 

Gitオブジェクトの正体

treeオブジェクト / blobオブジェクト

ところで、少し前にcommitオブジェクトの中身を調べた時に、treeという項目に謎のGitオブジェクトのハッシュ値が書かれていました。ここではこの謎のGitオブジェクトを調べていこうと思います!
調べる前に、説明のために使いたいコミットをひとつだけ積もうと思います。

% git switch main
% mkdir dir
% echo '# カレーのレシピ\n美味しいカレーを作ろう!' > dir/sample.txt
% git add -A && git commit -m 'README.mdと同じ内容のファイルを作成'
% tree
.
├── README.md
├── curry-ingredients.md
└── dir
    └── sample.txt

1 directory, 3 files

では早速調べていきましょう!

% git log --oneline -1
845a32f (HEAD -> main) README.mdと同じ内容のファイルを作成

% git cat-file -p 845a32f
tree 0cdbafebf15332c0788686f2457a87d8ea3ddbf5  # こいつを調べます
parent 8add7f0af64c4166aa39f3b2430c07c2f3bd3d4f
author Sayama <sayama@example.co.jp> 1673766412 +0900
committer Sayama <sayama@example.co.jp> 1673766412 +0900

README.mdと同じ内容のファイルを作成

% git cat-file -t 0cdbafe
tree

% git cat-file -p 0cdbafe
100644 blob 944b8ef2e83aea596fd2a662d629042f3e92edc3	README.md
100644 blob 87f3f8afa28796b2eeda4094bee471acbde78dcc	curry-ingredients.md
040000 tree 6fc8f11b5d479640d1c79f9f8697c35f66d08f67	dir

Gitオブジェクト0cdbafetreeというタイプだということがわかりました。今後はこのタイプのGitオブジェクトのことを簡単にtreeオブジェクトということにします。

treeオブジェクト0cdbafeの中身を見てみると3つのGitオブジェクトについての情報が記載されていそうですね。多分こんな感じの内容なんじゃないでしょうか。

  • README.mdに対応していそうなGitオブジェクト944b8ef。おそらくタイプはblob
  • curry-ingredients.mdに対応していそうなGitオブジェクト87f3f8a。おそらくタイプはblob
  • dirに対応していそうなGitオブジェクト6fc8f11。おそらくタイプはtree

blobという新たなタイプと思われるGitオブジェクトも登場したので、本当にそうなのか、こちらもgit cat-fileで詳しく調べておきましょう!

% git cat-file -t 944b8ef
blob

README.mdに対応していそうなGitオブジェクト944b8efは、確かにblobというタイプだということがわかりました!今後はこのタイプのGitオブジェクトのことを簡単にblobオブジェクトということにします。

さて、これでtreeオブジェクトとblobオブジェクトの存在を確認することができましたが、これらは一体何者なのでしょう?もう一度、treeオブジェクト0cdbafeの中身をよく観察してみましょう。

% git cat-file -p 0cdbafe
100644 blob 944b8ef2e83aea596fd2a662d629042f3e92edc3	README.md
100644 blob 87f3f8afa28796b2eeda4094bee471acbde78dcc	curry-ingredients.md
040000 tree 6fc8f11b5d479640d1c79f9f8697c35f66d08f67	dir

% tree
.
├── README.md
├── curry-ingredients.md
└── dir
    └── sample.txt

1 directory, 3 files

すると、treeオブジェクト0cdbafeに書かれている3つのファイル/ディレクトリ名は、Git管理下 (repo/) の直下にある3つのファイル/ディレクトリ名と一致していることが分かります!
さらに、ファイルはblobオブジェクトに、ディレクトリはtreeオブジェクトに対応していることも分かります!

blobオブジェクト944b8efの中身についてもよく観察してみましょう!

% git cat-file -p 944b8ef
# カレーのレシピ
美味しいカレーを作ろう!

% cat README.md
# カレーのレシピ
美味しいカレーを作ろう!

すると、README.mdに対応していそうなblobオブジェクト944b8efの中身はまさしく、README.mdの中身そのものだという事が分かります!

以上の観察を簡単にまとめてみます。

  • treeオブジェクトはディレクトリに対応していそうで、その中身には対応するディレクトリ直下にあるファイル/ディレクトリに対応するGitオブジェクトが書かれている。
  • blobオブジェクトはファイルに対応していそうで、その中身には対応するファイルの中身が書かれている。

実はこれらの予想は正しく、treeオブジェクトはディレクトリを表すGitオブジェクトのことです。中にあるファイルやディレクトリの種類/権限を表す6桁の数字 (100644, 040000)、それらファイルやディレクトリに対応するGitオブジェクトのタイプ (blob, tree)、それぞれのGitオブジェクトのハッシュ値 (944b8ef..., 87f3f8a...)、ファイル名/ディレクトリ名 (README.md, curry-ingredients.md) が記載されていることがわかります。ここでひとつポイントになっている点は、treeオブジェクトは自分自身のディレクトリ名の情報を持っていないことです。

そしてもちろん、blobオブジェクトとはファイルを表すGitオブジェクトのことです。blobオブジェクトの中にはファイルの中身そのものが記載されています。また、ここでもポイントになるのが、このblobオブジェクトは自分自身のファイル名の情報を持っていないということです。

最後に他のtreeオブジェクト/blobオブジェクトの中身も観察して、この節で分かったポイントをまとめておきましょう。

# dirに対応するtreeオブジェクト6fc8f11について
% git cat-file -p 6fc8f11
100644 blob 944b8ef2e83aea596fd2a662d629042f3e92edc3	sample.txt

% tree dir
dir
└── sample.txt

0 directories, 1 file

# curry-ingredients.mdに対応するblobオブジェクト87f3f8aについて
% git cat-file -p 87f3f8a
- にんじん
- じゃがいも
- カレールー

% cat curry-ingredients.md
- にんじん
- じゃがいも
- カレールー

この節のまとめ

  • commitタイプのGitオブジェクト(コミット)には、親コミットのハッシュ値以外に、ルートディレクトリに対応したtreeオブジェクトのハッシュ値も記載されている。
  • treeタイプのGitオブジェクトは、ディレクトリに対応していて、自身に含まれるファイルやディレクトリに対応するGitオブジェクトのハッシュ値と、それらの名前が記載されている。ただし自分の名前は知らない。
  • blobタイプのGitオブジェクトは、ファイルに対応していて、自身の中身が記載されている。ただし自分の名前は知らない。

treeやblobオブジェクトは自分の名前を知らなくてもいい

treeオブジェクトやblobオブジェクトがディレクトリやファイルに対応しているが、自分自身の名前の情報を持っていないことを上で観察しました。自分の名前を知らない、これらtreeオブジェクトやblobオブジェクトから元のディレクトリ・ファイル群を再現できないのでは?と一瞬思うかもしれません。しかし、無問題です!
あるblobオブジェクトは、対応するファイルの中身を知っています。そしてそのファイル名は、このblobオブジェクトを含むtreeオブジェクトに記載されています。このtreeオブジェクトに対応するディレクトリの名前は、その親のtreeオブジェクトに記載されていて、その親の名前のそのまた親に記載されて、という具合に、ルートディレクトリに対応するtreeオブジェクトまで遡れば、全てのファイルの名前と中身、全てのディレクトリの名前と中身がきちんと再現できることがわかります。そしてルートディレクトリに名前は必要ありません。

この節のまとめ

  • コミットはルートディレクトリに対応するtreeオブジェクトを参照している。
  • treeオブジェクトはディレクトリに対応している。
  • blobオブジェクトはファイルに対応している。
  • これらtreeオブジェクトとblobオブジェクトがあれば、ディレクトリ・ファイル群の状態を再構築できる。
【余談 ☕️ 】参照の向きと、親子

commitオブジェクトの時は、参照先を親コミットと呼びましたが、tree/blobオブジェクトの時は参照元のtreeオブジェクトのことを親と呼びました。

親コミットという言い方は一般的ですが、tree/blobオブジェクトに関しては僕のオリジナルです。ディレクトリ構造を考えたときに、サブディレクトリを子と呼びたい気持ちから、参照元を親と呼んでいます。

 

ハッシュ値の計算

先ほど、treeオブジェクトを調べた際に、README.mddir/sample.txtに対応するblobオブジェクトのハッシュ値が全く同じものだったことに気づいてましたか?

% git cat-file -p 0cdbafe
100644 blob 944b8ef2e83aea596fd2a662d629042f3e92edc3	README.md   # ハッシュ値が全く同じ!
100644 blob 87f3f8afa28796b2eeda4094bee471acbde78dcc	curry-ingredients.md
040000 tree 6fc8f11b5d479640d1c79f9f8697c35f66d08f67	dir

% git cat-file -p 6fc8f11
100644 blob 944b8ef2e83aea596fd2a662d629042f3e92edc3	sample.txt  # ハッシュ値が全く同じ!

実はblobオブジェクトのハッシュ値は、そのファイルの中身の情報だけから計算されます。自分自身のファイル名などの情報は一切使いません!
あなたのPCで、これらREADME.mdと同じ内容のファイルを作成したなら、僕と全く同じハッシュ値をもつblobオブジェクトが作成されているはずです!

もうひとつ例を見ておきましょう。例えば、初めの方のコミットで2つの空ファイルを追加しています。対応する空ファイルのハッシュ値はやはりどちらもe69de29になっています[4]

% git cat-file -p a4c662b
tree eda14422e5a939043e04ea00fc403e0ee1798a44  # 空のファイルが2つあるはず
..

% git cat-file -p eda1442
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391	README.md             # ハッシュ値が全く同じ!
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391	curry-ingredients.md  # ハッシュ値が全く同じ!

このように、ファイルの中身だけを考えていれば、「同じ中身なのにファイル名が違うため異なるblobオブジェクトが必要」というような無駄が無くなります。Gitオブジェクトが節約できるんですね!

treeオブジェクトのハッシュ値も全く同じ考え方で計算されています。treeオブジェクトの中に書かれている、ファイル/ディレクトリ名やそれらに対応するGitオブジェクトのハッシュ値から、自身のハッシュ値が計算されています。つまり自分自身のディレクトリ名の情報はハッシュ値の計算に全く使用されません!
もしあなたが一緒に手を動かしながら、この記事の内容を辿ってきたなら、あなたのPCで計算されたblobオブジェクト、treeオブジェクトのハッシュ値は全く同じものになっているはずです!

一方で、commitオブジェクトのハッシュ値はおそらく僕のそれと別の値になっていると思います。
これはcommitオブジェクトのハッシュ値が、authorcommitterなどの情報も含めて、計算されているためです。僕とあなたとでは、名前もメールアドレスも、コミットした時間も違うため異なるハッシュ値になります。

この節のまとめ

  • Gitオブジェクトはその中身の情報だけからハッシュ値が計算されている。

 

変化の伝播とファイル群の再現

最後に、Gitがどのようにコミット単位でファイル群の状態を記憶しているのかを説明して終わりにしたいと思います。

あるファイルの中身が変更されると、その変更されたファイルのblobオブジェクトの中身も変更されるわけですから、そのハッシュ値が変化します。すると、親のtreeオブジェクトに記載されているblobオブジェクトのハッシュ値も変更されるため、もちろんそのtreeオブジェクトのハッシュ値も再計算され、別のハッシュ値が与えられます。すると、ハッシュ値が変化したこのtreeオブジェクトの親treeオブジェクトも、同様の理由により新たなハッシュ値が与えられます。この変化の伝播が繰り返されると必ずルートディレクトリに対応するtreeオブジェクトのハッシュ値が変化します。

これはもちろん、あるディレクトリの名前を変更した時も同様です。treeオブジェクトのハッシュ値の変更が木の根本まで伝播していき、最終的には必ずルートディレクトリに対応するtreeオブジェクトのハッシュ値が変化します。

事実1
「blobオブジェクトやtreeオブジェクトのハッシュ値が、その中身から計算されていること」
「treeオブジェクトには子であるblob/treeオブジェクトのハッシュ値が記載されていること」
この2つの事実から、「木構造の末端であるblobオブジェクト(ファイル)や枝の途中であるtreeオブジェクト(ディレクトリ)が更新されると、最終的にルートディレクトリに対応するtreeオブジェクトのハッシュ値が変化する」 ということがわかりました。もう少し別の言い方をすると、「その時のファイル群の状態に応じて、ルートディレクトリに対応するtreeオブジェクトのハッシュ値が一意[5]に決められる」 ということがわかりました。

そして、commitオブジェクト (コミット) にはルートディレクトリに対応するtreeオブジェクトのハッシュ値が記載されているので、そのコミットから、その時のファイル群の状態を再構築することができます。

事実2
「commitオブジェクトは、ルートディレクトリに対応するtreeオブジェクトを参照している」
「ルートディレクトリに対応するtreeオブジェクトから、その時のファイル群の状態を再構築できる」
この2つの事実から、「Gitはコミット単位でファイル群の状態を覚えておくことができる」 ということがわかりました。

例えば、meatブランチにスイッチすると、HEADmeatブランチを参照します。そしてmeatブランチはあるコミットを参照しています。そしてそのコミットは、ルートディレクトリに対応するtreeオブジェクトを参照しています。このtreeオブジェクトからその時のファイル群の状態を再構築できます。

事実3
HEADは現在の状態を表す」
「適当なブランチにスイッチするとHEADの参照先が変化する」
「ブランチはあるコミットを参照している」
「コミットからその時の状態を再現することができる」
この4つの事実から、「適当なブランチにスイッチすると、その時のファイル群の状態に戻すことができる」 というよく知られた事実が再確認できました!

 

紹介しなかった部分

  • Gitオブジェクトにはcommittreeblobの他にもtagというタイプのGitオブジェクトがあります。
  • indexについては全く触れませんでした。
  • Gitオブジェクトの圧縮や中身のフォーマットについても触れませんでした。

その他諸々、紹介しきれなかった部分、もしくはただ僕が知らないだけの部分、などなどたくさんあるかもしれませんが、今回はここで終わろうと思います。ありがとうございました!

 

参考にしたもの

今回の記事を書くのに、以下のサイトを参考にしました。
こせきの技術日記 Gitの仕組み(1)
非常にわかりやすい記事で、僕の記事はただの縮小再生産のようなものです。一応、僕の言葉でわかりやすい説明内容を目指しましたが、「よく分からんかった」「もっと詳しく知りたい」という方は、この種記事の方も参考にしてみてください。超面白かったです。

脚注
  1. あなたのPCで同じ作業をしたとしても、このコミットのハッシュ値は異なる値になっているはずです。しかし、この記事の中に出てくる一部のGitオブジェクトのハッシュ値はあなたのPCで作成したものと同じになるものがあります。Gitオブジェクトの理解につながると思うので、是非あなたのPCでも試してみてください! ↩︎

  2. ハッシュ値とはざっくり言うと、ある値から計算される整数値のことです。今回はGitオブジェクトから16進数表示された整数値であるbf734d3ec625afdec625acfd010f084501ba6305のような値を計算して、これをこのGitオブジェクトのハッシュ値と呼んでいます。ハッシュ値はできるだけ被らないことが大切です。例えば全く別のGitオブジェクトAとBについて、同じハッシュ値になってしまってはGitシステムくんはどっちがどっちのGitオブジェクトなのか区別をつけることができなくなってしまいます。しかし今回は40桁の16進数ということですから、うまくハッシュ値を計算することができているなら、Gitオブジェクトのハッシュ値が被ってしまう確率は、あなたが毎年連続で年末ジャンボ宝くじに当たる確率より遥かに低いです。どうせ一回も当たったことないと思うので安心してください! ↩︎

  3. bfが分かれてディレクトリ名になっているのは、おそらく検索性を高めるためだと思っています。しかし、実際の理由は知りません。 ↩︎

  4. 空ファイルのようなあるあるファイルのハッシュ値は、世界中の様々なPCで全く同じものが作成されています。このハッシュ値を検索してみると、いくつかヒットするので、確かにblobオブジェクトのハッシュ値はその中身にしか依存していないんだなということが実感できます。 ↩︎

  5. 被りなくという意味です。 ↩︎

Discussion