
# Формат индекса

Файл индекса (`.git/index`) — это область подготовленных изменений. В нем записаны файлы, которые войдут в следующий коммит, а также метаданные для эффективного вычисления статуса. Pitmaster читает версии индекса 2, 3 и 4, а записывает версии 2 и 3. Запись индекса, прочитанного в версии 4, создает файл, который git не может прочитать ([#15](https://github.com/inline0/pitmaster/issues/15)), поэтому перед записью через Pitmaster преобразуйте такие репозитории с помощью `git update-index --index-version 2`.

## Чтение индекса

```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()` возвращает пустой индекс, если файла нет. `entries()` возвращает только записи этапа 0; `allEntries()` включает этапы конфликтов, а `paths()` выводит отслеживаемые пути.

## IndexEntry

Каждая запись в индексе — неизменяемый объект-значение:

```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
```

### Создание записей

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

## Изменение индекса

```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()` и `removeEntries()` позволяют за один проход обработать несколько записей или путей. Любое изменение записей удаляет сохраненные расширения (см. ниже).

## Запись индекса

```php
use Pitmaster\Index\IndexWriter;

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

Или через репозиторий, который выполняет эту операцию автоматически:

```php
$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
```

Поле flags:
- Бит 15: флаг «считать действительным»
- Биты 13-12: этап слияния (0-3)
- Биты 11-0: длина имени пути или 0xFFF, если путь составляет 4095 байт или больше (в этом случае считыватель ищет NUL)

### Различия между версиями

**Версия 2**: базовый формат, описанный выше. Самый распространенный.

**Версия 3**: добавляет расширенные флаги (2 дополнительных байта после обычных флагов). Поддерживает биты `skip-worktree` и `intent-to-add`. Используется, если хотя бы одной записи нужны расширенные флаги.

**Версия 4**: добавляет сжатие префиксов путей. Вместо полных путей в каждой записи хранится число байтов, которые нужно удалить из пути предыдущей записи, а затем новый суффикс. Это значительно уменьшает размер индекса для глубоко вложенных деревьев.

## Этапы конфликта

При конфликте слияния для одного пути может быть несколько записей индекса на разных этапах:

| Этап | Значение |
|-------|---------|
| 0 | Обычная запись (конфликта нет) |
| 1 | Базовая версия (общий предок) |
| 2 | Наша версия (текущая ветка) |
| 3 | Их версия (сливаемая ветка) |

```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]
```

## Расширения индекса

Расширения добавляются после записей и перед контрольной суммой. Каждое расширение содержит 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` сравнивает:
1. Дерево HEAD и индекс: обнаруживает подготовленные изменения
2. Индекс и рабочее дерево: обнаруживает неподготовленные изменения

Поля stat (ctime, mtime, dev, ino, size, mode, uid, gid) позволяют быстро проверять, изменился ли файл, не читая его содержимое. Если поля stat совпадают и файл был изменен до начала сканирования статуса, считается, что файл не менялся. Несовпадение размера сразу помечает файл как измененный. В противном случае Pitmaster читает файл и вычисляет его хеш, чтобы проверить, изменилось ли содержимое.

## Флаг assume-valid

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

Этот флаг указывает git пропустить проверку файла при вычислении статуса, даже если поля stat изменились (`git update-index --assume-unchanged`). Pitmaster сохраняет этот бит при перезаписи записей, но его вычисление статуса не учитывает флаг и по-прежнему сравнивает файл.
