
# Клонирование, получение, отправка

Pitmaster поддерживает три основные сетевые операции git: клонирование (первоначальная загрузка), получение данных (инкрементальная загрузка) и отправку данных (выгрузка). URL SSH (`ssh://...`, `user@host:path`) используют транспорт SSH; для остальных URL применяется умный протокол HTTP, а клонирование и получение данных переключаются на простой HTTP, если обмен по умному протоколу завершается сбоем (см. раздел [Протоколы](https://pitmaster.inline0.dev/ru/docs/network/protocols)).

Все три операции запускают сетевые транспорты, поэтому при вызове для дескриптора, открытого с параметром `['processes' => false]` или `Pitmaster::processFreeOptions()`, возникает исключение `RuntimeException`.

## Клонирование

```php
use Pitmaster\Pitmaster;

// public static function clone(string $url, string $path, ?int $depth = null, array $options = []): Repository
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/path/to/destination');
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/path/to/shallow', 1);
```

В новом репозитории всегда используется SHA-1. Если клонирование завершается сбоем, а целевого каталога до этого не было, частично созданный каталог удаляется.

### Процесс клонирования

1. **Инициализация**: создайте новый пустой репозиторий по целевому пути
2. **Настройка удалённого репозитория**: сохраните URL как `remote.origin.url` и настройте refspec для получения данных
3. **Обнаружение ссылок**: отправьте `GET /info/refs?service=git-upload-pack`, чтобы узнать, что есть на сервере
4. **Согласование**: отправьте строки `want` для всех ссылок удалённого репозитория (без повторов по хешу)
5. **Получение pack-файла**: загрузите pack-файл со всеми запрошенными объектами
6. **Запись pack-файла**: сохраните pack-файл в `.git/objects/pack/`
7. **Индексация pack-файла**: создайте файл `.idx` для быстрого поиска
8. **Настройка ссылок**: создайте ветки отслеживания удалённого репозитория (`refs/remotes/origin/*`) и теги
9. **Настройка HEAD**: укажите для HEAD ветку по умолчанию (из возможности сервера `symref=HEAD:...` или `refs/heads/main`, если сервер её не сообщает) и задайте `branch.<name>.remote` и `branch.<name>.merge`
10. **Переключение на ветку**: переключитесь на эту ветку, чтобы заполнить рабочее дерево и индекс

Ошибки при итоговом переключении на ветку перехватываются и игнорируются, поэтому после клонирования рабочее дерево может оказаться пустым или заполненным частично ([#19](https://github.com/inline0/pitmaster/issues/19)).

```php
$repo = Pitmaster::clone($url, $path);
$head = $repo->head();
echo $head->message;

$branches = $repo->branches();
$files = $repo->status();
```

### Заполнение рабочего дерева

Получив pack-файл, Pitmaster вызывает `$repo->checkout()` для ветки по умолчанию. Эта операция записывает каждый файл из дерева коммита HEAD в рабочий каталог и создаёт индекс по записям дерева, поэтому `status()` сообщает, что рабочее дерево чистое.

## Получение данных

```php
// public function fetch(string $remote = 'origin', ?int $depth = null): void
$repo->fetch();              // Fetch from 'origin'
$repo->fetch('upstream');    // Fetch from a named remote
$repo->fetch('origin', 1);   // Shallow fetch
```

### Процесс получения данных

1. **Чтение URL удалённого репозитория**: из `.git/config` (`remote.<name>.url`)
2. **Обнаружение ссылок**: получите текущее состояние всех ссылок удалённого репозитория
3. **Согласование запрошенных и имеющихся объектов**:
   - Запрошенные: ссылки удалённого репозитория, соответствующие refspec `remote.<name>.fetch` (по умолчанию `+refs/heads/*:refs/remotes/<name>/*`), объекты которых отсутствуют локально (при указанной глубине — все такие объекты)
   - Имеющиеся: вершины всех локальных ссылок
4. **Получение pack-файла**: загрузите только новые объекты
5. **Запись pack-файла**: сохраните его в `.git/objects/pack/` и создайте индекс
6. **Обновление отслеживаемых ссылок**: обновите `refs/remotes/<remote>/<branch>` для каждой совпавшей ветки удалённого репозитория и запишите все объявленные ссылки на теги. Объекты тегов отдельно не запрашиваются, поэтому новая ссылка на аннотированный тег может указывать на объект, который не был загружен ([#24](https://github.com/inline0/pitmaster/issues/24))

```php
// Fetch is incremental: only downloads what's new
$repo->fetch();

// After fetching, merge or rebase to incorporate changes (use the full ref name)
$result = $repo->merge('refs/remotes/origin/main');
```

`resolve()` ищет имена как `refs/heads/<name>`, `refs/tags/<name>` и само имя, поэтому краткая форма `origin/main` не находится.

Pitmaster запрашивает тонкие pack-файлы, которые пока не может индексировать ([#16](https://github.com/inline0/pitmaster/issues/16)). Если сервер отвечает на инкрементальный запрос получения данных тонким pack-файлом, `fetch()` вызывает исключение `PackParseException`.

### Актуальная версия уже получена

Если все объекты, на которые указывают ссылки удалённого репозитория, уже есть локально, pack-файл не запрашивается, а `fetch()` возвращает управление после обнаружения ссылок. В этом случае отслеживаемые ссылки тоже не обновляются, даже если ветки удалённого репозитория переместились на уже имеющиеся у вас коммиты ([#23](https://github.com/inline0/pitmaster/issues/23)).

## Отправка данных

```php
$repo->push();                        // Push current branch to 'origin'
$repo->push('origin', 'main');        // Push a specific branch
$repo->push('upstream', 'feature');   // Push to a named remote
```

### Процесс отправки данных

1. **Чтение URL удалённого репозитория**: из `.git/config`
2. **Определение данных для отправки**: разрешите локальную ветку до хеша её коммита
3. **Обнаружение ссылок удалённого репозитория**: получите текущее состояние удалённой ветки (чтобы вычислить старый хеш)
4. **Проверка перемотки вперёд**: если удалённая ветка не является предком локальной, вызовите исключение `RuntimeException("Push rejected: non-fast-forward update to <branch>")` до отправки данных
5. **Отправка обновления ссылки**: `<old-hash> <new-hash> refs/heads/<branch>`
6. **Отправка данных pack-файла**: отправьте pack-файл с объектами, нужными удалённому репозиторию
7. **Проверка результата**: сервер сообщает об успехе или сбое для каждой ссылки

Хук `pre-push` выполняется до отправки обновления, если хуки включены.

```php
// Push the current branch
$repo->push();

// Push a specific branch to a specific remote
$repo->push('origin', 'feature/login');
```

### Требования для отправки данных

- Текущая ветка должна существовать локально
- Для удалённого репозитория должен быть настроен URL
- Удалённый репозиторий должен принимать отправленные данные (аутентификация, разрешения, правила запрета перемотки вперёд)

### Принудительная отправка данных

Pitmaster передаёт старый хеш из объявления актуальных ссылок удалённого репозитория, а `push()` запрещает локально обновления не в режиме fast-forward. Чтобы перезаписать ветку на удалённом сервере, используйте force-with-lease:

```php
// Lease defaults to refs/remotes/<remote>/<branch>; pass an ObjectId to set it explicitly
$repo->pushForceWithLease('origin', 'main');
$repo->pushForceWithLease('origin', 'main', $repo->resolve('refs/remotes/origin/main'));

// Update several branches in one atomic request
$repo->pushAtomic('origin', ['main', 'release']);

// Make the remote mirror local refs (also deletes remote refs that no longer exist locally)
$repo->pushMirror('origin');
```

## UploadPackClient (внутреннее устройство fetch)

```php
use Pitmaster\Protocol\UploadPackClient;
use Pitmaster\Protocol\SmartHttpClient;

$http = new SmartHttpClient();
$uploadPack = new UploadPackClient($http);

// Fetch with explicit wants and haves
$packData = $uploadPack->fetch($url, $wants, $haves);
```

Согласование want/have формирует запрос pkt-line:

```
want <hash1> <capabilities>\n
want <hash2>\n
deepen <depth>\n        (only when a depth is given)
0000
have <hash3>\n
have <hash4>\n
done\n
```

Сервер отвечает подтверждениями, а затем данными pack (возможно, упакованными в side-band).

## ReceivePackClient (внутреннее устройство push)

```php
use Pitmaster\Protocol\ReceivePackClient;
use Pitmaster\Protocol\SmartHttpClient;

$http = new SmartHttpClient();
$receivePack = new ReceivePackClient($http);

$receivePack->push($url, [
    [
        'old' => $oldId,      // Current remote hash (or zero hash for new branch)
        'new' => $localId,    // Local commit hash to push
        'ref' => 'refs/heads/main',
    ],
], $packData);
```

Формат запроса push:

```
<old-hash> <new-hash> refs/heads/main\0<capabilities>\n
0000
PACK<pack data>
```

## Обработка ошибок

```php
use Pitmaster\Exceptions\ProtocolException;

try {
    $repo->fetch();
} catch (ProtocolException $e) {
    // Network unreachable, auth failed, server error, etc.
    echo $e->getMessage();
}

try {
    $repo->push();
} catch (\RuntimeException $e) {
    // Remote not configured, not on a branch, branch not found, non-fast-forward
    echo $e->getMessage();
}
```

`fetch()` также может выбрасывать обычные исключения `RuntimeException` (неизвестный удалённый репозиторий, дескриптор без процессов) или `PackParseException` (pack, который невозможно индексировать). `ProtocolException` и `PackParseException` наследуются от `\RuntimeException`, поэтому обработка `\RuntimeException` охватывает их все.

## Конфигурация удалённых репозиториев

Удалённые репозитории хранятся в `.git/config`:

```ini
[remote "origin"]
    url = https://github.com/user/repo.git
    fetch = +refs/heads/*:refs/remotes/origin/*
```

Доступ через конфигурацию API:

```php
$config = $repo->config();
$url = $config->get('remote.origin.url');
$config->set('remote.upstream.url', 'https://github.com/other/repo.git');
$config->set('remote.upstream.fetch', '+refs/heads/*:refs/remotes/upstream/*');

// set() changes the in-memory config only; write it to persist the change
$config->writeToFile($repo->commonGitDir() . '/config');
```

## Данные pack для push

Сейчас Pitmaster собирает объекты, достижимые из отправляемых коммитов, но недостижимые из текущих ссылок удалённого репозитория, и записывает их в pack как базовые объекты (без дельта-сжатия). Это работает корректно, но создаёт pack большего размера, чем `git push`. Дельта-сжатие при записи pack запланировано как оптимизация.
