Format indeksu
Plik indeksu (.git/index) jest obszarem indeksu. Zapisuje pliki, które zostaną uwzględnione w następnym commicie, wraz z metadanymi ułatwiającymi szybkie obliczanie statusu. Pitmaster odczytuje wersje indeksu 2, 3 i 4, a zapisuje wersje 2 i 3. Zapisanie indeksu odczytanego jako wersja 4 tworzy plik, którego git nie potrafi odczytać (#15), dlatego przed zapisem za pomocą Pitmaster przekonwertuj takie repozytoria przy użyciu git update-index --index-version 2.
Odczytywanie indeksu
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() zwraca pusty indeks, jeśli plik nie istnieje. entries() zwraca tylko wpisy etapu 0; allEntries() uwzględnia etapy konfliktu, a paths() wyświetla śledzone ścieżki.
IndexEntry
Każdy wpis indeksu jest niezmiennym obiektem wartości:
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
Tworzenie wpisów
// 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);
Modyfikowanie indeksu
// 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() i removeEntries() przetwarzają wiele wpisów lub ścieżek w jednym przebiegu. Każda zmiana wpisów usuwa zapisane rozszerzenia (zob. poniżej).
Zapisywanie indeksu
use Pitmaster\Index\IndexWriter;
IndexWriter::write($index, '/path/to/.git/index');
Lub za pośrednictwem repozytorium (które obsługuje to automatycznie):
$repo->add('src/File.php'); // Reads file, creates blob, updates index, writes
$repo->remove('--cached', 'old-file.txt'); // Removes from index only, writes
Format binarny
Nagłówek
Signature: DIRC (4 bytes, ASCII)
Version: 4 bytes, big-endian (2, 3, or 4)
Entry count: 4 bytes, big-endian
Format wpisu (v2)
Każdy wpis ma postać:
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
Pole flag:
- Bit 15: flaga assume-valid
- Bity 13-12: etap scalania (0-3)
- Bity 11-0: długość nazwy ścieżki lub 0xFFF, gdy ścieżka ma co najmniej 4095 bajtów (czytnik szuka wtedy znaku NUL)
Różnice między wersjami
Wersja 2: podstawowy format opisany powyżej. Najczęściej używany.
Wersja 3: dodaje rozszerzone flagi (2 dodatkowe bajty po zwykłych flagach). Obsługuje bity skip-worktree i intent-to-add. Używana, gdy dowolny wpis wymaga rozszerzonych flag.
Wersja 4: dodaje kompresję prefiksów ścieżek. Zamiast zapisywać pełne ścieżki, każdy wpis zawiera liczbę bajtów do usunięcia ze ścieżki poprzedniego wpisu, a następnie nowy sufiks. Znacznie zmniejsza rozmiar indeksu w przypadku głęboko zagnieżdżonych drzew.
Etapy konfliktu
Podczas konfliktu scalania jedna ścieżka może mieć wiele wpisów indeksu na różnych etapach:
| Etap | Znaczenie |
|---|---|
| 0 | Normalny (brak konfliktu) |
| 1 | Wersja bazowa (wspólny przodek) |
| 2 | Nasza wersja (bieżąca gałąź) |
| 3 | Ich wersja (scalana gałąź) |
// 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]
Rozszerzenia indeksu
Rozszerzenia są dopisywane po wpisach, przed sumą kontrolną. Każde rozszerzenie ma 4-bajtowy podpis, 4-bajtowy rozmiar i dane o zmiennej długości.
Pitmaster zachowuje bloki rozszerzeń jako surowe bajty ($index->extensions() zwraca pary signature i data) i zapisuje je bez zmian, jeśli wpisy nie zostały zmodyfikowane. Nie interpretuje ani nie generuje żadnych rozszerzeń, a każda zmiana wpisów usuwa je wszystkie; git akceptuje takie działanie i w razie potrzeby odbudowuje rozszerzenia. Opis używanych przez git rozszerzeń znajduje się poniżej.
Rozszerzenie TREE
Buforuje wstępnie obliczone obiekty drzewa dla katalogów. Przyspiesza git status i git commit, unikając ponownego obliczania drzewa, gdy zmienił się tylko indeks.
Podpis: TREE (0x54524545)
Rozszerzenie REUC
Zapisuje informacje potrzebne do cofnięcia rozwiązania konfliktu. Po rozwiązaniu konfliktu pierwotne wpisy etapów 1/2/3 są przenoszone tutaj, skąd można je przywrócić za pomocą git checkout -m.
Podpis: REUC (0x52455543)
Inne rozszerzenia
git zapisuje również:
EOIE(koniec wpisu indeksu): oznacza koniec wpisów i początek rozszerzeńIEOT(tabela przesunięć wpisów indeksu): umożliwia równoległe wczytywanie indeksulink(indeks dzielony): do bardzo dużych repozytoriów korzystających z indeksu dzielonego
W formacie git rozszerzenia, których podpis zaczyna się wielką literą, są opcjonalne, a te zaczynające się małą literą — wymagane. Pitmaster akceptuje oba rodzaje bez sprawdzania. Indeks dzielony (link) nie jest wczytywany, więc repozytoria, które go używają, nie są obsługiwane.
Suma kontrolna
Plik indeksu kończy się sumą kontrolną wszystkich wcześniejszych bajtów, obliczaną przy użyciu skrótu repozytorium (20 bajtów SHA-1, 32 bajty SHA-256). Pitmaster zapisuje ją podczas zapisu, ale nie weryfikuje jej podczas odczytu.
Index::open() i Index::parse() zgłaszają IndexParseException w przypadku braku podpisu DIRC lub nieobsługiwanej wersji.
Indeks i status
Indeks ma kluczowe znaczenie dla obliczania statusu. WorkingTreeStatus porównuje:
- Drzewo HEAD z indeksem: wykrywa zmiany dodane do indeksu
- Indeks z drzewem roboczym: wykrywa zmiany niezindeksowane
Pola stat (ctime, mtime, dev, ino, size, mode, uid, gid) umożliwiają szybkie sprawdzenie „czy ten plik się zmienił?” bez odczytywania jego zawartości. Jeśli pola stat są zgodne, a plik zmodyfikowano przed rozpoczęciem skanowania statusu, uznaje się, że plik się nie zmienił. Inny rozmiar od razu oznacza modyfikację. W przeciwnym razie Pitmaster odczytuje plik i oblicza jego skrót, aby sprawdzić, czy zawartość rzeczywiście się zmieniła.
Flaga assume-valid
if ($entry->assumeValid()) {
// Entry has assume-valid set (flags & 0x8000)
}
Ta flaga informuje git, aby pomijał sprawdzanie pliku podczas ustalania statusu, nawet jeśli zmieniły się pola stat (git update-index --assume-unchanged). Pitmaster zachowuje ten bit podczas ponownego zapisywania wpisów, ale jego mechanizm ustalania statusu go nie uwzględnia i nadal porównuje plik.