Ref・ブランチ
GitのRef(参照)機構を深堀りし、HEAD、ブランチ、タグ、reflogの内部表現とdetached HEAD状態の正しい理解・復旧方法を解説する。
Ref・ブランチ
GitのRef(参照)機構を深堀りし、HEAD、ブランチ、タグ、reflogの内部表現とdetached HEAD状態の正しい理解・復旧方法を解説する。
この章で学ぶこと
- Refの種類と内部表現 — ブランチ、タグ、リモート追跡ブランチがファイルシステム上でどう管理されるか
- HEADの仕組みとdetached HEAD — シンボリック参照の動作原理と安全な運用方法
- reflogによる履歴復元 — 失われたコミットの追跡と救出テクニック
- packed-refsと参照解決 — 大量のrefの最適化と解決優先順位
- タグの内部表現 — lightweight tagとannotated tagの違い
- ブランチ運用パターン — 実務で遭遇する様々なブランチ操作と内部動作の理解
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- Gitオブジェクトモデル の内容を理解していること
1. Refとは何か
RefはSHA-1ハッシュへのポインタであり、.git/refs/配下のテキストファイルとして保存される。Gitのオブジェクトモデル(blob、tree、commit、tag)では、すべてのオブジェクトがSHA-1(またはSHA-256)ハッシュで一意に識別されるが、40文字のハッシュを直接覚えるのは人間には困難である。Refはこのハッシュに人間が読みやすい名前を与える仕組みである。
1.1 Refのファイルシステム上の配置
.git/
├── HEAD ← シンボリック参照
├── ORIG_HEAD ← merge/rebase/reset前のHEAD位置
├── MERGE_HEAD ← マージ中の相手側HEAD
├── FETCH_HEAD ← fetch結果の一時参照
├── CHERRY_PICK_HEAD ← cherry-pick中の参照
├── REVERT_HEAD ← revert中の参照
├── refs/
│ ├── heads/ ← ローカルブランチ
│ │ ├── main ← "main"ブランチ
│ │ ├── develop ← "develop"ブランチ
│ │ └── feature/auth ← "feature/auth"ブランチ
│ ├── tags/ ← タグ
│ │ ├── v1.0.0
│ │ └── v2.0.0
│ ├── remotes/ ← リモート追跡ブランチ
│ │ ├── origin/
│ │ │ ├── main
│ │ │ ├── develop
│ │ │ └── feature/auth
│ │ └── upstream/
│ │ └── main
│ ├── stash ← stashの最新エントリ
│ └── notes/ ← git notesの参照
│ └── commits
├── packed-refs ← pack済みref(最適化)
└── logs/ ← reflog
├── HEAD
└── refs/
├── heads/
│ ├── main
│ └── feature/auth
└── remotes/
└── origin/
└── main
1.2 Refの実体を確認する
# ブランチの実体を確認
$ cat .git/refs/heads/main
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# HEADの実体を確認(シンボリック参照)
$ cat .git/HEAD
ref: refs/heads/main
# git rev-parseでRefをSHA-1に変換
$ git rev-parse main
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
$ git rev-parse HEAD
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# refが指すオブジェクトの型を確認
$ git cat-file -t refs/heads/main
commit
# 全てのrefを一覧表示
$ git for-each-ref --format='%(refname) %(objecttype) %(objectname:short)' refs/
refs/heads/develop commit a1b2c3d
refs/heads/feature/auth commit f5e6d7c
refs/heads/main commit a1b2c3d
refs/remotes/origin/main commit a1b2c3d
refs/tags/v1.0.0 tag 1234567
refs/tags/v2.0.0 commit 89abcde1.3 Refの名前解決ルール
Gitは省略されたref名を以下の順序で解決する。この順序を理解しておくことで、同名のブランチとタグが存在する場合の挙動を予測できる。
git rev-parse <name> の解決順序:
1. <name> をそのまま試す(例: HEAD、ORIG_HEAD)
2. refs/<name>
3. refs/tags/<name>
4. refs/heads/<name>
5. refs/remotes/<name>
6. refs/remotes/<name>/HEAD
# 同名のブランチとタグがある場合の問題
$ git branch v1.0.0 # ブランチ "v1.0.0" を作成
$ git checkout v1.0.0 # タグ? ブランチ? → 警告が出る
warning: refname 'v1.0.0' is ambiguous.
# 明示的に指定する方法
$ git checkout refs/heads/v1.0.0 # ブランチを指定
$ git checkout refs/tags/v1.0.0 # タグを指定(detached HEAD)
# rev-parseでの明示的な解決
$ git rev-parse refs/heads/v1.0.0 # ブランチのSHA-1
$ git rev-parse refs/tags/v1.0.0 # タグのSHA-11.4 特殊なRef
Gitには特定の操作中に自動的に設定される特殊なRefがある。
| Ref名 | 設定タイミング | 用途 |
|---|---|---|
HEAD |
常時 | 現在のチェックアウト位置 |
ORIG_HEAD |
merge/rebase/reset後 | 操作前のHEAD位置(取り消し用) |
MERGE_HEAD |
merge中 | マージ中の相手ブランチのHEAD |
FETCH_HEAD |
fetch後 | 最後にfetchした結果 |
CHERRY_PICK_HEAD |
cherry-pick中 | cherry-pick対象のコミット |
REVERT_HEAD |
revert中 | revert対象のコミット |
BISECT_HEAD |
bisect中 | 現在のbisectチェックポイント |
# ORIG_HEADを使った操作の取り消し
$ git merge feature/auth
# マージを取り消したい場合:
$ git reset --hard ORIG_HEAD
# FETCH_HEADの確認
$ git fetch origin
$ cat .git/FETCH_HEAD
a1b2c3d4e5f6... branch 'main' of https://github.com/user/repo
# MERGE_HEADはマージ中のみ存在
$ git merge feature/auth
# コンフリクト発生中:
$ cat .git/MERGE_HEAD
f5e6d7c8b9a0e1f2d3c4b5a6d7e8f9a0b1c2d3e4
# マージ完了後はファイルが削除される2. HEADの仕組み
HEADはGitで最も重要なRefであり、現在のチェックアウト位置を示す。通常はブランチへのシンボリック参照だが、特定のコミットを直接指すこともある(detached HEAD)。
2.1 通常のHEAD(attached)
| .git/HEAD | |
|---|---|
| "ref: refs/heads/feature/auth" | |
| ▼ | |
| .git/refs/heads/feature/auth | |
| "c3d4e5f6..." | |
| ▼ | |
| commit c3d4e5f6... | |
| ├── tree ... | |
| ├── parent ... | |
| └── message: "Add login form" |
新しいコミット時:
1. 新commitオブジェクト作成(parent = c3d4e5f6...)
2. refs/heads/feature/auth を新commitのSHA-1に更新
3. HEADは refs/heads/feature/auth を指したまま
HEADがブランチを間接参照している状態では、git commitを実行するとブランチのポインタが自動的に前進する。これがGitの通常の動作であり、ブランチが「成長する」仕組みの本質である。
# HEADの状態を確認するコマンド群
$ git symbolic-ref HEAD
refs/heads/feature/auth # ブランチ名が返る = attached
$ git rev-parse HEAD
c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0a1b2 # SHA-1が返る
$ git rev-parse --abbrev-ref HEAD
feature/auth # 短縮形のブランチ名
# コミット前後のブランチ位置の変化
$ git rev-parse feature/auth
c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0a1b2
$ echo "new content" >> file.txt && git add file.txt && git commit -m "update"
$ git rev-parse feature/auth
d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0a1b2c3 # 新しいSHA-1に更新2.2 detached HEAD
detached HEADは、HEADがブランチではなく特定のコミットを直接指している状態である。
# detached HEADになる主な操作
$ git checkout a1b2c3d # 特定コミットのチェックアウト
$ git checkout v1.0.0 # タグのチェックアウト
$ git checkout origin/main # リモート追跡ブランチのチェックアウト
$ git rebase --onto main feature HEAD~3 # rebase操作の途中
# HEADの状態確認
$ cat .git/HEAD
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# ← "ref:" プレフィックスがない = detached
$ git symbolic-ref HEAD
fatal: ref HEAD is not a symbolic ref
# ← symbolic-refはdetached HEADではエラーになる
$ git status
HEAD detached at a1b2c3d
# Gitはdetached HEAD状態を明確に表示する| 通常のHEAD(attached) |
|---|
| HEAD ──→ refs/heads/main ──→ commit |
| detached HEAD |
| HEAD ──→ commit(直接参照) |
| refs/heads/main ──→ 別のcommit |
| ※ この状態で新コミットを作ると |
| どのブランチにも属さないコミット |
| が生成される(GC対象になりうる) |
2.3 detached HEADの正しい活用シーン
detached HEADは必ずしも危険な状態ではない。以下のようなユースケースでは意図的に使用される。
# ユースケース1: 過去のコミットを一時的に確認する
$ git checkout v1.0.0
# テストを実行して過去のバージョンの動作を確認
$ make test
# 確認が終わったら元のブランチに戻る
$ git checkout main
# ユースケース2: CI/CDパイプラインでのタグチェックアウト
# Jenkins/GitHub Actionsなどでタグベースのビルドを行う
$ git checkout v2.1.0
$ docker build -t myapp:2.1.0 .
# ユースケース3: bisect中の自動チェックアウト
$ git bisect start
$ git bisect bad HEAD
$ git bisect good v1.0.0
# → Gitが自動的にdetached HEADで中間コミットをチェックアウト
# ユースケース4: worktreeでの一時的な作業
$ git worktree add /tmp/hotfix v1.0.0
# → worktreeはdetached HEADで作成可能2.4 detached HEADからの復帰
# 方法1: 新しいブランチを作成して退避
$ git checkout -b rescue-branch
# → 現在のHEAD位置に新ブランチを作成し、attachedに戻る
# 方法2: 既存ブランチに戻る
$ git checkout main
# → detached HEAD中に作ったコミットがある場合、
# reflogにのみ記録される(ブランチには属さない)
# 方法3: detached HEAD中に作ったコミットを救出
$ git reflog
# a1b2c3d HEAD@{0}: checkout: moving from main to a1b2c3d
# f5e6d7c HEAD@{1}: commit: important work in detached state
$ git branch rescue-branch f5e6d7c
# 方法4: Git 2.23以降のswitchコマンド
$ git switch main # ブランチに戻る
$ git switch -c new-branch # 新ブランチを作って戻る
$ git switch --detach v1.0.0 # 意図的にdetachする(明示的)
# 方法5: detached HEAD中の複数コミットをまとめて救出
$ git reflog
# abc1234 HEAD@{0}: commit: third fix
# def5678 HEAD@{1}: commit: second fix
# 789abcd HEAD@{2}: commit: first fix
# a1b2c3d HEAD@{3}: checkout: moving from main to a1b2c3d
$ git branch rescue-branch abc1234
# → abc1234から辿れる全コミット(first/second/third fix)が保護される2.5 HEADの内部操作
git update-refコマンドを使うことで、低レベルでRefを操作できる。通常のGitコマンドの裏側で実行されている処理を理解するのに役立つ。
# HEADが指すブランチを変更(git checkoutの内部動作に近い)
$ git symbolic-ref HEAD refs/heads/feature/auth
# ブランチを新しいコミットに更新(git commitの内部動作の一部)
$ git update-ref refs/heads/main a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# update-refは安全なref更新を行う
# - reflogエントリを自動作成
# - ロックファイル(.lock)を使用して並行アクセスを保護
$ ls .git/refs/heads/main.lock
# → update-ref実行中のみ存在する一時ファイル
# refの削除
$ git update-ref -d refs/heads/old-branch
# → refs/heads/old-branchファイルを削除し、reflogにも記録3. ブランチの操作と内部動作
3.1 ブランチの作成・削除の内部動作
# ブランチ作成 = ファイル作成
$ git branch feature/new-ui
# → .git/refs/heads/feature/new-ui にHEADのSHA-1を書き込み
# → .git/logs/refs/heads/feature/new-ui にreflogエントリを作成
# 特定コミットからブランチ作成
$ git branch feature/from-tag v1.0.0
# → v1.0.0が指すSHA-1を書き込み
# ブランチ削除 = ファイル削除
$ git branch -d feature/new-ui
# → .git/refs/heads/feature/new-ui を削除
# (commitオブジェクト自体は削除されない)
# マージ済みでない場合はエラーになる
# 強制削除(マージ状態を確認しない)
$ git branch -D feature/new-ui
# → -d --force と同等、マージ済みでなくても削除
# ブランチ名の変更 = ファイルのリネーム + reflog更新
$ git branch -m old-name new-name
# → refs/heads/old-name → refs/heads/new-name
# → logs/refs/heads/old-name → logs/refs/heads/new-name
# → configのブランチ設定も更新3.2 ブランチの内部操作を手動で再現する
Gitのブランチは本質的には「commitオブジェクトのSHA-1が書かれたテキストファイル」に過ぎない。この事実を確認するために、手動でブランチを操作してみる。
# 現在のHEADのSHA-1を確認
$ git rev-parse HEAD
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# 手動でブランチを作成(git branchの代替)
$ echo "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0" > .git/refs/heads/manual-branch
# → git branch -a で manual-branch が表示される
# ただし上記の方法はreflogが作成されず非推奨
# 正しい低レベル操作:
$ git update-ref refs/heads/manual-branch HEAD
# → reflogエントリも作成される
# 手動でブランチを移動(git reset --hardの内部動作に近い)
$ git update-ref refs/heads/main f5e6d7c8b9a0e1f2d3c4b5a6d7e8f9a0b1c2d3e4
# → mainブランチが別のコミットを指すようになる
# ブランチのトラッキング設定
$ git branch --set-upstream-to=origin/main main
# → .git/config に以下が書き込まれる:
# [branch "main"]
# remote = origin
# merge = refs/heads/main3.3 ブランチの階層構造(名前空間)
Gitのブランチ名にはスラッシュ(/)を含めることができ、ファイルシステム上ではディレクトリ階層として表現される。
# スラッシュを含むブランチ名
$ git branch feature/auth/login
$ git branch feature/auth/signup
$ git branch feature/ui/dashboard
# ファイルシステム上の構造
$ find .git/refs/heads -type f
.git/refs/heads/main
.git/refs/heads/feature/auth/login
.git/refs/heads/feature/auth/signup
.git/refs/heads/feature/ui/dashboard
# 注意: "feature/auth" というブランチと "feature/auth/login" は共存できない
# → "feature/auth" はファイルだが "feature/auth/" はディレクトリになるため
$ git branch feature/auth
fatal: cannot lock ref 'refs/heads/feature/auth':
'refs/heads/feature/auth/login' exists; cannot create 'refs/heads/feature/auth'
# ブランチ一覧のフィルタリング
$ git branch --list 'feature/*'
feature/auth/login
feature/auth/signup
feature/ui/dashboard
$ git branch --list 'feature/auth/*'
feature/auth/login
feature/auth/signup3.4 リモート追跡ブランチ
# リモート追跡ブランチの一覧
$ git branch -r
origin/main
origin/develop
origin/feature/auth
upstream/main
# 全ブランチ(ローカル + リモート追跡)
$ git branch -a
develop
feature/auth
* main
remotes/origin/develop
remotes/origin/feature/auth
remotes/origin/main
remotes/upstream/main
# fetch時の動作
$ git fetch origin
# → refs/remotes/origin/* を更新
# → ローカルブランチは変更しない
# リモート追跡ブランチの更新ルール(refspec)
$ cat .git/config
[remote "origin"]
url = https://github.com/user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[remote "upstream"]
url = https://github.com/upstream/repo.git
fetch = +refs/heads/*:refs/remotes/upstream/*| refspec の構造 | |||
|---|---|---|---|
| +refs/heads/*:refs/remotes/origin/* | |||
| └── ローカル側のref | |||
| └── リモート側のref | |||
| └── "+" = 非fast-forwardでも強制更新 | |||
| 例: origin/main が更新された場合 | |||
| refs/heads/main (remote) | |||
| → refs/remotes/origin/main (local) |
3.5 高度なrefspec操作
# 特定ブランチのみfetch
$ git fetch origin main
# → refs/remotes/origin/main のみ更新
# カスタムrefspecでfetch
$ git fetch origin +refs/heads/release/*:refs/remotes/origin/release/*
# → release/ で始まるブランチのみ取得
# pushのrefspec
$ git push origin main:main
# → ローカルのmain をリモートの main にpush
$ git push origin feature/auth:refs/heads/feature/auth
# → 明示的なrefspec指定
# リモートブランチの削除
$ git push origin --delete feature/old
# → リモートの feature/old ブランチを削除
# → ローカルの refs/remotes/origin/feature/old も削除
# refspecでリモートブランチを削除する別の方法
$ git push origin :feature/old
# → "空" をfeature/oldにpush = 削除
# 不要になったリモート追跡ブランチの整理
$ git remote prune origin
# → リモートに存在しなくなったrefs/remotes/origin/*を削除
$ git fetch --prune origin
# → fetchと同時にpruneも実行(推奨設定)
# 自動pruneの設定
$ git config fetch.prune true
# → 毎回のfetchで自動的にpruneが実行される3.6 上流ブランチ(upstream)の設定と活用
# 上流ブランチの設定
$ git branch --set-upstream-to=origin/main main
# または
$ git push -u origin feature/auth
# → push時に自動的に上流ブランチを設定
# 上流ブランチの確認
$ git branch -vv
* feature/auth abc1234 [origin/feature/auth: ahead 2] latest commit
main def5678 [origin/main] synced commit
develop 789abcd [origin/develop: behind 3] older commit
# 上流ブランチとの差分確認
$ git log @{upstream}..HEAD # ローカルにあってリモートにないコミット
$ git log HEAD..@{upstream} # リモートにあってローカルにないコミット
$ git log @{upstream}...HEAD # 双方の差分(対称差分)
# @{push}との違い(Git 2.5+)
$ git log @{push}..HEAD
# → pushする先のブランチとの差分(triangular workflowで有用)
# 例: fetchはupstreamから、pushはoriginへ、という運用4. タグの内部表現
4.1 lightweight tag vs annotated tag
Gitのタグには2種類があり、内部表現が異なる。
# lightweight tag の作成
$ git tag v1.0.0-rc1
# → .git/refs/tags/v1.0.0-rc1 にcommitのSHA-1を直接保存
# → タグオブジェクトは作成されない
$ cat .git/refs/tags/v1.0.0-rc1
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
$ git cat-file -t v1.0.0-rc1
commit # ← commitオブジェクトを直接指している
# annotated tag の作成
$ git tag -a v1.0.0 -m "Release version 1.0.0"
# → tagオブジェクトが作成される
# → .git/refs/tags/v1.0.0 にtagオブジェクトのSHA-1を保存
$ git cat-file -t v1.0.0
tag # ← tagオブジェクトを指している
$ git cat-file -p v1.0.0
object a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
type commit
tag v1.0.0
tagger Gaku <gaku@example.com> 1707600000 +0900
Release version 1.0.0| lightweight tag |
|---|
| refs/tags/v1.0.0-rc1 ──→ commit object |
| (SHA-1を直接保存) |
| annotated tag |
| refs/tags/v1.0.0 ──→ tag object ──→ commit object |
| (tagオブジェクトを経由) |
| tag objectの内容: |
| - object: 対象commitのSHA-1 |
| - type: commit |
| - tag: タグ名 |
| - tagger: 作成者情報 |
| - message: タグメッセージ |
| - GPG signature(署名付きの場合) |
4.2 署名付きタグ
# GPG署名付きタグの作成
$ git tag -s v1.0.0 -m "Signed release v1.0.0"
# → tagオブジェクトにGPG署名が含まれる
# 署名の検証
$ git tag -v v1.0.0
object a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
type commit
tag v1.0.0
tagger Gaku <gaku@example.com> 1707600000 +0900
Signed release v1.0.0
gpg: Signature made Mon 12 Feb 2024 10:00:00 AM JST
gpg: Good signature from "Gaku <gaku@example.com>"
# SSH署名(Git 2.34+)
$ git config gpg.format ssh
$ git config user.signingKey ~/.ssh/id_ed25519.pub
$ git tag -s v2.0.0 -m "SSH signed release v2.0.0"4.3 タグのpush
タグはデフォルトではgit pushでリモートに送信されない。明示的な操作が必要。
# 個別タグのpush
$ git push origin v1.0.0
# 全タグのpush
$ git push origin --tags
# → lightweight tag と annotated tag の両方がpushされる
# annotated tagのみpush(Git 2.4+)
$ git push origin --follow-tags
# → annotated tagのみ選択的にpush
# リモートのタグを削除
$ git push origin --delete v1.0.0
# または
$ git push origin :refs/tags/v1.0.0
# リモートからタグを再取得
$ git fetch origin --tags
# → ローカルに存在しないタグをリモートから取得4.4 タグの"peeling"
packed-refsやfor-each-refでは、annotated tagが最終的に指すcommitのSHA-1も記録される。これを「peeling」と呼ぶ。
# peelされたタグの確認
$ git for-each-ref --format='%(refname) %(objectname:short) → %(objectname:short=,deref)' refs/tags/
# peel先のcommit SHA-1を直接取得
$ git rev-parse v1.0.0^{}
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# ^{} は tagオブジェクトを「剥がして」内部のcommitを返す
# packed-refsでのpeeled表現
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
f5e6d7c8b9a0e1f2d3c4b5a6d7e8f9a0b1c2d3e4 refs/tags/v1.0.0
^a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# ^ から始まる行がpeeled SHA-1(tagが指すcommit)5. reflog — 操作履歴の記録
5.1 reflogの基本
reflogはrefの変更履歴を記録するローカル専用の仕組みである。git cloneやgit pushでは転送されない。リポジトリローカルの「操作日誌」であり、誤操作からの復旧の最後の手段となる。
# HEADのreflogを表示
$ git reflog
a1b2c3d HEAD@{0}: commit: feat: add authentication
f5e6d7c HEAD@{1}: checkout: moving from feature to main
b8c9d0e HEAD@{2}: commit: fix: typo in README
1234567 HEAD@{3}: merge feature/auth: Fast-forward
89abcde HEAD@{4}: commit: refactor: extract utils
fedcba0 HEAD@{5}: rebase (finish): returning to refs/heads/main
fedcba0 HEAD@{6}: rebase (pick): update config
1111111 HEAD@{7}: rebase (start): checkout origin/main
# 特定ブランチのreflog
$ git reflog show main
a1b2c3d main@{0}: commit: feat: add authentication
f5e6d7c main@{1}: merge feature/ui: Merge made by 'ort'
b8c9d0e main@{2}: commit: initial setup
# 日時指定でのアクセス
$ git show main@{2.days.ago}
$ git show HEAD@{2024-02-01}
$ git show HEAD@{yesterday}
$ git show main@{1.week.ago}
# reflogの詳細表示
$ git reflog --format='%C(auto)%h %gd %gs %ci'
a1b2c3d HEAD@{0} commit: feat: add authentication 2024-02-12 10:00:00 +0900
f5e6d7c HEAD@{1} checkout: moving from feature to main 2024-02-12 09:45:00 +0900
# reflogのdiff表示
$ git diff HEAD@{0} HEAD@{3}
# → 3操作前との差分を表示5.2 reflogの保存場所と形式
# reflogファイルの確認
$ cat .git/logs/HEAD
# 各行: 旧SHA-1 新SHA-1 操作者 タイムスタンプ 操作内容
$ cat .git/logs/refs/heads/main
0000000... a1b2c3d... Gaku <gaku@example.com> 1707600000 +0900 commit (initial): first commit
a1b2c3d... f5e6d7c... Gaku <gaku@example.com> 1707603600 +0900 commit: second commit
f5e6d7c... b8c9d0e... Gaku <gaku@example.com> 1707607200 +0900 merge feature/auth: Merge made by 'ort'| reflogエントリの形式 |
|---|
| <旧SHA-1> <新SHA-1> <名前> <<メール>> <UNIXtime> <TZ>\t<メッセージ> |
| 例: |
| a1b2c3d... f5e6d7c... Gaku <g@ex.com> 1707600000 +0900 |
| \tcommit: add feature |
| 旧SHA-1が 0000000... の場合 = ブランチの新規作成 |
| 新SHA-1が 0000000... の場合 = ブランチの削除 |
5.3 reflogの有効期限
| 種別 | デフォルト期限 | 設定キー |
|---|---|---|
| 到達可能なエントリ | 90日 | gc.reflogExpire |
| 到達不可能なエントリ | 30日 | gc.reflogExpireUnreachable |
# reflogの期限を変更
$ git config gc.reflogExpire "180 days"
$ git config gc.reflogExpireUnreachable "60 days"
# 特定のrefに対する個別設定
$ git config gc.main.reflogExpire "365 days"
# → mainブランチのreflogは1年間保持
# 手動でreflogを期限切れにする
$ git reflog expire --expire=now --all
# → 全refの全reflogエントリを即座に期限切れに(危険!)
# 特定のrefのreflogのみ期限切れにする
$ git reflog expire --expire=30.days.ago refs/heads/feature/old
# dry-runで確認
$ git reflog expire --expire=30.days.ago --dry-run --all
# → 実際には削除せず、削除対象を表示5.4 reflogを使った復旧テクニック
# テクニック1: git reset --hard の取り消し
$ git reset --hard HEAD~3 # 直近3コミットを破棄
# "やっぱり元に戻したい"
$ git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# f5e6d7c HEAD@{1}: commit: important commit 3
# b8c9d0e HEAD@{2}: commit: important commit 2
# 1234567 HEAD@{3}: commit: important commit 1
$ git reset --hard HEAD@{1}
# → reset前の状態に復帰
# テクニック2: 削除したブランチの復元
$ git branch -D feature/important
# "削除すべきではなかった"
$ git reflog
# → feature/importantの最後のコミットを探す
$ git branch feature/important HEAD@{2}
# テクニック3: 失敗したrebaseの取り消し
$ git rebase main
# コンフリクトだらけで収拾がつかない
$ git rebase --abort # rebase中なら --abort が使える
# rebase完了後に元に戻したい場合
$ git reflog
# → rebase開始前のHEAD位置を探す
$ git reset --hard HEAD@{5} # rebase前の状態に復帰
# テクニック4: amend前のコミットを取得
$ git commit --amend -m "corrected message"
# "amend前のコミットも保存しておきたい"
$ git reflog
# a1b2c3d HEAD@{0}: commit (amend): corrected message
# f5e6d7c HEAD@{1}: commit: original message
$ git branch backup-original f5e6d7c
# テクニック5: stash dropの復元
$ git stash drop stash@{0}
# "ドロップしたstashを取り戻したい"
$ git fsck --no-reflogs | grep commit
# dangling commit f5e6d7c...
$ git stash apply f5e6d7c5.5 reflogとgit fsckの連携
reflogの期限が切れた後でも、GCが実行されるまではオブジェクト自体は残っている可能性がある。git fsckで到達不可能なオブジェクトを探索できる。
# 到達不可能なオブジェクトを探す
$ git fsck --unreachable
unreachable commit a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
unreachable blob f5e6d7c8b9a0e1f2d3c4b5a6d7e8f9a0b1c2d3e4
unreachable tree 1234567890abcdef1234567890abcdef12345678
# 到達不可能なコミットを lost-found に保存
$ git fsck --lost-found
# → .git/lost-found/commit/ にコミットのSHA-1ファイルが作成される
# → .git/lost-found/other/ にその他のオブジェクトが保存される
# dangling object(どこからも参照されていないオブジェクト)の確認
$ git fsck --no-reflogs
# → reflogからの到達可能性を無視して判定
# → reflogでのみ保護されているオブジェクトも表示される
# 特定のdanglingコミットの内容を確認
$ git log --oneline --graph a1b2c3d4
$ git show a1b2c3d46. packed-refs
大量のrefがある場合、個別ファイルではなくpacked-refsにまとめて性能を向上させる。
6.1 packed-refsの構造
# packed-refsの中身
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 refs/heads/main
b8c9d0e1f2a3b4c5d6e7f8a9b0a1b2c3d4e5f6a7 refs/heads/develop
f5e6d7c8b9a0e1f2d3c4b5a6d7e8f9a0b1c2d3e4 refs/tags/v1.0.0
^a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
89abcdef0123456789abcdef0123456789abcdef refs/tags/v2.0.0
# ^ = peeled tag(tagオブジェクトが指すcommitのSHA-1)
# 行頭の # はコメント
# 手動でpackする
$ git pack-refs --all
# → 全てのloose refをpacked-refsに統合
# → アクティブなブランチ(現在のHEAD)のloose refは残る場合がある
# packのみ(looseを削除しない)
$ git pack-refs --no-prune6.2 ref解決の優先順位と動作
| ref解決の優先順位 |
|---|
| 1. .git/refs/heads/<name> (loose ref) |
| 2. .git/packed-refs 内の該当行 |
| → looseが存在すればそちらが優先 |
| → ブランチ更新時は loose に書き込み |
| → git gc / pack-refs で packed に統合 |
| 更新の流れ: |
| 1. git commit → loose ref が更新される |
| 2. looseとpackedの両方に同じrefが存在する場合 |
| → looseが最新(packed側は古いまま) |
| 3. git gc → loose を packed に統合 |
| → loose ファイルを削除 |
| 4. 次のcommit → 再びloose refが作成される |
6.3 packed-refsの性能への影響
# 大量のブランチ/タグがある場合の比較
# (例: 10,000タグのリポジトリ)
# looseオブジェクトの場合:
# → 10,000個のファイルがrefs/tags/に作成される
# → ファイルシステムのinode消費が激しい
# → ls-remote, branch -a 等が遅くなる
# packed-refsの場合:
# → 1ファイルに全タグが格納される
# → ファイルシステムの負荷が大幅に減少
# → 参照解決も高速化(sequential read vs random seek)
# パフォーマンス測定
$ time git for-each-ref refs/tags/ | wc -l
# packed-refs: ~0.01s
# loose refs: ~0.5s (10,000ファイルの場合)
# reftableフォーマット(Git 2.45+、実験的)
# → packed-refsのさらなる進化版
# → バイナリ形式で高速なルックアップが可能
# → JGit発のフォーマットをC実装に移植
$ git config core.repositoryFormatVersion 1
$ git config extensions.refStorage reftable
# → 注意: reftableは実験的機能であり、互換性リスクがある7. シンボリック参照
シンボリック参照は他のrefを間接参照するrefである。最も一般的な例はHEADであり、通常はブランチrefを指す。
7.1 基本操作
# HEADが最も一般的なシンボリック参照
$ git symbolic-ref HEAD
refs/heads/main
# カスタムシンボリック参照の作成
$ git symbolic-ref refs/custom/current refs/heads/feature/auth
# → refs/custom/current は feature/auth を間接参照する
# detached HEAD時はエラーになる
$ git symbolic-ref HEAD
fatal: ref HEAD is not a symbolic ref
# シンボリック参照の安全な確認(エラーを回避)
$ git symbolic-ref --quiet HEAD && echo "attached" || echo "detached"7.2 シンボリック参照の実用例
# リモートのデフォルトブランチ(HEAD)
$ git remote show origin
# → "HEAD branch: main" と表示される
# → これは refs/remotes/origin/HEAD がシンボリック参照
$ cat .git/refs/remotes/origin/HEAD
ref: refs/remotes/origin/main
# origin/HEADを更新
$ git remote set-head origin develop
# → refs/remotes/origin/HEAD が refs/remotes/origin/develop を指すように変更
# origin/HEADを自動検出で設定
$ git remote set-head origin --auto
# → リモートに問い合わせてデフォルトブランチを自動設定
# ワークツリーでのHEAD
# メインワークツリー: .git/HEAD
# 追加ワークツリー: .git/worktrees/<name>/HEAD
$ git worktree add ../feature-worktree feature/auth
$ cat .git/worktrees/feature-worktree/HEAD
ref: refs/heads/feature/auth8. ブランチ保護と運用パターン
8.1 ローカルでのブランチ保護
# receive.denyNonFastForwardsによる保護(共有リポジトリ)
$ git config receive.denyNonFastForwards true
# → 非fast-forwardのpushを全て拒否
# receive.denyDeletesによる削除防止
$ git config receive.denyDeletes true
# → ブランチ/タグの削除pushを拒否
# pre-receiveフックによるブランチ保護(サーバーサイド)
# .git/hooks/pre-receive:
#!/bin/bash
while read oldrev newrev refname; do
if [ "$refname" = "refs/heads/main" ]; then
# mainブランチへの直接pushを拒否
echo "ERROR: Direct push to main is not allowed."
echo "Please create a pull request instead."
exit 1
fi
done
# update フックによる個別ref制御
# .git/hooks/update:
#!/bin/bash
refname="$1"
oldrev="$2"
newrev="$3"
if [ "$refname" = "refs/heads/main" ] && \
[ "$(git merge-base $oldrev $newrev)" != "$oldrev" ]; then
echo "ERROR: Non-fast-forward push to main is not allowed."
exit 1
fi8.2 ブランチの整理と棚卸し
# マージ済みブランチの一覧
$ git branch --merged main
feature/auth # mainにマージ済み
feature/old-ui # mainにマージ済み
* main
# 未マージブランチの一覧
$ git branch --no-merged main
feature/wip # 進行中の作業
# マージ済みブランチを一括削除
$ git branch --merged main | grep -v '^\*' | grep -v 'main' | xargs git branch -d
# リモートで削除済みだがローカルに残っている追跡ブランチの確認
$ git remote prune origin --dry-run
Pruning origin
* [would prune] origin/feature/deleted-remote
# 最終コミット日時でソートしたブランチ一覧
$ git for-each-ref --sort=-committerdate --format='%(committerdate:short) %(refname:short)' refs/heads/
2024-02-12 main
2024-02-10 feature/auth
2024-01-15 feature/old
2023-11-20 feature/ancient
# 3ヶ月以上更新のないブランチを検出するスクリプト
$ git for-each-ref --sort=committerdate --format='%(committerdate:unix) %(refname:short)' refs/heads/ | \
while read timestamp branch; do
if [ "$timestamp" -lt "$(date -d '3 months ago' +%s)" ]; then
echo "Stale: $branch ($(date -d @$timestamp +%Y-%m-%d))"
fi
done8.3 ブランチの命名規則
実務でよく使われるブランチ命名パターンとその内部動作への影響を整理する。
| ブランチ命名規則の例 |
|---|
| パターン 例 用途 |
| ──────────────────────────────────────────────────── |
| feature/<name> feature/user-auth 新機能 |
| bugfix/<name> bugfix/login-crash バグ修正 |
| hotfix/<name> hotfix/security-patch 緊急修正 |
| release/<ver> release/2.1.0 リリース準備 |
| chore/<name> chore/update-deps メンテナンス |
| refactor/<name> refactor/auth-module リファクタ |
| 注意: "feature" と "feature/x" は共存不可 |
| → ディレクトリとファイルの衝突 |
| → 命名規則を決めたらチームで統一する |
# ブランチ名に使えない文字
# - スペース、~、^、:、?、*、[、\
# - ".." を含む名前
# - "." で始まる名前
# - "/" で終わる名前
# - ".lock" で終わる名前
# - ASCII制御文字
# ブランチ名のバリデーション
$ git check-ref-format --branch "feature/valid-name"
feature/valid-name # 有効
$ git check-ref-format --branch "feature/invalid..name"
fatal: 'feature/invalid..name' is not a valid branch name9. Refの並行アクセス制御
9.1 ロックファイルによる排他制御
Gitはref更新時にロックファイル(.lockサフィックス)を使用して並行アクセスを制御する。
# ロックの仕組み
# 1. refs/heads/main を更新する場合:
# → .git/refs/heads/main.lock を作成(排他ロック取得)
# → main.lock に新しいSHA-1を書き込み
# → main.lock → main にアトミックにリネーム
# → ロック解放
# ロック競合が発生した場合のエラー
$ git checkout feature/auth
error: Unable to create '/path/to/repo/.git/refs/heads/main.lock':
File exists.
Another git process seems to be running in this repository.
If no other git process is running, remove the file manually.
# 強制ロック解除(他のgitプロセスが本当に動いていないことを確認してから)
$ rm .git/refs/heads/main.lock
# index.lockも同様の仕組み
$ rm .git/index.lock # インデックスのロック解除9.2 CAS(Compare-And-Swap)によるref更新
# update-refでのCAS操作
$ git update-ref refs/heads/main <new-sha1> <expected-old-sha1>
# → expected-old-sha1 と現在のSHA-1が一致する場合のみ更新
# → 一致しない場合はエラー(他のプロセスが先に更新した)
# pushでのCAS(--force-with-lease)
$ git push --force-with-lease origin main
# → リモートのmainが最後にfetchした時から変わっていない場合のみforce push
# → 他の開発者のpushを上書きするリスクを低減
# 期待値を明示するforce-with-lease
$ git push --force-with-lease=main:a1b2c3d origin main
# → リモートのmainが a1b2c3d の場合のみforce push10. アンチパターン
アンチパターン1: detached HEADでの長時間作業
# NG: detached HEAD状態で何日も作業を続ける
$ git checkout v1.0.0
# (detached HEAD)
$ ... 数日間の作業 ...
$ git commit -m "important changes"
# → ブランチに属さないコミットが作られる
# → git gcで消失する可能性がある
# OK: 必ずブランチを作成してから作業する
$ git checkout v1.0.0
$ git checkout -b hotfix/v1.0.0-patch
$ ... 作業 ...
$ git commit -m "important changes"理由: detached HEADで作成されたコミットはどのrefからも到達不可能になった時点でGCの対象になる。reflogの期限(デフォルト30日)を過ぎると完全に消失する。
アンチパターン2: reflogに依存した「バックアップ」戦略
# NG: "reflogがあるからreset --hardしても大丈夫"
$ git reset --hard HEAD~5
# → reflogには残るが、30-90日で期限切れ
# → git gcでオブジェクト自体が削除される可能性
# OK: 明示的にブランチやタグで保護する
$ git tag backup/before-cleanup HEAD
$ git reset --hard HEAD~5
# → tagがある限りGCされない理由: reflogはローカル専用で、git cloneやgit pushでは転送されない。サーバー側にはreflogが存在しない場合もある。
アンチパターン3: ブランチ名とタグ名の衝突
# NG: タグと同名のブランチを作成
$ git tag release-v1.0
$ git branch release-v1.0
# → 参照が曖昧になり、コマンドによって解決結果が異なる
# → checkout時は警告が出るが、他のコマンドでは暗黙的に片方が選ばれる
# OK: 命名規則でブランチとタグの名前空間を明確に分離
$ git tag v1.0.0 # タグ: vX.Y.Z
$ git branch release/1.0.0 # ブランチ: release/X.Y.Z理由: Gitのref解決順序(タグ → ブランチの順)により、同名のrefが存在すると予期しないrefが選択される可能性がある。
アンチパターン4: packed-refsの手動編集
# NG: packed-refsファイルを直接テキストエディタで編集
$ vim .git/packed-refs
# → ソート順が崩れたり、チェックサムが不整合になる可能性
# OK: Gitコマンドを通じて操作
$ git update-ref refs/heads/main <new-sha1>
$ git pack-refs --all理由: packed-refsは内部フォーマットの整合性(ソート順、peeled行の位置)が重要であり、手動編集は破損リスクが高い。
アンチパターン5: 全ブランチの一括force push
# NG: 全ブランチを一括force push
$ git push --force --all origin
# → リモートの全ブランチを上書き
# → 他の開発者の作業が消失する可能性
# OK: 必要なブランチのみを個別にforce push
$ git push --force-with-lease origin feature/my-branch
# → 自分専用のブランチのみ、安全にforce push理由: --force --allはリモートの全ブランチを無条件に上書きする。チーム開発では他のメンバーのpushを巻き戻す危険がある。--force-with-leaseを使えば、他のpushがあった場合に拒否される。
11. 実務シナリオ集
シナリオ1: ブランチの分岐点を調べる
# feature/authがmainから分岐した地点を特定
$ git merge-base main feature/auth
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# 分岐後のコミット数を確認
$ git rev-list --count main..feature/auth
5 # feature/authにあってmainにないコミット数
$ git rev-list --count feature/auth..main
3 # mainにあってfeature/authにないコミット数
# グラフで分岐を視覚化
$ git log --oneline --graph main feature/auth
* abc1234 (feature/auth) latest feature commit
* def5678 add feature logic
* 789abcd start feature
| * fedcba0 (main) latest main commit
| * 1111111 fix bug
| * 2222222 update docs
|/
* a1b2c3d common ancestor (merge-base)シナリオ2: 複数のリモートからの同期
# フォークしたリポジトリで、上流の変更を取り込む
$ git remote add upstream https://github.com/original/repo.git
$ git fetch upstream
$ git checkout main
$ git merge upstream/main
# 複数リモートのref状況を一覧
$ git for-each-ref --format='%(refname:short) %(upstream:short) %(upstream:track)' refs/heads/
main origin/main [ahead 0, behind 0]
feature/auth origin/feature/auth [ahead 2]シナリオ3: refs/notesの活用
# コミットにメモ(note)を追加
$ git notes add -m "このコミットはパフォーマンスに影響あり" abc1234
# → refs/notes/commits にnoteオブジェクトが保存される
# noteの表示
$ git log --show-notes abc1234
commit abc1234...
fix: update query
Notes:
このコミットはパフォーマンスに影響あり
# noteのpush(明示的に指定が必要)
$ git push origin refs/notes/commitsシナリオ4: replace refによるコミットの差し替え
# コミットメッセージを修正したいが、pushbackはしたくない場合
$ git replace abc1234 def5678
# → refs/replace/abc1234 が作成される
# → abc1234へのアクセスがdef5678に透過的に差し替えられる
# replaceの確認
$ git replace -l
abc1234
# replaceの削除
$ git replace -d abc1234
# replaceされたコミットを表示
$ git log --no-replace-objects abc1234
# → 元のコミットが表示される(replace無視)実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
12. FAQ
Q1. git branch -dで削除したブランチを復元できるか?
A1. はい、reflogを使えば復元できます。
$ git reflog
# 削除前のブランチが指していたcommit SHA-1を見つける
$ git branch recovered-branch <SHA-1>ただし、reflogの有効期限内に限ります。期限切れ後はgit fsck --lost-foundで到達不可能オブジェクトから探す必要があります。
Q2. HEADとORIG_HEADの違いは何か?
A2. HEADは現在のチェックアウト位置を指すシンボリック参照です。ORIG_HEADはmerge、rebase、resetなどHEADを大きく移動させる操作の直前の位置を記録する特殊参照です。操作を取り消したい場合にgit reset --hard ORIG_HEADのように使用します。
Q3. refs/stashはどのような仕組みか?
A3. git stashはワーキングディレクトリの変更をcommitオブジェクトとして保存し、refs/stashがその最新のstashエントリを指します。過去のstashエントリはreflog(stash@{0}, stash@{1}, ...)として保持されます。内部的には通常のcommit/tree/blobオブジェクトで構成されています。
# stashの内部構造を確認
$ git cat-file -p refs/stash
tree d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0a1b2c3
parent a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
parent f5e6d7c8b9a0e1f2d3c4b5a6d7e8f9a0b1c2d3e4
author Gaku <gaku@example.com> 1707600000 +0900
committer Gaku <gaku@example.com> 1707600000 +0900
WIP on main: a1b2c3d commit message
# stashコミットは2つ(またはuntracked含め3つ)のparentを持つ
# parent 1: stash時のHEADコミット
# parent 2: インデックスの状態を記録したコミット
# parent 3: --include-untracked時のuntracked filesコミットQ4. SHA-1からSHA-256への移行はrefにどのような影響があるか?
A4. Git 2.29以降、SHA-256をオブジェクトハッシュとして使用する実験的サポートが追加されています。SHA-256リポジトリでは、refファイルに格納されるハッシュが64文字(40文字ではなく)になります。ただし、SHA-1とSHA-256のリポジトリ間の相互運用性は限定的であり、実務での移行はまだ先の話です。
# SHA-256リポジトリの作成(実験的)
$ git init --object-format=sha256 my-repo
$ cd my-repo
$ echo "test" | git hash-object --stdin
# → 64文字のSHA-256ハッシュが返るQ5. refの操作をフックで監視する方法は?
A5. reference-transactionフック(Git 2.28+)を使うと、全てのref更新をフックで監視できます。
# .git/hooks/reference-transaction の例
#!/bin/bash
# $1 = "prepared" | "committed" | "aborted"
while read oldvalue newvalue refname; do
if [ "$1" = "committed" ]; then
echo "Ref updated: $refname $oldvalue -> $newvalue" >> /tmp/git-ref-log.txt
fi
doneQ6. git worktreeとHEADの関係は?
A6. 各worktreeは独自のHEADを持ちます。メインのworktreeは.git/HEADを使い、追加worktreeは.git/worktrees/<name>/HEADを使います。重要な制約として、複数のworktreeで同じブランチをチェックアウトすることはできません。
# worktree追加
$ git worktree add ../feature-wt feature/auth
# → .git/worktrees/feature-wt/HEAD = "ref: refs/heads/feature/auth"
# 同じブランチをチェックアウトしようとするとエラー
$ git worktree add ../another-wt feature/auth
fatal: 'feature/auth' is already checked out at '/path/to/feature-wt'
# worktree一覧
$ git worktree list
/path/to/main a1b2c3d [main]
/path/to/feature-wt f5e6d7c [feature/auth]FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 概念 | 要点 |
|---|---|
| Ref | SHA-1ハッシュへのポインタ、.git/refs/配下にテキストファイル |
| ブランチ | refs/heads/<name> に保存、作成・削除はファイル操作 |
| HEAD | シンボリック参照、通常はブランチを間接参照 |
| detached HEAD | HEADがcommitを直接参照、ブランチなしで危険 |
| reflog | refの変更履歴、ローカル専用、30-90日で期限切れ |
| packed-refs | 大量refの最適化、looseが優先 |
| リモート追跡ブランチ | refs/remotes/<remote>/<branch>、fetch時に更新 |
| lightweight tag | commitを直接指すref、メタデータなし |
| annotated tag | tagオブジェクトを経由、作成者・メッセージ・署名を格納 |
| シンボリック参照 | 他のrefへの間接参照、HEADが代表例 |
| ロックファイル | .lockサフィックスで並行アクセスを排他制御 |
| ORIG_HEAD | 破壊的操作前のHEAD位置を記録、取り消しに使用 |
次に読むべきガイド
- Gitオブジェクトモデル — blob/tree/commit/tagの基礎
- マージアルゴリズム — 3-way mergeとortの内部動作
- Packfile/GC — オブジェクトの圧縮とガベージコレクション
- インタラクティブRebase — HEADの書き換え操作
参考文献
- Pro Git Book — Scott Chacon, Ben Straub "Git Internals - Git References" https://git-scm.com/book/en/v2/Git-Internals-Git-References
- Git公式ドキュメント —
git-symbolic-ref,git-reflog,git-update-ref,git-for-each-refhttps://git-scm.com/docs - GitHub Blog — "Commits are snapshots, not diffs" https://github.blog/2020-12-17-commits-are-snapshots-not-diffs/
- Git公式ドキュメント —
git-pack-refs,git-check-ref-formathttps://git-scm.com/docs - Derrick Stolee — "Scaling monorepo maintenance" https://github.blog/2021-04-29-scaling-monorepo-maintenance/
- Git Reference Transaction Hook — https://git-scm.com/docs/githooks#_reference_transaction
- reftable specification — https://www.git-scm.com/docs/reftable