Zum Inhalt springen

Klonen, abrufen, übertragen

Pitmaster unterstützt die drei grundlegenden git-Netzwerkoperationen: Clone (erstmaliger Download), Fetch (inkrementeller Download) und Push (Upload). SSH-URLs (ssh://..., user@host:path) verwenden den Transport SSH; andere URLs nutzen Smart-HTTP. Wenn der Smart-Austausch fehlschlägt, greifen Clone und Fetch auf Dumb-HTTP zurück (siehe Protokolle).

Alle drei starten Netzwerktransporte und werfen daher RuntimeException, wenn ein Handle mit ['processes' => false] oder Pitmaster::processFreeOptions() geöffnet wurde.

Klonen

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

Das neue Repository verwendet immer SHA-1. Schlägt das Klonen fehl und das Zielverzeichnis existierte vorher nicht, wird das teilweise erstellte Verzeichnis entfernt.

Ablauf des Klonens

  1. Initialisieren: Erstelle ein neues, leeres Repository am Zielpfad
  2. Remote konfigurieren: Speichere URL als remote.origin.url und richte die Fetch-Refspec ein
  3. Refs ermitteln: GET /info/refs?service=git-upload-pack, um herauszufinden, was auf dem Server vorhanden ist
  4. Aushandeln: Sende want-Zeilen für alle Remote-Refs (nach Hash dedupliziert)
  5. Pack empfangen: Lade die Packdatei mit allen angeforderten Objekten herunter
  6. Pack schreiben: Speichere die Packdatei unter .git/objects/pack/
  7. Pack indizieren: Erzeuge die Datei .idx für schnelle Suchvorgänge
  8. Refs einrichten: Erstelle Remote-Tracking-Branches (refs/remotes/origin/*) und Tags
  9. HEAD setzen: Verweise mit HEAD auf den Standard-Branch (aus der symref=HEAD:...-Fähigkeit des Servers oder auf refs/heads/main, wenn der Server keine sendet) und setze branch.<name>.remote und branch.<name>.merge
  10. Checkout: Checke diesen Branch aus. Dabei werden der Arbeitsbaum und der Index geschrieben

Fehler beim abschließenden Checkout werden abgefangen und ignoriert. Daher kann das Klonen mit einem leeren oder unvollständigen Arbeitsbaum zurückkehren (#19).

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

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

Arbeitsbaum erstellen

Nach dem Empfang des Packs ruft Pitmaster für den Standard-Branch $repo->checkout() auf. Dadurch wird jede Datei im Baum des HEAD-Commits in das Arbeitsverzeichnis geschrieben und der Index aus den Baumeinträgen erstellt, sodass status() einen sauberen Arbeitsbaum meldet.

Abrufen

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

Ablauf des Abrufens

  1. Remote-URL lesen: aus .git/config (remote.<name>.url)
  2. Refs ermitteln: Ermittle den aktuellen Stand aller Remote-Refs
  3. Wants und Haves aushandeln:
    • Want: Remote-Refs, auf die die remote.<name>.fetch-Refspecs zutreffen (Standard: +refs/heads/*:refs/remotes/<name>/*) und deren Objekte lokal fehlen (bei angegebener Tiefe alle)
    • Have: jeder lokale Ref-Tip
  4. Pack empfangen: Lade nur die neuen Objekte herunter
  5. Pack schreiben: Speichere es unter .git/objects/pack/ und erstelle seinen Index
  6. Tracking-Refs aktualisieren: Aktualisiere refs/remotes/<remote>/<branch> für jeden passenden Remote-Branch und schreibe alle angekündigten Tag-Refs. Tag-Objekte werden nicht separat angefordert. Ein neuer annotierter Tag kann daher als Ref auf ein nicht heruntergeladenes Objekt geschrieben werden (#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() sucht Namen als refs/heads/<name>, refs/tags/<name> und als den Namen selbst. Daher wird die Kurzform origin/main nicht gefunden.

Pitmaster fordert Thin Packs an, die es noch nicht indizieren kann (#16). Antwortet ein Server auf einen inkrementellen Abruf mit einem Thin Pack, wirft fetch() PackParseException.

Bereits aktuell

Wenn alle Objekte, auf die die Remote-Refs verweisen, bereits lokal vorhanden sind, wird kein Pack angefordert und fetch() kehrt nach dem Ermitteln der Refs zurück. In diesem Fall werden auch die Tracking-Refs nicht aktualisiert, selbst wenn sich die Remote-Branches auf Commits verschoben haben, die bereits lokal vorhanden sind (#23).

Übertragen

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

Ablauf der Übertragung

  1. Remote-URL lesen: aus .git/config
  2. Zu übertragende Änderungen bestimmen: Löse den lokalen Branch in seinen Commit-Hash auf
  3. Remote-Refs ermitteln: Ermittle den aktuellen Stand des Remote-Branches, um den alten Hash zu berechnen
  4. Fast-Forward prüfen: Ist der Remote-Branch kein Vorfahr des lokalen Branches, wirf RuntimeException("Push rejected: non-fast-forward update to <branch>"), bevor etwas gesendet wird
  5. Ref aktualisieren: <old-hash> <new-hash> refs/heads/<branch>
  6. Packdaten senden: Pack mit den Objekten, die der Remote benötigt
  7. Ergebnis prüfen: Der Server meldet Erfolg oder Fehlschlag für jeden Ref

Der Hook pre-push wird ausgeführt, bevor die Aktualisierung gesendet wird, sofern Hooks nicht deaktiviert sind.

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

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

Voraussetzungen für die Übertragung

  • Der aktuelle Branch muss lokal vorhanden sein
  • Ein Remote muss mit einem URL konfiguriert sein
  • Der Remote muss die Übertragung akzeptieren (Authentifizierung, Berechtigungen, Richtlinien für Nicht-Fast-Forward-Updates)

Erzwungene Übertragung

Pitmaster sendet den alten Hash aus der aktuellen Ref-Ankündigung des Remote, und push() weist Nicht-Fast-Forward-Updates lokal zurück. Um einen Remote-Branch zu überschreiben, verwende 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 (Interna des Abrufs)

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

Die Want/Have-Aushandlung erstellt eine pkt-line-Anfrage:

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

Der Server antwortet mit Bestätigungen und anschließend den Packdaten (möglicherweise in Side-Band verpackt).

ReceivePackClient (Interna der Übertragung)

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

Das Format der Übertragungsanfrage:

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

Fehlerbehandlung

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() kann auch ein einfaches RuntimeException (unbekannter Remote, Handle ohne Prozess) oder PackParseException (ein Pack, das nicht indiziert werden kann) werfen. ProtocolException und PackParseException erweitern beide \RuntimeException, sodass das Abfangen von \RuntimeException alle abdeckt.

Remote-Konfiguration

Remotes werden in .git/config gespeichert:

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

Zugriff über die Konfiguration 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');

Packdaten für die Übertragung

Derzeit sammelt Pitmaster alle Objekte, die von den übertragenen Commits aus erreichbar, aber von den aktuellen Refs des Remote aus nicht erreichbar sind, und schreibt sie als Basisobjekte (ohne Delta-Komprimierung) in ein Pack. Das ist funktional korrekt, erzeugt aber größere Packs als git push. Delta-Komprimierung beim Schreiben von Packs ist als Optimierung geplant.