はじめに
前編では、Gitのオブジェクトモデル(blob/tree/commit)、ブランチの正体、マージ・cherry-pick・rebaseの内部動作を整理しました。本記事はその実践編です。実際のリポジトリに対してgit cat-fileなどのコマンドを使い、前編で説明した構造を手を動かして確認します。あわせて、確認に使えるコマンドを逆引き形式でまとめます。
commitオブジェクトの実データを追う
commitの生データとgpgsig
$ git cat-file -p HEAD
tree 4c1f517281fc1f742302606099e14cb1170bf351
parent a2660dacecd11c9dbaed9bfb0ce9c92706aca87d
author 開発者名 <email@example.com> 1786554229 +0900
committer GitHub <noreply@github.com> 1786554229 +0900
gpgsig -----BEGIN PGP SIGNATURE-----
...
-----END PGP SIGNATURE-----
コミットメッセージ
前編で説明したtree/parent/author/committerに加えて、gpgsigという行が付与されることがあります。これはcommitが改ざんされていないことを保証する暗号署名で、GitHub上でGPG/SSH署名によるverified commitを有効にすると付与されます。commitハッシュの計算対象にこの署名データ自体も含まれるため、署名の有無によってもハッシュは変わります。
また、GitHub上でPRを「Squash and merge」した場合、複数commitが1つに統合され、committerがGitHub自身になることがあります。前編の「committerは今この歴史に組み込んだ人」という定義がここでも成立しています。
treeを手作業で辿り、ファイルの実体に到達する
ハッシュから、git cat-file -pを繰り返すだけでファイルの中身まで辿り着けます。
# 1階層目:tree
$ git cat-file -p 001db07beb227d391a7c2c672119c3feab7734a3
040000 tree 1e768f57ba872a660913f7bbef652ba2d7226b47 ecapi
# 2階層目:tree
$ git cat-file -p 1e768f57ba872a660913f7bbef652ba2d7226b47
100644 blob 5b5534426644101c45fa8b800d69c35650632b1b EcApiApplication.java
040000 tree ... controller
...
# 3階層目:blob(ファイルの実体)
$ git cat-file -p 5b5534426644101c45fa8b800d69c35650632b1b
package com.example.ecapi;
...
このblobにはEcApiApplication.javaというファイル名がどこにも書かれていません。ファイル名は1つ前のtreeのエントリ側に記録されていたことを踏まえると、「blobはファイル名を持たない」という前編の原則が実データで裏付けられます。
逆引き:ハッシュからフルパスを特定する
手作業で辿った経路が正しいかは、次のコマンドで裏付けが取れます。
$ git rev-list --objects --all | grep 5b5534426644101c45fa8b800d69c35650632b1b
5b5534426644101c45fa8b800d69c35650632b1b src/main/java/com/example/ecapi/EcApiApplication.java
git rev-list --objects --allは全ブランチ・全commitの履歴を辿り、到達可能な全オブジェクトについて、そのオブジェクトがいずれかのcommit時点でどのパスに対応していたかを教えてくれます。
リネーム検出の実態を確認する
あるcommitでディレクトリ構成が大きく変わった(例:単一モジュール構成からmulti-module構成への移行)場合、git show --statでその内訳を確認できます。
$ git show <commitハッシュ> --stat
.env.example => backend/.env.example | 0
README.md => backend/README.md | 0
build.gradle.kts => backend/build.gradle.kts | 31 +-
{src => backend/src}/main/java/.../EcApiApplication.java | 2 +-
backend/src/main/java/.../config/JpaAuditConfig.java | 24 +
- 変更行数 | 0:中身は一切変わらず、パスだけが変わったファイル。blobレベルでは同じハッシュのオブジェクトが、新しいtree構造の中で別の場所に配置し直されただけ
- 旧パス => 新パス | N +-:リネームと同時に中身も変更されたファイル。新しいblobが生成される
- {旧 => 新}という表記のない+のみの行:リネームではなく新規追加として検出されたファイル
Gitには「ファイルを移動する」という専用の操作(オブジェクト)は存在しません。実際には旧パスのtreeエントリを削除し、新パスのtreeエントリとして同じ(または新しい)blobハッシュを登録し直しているだけです。「rename」という表示は、削除された内容と追加された内容をGitが後から統計的に類似度判定した結果に過ぎません。
あるファイルがいつ移動・削除されたかを追うには、パス指定のログが有効です。
$ git log --all --oneline -- src/main/java/com/example/ecapi/EcApiApplication.java
コマンド逆引き一覧
HEAD・ブランチ・現在位置
cat .git/HEAD # HEADの中身(間接参照か直接ハッシュか)
git branch --show-current # 現在のブランチ名
git branch -a # 全ブランチ一覧
cat .git/refs/heads/main # 特定ブランチが指すcommitハッシュ
git rev-parse main # 同上(コマンド経由)
commitオブジェクトの中身
git cat-file -p HEAD # 最新commitの生データ
git cat-file -p <commitハッシュ> # 特定commitの生データ
git cat-file -t <ハッシュ> # オブジェクトの種別(blob/tree/commit)
git log --oneline --graph --all # commit一覧(グラフ形式)
treeオブジェクト・ファイル構造
git cat-file -p HEAD^{tree} # 最新commitのroot treeの中身
git cat-file -p <treeハッシュ> # 特定treeの中身
git ls-tree HEAD backend/ # あるパスの現在のtreeハッシュ
blobオブジェクト・ファイル中身
git cat-file -p <blobハッシュ> # blobの中身をそのまま表示
git ls-tree HEAD -- path/to/file.java # あるファイルの現在のblobハッシュ
git hash-object <ファイルパス> # 内容からハッシュを逆算
echo "内容" | git hash-object --stdin # 標準入力の内容からハッシュを逆算
パスからの逆引き・履歴追跡
git rev-list --objects --all | grep <ハッシュ> # ハッシュ→過去のパスを逆引き
git log --all --oneline -- path/to/file.java # あるパスの変更履歴
git show <commitハッシュ> --stat # 変更ファイル一覧・行数
git show <commitハッシュ> # 変更内容の詳細diff
git show <commitハッシュ> -M90% --stat # リネーム類似度しきい値を指定
refs・reflogの確認
git show-ref # 全refの一覧(タグ含む)
git reflog # HEADが過去どう動いたか
git reflog show main # 特定ブランチのreflog
cat .git/packed-refs # 個別ファイル化されていないrefの一覧
objectsの物理格納状況
find .git/objects -type f -not -path "*/pack/*" # loose objectの一覧
ls .git/objects/pack/ # packファイルの一覧
git verify-pack -v .git/objects/pack/*.idx | grep <ハッシュ> # pack内での位置
git verify-pack -v .git/objects/pack/*.idx | tail -10 # pack統計
git count-objects -v # オブジェクト数・サイズ概要
まとめ
- commitの生データにはgpgsigのような追加フィールドが付くことがあり、GitHub上のsquash mergeではcommitterがGitHub自身になることもある
- treeを手作業で辿ることでファイルの実体(blob)に到達でき、git rev-list --objects --allで経路の答え合わせができる
- リネームはGit内部の専用操作ではなく、削除+追加をGitが事後的に類似度判定して表示しているだけ
前編で整理した概念が、実際のリポジトリでどう物理的に格納・検出されているかを、コマンドを通じて確認しました。手元のリポジトリで同様にgit cat-fileやgit verify-packを実行してみると、抽象的な仕組みの理解がより実感を伴ったものになるはずです。