diff und patch - die wichtigsten Tools der Zusammenarbeit

An dieser Stelle möchte ich zwei Tools beschreiben, von denen ich sehr angetan bin. Sie sind ziemlich alt (über 20 Jahre), sehr simpel aber dennoch effektiv. Sie sind die Grundlage vieler heutiger Versionsverwaltungssysteme und in der Free Software Szene unersetzlich. Ihre Namen sind diff und patch. Sie gehören zu den GNU-Kern Tools und sind somit Teil jeder modernen Linux Distribution; Für Windows könnt ihr die GnuWin32 Ports hier herunterladen: http://gnuwin32.sourceforge.net/packages.html.

Der Unterschied als Grundgedanke

Die Idee hinter den Tools ist recht einfach. Früher war es üblich Software nur als Quellcode zu verteilen und zu vertreiben, da es sehr viele verschiedene Betriebssysteme und Prozessorarchitekturen gab. Somit war der Quellcode immer offen, und die Benutzer (oft selbst Programmierer) konnten die Software verändern und verbessern - so entstanden viele gut ausgereifte Open-Source-Tools.

Die Frage die zur Entwicklung der beiden Tools führte war folgende: Wie transportiere ich am besten eine veränderte Version der Software zurück zum Autor, damit er die Verbesserung auch nutzen kann? Eine naive Antwort auf diese Frage wäre, dass man einfach die ganze veränderte Software in ein Archiv packt und verschickt. Aus zwei Gründen ist das nicht praktikabel: 1. waren früher die Netzwerkanbindungen nur sehr langsam und rar, man konnte sich kaum leisten ein ganzes Softwarepaket zu verschicken. 2., und das ist der heute viel interessantere Grund: Die ganze Software interessiert den Autor selten - er hat sie ja geschrieben, er weiß ja wie sie aussieht. Was ihn interessiert sind die Veränderungen - die Unterschiede, die die Software verbessert haben. Würde ihm der Beitragende das ganze Softwarepaket schicken, müsste sich der Originalautor erstmal durch den gesamten Quellcode kämpfen und nach Unterschieden suchen. Das ist lästig und ermüdend - also genau die Arbeit die gern vom Computer erledigt wird.

Anstatt nun den gesamten Quellcode zu verschicken kann man die zwei Versionen vergleichen und eine kleine Datei mit den Änderungen verschicken. Und hier kommt diff ins Spiel.

Wozu ihr diff und patch brauchen könntet

Vielleicht habe ich das Wort „früher“ im vorherigen Absatz etwas zu oft erwähnt und ihr werdet euch Fragen, wozu ihr das ganze überhaupt braucht. Hier sind ein paar mögliche Antworten:

  • Es ist die einfachste Art Quellcodeänderungen zu übertragen. Wenn ihr euch das Programm eines Freundes anseht und etwas geändert habt, ist das die einfachste Art ihm die Änderung zu schicken.
  • Sehr viele freie Softwareprojekte verwenden Patches als Mittel um Außenstehende an der Entwicklung teilhaben zu lassen. Da Versionsverwaltungssysteme meist eigene Accounts mit viel Vertrauen benötigen, ist der Patch die beste Methode um Beiträge von „Fremden“ anzunehmen.
  • Vielleicht habt ihr schon mal gelesen „dazu müsst ihr das Programm patchen“. Oft sind interessante Änderungen eines Programms noch nicht in einem Release oder in einem Versionsverwaltungsysstem enthalten, sondern liegen nur als Patch in irgendeinem Anhang in irgendeinem Post irgendeines Forums vor. Dann ist der Patch die einzige Möglichkeit das Feature zu bekommen.
  • Wenn ihr Subversion (SVN) verwendet, ist es eine mögliche Arbeitsweise um zwischen verschiedenen lokalen Entwicklungszweigen hin- und herzuschalten. Dann arbeitet ihr an einem Feature, erstellt einen Patch, stellt eure Arbeitskopie wieder auf den letzten Commit her und arbeitet an einem neuen Feature. Die alten Änderungen sind in der Patchdatei gespeichert. Das ist natürlich sehr lästig, aber wenn ihr SVN verwendet seid ihr selbst schuld. ;-) (Zum Beispiel mit Git ist es ganz leicht möglich zwischen verschiedenen Entwicklungszweigen hin- und herzuschalten.)

Spaß mit diff

Anstatt euch mit schlechten und künstlichen Beispiel zu langweilen, zeige ich euch das Programm diff an einer (zum Zeitpunkt des Verfassens dieses Artikels) aktuellen Änderung am Linux-Kernel 1).

Die Veränderung hat einige Dateien betroffen, ich werde hier aber nur die relevanten Abschnitte einer Datei, sagen wir der Datei drivers/gpu/drm/drm_crtc.c zeigen. Oben seht ihr die Datei vor der Veränderung, unten die Datei nach der Veränderung:

alt/drivers/gpu/drm/drm_crtc.c:

/**
 * drm_framebuffer_cleanup - remove a framebuffer object
 * @fb: framebuffer to remove
 *
 * LOCKING:
 * Caller must hold mode config lock.
 *
 * Scans all the CRTCs in @dev's mode_config.  If they're using @fb, removes
 * it, setting it to NULL.
 */
void drm_framebuffer_cleanup(struct drm_framebuffer *fb)
{
	struct drm_device *dev = fb->dev;
	struct drm_crtc *crtc;
 
	/* remove from any CRTC */
	list_for_each_entry(crtc, &dev->mode_config.crtc_list, head) {
		if (crtc->fb == fb)
			crtc->fb = NULL;
	}
 
	drm_mode_object_put(dev, &fb->base);
	list_del(&fb->head);
	dev->mode_config.num_fb--;
}
EXPORT_SYMBOL(drm_framebuffer_cleanup);


neu/drivers/gpu/drm/drm_crtc.c:

/**
 * drm_framebuffer_cleanup - remove a framebuffer object
 * @fb: framebuffer to remove
 *
 * LOCKING:
 * Caller must hold mode config lock.
 *
 * Scans all the CRTCs in @dev's mode_config.  If they're using @fb, removes
 * it, setting it to NULL.
 */
void drm_framebuffer_cleanup(struct drm_framebuffer *fb)
{
	struct drm_device *dev = fb->dev;
	struct drm_crtc *crtc;
	struct drm_mode_set set;
	int ret;
 
	/* remove from any CRTC */
	list_for_each_entry(crtc, &dev->mode_config.crtc_list, head) {
		if (crtc->fb == fb) {
			/* should turn off the crtc */
			memset(&set, 0, sizeof(struct drm_mode_set));
			set.crtc = crtc;
			set.fb = NULL;
			ret = crtc->funcs->set_config(&set);
			if (ret)
				DRM_ERROR("failed to reset crtc %p when fb was deleted\n", crtc);
		}
	}
 
	drm_mode_object_put(dev, &fb->base);
	list_del(&fb->head);
	dev->mode_config.num_fb--;
}
EXPORT_SYMBOL(drm_framebuffer_cleanup);


Hier seht ihr schon: unten ist einiges dazugekommen. Speichert die beiden Files als drm_crtc-alt.c und drm_crtc-neu.c ab.

Jetzt wollen wir herausfinden, was nun dazugekommen ist und was weg. Dazu rufen wir das diff Programm in diesem Verzeichnis folgendermaßen auf:

$ diff -u drm_crtc-alt.c drm_crtc-neu.c

Die Option -u bestimmt, dass die Ausgabe im sogenannten Unified Diff Format sein soll. Ich werde es gleich besprechen. Habt ihr ungefähr das gleiche gemacht wie ich, sollte die Ausgabe folgendermaßen aussehen:

--- drm_crtc-alt.c	2009-08-20 23:47:31.000000000 +0200
+++ drm_crtc-neu.c	2009-08-20 23:47:41.000000000 +0200
@@ -12,11 +12,20 @@
 {
 	struct drm_device *dev = fb->dev;
 	struct drm_crtc *crtc;
+	struct drm_mode_set set;
+	int ret;
 
 	/* remove from any CRTC */
 	list_for_each_entry(crtc, &dev->mode_config.crtc_list, head) {
-		if (crtc->fb == fb)
-			crtc->fb = NULL;
+		if (crtc->fb == fb) {
+			/* should turn off the crtc */
+			memset(&set, 0, sizeof(struct drm_mode_set));
+			set.crtc = crtc;
+			set.fb = NULL;
+			ret = crtc->funcs->set_config(&set);
+			if (ret)
+				DRM_ERROR("failed to reset crtc %p when fb was deleted\n", crtc);
+		}
 	}
 
 	drm_mode_object_put(dev, &fb->base);


Hier seht ihr einen Patch im Unified Diff Format. Das „Unified Diff“-Format zeigt die Unterschiede zwischen Dateien zeilenweise an. Die Änderungen werden hier als „Entfernen und Hinzufügen“ von Zeilen betrachtet. Ändert ihr in einer Zeile nur einen Buchstaben, wird die ganze Zeile als erstetzt angezeigt. Die entfernten und hinzugefügten Zeilen werden hier nebeneinander vereinigt (engl. für vereinigt: unified) dargestellt.
Es gibt zwar auch andere Patch-formate, und diff kann sie sehr wohl erstellen - allerdings werden diese kaum verwendet. Das heute am weitesten verbreitete Patch Format ist Unified Diff, und ehrlich gesagt habe ich außer in der manpage von diff(1) noch keine anderen Patch Formate gesehen.

Gehen wir nun die Ausgabe Zeile für Zeile durch.
Die ersten beiden Zeilen sehen wie folgt aus:

--- drm_crtc-alt.c	2009-08-20 23:47:31.000000000 +0200
+++ drm_crtc-neu.c	2009-08-20 23:47:41.000000000 +0200

Die erste Zeile beginnt mit drei Minuszeichen, gefolgt vom Dateinamen der alten Datei. Daneben steht der Zeitpunkt der letzten Änderung, wie er vom Dateisystem festgehalten wurde; die zweite Zeile mit drei Pluszeichen, dem Dateinamen der neuen Datei und ebenfalls dem Zeitpunkt der letzten Änderung. Der Zeitpunkt wird allerdings nur zur Dokumentation festgehalten, er hat keine Auswirkungen auf das Anwenden von Patches; jeglicher Text nach dem Dateinamen wird nämlich ignoriert.

Ein Patch enthält viele Minus- und Pluszeichen. Ein Minuszeichen ist immer mit der „alten“ Datei verbunden, ein Pluszeichen immer mit der „neuen“ Datei. Das ist ein guter Anhaltspunkt zum Verständnis von Patch Files.

Nach den ersten zwei Zeilen beginnen die Stückchen der Änderungen. Es werden immer die geänderten Zeilen und drei Zeilen davor und danach angezeigt (die Zahl drei ist eine Standardeinstellung von diff, sie kann durch hinzufügen einer Zahl zur Option -u verändert werden, also z.B. -u 5).

Ein stückchen beginnt mit einer Zeile die ich Sinngemäß als „Kontextzeile“ bezeichne, sie Zeigt nämlich den Ort der Änderung als Zeilennummer an:

@@ -12,11 +12,20 @@


Sie beginnt und endet mit zwei @-Zeichen und enthält zwei Zahlenpaare. Vor dem ersten Zahlenpaar steht ein Minus, das Zahlenpaar zeigt den geänderten Bereich für die alte Datei an, das zweite Zahlenpaar hat ein Pluszeichen und gibt den Bereich für die neue Datei an. Die Bereiche sind folgendermaßen angegeben: Die erste Zahl ist die Zeilennummer der im Patch nachfolgenden Zeile in der jeweiligen Datei.
Vielleicht einfach nochmal zur Erklärung: Die erste Zeile des Stückchens hat in der geänderten Datei diese Zeilennummer. Die zweite Zahl gibt die Länge des Stückchens an. Unsere Zeile bedeutet also, dass das geänderte Stückchen in der alten Datei in der Zeile 12 beginnt und 11 Zeilen lang ist und in der neuen Datei ebenfalls in der Zeile 12 beginnt und 20 Zeilen lang ist.

Nach dieser Zeile kommen drei Zeilen Kontext - also Zeilen, die gleich geblieben sind - und anschließend beginnen die eigentlichen Änderungen. Diese sind so dargestellt, als ob man von der alten zur neuen Datei kommen würde, wenn man in der alten Datei bestimmte Zeilen entfernt und neue Zeilen einfügt. Die Zeilen die mit einem Pluszeichen beginnen werden eingefügt und die Zeilen die mit einem Minuszeichen beginnen werden entfernt.

Diese Abfolge, also „Kontextzeile“ - Kontext - Änderungen - Kontext wiederholt sich so oft wie es geänderte Stückchen in der Datei gibt. Wenn eine geänderte Datei am Ende keinen Zeilenumbruch hat und mit einem Buchstaben endet wird noch zusätzlich der Text \ No newline at end of file angehängt.

Patches anwenden - das Programm ''patch''

Bis jetzt haben wir das diff Programm nur zum „Anzeigen“ von Unterschieden genutzt. Um sie allerdings abzuspeichern und auch tatsächlich verschicken zu können, muss man die Ausgabe des diff Programms umleiten. Das geht mit dem Shell-operator >:

$ diff -u drm_crtc-alt.c drm_crtc-neu.c > aenderung.diff


Die Dateiendung „.diff“ ist keineswegs zwingend, ihr könnt die Datei so benennen wie ihr wollt. Für Patches haben sich die Endungen „.diff“ und „.patch“ eingebürgert.

Diesen erstellten Patch könnt ihr nun verschicken, abspeichern oder damit tun was ihr wollt. Solange ihr diese Datei behaltet könnt ihr auch eure Änderungen, die ihr an der ursprünglichen Datei gemacht habt verwerfen - alles was ihr getan habt ist in der Patch-Datei gespeichert.

Wenn ihr nun die Änderungen wiederherstellen wollt, müsst ihr ihn „Anwenden“. Das geht mit dem patch- Programm. Das Patch-Programm erwartet von der Standardeingabe Patches und wendet sie dann an. Ihr könnt die gespeicherte Patch-Datei mithilfe des < Operators der Shell als Standardeingabe verwenden. Beispielsweise könnte das einfach so aussehen:

$ patch < aenderungen.diff


Vorausgesetzt die Datei aenderungen.diff ist dieselbe wie jene die wir zuvor erstellt haben, wird nun nach der Datei drm_crtc-alt.c gesucht und der Inhalt dieser Datei umgewandelt. Die Datei sollte nach dem Patchen genauso aussehen wie die Datei drm_crtc-neu.c. Wenn die Datei die ihr patchen wollt anders heißt, oder wenn ihr die entstehende Datei anders benennen wollt, dann könnt ihr das natürlich auch angeben:

$ patch drm_crtc.c rm_crtc-ganz_anders.c < aenderungen.diff


Jetzt solltet ihr eine Nachricht sehen wie das patchen verlaufen ist. Wenn sie so aussieht, ist alles im grünen Bereich:

patching file drm_crtc-alt.c

Ihr seht ihr habt nun aus einer alten Datei eine neue gemacht, und habt dazu nur die Änderungen verwendet. Das geht aber auch umgekehrt! Um aus neu alt zu machen, gebt patch einfach den -R oder --reverse Switch an. Dann wird einfach aus jeder entfernten Zeile eine hinzugefügte und umgekehrt.

Was tut patch bei Fehlern? Was passiert, wenn die Datei die ihr Patchen wollt ein andere ist als die Datei von der aus der Patch erstellt worden ist? Dazu sehen wir uns die möglichen Fälle an:

Fügen wir ganz oben in der ursprünglichen Datei (drm_crtc-alt.c), zum Beispiel im Kommentar ein paar Zeilen ein. Wenn wir dann den Patch anwenden bekommen wir folgende Ausgabe zurück:

patching file drm_crtc-alt.c
Hunk #1 succeeded at 14 (offset 2 lines).


Das bedeutet, das erste Stückchen konnte angewendet werden, allerdings mit einem Offset von zwei Zeilen. Soweit so gut. Das bedeutet also, wir können in der Datei vor oder nach dem Stückchen so viel hinzufügen und entfernen wie wir wollen, abgesehen von der Warnung wird es keine Probleme geben.

Was passiert wenn wir im Kontextbereich, also „im“ Stückchen etwas verändern? Wir fügen irgendetwas an eine Zeile an wo kein + bzw. - davorsteht und rufen dann patch auf. Die Ausgabe sieht folgendermaßen aus:

patching file drm_crtc-alt.c
Hunk #1 succeeded at 12 with fuzz 1.


Das heißt, das erste Stückchen konnte an Zeile 12 angewendet werden, allerdings mit einem störenden Texteil.

Ihr seht, selbst Veränderungen im Kontext kann patch noch verarbeiten.

Jetzt wirds kritisch! Was passiert, wenn wir den Bereich verändern, der vom Patch selbst verändert wird? Ändern wir dazu eine Zeile die entfernt wird, z.B. Zeile 18 der Datei drm_crtc-alt. Die Antwort von patch sieht so aus:

patching file drm_crtc-alt.c
Hunk #1 FAILED at 12.
1 out of 1 hunk FAILED -- saving rejects to file drm_crtc-alt.c.rej


Das war zuviel für patch. Wenn selbst der veränderte Bereich nicht mit der Originaldatei übereinstimmt, dann macht es nicht viel Sinn diese Datei zu patchen. Die „rejects“-Datei ist einfach ein Patch mit allen Stückchen, die nicht funktioniert haben, allerdings nicht im Unified Diff Format.

Diff und Patch von Verzeichnissen

Wenn man für jede Datei, die ihr in einem Projektverzeichnis geändert habt einen eigenen Patch erstellen müsstet, wäre das bei größeren Änderungen ziemlich anstrengend. Glücklicherweise kann man mit diff Patches von ganzen Verzeichnissen erstellen und mit patch auch ganz einfach anwenden. Versuchen wir nun eine kleine Verzeichnisstruktur aufzusetzen. Gebt folgende Befehle in die Kommandozeile ein:

$ mkdir -p alt/drivers/gpu/drm	 
$ mkdir -p neu/drivers/gpu/drm

Legt die alte Datei in alt/drivers/gpu/drm und die neue Datei in neu/drivers/gpu/drm ab. Eure Verzeichnisstruktur sollte jetzt so aussehen:

$ find .
.
./neu
./neu/drivers
./neu/drivers/gpu
./neu/drivers/gpu/drm
./neu/drivers/gpu/drm/drm_crtc.c
./alt
./alt/drivers
./alt/drivers/gpu
./alt/drivers/gpu/drm
./alt/drivers/gpu/drm/drm_crtc.c


Zusätzlich erstellen wir im neuen und alten Verzeichnis eine Datei, um zu testen wie diff auf neue und gelöschte Dateien reagiert:

$ echo 'Ich bin alt!' > alt/drivers/alt	 
$ echo 'Ich bin neu!' > neu/drivers/neu


Somit haben wir unsere „Verzeichnisstruktur“ zum Testen hergestellt.

Diff von Verzeichnissen

Wir rufen nun Diff mit den Option -Nru auf. Diese Option -r bestimmt dabei, dass nicht nur Dateien sondern ganze Verzeichnisse verglichen werden. Die Option -u (die man wie hier auch einfach als u ans -r anhängen kann) bestimmt, dass die Ausgabe im sogenannten Unified Diff Format sein soll, das oben besprochen wurde. Die Option -N gibt dabei an, dass wenn eine Datei nur in einem Verzeichnis gefunden wird, sie als vorhanden aber leer im anderem Verzeichnis behandelt werden soll.

Statt den Dateinamen übergeben wir nun allerdings die Verzeichnisnamen. Die Kommandozeile sollte jetzt so aussehen:

$ diff -Nru alt/ neu/


Die Ausgabe sieht nun wie folgt aus:

diff -Nru alt/drivers/altedatei neu/drivers/altedatei
--- alt/drivers/altedatei	2012-07-13 16:45:13.183744362 +0200
+++ neu/drivers/altedatei	1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-Ich bin alt!
diff -Nru alt/drivers/gpu/drm/drm_crtc.c neu/drivers/gpu/drm/drm_crtc.c
--- alt/drivers/gpu/drm/drm_crtc.c	2012-07-13 16:45:13.184744352 +0200
+++ neu/drivers/gpu/drm/drm_crtc.c	2012-07-13 16:44:45.020025672 +0200
@@ -12,11 +12,20 @@
 {
     struct drm_device *dev = fb->dev;
     struct drm_crtc *crtc;
+    struct drm_mode_set set;
+    int ret;
 
     /* remove from any CRTC */
     list_for_each_entry(crtc, &dev->mode_config.crtc_list, head) {
-        if (crtc->fb == fb)
-            crtc->fb = NULL;
+        if (crtc->fb == fb) {
+            /* should turn off the crtc */
+            memset(&set, 0, sizeof(struct drm_mode_set));
+            set.crtc = crtc;
+            set.fb = NULL;
+            ret = crtc->funcs->set_config(&set);
+            if (ret)
+                DRM_ERROR("failed to reset crtc %p when fb was deleted\n", crtc);
+        }
     }
 
     drm_mode_object_put(dev, &fb->base);
diff -Nru alt/drivers/neuedatei neu/drivers/neuedatei
--- alt/drivers/neuedatei	1970-01-01 01:00:00.000000000 +0100
+++ neu/drivers/neuedatei	2012-07-13 16:44:45.020025672 +0200
@@ -0,0 +1 @@
+Ich bin neu!


Sehen wir uns nun die Ausgabe an: Sie ist zusammengesetzt aus 3 teilen, und zwar einem Patch für jede veränderte Datei.

Die erste Zeile sieht so aus:

diff -Nru alt/drivers/altedatei neu/drivers/altedatei


Sie enthält den Befehl den man ausführen muss um den jeweiligen Patch zu erhalten. Diese Zeile wird zwar vom Programm diff immer so ausgegeben, sie ist aber keineswegs bindend! In diese Zeile könnt ihr reinschreiben was ihr wollt. In der Tat ist es so, dass die Datei bis zum ersten Vorkommen der Zeichenkette --- (drei Minuszeichen hintereinander) ignoriert wird. Dieser Umstand wird auch von vielen Versionsverwaltungssystemen genutzt, um zusätzliche Informationen in die Patchdatei zu stecken. Die „echte“ Patchdatei aus dem Git Repository des Kernels beginnt nämlich so:

diff --git a/drivers/gpu/drm/drm_crtc.c b/drivers/gpu/drm/drm_crtc.c
index 33be210..2f631c7 100644 (file)
--- a/drivers/gpu/drm/drm_crtc.c
+++ b/drivers/gpu/drm/drm_crtc.c


Danach folgt der Patch der jeweiligen Datei im oben beschriebenen Format.

Dazu ist zu erwähnen, dass diff mit der Option -N kein löschen oder erstellen von Dateien kennt. Für diff ist eine Löschoperation nichts anderes als das entfernen aller Zeilen, und die Erstellung einer neuen Datei das hinzufügen aller neuen Zeilen. Um dies zu verdeutlichen wird die Zugriffszeit der gelöschten Datei im neuen Zustand (oder der erstellten Datei im alten Zustand) auf die Unix-Zeit 0 gesetzt.

Patch auf Verzeichnisse

Selbstverständlich könnt ihr den erstellten Patch auch dazu verwenden ein bereits vorhandenes „altes“ Verzeichnis zu einem „neuen“ zu patchen.

Dazu wird wieder das patch-Programm verwendet:

$ patch -p1 alt/ < fix.patch


Das zweite Argument ist dabei das Verzeichnis, von dem ausgegangen werden soll wenn die Pfade des Patches aufgelöst werden. Wird dieses nicht angegeben, wird das aktuelle Verzeichnis betrachtet.

Das erste Argument -p1 gibt an, wieviele Verzeichniskomponenten vom Beginn aller Pfadnamen des Patches entfernt werden müssen. Mit -p1 würde der erste Komponentname entfernt werden (also in unserem fall alt/ und neu/), und patch würde im angegebenen Verzeichnis nach dem nächsten Verzeichnis, also drivers/ suchen.

Dies macht aus dem Inhalt des Verzeichnisses alt/ den Inhalt des Verzeichnisses neu/.

Verbesserungsvorschläge

Hat dir diese Antwort geholfen? Wenn nicht oder wenn du Verbesserungs- bzw. Erweiterungsvorschläge hast, dann schreib das bitte einfach auf die Diskussionsseite.

1)
Wenn ihrs genau wissen wollt, es ist der Commit cad2c8fd9b3afceced08838c87c520e6da417a65 zum master-Branch den ihr hier ansehen könnt: http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git