Skip to content
Published on

Git内部動作の仕組み:Blob、Tree、Commit、そしてDAG — Gitが本当に動く方法

Authors

はじめに:なぜGit内部(ないぶ)を理解(りかい)すべきか

ほとんどの開発者(かいはつしゃ)はgit addgit commitgit push程度(ていど)しか使(つか)いません。しかしGitの内部動作(ないぶどうさ)を理解(りかい)すると、以下(いか)のようなメリットがあります。

  • デバッグ能力(のうりょく)の向上(こうじょう): detached HEAD、コンフリクト、失(うしな)われたコミットを正確(せいかく)に理解(りかい)し復旧(ふっきゅう)できる
  • 高度(こうど)なワークフロー活用(かつよう): rebase、cherry-pick、bisectを自信(じしん)を持(も)って使用(しよう)
  • 技術面接(ぎじゅつめんせつ)対策(たいさく): FAANG級企業(きゅうきぎょう)でGit内部構造(ないぶこうぞう)の質問(しつもん)が頻出(ひんしゅつ)
  • トラブルシューティング: リポジトリ破損(はそん)、大容量(だいようりょう)ファイル問題(もんだい)、遅(おそ)いcloneの解決(かいけつ)

この記事(きじ)ではGitの内部(ないぶ)を基礎(きそ)から解剖(かいぼう)します。オブジェクトモデル、ハッシュ、ディレクトリ構造(こうぞう)、DAG、ブランチの実体(じったい)、merge/rebaseの内部動作(ないぶどうさ)、reflog、packファイルまで全(すべ)てをカバーします。


1. Gitオブジェクトモデル — すべての基礎(きそ)

Gitは本質的(ほんしつてき)にContent-Addressable Storage(内容(ないよう)アドレス指定(してい)ストレージ)です。ファイル内容(ないよう)のSHA-1ハッシュをキーとして4種類(しゅるい)のオブジェクトを格納(かくのう)します。

1.1 Blob(Binary Large Object)

Blobはファイルの内容(ないよう)のみを格納(かくのう)します。ファイル名(めい)やパス情報(じょうほう)は含(ふく)みません。

# コンテンツからblob生成
echo "Hello, Git!" | git hash-object --stdin
# 出力: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec

# blob内容確認
git cat-file -p 0907f4a
# 出力: Hello, Git!

# blobタイプ確認
git cat-file -t 0907f4a
# 出力: blob

核心(かくしん)ポイント: 同(おな)じ内容(ないよう)のファイルが100個(こ)あってもblobは1つだけ格納(かくのう)されます。これがGitが効率的(こうりつてき)な理由(りゆう)です。

1.2 Tree(ツリー)

Treeは**ディレクトリ構造(こうぞう)**を表(あらわ)します。ファイル名(めい)、ファイルモード、blob/tree参照(さんしょう)を含(ふく)みます。

# 最新コミットのtree確認
git cat-file -p HEAD^{tree}
# 出力:
# 100644 blob a1b2c3d... README.md
# 100644 blob d4e5f6a... package.json
# 040000 tree b7c8d9e... src
Tree (root)
├── blob: README.md (100644)
├── blob: package.json (100644)
└── tree: src/
    ├── blob: index.ts (100644)
    └── blob: utils.ts (100644)

ファイルモードの意味(いみ):

モード意味(いみ)
100644通常(つうじょう)ファイル
100755実行(じっこう)ファイル
120000シンボリックリンク
040000ディレクトリ(tree)

1.3 Commit(コミット)

Commitオブジェクトはスナップショットのメタデータを格納(かくのう)します。

git cat-file -p HEAD
# 出力:
# tree a1b2c3d4e5f6...
# parent 9f8e7d6c5b4a...
# author Kim <kim@example.com> 1711234567 +0900
# committer Kim <kim@example.com> 1711234567 +0900
#
# feat: add user authentication

Commitオブジェクトの構成要素(こうせいようそ):

  • tree: このコミット時点(じてん)のプロジェクト全体(ぜんたい)のスナップショット(ルートtree参照(さんしょう))
  • parent: 親(おや)コミットSHA-1(最初(さいしょ)のコミットはparentなし、マージコミットは2つ以上(いじょう))
  • author: コードを書(か)いた人(ひと)
  • committer: コミットを作成(さくせい)した人(ひと)(cherry-pick時(じ)に異(こと)なる場合(ばあい)あり)
  • message: コミットメッセージ

1.4 Tag(タグ)

Annotated tagは別個(べっこ)のオブジェクトとして格納(かくのう)されます。

# annotated tag作成
git tag -a v1.0.0 -m "Release version 1.0.0"

# tagオブジェクト確認
git cat-file -p v1.0.0
# 出力:
# object d4e5f6a7b8c9...
# type commit
# tag v1.0.0
# tagger Kim <kim@example.com> 1711234567 +0900
#
# Release version 1.0.0

1.5 オブジェクト関係図(かんけいず)

Tag ──▶ Commit ──▶ Tree ──▶ Blob
           │         │
           ▼         ▼
        Commit     Tree ──▶ Blob
        (parent)

2. SHA-1ハッシュとContent-Addressable Storage

2.1 SHA-1の動作(どうさ)の仕組(しく)み

Gitはオブジェクトの内容(ないよう)にヘッダーを付(つ)けてSHA-1ハッシュを計算(けいさん)します。

# Gitが内部的に行う計算
content="Hello, Git!"
header="blob ${#content}\0"
echo -en "${header}${content}" | sha1sum
# 結果: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec

ヘッダー形式(けいしき): タイプ サイズ\0内容

blob 11\0Hello, Git!
└─┬─┘└┬┘└┬┘└────┬────┘
 タイプ サイズ null 実際の内容

2.2 Content-Addressableの意味(いみ)

同(おな)じ内容(ないよう)は常(つね)に同(おな)じハッシュを生成(せいせい)します。これにより以下(いか)が保証(ほしょう)されます:

  • 整合性(せいごうせい): データが改竄(かいざん)されるとハッシュが変(か)わる
  • 重複排除(じゅうふくはいじょ): 同(おな)じファイルは1回(かい)だけ格納(かくのう)
  • 効率的(こうりつてき)な比較(ひかく): ハッシュだけ比較(ひかく)すれば内容(ないよう)の同一性(どういつせい)を即座(そくざ)に判断(はんだん)

2.3 SHA-1衝突(しょうとつ)とSHA-256移行(いこう)

2017年(ねん)にGoogleがSHA-1衝突(しょうとつ)を実証(じっしょう)しました(SHAttered攻撃(こうげき))。GitはSHA-256への移行(いこう)を進(すす)めています。

# SHA-256リポジトリ作成(Git 2.29+)
git init --object-format=sha256

# 現在のリポジトリのハッシュアルゴリズム確認
git rev-parse --show-object-format
項目(こうもく)SHA-1SHA-256
ハッシュ長(ちょう)40文字(もじ)64文字(もじ)
セキュリティ衝突(しょうとつ)発見済(はっけんず)み安全(あんぜん)
互換性(ごかんせい)全(すべ)てのGitバージョンGit 2.29+
ステータスデフォルト実験的(じっけんてき)

3. .gitディレクトリの解剖(かいぼう)

git init後(ご)に生成(せいせい)される.gitディレクトリの構造(こうぞう)を見(み)てみましょう。

.git/
├── HEAD              # 現在チェックアウトされているブランチの参照
├── config            # リポジトリ固有の設定
├── description       # GitWeb用の説明(ほぼ使われない)
├── hooks/            # クライアント/サーバーフックスクリプト
│   ├── pre-commit.sample
│   ├── commit-msg.sample
│   └── ...
├── info/
│   └── exclude       # .gitignoreのローカル版
├── objects/          # 全てのGitオブジェクト
│   ├── 09/
│   │   └── 07f4a3c4740fa3a5c919cb4447fdb1f1a66aec
│   ├── info/
│   └── pack/         # packファイル
├── refs/             # ブランチとタグの参照
│   ├── heads/        # ローカルブランチ
│   │   └── main
│   ├── remotes/      # リモートブランチ
│   │   └── origin/
│   └── tags/         # タグ
└── index             # ステージングエリア(バイナリ)

3.1 HEADファイル

# HEADは現在のブランチを指す
cat .git/HEAD
# ref: refs/heads/main

# detached HEAD状態ではコミットハッシュを直接指す
git checkout a1b2c3d
cat .git/HEAD
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

3.2 refsディレクトリ

# ブランチは単にコミットハッシュを含むファイル
cat .git/refs/heads/main
# d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3

# 手動でブランチ作成も可能!
echo "d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3" > .git/refs/heads/my-branch

3.3 objectsディレクトリ

オブジェクトはSHA-1ハッシュの最初(さいしょ)の2文字(もじ)でディレクトリを作(つく)り、残(のこ)りの38文字(もじ)でファイル名(めい)を指定(してい)します。

# ハッシュ: 0907f4a3c4740fa3a5c919cb4447fdb1f1a66aec
# 保存場所: .git/objects/09/07f4a3c4740fa3a5c919cb4447fdb1f1a66aec

# オブジェクトはzlibで圧縮されている
python3 -c "
import zlib
with open('.git/objects/09/07f4a3c4740fa3a5c919cb4447fdb1f1a66aec', 'rb') as f:
    print(zlib.decompress(f.read()))
"

3.4 indexファイル(Staging Area)

# ステージングエリアの内容確認
git ls-files --stage
# 100644 a1b2c3d4... 0    README.md
# 100644 d4e5f6a7... 0    src/index.ts

# indexはバイナリファイル("DIRC"シグネチャで開始)
hexdump -C .git/index | head -3

4. DAGとコミットグラフ

4.1 DAG(有向非巡回(ゆうこうひじゅんかい)グラフ)とは

Gitのコミットヒストリーは**有向非巡回(ゆうこうひじゅんかい)グラフ(DAG)**というデータ構造(こうぞう)です。

  • 有向(ゆうこう)(Directed): コミットが親(おや)を指(さ)す(子(こ)から親(おや)方向(ほうこう))
  • 非巡回(ひじゅんかい)(Acyclic): 循環(じゅんかん)参照(さんしょう)不可能(ふかのう)(AがBの親(おや)かつ子(こ)にはなれない)
  • グラフ(Graph): ノード(コミット)とエッジ(親(おや)参照(さんしょう))で構成(こうせい)
    A ◀── B ◀── C ◀── D  (main)
                 └── E ◀── F  (feature)

4.2 コミットグラフの可視化(かしか)

# グラフの可視化
git log --oneline --graph --all

# 出力例:
# *   f1a2b3c (HEAD -> main) Merge branch 'feature'
# |\
# | * d4e5f6a (feature) feat: add search
# | * a7b8c9d feat: add filter
# |/
# * 1e2f3a4 initial commit

4.3 親(おや)ポインタ

# 最初の親のみ(マージコミットで有用)
git log --first-parent --oneline

# 親コミットへのアクセス
git cat-file -p HEAD^1   # 最初の親
git cat-file -p HEAD^2   # 2番目の親(マージコミット)
git cat-file -p HEAD~3   # 3段階上の祖先

親(おや)参照(さんしょう)の構文(こうぶん):

HEAD~1 = HEAD^  = 最初の親
HEAD~2 = HEAD^^ = 最初の親の最初の親
HEAD^2           = 2番目の親(マージコミットでのみ)

5. ブランチとタグ — 単(たん)なるポインタ

5.1 ブランチはポインタである

Gitのブランチは特定(とくてい)のコミットを指(さ)す40バイトのファイルに過(す)ぎません。

# ブランチ作成 = ファイル作成
git branch feature
# 上のコマンドは実質的に以下と同じ:
# echo $(git rev-parse HEAD) > .git/refs/heads/feature

# ブランチ切り替え = HEADファイルの修正
git checkout feature
# 上のコマンドは実質的に:
# 1. .git/HEADに "ref: refs/heads/feature" を記録
# 2. ワーキングツリーを該当コミットのtreeに更新
# 3. indexを該当treeに合わせて更新
.git/refs/heads/
├── main     → d4e5f6a (commit)
├── feature  → a7b8c9d (commit)
└── hotfix   → 1e2f3a4 (commit)

.git/HEAD → ref: refs/heads/main

5.2 HEADの役割(やくわり)

HEADは現在(げんざい)チェックアウトされている位置(いち)を指(さ)すポインタです。

通常状態:
HEAD → refs/heads/main → commit d4e5f6a

Detached HEAD状態:
HEAD → commit a7b8c9d(直接コミットを指す)

Detached HEADの注意点(ちゅういてん): この状態(じょうたい)でコミットすると、どのブランチも参照(さんしょう)しないため、他(ほか)のブランチにチェックアウトするとそのコミットにアクセスできなくなります。git gcが実行(じっこう)されるとそのコミットが削除(さくじょ)される可能性(かのうせい)があります。

# detached HEAD状態での作業を保存
git checkout -b recovery-branch

5.3 LightweightタグとAnnotatedタグ

# Lightweight tag: 単純な参照(refs/tags/にコミットハッシュのみ格納)
git tag v1.0.0-rc1

# Annotated tag: 別個のtagオブジェクト生成
git tag -a v1.0.0 -m "Release 1.0.0"

# 違いの確認
git cat-file -t v1.0.0-rc1  # commit
git cat-file -t v1.0.0      # tag

6. Mergeの内部動作(ないぶどうさ)

6.1 Fast-Forward Merge

mainからfeatureが分岐(ぶんき)した後(あと)、mainに追加(ついか)コミットがない場合(ばあい):

Before:
A ◀── B ◀── C (main)
              ◀── D ◀── E (feature)

After (fast-forward):
A ◀── B ◀── C ◀── D ◀── E (main, feature)
# fast-forward merge
git checkout main
git merge feature
# "Fast-forward" メッセージ表示

# fast-forwardせずにマージコミット作成を強制
git merge --no-ff feature

Fast-forwardはポインタを移動(いどう)するだけなので新(あたら)しいコミットは生成(せいせい)されません。

6.2 Three-Way Merge(3方向(ほうこう)マージ)

両方(りょうほう)のブランチに新(あたら)しいコミットがある場合(ばあい):

Before:
A ◀── B ◀── C ◀── F (main)
       ◀── D ◀── E (feature)

After (3-way merge):
A ◀── B ◀── C ◀── F ◀── G (main) [merge commit]
       ◀── D ◀── E ──────┘
              (feature)

3方向(ほうこう)マージアルゴリズム:

  1. 共通(きょうつう)祖先(そせん)(Merge Base)を探(さが)す: Bが共通(きょうつう)祖先(そせん)
  2. 両(りょう)ブランチの変更点(へんこうてん)を計算(けいさん): BからFまで、BからEまで
  3. 変更(へんこう)を統合(とうごう): コンフリクトなしなら自動(じどう)マージ、あれば手動(しゅどう)解決(かいけつ)
# merge base確認
git merge-base main feature
# 出力: BのSHA-1ハッシュ

# マージのための3つのtree比較
git diff $(git merge-base main feature) main    # base vs main
git diff $(git merge-base main feature) feature  # base vs feature

6.3 Recursive Strategy

Merge baseが複数(ふくすう)ある場合(ばあい)、Gitはrecursive strategyを使用(しよう)します。

    A ◀── B ◀── E (main)
    ▲      ◀── F (feature)
    └── C ◀── D
    (クロスマージヒストリー)

この場合(ばあい)、Gitは:

  1. 複数(ふくすう)のmerge baseを発見(はっけん)
  2. merge base同士(どうし)をまず仮想(かそう)マージ
  3. その結果(けっか)を実際(じっさい)のmerge baseとして使用(しよう)
# merge strategy指定
git merge -s recursive feature
git merge -s ort feature        # Git 2.34+ デフォルト(Ostensibly Recursive's Twin)

6.4 Octopus Merge

3つ以上(いじょう)のブランチを同時(どうじ)にマージする場合(ばあい)に使用(しよう)します。

git merge feature1 feature2 feature3
    A ◀── B (main)
    ◀── C (feature1)
    ◀── D (feature2)
    ◀── E (feature3)


    A ◀── B ◀── M (main) [3つの親]
    ◀── C ──────┘
    ◀── D ──────┘
    ◀── E ──────┘

注意(ちゅうい): コンフリクトが発生(はっせい)するとoctopus mergeは失敗(しっぱい)します。


7. Rebaseの内部動作(ないぶどうさ)

7.1 Rebaseの本質(ほんしつ): コミットの再生成(さいせいせい)

Rebaseは既存(きそん)のコミットを**コピーして新(あたら)しい親(おや)の上(うえ)に再生成(さいせいせい)**します。

Before:
A ◀── B ◀── C (main)
       ◀── D ◀── E (feature)

After rebase (git checkout feature && git rebase main):
A ◀── B ◀── C (main)
              ◀── D' ◀── E' (feature)

重要(じゅうよう): D'とE'はDとEとは異(こと)なるコミットです。内容(ないよう)は同(おな)じですが親(おや)が異(こと)なるためSHA-1も異(こと)なります。

7.2 Rebaseの内部(ないぶ)ステップ

git checkout feature
git rebase main

Gitが内部的(ないぶてき)に実行(じっこう)する作業(さぎょう):

  1. featureブランチのコミットでmainにないものを探(さが)す(D、E)
  2. これらのコミットのパッチを一時保存(いちじほぞん)(.git/rebase-apply/または.git/rebase-merge/
  3. featureをmainの最新(さいしん)コミット(C)にreset
  4. 保存(ほぞん)したパッチを1つずつ適用(てきよう)して新(あたら)しいコミットを生成(せいせい)(D'、E')
# rebase中の一時ファイル確認
ls .git/rebase-merge/
# done        # 完了したコミット
# git-rebase-todo  # 残りのコミット
# head-name   # 元のブランチ名
# onto        # rebase対象コミット

7.3 Interactive Rebase

git rebase -i HEAD~3
# .git/rebase-merge/git-rebase-todoの内容:
pick a1b2c3d feat: add login
pick d4e5f6a feat: add signup
pick 7b8c9d0 fix: typo in login

# コマンド:
# pick   = コミットを使用
# reword = メッセージ変更
# edit   = コミット修正後に続行
# squash = 前のコミットと結合(メッセージも結合)
# fixup  = 前のコミットと結合(メッセージは破棄)
# drop   = コミット削除

7.4 Rebase vs Mergeの比較(ひかく)

項目(こうもく)MergeRebase
ヒストリー分岐(ぶんき)維持(いじ)(非線形(ひせんけい))直線形(ちょくせんけい)
既存(きそん)コミット変更(へんこう)なし新(あたら)しいコミット生成(せいせい)
コンフリクト解決(かいけつ)1回(かい)のみコミットごとに可能(かのう)
共有(きょうゆう)ブランチ安全(あんぜん)危険(きけん)(force push必要(ひつよう))
マージコミット生成(せいせい)されるなし

黄金(おうごん)ルール: すでにpushしたコミットはrebaseしないでください。他(ほか)の人(ひと)がそのコミットをベースに作業(さぎょう)している可能性(かのうせい)があります。


8. Cherry-PickとRevert

8.1 Cherry-Pickの内部動作(ないぶどうさ)

Cherry-pickは**特定(とくてい)のコミットの変更点(へんこうてん)のみを現在(げんざい)のブランチに適用(てきよう)**します。

git cherry-pick d4e5f6a

内部動作(ないぶどうさ):

  1. 対象(たいしょう)コミット(d4e5f6a)とその親(おや)間(かん)のdiffを計算(けいさん)
  2. 現在(げんざい)のHEADに該当(がいとう)diffを適用(てきよう)
  3. 新(あたら)しいコミットを生成(せいせい)(同(おな)じメッセージ、異(こと)なるSHA-1)
Before:
A ◀── B ◀── C (main)
       ◀── D ◀── E (feature)

git checkout main && git cherry-pick E:
A ◀── B ◀── C ◀── E' (main) [Eの変更点のみコピー]
       ◀── D ◀── E (feature)

8.2 Revertの内部動作(ないぶどうさ)

Revertは**特定(とくてい)のコミットの逆方向(ぎゃくほうこう)パッチを適用(てきよう)**する新(あたら)しいコミットを生成(せいせい)します。

git revert d4e5f6a
Before:
A ◀── B ◀── C (main)

git revert B:
A ◀── B ◀── C ◀── B' (main) [Bの変更を元に戻す新しいコミット]

Revertはヒストリーを書(か)き換(か)えないため、共有(きょうゆう)ブランチで安全(あんぜん)です。

8.3 マージコミットのRevert

# マージコミットをrevertする際はどの親を基準にするか指定
git revert -m 1 MERGE_COMMIT_HASH
# -m 1: 最初の親(main)を基準に元に戻す
# -m 2: 2番目の親(feature)を基準に元に戻す

9. Reflog — セーフティネット

9.1 Reflogとは

Reflogは**HEADとブランチ参照(さんしょう)の全(すべ)ての変更(へんこう)履歴(りれき)**を記録(きろく)します。誤(あやま)ってコミットを削除(さくじょ)したりrebaseでヒストリーを壊(こわ)したりした際(さい)の復旧(ふっきゅう)に不可欠(ふかけつ)なツールです。

# reflog確認
git reflog
# d4e5f6a HEAD@{0}: commit: feat: add auth
# a1b2c3d HEAD@{1}: checkout: moving from feature to main
# 7b8c9d0 HEAD@{2}: commit: feat: add search
# 1e2f3a4 HEAD@{3}: rebase finished
# ...

# 特定ブランチのreflog
git reflog show feature

9.2 Reflogを利用(りよう)した復旧(ふっきゅう)シナリオ

シナリオ1: 誤(あやま)ってhard resetした場合(ばあい)

# ミス!
git reset --hard HEAD~3

# reflogで以前の状態を確認
git reflog
# a1b2c3d HEAD@{1}: 以前の状態

# 復旧
git reset --hard a1b2c3d

シナリオ2: rebase後(ご)に元(もと)の状態(じょうたい)に戻(もど)す

# rebase前の状態はreflogに残っている
git reflog
# ... HEAD@{5}: rebase (start): checkout main

# rebase前に復旧
git reset --hard HEAD@{5}

シナリオ3: 削除(さくじょ)したブランチの復旧(ふっきゅう)

# ブランチ削除
git branch -D feature

# reflogで該当ブランチの最後のコミットを探す
git reflog | grep feature

# ブランチ再作成
git branch feature a1b2c3d

9.3 Reflogの有効期限(ゆうこうきげん)

# デフォルトの有効期限
# 到達可能なエントリ: 90日
# 到達不可能なエントリ: 30日

# 有効期限の設定
git config gc.reflogExpire "180 days"
git config gc.reflogExpireUnreachable "60 days"

# 手動期限切れ
git reflog expire --expire=now --all

10. Packファイルとガベージコレクション

10.1 Looseオブジェクト vs Packedオブジェクト

Gitは最初(さいしょ)各(かく)オブジェクトを個別(こべつ)のファイル(looseオブジェクト)として格納(かくのう)します。オブジェクトが増(ふ)えるとpackファイルに圧縮(あっしゅく)します。

# looseオブジェクト数の確認
find .git/objects -type f | grep -v 'pack\|info' | wc -l

# packファイル確認
ls .git/objects/pack/
# pack-a1b2c3d4e5f6.idx   (インデックス)
# pack-a1b2c3d4e5f6.pack  (データ)

10.2 Delta Compression

PackファイルはDelta Compressionを使用(しよう)します。類似(るいじ)のファイルは差分(さぶん)のみを格納(かくのう)します。

# packファイルの内容確認
git verify-pack -v .git/objects/pack/pack-*.idx
# SHA-1 type size size-in-pack offset depth base-SHA-1
# a1b2c3d blob 10240 3521 12 0
# d4e5f6a blob 10245 45   1200 1 a1b2c3d  # delta!

上(うえ)の例(れい)ではd4e5f6aはa1b2c3dとの差分(さぶん)のみ45バイトで格納(かくのう)されています。

10.3 git gc(ガベージコレクション)

# ガベージコレクション実行
git gc

# 実行する作業:
# 1. looseオブジェクトをpackファイルに圧縮
# 2. 到達不可能なオブジェクトの削除
# 3. reflogのクリーンアップ
# 4. refsをpacked-refsに圧縮

# より積極的なGC
git gc --aggressive --prune=now

# GC統計確認
git count-objects -v
# count: 0        (looseオブジェクト)
# size: 0         (looseオブジェクトサイズ、KB)
# in-pack: 1234   (packされたオブジェクト)
# packs: 1        (packファイル数)
# size-pack: 5678 (packファイルサイズ、KB)
# prune-packable: 0
# garbage: 0

10.4 大容量(だいようりょう)ファイルの問題(もんだい)と解決(かいけつ)

# リポジトリで最も大きいファイルを探す
git rev-list --objects --all | \
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
  sed -n 's/^blob //p' | sort -rnk2 | head -10

# BFG Repo-Cleanerで大容量ファイル削除
bfg --strip-blobs-bigger-than 100M

# git filter-repo(推奨)
git filter-repo --strip-blobs-bigger-than 100M

11. PlumbingコマンドとPorcelainコマンド

Gitコマンドは2つの階層(かいそう)に分(わ)かれます。

11.1 Porcelain(磁器(じき)) — ユーザーフレンドリー

日常的(にちじょうてき)に使用(しよう)する高(こう)レベルコマンド:

git add, git commit, git push, git pull
git branch, git checkout, git merge, git rebase
git log, git diff, git status
git stash, git tag, git remote

11.2 Plumbing(配管(はいかん)) — 低(てい)レベル

Git内部(ないぶ)で使用(しよう)する低(てい)レベルコマンド:

# オブジェクト操作
git hash-object    # オブジェクトのハッシュ計算と格納
git cat-file       # オブジェクトの内容/タイプ/サイズ確認
git write-tree     # indexからtreeオブジェクト生成
git commit-tree    # treeからcommitオブジェクト生成
git update-ref     # 参照の更新

# Index操作
git update-index   # indexにファイル追加
git ls-files       # indexの内容確認
git read-tree      # treeをindexに読み込む

# 転送
git pack-objects   # packファイル生成
git unpack-objects # packファイル解凍
git send-pack      # オブジェクト送信
git receive-pack   # オブジェクト受信

11.3 Plumbingのみでコミットを作成(さくせい)

Porcelainなしでコミットを作(つく)る過程(かてい):

# 1. blob生成
echo "Hello World" | git hash-object -w --stdin
# a1b2c3d...

# 2. indexに追加
git update-index --add --cacheinfo 100644 a1b2c3d hello.txt

# 3. tree生成
git write-tree
# d4e5f6a...

# 4. commit生成
echo "first commit" | git commit-tree d4e5f6a
# 7b8c9d0...

# 5. ブランチがこのコミットを指すようにする
git update-ref refs/heads/main 7b8c9d0

12. 面接(めんせつ)質問集(しつもんしゅう)(10問(もん))

Q1. git addを実行(じっこう)するとGit内部(ないぶ)で何(なに)が起(お)きるか

模範(もはん)回答(かいとう): ファイル内容(ないよう)がSHA-1でハッシュされ、blobオブジェクトとして.git/objects/に格納(かくのう)されます。そして.git/index(ステージングエリア)に該当(がいとう)blobのハッシュとファイルパスが記録(きろく)されます。以前(いぜん)のバージョンはそのまま残(のこ)り、新(あたら)しいバージョンのみ追加(ついか)されます。

Q2. Gitブランチの内部構造(ないぶこうぞう)を説明(せつめい)せよ

模範(もはん)回答(かいとう): ブランチは.git/refs/heads/ディレクトリにある40バイトのファイルです。このファイルにはブランチが指(さ)すコミットのSHA-1ハッシュのみが格納(かくのう)されています。ブランチの切(き)り替(か)えはHEADファイルを修正(しゅうせい)し、ワーキングツリーとindexを該当(がいとう)コミットのtreeに更新(こうしん)することです。

Q3. mergeとrebaseの内部的(ないぶてき)な違(ちが)いを説明(せつめい)せよ

模範(もはん)回答(かいとう): Mergeは両方(りょうほう)のブランチの最新(さいしん)コミットと共通(きょうつう)祖先(そせん)を使(つか)った3方向(ほうこう)マージで新(あたら)しいマージコミットを生成(せいせい)します。Rebaseは現在(げんざい)のブランチのコミットを対象(たいしょう)ブランチの上(うえ)に1つずつ再生成(さいせいせい)します。Rebaseは新(あたら)しいコミットを作(つく)るためSHA-1が変(か)わります。

Q4. Detached HEADとは何(なに)か、なぜ危険(きけん)か

模範(もはん)回答(かいとう): HEADがブランチを経由(けいゆ)せず直接(ちょくせつ)コミットを指(さ)す状態(じょうたい)です。この状態(じょうたい)で新(あたら)しいコミットを作(つく)ると、どのブランチも参照(さんしょう)しないため、他(ほか)のブランチにチェックアウトするとアクセスできなくなります。git gcが実行(じっこう)されるとこれらの未参照(みさんしょう)コミットは削除(さくじょ)されます。

Q5. reflogで削除(さくじょ)されたコミットを復旧(ふっきゅう)する方法(ほうほう)を説明(せつめい)せよ

模範(もはん)回答(かいとう): git reflogでHEADの変更(へんこう)履歴(りれき)を確認(かくにん)し、復旧(ふっきゅう)したいコミットのハッシュを見(み)つけます。その後(ご)git reset --hard HASHまたはgit checkout -b recovery HASHで復旧(ふっきゅう)します。reflogはデフォルトで30日(にち)(未参照(みさんしょう))~90日(にち)(参照(さんしょう))保持(ほじ)されます。

Q6. packファイルとは何(なに)か、なぜ必要(ひつよう)か

模範(もはん)回答(かいとう): Gitは最初(さいしょ)各(かく)オブジェクトを個別(こべつ)ファイル(looseオブジェクト)として格納(かくのう)しますが、オブジェクトが増(ふ)えるとファイルシステムのパフォーマンスが低下(ていか)します。Packファイルは複数(ふくすう)のオブジェクトを1つのファイルに圧縮(あっしゅく)し、delta compressionで類似(るいじ)オブジェクト間(かん)の差分(さぶん)のみ格納(かくのう)します。git gcgit push時(じ)に自動生成(じどうせいせい)されます。

Q7. SHA-1衝突(しょうとつ)がGitに与(あた)える影響(えいきょう)と対策(たいさく)は

模範(もはん)回答(かいとう): SHA-1衝突(しょうとつ)時(じ)、異(こと)なる内容(ないよう)が同(おな)じハッシュを持(も)ちデータの整合性(せいごうせい)が壊(こわ)れます。2017年(ねん)のSHAttered攻撃(こうげき)で衝突(しょうとつ)が実証(じっしょう)され、Gitは衝突検出(しょうとつけんしゅつ)ロジックの追加(ついか)とSHA-256への移行(いこう)を進(すす)めています。Git 2.29以降(いこう)で--object-format=sha256オプションをサポートしています。

Q8. git clone --depth 1の内部動作(ないぶどうさ)を説明(せつめい)せよ

模範(もはん)回答(かいとう): Shallow cloneはヒストリーの一部(いちぶ)のみを取得(しゅとく)します。サーバーから最新(さいしん)のコミットとそれに必要(ひつよう)なtree、blobのみがpackファイルで転送(てんそう)されます。.git/shallowファイルにshallow boundaryコミットが記録(きろく)され、それ以前(いぜん)のヒストリーにはアクセスできません。

Q9. 3方向(ほうこう)マージアルゴリズムを説明(せつめい)せよ

模範(もはん)回答(かいとう): 2つのブランチの共通(きょうつう)祖先(そせん)(merge base)を探(さが)し、baseから各(かく)ブランチまでのdiffを計算(けいさん)します。片方(かたほう)だけ変更(へんこう)した部分(ぶぶん)は自動(じどう)適用(てきよう)、両方(りょうほう)が同(おな)じ部分(ぶぶん)を異(こと)なるように変更(へんこう)した場合(ばあい)はコンフリクトとしてマークします。Gitはgit merge-baseで共通(きょうつう)祖先(そせん)を探(さが)します。

Q10. Plumbingコマンドのみでコミットを作成(さくせい)する過程(かてい)を説明(せつめい)せよ

模範(もはん)回答(かいとう): (1) git hash-object -wでblob生成(せいせい)、(2) git update-indexでindexに追加(ついか)、(3) git write-treeでtree生成(せいせい)、(4) git commit-treeでcommit生成(せいせい)、(5) git update-refでブランチが新(あたら)しいコミットを指(さ)すようにします。


13. 実践(じっせん)クイズ(5問(もん))

Q1. 次のうちGitオブジェクトタイプでないものは? (a) blob (b) tree (c) branch (d) commit (e) tag

正解(せいかい): (c) branch

ブランチはGitオブジェクトではありません。ブランチはコミットを指(さ)すリファレンスで、.git/refs/heads/にテキストファイルとして格納(かくのう)されます。Gitの4つのオブジェクトタイプはblob、tree、commit、tagです。

Q2. git rebase maingit merge mainの結果(けっか)が同(おな)じになるケースは?

正解(せいかい): Fast-forwardが可能(かのう)な場合(ばあい)

現在(げんざい)のブランチがmainから分岐(ぶんき)後(ご)、mainに追加(ついか)コミットがない場合(ばあい)、mergeはfast-forwardされ、rebaseも同(おな)じ結果(けっか)になります。両方(りょうほう)とも同(おな)じ線形(せんけい)ヒストリーになります。

Q3. git reset --hard HEAD~1後(ご)に削除(さくじょ)されたコミットを復旧(ふっきゅう)するには?

正解(せいかい): git reflogで削除(さくじょ)されたコミットのハッシュを探(さが)し、git reset --hard HASHまたはgit branch recovery HASHで復旧(ふっきゅう)します。reflogはHEADの全(すべ)ての変更(へんこう)を記録(きろく)しているので、reset前(まえ)のコミットハッシュが残(のこ)っています。

Q4. 同(おな)じ内容(ないよう)のファイルが異(こと)なる名前(なまえ)で3つあるとき、Gitはblobを何個(なんこ)格納(かくのう)するか?

正解(せいかい): 1個(こ)

Gitはcontent-addressable storageなので、ファイル内容(ないよう)のSHA-1ハッシュをキーとして使用(しよう)します。同(おな)じ内容(ないよう)は同(おな)じハッシュなので、blobは1つだけ格納(かくのう)されます。ファイル名(めい)やパスの情報(じょうほう)はtreeオブジェクトに格納(かくのう)されます。

Q5. マージコミットの親(おや)が3つ以上(いじょう)になるケースは?

正解(せいかい): Octopus merge

git merge branch1 branch2 branch3のように3つ以上(いじょう)のブランチを同時(どうじ)にマージするとoctopus mergeが実行(じっこう)され、生成(せいせい)されるマージコミットは3つ以上(いじょう)の親(おや)を持(も)ちます。ただし、コンフリクトが発生(はっせい)するとoctopus mergeは失敗(しっぱい)します。


14. 参考(さんこう)資料(しりょう)

  1. Pro Git Book - Git Internals — 公式(こうしき)ドキュメント
  2. Git from the Bottom Up — John Wiegley
  3. How Git Works Internally — GitHub Blog
  4. Git Object Model — 公式(こうしき)ドキュメント
  5. SHAttered Attack — SHA-1衝突(しょうとつ)実証(じっしょう)
  6. Git SHA-256 Transition — SHA-256移行(いこう)計画(けいかく)
  7. Merge Strategies in Git — 公式(こうしき)ドキュメント
  8. Git Rebase Documentation — 公式(こうしき)ドキュメント
  9. Git Internals - Transfer Protocols
  10. Unpacking Git Packfiles — Recurse Center
  11. Git Delta Compression — Matthew McCullough
  12. Think Like a Git — DAGとグラフ理論(りろん)からGitを理解(りかい)