Перейти до вмісту

Клонування, отримання, надсилання

Pitmaster підтримує три основні мережеві операції git: клонування (початкове завантаження), отримання (інкрементне завантаження) та надсилання (вивантаження). URL-адреси SSH (ssh://..., user@host:path) використовують транспорт SSH; інші URL-адреси використовують розумний HTTP, а клонування та отримання переходять до простого HTTP, якщо розумний обмін даними не вдається (див. Протоколи).

Усі три операції запускають мережеві транспорти, тому для дескриптора, відкритого з ['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 і налаштувати специфікацію отримання посилань
  3. Виявлення посилань: виконати GET /info/refs?service=git-upload-pack, щоб дізнатися, що є на сервері
  4. Узгодження: надіслати рядки want для всіх віддалених посилань (без дублікатів за хешем)
  5. Отримання пакета: завантажити файл пакета з усіма запитаними об’єктами
  6. Запис пакета: зберегти файл пакета в .git/objects/pack/
  7. Індексація пакета: створити файл .idx для швидкого пошуку
  8. Налаштування посилань: створити гілки для відстеження віддалених гілок (refs/remotes/origin/*) і теги
  9. Налаштування HEAD: спрямувати HEAD на типову гілку (з можливості сервера symref=HEAD:... або на refs/heads/main, якщо сервер її не надішле), а також налаштувати branch.<name>.remote і branch.<name>.merge
  10. Вибір гілки: вибрати цю гілку, щоб створити робоче дерево та індекс

Помилки під час завершального вибору гілки перехоплюються та ігноруються, тому клонування може завершитися з порожнім або частково створеним робочим деревом (#19).

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

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

Створення робочого дерева

Після отримання пакета 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. Узгодження потрібних і наявних об’єктів:
    • Потрібні: віддалені посилання, що відповідають специфікаціям remote.<name>.fetch (типово +refs/heads/*:refs/remotes/<name>/*), об’єкти яких відсутні локально (усі вони, якщо вказано глибину)
    • Наявні: усі локальні вершини посилань
  4. Отримання пакета: завантажити лише нові об’єкти
  5. Запис пакета: зберегти в .git/objects/pack/ і створити його індекс
  6. Оновлення посилань відстеження: оновити refs/remotes/<remote>/<branch> для кожної відповідної віддаленої гілки та записати кожне оголошене посилання на тег. Об’єкти тегів окремо не запитуються, тому новий анотований тег може бути записаний як посилання на незавантажений об’єкт (#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 запитує тонкі пакети, які поки не може індексувати (#16). Якщо сервер відповідає на інкрементне отримання тонким пакетом, fetch() викликає PackParseException.

Уже актуально

Якщо всі об’єкти, на які вказують віддалені посилання, вже є локально, пакет не запитується, а fetch() повертається після виявлення посилань. У цьому разі посилання відстеження також не оновлюються, навіть якщо віддалені гілки перемістилися на коміти, які вже є у вас (#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. Надсилання даних пакета: пакет з об’єктами, потрібними віддаленому репозиторію
  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() відхиляє локальні оновлення, що не є перемотуванням уперед. Щоб перезаписати гілку віддаленого репозиторію, використовуйте 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

Сервер відповідає підтвердженнями, а потім даними пакета (можливо, загорнутими в 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 (пакет, який неможливо індексувати). І 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');

Дані пакета для push

Наразі Pitmaster збирає об’єкти, досяжні з комітів, які надсилаються, але недосяжні з поточних посилань віддаленого репозиторію, і записує їх у пакет як базові об’єкти (без дельта-стиснення). Функціонально це правильно, але пакети виходять більшими, ніж у git push. Дельта-стиснення під час запису пакетів заплановано як оптимізацію.