Vai al contenuto

Clone, fetch, push

Pitmaster supporta le tre operazioni di rete fondamentali di git: clone (download iniziale), fetch (download incrementale) e push (caricamento). Gli URL SSH (ssh://..., user@host:path) usano il trasporto SSH; gli altri URL usano HTTP intelligente, mentre clone e fetch ripiegano su HTTP semplice quando lo scambio intelligente fallisce (vedere Protocolli).

Tutte e tre avviano trasporti di rete, quindi generano RuntimeException con un handle aperto con ['processes' => false] o Pitmaster::processFreeOptions().

Clonare

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

Il nuovo repository usa sempre SHA-1. Se la clonazione fallisce e la destinazione non esisteva già, la directory creata parzialmente viene rimossa.

Procedura di clonazione

  1. Inizializzazione: crea un nuovo repository vuoto nel percorso di destinazione
  2. Configurazione del remoto: salva URL come remote.origin.url e configura la refspec di fetch
  3. Individuazione dei riferimenti: usa GET /info/refs?service=git-upload-pack per scoprire cosa contiene il server
  4. Negoziazione: invia righe want per tutti i riferimenti remoti (deduplicate per hash)
  5. Ricezione del pack: scarica il file pack contenente tutti gli oggetti richiesti
  6. Scrittura del pack: salva il file pack in .git/objects/pack/
  7. Indicizzazione del pack: genera il file .idx per ricerche rapide
  8. Configurazione dei riferimenti: crea i rami di tracciamento remoto (refs/remotes/origin/*) e i tag
  9. Impostazione di HEAD: punta HEAD al ramo predefinito (dalla funzionalità symref=HEAD:... del server, oppure refs/heads/main se il server non ne invia alcuna) e imposta branch.<name>.remote e branch.<name>.merge
  10. Checkout: esegue il checkout di quel ramo, scrivendo l'albero di lavoro e l'indice

Gli errori durante il checkout finale vengono intercettati e ignorati, quindi la clonazione può terminare con un albero di lavoro vuoto o parziale (#19).

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

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

Materializzazione dell'albero di lavoro

Dopo aver ricevuto il pack, Pitmaster chiama $repo->checkout() per il ramo predefinito. Questo scrive nella directory di lavoro ogni file contenuto nell'albero del commit HEAD e crea l'indice dalle voci dell'albero, così status() segnala che l'albero di lavoro è pulito.

Recuperare gli aggiornamenti

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

Procedura di fetch

  1. Leggere il remoto URL: da .git/config (remote.<name>.url)
  2. Individuare i riferimenti: ottenere lo stato corrente di tutti i riferimenti remoti
  3. Negoziare ciò che serve e ciò che si possiede:
    • Da ottenere: riferimenti remoti corrispondenti alle refspec remote.<name>.fetch (predefinite +refs/heads/*:refs/remotes/<name>/*) i cui oggetti mancano localmente (tutti, se viene specificata una profondità)
    • Già posseduti: punte di tutti i riferimenti locali
  4. Ricezione del pack: scaricare solo i nuovi oggetti
  5. Scrittura del pack: salvare in .git/objects/pack/ e crearne l'indice
  6. Aggiornamento dei riferimenti di tracciamento: aggiornare refs/remotes/<remote>/<branch> per ogni ramo remoto corrispondente e scrivere ogni riferimento ai tag annunciato. Gli oggetti tag non vengono richiesti separatamente, quindi un nuovo tag annotato può essere scritto come riferimento a un oggetto non scaricato (#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() cerca i nomi come refs/heads/<name>, refs/tags/<name> e come nome stesso, quindi non trova la forma abbreviata origin/main.

Pitmaster richiede pack sottili, che ancora non è in grado di indicizzare (#16). Se un server risponde a un fetch incrementale con un pack sottile, fetch() genera PackParseException.

Già aggiornato

Se tutti gli oggetti a cui puntano i riferimenti remoti esistono già in locale, non viene richiesto alcun pack e fetch() termina dopo aver individuato i riferimenti. In tal caso, i riferimenti di tracciamento non vengono aggiornati neanche se i rami remoti sono passati a commit già presenti in locale (#23).

Inviare le modifiche

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

Procedura di push

  1. Leggere il remoto URL: da .git/config
  2. Determinare cosa inviare: risolvere il ramo locale nel relativo hash di commit
  3. Individuare i riferimenti remoti: ottenere lo stato corrente del ramo remoto (per calcolare il vecchio hash)
  4. Verificare l'avanzamento rapido: se il ramo remoto non è un antenato del ramo locale, generare RuntimeException("Push rejected: non-fast-forward update to <branch>") prima di inviare qualsiasi dato
  5. Inviare l'aggiornamento del riferimento: <old-hash> <new-hash> refs/heads/<branch>
  6. Inviare i dati del pack: il pack contenente gli oggetti necessari al remoto
  7. Verificare il risultato: il server risponde indicando successo o fallimento per ciascun riferimento

L'hook pre-push viene eseguito prima dell'invio dell'aggiornamento, a meno che gli hook non siano disabilitati.

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

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

Requisiti per il push

  • Il ramo corrente deve esistere in locale
  • Deve essere configurato un remoto con un URL
  • Il remoto deve accettare il push (autenticazione, autorizzazioni, criteri per gli aggiornamenti non fast-forward)

Push forzato

Pitmaster invia l'hash precedente dall'annuncio live dei riferimenti del remote, mentre push() rifiuta localmente gli aggiornamenti non fast-forward. Per sovrascrivere un branch remoto, usate 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 (interni del 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);

La negoziazione want/have crea una richiesta pkt-line:

Codice
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

Il server risponde con gli acknowledgment e poi i dati del pack (eventualmente incapsulati side-band).

ReceivePackClient (interni del 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);

Formato della richiesta push:

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

Gestione degli errori

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() può anche generare un semplice RuntimeException (remote sconosciuto, handle senza processi) o PackParseException (un pack che non può essere indicizzato). Sia ProtocolException sia PackParseException estendono \RuntimeException, quindi intercettando \RuntimeException li coprite tutti.

Configurazione dei remote

I remote sono memorizzati in .git/config:

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

Accesso tramite la configurazione 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');

Dati del pack per il push

Attualmente, Pitmaster raccoglie gli oggetti raggiungibili dai commit inviati che non sono raggiungibili dai riferimenti correnti del remote e li scrive in un pack come oggetti di base (senza compressione delta). È funzionalmente corretto, ma produce pack più grandi rispetto a git push. La compressione delta durante la scrittura dei pack è un'ottimizzazione prevista.