Formato del índice
El archivo de índice (.git/index) es el área de preparación. Registra los archivos que se incluirán en el siguiente commit, junto con metadatos que permiten calcular el estado de forma eficiente. Pitmaster lee las versiones 2, 3 y 4 del índice, y escribe las versiones 2 y 3. Escribir un índice leído como versión 4 genera un archivo que git no puede leer (#15); por tanto, convierte esos repositorios con git update-index --index-version 2 antes de escribir mediante Pitmaster.
Lectura del í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() devuelve un índice vacío si el archivo no existe. entries() devuelve solo las entradas de la etapa 0; allEntries() incluye las etapas de conflicto y paths() enumera las rutas con seguimiento.
IndexEntry
Cada entrada del índice es un objeto de valor de solo lectura:
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
Creación de 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);
Modificación del í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() y removeEntries() aceptan varias entradas o rutas en una sola pasada. Cualquier cambio en las entradas elimina las extensiones almacenadas (consulta más abajo).
Escritura del índice
use Pitmaster\Index\IndexWriter;
IndexWriter::write($index, '/path/to/.git/index');
O a través del repositorio (que lo gestiona automáticamente):
$repo->add('src/File.php'); // Reads file, creates blob, updates index, writes
$repo->remove('--cached', 'old-file.txt'); // Removes from index only, writes
Formato binario
Encabezado
Signature: DIRC (4 bytes, ASCII)
Version: 4 bytes, big-endian (2, 3, or 4)
Entry count: 4 bytes, big-endian
Formato de entrada (v2)
Cada entrada tiene esta estructura:
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
El campo de indicadores:
- Bit 15: indicador de válido por defecto
- Bits 13-12: etapa de fusión (0-3)
- Bits 11-0: longitud del nombre de ruta, o 0xFFF cuando la ruta tiene 4095 bytes o más (el lector busca entonces el NUL)
Diferencias entre versiones
Versión 2: el formato base descrito arriba. Es la más común.
Versión 3: añade indicadores extendidos (2 bytes adicionales tras los indicadores normales). Admite los bits skip-worktree y intent-to-add. Se usa cuando alguna entrada necesita indicadores extendidos.
Versión 4: añade compresión de prefijos de ruta. En vez de almacenar rutas completas, cada entrada guarda el número de bytes que se deben quitar de la ruta de la entrada anterior, seguido del nuevo sufijo. Reduce considerablemente el tamaño del índice en árboles muy anidados.
Etapas de conflicto
Durante un conflicto de fusión, una ruta puede tener varias entradas de índice en distintas etapas:
| Etapa | Significado |
|---|---|
| 0 | Normal (sin conflicto) |
| 1 | Versión base (ancestro común) |
| 2 | La nuestra (rama actual) |
| 3 | La suya (rama que se está fusionando) |
// 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]
Extensiones del índice
Las extensiones se añaden después de las entradas y antes de la suma de comprobación. Cada extensión tiene una firma de 4 bytes, un tamaño de 4 bytes y datos de longitud variable.
Pitmaster conserva los bloques de extensión como bytes sin procesar ($index->extensions() devuelve pares de signature y data) y los vuelve a escribir sin cambios si las entradas no se han modificado. No interpreta ni genera extensiones; cualquier cambio en las entradas las elimina todas, algo que git acepta y reconstruye cuando es necesario. A continuación se describen como referencia las extensiones que usa git.
Extensión TREE
Almacena en caché objetos de árbol precalculados para directorios. Acelera git status y git commit al evitar recalcular los árboles cuando solo ha cambiado el índice.
Firma: TREE (0x54524545)
Extensión REUC
Registra información para deshacer la resolución de conflictos. Una vez resuelto un conflicto, las entradas originales de las etapas 1/2/3 se trasladan aquí para poder restaurarlas con git checkout -m.
Firma: REUC (0x52455543)
Otras extensiones
git también escribe:
EOIE(final de la entrada del índice): indica dónde terminan las entradas y empiezan las extensionesIEOT(tabla de desplazamientos de entradas del índice): permite cargar el índice en paralelolink(índice dividido): para repositorios muy grandes que usan un índice dividido
En el formato de git, las extensiones cuya firma empieza por una letra mayúscula son opcionales; las que empiezan por una minúscula son obligatorias. Pitmaster acepta ambos tipos sin comprobarlos. No se carga un índice dividido (link), por lo que no se admiten los repositorios que lo usan.
Suma de comprobación
El archivo de índice termina con una suma de comprobación de todos los bytes anteriores, calculada con el algoritmo hash del repositorio (20 bytes SHA-1, 32 bytes SHA-256). Pitmaster la escribe al guardar, pero no la verifica al leer.
Index::open() y Index::parse() lanzan IndexParseException si falta la firma DIRC o si la versión no es compatible.
Índice y estado
El índice es fundamental para calcular el estado. WorkingTreeStatus compara:
- Árbol de HEAD e índice: detecta cambios preparados
- Índice y árbol de trabajo: detecta cambios sin preparar
Los campos stat (ctime, mtime, dev, ino, size, mode, uid, gid) permiten comprobar rápidamente si un archivo ha cambiado sin leer su contenido. Si los campos stat coinciden y el archivo se modificó por última vez antes de que empezara el análisis del estado, se considera que no ha cambiado. Un tamaño distinto indica de inmediato que se ha modificado. De lo contrario, Pitmaster lee el archivo y calcula su hash para comprobar si el contenido ha cambiado realmente.
Indicador assume-valid
if ($entry->assumeValid()) {
// Entry has assume-valid set (flags & 0x8000)
}
Este indicador le dice a git que no compruebe el archivo durante el análisis del estado, aunque hayan cambiado los campos stat (git update-index --assume-unchanged). Pitmaster conserva el bit al reescribir las entradas, pero su cálculo del estado no lo respeta y sigue comparando el archivo.