この Note はなに?
url: https://docs.google.com/presentation/d/1ukjfgaF7uiNzeSvD7yTMz9vlcl3USD95PvgcLV9YkXg/edit?slide=id.g1276827f2e_0_5#slide=id.g1276827f2e_0_5
title: "git資料"
description: "Gitを内部構造で 完全理解する1時間 git init からgit rebase まで"
host: docs.google.com
image: https://lh7-us.googleusercontent.com/docs/AHkbwyIW4syt5z0vsAuQ6vQyu2TqUVO7WEdlQahFLXm0oXWBEYa0odwxPN8fjAEWvyCXxtP7nfqK7TzQwbnqql6cNFa56Hv0bSxFu9jHTjRdwg5g60uysqQn=w1200-h630-p
url: https://supporterz-seminar.connpass.com/event/365286/
title: "【勉強会】Gitを内部構造で完全理解する1時間 (2025/09/04 12:00〜)"
description: "## 📍 イベント概要 新卒1年目、初めてrebaseを触ったとき何が起きているのか分からず、ただ怖かった――そんな経験から、Gitの“中身”を理解したいと思うようになりました。 仕事では巨大なコードベースに向き合い、過去コミットを読み解く必要も多く、Gitは「なんとなく使うツール」では済まされないと感じています。 本勉強会では、Gitの内部構造を1から紐解き、addやcommitの正体からrebaseの仕組みまで、図解と構造で完全に理解することを目指します。 ※本勉強会は、サポーターズCoLabのユーザーが講師を務める勉強会です ※サポーターズCoLabでは勉強..."
host: supporterz-seminar.connpass.com
image: https://media.connpass.com/thumbs/d6/93/d693cede8933ffb8f5f256d08716f417.png
@rxuichiii さんによる Git についての解説講義について、自分なりの解釈を付け足しながら まとめています。 自分で調べたこと・自分の解釈を追加することによって記憶に定着させるためです。
よって、 @rxuichiii さんの内容と ganyariya の調べごとが混ざっているためご注意ください。
なお、 ganyariya のものについては gemini/claude に相談しながらインターネット記事を参考にしており、 Git 本を読めていないためご注意ください。
git init
git object
- file
- directory
- commit
これら 3 情報を
git object
という名前で git では扱っている。
git-object には
- commit object
- commit 自体を表す情報
- tree object
- Worktree のディレクトリを表す情報
- tree T のなかに、どの blob object, tree object があるか?を1階層分だけ保存する
- Worktree のディレクトリを表す情報
- blob object
- git で管理されるファイル自体を表す情報
がある。
これらはすべて .git/objects 配下に接頭辞 2 文字で区切られてごちゃまぜに配置される。
git cat-file -p
で (commit|tree|blob) に紐づく hash がどのような内容かがわかる。
これで git object が実際に commit なのか tree なのか… blob なのか、がわかる。
git cat-file -p 8cec2a7ea67e79e40433e9afc276f1adab18470b
ただし、 注意点としてすべての commit, tree, blob が直接 .git/objects に配置されるわけではない。 git は効率化・省スペースのために ファイル自体を表す pack-file というものを作成する。
.git/objects/pack 配下に作成し、この中に複数の commit/blob/tree が一個にまとめ上げられている。
.git/objects/pack on feature/learn-c-compile-assemble [!?] [☁️ ]
❯ ls
pack-d3a6e63ce60f966c4680dc73cb8029317a44c54a.idx pack-d3a6e63ce60f966c4680dc73cb8029317a44c54a.pack
commit オブジェクトから tree/blob をたどる
# HEAD commitオブジェクトの hash を取得する
COMMIT=$(git rev-parse HEAD)
playground on feature/learn-c-compile-assemble [!?] [☁️ ]
❯ echo $COMMIT
a59ec8c4d15c24a8f55615883d70cca41de259f8
# HEAD commit オブジェクトがルートとして持つ tree object を取得する
playground on feature/learn-c-compile-assemble [!?] [☁️ ]
❯ git cat-file -p $COMMIT
tree bf2a2d0ed6b8e2fe622a5370e56b5498bfee1958
parent ff15f3be8b64de904836454ee2db945e6a32ebb6
parent 32adc63245ad29185a827e2d699ce0a8f6de6e84
playground on feature/learn-c-compile-assemble [!?] [☁️ ]
❮ ROOT_TREE=$(git cat-file -p $COMMIT | grep "^tree " | cut -d ' ' -f2)
playground on feature/learn-c-compile-assemble [!?] [☁️ ]
❯ echo $ROOT_TREE
bf2a2d0ed6b8e2fe622a5370e56b5498bfee1958
# Root tree オブジェクトを見ると、ルート階層にある
# blob/tree object が取得できる
❯ git cat-file -p $ROOT_TREE
100644 blob e43b0f988953ae3a84b00331d0ccf5f7d51cb3cf .gitignore
100644 blob a4eec53c0823204dc060c890ed728917241d6504 .gitmodules
040000 tree 8f237c6081af2354e4750a4b08e6174809503957 .vscode
100644 blob 2f8e670b8e791f86f3f2af6fc8b6e71d0e96e296 README.md
040000 tree 86578545567f037d0d0122d8e31e9c536115ea50 gcp-disk-snapshot
040000 tree beb76b48461cad8b09b073071c7b8172bfabd6f8 gcp-packer
040000 tree 0d0b7a22fd2f9860fd5572b8dfcbdbba27cbae0b isucon-book
040000 tree 715a8dad2256712a83c404b7187560dd290738bb jenkins-agent-connection
040000 tree d9e8d485b79d42fa0f72eb7225dc5dbecc1f33c1 jwt
040000 tree 2220953c28e8462eb929299002b01fdbca67c777 linux-in-practice-2nd
040000 tree 95652f8931ce15b4b884d30cd1e62736c2146ff5 linux-tcpip
040000 tree 919be9ccf1ab09ed8dc09d80ca38dbbb6dee7779 simple-go-observe
040000 tree 1b4800228336812204579a1c06dce3c7bfc87375 trial-gke-pv-pvc
# blob オブジェクトの場合はそのまま `-p` すれば中身が確認できる
playground on feature/learn-c-compile-assemble [!?] [☁️ ]
❯ git cat-file -p 2f8e670b8e791f86f3f2af6fc8b6e71d0e96e296
---
# playground
ganyariya's programming playground
---
# tree オブジェクトの場合は、また再帰的に git cat-file -p $tree-object すればよい
playground on feature/learn-c-compile-assemble [!?] [☁️ ]
❯ git cat-file -p 919be9ccf1ab09ed8dc09d80ca38dbbb6dee7779
100644 blob 7777acaa37bf359e4b82939c597f4c9db6ecfa0c .gitignore
100644 blob 1d9852359c6286a482139c78d80b171530e88737 Dockerfile
100644 blob b9278f8bf1d660352d2945b7d1375a4421a94f9a README.md
040000 tree 963d6fed7e26d8812775d74a1fbe196583978756 conf
100644 blob ed9f657ee0aa264af86393b7d59f8500a21e2a20 docker-compose.yml
100644 blob c6d93767e3763dfe71a4c1fc5b8e0cc0990722ed go.mod
100644 blob 8f5bccf226e30bfbfd166702b96652759f1e938a go.sum
040000 tree 9604be140bdcacd87e8f1d358f5c2f866a59dfd2 src
❯ git cat-file -p b9278f8bf1d660352d2945b7d1375a4421a94f9a
# simple-go-observe
o11y ツールを理解するために
- 簡易的な go http server を立てる
- go http server に go-opentelemetry を組み込む
- trace, logs を go-opentelemetry で計測して opentelemetry-collector に送信する
- それらのメトリクスを複数のツールで可視化する
よって、 git commit object からその時点におけるディレクトリ構成とファイルを確認したい場合は
git cat-file -p
を利用する- はじめに対象とする $COMMIT ハッシュを指定する
- $COMMIT ハッシュは 1 つの ROOT tree オブジェクト のハッシュを持つ
- はじまりはかならず単一の Root Tree オブジェクト
- Root Tree が再帰的に blob/root object を持つ
- ROOT tree オブジェクトハッシュをもとに、ルートディレクトリの blob/tree を取得する
- blob の場合はそのときの blob ファイル状態を取得する
- tree の場合は再帰的に該当のディレクトリの blob / tree を取得する
を行えばよい
commit / tree / blob の関係性
commit C は 1 つの gachit ルート tree object を持っている。
そして、 README.md に編集を行うと commit D になり、更新された gachit ルート tree object を持つようになる。
ここで src
ディレクトリについては変更が加わっていないため、 C/D は同じ src tree object ccfd88
を持っている。
かわりに README.md については更新がおこなわれたため新しい README.md blob object が作成される。
これであれば 変更された blob object に紐づくディレクトリの tree object のみ作成すればよい。 変更していないディレクトリの tree object / blob object は更新しなくてよいため、 git object が最小限ですみ、省スペースになってうれしい。
となると末端の blob object を更新した場合、 tree object が大量に更新されるのでは?と考えましたが、どうやらあっているようです。
この場合 src/a/b/c/d/e/f/g/h.html の h.html を修正したとします。
このとき、src/a/.../g まですべての tree object が更新されるのでしょうか。
tree object のハッシュ計算には tree object が含む blob/tree object によって計算されると考えており、深いディレクトリの blob object が更新されると再帰的に tree object が作成されると考えています
→ あっている
blob / tree / commit
blog = ファイル自体
tree object = ディレクトリ自体となっており、 1 階層分の blob, tree のみ記録する。 一個下のディレクトリについては、そのサブディレクトリ用の tree object が再帰的に用意される。
commit object は tree, parent, author, comment のみを所持している最小限のメタデータオブジェクトになっている。 下記メルカリの記事にもあるように、「commit したあとの状態」を保持しており、commit する行為、を保持しているわけではない。 https://engineering.mercari.com/blog/entry/2016-02-08-173000/ 最小限のメタデータしか持っていないため、高速に git log できるのもなるほどですね。
ハッシュ値の決め方
blob ファイルについては以下のように決められます。
- Header を生成する
- Object Type:
blob
- Byte Size: blob ファイルのバイトサイズ
- 画像だと 54
- Object Type:
- blob ファイルのテキストを結合する = Z とする
- 画像だと
if _name_
… の箇所
- 画像だと
- Z から SHA1 ハッシュを計算し 40 文字のハッシュを得る
- 最後に Z を zlib 圧縮して .git/objects に配置する
画像では zlib → sha1 とありますが、 gemini だと sha1 (この時点でハッシュ値が確定) → zlib でした。 他の方の記事を見たところ、 sha1 → zlib のようです。
url: https://engineering.mercari.com/blog/entry/2016-02-08-173000/
title: "Gitのコミットハッシュ値は何を元にどうやって生成されているのか | メルカリエンジニアリング"
description: "こんにちは。サーバサイドエンジニアの @DQNEO です。前回の「Gitのつくりかた」に続いてGitのコアな部分のお話です。Gitのコミットハッシュ値とは何かGitを使っていると必ずコミットハッシュ値というものが出てきます。9e47c22み"
host: engineering.mercari.com
favicon: https://engineering.mercari.com/favicon.ico
image: https://engineering.mercari.com/img/ogp/ogp_a.jpg
commit object についても同様に計算されます。 下記表示される commit object 中身のテキストを T とします。
❯ git cat-file -p $COMMIT
tree bf2a2d0ed6b8e2fe622a5370e56b5498bfee1958
parent ff15f3be8b64de904836454ee2db945e6a32ebb6
parent 32adc63245ad29185a827e2d699ce0a8f6de6e84
author ganyariya <[email protected]m> 1739097584 +0900
committer GitHub <[email protected]m> 1739097584 +0900
Merge pull request #9 from ganyariya/feature/gcp-disk-snapshot
gcp-disk-snapshot テスト用に tf を用意する%
このとき、 Type commit を先頭につけて、下記のように計算することで hash が計算されます。 blob との違いは先頭の type が commit か blob かの違いのみですね。
hash = sha1("commit<半角スペース><コミットオブジェクトのバイト数>\0<コミットオブジェクトの中身=T>")
同様に tree object についても先頭に tree をヘッダとして付与しつつ、 git cat-file -p tree-hash で得られるテキストを hash することで得られるのだとおもいます。
git add / commit の仕組み
git-index-file は ステージングエリアの実体を指すファイルであり、 .git/index
にバイナリファイルとして置いてある。
ある時点 T において、T 時点のステージングエリアをすべて再現できるような blob ファイル hash とファイル名のリストを持っている。
注意点として、 「10 回コミットしている&なにも git add していない」状態においても .git/index バイナリファイルには Worktree をすべて再現できる、つまり時点 T のステージングエリアを再現できるリストが用意されている。
git ls-files —stage でステージングエリアを再現するすべてのファイルの blob hash とファイル名が得られる。 tree object は表示されない。
❯ git ls-files --stage
100644 e43b0f988953ae3a84b00331d0ccf5f7d51cb3cf 0 .gitignore
100644 a4eec53c0823204dc060c890ed728917241d6504 0 .gitmodules
100644 d99f2f30ccdcfdaa059508e5f2a71c8d80d9f88d 0 .vscode/settings.json
100644 2f8e670b8e791f86f3f2af6fc8b6e71d0e96e296 0 README.md
100644 00a076af3cdc23bdac2c7dac3206213cec676e5d 0 gcp-disk-snapshot/.gitignore
100644 7033e033e61ca5890e97b5fbda2c3d6842863d42 0 gcp-disk-snapshot/README.md
100644 f293a0a61452ffba3d9ce39d46bb85ab79efbfd8 0 gcp-disk-snapshot/terraform/.terraform.lock.hcl
100644 2645952de37fbf8c5bcb6ce18de5b8ec3e37cf5f 0 gcp-disk-snapshot/terraform/instance.tf
git add したときに新たな blob file (blob hash も新たに作成される) が作成され、 .git/objects に保存される。 その後、ステージングエリアを更新するために Index ファイルにおける該当ファイルの hash が新しい blob hash に置き換わる。
git-commit を実行したとき、 Index ファイルにはコミット時のすべての blob ファイルパスと hash が存在する。 そのため、それらをディレクトリ単位で末端から再帰的に tree object を作成する。 このとき、変更したファイルを含むディレクトリについては tree object が新たに作成され、変更がなければ新しい tree object は作成されない。
最終的に root tree object が作成される。 なにかしら blob ファイルを更新すれば必ず root tree object は更新される。 この root tree object と parent commit hash があれば commit object の情報が揃うため、 commit-object が作成できる。
git checkout
.git/refs に commit hash を参照するための ref が用意されている。
- HEAD
- git が対象としている branch / commit
- branch
- 特定のコミットに紐づく commit ref
- tag
.git/refs には heads/remotes/tags の 3type が存在する。
playground/.git/refs on feature/learn-c-compile-assemble [!?] [☁️ ]
❯ ls
heads remotes tags
.git/refs/heads on feature/learn-c-compile-assemble [!?] [☁️ ] took 3s
❯ tree
.
├── feature
│ └── learn-c-compile-assemble
├── git-lfs-conflict
└── main
❯ cat -p feature/learn-c-compile-assemble
a59ec8c4d15c24a8f55615883d70cca41de259f8
.git/refs/remotes on feature/learn-c-compile-assemble [!?] [☁️ ]
❯ tree
.
└── origin
└── HEAD
heads にはローカルブランチのハッシュポインタが格納されている。 remotes にはリモートリポジトリブランチのハッシュポインタが格納されている。 tags はタグのハッシュポインタが格納されている。
ローカルの HEAD は .git/HEAD に直接可能されている。 この例では refs/heads/feature/learn-c-compile-assemble、つまりローカルリポジトリの learn-c-compile-assemble ブランチを HEAD が参照していることがわかる。
playground/.git on feature/learn-c-compile-assemble [!?] [☁️ ]
❯ cat -p HEAD
ref: refs/heads/feature/learn-c-compile-assemble
commit-A を見ている状態で git branch -c feat をすると、 .git/refs/feat が作成される。 checkout の場合はかつ .git/HEAD の値も feat をブランチを指すように書き換えられる
main ブランチから feat ブランチへ checkout するときを考える。
まず HEAD を feat branch へ移動する。 かつ、このとき 元の HEAD (main) と ターゲットブランチ (feat) の差分を計算する。 Worktree を修正し、かつ INDEX ファイルも更新することで Staging エリアも正しくする。
ここで C コミットと D コミットの差分を計算するとき、 root tree ファイルからたどる。 root tree ファイルに含まれる blob/tree object が同じ hash であればそれについてはスキップすればよい。
これによって**差分計算は「事前計算しておいた blob/tree object hash を利用して最小限の比較をする」で実現できる。
git reset を実行する場合、直接 HEAD ならびにブランチを付け替える作業になる。 よって、 commit D から commit C へ飛ぶ場合、 commit D へ戻ることはできなくなってしまう。
一方、checkout のときは、 HEAD のみ を D から C へ戻すため、 feat ブランチは D を指したままになる。 これがいわゆる detached-HEAD 状態になる。
git reset —soft の場合は branch と HEAD を過去コミットに戻す。 Worktree と Staging エリアは元のコミットのままになる。
mixed の場合は、 Staging エリアも過去コミットに巻き戻る。
よって、git add すると Staging エリアへ commit D ファイルを追加できる
が行える。
hard はすべてを無に帰す。
git-reflog をつかえば、 git reset —hard の操作自体をなかったことにすることで元に戻せる。
git merge
2-way-merge は 2 つのブランチの最新の状態だけを比較して変更を統合する。 この方法の場合、同じファイル X を各ブランチが変更・削除していた場合どちらが正しいかを判断できない。
よって、 git は 3-way-merge を利用する。
3-way-merge においては 2 つのブランチ (A/B) に加えて、共通の祖先コミット (merge base commit) を比較し、変更を統合する。
- 両方のブランチで変更している場合
- Merge Base Commit と比較して、 Branch A/B 両方でファイル X を変更した場合、
コンフリクト
として扱い resolve するように促す- (正確にはファイル X の hunk)
- Merge Base Commit と比較して、 Branch A/B 両方でファイル X を変更した場合、
- 一方のブランチのみで変更している場合
- Merge Base Commit と比較して、片方の Branch のみで更新しているファイル X の場合、その変更を自動的に取り込む
共通の祖先を第 3 の比較要素に加えることによって
- A ブランチでは追加された
- B ブランチでは削除された
- → resolve しないといけない という競合をきちんと判断できる。
3 コミットを比較してマージするため、 3-way となる。
url: https://it-biz.online/it-skills/git-merge/
title: "Gitのマージ(merge)を「本質」から理解する3分間"
description: "Gitのマージの仕組みを徹底解説!3-wayマージやコンフリクト解消、マージコミットの役割を初心者にもわかりやすく説明します。ブランチ戦略やリベースとの違いも視覚的な例を使って解説。Gitをより深く理解したい方必見の記事です。"
host: it-biz.online
favicon: https://it-biz.online/wp-content/uploads/2019/10/cropped-4a332f05ade4ac7bb3c46c472cb5eac8-32x32.png
image: https://it-biz.online/wp-content/uploads/2024/12/image-7.png
A, B, D を比較して merge commit M を作成する。 Commit A は Best Common Ancestor としてベースコミットとして利用される。
Commit M を作成するとき (B - A) と (D - A) の差分を計算し、その差分をマージすることで M が作成される。 途中の歴史のコミット C は利用されないことに注意する。
これは A と D の差分を見るだけで C の変更内容も検知できるため。
cherry-pick
cherry-pick する場合、 「コミット B に (D - C) の差分を追加する」ではうまくいかない。 もしコミット B でファイル X を編集しており、かつコミット D でもファイル X を編集しているとうまくいかない。
cherry-pick においては (D - C) の差分を計算して、コミット B に patch する、という仕組みになる。 3-way-merge のような仕組みではないらしい。
git rebase
git-rebase はこれまでの仕組みを応用して実現されている。
コミット D でなにか追加の作業をしていたとする。 まず、 git reset —hard main で、 main ブランチにリセットする。
あとは cherry-pick を繰り返して強引に C, D を持ってくる。
fast-forward merge
A ← B ← C (main)
↑ D ← E ← F (feature)
のときに
A ← B ← C ← D ← E ← F (main, feature) にする
上記のように、 main ブランチに feature ブランチを取り込むときに「main ブランチから単純なコミットを繰り返して feature ブランチになっている」ときがある。 この場合、main ブランチの参照先を feature ブランチのコミットハッシュまで持ってくれば、新しいマージコミットオブジェクトを作らなくて済む。
よって、 main ブランチを feature ブランチが指すコミットオブジェクトまで 先送り (fast-forward) することでコミットオブジェクトの作成をスキップできる。
本のおすすめ
参加させていただいた所感
Git に関する知識が非常にわかりやすく丁寧に解説されており、とても楽しい講義でした。 自分で復習することで
- git object とはなにか
- どうやって hash (blob/tree/commit) が作成されているのか
- git add/commit/merge/cherry-pick/checkout が何をしているのか
- 3-way-merge とはなにか
が理解できました。
時間が用意できたら下記のような git を自作するコースをおこなって、 git について学びたいなとおもいます。
url: https://wyag.thb.lt/#intro
title: "Write yourself a Git!"
host: wyag.thb.lt
url: https://www.leshenko.net/p/ugit/#
title: "Git Internals - Learn by Building Your Own Git"
host: www.leshenko.net
favicon: favicon.png
url: https://app.codecrafters.io/courses/git/overview
title: "The Software Pro's Best Kept Secret."
description: "Real-world proficiency projects designed for experienced engineers. Develop software craftsmanship by recreating popular devtools from scratch."
host: app.codecrafters.io
favicon: https://app.codecrafters.io/assets/favicon.ico
image: https://codecrafters.io/images/app_og/course-git.jpg
追記
url: https://koseki.hatenablog.com/entry/2014/04/22/inside-git-1
title: "Git の仕組み (1) - こせきの技術日記"
description: "目次 はじめに Git を使ったことがない方へ 生のデータが見たい方へ Git の全体像 .git の中身 Git オブジェクトデータベース 4種類のオブジェクト リファレンス リファレンスのリファレンス 大きなツリー Git オブジェクトの ID と 中身 ハッシュ関数 SHA1 の簡単な説明 tree と blob オブジェクト tree と blob の参照関係 ルートツリーの ID でツリー全体を識別する commit オブジェクト リファレンスとブランチ ブランチ ブランチ先頭を指すリファレンス HEAD リファレンス detached HEAD 2種類のタグ 一時待避 (stash…"
host: koseki.hatenablog.com
favicon: https://koseki.hatenablog.com/icon/link
image: https://cdn.image.st-hatena.com/image/scale/44dd77f6928730973a64df433e2acc1823c27184/backend=imagemagick;version=1;width=1300/http%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Fk%2Fkoseki2%2F20140420%2F20140420183642.png
Git の中身を簡潔かつわかりやすく丁寧に説明されている資料です。