Клонирование, получение, отправка
Pitmaster поддерживает три основные сетевые операции git: клонирование (первоначальная загрузка), получение данных (инкрементальная загрузка) и отправку данных (выгрузка). URL SSH (ssh://..., user@host:path) используют транспорт SSH; для остальных URL применяется умный протокол HTTP, а клонирование и получение данных переключаются на простой HTTP, если обмен по умному протоколу завершается сбоем (см. раздел Протоколы).
Все три операции запускают сетевые транспорты, поэтому при вызове для дескриптора, открытого с параметром ['processes' => false] или Pitmaster::processFreeOptions(), возникает исключение RuntimeException.
Клонирование
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. Если клонирование завершается сбоем, а целевого каталога до этого не было, частично созданный каталог удаляется.
Процесс клонирования
- Инициализация: создайте новый пустой репозиторий по целевому пути
- Настройка удалённого репозитория: сохраните URL как
remote.origin.urlи настройте refspec для получения данных - Обнаружение ссылок: отправьте
GET /info/refs?service=git-upload-pack, чтобы узнать, что есть на сервере - Согласование: отправьте строки
wantдля всех ссылок удалённого репозитория (без повторов по хешу) - Получение pack-файла: загрузите pack-файл со всеми запрошенными объектами
- Запись pack-файла: сохраните pack-файл в
.git/objects/pack/ - Индексация pack-файла: создайте файл
.idxдля быстрого поиска - Настройка ссылок: создайте ветки отслеживания удалённого репозитория (
refs/remotes/origin/*) и теги - Настройка HEAD: укажите для HEAD ветку по умолчанию (из возможности сервера
symref=HEAD:...илиrefs/heads/main, если сервер её не сообщает) и задайтеbranch.<name>.remoteиbranch.<name>.merge - Переключение на ветку: переключитесь на эту ветку, чтобы заполнить рабочее дерево и индекс
Ошибки при итоговом переключении на ветку перехватываются и игнорируются, поэтому после клонирования рабочее дерево может оказаться пустым или заполненным частично (#19).
$repo = Pitmaster::clone($url, $path);
$head = $repo->head();
echo $head->message;
$branches = $repo->branches();
$files = $repo->status();
Заполнение рабочего дерева
Получив pack-файл, Pitmaster вызывает $repo->checkout() для ветки по умолчанию. Эта операция записывает каждый файл из дерева коммита HEAD в рабочий каталог и создаёт индекс по записям дерева, поэтому status() сообщает, что рабочее дерево чистое.
Получение данных
// 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
Процесс получения данных
- Чтение URL удалённого репозитория: из
.git/config(remote.<name>.url) - Обнаружение ссылок: получите текущее состояние всех ссылок удалённого репозитория
- Согласование запрошенных и имеющихся объектов:
- Запрошенные: ссылки удалённого репозитория, соответствующие refspec
remote.<name>.fetch(по умолчанию+refs/heads/*:refs/remotes/<name>/*), объекты которых отсутствуют локально (при указанной глубине — все такие объекты) - Имеющиеся: вершины всех локальных ссылок
- Запрошенные: ссылки удалённого репозитория, соответствующие refspec
- Получение pack-файла: загрузите только новые объекты
- Запись pack-файла: сохраните его в
.git/objects/pack/и создайте индекс - Обновление отслеживаемых ссылок: обновите
refs/remotes/<remote>/<branch>для каждой совпавшей ветки удалённого репозитория и запишите все объявленные ссылки на теги. Объекты тегов отдельно не запрашиваются, поэтому новая ссылка на аннотированный тег может указывать на объект, который не был загружен (#24)
// 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). Если сервер отвечает на инкрементальный запрос получения данных тонким pack-файлом, fetch() вызывает исключение PackParseException.
Актуальная версия уже получена
Если все объекты, на которые указывают ссылки удалённого репозитория, уже есть локально, pack-файл не запрашивается, а fetch() возвращает управление после обнаружения ссылок. В этом случае отслеживаемые ссылки тоже не обновляются, даже если ветки удалённого репозитория переместились на уже имеющиеся у вас коммиты (#23).
Отправка данных
$repo->push(); // Push current branch to 'origin'
$repo->push('origin', 'main'); // Push a specific branch
$repo->push('upstream', 'feature'); // Push to a named remote
Процесс отправки данных
- Чтение URL удалённого репозитория: из
.git/config - Определение данных для отправки: разрешите локальную ветку до хеша её коммита
- Обнаружение ссылок удалённого репозитория: получите текущее состояние удалённой ветки (чтобы вычислить старый хеш)
- Проверка перемотки вперёд: если удалённая ветка не является предком локальной, вызовите исключение
RuntimeException("Push rejected: non-fast-forward update to <branch>")до отправки данных - Отправка обновления ссылки:
<old-hash> <new-hash> refs/heads/<branch> - Отправка данных pack-файла: отправьте pack-файл с объектами, нужными удалённому репозиторию
- Проверка результата: сервер сообщает об успехе или сбое для каждой ссылки
Хук pre-push выполняется до отправки обновления, если хуки включены.
// 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:
// 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)
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)
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>
Обработка ошибок
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:
[remote "origin"]
url = https://github.com/user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
Доступ через конфигурацию API:
$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 запланировано как оптимизация.