Git Internals and History Editing

最終更新: 2024-03-07 13:17:55 +0800 +0800
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

Git は不変オブジェクトを保存し、その間で小さな参照を移動します。日常作業では porcelain コマンドを使いますが、 ストレージモデルを知ると、書き換えで新しい履歴が作られる理由と、一定期間リカバリーできる理由を理解できます。

リポジトリデータベースの検査 #

.git ディレクトリにはリポジトリローカルの状態が含まれます。

パス用途
HEADチェックアウトしたブランチのシンボリック名、または detached object ID
configリポジトリ設定
index次のコミット向けの候補スナップショット
objects/ルーズオブジェクトとパックファイル
refs/ と packed-refsブランチ、タグ、リモート追跡名
logs/参照のローカル reflog
hooks/クライアント側フックのサンプルとインストール済みフック
modules/初期化済みサブモジュールの Git ディレクトリ

ワークツリーはチェックアウト済みファイルを保持します。index は候補ツリーを記録します。 コミットは親 ID とメタデータとともにツリーを指します。ブランチはコミットへの移動可能な参照であり、変更可能なコミット容器ではありません。

Inspect names and object types without decoding files manually:

sh
git rev-parse --git-dir
git show-ref --head
git cat-file -t HEAD
git cat-file -p HEAD
git count-objects -vH

オブジェクトモデルとコンテンツアドレス #

Git は正規バイト列をハッシュしてオブジェクトを識別します。

text
<type><space><decimal-size><NUL><content>

SHA-1 リポジトリは 160 ビットのオブジェクト ID を使います。新しいリポジトリでは SHA-256 を使えます。 オブジェクト ID はコンテンツと構造を識別し、時刻やシーケンス番号をエンコードしません。

Git には 4 つの主要なオブジェクト型があります。

  1. blob はファイル名なしでファイルのバイト列を保存します。
  2. tree はパス名とモードを blob およびサブツリーへ対応付けます。
  3. commit はルートツリー、0 個以上の親、作者とコミッターの ID・時刻、メッセージを指定します。
  4. annotated tag はタグメタデータを保存し、別のオブジェクトを指します。

tree エントリーモード 100644 は通常の非実行ファイル、100755 は実行ファイル、120000 はシンボリックリンク、 160000 はサブモジュール gitlink です。gitlink はリポジトリのファイルを埋め込むのではなく、別リポジトリのコミットを指します。

Create and inspect objects in a disposable repository:

sh
printf 'hello\n' | git hash-object -w --stdin
git cat-file -p <object-id>
git ls-tree HEAD
git ls-tree -r HEAD

Loose objects live under .git/objects/aa/bb..., where aa is the first byte of the hexadecimal ID. Git zlib-compresses their canonical representation. git cat-file is safer than manually adding compression headers or using ad-hoc decompression pipelines.

パック、デルタ、到達可能性の理解 #

多数のルーズオブジェクトはファイルシステムのメタデータ操作コストを増やします。 git gc は到達可能なオブジェクトを .git/objects/pack/ のパックファイルへ集約できます。 .pack はオブジェクト表現を保持し、.idx はオブジェクト ID をパックオフセットへ対応付け、 .rev はパック順序を必要とする操作を高速化できます。

パックファイルは他のオブジェクトとの差分でオブジェクトを圧縮できます。デルタはストレージ最適化であり Git の履歴モデルではありません。 各コミットは完全なルートツリーを指定し、Git はオブジェクトグラフを通じて参照された blob を再構築できます。 コミットが保存するのは親との差分テキストだけではありません。

Reachability starts at references such as branches, tags, remote-tracking branches, and reflogs. Garbage collection retains reachable objects and can prune unreachable objects after configured expiration. git fsck reports connectivity and can locate unreachable objects:

sh
git fsck --full --no-reflogs --unreachable
git cat-file -p <unreachable-object-id>

参照と index の読み取り #

HEAD には通常 ref: refs/heads/main が含まれます。detached checkout にはオブジェクト ID が直接含まれます。 ルーズなブランチ名は refs/heads/ 下のファイルであり、多数の参照は packed-refs に集約できます。

index は変更ファイルの一覧ではありません。次のツリー向けにパス、モード、オブジェクト ID、stat キャッシュを保存します。 マージ競合中、index は 1 つのパスに複数のステージを持てます。

ステージ意味
1マージベース
2現在のブランチのバージョン
3もう一方のブランチのバージョン

Inspect index entries with:

sh
git ls-files --stage
git diff --cached

Resolving a conflict and running git add replaces stages 1–3 with one stage-0 entry.

書き換えで ID が変わる理由 #

コミットには親オブジェクト ID が含まれます。メッセージ、ツリー、作者、コミッター、タイムスタンプ、親を変更すると別のコミット ID になります。 記録された親が変わるため、すべての子孫を再作成する必要があります。

履歴が改善されるなら、ローカルで未公開の作業は自由に書き換えます。共有ブランチを書き換える前には調整します。 強制更新は共同作業者の新しいリモートコミットを破棄する可能性があるため、force push が必要なら --force-with-lease を使います。

sh
git push --force-with-lease origin feature/topic

最新コミットの修正 #

git commit --amend は HEAD の置換コミットを作り、現在のブランチを移動します。

sh
git add README.md
git commit --amend --no-edit

メッセージを置き換えるには --no-edit なしで git commit --amend を実行します。 元のコミットは reflog の期限切れと prune まで回復可能です。

対話的な古いコミット編集 #

Interactive rebase rewrites the commits after the selected endpoint:

sh
git rebase -i HEAD~3

このコマンドは直近 3 件のコミットを提示します。todo コマンドには次が含まれます。

コマンド効果
pickコミットを保持する
rewordメッセージを置き換える
edit適用後に停止する
squash直前のコミットと結合し、メッセージを編集する
fixup結合してメッセージを破棄する
drop省略する

To change an older commit’s contents, mark it edit, amend when the rebase stops, and continue:

sh
git add path/to/file
git commit --amend --no-edit
git rebase --continue

Resolve conflicts, stage the resolved paths, and continue. git rebase --abort returns the branch to its pre-rebase state. git rebase --edit-todo changes remaining instructions while a rebase is paused.

reflog による回復 #

すべてのローカル参照の移動には reflog エントリーを作成できます。危険な書き換えの前に明示的なバックアップ名を作ります。

sh
git branch backup/before-rewrite

Then locate and name an earlier state:

sh
git reflog show --date=iso HEAD
git switch -c recovered-work HEAD@{1}

reflog は分散バックアップではなく、ローカル実装履歴です。重要な作業は公開し、リモートバックアップを保持し、 回復が完了する前に破壊的なクリーンアップを実行しないでください。

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)