
# PHP API

## `Pitmaster\Pitmaster`

Класс `Pitmaster\Pitmaster` — статическая точка входа. Он предоставляет фабричные методы для открытия, создания и клонирования репозиториев, а также вспомогательные методы обнаружения.

```php
use Pitmaster\Pitmaster;
```

### `open`

```php
public static function open(string $path, array $options = []): Repository
```

Открывает существующий репозиторий по пути `$path`. Путь должен указывать на корень рабочего дерева (содержащий `.git/`). Поддерживаются также связанные рабочие деревья (где `.git` — файл, содержащий `gitdir: <path>`) и репозитории без рабочего дерева (где `$path` — сам каталог `.git`).

`$options` принимает два логических значения, оба по умолчанию равны `true`:

- `hooks`: запускать скрипты `.git/hooks`. Передайте `false`, чтобы отключить их для этого дескриптора.
- `processes`: разрешить все действия, запускающие процесс или открывающие сетевой транспорт. Передайте `false`, чтобы отключить хуки, хук fsmonitor, clone, fetch, push и транспорт процессов SSH. Чтение и запись локальных данных продолжат работать.

Вызывает `InvalidArgumentException`, если путь не указывает на репозиторий git.

```php
$repo = Pitmaster::open('/home/user/project');
$repo = Pitmaster::open('/home/user/project', ['hooks' => false]);
$repo = Pitmaster::open('/home/user/project', Pitmaster::processFreeOptions());
```

### `init`

```php
public static function init(string $path, string $objectFormat = 'sha1', array $options = []): Repository
```

Инициализирует новый репозиторий по пути `$path`. Создает структуру каталога `.git` с элементами `objects/`, `refs/heads/`, `refs/tags/`, `hooks/`, `info/`, `HEAD` (указывающим на `refs/heads/main`) и файлом `config` с настройками по умолчанию.

`$objectFormat` — это `'sha1'` или `'sha256'`. Для репозитория SHA-256 в конфигурации задаются `repositoryformatversion = 1` и `extensions.objectformat = sha256`. Любое другое значение вызывает `InvalidArgumentException`. `$options` принимает те же ключи, что и `open()`.

Вызывает `RuntimeException`, если по указанному пути уже существует репозиторий.

```php
$repo = Pitmaster::init('/home/user/new-project');
$repo = Pitmaster::init('/home/user/sha256-project', 'sha256');
```

### `clone`

```php
public static function clone(string $url, string $path, ?int $depth = null, array $options = []): Repository
```

Клонирует удаленный репозиторий. Инициализирует новый репозиторий, обнаруживает удаленные ссылки, загружает файл pack, настраивает ветки отслеживания удаленных веток и создает рабочее дерево. Для URL SSH (`ssh://...` или `user@host:path`) используется транспорт SSH; для остальных URL используется умный протокол HTTP с переходом на простой протокол HTTP при сбое обмена данными. Для поверхностного клонирования передайте `$depth`.

Вызывает `RuntimeException`, если `$options['processes']` имеет значение `false`. Если клонирование завершается ошибкой и `$path` не существовал до его начала, частично созданный каталог удаляется.

```php
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/tmp/repo');
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/tmp/shallow', 1);
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/tmp/repo', null, ['hooks' => false]);
```

### `processFreeOptions`

```php
public static function processFreeOptions(): array
```

Возвращает `['processes' => false, 'hooks' => false]` для сред выполнения, в которых нельзя запускать процессы хоста. Передайте его в `open()` или `init()`.

### `isRepository`

```php
public static function isRepository(string $path): bool
```

Проверяет, является ли путь репозиторием git. Возвращает `true` для обычных репозиториев (каталог `.git/`), связанных рабочих деревьев (файл `.git`) и репозиториев без рабочего дерева (при наличии файла `HEAD`).

```php
if (Pitmaster::isRepository('/path/to/check')) {
    // It's a git repo
}
```

### `isWorktree`

```php
public static function isWorktree(string $path): bool
```

Проверяет, является ли путь связанным рабочим деревом (не основным репозиторием). Возвращает `true`, только если `.git` — файл, содержащий ссылку `gitdir:`.

```php
if (Pitmaster::isWorktree('/path/to/worktree')) {
    // It's a linked worktree
}
```

### `commonGitDir`

```php
public static function commonGitDir(string $path): ?string
```

Определяет общий каталог git по пути к любой рабочей копии. Для обычных репозиториев это каталог `.git`. Для связанных рабочих деревьев это общий каталог git, содержащий объекты, конфигурацию и packed-refs. Возвращает `null`, если путь не указывает на репозиторий.

```php
$common = Pitmaster::commonGitDir('/path/to/worktree');
// '/path/to/main-repo/.git'
```

---

## `Pitmaster\Repository`

Класс `Pitmaster\Repository` — основной дескриптор для всех операций git. Получить его можно с помощью `Pitmaster::open()`, `Pitmaster::init()` или `Pitmaster::clone()`.

```php
use Pitmaster\Repository;
```

### Методы доступа к путям

#### `gitDir`

```php
public function gitDir(): string
```

Возвращает отдельный для рабочего дерева каталог git. Для обычных репозиториев это `.git/`. Для связанных рабочих деревьев — `.git/worktrees/<name>/`.

#### `commonGitDir`

```php
public function commonGitDir(): string
```

Возвращает общий каталог git. Для обычных репозиториев он совпадает с `gitDir()`. Для связанных рабочих деревьев это каталог `.git/` основного репозитория (в нем хранятся объекты, конфигурация и packed-refs).

#### `workDir`

```php
public function workDir(): string
```

Возвращает путь к корню рабочего дерева.

#### `isLinkedWorktree`

```php
public function isLinkedWorktree(): bool
```

Возвращает `true`, если дескриптор этого репозитория относится к связанному рабочему дереву.

#### `isBare`, `hooksEnabled`, `processesEnabled`

```php
public function isBare(): bool
public function hooksEnabled(): bool
public function processesEnabled(): bool
```

Сообщает, есть ли у репозитория рабочее дерево, и какие параметры `hooks` / `processes` действуют для этого дескриптора. `hooksEnabled()` всегда равен `false`, если процессы отключены.

---

### Объекты

#### `readObject`

```php
public function readObject(string $hash): GitObject
```

Читает любой объект по полному шестнадцатеричному хешу (40 символов или 64 в репозитории SHA-256). Сокращенные хеши не принимаются. Возвращает экземпляр `Blob`, `Tree`, `Commit` или `Tag`. Сначала выполняется поиск среди несжатых объектов, затем в pack-файлах.

Вызывает `ObjectNotFoundException`, если хеш не найден.

```php
$object = $repo->readObject('a1b2c3d4e5f6...');
```

#### `writeObject`

```php
public function writeObject(GitObject $object): ObjectId
```

Записывает объект в хранилище несжатых объектов. Возвращает вычисленное значение `ObjectId`.

```php
use Pitmaster\Object\Blob;

$blob = Blob::fromContent('Hello, world!');
$id = $repo->writeObject($blob);
```

#### `catFile`

```php
public function catFile(string $hash): string
```

Возвращает исходное содержимое объекта; эквивалентно `git cat-file -p`.

```php
echo $repo->catFile('a1b2c3d4...');
```

#### `objectExists`

```php
public function objectExists(string $hash): bool
```

Проверяет, существует ли объект в репозитории (среди несжатых объектов или в pack-файлах).

#### `listObjects`

```php
public function listObjects(): array
```

Перечисляет все хеши объектов в репозитории. Возвращает массив шестнадцатеричных строк.

---

### Ссылки

#### `head`

```php
public function head(): Commit
```

Возвращает текущий коммит HEAD. Переходит по символическим ссылкам (HEAD -> refs/heads/main -> хеш коммита).

Вызывает `RuntimeException`, если HEAD не указывает на допустимый коммит (например, e.g — пустой репозиторий).

#### `branch`

```php
public function branch(?string $name = null): ?string
```

Без аргументов возвращает имя текущей ветки (например, e.g — это `'main'`) или `null`, если HEAD отсоединен.

С именем ветки разрешает ее в хеш коммита (шестнадцатеричную строку) или возвращает `null`, если ветка не существует.

```php
$current = $repo->branch();        // 'main'
$hash = $repo->branch('feature');   // 'abc123...' or null
```

#### `branches`

```php
public function branches(): array
```

Возвращает отсортированный массив имен всех веток (без префикса `refs/heads/`).

```php
$branches = $repo->branches();
// ['bugfix/typo', 'feature/login', 'main']
```

#### `tags`

```php
public function tags(): array
```

Возвращает отсортированный массив имен всех тегов (без префикса `refs/tags/`).

#### `resolve`

```php
public function resolve(string $revision): ObjectId
```

Разрешает выражение ревизии в `ObjectId`. Поддерживаются:
- Полные шестнадцатеричные хеши (40 символов или 64 для SHA-256). Сокращенные хеши, например `abc1234`, не разрешаются.
- `HEAD`
- Имена веток (`main`, `feature/login`)
- Имена тегов (`v1.0.0`)
- Полные пути ссылок (`refs/heads/main`)
- Выражения ревизий (`HEAD~3`, `main^2`)

Вызывает `RuntimeException`, если ревизию не удается разрешить.

```php
$id = $repo->resolve('HEAD~3');
$id = $repo->resolve('v1.0.0');
$id = $repo->resolve('feature/login');
```

#### `allRefs`

```php
public function allRefs(): array
```

Возвращает все ссылки в виде ассоциативного массива: имя ссылки → шестнадцатеричный хеш.

```php
$refs = $repo->allRefs();
// ['refs/heads/main' => 'abc123...', 'refs/tags/v1.0.0' => 'def456...']
```

#### `updateRef`

```php
public function updateRef(string $name, ObjectId $target): void
```

Перемещает ссылку на новую цель.

```php
$repo->updateRef('refs/heads/main', $commitId);
```

#### `createBranch`

```php
public function createBranch(string $name, ?ObjectId $from = null): void
```

Создает новую ветку. Если `$from` равно null, создает ветку от текущего HEAD.

```php
$repo->createBranch('feature/new-thing');
$repo->createBranch('hotfix', $repo->resolve('v1.0.0'));
```

#### `deleteBranch`

```php
public function deleteBranch(string $name): void
```

Удаляет ветку по имени. Вызывает `RuntimeException`, если ветка уже извлечена или не существует. В отличие от `git branch -d`, этот метод не проверяет, слита ли ветка.

```php
$repo->deleteBranch('feature/old');
```

#### `createLightweightTag`

```php
public function createLightweightTag(string $name, ?ObjectId $target = null): void
```

Создает легковесный тег. Если `$target` равно null, тег напрямую указывает на текущий HEAD.

```php
$repo->createLightweightTag('v2.0.0');
$repo->createLightweightTag('hotfix-start', $repo->resolve('main~2'));
```

#### `createTag`

```php
public function createTag(string $name, string $message, ?ObjectId $target = null, ?string $tagger = null): ObjectId
```

Создать аннотированный тег. Если `$target` равно null, тег создаётся для текущего HEAD. Возвращает `ObjectId` объекта тега.

```php
$tagId = $repo->createTag('v2.0.0', 'Release version 2.0.0');
```

#### `deleteTag`

```php
public function deleteTag(string $name): void
```

Удалить тег по имени.

```php
$repo->deleteTag('v1.0.0');
```

#### `packRefs`

```php
public function packRefs(): void
```

Перезаписать общий файл `packed-refs` на основе текущего набора свободных ссылок и удалить соответствующие общие свободные ссылки. Для связанных рабочих деревьев запись выполняется в общий каталог git.

```php
$repo->packRefs();
```

#### `defaultBranch`

```php
public function defaultBranch(): string
```

Определить ветку репозитория по умолчанию. Сначала проверяется символьная ссылка HEAD удалённого репозитория, затем локальный HEAD, а если это не удаётся — используется `main` или `master`.

#### `isBranchMerged`

```php
public function isBranchMerged(string $branch, ?string $target = null): bool
```

Проверить, полностью ли слита ветка в другую ветку (по умолчанию — в ветку по умолчанию).

```php
if ($repo->isBranchMerged('feature/done')) {
    $repo->deleteBranch('feature/done');
}
```

#### `checkout`

```php
public function checkout(string $target): void
```

Переключить ветку или отсоединить HEAD. Обновляет HEAD, индекс и рабочее дерево. `$target` — это имя ветки, тег, полный хеш или выражение ревизии; если это не ветка, HEAD отсоединяется.

Вызывает `RuntimeException`, если локальное изменение (в индексе или нет) либо неотслеживаемый файл находится по пути, который различается в текущем и целевом деревьях.

Незакоммиченные изменения в других отслеживаемых файлах не сохраняются: индекс и рабочее дерево заново создаются на основе целевого дерева, поэтому изменения файлов, одинаковых в обоих деревьях, будут потеряны, тогда как git их сохраняет ([#2](https://github.com/inline0/pitmaster/issues/2)). Сначала создайте коммит или сохраните изменения во временное хранилище. `merge()`, `cherryPick()`, `revert()`, `rebase()` и методы bisect пересоздают рабочее дерево таким же образом.

```php
$repo->checkout('feature/login');
$repo->checkout($repo->resolve('v1.0.0')->hex);  // Detached HEAD
```

---

### Индекс

#### `index`

```php
public function index(): Index
```

Прочитать и вернуть текущий индекс (область подготовки) из `.git/index`.

#### `add`

```php
public function add(string ...$paths): void
```

Добавить файлы в индекс. Читает каждый файл из рабочего дерева, создаёт объект blob и обновляет индекс. Каждый аргумент должен быть путём к файлу (или символической ссылке) относительно корня рабочего дерева; каталоги и шаблоны не раскрываются, а при отсутствии файла вызывается `RuntimeException`.

```php
$repo->add('src/Feature.php', 'tests/FeatureTest.php');
```

#### `remove`

```php
public function remove(string ...$paths): void
public function removeCached(string ...$paths): void
```

Удалить отслеживаемые пути и из индекса, и из рабочего дерева. Поддерживает `--cached` и `-r` / `--recursive` в списке аргументов для поведения в стиле Git.

```php
$repo->remove('old-file.txt');
$repo->remove('-r', 'src');
$repo->remove('--cached', 'old-file.txt');
$repo->removeCached('old-file.txt');
```

#### `mv`

```php
public function mv(string $source, string $destination): void
```

Переместить/переименовать файл и в рабочем дереве, и в индексе.

```php
$repo->mv('old-name.php', 'new-name.php');
```

---

### Коммиты

#### `commit`

```php
public function commit(?string $message = null, ?string $author = null): ObjectId
```

Создать коммит на основе текущего индекса. Строит иерархию дерева по записям индекса, создаёт объект коммита с HEAD в качестве родителя (если он есть) и обновляет HEAD. Возвращает `ObjectId` нового коммита.

`$message` может быть `null` только при остановке с конфликтом во время слияния, переноса коммита, отмены коммита или перемещения базы; в этом случае используется подготовленное сообщение. В остальных случаях пустое сообщение `null` вызывает `RuntimeException('Commit message required')`. Коммит также вызывает ошибку, если в индексе есть неслитые пути, если индекс пуст в репозитории без коммитов или если дерево не изменилось относительно HEAD.

Параметр `$author` — это полная строка автора git (`Name <email> timestamp timezone`). Если значение равно null, оно определяется из переменных окружения, констант и `.git/config` в порядке, описанном в разделе [Начало работы](https://pitmaster.inline0.dev/ru/docs/getting-started).

```php
$id = $repo->commit('Fix null pointer bug');
```

#### `show`

```php
public function show(string $revision): array
```

Показать коммит и его различия с первым родителем. Если ревизия разрешается в аннотированный тег, возвращаемое значение также содержит `['tag' => Tag]`, а `commit` — это коммит, на который указывает тег.

```php
$result = $repo->show('HEAD');
echo $result['commit']->message;

foreach ($result['diff'] as $diff) {
    echo $diff->format();
}
```

#### `reset`

```php
public function reset(string $revision, string $mode = 'mixed'): void
```

Сбросить HEAD до указанного коммита. Режимы:
- `'soft'`: переместить только HEAD
- `'mixed'`: переместить HEAD и сбросить индекс (по умолчанию)
- `'hard'`: переместить HEAD, сбросить индекс и сбросить рабочее дерево

```php
$repo->reset('HEAD~1', 'soft');
$repo->reset('main', 'hard');
```

#### `restore`

```php
public function restore(string $path, ?string $source = null, bool $staged = false, bool $worktree = false): void
```

Восстановить путь из индекса или указанной исходной ревизии. Если ни один флаг не задан, Pitmaster восстанавливает рабочее дерево из индекса (или из `$source`, если он указан). `staged: true` восстанавливает индекс из HEAD (или `$source`), не затрагивая рабочее дерево; чтобы восстановить и то и другое, передайте `staged: true, worktree: true`.

```php
$repo->restore('src/File.php');                                        // Worktree from index
$repo->restore('src/File.php', staged: true);                          // Index from HEAD
$repo->restore('src/File.php', 'HEAD~1');                              // Worktree from another commit
$repo->restore('src/File.php', 'HEAD~1', staged: true, worktree: true); // Index + worktree from another commit
```

#### `cherryPick`

```php
public function cherryPick(string $revision): ObjectId
public function cherryPickContinue(): ObjectId
public function cherryPickAbort(): void
```

Применить коммит как новый коммит в текущей ветке, сохранив исходного автора. Если операция остановилась из-за конфликтов, разрешите их, добавьте пути в индекс, затем вызовите `cherryPickContinue()` или `cherryPickAbort()`.

```php
$newId = $repo->cherryPick('feature~2');

// After resolving conflicts:
$repo->add('src/File.php');
$repo->cherryPickContinue();
```

#### `revert`

```php
public function revert(string $revision): ObjectId
public function revertContinue(): ObjectId
public function revertAbort(): void
```

Создать коммит, отменяющий другой коммит. Если отмена остановилась из-за конфликтов, разрешите их, добавьте пути в индекс, затем вызовите `revertContinue()` или `revertAbort()`.

```php
$revertId = $repo->revert('HEAD~1');

// After resolving conflicts:
$repo->add('src/File.php');
$repo->revertContinue();
```

#### `rebase`

```php
public function rebase(string $onto): array
public function rebaseContinue(): array
public function rebaseAbort(): void
public function rebaseSkip(): array
```

Переместить текущую ветку на другую ревизию. Сейчас Pitmaster поддерживает линейное перемещение с состоянием остановки при конфликте в формате Git, а также продолжение, отмену и пропуск. HEAD должен указывать на ветку.

`rebase()`, `rebaseContinue()` и `rebaseSkip()` возвращают `['success' => bool, 'commits' => int, 'conflicts' => list<string>]`: сведения о том, завершилось ли перемещение базы, сколько коммитов было применено и какие пути конфликтовали при остановке.

```php
$result = $repo->rebase('main');

if (!$result['success']) {
    // Resolve conflicts, then continue or skip/abort.
    $repo->add('src/File.php');
    $repo->rebaseContinue();
}
```

---

### Состояние

#### `status`

```php
public function status(): array
```

Вычислить состояние рабочего дерева. Возвращает массив объектов `StatusEntry`, каждый из которых содержит:
- `path`: путь к файлу
- `index`: перечисление `FileStatus` для изменений между HEAD и индексом
- `worktree`: перечисление `FileStatus` для изменений между индексом и рабочим деревом
- `origPath`, `renameScore`: задаются для переименованных записей, в остальных случаях — `null`

В обеих колонках для неотслеживаемых файлов указано `FileStatus::Untracked`. Игнорируемые файлы не отображаются.

```php
foreach ($repo->status() as $entry) {
    echo $entry->shortFormat() . "\n"; // "M  src/File.php", "?? notes.txt"
}
```

#### `statusPorcelainV2`

```php
public function statusPorcelainV2(): string
```

Вывод состояния в машиночитаемом формате porcelain v2 от git.

```php
echo $repo->statusPorcelainV2();
// 1 M. N... 000000 000000 000000 0000...0000 0000...0000 modified-file.txt
// ? untracked-file.txt
```

---

### Различия

#### `diff`

```php
public function diff(?string $pathspec = null, string $algorithm = 'myers'): array
```

Сравнить рабочее дерево с индексом (изменения, не добавленные в индекс). Возвращает массив объектов `DiffResult`. `$pathspec` фильтрует по одному точному пути к файлу. `$algorithm` принимает значения `'myers'` (по умолчанию), `'patience'`, `'histogram'` или `'minimal'`; любое другое значение вызывает `InvalidArgumentException`.

```php
$diffs = $repo->diff();

foreach ($diffs as $diff) {
    echo $diff->format();  // Unified diff output
}
```

#### `diffStaged`

```php
public function diffStaged(?string $pathspec = null, string $algorithm = 'myers'): array
```

Сравнить индекс с HEAD (изменения, добавленные в индекс). Возвращает `DiffResult[]`, обнаруживая переименования между удалёнными и добавленными файлами.

Аргумент `$pathspec` сейчас игнорируется, поэтому возвращаются все добавленные в индекс файлы; до первого коммита метод возвращает пустой массив ([#5](https://github.com/inline0/pitmaster/issues/5)).

```php
$staged = $repo->diffStaged();
```

#### `diffTree`

```php
public function diffTree(ObjectId $a, ObjectId $b, string $algorithm = 'myers'): array
```

Сравнить два дерева по их `ObjectId`. Возвращает `DiffResult[]`. Передавайте идентификаторы деревьев, а не коммитов: идентификатор коммита не распознаётся как дерево и даёт пустой результат.

```php
$old = $repo->readObject($repo->resolve('HEAD~1')->hex);
$new = $repo->head();
$diffs = $repo->diffTree($old->tree, $new->tree);
```

---

### Журнал

#### `log`

```php
public function log(int $limit = 50, ?ObjectId $from = null): array
```

Обойти историю коммитов, начиная с самых новых по времени коммиттера. Возвращает `Commit[]`. Если `$from` равно null, обход начинается с HEAD. Если в HEAD ещё нет коммитов, возвращается пустой массив.

```php
$commits = $repo->log(10);
```

#### `logAll`

```php
public function logAll(int $limit = 50): array
```

Обойти коммиты, достижимые из каждой ссылки и HEAD, начиная с самых новых. Возвращает `Commit[]`.

#### `logOneline`

```php
public function logOneline(int $limit = 50, bool $all = false, ?string $path = null): array
```

Вернуть строки журнала в формате `git log --oneline` (`abc1234 Subject`), начиная с HEAD или всех ссылок (`$all`), либо отфильтрованные по пути.

#### `logPath`

```php
public function logPath(string $path, int $limit = 50): array
```

Показать журнал, отфильтрованный по пути к файлу. Возвращаются только коммиты, в которых сравнение с первым родителем добавляет, изменяет, удаляет или переименовывает этот конкретный файл. Пути к каталогам не учитываются.

Проверяются только последние `5 * $limit` коммитов, поэтому в результат могут не попасть более старые коммиты, затрагивающие файл ([#7](https://github.com/inline0/pitmaster/issues/7)).

```php
$commits = $repo->logPath('src/Repository.php', 20);
```

---

### Слияние

#### `merge`

```php
public function merge(string $branch, string $strategy = 'recursive'): MergeResult
public function mergeOctopus(array $branches): MergeResult
public function mergeContinue(): ObjectId
public function mergeAbort(): void
```

Слить ветку (или любую ревизию) в HEAD. Автоматически обнаруживает слияния с перемоткой указателя. Если перемотка невозможна, проверяет конфликты на уровне дерева и при их отсутствии создаёт коммит слияния. При конфликте записывает состояние слияния в формате Git в индекс и рабочее дерево; разрешите конфликты, добавьте пути в индекс, затем вызовите `mergeContinue()` или `mergeAbort()`.

`$strategy` `'ours'` создаёт коммит слияния, сохраняющий дерево HEAD; любое другое значение запускает слияние рекурсивным методом по умолчанию. `mergeOctopus()` объединяет две или более ветки в один коммит. Если в какой-либо ветке возник конфликт, возвращается результат с конфликтами без записи состояния слияния и создания коммита.

Слияние ветки, уже включённой в HEAD, создаёт ещё один коммит слияния вместо сообщения «Уже актуально» ([#6](https://github.com/inline0/pitmaster/issues/6)). Незакоммиченные изменения в отслеживаемых файлах перезаписываются, как описано в разделе `checkout()`.

Возвращает `MergeResult` со следующими полями:
- `clean`: прошло ли слияние без конфликтов
- `commitId`: идентификатор коммита слияния (если конфликтов нет)
- `conflictPaths`: массив путей к конфликтующим файлам (если есть конфликты)
- `mergedContents`: путь к файлу с содержимым и маркерами конфликта (если есть конфликты)

```php
$result = $repo->merge('feature/login');

if ($result->clean) {
    echo "Merged: {$result->commitId->hex}\n";
} else {
    echo "Conflicts in: " . implode(', ', $result->conflictPaths) . "\n";
    $repo->add('src/File.php');
    $repo->mergeContinue();
}
```

#### `mergeBase`

```php
public function mergeBase(ObjectId $a, ObjectId $b): ?ObjectId
```

Найти основу слияния (наименьшего общего предка) двух коммитов. Возвращает null, если общего предка нет.

```php
$base = $repo->mergeBase(
    $repo->resolve('main'),
    $repo->resolve('feature/login'),
);
```

### Бисекция

```php
public function bisectStart(string $bad, string $good): ObjectId
public function bisectGood(?string $revision = null): ?ObjectId
public function bisectBad(?string $revision = null): ?ObjectId
public function bisectReset(): void
```

Бинарный поиск коммита, в котором появилось изменение. `bisectStart()` записывает плохую и хорошую ревизии, переключается на первый кандидат и возвращает его. `bisectGood()` и `bisectBad()` помечают текущий HEAD (или `$revision`), переключаются на следующий кандидат и возвращают его. Когда остаётся один подозреваемый, этот коммит возвращается и остаётся кандидатом: это первый плохой коммит. `bisectReset()` завершает сеанс и возвращает к ветке или коммиту, с которого он начался.

```php
$candidate = $repo->bisectStart('HEAD', 'v1.0.0');
// test $candidate ...
$candidate = $repo->bisectBad();
$candidate = $repo->bisectGood();
$repo->bisectReset();
```

---

### Сеть

#### `fetch`

```php
public function fetch(string $remote = 'origin', ?int $depth = null): void
```

Получить данные с удалённого репозитория. Загружает новые объекты и обновляет ссылки отслеживания удалённых веток. Передайте `$depth`, чтобы углубить или ограничить сокращённую историю.

`fetch()` и методы `push*()` вызывают `RuntimeException` для дескриптора, открытого с помощью `['processes' => false]`.

```php
$repo->fetch();
$repo->fetch('upstream');
```

#### `push`

```php
public function push(string $remote = 'origin', ?string $branch = null): void
public function pushForceWithLease(string $remote = 'origin', ?string $branch = null, ?ObjectId $expected = null): void
public function pushAtomic(string $remote = 'origin', array $branches = []): void
public function pushMirror(string $remote = 'origin'): void
```

Отправить текущую (или указанную) ветку в удалённый репозиторий. Доступны варианты force-with-lease, atomic и mirror для более строгого или широкого обновления ссылок.

```php
$repo->push();
$repo->push('origin', 'feature/login');
$repo->pushForceWithLease('origin', 'main', $repo->resolve('refs/remotes/origin/main'));
$repo->pushAtomic('origin', ['main', 'release']);
$repo->pushMirror();
```

---

### Рабочие деревья

#### `addWorktree`

```php
public function addWorktree(string $path, string $branch, ?ObjectId $from = null, ?string $name = null): Worktree
```

Добавить связанное рабочее дерево. Создаёт ветку, если её ещё нет, настраивает метаданные рабочего дерева и создаёт файлы рабочего дерева. Передайте `$name`, если нужен детерминированный идентификатор метаданных, отличный от имени каталога checkout.

```php
$wt = $repo->addWorktree('/tmp/review', 'feature/review');
$wt = $repo->addWorktree('/tmp/checkouts/review', 'feature/review', name: 'review-wt');
```

#### `removeWorktree`

```php
public function removeWorktree(string $pathOrName, bool $force = false): void
```

Удалить связанное рабочее дерево. Передайте `$force = true`, чтобы удалить его, даже если оно заблокировано.

#### `worktrees`

```php
public function worktrees(): array
```

Перечислить все рабочие деревья (основное и связанные). Возвращает `Worktree[]`.

```php
foreach ($repo->worktrees() as $wt) {
    echo "{$wt->path} [{$wt->branch}]\n";
}
```

---

### Конфигурация

#### `config`

```php
public function config(): GitConfig
```

Получить доступ к конфигурации репозитория git. Читать и записывать значения конфигурации.

```php
$config = $repo->config();
$name = $config->get('user.name');
$config->set('user.email', 'new@example.com');
```

---

### Внутренний доступ

Эти методы предоставляют доступ к внутренним компонентам для расширенных сценариев использования.

#### `objectDatabase`

```php
public function objectDatabase(): ObjectDatabase
```

Получить доступ к необработанной базе объектов (свободные объекты и хранилища pack).

#### `refDatabase`

```php
public function refDatabase(): RefDatabase
```

Получить доступ к необработанной базе ссылок (свободные и упакованные ссылки).

---

## Классы низкого уровня

Эти классы не обёрнуты в `Repository` и создаются на основе его внутренних компонентов.

### `Pitmaster\Graph\Blame`

```php
public function blame(ObjectId $headId, string $path): array
```

Возвращает одну запись для каждой строки `$path` в `$headId`: `['line' => int, 'hash' => string, 'author' => string, 'content' => string]`. `author` — это полная строка идентификации (`Name <email> timestamp timezone`).

```php
use Pitmaster\Graph\Blame;

$blame = new Blame($repo->objectDatabase());

foreach ($blame->blame($repo->resolve('HEAD'), 'src/File.php') as $line) {
    echo substr($line['hash'], 0, 8) . " {$line['content']}\n";
}
```

### `Pitmaster\Graph\Grep`

```php
public function grep(ObjectId $treeId, string $pattern, string $prefix = '', array $options = []): array
```

Ищет в blob-объектах дерева. Возвращает по одному `['path' => string, 'line' => int, 'content' => string]` на каждую совпавшую строку. Параметры: `regex` (считать шаблон выражением PCRE) и `ignore_case`. При неверном регулярном выражении вызывается `InvalidArgumentException`.

```php
use Pitmaster\Graph\Grep;

$grep = new Grep($repo->objectDatabase());
$matches = $grep->grep($repo->head()->tree, 'TODO', '', ['ignore_case' => true]);
```

### `Pitmaster\Ref\Notes`

```php
public function get(ObjectId $commitId, string $notesRef = 'refs/notes/commits'): ?string
public function set(ObjectId $commitId, string $noteContent, string $notesRef = 'refs/notes/commits'): void
public function remove(ObjectId $commitId, string $notesRef = 'refs/notes/commits'): void
public function listAll(string $notesRef = 'refs/notes/commits'): array
public function merge(string $sourceRef, string $targetRef = 'refs/notes/commits'): void
```

Читает и записывает примечания git, хранящиеся по ссылке на примечания. `listAll()` возвращает хеш коммита и содержимое примечания.

```php
use Pitmaster\Ref\Notes;

$notes = new Notes($repo->objectDatabase(), $repo->refDatabase());
$notes->set($repo->resolve('HEAD'), "Reviewed\n");
echo $notes->get($repo->resolve('HEAD'));
```
