Das Symptom
Ein Projekt wird frisch geklont, der Build startet und bricht ab: eine Headerdatei fehlt, ein Theme-Ordner ist leer, ein Node-Paket unter vendor/ enthält nichts außer dem Verzeichnis selbst. Im Webinterface des Repositorys ist der Ordner dagegen sichtbar, mit einem kleinen Pfeil und einem Commit-Hash daneben. Der Kollege, bei dem alles läuft, hat das Projekt vor zwei Jahren geklont und weiß nicht mehr, was er damals getan hat.
Die Reflexe sind bekannt: den fehlenden Ordner aus einem Zip-Download nachfüllen, die Dateien aus dem Rechner des Kollegen kopieren oder das Submodul-Repository separat klonen und von Hand an die richtige Stelle schieben. Alles davon baut einmal und bricht beim nächsten Update.
Die „Layer 8“-Wahrheit
Der leere Ordner ist ein Submodul, und ein Submodul ist kein Inhalt, sondern ein Verweis. Das Hauptprojekt speichert in .gitmodules die Adresse des fremden Repositorys und im eigenen Baum den Commit-Hash, der dort gelten soll. Mehr nicht. Ein gewöhnliches git clone holt das Hauptprojekt und lässt die Verweise, wo sie sind. Git tut hier exakt, was es soll; es wurde nur nicht gebeten, mehr zu tun.
Die Dokumentation von git clone sagt, was die Bitte heißt. Die Option –recurse-submodules bewirkt: „After the clone is created, initialize and clone submodules within based on the provided …“ – gemeint ist ein Pfadmuster, das Git pathspec nennt. Ohne Pfadmuster gilt: „all submodules are initialized and cloned“. Und die Seite verrät auch, was das intern ist, nämlich dasselbe wie ein git submodule update –init –recursive unmittelbar nach dem Klonen. Genau diesen Befehl führt man also aus, wenn der Klon schon steht. Die Dokumentation von git submodule beschreibt update als den Schritt, der die registrierten Submodule auf den vom Hauptprojekt erwarteten Stand bringt, „by cloning missing submodules, fetching missing commits in submodules and updating the working tree of the submodules“. Die Option –init erspart den sonst getrennten Befehl git submodule init, der die Adressen aus .gitmodules nach .git/config überträgt.
Zweiter Stolperstein: Nach dem Aktualisieren steht das Submodul auf einem detached HEAD. Auch das ist beabsichtigt. Das Hauptprojekt zeichnet einen Commit auf, keinen Zweig, und die Dokumentation legt die Standardmethode fest: „If neither is given, a checkout is performed.“ Bei checkout wird der aufgezeichnete Commit im Submodul ohne Zweig ausgecheckt. Das garantiert, dass jeder Klon denselben Stand bekommt, unabhängig davon, was im Submodul-Repository inzwischen auf dem Hauptzweig passiert ist. Wer das Submodul weiterentwickeln will, legt dort einen Zweig an; wer nur bauen will, lässt es.
Dritter Stolperstein, meist Monate später: Das Submodul zieht auf einen anderen Server um. .gitmodules wird aktualisiert, aber die beim ersten init in .git/config kopierte Adresse bleibt stehen, und update läuft gegen die alte. Dafür gibt es git submodule sync, das laut Dokumentation die Konfiguration an .gitmodules angleicht, ausdrücklich für den Fall geänderter Adressen.
Was der Diagnose hilft, ist git submodule status. Die Dokumentation erklärt die Vorzeichen vor dem Hash: Minus für nicht initialisiert, Plus für einen ausgecheckten Commit, der nicht zu dem im Index des Hauptprojekts passt, U für Merge-Konflikte. Eine Liste voller Minuszeichen direkt nach dem Klonen ist kein Schaden, sondern der normale Ausgangszustand. Der Fehler liegt nicht in Git und nicht im Repository, sondern in der Annahme, dass ein Klon alles enthält, was im Webinterface zu sehen ist.