Формат індексу
Файл індексу (.git/index) — це область підготовки. У ньому записано файли, які буде включено до наступного коміту, а також метадані для ефективного обчислення статусу. Pitmaster читає індекси версій 2, 3 і 4 та записує версії 2 і 3. Запис індексу, прочитаного як версію 4, створює файл, який git не може прочитати (#15), тому перед записом через Pitmaster конвертуйте такі репозиторії за допомогою git update-index --index-version 2.
Читання індексу
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() повертає порожній індекс, якщо файлу немає. entries() повертає лише записи етапу 0; allEntries() включає етапи конфліктів, а paths() перелічує відстежувані шляхи.
IndexEntry
Кожен запис індексу — це незмінний об'єкт-значення:
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
Створення записів
// 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);
Зміна індексу
// 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() і removeEntries() обробляють кілька записів або шляхів за один прохід. Будь-яка зміна записів видаляє збережені розширення (див. нижче).
Запис індексу
use Pitmaster\Index\IndexWriter;
IndexWriter::write($index, '/path/to/.git/index');
Або через репозиторій (який робить це автоматично):
$repo->add('src/File.php'); // Reads file, creates blob, updates index, writes
$repo->remove('--cached', 'old-file.txt'); // Removes from index only, writes
Двійковий формат
Заголовок
Signature: DIRC (4 bytes, ASCII)
Version: 4 bytes, big-endian (2, 3, or 4)
Entry count: 4 bytes, big-endian
Формат запису (v2)
Кожен запис має вигляд:
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
Поле прапорців:
- Біт 15: прапорець assume-valid
- Біти 13-12: етап злиття (0-3)
- Біти 11-0: довжина назви шляху або 0xFFF, якщо шлях має 4095 байтів чи більше (тоді читач шукає NUL)
Відмінності версій
Версія 2: базовий формат, описаний вище. Найпоширеніша.
Версія 3: додає розширені прапорці (2 додаткові байти після звичайних прапорців). Підтримує біти skip-worktree і intent-to-add. Використовується, коли будь-який запис потребує розширених прапорців.
Версія 4: додає стиснення префіксів шляхів. Замість повних шляхів у кожному записі зберігається кількість байтів, які потрібно вилучити зі шляху попереднього запису, а потім новий суфікс. Значно зменшує розмір індексу для глибоко вкладених дерев.
Етапи конфлікту
Під час конфлікту злиття один шлях може мати кілька записів індексу на різних етапах:
| Етап | Значення |
|---|---|
| 0 | Звичайний (без конфлікту) |
| 1 | Базова версія (спільний предок) |
| 2 | Наша версія (поточна гілка) |
| 3 | Їхня версія (гілка, що зливається) |
// 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]
Розширення індексу
Розширення додаються після записів, перед контрольною сумою. Кожне розширення має 4-байтовий підпис, 4-байтовий розмір і дані змінної довжини.
Pitmaster зберігає блоки розширень як необроблені байти ($index->extensions() повертає пари signature і data) та записує їх назад без змін, якщо записи не змінено. Він не інтерпретує й не створює жодних розширень; будь-яка зміна записів видаляє їх усі, що git приймає та за потреби перебудовує. Розширення, які використовує git, описано нижче для довідки.
Розширення TREE
Кешує попередньо обчислені об'єкти дерев для каталогів. Прискорює git status і git commit, уникаючи повторного обчислення дерева, коли змінився лише індекс.
Підпис: TREE (0x54524545)
Розширення REUC
Записує інформацію для скасування розв'язання конфлікту. Після розв'язання конфлікту початкові записи етапів 1/2/3 переміщуються сюди, щоб їх можна було відновити за допомогою git checkout -m.
Підпис: REUC (0x52455543)
Інші розширення
git також записує:
EOIE(кінець запису індексу): позначає, де закінчуються записи й починаються розширенняIEOT(таблиця зміщень записів індексу): дає змогу паралельно завантажувати індексlink(розділений індекс): для дуже великих репозиторіїв із розділеним індексом
У форматі git розширення, підпис яких починається з великої літери, є необов'язковими, а з малої — обов'язковими. Pitmaster приймає обидва типи без перевірки. Розділений індекс (link) не завантажується, тому репозиторії, що його використовують, не підтримуються.
Контрольна сума
Файл індексу завершується контрольною сумою всіх попередніх байтів, обчисленою хеш-функцією репозиторію (20 байтів SHA-1, 32 байти SHA-256). Pitmaster записує її під час збереження, але не перевіряє під час читання.
Index::open() і Index::parse() генерують IndexParseException, якщо відсутній підпис DIRC або версія не підтримується.
Індекс і статус
Індекс має ключове значення для обчислення статусу. WorkingTreeStatus порівнює:
- Дерево HEAD та індекс: виявляє підготовлені зміни
- Індекс і робоче дерево: виявляє непідготовлені зміни
Поля stat (ctime, mtime, dev, ino, size, mode, uid, gid) дають змогу швидко перевірити, чи змінився файл, не читаючи його вміст. Якщо поля stat збігаються, а останню зміну файлу було внесено до початку сканування статусу, файл вважається незміненим. Інший розмір одразу означає, що файл змінено. Інакше Pitmaster читає файл і обчислює його хеш, щоб перевірити, чи змінився вміст.
Прапорець assume-valid
if ($entry->assumeValid()) {
// Entry has assume-valid set (flags & 0x8000)
}
Цей прапорець наказує git пропускати перевірку файлу під час визначення статусу, навіть якщо поля stat змінилися (git update-index --assume-unchanged). Pitmaster зберігає цей біт під час перезапису записів, але його механізм визначення статусу не враховує прапорець і все одно порівнює файл.