Pack-Dateien
Pack-Dateien ermöglichen effiziente Speicherung und Übertragung in git. Statt jedes Objekt als separate zlib-komprimierte Datei abzulegen, fassen sie viele Objekte in einer Datei zusammen und können sie optional per Delta-Komprimierung speichern. Fast jedes geklonte Repository verwendet Pack-Dateien.
Aufbau einer Pack-Datei
Eine Pack-Datei (.git/objects/pack/pack-{hash}.pack) enthält:
Header: PACK (4 bytes) + version (4 bytes, big-endian) + count (4 bytes, big-endian)
Entries: [type+size varint] [delta-base] [zlib-compressed data] ...
Trailer: checksum of the pack (20 bytes SHA-1, 32 bytes SHA-256)
Kodierung des Objekttyps
Jeder Eintrag beginnt mit einer Ganzzahl variabler Länge, in der Typ und Größe ineinander verschachtelt sind:
First byte: 1TTTSSSS
TTT = object type (1-4 for base objects, 6 for OFS_DELTA, 7 for REF_DELTA)
SSSS = first 4 bits of uncompressed size
MSB = 1 means more size bytes follow
Typwerte:
| Wert | Typ |
|---|---|
| 1 | Commit |
| 2 | Tree |
| 3 | Blob |
| 4 | Tag |
| 6 | OFS_DELTA (Delta auf Offsetbasis) |
| 7 | REF_DELTA (Hash-basiertes Delta) |
Pack-Dateien lesen
use Pitmaster\Pack\PackFile;
$pack = PackFile::open(
'/path/to/.git/objects/pack/pack-abc123.pack',
'/path/to/.git/objects/pack/pack-abc123.idx',
);
$pack->has($hex); // Is the object in this pack?
$object = $pack->read($hex); // Resolved GitObject, or null
$object = $pack->readAtOffset($offset);
$hashes = $pack->allHashes();
Die Klasse PackFile:
- Validiert den
PACK-Header und die Versionsnummer - Ermittelt den Offset über die
.idx-Datei und liest den Varint für Typ und Größe des Eintrags - Dekomprimiert die zlib-komprimierten Daten
- Gibt bei Basisobjekten den Inhalt direkt zurück
- Löst bei Delta-Objekten das Basisobjekt auf (auch über Ketten hinweg), wendet das Delta an und speichert aufgelöste Objekte im Cache
Pack-Indexdateien
Zu jeder .pack-Datei gehört eine .idx-Datei für schnelle Suchvorgänge. Ohne den Index müsste zum Auffinden eines Objekts das gesamte Pack durchsucht werden.
Indexformat v2
Magic: FF 74 4F 63 (identifies v2)
Version: 00 00 00 02
Fanout: 256 x 4-byte big-endian counts (cumulative)
Names: N x 20-byte SHA-1 hashes, sorted (32 bytes for SHA-256)
CRC32s: N x 4-byte CRC32 checksums
Offsets: N x 4-byte pack offsets (MSB=1 means large offset table)
Large: M x 8-byte offsets (for packs > 2GB)
Checksums: pack checksum + index checksum (20 bytes each for SHA-1)
use Pitmaster\Pack\PackIndex;
$index = PackIndex::open('/path/to/pack-abc123.idx');
// Look up an object
$offset = $index->findOffset($hex); // Pack file offset, or null
// List all objects in the pack
$hashes = $index->allHashes();
$count = $index->objectCount();
Die Fan-out-Tabelle ermöglicht eine binäre Suche in O(log n): Die 256 Einträge geben an, wie viele Hashes mit dem jeweiligen möglichen ersten Byte beginnen.
Indexformat v1
Älteres Format ohne CRC32-Prüfsummen. Pitmaster unterstützt beide Formate: PackIndex::open() liest eine v1-Datei, wenn die v2-Magic Number fehlt, sodass derselbe Code mit beiden Formaten funktioniert.
Delta-Auflösung
Delta-Objekte speichern Anweisungen, mit denen sich der Inhalt aus einem Basisobjekt rekonstruieren lässt, statt den vollständigen Inhalt zu speichern. Das spart bei ähnlichen Objekten erheblich Speicherplatz (e.g. aufeinanderfolgende Versionen derselben Datei).
Delta-Typen
OFS_DELTA (Typ 6): Das Basisobjekt wird durch einen negativen Offset innerhalb derselben Pack-Datei identifiziert. Dieser Typ ist häufiger, da der Hash des Basisobjekts nicht bekannt sein muss.
REF_DELTA (Typ 7): Das Basisobjekt wird durch seinen Hash identifiziert. In git kann sich das Basisobjekt außerhalb des Packs befinden (Thin Packs). Pitmaster sucht REF_DELTA-Basisobjekte nur im selben Pack, daher werden Thin Packs nicht unterstützt (#16).
Delta-Anweisungen
Delta-Inhalte bestehen aus einer Folge von Kopier- und Einfügeanweisungen:
Copy: 1XXXXXXX [offset bytes] [size bytes] (copy from base)
Insert: 0LLLLLLL <L bytes of literal data> (insert new data)
Kopieranweisungen: Die 7 X-Bits geben an, welche Offset- und Größenbytes vorhanden sind. Der Offset ist bis zu 4 Bytes lang, die Größe bis zu 3 Bytes. Ist die Größe null, steht sie für 0x10000 (65536) Bytes.
Einfügeanweisungen: Die 7 L-Bits geben die Länge der literalen Daten an (1-127 Bytes). L=0 ist reserviert und ungültig.
DeltaApplier
use Pitmaster\Pack\DeltaApplier;
$result = DeltaApplier::apply($baseContent, $deltaInstructions);
Der DeltaApplier:
- Liest die erwartete Basisgröße und Ergebnisgröße aus dem Delta-Header (LEB128-kodiert)
- Durchläuft die Anweisungen:
- Kopieren: Ermittelt Offset und Größe und kopiert Bytes aus dem Basisinhalt
- Einfügen: Ermittelt die Länge und kopiert literale Bytes aus den Delta-Daten
- Prüft, ob die Ergebnisgröße der erwarteten Größe entspricht
Delta-Ketten
Das Basisobjekt eines Delta-Objekts kann selbst ein Delta-Objekt sein. PackFile::read() und readAtOffset() verfolgen die Kette:
- Liest den Eintrag. Ist er ein Basistyp (Commit, Tree, Blob, Tag), wird er zurückgegeben.
- Bei OFS_DELTA: Berechnet den Basis-Offset (aktueller Offset minus kodierter negativer Offset), liest den Basiseintrag und ruft sich rekursiv auf.
- Bei REF_DELTA: Sucht das Basisobjekt anhand des Hashes im Index desselben Packs und ruft sich rekursiv auf.
- Wendet die Delta-Anweisungen auf das aufgelöste Basisobjekt an.
- Erzwingt
PITMASTER_MAX_DELTA_CHAIN(standardmäßig 50), um endlose Ketten zu verhindern.
// Configure max chain depth (integer)
define('PITMASTER_MAX_DELTA_CHAIN', 50);
// Read the effective limit
$max = \Pitmaster\Pack\DeltaResolver::maxChainDepth();
PackWriter
Pitmaster kann Pack-Dateien für Netzwerkoperationen (Push) schreiben.
use Pitmaster\Pack\PackWriter;
use Pitmaster\Pack\PackIndexer;
$packData = PackWriter::encode($objects); // GitObject[] to pack bytes
$paths = PackWriter::write($packDir, $objects); // ['packPath' => ..., 'idxPath' => ..., 'hash' => ...]
$idxPath = PackIndexer::writeIndex($packPath); // Build the .idx for a received pack
Der PackWriter:
- Sammelt die ins Pack aufzunehmenden Objekte
- Serialisiert jedes Objekt mit einem Varint-Header für Typ und Größe
- Komprimiert den Inhalt mit zlib
- Schreibt den PACK-Header, die Einträge und den SHA-1-Trailer
Derzeit werden nur Basisobjekte geschrieben (ohne Delta-Komprimierung). Delta-Komprimierung ist eine Optimierung, die später ergänzt werden kann; für effiziente Speicherung kann git gc ein neues Pack erstellen.
PackEnumerator
Alle Objekte eines Packs in Indexreihenfolge durchlaufen:
use Pitmaster\Pack\PackEnumerator;
$enumerator = new PackEnumerator($packFile); // a PackFile opened with its .idx
foreach ($enumerator->enumerate() as $object) {
echo "{$object->type->value} {$object->id->hex} {$object->size()}\n";
}
Jedes Element ist ein aufgelöstes GitObject.
MultiPackIndex
Bei Repositorys mit vielen Pack-Dateien ermöglicht der Multi-Pack-Index (MIDX eine einzige Suche über alle Packs hinweg.
use Pitmaster\Pack\MultiPackIndex;
Die MIDX-Datei (.git/objects/pack/multi-pack-index) enthält:
- Eine Fan-out-Tabelle (wie bei Pack-Index v2)
- Objekt-IDs, sortiert über alle Packs hinweg
- Pack-Dateinamen und Pack-Zuordnungen pro Objekt
- Offsets in den einzelnen Pack-Dateien
MultiPackIndex::open($path) liest eine MIDX-Datei ein und stellt findObject($hex) und packNames() bereit. Objekte im Repository werden nicht darüber gesucht: Die Objektdatenbank liest immer die einzelnen .idx-Dateien.
CommitGraph
Die Commit-Graph-Datei beschleunigt das Durchlaufen der Commit-Historie und die Berechnung der Merge-Basis.
use Pitmaster\Pack\CommitGraph;
Sie befindet sich unter .git/objects/info/commit-graph und enthält:
- Commit-OIDs in sortierter Reihenfolge
- Informationen zu Eltern-Commits (indiziert, ohne Objekte lesen zu müssen)
- Generationsnummern (für schnellere Erreichbarkeitsabfragen)
- Commit-Zeitstempel
Wenn eine Commit-Graph-Datei vorhanden ist, liest die Berechnung der Merge-Basis hinter Repository::merge(), mergeBase(), isBranchMerged() und der Push-Fast-Forward-Prüfung die Elternverknüpfungen aus dem Commit-Graph statt einzelne Commit-Objekte zu lesen. CommitWalker (Log) verwendet sie nicht.
Ganzzahlen variabler Länge
Pack-Dateien verwenden mehrere Kodierungen für Ganzzahlen variabler Länge:
Git-Varint (Typ-/Größen-Header)
MSB-Fortsetzungskodierung, bei der das höchstwertige Bit anzeigt, dass weitere Bytes folgen:
use Pitmaster\Encoding\VarInt;
$size = VarInt::decodePackSize($reader, $initialBits); // continues the size from the first header byte
$offset = VarInt::decodeOfsOffset($reader); // OFS_DELTA base distance
$header = VarInt::encodePackSize($type, $size);
LEB128 (Delta-Header)
Wird in Delta-Anweisungs-Headern für Basis- und Ergebnisgröße verwendet:
use Pitmaster\Encoding\Leb128;
$value = Leb128::decodeUnsigned($reader); // a BinaryReader
$bytes = Leb128::encodeUnsigned($value);
BinaryReader
Byte-Stream auf niedriger Ebene mit Positionsverfolgung:
use Pitmaster\Encoding\BinaryReader;
$reader = new BinaryReader($data);
$byte = $reader->readByte();
$int = $reader->readUint32(); // big-endian
$bytes = $reader->read(20);
$hash = $reader->readHash(20);
$reader->position(); // Current offset
$reader->seek(0);