umpf - Git on a New Level

Mainline First
Neben einer umfassenden Dokumentation von umpf ist auch der Quellcode in unserem Github-Repository zu finden.

Moderne Softwareentwicklung ohne begleitende Versionsverwaltung wie Git ist heutzutage unvorstellbar - Änderungen am Quellcode sollen schließlich nachvollziehbar dokumentiert und beliebige Verssionsstände jederzeit einfach reproduziert werden können. Für Arbeiten an komplexeren Projekten wie etwa dem BSP ("Board Support Package") eines eingebetteten Systems mit mehreren Entwicklungssträngen skaliert ein bloßes Aufeinanderstapeln der einzelnen Änderungen jedoch nicht.

Häufig werden ausgehend von einem gemeinsamen Softwarestand parallel verschiedene Funktionalitäten entwickelt, müssen Back-Ports integriert und für Upstream gedachte Arbeiten separat verwaltet werden. Die dazu notwendige thematische Aufteilung bei der Entwicklung neuer Patches für das BSP geschieht daher in der Regel auf voneinander unabhängigen Zweigen in separaten Git Branches.

Kompliziert wird es mit diesem Modell allerdings bei der anschließenden Zusammenführung von mehr als zwei Branches, etwa, um das Gesamtprojekt testen zu können oder zur Veröffentlichung eines Releases. Das manuelle Mergen der einzelnen Zweige ist so aufwendig wie fehleranfällig und kollidiert mit den möglichst kurzen Testzyklen iterativer Entwicklungsmodelle.

Branches sinnvoll zusammenführen

Unter Anderem, um diesen letzten Schritt zu vereinfachen und aus einer Auswahl von Topic Branches reproduzierbar einen einzelnen Patch Stack generieren zu können, haben wir den Universal Magic Patch Functionator (umpf) als quelloffene Erweiterung für die lokale Git-Installation entwickelt.

Mit umpf lässt sich innerhalb der gewohnten Git Infrastruktur und über mehrere Git Branches ("Topic Branches") hinweg eine lineare Patch-Serie generieren, deren Abhängigkeiten dabei konserviert werden.

Diese Abfolge von Commits wird durch einen regulären Git Tag beschrieben, der von umpf um Referenzen zu den enthaltenen Topic Branches ergänzt wird. Ein solcher utag vereint somit mehrere Eigenschaften:

  • Der Marker beinhaltet einen Datumsstempel und ist dadurch eindeutig zuordbar.
  • Es kann ein linearer Patch Stack als Patch-Serie exportiert werden.
  • Der Stand der eingebundenen Branches zum umpf-Zeitpunkt wird konserviert. Spätere auch tiefgreifende Änderungen eines Zweiges (z.B. Force-Pushes nach Bug-Fixes) haben dadurch keinen Einfluss auf den utag, weswegen der tatsächliche Entwicklungsstand zum Tagging-Zeitpunkt problemlos wiederhergestellt und nachvollzogen werden kann.
  • Durch die Archivierung der enthaltenen Branches können deren eventuelle Abhängigkeiten untereinander mit normalen Git-Werkzeugen nachvollzogen werden.
  • Durch Heuristik wird die Art des Projektes erkannt und passend ein Datumsstempel integriert - bspw. als EXTRAVERSION in einem Makefile. Dies ermöglicht später eine eindeutige Identifikation auch des bereits kompilierten und ausgelieferten Softwarestands.

Die Erstellung eines utags und seiner linearen Patch-Serie ist dabei relativ unkompliziert und setzt neben umpf lediglich ein bestehendes Git Projekt voraus. umpf selbst ist ein Bash Skript und bedient sich jener Werkzeuge, die Git selbst zur Verfügung stellt. Deshalb können durch umpf generierte Tags und Merges im Anschluss von jeder normalen Git Installation verarbeitet werden. Ein utag wird folglich auch regulär mittels git push dem bereits bestehenden Repository hinzugefügt und kann analog dazu über git checkout wieder aufgerufen werden.

Um einen utag erzeugen zu können benötigt umpf drei Parameter, die sich zum Beispiel in Form einer useries-Textdatei oder über Aufruf von umpf init übergeben lassen:

  • base: Der Ausgangspunkt, auf dem die spätere Patchserie aufbauen soll. Die umpf-base ist meistens ein Upstream Commitish, etwa ein Release Tag (bei Linux z.B.: "v6.3").
  • name: Der eigentliche Name des umpfs, der später auch Teil des utags wird. Es empfiehlt sich, diesen von der base abzuleiten, bspw. "6.3/release-name".
  • topic: Die eigentlichen Git-Branches (Topic Branches) werden mit je einem vorangestellten # umpf-topic: angeführt. Sie sollten selber auf dem base commit aufbauen, um Komplikationen zu vermeiden. Es ist daher ratsam, Branches mit einem anderen Ausgangspunkt als die umpf-base zuerst auf ebendiese zu re-basen.

Integration von Backports

Gerade bei Arbeiten mit Upstream-Projekten kommt es immer wieder vor, dass Bugfixes oder Funktionserweiterungen von dritter Seite eingepflegt werden müssen. Die Stärke von umpf ist hier, dass solche Änderungen in einem eigenen Branch gepflegt werden können und sie nicht weiter mit den Arbeiten am eigenen Projekt interferieren.

Zuverlässig funktioniert diese Branch-Integration bei umpf allerdings nur, wenn der Ausgangscommit vergleichbar ist: Topic Branches für bspw. v5.15 und v5.16 können in der Regel problemlos durch umpf zusammen verarbeitet werden. Ein weiterer Branch basierend etwa auf v6.2 wird wegen der umfangreichen allgemeinen Unterschiede im Code allerdings zu Komplikationen beim umpfen führen.

Um Bugfixes und sonstige Backports zu integrieren ist es daher empfehlenswert, diese mit ihrem eigenen Topic Branch einmalig auf die umpf-base zu re-basen.

Behandlung von Merge Konflikten

Git Performance
Sehr große Git-Projekte, wie etwa der Linux Kernel, benötigen ob ihrer zahlreichen Commits entsprechend viel Rechenleistung für die Verarbeitung. Beschleunigen lässt sich dies durch git commit-graph.

umpf kann selbstständig keine Merge-Konflikte auflösen. Die händische Konfliktlösung während des umpfens ist zwar möglich und bspw. bei der Arbeit mit Device-Trees manchmal unvermeidlich, sollte sich jedoch auf ein Minimum beschränken.

Die Lösungen zu den einzelnen Konflikten können allerdings mittels git-rerere gespeichert und im Wiederholungsfalle dadurch automatisiert aufgelöst werden. Die zu integrierenden Branche sollten trotzdem, falls nötig, im Vorfeld eines umpfs bereits entprechend umstrukuriert und aufgeräumt worden sein.

Lineare Serie generieren

Die so erstellte useries kann nun benutzt werden, um aus ihr einen utag abzuleiten und alle eingetragenen Entwicklungsstränge in einem gemeinsamen Release Tag vereinen zu lassen:

~/epic-project $  cat ./useries

# umpf-base: v6.3
# umpf-name: 6.3/special-customer-release
# umpf-topic: v6.3/topic/bugfix-branch
# umpf-topic: v6.3/topic/more-fixes
# umpf-end
~/epic-project $  umpf tag ./useries

umpf wird nun alle Branches in der Reihenfolge, wie sie in der useries eingetragen wurden, auf der angegebenen umpf-base stapeln. Dabei wird auch ein Autosquash durchgeführt, sodass Fixup-Commits aufgelöst werden und nicht in den finalen Patchstack gelangen.

Ein Beispiel, wie ein solcher utag in Git dargestellt wird, kann in der folgenden Abbildung betrachtet werden. Hierbei handelt es sich um einen so genannten qualifizierten umpf, da dieser alle notwendigen Informationen zur vollständigen Rekonstruktion beinhaltet:

  • umpf-base: Der Ausgangscommit. Meist ist dies ein Upstream-Tag.
  • Patch Stack: Commits der Topic Branches, gestapelt auf die umpf-base. Die Reihenfolge der Branches entspricht jener, wie sie umpf übergeben wurden.
  • References: Der Zustand der integrierten Branches zum Zeitpunkt des umpf-ens. Dieser Zustand wird im Commit des utag über die HEADs der Zweige referenziert und dadurch konserviert, sodass selbst eine nachträgliche Änderung eines Branches keine Auswirkung hat, sollte der utag zu einem späteren Zeitpunkt erneut ausgechecked werden. Die Referenzierung bewirkt außerdem, dass die Git Garbage Collection die vermeintlich verwaiste Konstellation im Repository belässt. Die Reproduzierbarkeit des utag bleibt somit langfistig gegeben.
  • utag: Ein regulärer Git Tag, in dem die integrierten Branches sowie die jeweils neusten Commit-IDs (HEADs) zum Zeitpunkt des umpf-ens hinterlegt sind.
  • Release Tag: Ein weiterer regulärer Git Tag. Dieser wird für die Generierung einer Patch-Serie referenziert. Der Commit beinhaltet ggf. eine Modifikation des Projektcodes, um einen Datumsstempel zu integrieren.

Das abschließende Exportieren des Patchstacks erfolgt über den Befehl umpf format-patch. Der Export kann mit Parametern variiert werden: Ein angehängtes -bb erzeugt eine zu Yocto-Projekten kompatible Patch-Serie, mit -p kann der Zielpfad festgelegt, und mit -u das Überschreiben einer eventuell bereits vorhandenen Serie veranlasst werden. Weitere Hinweise liefert umpf --help.

umerge

Anstelle eines utags können wir auch ohne vorhandene useries durch den Aufruf von umpf merge schrittweise durch händisches einfügen der einzelnen Topic Branches einen umerge aufbauen. Hierzu genügt es, mittels git checkout auf den Stand der erwünschten umpf-base zu wechseln um im Anschluss mit umpf merge gezielt die benötigten Branches nachzuladen:

~/epic-project $  git checkout v6.3
~/epic-project $  umpf merge v6.3/topic/bugfix-branch

umpf: merging 'v6.3/topic/bugfix-branch'...
[...]

~/epic-project $  umpf merge v6.3/topic/more-fixes

umpf: merging 'v6.3/topic/more-fixes'...
[...]

Diese Zusammenführung von mehreren Zweigen ähnelt einem Octopus Merge, welcher bei Git für das gleichzeitige Mergen von mehr als zwei Branches genutzt werden kann. Anders als bei Git bleibt nach einem umpf merge jedoch analog zum utag die Herkunft der hinzugekommenen Commits erhalten: Die zugrundeliegenden Branches werden in ihrem Zustand zum Zeitpunkt des umerge durch einen zusätzlichen Eintrag in der Commit-Nachricht des Merges referenziert. Dadurch kann zu einem beliebigen späteren Zeitpunkt die Herkunft der einzelnen Commits - also die ursprünglichen Namen ihrer Branches sowie deren Historie - nachvollzogen werden.

Ein umerge kann auch direkt mit einer useries-Datei erzeugt werden. Hierzu genügt der Aufruf von umpf build ./useries.

Ein umerge bietet sich vorallem für die iterative Entwicklung an, bei der alle Topic Branches eines Projekts benötigt werden, um bspw. hardwarenahe Entwicklung am Kernel. Zusätzliche Änderungen am Quellcode können dann einfach auf den umerge aufgesetzt und getestet werden. Im Anschluss lassen sich durch Aufruf von umpf distribute die neu erzeugten Patche interaktiv einem der ursprünglichen Zweige zuordnen und werden von umpf dort direkt integriert.

umpf distribute kann auch genutzt werden, wenn die Commits nicht auf einem umerge, sondern stattdessen auf einem utag gestapelt wurden.

umerges können allerdings auch direkt in Projekten Verwendung finden, wenn Abhängigkeiten zwischen den einzelnen Topic Branches bestehen. Durch das Stacking Feature von umpf wird bei der späteren Verarbeitung der auf einem umerge aufbauende Zweig dann regulär in die linearisierte Abfolge der Commits integriert. Dabei ist zu beachten, dass der Eintrag des fraglichen Branches in der useries Datei nicht vor den Zweigen, von denen er abhängt, eingefügt werden darf; die Reihenfolge der Nennung ist hier ausschlaggebend.

Da der umerge den Zustand der Basiszweige für den abhängigen Branch konserviert, werden nachträgliche Arbeiten an diesen auch nicht auf bereits bestehende umerges übertragen. Dies kann beim anschließenden Genrerien eines neuen utags zu Konflikten führen, wenn dieser mit einem älteren umerge erzeugt werden soll. In einem solchen Fall dürfen sich nachträgliche Änderungen an den Basiszweigen nicht auf direkte Abhängigkeiten des darauf aufbauenden Branches auswirken.

Warum wir umpfen

Bei Pengutronix haben wir uns auf Embedded Linux, und damit auf die Arbeit am Linux Kernel und den assoziierten Projekten spezialisiert. Im Rahmen unserer "Mainline First"-Strategie bringen wir dabei die in unseren Projekten erarbeiteten Funktionalitäten so früh wie möglich in die offiziellen Upstream-Kanäle ein. Dies bringt zwar langfristig viele Vorteile mit sich - wie etwa Sicherstellung des hohen Qualitätsstandards unseres Codes durch Community Review - bedeutet auf der anderen Seite aber auch, dass die Verwaltung der zum Projekt gehörenden Patches nicht nur eine direkte Integration in unsere spezifischen BSPs ermöglichen, sondern eben auch den Anforderungen von Upstream genügen muss. Dies setzt eine saubere Trennung des jeweiligen Topic Branches vom restlichen Projekt voraus.

Der Anstoß zur Entwicklung von umpf kam letztendlich aus unserer Arbeit an der Treiberentwicklung für eingebettete Systeme: Moderne SoCs decken heutzutage ein unglaublich breites Spektrum an Implementierungen und Funktionalitäten ab; zugleich werden nach dem Baukasten-Prinzip aber oftmals baugleiche Komponenten (sog. "intellectual property cores" bzw. "IP-Cores") genutzt - auch herstellerübergreifend. Entsprechend haben wir deshalb bei uns spezifische Treiber-Branches aufgebaut, die sich ohne weitere Anpassungen über mehrere unserer Integrationsprojekte hinweg verwenden lassen.

Das direkte Arbeiten am Linux-Kernel mit seinen hunderten von Branches und fortwährender Upstream-Entwicklung ist natürlich ein Paradebeispiel dafür, wie umfangreich ein mit Git verwaltetes Projekt werden kann. Aber auch wesentlich kleinere Projekte profitieren spätestens ab dem zweiten eingeführten Branch von einer Automatisierungshilfe wie umpf.

Auch, wenn eine Erweiterung wie unser Universal Magic Patch Functionator Grenzen mit Blick auf die tatsächlich mögliche Komplexität eines Git Repositories aufweist, so befähigt umpf uns trotzdem seit Jahren erfolgreich dazu, bei einer Vielzahl von Projekten eine ganze Reihe verschiedener Patchstacks sauber zu verwalten und den generellen Versionierungsaufwand in dieser Beziehung überschaubar zu halten.

Wir haben uns nun entschlossen, dieses Tool unter MIT-Lizenz der Open Source Gemeinde zur Verfügung zu stellen - und wir freuen uns wie immer über eine rege Beteiligung!


Weiterführende Links

Komplexität beherrschen mit Open Source

Vor ein paar Tagen ist etwas spannendes passiert: Ich habe mein allererstes Embedded System wiedergesehen - eine nach nunmehr ca. 34 Jahren defekte Schrittmotorsteuerung für die Teleskope der Volkssternwarte Rothwesten, die ich in den Sommerferien in der 12. Klasse gebaut habe. Schaut man sich die Entwicklung von damals bis hin zu unseren aktuellen industriellen Embedded Systems an, wird schnell klar, warum sowas heute nur noch mit Open Source Software sinnvoll beherrschbar ist.


Pengutronix at the Linux Plumbers Conference

The Linux Plumbers Conference 2024 will take place in Vienna from 18. to 20.09.2024. Luckily this does not overlap with the ELCE. Pengutronix will attend the LPC with six colleagues - so watch out for our T-shirts and hoodies and and feel free to chat with us.


Pengutronix at FrOSCon 2024

Am 17. und 18. 08. 2024 ist es wieder soweit: Die FrOSCon findet an der Hochschule Bonn-Rhein-Sieg in Sankt Augustin statt - und Pengutronix ist wieder als Partner dabei.


Pulse Width Modulation (PWM) is easy, isn't it? - Turning it off and on again

Part of Uwe Kleine-König's work at Pengutronix is to review PWM (Pulse Width Modulation) drivers. In addition, he also sometimes refactors existing drivers and the Linux kernel PWM subsystem in general.


Yes we CAN... add new features

Have you ever experienced an otherwise fine product that is missing just the one feature you need for your application?


Pengutronix at Embedded World 2022

Welcome to our booth at the Embedded World 2022 in Nürnberg!


Die Pengutronix Kernel-Beiträge in 2021

2022 hat begonnen, und obwohl Corona unsere Workflows stark verändert hat, hat das Pengutronix Team auch in diesem Jahr wieder etliche Beiträge zum Linux-Kernel geleistet. Das letzte Kernel-Release in 2020 war 5.10, das letzte in 2021 war 5.15 - schauen wir also, was sich dazwischen bei uns getan hat.


Pengutronix at FOSDEM 2021

"FOSDEM is a free event for software developers to meet, share ideas and collaborate. Every year, thousands of developers of free and open source software from all over the world gather at the event in Brussels. In 2021, they will gather online." -- FOSDEM



15 Years of i.MX in Mainline Linux

Today it has been 15 years since we mainlined support for Freescale/NXP's i.MX architecture in the Linux kernel! That was one small step for [a] man, one giant leap for (industrial Linux users') mankind :-) Here is some background about why it happened and what you might want to learn from history for your next embedded Linux project.