Aller au contenu

Format de l’index

Le fichier d’index (.git/index) constitue la zone d’indexation. Il enregistre les fichiers qui seront inclus dans le prochain commit, ainsi que les métadonnées nécessaires au calcul efficace de l’état. Pitmaster lit les versions 2, 3 et 4 de l’index, et écrit les versions 2 et 3. Écrire un index lu en version 4 produit un fichier illisible par Git (#15) ; convertissez donc ces dépôts avec git update-index --index-version 2 avant d’écrire via Pitmaster.

Lecture de l’index

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() renvoie un index vide si le fichier n’existe pas. entries() renvoie uniquement les entrées de l’étape 0 ; allEntries() inclut les étapes de conflit, et paths() répertorie les chemins suivis.

IndexEntry

Chaque entrée de l’index est un objet valeur en lecture seule :

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

Création d’entrées

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);

Modification de l’index

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() et removeEntries() prennent plusieurs entrées ou chemins en une seule passe. Toute modification des entrées supprime les extensions stockées (voir ci-dessous).

Écriture de l’index

PHP
use Pitmaster\Index\IndexWriter;

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

Ou via le dépôt (qui gère cela automatiquement) :

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

Format binaire

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

Format d’entrée (v2)

Chaque entrée se compose de :

Code
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

Le champ des indicateurs :

  • Bit 15 : indicateur assume-valid
  • Bits 13-12 : étape de fusion (0-3)
  • Bits 11-0 : longueur du nom de chemin, ou 0xFFF lorsque le chemin fait 4095 octets ou plus (le lecteur recherche alors le NUL)

Différences entre versions

Version 2 : format de base décrit ci-dessus. Le plus courant.

Version 3 : ajoute des indicateurs étendus (2 octets supplémentaires après les indicateurs standard). Prend en charge les bits skip-worktree et intent-to-add. Utilisée lorsqu’une entrée requiert des indicateurs étendus.

Version 4 : ajoute la compression des préfixes de chemin. Au lieu de stocker des chemins complets, chaque entrée stocke le nombre d’octets à supprimer du chemin de l’entrée précédente, puis le nouveau suffixe. Cela réduit considérablement la taille de l’index pour les arbres profondément imbriqués.

Étapes de conflit

Lors d’un conflit de fusion, un même chemin peut avoir plusieurs entrées d’index à différentes étapes :

Étape Signification
0 Normal (aucun conflit)
1 Version de base (ancêtre commun)
2 La nôtre (branche actuelle)
3 La leur (branche en cours de fusion)
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]

Extensions de l’index

Les extensions sont ajoutées après les entrées, avant la somme de contrôle. Chaque extension contient une signature de 4 octets, une taille de 4 octets et des données de longueur variable.

Pitmaster conserve les blocs d’extension sous forme d’octets bruts ($index->extensions() renvoie des paires signature et data) et les réécrit à l’identique si les entrées n’ont pas été modifiées. Il n’interprète ni ne génère aucune extension, et toute modification des entrées les supprime toutes, ce que git accepte et reconstruit si nécessaire. Les extensions utilisées par git sont décrites ci-dessous à titre de référence.

Extension TREE

Met en cache les objets arbre précalculés pour les répertoires. Accélère git status et git commit en évitant de recalculer les arbres lorsque seul l’index a changé.

Signature : TREE (0x54524545)

Extension REUC

Enregistre les informations d’annulation de résolution. Une fois un conflit résolu, les entrées d’origine des étapes 1/2/3 sont déplacées ici afin de pouvoir être restaurées avec git checkout -m.

Signature : REUC (0x52455543)

Autres extensions

git écrit également :

  • EOIE (fin d’une entrée d’index) : indique où les entrées se terminent et où les extensions commencent
  • IEOT (table des décalages des entrées d’index) : permet le chargement parallèle de l’index
  • link (index fractionné) : pour les très grands dépôts utilisant un index fractionné

Dans le format de git, les extensions dont la signature commence par une majuscule sont facultatives, et celles dont la signature commence par une minuscule sont obligatoires. Pitmaster accepte les deux types sans vérification. L’index fractionné (link) n’est pas chargé ; les dépôts qui l’utilisent ne sont donc pas pris en charge.

Somme de contrôle

Le fichier d’index se termine par une somme de contrôle de tous les octets précédents, calculée avec le hachage du dépôt (20 octets SHA-1, 32 octets SHA-256). Pitmaster l’écrit lors de l’enregistrement, mais ne la vérifie pas à la lecture.

Index::open() et Index::parse() lèvent IndexParseException si la signature DIRC est absente ou si la version n’est pas prise en charge.

Index et état

L’index est au cœur du calcul de l’état. WorkingTreeStatus compare :

  1. L’arbre HEAD et l’index : détecte les modifications indexées
  2. L’index et l’arbre de travail : détecte les modifications non indexées

Les champs stat (ctime, mtime, dev, ino, size, mode, uid, gid) permettent de vérifier rapidement si un fichier a changé, sans lire son contenu. Si ces champs correspondent et que le fichier a été modifié avant le début de l’analyse de l’état, il est considéré comme inchangé. Une taille différente le marque immédiatement comme modifié. Sinon, Pitmaster lit le fichier et calcule son hachage pour vérifier si son contenu a réellement changé.

Indicateur assume-valid

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

Ce drapeau indique à git d’ignorer la vérification du fichier lors de l’état, même si les champs stat ont changé (git update-index --assume-unchanged). Pitmaster conserve ce bit lors de la réécriture des entrées, mais son état ne le respecte pas et compare quand même le fichier.