Formato do índice
O arquivo de índice (.git/index) é a área de preparação. Ele registra os arquivos que serão incluídos no próximo commit, junto com metadados para calcular o status com eficiência. Pitmaster lê as versões 2, 3 e 4 do índice e grava as versões 2 e 3. Gravar um índice que foi lido como versão 4 produz um arquivo que git não consegue ler (#15), portanto converta esses repositórios com git update-index --index-version 2 antes de gravar usando Pitmaster.
Lendo o índice
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() retorna um índice vazio quando o arquivo não existe. entries() retorna apenas entradas do estágio 0; allEntries() inclui estágios de conflito, e paths() lista os caminhos rastreados.
IndexEntry
Cada entrada do índice é um objeto de valor somente para leitura:
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
Criando entradas
// 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);
Modificando o índice
// 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() e removeEntries() processam várias entradas ou caminhos em uma única passagem. Qualquer alteração nas entradas descarta as extensões armazenadas (veja abaixo).
Gravando o índice
use Pitmaster\Index\IndexWriter;
IndexWriter::write($index, '/path/to/.git/index');
Ou pelo repositório (que trata disso automaticamente):
$repo->add('src/File.php'); // Reads file, creates blob, updates index, writes
$repo->remove('--cached', 'old-file.txt'); // Removes from index only, writes
Formato binário
Cabeçalho
Signature: DIRC (4 bytes, ASCII)
Version: 4 bytes, big-endian (2, 3, or 4)
Entry count: 4 bytes, big-endian
Formato das entradas (v2)
Cada entrada é composta por:
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
O campo de flags:
- Bit 15: flag assume-valid
- Bits 13-12: estágio de merge (0-3)
- Bits 11-0: comprimento do nome do caminho ou 0xFFF quando o caminho tem 4095 bytes ou mais (o leitor então procura o NUL)
Diferenças entre versões
Versão 2: o formato básico descrito acima. O mais comum.
Versão 3: adiciona flags estendidas (2 bytes extras após as flags normais). É compatível com os bits skip-worktree e intent-to-add. Usada quando alguma entrada precisa de flags estendidas.
Versão 4: adiciona compressão de prefixos de caminho. Em vez de armazenar caminhos completos, cada entrada armazena o número de bytes a remover do caminho da entrada anterior, seguido pelo novo sufixo. Reduz significativamente o tamanho do índice em árvores profundamente aninhadas.
Estágios de conflito
Durante um conflito de merge, um único caminho pode ter várias entradas de índice em estágios diferentes:
| Estágio | Significado |
|---|---|
| 0 | Normal (sem conflito) |
| 1 | Versão base (ancestral comum) |
| 2 | Nossa versão (branch atual) |
| 3 | Versão deles (branch em processo de merge) |
// 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]
Extensões do índice
As extensões são anexadas após as entradas, antes da soma de verificação. Cada extensão tem uma assinatura de 4 bytes, um tamanho de 4 bytes e dados de comprimento variável.
Pitmaster mantém os blocos de extensão como bytes brutos ($index->extensions() retorna pares signature e data) e os grava novamente sem alterações quando as entradas não foram modificadas. Ele não interpreta nem gera nenhuma extensão, e qualquer alteração nas entradas descarta todas elas, que git aceita e recria conforme necessário. As extensões usadas por git são descritas abaixo para referência.
Extensão TREE
Armazena em cache objetos de árvore pré-computados para diretórios. Acelera git status e git commit, evitando recalcular a árvore quando apenas o índice foi alterado.
Assinatura: TREE (0x54524545)
Extensão REUC
Registra informações de desfazer resolução. Após a resolução de um conflito, as entradas originais dos estágios 1/2/3 são movidas para cá para que possam ser restauradas com git checkout -m.
Assinatura: REUC (0x52455543)
Outras extensões
git também grava:
EOIE(fim da entrada do índice): marca onde as entradas terminam e as extensões começamIEOT(tabela de deslocamentos das entradas do índice): permite o carregamento paralelo do índicelink(índice dividido): para repositórios muito grandes que usam índice dividido
No formato de git, extensões cuja assinatura começa com uma letra maiúscula são opcionais, e as que começam com uma minúscula são obrigatórias. Pitmaster aceita os dois tipos sem verificá-los. Um índice dividido (link) não é carregado, então repositórios que o usam não são compatíveis.
Soma de verificação
O arquivo de índice termina com uma soma de verificação de todos os bytes anteriores, usando o hash do repositório (20 bytes SHA-1, 32 bytes SHA-256). Pitmaster a grava ao salvar, mas não a verifica na leitura.
Index::open() e Index::parse() lançam IndexParseException se a assinatura DIRC estiver ausente ou se a versão não for compatível.
Índice e status
O índice é central para o cálculo do status. WorkingTreeStatus compara:
- Árvore HEAD com índice: detecta alterações preparadas
- Índice com árvore de trabalho: detecta alterações não preparadas
Os campos stat (ctime, mtime, dev, ino, size, mode, uid, gid) permitem verificar rapidamente se um arquivo foi alterado sem ler seu conteúdo. Se os campos stat corresponderem e o arquivo tiver sido modificado pela última vez antes do início da verificação de status, ele será considerado inalterado. Um tamanho diferente o marca como modificado imediatamente. Caso contrário, Pitmaster lê o arquivo e calcula seu hash para verificar se o conteúdo realmente mudou.
Flag assume-valid
if ($entry->assumeValid()) {
// Entry has assume-valid set (flags & 0x8000)
}
Essa flag informa git para ignorar a verificação do arquivo durante a consulta de status, mesmo que os campos stat tenham mudado (git update-index --assume-unchanged). Pitmaster mantém o bit ao regravar entradas, mas seu status não o respeita e ainda compara o arquivo.