Mit Git und dem WordPress-SVN arbeiten

In den vergangenen Tagen habe ich mein erstes Plugin im WordPress Repository veröffentlicht. Für jemanden der wie ich schon seit Version 1.2 mit WordPress arbeitet, eigentlich ein recht später Zeitpunkt. Die Gründe hierfür sind jedoch weitreichend und sehr verschieden. Neben den sehr restriktiven bürokratischen Hürden war vor allem der Umstand das es sich beim WordPress Repository um ein SVN-Repository handelt ein Grund dort keinen Code einzureichen. Warum WordPress unbedingt ein SVN-Repo betreiben muss, erschließt sich mir nach wie vor nicht. Es würde reichen wenn sie Zip-Archive der Plugins hosten, alles was ein SVN-Repo an Möglichkeiten bietet, könnte man aus dem Zip-Archiv ableiten. Aber egal, sei’s drum.
Das allerdings nicht nur ich eine Abneigung gegen SVN habe, zeigte sich schnell in einer kleinen Diskussion die sich entwickelte als ich zu meinen Plugin auf Google+ einen Beitrag veröffentlichte. Kernpunkt der Diskussion war, wie man am besten Git und SVN unter einen Hut bekommt.
Die Schwierigkeit dabei ist gar nicht einmal beide unter einen Hut zu bekommen, dafür gibt es git svn, sondern auch noch die zusätzlichen Hürden zu überwinden die von WordPress aufgestellt wurden. So darf man z.B. nicht jeden Commit den man durchführt ins WP-Repo überführen, sondern lediglich wenn man eine neue Version veröffentlicht. Zudem muss man die neue Version taggen. git svn kann weder das eine noch das andere wirklich sauber durchführen. Nutzt man git svn, muss man vor jeden Commit ins SVN-Repo die Commit-History bereinigen (squashen), taggen muss man dann sogar von Hand da auch hier git svn nicht sehr sauber arbeitet. Es mag sein das git svn dies sehrwohl kann und ich es einfach nicht heraus bekommen habe wie man das sauber hin bekommt, da lasse ich mich gerne eines besseren belehren, jedoch hatte ich irgendwann auch einfach keine Lust mehr mich mit git svn auseinander zusetzen.

Ein paar Basics

Im Grunde genommen nutze ich immer zwei Git-Repos. Zum einen ein lokales Repo im Ordner in dem ich den Code entwickele. Zum anderen ein entferntes (remote) Repo in dem ich meine Arbeit speichere. Viele nutzen als Remote-Repo GitHub, BitBucket oder einen Firmenserver. Ich nutze aus verschiedenen Gründen einen Ordner in meiner Dropbox. Da im späteren Verlauf eine lokale Kopie des SVN-Repos benötigt wird, empfiehlt es sich neben den üblichen Remote-Repos (GitHub, BitBucket, usw.) ein Remote-Repo auf der eigenen Festplatte einzurichten. Das muss nicht zwingend in einer Dropbox sein, bietet sich jedoch an wenn man, so wie ich zum Beispiel, an mehreren Rechnern oder im Team an einem Plugin arbeitet.
Beispielhaft sieht die Ordnerstruktur nun so aus das /d/local/plugin-name/ der Ordner ist in dem entwickelt wird und /d/dropbox/plugin-name/ der Ordner für das Remote-Repo darstellt. Um es etwas einfacher zu machen verwende ich ‘local‘ als Namen für den lokalen Entwickelungs-Ordner und ‘remote‘ als Namen für den Ordner der das Remote-Repo enthält.

Hooks

Um das automatische Committen ins SVN-Repo durchzuführen hatte ich recht schnell git hooks als Lösungsweg ausgespäht. Dank einer guten Dokumentation zu Git und sich selbst installierende Beispielen, fand ich dann auch schnell einen passenden Hook und wie ich ihn zu meinen Zwecken nutzen kann. Bei dem Hook handelt es sich um den update hook, der immer dann ausgeführt wird, wenn in ein Remote-Repo hinein gepusht wird. Um das zu verstehen, ein kurzer Einblick in die Hooks von Git.
Es gibt Hooks die nur dann ausgeführt werden wenn eine Aktion im lokalen Repo durchgeführt wird (z.B. beim Committen). Und es gibt Hooks die nur im Remote-Repo ausgeführt werden (z.B. beim Pushen). Die Hooks unterscheiden sich allerdings nicht nur darin wann (und wo) sie ausgeführt werden, sondern auch darin, welche Parameter sie mitgegeben bekommen. Da es keinen Hook gibt den man im lokalen Repo nutzen kann sofern man feststellen möchte ob ein Tag gesetzt bzw. gepusht wurde, entschied ich mich auf das Remote-Repo auszuweichen wo es den update hook gibt der von Git die benötigten Parameter übergeben bekommt.

Das Setup

Nun müssen noch ein paar Kleinigkeiten eingerichtet werden. Das git init im local Ordner dürfte mittlerweile in Fleisch und Blut übergegangen sein. Als nächstes wird mit einer Zeile sowohl das remote Verzeichnis als auch die Kopie des SVN-Repos angelegt. Dazu nach /d/dropbox/ wechseln, eine Shell öffnen und svn checkout http://plugins.svn.wordpress.org/plugin-name eingegeben. SVN erzeugt einen Ordner plugin-name der bereits alle für SVN benötigten Unterordner enthält. Da nun noch das Remote-Git-Repo fehlt, wird in den Ordner plugin-name gewechselt und mit git init --bare ein Remote-Git-Repo angelegt. Wer es mal ausprobiert, wird merken das git svn im Grunde genommen nichts anderes macht. Es werden einfach zwei Repos unterschiedlicher CVS in einen Ordner untergebracht. Zum Schluß wird der remote noch in local als Remote-Repo hinzugefügt: git remote add dropbox /d/dropbox/plugin-name
Jetzt ist das Setup fast geschafft, was noch fehlt ist das Script für den update hook. In /d/dropbox/plugin-name/hooks/ finden sich einige Beispiele für git hooks. Die können alle gelöscht werden um es schön übersichtlich zu halten. Statt der Beispiel-Scripte wird das update Script hier hinein kopiert und damit ist das Setup abgeschlossen.

Jetzt können wir fleißig an unseren Code im Entwickelungs-Ordner local arbeiten. Als gute Entwickler committen wir oft und aussagekräftig. Am Ende eines jeden Tages (oder auch öfters), pushen wir unsere Arbeit in den remote. Wenn man nun einmal in den remote schaut, wird man merken das sich nichts (sichtbares) dort tut. Weder kommt Code hinzu noch wird ein SVN-Commit durchgeführt (lässt sich ganz einfach mit svn log prüfen). Dies ist jedoch auch genau so gewollt. WordPress verbietet es uns jeden Commit in das WP-SVN zu committen.
Wie kommt nun unsere neue Version ins WP-SVN? Wenn es denn soweit ist das eine neue Version fertig ist, dann wird diese in Git getaggt: git tag 1.0 -a -m 'A new version of plugin-name was released with version number 1.0' oder mit einen schlichteren “un-annotated tag” git tag 1.0. Dieser Tag muss noch nach remote gepusht werden (git push dropbox 1.0), ab jetzt fängt die Magie des update hooks an zu wirken.
Das Shell-Script macht nun folgendes:

  • Prüfe ob ein Git-Repo im Ordner trunk vorhanden ist. Wenn nicht, dann erzeuge eines
  • Prüfe ob in diesen Git-Repo ein Remote-Repo namens svn-master vorhanden ist. Wenn nicht, dann füge es hinzu
  • Hole dir die Daten aus dem Remote-Repo svn-master in den master branch (pull = fetch&merge)
  • Füge den neuen Code/Dateien im SVN-Repo hinzu und führe ein SVN-Commit durch
  • Erzeuge mit svn copy einen sauberen SVN-Tag im Remote-SVN-Repo

Das update Script muss dabei auf einen kleinen Trick zurück greifen. Tags sind nichts anderes als Revisionen die einen bestimmten Zustand des Codes widerspiegeln. Normalerweise könnte man beim pull auch auf den Tag zugreifen, dieser existiert im Remote-Repo allerdings zu diesen Zeitpunkt noch nicht. Deswegen muss der Code erst in einen Commit zum Remote-Repo gepusht werden, da der Code anschließend aus den letzten Commit gezogen wird!
Es gibt sicherlich Möglichkeiten auch Clientseitige Hooks, also Hooks die im lokalen Repo ausgeführt werden, zu nutzen. Dann müsste man jedoch bei jedem klonen des Remote-Repos diese Hooks neu einrichten. So läuft der Prozess mehr oder minder zentral ab und man muss alles nur einmal einrichten egal mit wie vielen Computern oder Teammitgliedern man das ganze nutzt.

Fazit

Das ganze ist noch etwas wackelig, vor allem was Merge-Konflikte angeht. Mir fehlen dazu bisher die praktischen Erfahrungen und weitere Tests. Da das Git-Repo im Verzeichnis trunk jedoch einzig und alleine dazu dient den Code in das trunk Verzeichnis zu kopieren, kann man hier sehr grob vorgehen und einfach den bestehenden Code gnadenlos überschreiben. Es finden sich dazu mehrere Methoden im Netz, ich bin mir nur noch nicht ganz sicher welche die beste ist. Wahrscheinlich ist es das Beste den Code aus dem Remote-Repo in einen neuen, temporären Branch zu kopieren, den SVN-Commit durchzuführen und den Branch anschließend wieder zu löschen. Wer es ganz hart mag, kann das Git-Repo auch einfach komplett löschen indem er den Ordner .git im Verzeichnis trunk löscht.
Auf ein svn update verzichte ich bewusst, da dies ebenfalls zu Merge-Konflikten führen kann. Meine Lösung ist also nichts für diejenigen, die mit mehreren an einen WP-SVN-repo arbeiten. Hier sind andere Strategien gefragt die um etwas Handarbeit, vor allem beim Lösen von Merge-Konflikten, nicht drum herum kommen.

Diejenigen die sich mit Shell- bzw. Bash-Scripten auskennen, werden schnell erkennen das dies mein allererstes Bash-Script ist. Der Code ist definitiv nicht optimal, erfüllt jedoch seinen Zweck. Auch lässt sich das eine oder andere in Sachen SVN vielleicht noch optimieren, denn auch SVN war bisher nicht mein Fachgebiet (wird es wohl auch nie werden).
Ein paar Kleinigkeiten sollten noch gelöst werden, so zum Beispiel das automatische Übernehmen der Commit-Nachricht bei annotated Tags. In meinen Augen aber eher Komfort-Funktionen die eine etwas geringere Priorität haben.
Das es ein Repo ist, kann sich ja ohnehin jeder den Code ziehen und die Verbesserungen/Optimierungen einbauen die er meint das sie dort hin gehören. Da es sich um “Social Coding” handelt, würde es mich freuen wenn auch der eine oder andere Pull Request bei mir ankommt.