0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gitの内部構造を理解する【後編】— 実リポジトリで検証

0
Posted at

はじめに

前編では、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を実行してみると、抽象的な仕組みの理解がより実感を伴ったものになるはずです。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?