「ブランチを切る」と聞くと、コードをまるごとコピーする操作を想像しがちです。私も長いあいだそう思っていました。けれど実物を覗くと、ブランチの正体は たった 40 文字が書かれた 1 個のテキストファイル です。
コミットも、ブランチも、.git の中では驚くほど単純なデータとして存在しています。その実物を一度見ておくと、AI に「この変更だけ取り消して」「気に入らないからブランチごと捨ててやり直して」と頼むときの判断が一段速くなります。何を操作しているのかが、頭の中で像を結ぶからです。
第 3 回は、前回そろえた環境を使って、Git の内部を実際に解剖します。使い捨てのリポジトリ (Git 管理する場所) を 1 つ作り、.git の中を自分の目で覗いていきます。
TL;DR
- Git の保管庫に入るのは blob(中身)・tree(名前の目次)・commit(誰が・いつ・親)・tag(目印) の 4 種類だけ。コミットという 1 枚の写真を形づくるのは前の 3 つで、tag は特定のコミットに貼る名札です。
- オブジェクトの名前は 中身から計算した「住所」(ハッシュ値)。だから同じ中身は 1 つしか保存されず (重複排除)、1 バイトの改ざんもすぐバレます。
- ブランチは 40 文字のしおりにすぎません。だから作成も切り替えも一瞬で、気軽に捨ててやり直せます。
- 容量を決めるのは枝の数ではなく「変更した中身の量」。blob は完全版で保存され、ディスク上の容量は裏側の差分圧縮が抑えています。
解剖の準備
Git を理解する為に、ターミナルで使い捨てのリポジトリを作ります。Microsoft の VSCode を使うと便利です。ダウンロード してインストールしておきましょう。続いて以下のように操作します。
- デスクトップに
git-labという名前でフォルダ作成 - そのフォルダを VSCode にドラッグ&ドロップ
git-labが VSCode で開く- VSCode の設定を開く (Windows:
Ctrl+,/ Mac:⌘+,) - 検索窓に
files.excludeと入力し**/.gitの除外項目を削除し設定画面を閉じる (これで VSCode で.git フォルダが見えるようになる)
6. VSCode 右上のパネルの切替えをクリックしターミナルを表示
7. ターミナルに以下の Git 管理を始めるためのコマンドを入力し、Enter キーで実行
git init -b main
エクスプローラーで.git フォルダが作られたことが確認できます。

.git の中で、これから注目するのは 3 つです。
objects/がオブジェクトの保管庫refs/が参照 (ポインタ) の置き場HEADが「今どこにいるか」を示すファイルです。
ひとつ先に約束ごとを。これ以降のコマンド例に出てくる 56266d36... のような英数字は ハッシュ (住所) で、末尾の ... は「以下省略」の意味です。そのままコピーしても動きません。自分のターミナルに出た値(先頭 7〜8 文字でも通じます) に読み替えて打ってください。とくにコミットやブランチの住所は、作った時刻や著者で変わるので、記事と違う値が出ても正常です。
それと、すでに git init -b main で 1 回出てきた main と、後の節で出てくる origin という 2 つの名前についても、ここで一言だけ。
main:Git が作る最初の ブランチ (本記事後半で出てくる「40 文字のしおり」) の名前です。意味は「主となる流れ」。むかしはmasterという名前でしたが、いまは多くのプロジェクトでmainが標準になりました。ここでは「いちばん最初の保管場所の名札」くらいに思ってください。実物 (中身が 40 文字のテキストファイル) は本記事後半の「ブランチの正体は、40 文字のファイル」で開けて確かめます。origin:GitHub のような 送り先 (リモート) に付ける、慣習的なあだ名です。「ここに上げる相手」を毎回 URL で書くのは面倒なので、originというあだ名で呼べるようにしておきます。これも「とりあえずこう名付けるのが普通」というだけで、別の名前にもできます。詳しくは後述します。
どちらも、Git の世界で「とりあえずこの名前を使う」と決まっているだけの名札です。深い意味はないので、いったん名前として受け入れて先に進んで構いません。
コミットの中身は、たった 4 種類のオブジェクト
Git がデータを保存する .git/objects/(以下、保管庫と呼びます) には、たった 4 種類のものしか入っていません。
- blob(ブロブ):ファイルの中身そのもの。ファイル名は持たない
- tree(ツリー):フォルダのようなもの。ファイル名と、対応する blob や tree の一覧
- commit(コミット):1 つの tree +親コミット+作者+メッセージ
- tag(タグ):特定のコミットに付ける名前付きの目印 (
v1.0など)
このうち commit・tree・blob の 3 つが入れ子になって、1 つのコミット=「プロジェクト全体の写真」を形づくります (tag はその写真に貼る名札なので、入れ子には入りません)。
オブジェクトの階層イメージ
Commit
↓ (tree)
Tree (ルートフォルダ)
├── blob: README.md (の中身)
└── Tree: src/ (フォルダ)
└── blob: main.py (の中身)
これが前回話した「スナップショット方式」の実体です。図だけだとピンと来ないので、実際に作って中を開けてみます。
ファイルを 1 つ作って git add し、保管庫を覗きます。
printf 'My Project\n' > README.md git add README.md find .git/objects -type f # → .git/objects/56/266d36... が出来ている git cat-file -t 56266d36 # → blob (-t は種類を尋ねる) git cat-file -p 56266d36 # → My Project (-p は中身を表示)
まだコミットしていないのに、git add した瞬間に何かが生まれています。種類を尋ねると blob、中身を見ると My Project。これが blob の正体で、ファイルの中身だけが入っていて、「README.md」という名前はどこにも持っていません。
続けてコミットすると、blob 以外に tree と commit が増えます。
git commit -m "first commit"
git cat-file -p HEAD # commit の中身(tree / author / committer / message)
git cat-file -p HEAD^{tree} # tree の中身(100644 blob 56266d... README.md)
commit の中身は「どの tree か」「誰が・いつ」「メッセージ」だけ。その tree を覗くと「README.md という名前→さっきの blob」という対応が入っています。blob が名前を持たなかった理由はこれで、名前と中身を結びつけているのは tree のほうなのです。
残る 4 つめの tag は、ここまでの 3 つとは少し毛色が違います。blob・tree・commit が「写真そのもの」を作るのに対し、tag は特定のコミットに「v1.0」のような分かりやすい名前を貼る、いわばバージョンの名札です。
「このコミットは特別!」というバージョン名・リリース名を付ける機能で、リリース管理、バージョン管理で非常に重要です。tree や commit とは違って、人間が読みやすい名前を付けるためのものです。
タグとブランチの違いは、
- ブランチ:動くポインタ (コミットを進めると先端が移動)
- タグ:基本的に動かない (固定の目印)
タグは一度付けたら普通は動かしません。
実際に作って、4 つめのオブジェクトを登場させてみます。
git tag -a v1.0 -m "最初のリリース" # 注釈付きタグを作る git cat-file -t v1.0 # → tag (4つめのオブジェクトが現れた) git cat-file -p v1.0 # →指すコミットの住所・作者・メッセージ
これで 4 種類すべてが出そろいました。blob・tree・commit が 1 枚の写真を形づくり、tag はその写真に貼る名札、という関係です。ひとつ補足すると、-a を付けずに git tag v1.0 と打った場合は、独立した tag オブジェクトは作られず、ブランチと同じ「コミットの住所を指すだけの軽いしおり」になります (これを軽量タグと呼びます)。-a を付けた注釈付きタグのときだけ、上のような 4 つめのオブジェクトが生まれる、と押さえておけば十分です。
ちなみに、自分のリポジトリの .git/objects を覗くと 03 07 0b…という 2 文字フォルダしか見えず、面食らうことがあります。この 2 文字フォルダこそ、blob や tree や commit が入っている場所です。種類がフォルダ名にもファイル名にも書かれていないので、見ただけでは区別がつきません。次の節を読むと、その名前の付き方が腑に落ちます。
ファイルの名前は、中身から決まる「住所」
Git のいちばん独創的な部分が、オブジェクトの名前の付け方です。56266d36... という名前は、ランダムな連番ではありません。中身から計算したハッシュ値 (SHA-1、40 桁) です。中身が決まれば、名前が自動的に決まります。
そしてこの名前は、保管庫での置き場所そのものでもあります。さきほど blob が .git/objects/56/266d36... にいたのを思い出してください。名前の先頭 2 文字がフォルダ名になっていました。中身から決まった名前が、そのまま「どこに置くか・どこから取り出すか」を指しているわけです。番地を見れば家にたどり着けるのと同じなので、ここから先はこの名前を「住所」と呼びます。
printf 'My Project\n' | git hash-object --stdin # → 56266d36... printf 'My Project\n' | git hash-object --stdin # → 56266d36...(毎回・どのパソコンでも同じ) printf 'My Project!\n' | git hash-object --stdin # →まったく別の40文字
同じ中身なら、何度計算しても、どのパソコンでも同じ住所になります。逆に 1 文字でも変えれば、まったく別の住所に変わります。ここから 2 つの性質が自動的に生まれます。
ひとつは 重複の排除 です。同じ中身は同じ住所なので、保管庫には 1 つしか保存されません。もうひとつは 改ざんの検知 です。中身を 1 バイトでも書き換えれば住所が変わる。しかもコミットの住所は「中の tree の住所+親コミットの住所」から計算されるので、過去のコミットを 1 つこっそり書き換えると、それ以降の全コミットの住所が連鎖的に変わってしまいます。歴史を後から差し替えるとすぐにバレる、という頑丈さが、この仕組みだけで成立しています。
git log で見るあの長い英数字 (a3f28d1...) も、実はそのコミットの全内容から計算した住所だったわけです。
ブランチの正体は、40 文字のファイル
ここが第 3 回の山場です。refs/heads/main を開いてみます。
cat .git/refs/heads/main # → b4c96315ae08b029192fa57b46b035d0317018cc
出てきたのは、コミットの住所が 1 行書かれただけのテキストファイルでした。ブランチは「コードのコピー」ではなく、1 つのコミットを指す、たった 40 文字のしおり だったのです。
だからブランチを新しく作るとは、40 文字のファイルを 1 個増やすだけのこと。試しに作って中身を比べると、はっきりします。
git branch feature cat .git/refs/heads/feature # → b4c96315...(main とまったく同じ住所)
feature の中身は main と同じ住所です。コードは 1 バイトもコピーされていません。ブランチの作成も切り替えも一瞬で終わり、いくつ作っても重くならないのは、正体がこの軽さだからです。
前回までに何度か出てきた「気に入らなければブランチごと捨ててやり直す」が、なぜノーコストでできるのかも、これで腑に落ちます。AI に新機能を別のブランチで作らせ、出来が違えば、そのしおりを 1 枚捨てるだけ。本流 (main) は 1 バイトも汚れません。
そして「今どのブランチにいるか」を覚えているのが HEAD です。通常 HEAD はブランチを指し、ブランチがコミットを指す、という二段構えになっています。
flowchart TD
HEAD["HEAD<br/>今いる場所"] --> main["main<br/>ブランチ"] --> C["コミット"] --> T["tree"] --> B["blob"]git switch でブランチを移動するとは、この HEAD の中身 (どのブランチを指すか) を書き換えるだけのことです。
cat .git/HEAD # → ref: refs/heads/main git switch feature cat .git/HEAD # → ref: refs/heads/feature (指す先が変わった) git switch main # main に戻しておく
この HEAD →ブランチ→コミット→ tree → blob という 5 段のつながりが、Git のほぼすべての中心です。これから出てくる操作は、どれも「この鎖のどこかを動かすもの」として理解できます。
「写真を撮り続けたら容量が爆発する」問題の答え
第1回で、こんな疑問を示しました。「全部の写真を撮ったら容量が爆発するのでは? 」。その答えを、ここで実物で確かめます。
まず押さえたいのが、容量を決めるのは「ブランチの数」ではない、という点です。ブランチは 40 文字のしおりなので、10 個作ってもほぼ増えません。
git count-objects -vH # サイズを記録 for i in $(seq 1 10); do git branch t-$i; done git count-objects -vH # ほとんど変わらない for i in $(seq 1 10); do git branch -d t-$i; done
増えるのは「実際に変更したファイルの blob」だけです。100 ファイルのうち 1 つを変えてコミットしても、新しく生まれる blob は 1 個。残り 99 ファイルは前の blob を指すだけで、保存し直されません。コミットを「全体の写真」として撮りつつ、変わっていない部分は前の写真を使い回しているわけです。
ではその「変えたファイルの blob」には、変更点 (差分) だけが入るのでしょうか。答えは「いいえ、完全版が丸ごと入る」です。1000 行のファイルの 1 行を変えれば、新しい blob には 1000 行すべてが入ります。
seq 1000 > big.txt && git add big.txt && git commit -m "v1" sed -i '' 's/^500$/500-changed/' big.txt && git add big.txt && git commit -m "v2" git cat-file -s HEAD:big.txt # →約3900バイト(1000行ぶん全部) git cat-file -s HEAD~1:big.txt # →約3893バイト(1000行ぶん全部)
(sed -i '' は Mac の書き方です。Linux や Windows の Git Bash では、'' を取って sed -i 's/^500$/500-changed/' big.txt としてください。)
新旧どちらの blob も「1000 行ぶんのサイズ」です。差分ではなく完全版が 2 つ保存されています。
「それなら、やっぱり容量を食うのでは」と思いますよね。ここで第1回の二層構造が効きます。私たちに見せるモデルは完全版のスナップショットのまま。その裏で、Git は似た blob どうしを差分圧縮して、ディスク上のサイズを小さく抑えています。git gc(不要物の整理) が走ると、バラバラのオブジェクトが 1 つの圧縮ファイルにまとめられます。「人には分かりやすい完全版を見せ、内部では賢く圧縮する」——この割り切りが、容量の心配を裏で解消しているのです。
試しに GitHub へ上げてみる
ここまで手元で解剖してきた git-lab を、そのまま GitHub に上げてみます。第2回で gh の認証を通してあれば、リポジトリの作成と初回アップロードは 1 行で済みます。
gh repo create git-lab --private --source=. --remote=origin --push git push --tags # v1.0 のタグも一緒に送る
gh repo create が GitHub 上に空のリポジトリを作り、--source=. で今いるフォルダを送り元にし、--remote=origin で送り先を「origin」という名前で登録し、--push で main を初めてアップロードします。--private を付けたので、自分だけが見える練習用リポジトリです。
GitHub のページを開くと、さっき手元で作った README やコミットがそのまま並んでいます。.git の中で見た blob・tree・commit が、ネットの向こうにもそっくり複製された、ということです。この送り先 (リモート) の仕組みは次回くわしく扱いますが、ここでは「手元の git-lab が GitHub に載った」とだけ掴めれば十分です。この git-lab は次回以降も使うので、手元で実験するたびに git push で上げ直せます。
AI に頼むなら
この git-lab を、共有の置き場(GitHub)に初めて上げて。送り先を origin として登録して、main をアップロードして
この一言で、AI は送り先を git remote add origin … で登録し、git push -u origin main で初回アップロードします (-u を付けるので、次からは git push だけで済みます)。まだ GitHub 上にリポジトリ自体が無いときは、上の gh repo create --source=. --push が「リポジトリの作成・origin の登録・初回 push」を一度にこなします。
まとめ
第 3 回として、コミットとブランチの正体を .git の中で確かめました。
- コミットは blob(中身)・tree(名前の目次)・commit(誰が・いつ・親) の入れ子で、プロジェクト全体の写真を表す。
- オブジェクトの名前は 中身から計算した住所 (ハッシュ)。だから重複は自動で排除され、改ざんはすぐバレる。
- ブランチは 40 文字のしおりにすぎない。だから作成も切り替えも一瞬で、気軽に捨ててやり直せる。
- 容量を決めるのは枝の数ではなく「変更した中身の量」。blob は完全版で保存され、容量はディスク上の差分圧縮が裏で抑える。
HEAD →ブランチ→コミット→ tree → blob という鎖が頭に入れば、次回からの各論コマンドは「この鎖のどこを動かすか」で一気に整理できます。次回は、毎日使う add / commit / branch / merge を、この鎖の上で何が起きているかとして読み解きます。あわせて、作業を「カゴ」に入れてから記録する 3 つの領域 (作業ツリー・ステージング・リポジトリ) も見ていきます。
パイソンエンジニア部 

