Aller au contenu

Cloner, récupérer, envoyer

Pitmaster prend en charge les trois opérations réseau fondamentales de git : le clonage (téléchargement initial), la récupération (téléchargement incrémentiel) et l’envoi (téléversement). Les URL SSH (ssh://..., user@host:path) utilisent le transport SSH ; les autres URL utilisent HTTP intelligent, et le clonage et la récupération basculent sur HTTP simple lorsque l’échange intelligent échoue (voir Protocoles).

Ces trois opérations démarrent des transports réseau ; elles déclenchent donc RuntimeException sur un handle ouvert avec ['processes' => false] ou Pitmaster::processFreeOptions().

Clonage

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

Le nouveau dépôt utilise toujours SHA-1. Si le clonage échoue et que la destination n’existait pas auparavant, le répertoire partiellement créé est supprimé.

Déroulement du clonage

  1. Initialiser : créer un nouveau dépôt vide au chemin de destination
  2. Configurer le dépôt distant : enregistrer URL sous le nom remote.origin.url et configurer le refspec de récupération
  3. Découvrir les références : GET /info/refs?service=git-upload-pack pour connaître le contenu du serveur
  4. Négocier : envoyer les lignes want pour toutes les références distantes (hachages dédupliqués)
  5. Recevoir le pack : télécharger le fichier pack contenant tous les objets demandés
  6. Écrire le pack : enregistrer le fichier pack dans .git/objects/pack/
  7. Indexer le pack : générer le fichier .idx pour accélérer les recherches
  8. Configurer les références : créer des branches de suivi distant (refs/remotes/origin/*) et des tags
  9. Définir HEAD : pointer HEAD vers la branche par défaut (indiquée par la capacité symref=HEAD:... du serveur, ou refs/heads/main si le serveur n’envoie rien), puis définir branch.<name>.remote et branch.<name>.merge
  10. Extraire : extraire cette branche, ce qui écrit l’arbre de travail et l’index

Les erreurs survenant lors de l’extraction finale sont interceptées et ignorées ; le clonage peut donc renvoyer un arbre de travail vide ou partiel (#19).

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

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

Matérialisation de l’arbre de travail

Après avoir reçu le pack, Pitmaster appelle $repo->checkout() pour la branche par défaut. Cette opération écrit dans le répertoire de travail tous les fichiers de l’arbre du commit HEAD et construit l’index à partir des entrées de l’arbre, afin que status() indique un arbre de travail propre.

Récupération

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

Déroulement de la récupération

  1. Lire URL du dépôt distant : depuis .git/config (remote.<name>.url)
  2. Découvrir les références : obtenir l’état actuel de toutes les références distantes
  3. Négocier les objets voulus et disponibles :
    • Voulus : références distantes correspondant aux refspecs remote.<name>.fetch (+refs/heads/*:refs/remotes/<name>/* par défaut) dont les objets sont absents en local (toutes si une profondeur est indiquée)
    • Disponibles : pointe de chaque référence locale
  4. Recevoir le pack : télécharger uniquement les nouveaux objets
  5. Écrire le pack : l’enregistrer dans .git/objects/pack/ et créer son index
  6. Mettre à jour les références de suivi : mettre à jour refs/remotes/<remote>/<branch> pour chaque branche distante correspondante et écrire toutes les références de tags annoncées. Les objets de tags ne sont pas demandés séparément ; un nouveau tag annoté peut donc être écrit comme référence vers un objet qui n’a pas été téléchargé (#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() recherche les noms sous les formes refs/heads/<name>, refs/tags/<name> et nom lui-même ; la forme courte origin/main est donc introuvable.

Pitmaster demande des packs allégés, qu’il ne peut pas encore indexer (#16). Si un serveur répond à une récupération incrémentielle par un pack allégé, fetch() déclenche PackParseException.

Déjà à jour

Si tous les objets pointés par les références distantes existent déjà en local, aucun pack n’est demandé et fetch() revient après la découverte des références. Dans ce cas, les références de suivi ne sont pas mises à jour non plus, même si les branches distantes ont avancé vers des commits déjà présents localement (#23).

Envoi

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

Déroulement de l’envoi

  1. Lire URL du dépôt distant : depuis .git/config
  2. Déterminer le contenu à envoyer : résoudre la branche locale en son hachage de commit
  3. Découvrir les références distantes : obtenir l’état actuel de la branche distante (pour calculer l’ancien hachage)
  4. Vérifier l’avance rapide : si la branche distante n’est pas un ancêtre de la branche locale, déclencher RuntimeException("Push rejected: non-fast-forward update to <branch>") avant tout envoi
  5. Envoyer la mise à jour de référence : <old-hash> <new-hash> refs/heads/<branch>
  6. Envoyer les données du pack : pack contenant les objets nécessaires au dépôt distant
  7. Vérifier le résultat : le serveur renvoie le succès ou l’échec pour chaque référence

Le hook pre-push s’exécute avant l’envoi de la mise à jour, sauf si les hooks sont désactivés.

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

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

Conditions requises pour l’envoi

  • La branche actuelle doit exister en local
  • Un dépôt distant doit être configuré avec un URL
  • Le dépôt distant doit accepter l’envoi (authentification, autorisations, règles d’avance non rapide)

Envoi forcé

Pitmaster envoie l’ancien hachage issu de l’annonce en direct des références distantes, et push() refuse localement les mises à jour sans avance rapide. Pour écraser une branche distante, utilisez 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 (fonctionnement interne de la récupération)

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

La négociation des objets voulus et disponibles construit une requête pkt-line :

Code
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

Le serveur répond avec des accusés de réception, puis les données du pack (éventuellement encapsulées dans side-band).

ReceivePackClient (fonctionnement interne de l’envoi)

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

Format de la requête d’envoi :

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

Gestion des erreurs

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() peut également déclencher une simple RuntimeException (dépôt distant inconnu, handle sans processus) ou PackParseException (pack impossible à indexer). ProtocolException et PackParseException héritent toutes deux de \RuntimeException ; intercepter \RuntimeException les couvre donc toutes.

Configuration des dépôts distants

Les dépôts distants sont stockés dans .git/config :

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

Accédez-y via le API de configuration :

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

Données pack pour l’envoi

Actuellement, Pitmaster collecte les objets accessibles depuis les commits envoyés, mais pas depuis les références actuelles du dépôt distant, puis les écrit dans un pack comme objets de base (sans compression delta). Cette méthode est correcte, mais produit des packs plus volumineux que git push. La compression delta lors de l’écriture des packs est prévue comme amélioration.