W3docs

Git Subtree

Einführung in Git Subtrees: Vor- und Nachteile sowie der Unterschied zwischen Subtree und Submodul.

Wie auf der vorherigen Seite erwähnt, ist Git Submodule für bestimmte Anwendungsfälle nützlich. Für die Verfolgung von Software-Abhängigkeiten innerhalb eines einzelnen Repositorys bevorzugen viele Entwickler stattdessen Git Subtree.

Diese Seite erklärt, was ein Git Subtree ist, wann man ihn einem Submodul vorzieht und wie man einen Subtree hinzufügt, aktualisiert und Änderungen daran zurück zum Upstream beiträgt. Außerdem wird das Rebasing eines Repositorys mit Subtrees sowie die am häufigsten verwendeten Befehlsoptionen behandelt.

Was ist Git Subtree

Git Subtree ist eine Alternative zu Git Submodule. Es ermöglicht, ein Repository als Unterverzeichnis in einem anderen zu verschachteln und dabei die History des eingebetteten („Sub"-)Projekts beizubehalten. Es ist eine der Möglichkeiten, die History von Software-Abhängigkeiten zu verfolgen.

Der wesentliche Unterschied zu Submodulen besteht darin, dass ein Subtree lediglich ein Verzeichnis ist. Im Gegensatz zu Submodulen benötigen Subtrees keine .gitmodules-Datei oder spezielle Gitlinks im Repository. Die Dateien liegen direkt im Working Tree, werden zusammen mit allem anderen committet und werden bei jedem clone, branch und merge automatisch mitgenommen. Jeder, der das Repository klont, erhält den Code der Abhängigkeit, ohne einen zusätzlichen Befehl ausführen zu müssen.

Unter der Haube ist git subtree ein Porcelain-Befehl (ein Wrapper) um standardmäßige Plumbing-Befehle wie git merge, git read-tree und git filter-branch/split. Es ist kein neues Speicherformat zu erlernen — nur ein paar neue Befehle.

Info

Bei Submodulen liefert ein Clone ein leeres Verzeichnis, bis git submodule update --init ausgeführt wird. Bei Subtrees sind die Dateien bereits vorhanden. Dieser eine Unterschied bestimmt die meisten der unten aufgeführten Vor- und Nachteile.

Subtree vs. Submodule

Beide Ansätze betten ein Repository in ein anderes ein, treffen dabei aber entgegengesetzte Kompromisse. Die Wahl richtet sich danach, wer das Repository nutzt und wie oft Code zum Upstream zurückfließt.

AspektGit SubtreeGit Submodule
Dateien nach cloneSofort vorhandenLeer bis submodule update --init
Zusätzliche MetadatenKeine.gitmodules + Gitlinks
Pinnt einen genauen Upstream-CommitNein (History wird eingemergt)Ja (speichert einen SHA)
Beiträge zurück zum UpstreamManuell (git subtree split + Push)Natürlich (Commit innerhalb des Submoduls)
Repository-GrößeGrößer (Sub-Projekt-History wird kopiert)Kleiner (nur ein Zeiger)
Lernkurve für NutzerKeineMuss Submodul-Befehle lernen

Eine grobe Faustregel: Bevorzuge Subtrees, wenn du eine Abhängigkeit hauptsächlich verwendest und jeder Clone einfach funktionieren soll; bevorzuge Submodule, wenn das Sub-Projekt aktiv weiterentwickelt wird und du einen genauen Upstream-Commit pinnen oder dorthin pushen musst.

Warum Git Subtree verwenden

Vorteile

  • Unterstützt ab Git 1.7.10 (der subtree-Befehl ist in Git selbst enthalten).
  • Einfacher Workflow für Personen, die dein Repo klonen — keine zusätzlichen Befehle zu erlernen.
  • Der Code des Sub-Projekts ist direkt nach dem Klonen des Super-Projekts vorhanden.
  • Es werden keine neuen Metadaten-Dateien hinzugefügt (z. B. .gitmodules).
  • Die Abhängigkeit kann direkt bearbeitet werden, ohne ein separates Repository auszuchecken.

Nachteile

  • Erfordert das Erlernen einer neuen Merge-Strategie und einiger subtree-spezifischer Befehle.
  • Das Beitragen von Code zum Upstream ist ein mehrstufiger Prozess (git subtree split, dann Push).
  • Der Code von Super-Projekt und Sub-Projekt ist im selben Repository vermischt, was die Größe erhöht und die History unübersichtlich machen kann.

Wie man einen Subtree hinzufügt

Angenommen, es gibt ein externes Projekt, und du möchtest es unter einem bestimmten Verzeichnis zu deinem Repository hinzufügen.

Um beispielsweise eine Vim-Erweiterung in ein Repository einzubinden, das dein Vim-Setup speichert, führe folgenden Befehl aus:

git subtree add --prefix .vim/bundle/example https://github.com/Example/vim-example.git master --squash

Die einzelnen Teile dieses Befehls:

  • --prefix .vim/bundle/example — das Verzeichnis, in dem das Sub-Projekt abgelegt wird. Diese Option ist für jeden subtree-Befehl obligatorisch.
  • https://github.com/Example/vim-example.git — das Quell-Repository (eine URL oder später ein benannter Remote).
  • master — der Branch (oder Commit/Tag), von dem gezogen wird.
  • --squash — fasst die vollständige History des Sub-Projekts in einem einzigen Commit zusammen, damit das Log nicht überfrachtet wird. Lass diese Option weg, wenn die vollständige Upstream-History eingemergt werden soll.

Mit --squash speichert Git den SHA-1 von master zu diesem Zeitpunkt als zukünftige Referenz und erzeugt zwei Commits — den zusammengefassten Import und den Merge:

commit 6d7054b3acea64e2e31f4d6fb2e3be12e5865e87
Merge: 87fa91e ef86deb
Author: Ann Smith<[email protected]m>
Date:   Tue Jun 10 13:37:03 2016 +0200
    Merge commit 'fe67ddf158faccff4082d78a25c45d8cd93e8ba8' as '.vim/bundle/example'
commit fe67ddf158faccff4082d78a25c45d8cd93e8ba8
Author: Ann Smith<[email protected]m>
Date:   Tue May 12 13:37:03 2015 +0200
    Squashed '.vim/bundle/example/' content from commit b999b09
    git-subtree-dir: .vim/bundle/example
    git-subtree-split: b999b09cd9d69f359fa5668e81b09dcfde455cca

Einen Subtree aktualisieren

Um den Unterordner auf die neueste Version des Child-Repositorys zu aktualisieren, führe einen subtree pull mit demselben Prefix und derselben Quelle aus:

git subtree pull --prefix .vim/bundle/example https://github.com/Example/vim-example.git master --squash

Damit wird der Upstream-Branch geholt und in dein Subtree-Verzeichnis gemergt, wobei ein neuer Merge-Commit erzeugt wird. Verwende immer dasselbe --prefix und dieselbe --squash/kein---squash-Entscheidung wie bei subtree add, sonst kann Git die Historien nicht korrekt abgleichen.

Beachte, dass git subtree die Commit-IDs des Sub-Projekts in den Commit-Message-Metadaten speichert, nicht als symbolische Referenzen. Um den Branch- oder Tag-Namen zu einem gespeicherten Commit zu finden, frage den Remote ab:

git ls-remote https://github.com/Example/vim-example.git | grep <commit-sha>

Ersetze <commit-sha> durch den tatsächlichen Commit-Hash aus der git-subtree-split:-Zeile deines Import-Commits.

Rebasing nach Git Subtree

Um ein Repository, das Subtrees enthält, zu rebasen, verwende den --interactive-Modus von git rebase:

git rebase --interactive HEAD~5

Im Editor kannst du die Subtree-Merge-Commits verwerfen oder squashen, dann speichern und folgendes ausführen:

git rebase --continue

Da Rebasing die History umschreibt, können die Subtree-Merge-Commits entfernt oder verändert werden. Nach einer solchen Umschreibung muss der Subtree in der Regel durch erneutes Ausführen von git subtree add oder git subtree pull neu eingerichtet werden. Beachte, dass die veränderte Commit-Struktur während des Rebasings auch Merge-Konflikte auslösen kann. Daher empfiehlt es sich, nur Branches zu rebasen, die noch nicht gepusht und geteilt wurden.

Häufige Optionen

OptionBeschreibung
-q, --quietUnterdrückt unnötige Ergebnismeldungen auf stderr.
-d, --debugGibt zusätzliche Debug-Meldungen auf stderr aus.
-P <prefix>, --prefix=<prefix>Definiert den Pfad im Repository zum Subtree, der bearbeitet werden soll. Für alle Befehle obligatorisch.
-m <message>, --message=<message>Legt <message> als Commit-Message für den Merge-Commit fest. Gültig für add, merge und pull.
--squashImportiert das Sub-Projekt als einzelnen Commit, anstatt seine vollständige History einzumergen.

Git Subtree ohne Remote-Tracking verwenden

Füge den Git Subtree an einem bestimmten Prefix-Ordner hinzu. Verwende das --squash-Flag, um die gesamte Subprojekt-History in deinem Haupt-Repository zu erhalten:

git subtree add --prefix .vim/bundle/vim-double-upon https://hostname.org/example/vim-plugins.git master --squash

Der Befehl führt einen Fetch durch und squasht die History. Die Ausgabe zeigt typischerweise den Fetch-Fortschritt gefolgt von der Bestätigung des Hinzufügens:

git fetch https://hostname.org/example/vim-plugins.git  master
warning: no common commits
remote: Counting objects: 325, done.
remote: Compressing objects: 100% (145/145), done.
remote: Total 325 (delta 101), reused 313 (delta 89)
Receiving objects: 100% (325/325), 61.47 KiB, done.
Resolving deltas: 100% (110/110), done.
From https://hostname.org/vim-plugins.git
* branch master -> FETCH_HEAD
Added dir '.vim/bundle/vim-double-upon'

Dadurch wird ein Merge-Commit erstellt, indem die gesamte History des Subprojekts in einen einzigen Commit zusammengefasst wird:

3bca0ad [4 minutes ago] (HEAD, stree) Merge commit 'fa2f5dc4f1b94356bca8a440c786a94f75dc0a45' as '.vim/bundle/vim-double-upon' [John Brown]
fa2f5dc [4 minutes ago] Squashed '.vim/bundle/vim-double-upon/' content from commit 13189ec [John Brown]

Um den Code des Plugins aus dem Upstream-Repository zu aktualisieren, führe einen git subtree pull durch:

git subtree pull --prefix .vim/bundle/vim-double-upon https://hostname.org/example/vim-plugins.git master --squash

Änderungen zum Upstream beitragen

Da der Code des Sub-Projekts in dein Repository eingemischt ist, kann er nicht einfach zurückgepusht werden. Du musst zunächst die Änderungen, die das Subtree-Verzeichnis betreffen, mit git subtree split in einen eigenständigen Branch extrahieren:

git subtree split --prefix .vim/bundle/vim-double-upon -b split-branch

Damit werden nur die Commits, die .vim/bundle/vim-double-upon betreffen, in einen neuen Branch (split-branch) umgeschrieben, dessen Pfade relativ zum Sub-Projekt-Root sind. Anschließend kannst du diesen Branch in das Upstream-Repository pushen:

git push https://hostname.org/example/vim-plugins.git split-branch:master

Diese einseitige Extraktion ist der Grund, warum „Beiträge zum Upstream" oben als Nachteil aufgeführt ist — es gibt keine eingebaute bidirektionale Synchronisierung.

Um die alltäglichen Befehle add/pull/push zu verkürzen, füge das Sub-Projekt als Remote hinzu.

Sub-Projekt als Remote hinzufügen

Die Registrierung der Quelle als benannter Remote verkürzt alle späteren Befehle — du referenzierst vim-double-upon statt der vollständigen URL. Das -f-Flag führt sofort einen Fetch durch:

git remote add -f vim-double-upon https://hostname.org/example/vim-plugins.git

Den Subtree hinzufügen:

git subtree add --prefix .vim/bundle/vim-double-upon vim-double-upon master --squash

Das Sub-Projekt so aktualisieren:

git fetch vim-double-upon master
git subtree pull --prefix .vim/bundle/vim-double-upon vim-double-upon master --squash

Zusammenfassung

Git Subtree ist eine Alternative zu Git Submodule. Während ein Submodul das Sub-Projekt als separaten Zeiger führt, der initialisiert werden muss, führt ein Subtree das Sub-Projekt als reguläres Verzeichnis, das in jedem Clone vorhanden ist. Verwende git subtree add, um eine Abhängigkeit zu importieren, git subtree pull, um sie zu aktualisieren, und git subtree split + Push, um Änderungen zum Upstream zu senden — eine native bidirektionale Synchronisierung gibt es nicht. Um mehr zu erfahren, erkunde Git Branch und Git Merge, die den Subtree-Befehlen unter der Haube zugrunde liegen.

Übung

Übung
Was sind die Merkmale und die Verwendung von Git Subtree?
Was sind die Merkmale und die Verwendung von Git Subtree?
Was this page helpful?