コンテンツへスキップ

インデックス形式

インデックスファイル(.git/index)はステージング領域です。次のcommitに含めるファイルと、状態を効率的に計算するためのメタデータを記録します。Pitmasterはバージョン2、3、4を読み込み、バージョン2と3を書き込みます。バージョン4として読み込んだインデックスを書き込むと、gitで読めないファイルが生成されます(#15)。そのため、Pitmaster経由で書き込む前に、git update-index --index-version 2でそのリポジトリを変換してください。

インデックスの読み込み

PHP
use Pitmaster\Index\Index;

$index = Index::open('/path/to/.git/index');   // pass 32 as the second argument for SHA-256

// Or through the repository
$index = $repo->index();

// Entry count (all stages)
echo $index->count();

// Iterate stage 0 entries, keyed by path
foreach ($index->entries() as $path => $entry) {
    echo "{$entry->path} {$entry->hash->hex}\n";
}

// Look up a specific entry
$entry = $index->entry('src/File.php');

ファイルが存在しない場合、Index::open()は空のインデックスを返します。entries()はステージ0のエントリのみを返し、allEntries()は競合ステージを含めます。paths()は追跡対象のパスを一覧表示します。

IndexEntry

インデックス内の各エントリは読み取り専用の値オブジェクトです:

PHP
use Pitmaster\Index\IndexEntry;

$entry->path;          // File path relative to repo root
$entry->hash;          // ObjectId of the blob
$entry->mode;          // File mode as an int (0100644, 0100755, 0120000, 0160000)
$entry->fileSize;      // File size in bytes
$entry->ctimeSec;      // Metadata change time (seconds)
$entry->ctimeNsec;     // Metadata change time nanoseconds
$entry->mtimeSec;      // Modification time (seconds)
$entry->mtimeNsec;     // Modification time nanoseconds
$entry->dev;           // Device number
$entry->ino;           // Inode number
$entry->uid;           // User ID
$entry->gid;           // Group ID
$entry->flags;         // Raw flags field
$entry->extendedFlags; // v3 extended flags (skip-worktree, intent-to-add)
$entry->stage();       // Conflict stage (0 = normal, 1 = base, 2 = ours, 3 = theirs)
$entry->assumeValid(); // Assume-valid bit
$entry->skipWorktree(); // Skip-worktree bit

エントリの作成

PHP
// From file stat
$entry = IndexEntry::fromStat('src/File.php', $blobId, '/full/path/to/src/File.php');

// Minimal (no stat info)
$entry = IndexEntry::create('src/File.php', $blobId);

// With explicit mode
$entry = IndexEntry::create('deploy.sh', $blobId, 0100755);

インデックスの変更

PHP
// Add or replace an entry
$index->addEntry($entry);

// Remove an entry
$index->removeEntry('src/OldFile.php');

// Replace all stages of a conflicted path with one resolved entry
$index->resolveConflict('src/File.php', $entry);

addEntries()とremoveEntries()は、複数のエントリまたはパスを一度に処理します。エントリを変更すると、保存済みの拡張機能は破棄されます(下記参照)。

インデックスの書き込み

PHP
use Pitmaster\Index\IndexWriter;

IndexWriter::write($index, '/path/to/.git/index');

または、リポジトリ経由で書き込みます(自動的に処理されます):

PHP
$repo->add('src/File.php');    // Reads file, creates blob, updates index, writes
$repo->remove('--cached', 'old-file.txt'); // Removes from index only, writes

バイナリ形式

コード
Signature:   DIRC (4 bytes, ASCII)
Version:     4 bytes, big-endian (2, 3, or 4)
Entry count: 4 bytes, big-endian

エントリ形式(v2)

各エントリの構成:

コード
ctime seconds:     4 bytes, big-endian
ctime nanoseconds: 4 bytes, big-endian
mtime seconds:     4 bytes, big-endian
mtime nanoseconds: 4 bytes, big-endian
dev:               4 bytes, big-endian
ino:               4 bytes, big-endian
mode:              4 bytes, big-endian
uid:               4 bytes, big-endian
gid:               4 bytes, big-endian
file size:         4 bytes, big-endian
object hash:       20 bytes SHA-1 or 32 bytes SHA-256 (raw binary)
flags:             2 bytes, big-endian
path:              variable length, NUL-terminated
padding:           1-8 NUL bytes to align to 8-byte boundary

flagsフィールド:

  • ビット15:assume-validフラグ
  • ビット13-12:マージステージ(0-3)
  • ビット11-0:パス名の長さ。パスが4095バイト以上の場合は0xFFF(その場合、読み込み側はNULまで検索)

バージョンごとの差異

バージョン2:前述の基本形式です。最も一般的です。

バージョン3:拡張flags(通常のflagsの後に2バイト)を追加します。skip-worktreeとintent-to-addのビットをサポートします。拡張flagsが必要なエントリがある場合に使われます。

バージョン4:パスのプレフィックス圧縮を追加します。完全なパスを保存する代わりに、各エントリは前のエントリのパスから削除するバイト数と、新しい接尾辞を保存します。深くネストしたツリーでは、インデックスのサイズを大幅に削減できます。

競合ステージ

マージ競合中は、1つのパスに異なるステージのインデックスエントリが複数存在する場合があります:

ステージ 意味
0 通常(競合なし)
1 ベースのバージョン(共通の祖先)
2 ours(現在のブランチ)
3 theirs(マージ対象のブランチ)
PHP
// During a conflict, entries have non-zero stages
foreach ($index->allEntries() as $entry) {
    if ($entry->stage() > 0) {
        echo "Conflict: {$entry->path} (stage {$entry->stage()})\n";
    }
}

$index->hasUnmerged();            // true while conflicts remain
$index->unmergedPaths();          // conflicted paths
$index->stageEntries('a.txt');    // [1 => base, 2 => ours, 3 => theirs]

インデックス拡張機能

拡張機能はエントリの後、チェックサムの前に追加されます。各拡張機能は4バイトのシグネチャ、4バイトのサイズ、可変長のデータで構成されます。

Pitmasterは拡張ブロックを生バイト列として保持し($index->extensions()はsignatureとdataのペアを返します)、エントリが変更されていなければそのまま書き戻します。拡張機能の解釈や生成は行いません。また、エントリを変更するとすべての拡張機能が破棄されますが、これはgitが受け入れ、必要に応じて再構築します。以下では、参考としてgitが使う拡張機能を説明します。

TREE拡張機能

ディレクトリの計算済みtreeオブジェクトをキャッシュします。インデックスのみが変更された場合にtreeの再計算を避け、git statusとgit commitを高速化します。

シグネチャ:TREE (0x54524545)

REUC拡張機能

競合解決の取り消し情報を記録します。競合を解決すると、元のステージ1/2/3のエントリがここに移され、git checkout -mで復元できるようになります。

シグネチャ:REUC (0x52455543)

その他の拡張機能

gitは次の拡張機能も書き込みます:

  • EOIE(インデックスエントリの終端):エントリの終わりと拡張機能の開始位置を示します
  • IEOT(インデックスエントリのオフセットテーブル):並列でのインデックス読み込みを可能にします
  • link(分割インデックス):分割インデックスを使う非常に大規模なリポジトリ向けです

gitの形式では、シグネチャが大文字で始まる拡張機能は任意、小文字で始まるものは必須です。Pitmasterはいずれも確認せずに受け入れます。分割インデックス(link)は読み込まれないため、これを使うリポジトリはサポートされません。

チェックサム

インデックスファイルの末尾には、リポジトリのハッシュ形式による、それ以前の全バイトのチェックサムが格納されます(SHA-1は20バイト、SHA-256は32バイト)。Pitmasterは保存時にチェックサムを書き込みますが、読み込み時には検証しません。

Index::open()とIndex::parse()は、DIRCのシグネチャが見つからない場合、または未対応のバージョンの場合にIndexParseExceptionをスローします。

インデックスと状態

インデックスは状態の計算に不可欠です。WorkingTreeStatusは次を比較します:

  1. HEADのtreeとインデックス:ステージ済みの変更を検出
  2. インデックスと作業ツリー:未ステージの変更を検出

statフィールド(ctime、mtime、dev、ino、size、mode、uid、gid)は、ファイルの内容を読み込まずに「ファイルが変更されたか」をすばやく確認するために使われます。statフィールドが一致し、状態のスキャン開始前にファイルが最後に変更されていれば、変更なしとみなされます。サイズが異なる場合は、ただちに変更ありと判定されます。それ以外の場合、Pitmasterはファイルを読み込んでハッシュを計算し、内容が実際に変更されたか確認します。

assume-validフラグ

PHP
if ($entry->assumeValid()) {
    // Entry has assume-valid set (flags & 0x8000)
}

このフラグが設定されていると、statフィールドが変わっていても(git update-index --assume-unchanged)、gitは状態確認時にそのファイルをチェックしません。Pitmasterはエントリを書き換える際にこのビットを保持しますが、状態確認ではこれを無視してファイルの比較を続けます。