Für Software-Projekte werden häufiger mehrere Repositorys benötigt, in denen getrennt und zusammen voneinander entwickelt wird. Dies trifft insbesondere dann zu, wenn etwa eine Bibliothek in mehreren Projekten benötigt wird, aber unabhängig voneinander in den anderen Projekten genutzt wird. Hier ergibt es Sinn, ein separates Git-Repository für die Bibliothek zu nutzen und dieses in die anderen Projekte einzubinden.
git submodule
Eine Möglichkeit solche Unter-Repositorys einzubinden ist die Nutzung von
git submodule
. Diejenigen die es schon nutzen, dürften wissen, dass die Arbeit
mit Submodulen häufig sehr anstrengend und mühselig ist – und das aus verschiedenen
UX-Gründen.
Submodule müssen in einem Git-Repository mit git submodule add $REPO_URL
zunächst
hinzugefügt werden. Dadurch wird im entsprechenden Haupt-Repository die Datei .gitmodules
angelegt, die direkt zum Staging-Bereich hinzugefügt wird. Nach einem Commit ist
das Submodule korrekt eingebunden. Wenn ihr das Log anschaut, dann seht ihr das Log des
Submodules nicht. Wenn ihr in das Unterverzeichnis rein wechselt und euch von dort das Log
anschaut, dann seht ihr das Log des Submodule-Repositorys. Sobald ihr nun Commits im Submodule
macht, müsst ihr von dort aus die Commits pushen und zurück in das Haupt-Repository wechseln.
Von dort muss man dann erneut ein Commit tätigen, um das die Änderungen aus dem Submodule
im Haupt-Repository ebenfalls korrekt verwenden zu können. Eine Änderung im Submodule führt also
zu mindestens zwei Commits: ein Commit im Submodule und ein Commit im Haupt-Repository.
Komplizierter ist das Verhalten, wenn jemand das Haupt-Repository neuklont. Beim Klonen
muss ein rekursiver Clone mit git clone --recursive
notwendig oder ihr klont „normal“
und führt dann git submodule init
in dem jeweiligen Submodule Verzeichnis aus. Wenn ihr nun
noch eine Änderung machen wollt, dann müsst ihr im Submodule-Repository zunächst den richtigen Branch
auschecken. Letzteres ist häufig verwirrend, weil man das überhaupt nicht gewohnt ist. Die übrigen
Schritte, die zuvor erklärt wurden, kommen dann nochmal obendrauf.
Wer jetzt denkt „Hä?“ oder es ziemlich umständlich findet, denen kann ich jetzt zu Recht sagen: Stimmt! Angenehm ist was anderes. Ganz allgemein lohnen sich Submodule besonders dann, wenn die Versionen in Submodulen selten aktualisiert werden müssen und es demnach keine hohe Entwicklungsaktivität stattfindet.
git subtree
Eine Alternative zu git submodule
ist git subtree
. Es ist zwar auch nicht die perfekte
Lösung, bietet aber einige Vor- und Nachteile im Vergleich zu git submodule
.
In produktiven Umgebungen habe ich noch nicht mit git subtree
gearbeitet, sondern habe es
für diesen Blogpost erst angeschaut. In meinem Git-Repository für meinen Blog, den ihr gerade
lest, hatte ich bis gerade eben auch ein Repository als Submodule eingebunden, was dem
Theme der Webseite entspricht.
Im Folgenden gehe ich direkt mal in die Praxis und zeige, wie git subtree
an einem praktischen Beispiel
verwendet werden kann.
Zu Beginn muss das Repository als weiteres Remote-Repository hinzugefügt werden. In der Regel
besitzt man Remote-Repositorys origin
und vielleicht upstream
. In dem Fall hat man
zwei Remote-Repositorys, um nach origin
seine eigenen Änderungen pushen zu dürfen (bei einem Fork etwa)
und upstream
, was dem Upstream-Repository des Projektes entspricht.
Das Remote-Repository muss wie folgt angelegt werden:
$ git remote add -f $REMOTE_NAME $REPOSITORY_URL
Der Befehl muss, wie zu sehen ist, mit -f
aufgerufen werden. -f
ist die kurze Form von --force
.
Aber warum --force
? Das Remote-Repository, was als Subtree hinzugefügt werden soll, hat keine
gemeinsame Historie, da die Historie bisher komplett getrennt war. Es ist schlicht kein verwandtes Repository.
In der Praxis sah es bei mir dann so aus:
$ git remote add -f AllinOne git@git.svij.org:svij/hugo_allinone_theme.git
Aktualisiere AllinOne
warning: keine gemeinsamen Commits
remote: Enumerating objects: 968, done.
remote: Counting objects: 100% (968/968), done.
remote: Compressing objects: 100% (574/574), done.
remote: Total 968 (delta 362), reused 947 (delta 351)
Empfange Objekte: 100% (968/968), 24.54 MiB | 11.15 MiB/s, Fertig.
Löse Unterschiede auf: 100% (362/362), Fertig.
Von git.svij.org:svij/hugo_allinone_theme
* [neuer Branch] master -> AllinOne/master
* [neues Tag] v1.0 -> v1.0
* [neues Tag] v1.1 -> v1.1
* [neues Tag] v1.2 -> v1.2
* [neues Tag] v1.3 -> v1.3
* [neues Tag] v1.4 -> v1.4
Wie ihr sehen könnt, erfolgt eine Warnung, dass es keine gemeinsamen Commits gibt. Davon
abgesehen wird das Remote-Repository wie jedes andere Remote-Repository auch gefetcht, also
die Objekte heruntergeladen. Bis zu diesem Zeitpunkt haben wir noch nichts spezifisches für
git subtree
gemacht, das kommt aber jetzt.
Das Hinzufügen mit git subtree
benötigt einige Parameter:
$ git subtree add --prefix $PATH_IN_REPO $REMOTE_NAME $BRANCH --squash
Wie ihr seht, ruft man git subtree add
mit dem Parameter --prefix
auf. Hier kann auch die
Kurzform -P
genutzt werden. Der Prefix ist der Pfad im Repository, wo der Subtree landen
soll. Zudem muss noch der Name des zuvor hinzugefügten Remotes angegeben werden, sowie der Branch.
In diesem Fall habe ich auch noch --squash
hinzugefügt, damit die bisherigen Historie des
Subtree-Repositorys nicht im Haupt-Repository landen soll, sondern nur als einzigen Commit
in der Historie der Haupt-Repositorys erscheint.
In meinem Fall sah das Ganze in der Praxis dann so aus:
$ git subtree add --prefix themes/AllinOne AllinOne master --squash
git fetch AllinOne master
Von git.svij.org:svij/hugo_allinone_theme
* branch master -> FETCH_HEAD
Added dir 'themes/AllinOne'
Wenn wir uns aber nun die Historie ansehen, sehen wir zwei neue Commits:
$ git log -2 --oneline
e82400c (HEAD -> master) Merge commit '59be897c12c39412755cd93996d85590ef53fdc9' as 'themes/AllinOne'
59be897 Squashed 'themes/AllinOne/' content from commit 88c7956
In dem älteren Commit wurde das angesprochene Squashing des Repositorys durchgeführt, und in dem darauffolgenden Commit ist ein Merge durchgeführt worden. Im konkreten Fall wurde ein Merge von einem Branch durchgeführt, der nicht denselben Ursprung hat.
Falls sich im Subtree-Repository etwas unabhängig vom Haupt-Repository verändert, dann können die Änderungen wie folgt heruntergeladen werden:
$ git fetch AllinOne master
Von git.svij.org:svij/hugo_allinone_theme
* branch master -> FETCH_HEAD
Diese sind dann aber noch nicht (!) im Haupt-Repository gemergt. Dazu muss mit folgendem Befehl durchgeführt werden:
$ git subtree pull --prefix themes/AllinOne AllinOne master --squash
Der Parameter sind äquivalent zu den vorherigen Befehlen. Auch hier muss man die vielen Parameter mit angeben.
Anders sieht es allerdings aus, wenn man einen Commit im Haupt-Repository macht, wo
der Inhalt des Subtree-Repositorys angefasst wird. In meinem Beispiel will ich etwa
das Verzeichnis themes/AllinOne/exampleSite
aus dem Haupt-Repository und dem Subtree-Repository
entfernen. Die ersten Schritte sind soweit ganz normal:
$ rm -rf themes/AllinOne/exampleSite
$ git add themes/AllinOne
$ git commit -m "Remove exampleSite from AllinOne"
$ git push origin master
Mit diesem Befehl wurde im Haupt-Repository das Verzeichnis entfernt, ein Commit erstellt und gepusht. Dass es sich um ein Subtree-Repository handelt, ist von der Handhabung her nicht zu erkennen.
Was jetzt noch fehlt, ist das Pushen des Commits in das Subtree-Repository. Das funktioniert ähnlich wie auch beim Pull:
$ git subtree push --prefix=themes/AllinOne AllinOne master
Wenn man nun in die Historie des Subtree-Repositorys schaut, sieht man genau einen neuen
Commit mit der Commit-Message Remove exampleSite from AllinOne
.
Und das war es auch schon. Theoretisch und auch praktisch ist es auch möglich das ganze
ohne das git subtree
Kommando zu erledigen, was die Handhabung der Subtree-Funktion
allerdings nicht erleichtert.
Fazit
Mit git subtree
hat man einige Vor- und Nachteile im Vergleich zu git submodule
. Die
perfekte Lösung ist es auch nicht, da man zwangsläufig ein paar neue Befehle lernen muss
und nicht vergessen darf, damit in beiden Repositorys die Änderungen landen.
Welche Vorteile hat also git subtree
im Vergleich zu git submodule
?:
- Bei bestehenden Repositorys mit Subtree ist kein
git clone --recursive
notwendig. - Der Workflow um Änderungen im Haupt-Repositorys zu veröffentlichen ist vergleichsweise einfach.
- Nutzer von Repositorys mit Subtree müssen nicht wesentlich was Neues lernen.
Die Nachteile sind:
- Ein neuer Befehl
subtree
mit zahlreichen Parametern wird benötigt. - Das Veröffentlichen von Änderungen im Subtree-Repository kann vergessen werden.
Es gibt sicherlich noch den ein oder anderen Vor- und Nachteil in der Praxis, der mir
nicht bekannt ist. Da ich es bisher nicht produktiv eingesetzt habe, kann ich dazu noch
nicht so viel sagen. Es ist jedenfalls auf Anhieb deutlich angenehmer zu nutzen, als
git submodule
und das ist dann ja schon mal etwas.