Kapitel 4. Versionskontrolle

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Betrachte die Welt durch deine Polaroid-Brille

Für die Arbeiterklasse wird es viel besser aussehen.

Gang of Four, "I Found that Essence Rare"

In diesem Kapitel geht es um Versionskontrollsysteme (RCS), die Schnappschüsse der vielen verschiedenen Versionen eines Projekts während seiner Entwicklung aufbewahren, z. B. die Entwicklungsstufen eines Buchs, eines gequälten Liebesbriefs oder eines Programms.

Die Verwendung eines RCS hat meine Arbeitsweise verändert. Um es mit einer Metapher zu erklären, stell dir das Schreiben als Klettern vor. Wenn du selbst kein Kletterer bist, stellst du dir vielleicht eine massive Felswand und die einschüchternde und lebensbedrohliche Aufgabe vor, den Gipfel zu erreichen. Aber in der heutigen Zeit ist der Prozess viel schrittweiser. An einem Seil kletterst du ein paar Meter hoch und befestigst das Seil dann mit spezieller Ausrüstung (Nocken, Stifte, Karabiner und so weiter) an der Wand. Wenn du nun stürzt, bleibt dein Seil am letzten Karabiner hängen, was relativ sicher ist. An der Wand konzentrierst du dich nicht darauf, den Gipfel zu erreichen, sondern auf das viel leichter zu lösende Problem, den nächsten Karabiner zu finden.

Wenn ich wieder mit einem RCS schreibe, ist ein Arbeitstag nicht mehr eine funktionslose Plackerei auf dem Weg zum Gipfel, sondern eine Abfolge von kleinen Schritten. Welches Feature könnte ich hinzufügen? Welches Problem kann ich beheben? Sobald du einen Schritt gemacht hast und sicher bist, dass deine Codebasis in einem sicheren und sauberen Zustand ist, gibst du eine Revision ab. Wenn dein nächster Schritt katastrophal ausfällt, kannst du auf die Revision zurückgreifen, die du gerade abgegeben hast, anstatt wieder von vorne anzufangen.

Aber die Strukturierung des Schreibprozesses und die Möglichkeit, sichere Punkte zu markieren, sind nur der Anfang:

  • Unser Dateisystem hat jetzt eine Zeitdimension. Wir können das RCS-Repository für Dateiinformationen abfragen, um zu sehen, wie eine Datei letzte Woche aussah und wie sie sich von damals bis heute verändert hat. Auch ohne die anderen Fähigkeiten habe ich festgestellt, dass mich das allein schon zu einem selbstbewussteren Autor macht.

  • Wir können mehrere Versionen eines Projekts nachverfolgen, z. B. meine Kopie und die Kopie meines Koautors. Sogar bei meiner eigenen Arbeit kann es vorkommen, dass ich eine Version eines Projekts (einen Zweig) mit einer experimentellen Funktion haben möchte, die von der stabilen Version, die ohne Überraschungen laufen muss, getrennt werden sollte.

  • Auf GitHub gibt es derzeit etwa 314.000 Projekte, die nach eigenen Angaben hauptsächlich in C geschrieben sind, und es gibt noch mehr C-Projekte in anderen, kleineren RCS-Repositorien, wie z. B. in der GNU Savannah. Selbst wenn du den Code nicht verändern willst, ist das Klonen dieser Repositories ein schneller Weg, um das Programm oder die Bibliothek auf deine Festplatte zu bekommen und selbst zu nutzen. Wenn dein eigenes Projekt für die öffentliche Nutzung bereit ist (oder schon vorher), kannst du das Repository als weitere Verbreitungsmöglichkeit veröffentlichen.

  • Da du und ich beide Versionen desselben Projekts haben und beide gleichermaßen die Möglichkeit haben, unsere Versionen der Codebasis zu hacken, gibt uns die Versionskontrolle die Möglichkeit, unsere verschiedenen Threads so einfach wie möglich zusammenzuführen.

In diesem Kapitel geht es um Git, ein verteiltes Revisionskontrollsystem, das bedeutet, dass jede Kopie des Projekts als eigenständiges Repository des Projekts und seiner Geschichte funktioniert. Es gibt noch weitere Systeme, allen voran Mercurial und Bazaar, die zu den Spitzenreitern in dieser Kategorie gehören. Die Funktionen dieser Systeme sind größtenteils eins zu eins aufeinander abgestimmt, und die größten Unterschiede haben sich im Laufe der Jahre aufgelöst, so dass du nach dem Lesen dieses Kapitels in der Lage sein solltest, die anderen Systeme sofort zu verstehen.

Änderungen über diff

Die rudimentärste Art der Versionskontrolle ist die über diff und patch, die POSIX-Standard sind und sich daher ganz sicher auf deinem System befinden. Wahrscheinlich hast du irgendwo auf deiner Festplatte zwei Dateien, die sich einigermaßen ähneln. Wenn nicht, nimm eine beliebige Textdatei, ändere ein paar Zeilen und speichere die geänderte Version unter einem neuen Namen. Versuche es:

diff f1.c f2.c

und du erhältst eine Auflistung, die eher maschinenlesbar als menschenlesbar ist und in der die Zeilen angezeigt werden, die sich zwischen den beiden Dateien geändert haben. Weiterleitung der Ausgabe in eine Textdatei über diff f1.c f2.c >diffs und dann öffnen diffs in deinem Texteditor öffnest, erhältst du eine farbige Version, die leichter zu verstehen ist. Du wirst einige Zeilen sehen, die den Namen der Datei und den Ort innerhalb der Datei angeben, vielleicht ein paar Zeilen Kontext, die sich zwischen den beiden Dateien nicht geändert haben, und Zeilen, die mit + und - beginnen und die Zeilen zeigen, die hinzugefügt und entfernt wurden. Führe diff mit dem Flag -u aus, um ein paar Zeilen Kontext zu den Hinzufügungen und Entfernungen zu erhalten.

Gegeben sind zwei Verzeichnisse mit zwei Versionen deines Projekts, v1 und v2Erstelle mit der Option rekursiv (-r) eine einzige Diff-Datei im Unified-Diff-Format für die gesamten Verzeichnisse:

diff -ur v1 v2 > diff-v1v2

Der Befehl patch liest diff-Dateien und führt die dort aufgeführten Änderungen aus. Wenn du und ein Freund beide eine v1 des Projekts haben, könntest du diff-v1v2 an deine Freundin schicken und sie könnte es ausführen:

patch < diff-v1v2

um alle deine Änderungen auf ihre Kopie von v1.

Wenn du keine Freunde hast, kannst du diff von Zeit zu Zeit auf deinem eigenen Code laufen lassen und so die Änderungen, die du im Laufe der Zeit vorgenommen hast, aufzeichnen. Wenn du feststellst, dass du einen Fehler in deinen Code eingebaut hast, sind die Diffs der erste Ort, an dem du nach Hinweisen darauf suchst, was du angefasst hast, was du nicht hättest tun sollen. Wenn das nicht ausreicht und du bereits gelöscht hast v1gelöscht hast, kannst du den Patch in umgekehrter Reihenfolge aus dem v2 Verzeichnis, patch -R < diff-v1v2und so die Version 2 auf die Version 1 zurücksetzen. Wenn du bei Version 4 bist, könntest du sogar eine Reihe von Diffs ausführen, um noch weiter in der Zeit zurückzugehen:

cd v4
patch -R < diff-v3v4
patch -R < diff-v2v3
patch -R < diff-v1v2

Ich sage "denkbar", weil die Pflege einer solchen Reihe von Diffs mühsam und fehleranfällig ist. Deshalb gibt es das Revisionskontrollsystem, das die Diffs für dich erstellt und verfolgt.

Git's Objekte

Git ist ein C-Programm wie jedes andere und basiert auf einer kleinen Anzahl von Objekten. Das wichtigste Objekt ist das Commit-Objekt, das einer einheitlichen Diff-Datei ähnelt. Ausgehend von einem früheren Commit-Objekt und einigen Änderungen an dieser Baseline, werden die Informationen in einem neuen Commit-Objekt zusammengefasst. Es wird durch den Index unterstützt, der eine Liste der seit dem letzten Commit-Objekt registrierten Änderungen enthält, die in erster Linie für die Erstellung des nächsten Commit-Objekts verwendet wird.

Die Commit-Objekte sind miteinander verbunden und bilden einen Baum, wie jeder andere Baum auch. Jedes Commit-Objekt hat (mindestens) ein übergeordnetes Commit-Objekt. Das Auf- und Absteigen im Baum ist vergleichbar mit der Verwendung von patch und patch -R, um zwischen den Versionen zu wechseln.

Das Repository selbst ist formal kein einzelnes Objekt im Git-Quellcode, aber ich betrachte es als ein Objekt, weil die üblichen Operationen, die man definieren würde, wie new, copy und free, für das gesamte Repository gelten. Erstelle ein neues Repository in dem Verzeichnis, in dem du arbeitest, über:

git init

OK, du hast jetzt ein Revisionskontrollsystem eingerichtet. Du wirst es vielleicht nicht sehen, denn Git speichert alle Dateien in einem Verzeichnis mit dem Namen .git, wobei der Punkt bedeutet, dass alle üblichen Dienstprogramme wie ls es als versteckt betrachten. Du kannst sie z.B. über ls -a oder über die Option "Versteckte Dateien anzeigen" in deinem bevorzugten Dateimanager suchen.

Alternativ kannst du ein Repository auch über git clone kopieren. So würdest du ein Projekt von Savannah oder Github bekommen. Um den Quellcode für Git über git zu erhalten:

git clone https://github.com/gitster/git.git

Der Leser kann auch daran interessiert sein, das Repository mit den Beispielen für dieses Buch zu klonen:

git clone https://github.com/b-k/21st-Century-Examples.git

Wenn du etwas mit einem Repository in ~/myrepo testen willst und Angst hast, dass du etwas kaputt machst, gehst du in ein temporäres Verzeichnis (z.B. mkdir ~/tmp; cd ~/tmp), klonst dein Repository mit git clone ~/myrepound experimentiere los. Wenn du den Klon anschließend löschst (rm -rf ~/tmp/myrepo) hat keine Auswirkungen auf das Original.

Da sich alle Daten über ein Repository im Unterverzeichnis .git deines Projektverzeichnisses befinden, ist die Analogie zum Freigeben eines Repositorys einfach:

rm -rf .git

Da das gesamte Repository so in sich geschlossen ist, kannst du ohne viel Aufwand Ersatzkopien erstellen, um sie zwischen Zuhause und der Arbeit hin und her zu schieben, oder alles in ein temporäres Verzeichnis für ein schnelles Experiment kopieren und so weiter.

Wir sind fast so weit, einige Commit-Objekte zu erzeugen, aber da sie die Unterschiede seit dem Startpunkt oder einem früheren Commit zusammenfassen, müssen wir einige Unterschiede zum Commit zur Hand haben. Der Index (Git-Quelle: struct index_state) ist eine Liste von Änderungen, die im nächsten Commit gebündelt werden sollen. Es gibt ihn, weil wir nicht wollen, dass jede Änderung im Projektverzeichnis aufgezeichnet wird. Zum Beispiel, gnomes.c und gnomes.h wird zu gnomes.o und die ausführbare Datei gnomes. Dein RCS sollte gnomes.c und gnomes.h verfolgen und die anderen nach Bedarf neu generieren lassen. Die wichtigste Operation mit dem Index ist also das Hinzufügen von Elementen zu seiner Liste der Änderungen. Verwenden:

git add gnomes.c gnomes.h

um diese Dateien in den Index aufzunehmen. Andere typische Änderungen an der Liste der verfolgten Dateien müssen ebenfalls in den Index aufgenommen werden:

git add newfile
git rm oldfile
git mv flie file

Änderungen, die du an Dateien vorgenommen hast, die bereits von Git verfolgt werden, werden nicht automatisch zum Index hinzugefügt, was für Nutzer anderer RCSes eine Überraschung sein könnte (aber siehe unten). Füge sie einzeln über git add hinzu. changedfile, oder verwende:

git add -u

um Änderungen an allen Dateien, die Git bereits verfolgt, in den Index aufzunehmen.

Irgendwann hast du so viele Änderungen im Index, dass sie als Commit-Objekt im Repository aufgezeichnet werden sollten. Erzeuge ein neues Commit-Objekt über:

git commit -a -m "here is an initial commit."

Das Flag -m fügt der Revision eine Nachricht hinzu, die du später unter git log lesen kannst. Wenn du die Nachricht weglässt, startet Git den Texteditor, der in der Umgebungsvariablen EDITOR angegeben ist, damit du ihn eingeben kannst (der Standardeditor ist normalerweise vi; exportiere diese Variable in das Startskript deiner Shell, z. B. .bashrc oder .zshrc, wenn du etwas anderes möchtest).

Das -a Flag sagt Git, dass die Chancen gut stehen, dass ich vergessen habe, git add -u auszuführen, also führe es bitte kurz vor dem Commit aus. In der Praxis bedeutet das, dass du git add -u nie explizit ausführen musst, solange du dich immer an das -a Flag in git erinnerst commit -a.

Warnung

Es ist leicht, Git-Experten zu finden, die darauf bedacht sind, aus ihren Commits einen zusammenhängenden, sauberen Bericht zu erstellen. Anstelle von Commit-Meldungen wie "ein Index-Objekt hinzugefügt und nebenbei ein paar Fehler behoben", würde ein Git-Experte zwei Commits erstellen, einen mit der Meldung "ein Index-Objekt hinzugefügt" und einen mit "Fehler behoben". Diese Autoren haben eine solche Kontrolle, weil dem Index standardmäßig nichts hinzugefügt wird. Sie können also nur so viel hinzufügen, dass sie eine genaue Änderung im Code ausdrücken, den Index in ein Commit-Objekt schreiben und dann einen neuen Satz von Elementen zu einem sauberen Index hinzufügen, um das nächste Commit-Objekt zu erzeugen.

Ich habe einen Blogger gefunden, der seine Commit-Routine auf mehreren Seiten beschreibt: "Für die kompliziertesten Fälle drucke ich die Diffs aus, lese sie durch und markiere sie mit sechs verschiedenen Textmarkern..." Solange du kein Git-Experte bist, hast du jedoch viel mehr Kontrolle über den Index, als du wirklich brauchst oder willst. Das heißt, -a nicht mit git commit zu verwenden, ist eine fortgeschrittene Anwendung, mit der sich viele Leute nie befassen. In einer perfekten Welt wäre -a der Standard, aber das ist er nicht, also vergiss ihn nicht.

Wenn du git commit -a aufrufst, wird ein neues Commit-Objekt in das Repository geschrieben, das auf allen Änderungen basiert, die der Index erfassen konnte, und der Index wird gelöscht. Nachdem du deine Arbeit gespeichert hast, kannst du nun weitere Änderungen hinzufügen. Außerdem - und das ist der größte Vorteil der Revisionskontrolle - kannst du alles löschen, was du willst, und dich darauf verlassen, dass es wiederhergestellt werden kann, wenn du es wieder brauchst. Verstopfe deinen Code nicht mit großen Blöcken auskommentierter, veralteter Routinen - lösche!

Hinweis

Nachdem du einen Commit durchgeführt hast, wirst du mit Sicherheit feststellen, dass du etwas vergessen hast. Anstatt einen weiteren Commit durchzuführen , kannst du git commit --amend -a aufrufen, um deinen letzten Commit zu wiederholen.

Nachdem du ein Commit-Objekt erstellt hast, besteht deine Interaktion mit ihm hauptsächlich darin, seinen Inhalt zu betrachten. Du wirst git diff verwenden, um die Diffs zu sehen, die den Kern des Commit-Objekts bilden, und git log, um die Metadaten zu sehen.

Die wichtigsten Metadaten sind der Name des Objekts, der über eine unangenehme, aber sinnvolle Namenskonvention zugewiesen wird: der SHA1-Hash, eine 40-stellige Hexadezimalzahl, die einem Objekt zugewiesen werden kann, und zwar so, dass wir davon ausgehen können, dass keine zwei Objekte denselben Hash haben und dass dasselbe Objekt in jeder Kopie des Repository denselben Namen hat. Wenn du deine Dateien überträgst, siehst du die ersten Ziffern des Hashes auf dem Bildschirm und kannst git log ausführen, um die Liste der übertragenen Objekte in der Historie des aktuellen Übertragungsobjekts zu sehen, aufgelistet nach ihrem Hash und der Nachricht in menschlicher Sprache, die du beim Übertragen geschrieben hast (und siehe git help log für die anderen verfügbaren Metadaten). Glücklicherweise brauchst du nur so viel von dem Hash, wie du für die eindeutige Identifizierung deines Commits benötigst. Wenn du dir also das Log ansiehst und beschließt, dass du die Revisionsnummer fe9c49cddac5150dc974de1f7248a1c5e3b33e89 überprüfen möchtest, kannst du das mit:

git checkout fe9c4

Dies macht die Art von Zeitreise über Diffs, die patch fast zur Verfügung gestellt hat, indem es zum Stand des Projekts bei Commit fe9c4 zurückspult.

Da ein bestimmter Commit nur Zeiger auf seine Eltern, nicht aber auf seine Kinder hat, siehst du, wenn du git log aufrufst, nachdem du einen alten Commit ausgecheckt hast, die Spur der Objekte, die zu diesem Commit geführt haben, aber nicht die späteren Commits. Das selten genutzte git reflog zeigt dir die vollständige Liste der Commit-Objekte, die dem Repository bekannt sind, aber der einfachere Weg, um zur aktuellsten Version des Projekts zurückzuspringen, ist über ein Tag, einen menschenfreundlichen Namen, den du nicht im Log nachschlagen musst. Tags werden als separate Objekte im Projektarchiv verwaltet und enthalten einen Zeiger auf ein Commit-Objekt, das mit einem Tag versehen ist. Das am häufigsten verwendete Tag ist master, das sich auf das letzte Commit-Objekt im Master-Zweig bezieht (der, da wir noch nicht über Verzweigungen gesprochen haben, wahrscheinlich der einzige Zweig ist, den du hast). Wenn du also von einem früheren Zeitpunkt zum neuesten Stand zurückkehren willst, benutze:

git checkout master

Um auf git diff zurückzukommen, wird angezeigt, welche Änderungen du seit der letzten Commit-Revision vorgenommen hast. Die Ausgabe ist das, was in das nächste Commit-Objekt geschrieben werden würde über git commit -a. Wie bei der Ausgabe des einfachen Programms diff wird auch git diff > diffs in eine Datei geschrieben, die in deinem farbigen Texteditor vielleicht besser lesbar ist.

Ohne Argumente zeigt git diff den Unterschied zwischen dem Index und dem, was sich im Projektverzeichnis befindet; wenn du noch nichts zum Index hinzugefügt hast, sind das alle Änderungen seit der letzten Übertragung. Mit einem Commit-Objektnamen zeigt git diff die Reihenfolge der Änderungen zwischen diesem Commit und dem Projektverzeichnis an. Bei zwei Namen zeigt es die Reihenfolge der Änderungen von einem Commit zum anderen:

git diff                Show the diffs between the working directory and the index.
git diff --staged       Show the diffs between the index and the previous commit.
git diff 234e2a         Show the diffs between the working directory and the given commit object.
git diff 234e2a 8b90ac  Show the changes from one commit object to another.
Hinweis

Es gibt ein paar Vereinfachungen bei der Namensgebung, damit du dir das Hexadezimalsystem sparen kannst. Der Name HEAD bezieht sich auf den letzten ausgecheckten Commit. Normalerweise ist das die Spitze eines Zweigs; wenn das nicht der Fall ist, wird dies in den Git-Fehlermeldungen als "detached HEAD" bezeichnet.

Füge ~1 an einen Namen an, um auf den Elternteil des benannten Commits zu verweisen, ~2, um auf den Großelternteil zu verweisen, und so weiter. Die folgenden Angaben sind also alle gültig:

git diff HEAD~4         #Compare the working directory to four commits ago.
git checkout master~1   #Check out the predecessor to the head of the master branch.
git checkout master~    #Shorthand for the same.
git diff b0897~ b8097   #See what changed in commit b8097.

Zu diesem Zeitpunkt weißt du, wie man es macht:

  • Speichere häufige, schrittweise Überarbeitungen deines Projekts.

  • Erhalte ein Protokoll der von dir vorgenommenen Revisionen.

  • Finde heraus, was du kürzlich geändert oder hinzugefügt hast.

  • Schau dir frühere Versionen an, damit du bei Bedarf frühere Arbeiten wiederherstellen kannst.

Wenn du ein Backup-System hast, das so organisiert ist, dass du den Code getrost löschen und bei Bedarf wiederherstellen kannst, wirst du bereits ein besserer Autor sein.

Das Versteck

Commit-Objekte sind die Referenzpunkte, von denen aus die meisten Git-Aktivitäten stattfinden. Git wendet Patches bevorzugt relativ zu einem Commit an, und du kannst zu jedem Commit springen. Wenn du aber von einem Arbeitsverzeichnis wegspringst, das nicht zu einem Commit passt, hast du keine Möglichkeit, zurückzuspringen. Wenn es im aktuellen Arbeitsverzeichnis noch nicht übertragene Änderungen gibt, warnt Git dich, dass du dich nicht an einem Commit befindest, und verweigert in der Regel die Ausführung des Vorgangs, um den du es gebeten hast. Eine Möglichkeit, zu einem Commit zurückzukehren, besteht darin, alle Arbeiten, die du seit dem letzten Commit gemacht hast, aufzuschreiben, dein Projekt auf den letzten Commit zurückzusetzen, die Operation auszuführen und dann die gespeicherte Arbeit erneut zu machen, nachdem du mit dem Springen oder Patchen fertig bist.

Deshalb verwenden wir den Stash, ein spezielles Commit-Objekt, das im Wesentlichen dem entspricht, was du von git commit -a bekommst, aber mit ein paar besonderen Funktionen, wie z.B. dem Zurückhalten des ganzen nicht getrackten Mülls in deinem Arbeitsverzeichnis. Hier ist der typische Ablauf:

git stash
# Code is now as it was at last checkin.
git checkout fe9c4

# Look around here.

git checkout master    # Or whatever commit you had started with
# Code is now as it was at last checkin, so replay stashed diffs with:
git stash pop

Eine andere, manchmal geeignete Alternative zum Auschecken von Änderungen in deinem Arbeitsverzeichnis ist git reset --hard. Damit wird das Arbeitsverzeichnis in den Zustand zurückversetzt, in dem es sich beim letzten Auschecken befand. Der Befehl klingt streng, weil er es auch ist: Du bist dabei, alle Arbeit, die du seit dem letzten Auschecken gemacht hast, wegzuwerfen.

Bäume und ihre Äste

Es gibt einen Baum in einem Repository, der erzeugt wurde, als der erste Autor eines neuen Repositorys git init ausgeführt hat. Du bist wahrscheinlich mit Datenstrukturen vertraut, die aus einer Menge von Knoten bestehen, wobei jeder Knoten Links zu einer bestimmten Anzahl von Kindern und einem Link zu einem Elternteil hat (und in exotischen Bäumen wie dem von Git möglicherweise zu mehreren Elternteilen).

Tatsächlich haben alle Commit-Objekte außer dem ersten einen Elternteil, und das Objekt zeichnet die Unterschiede zwischen sich selbst und dem Eltern-Commit auf. Der letzte Knoten in der Sequenz, die Spitze des Zweigs, ist mit einem Zweignamen versehen. Für unsere Zwecke gibt es eine Eins-zu-Eins-Entsprechung zwischen den Zweigspitzen und der Reihe von Diffs, die zu diesem Zweig geführt haben. Die Eins-zu-Eins-Entsprechung bedeutet, dass wir uns auf Zweige und das Commit-Objekt an der Spitze des Zweigs beziehen können. Wenn also die Spitze des Zweigs master der Commit 234a3d ist, dann sind git checkout master und git checkout 234a3d völlig gleichwertig (bis ein neuer Commit geschrieben wird, der dann die Bezeichnung master erhält). Das bedeutet auch, dass die Liste der Commit-Objekte auf einem Zweig jederzeit wiederhergestellt werden kann, indem man an der Commit-Spitze beginnt und bis zum Ursprung des Baums zurückgeht.

In der Regel wird der Master-Zweig immer voll funktionsfähig gehalten. Wenn du eine neue Funktion hinzufügen oder eine neue Fragestellung ausprobieren willst, erstellst du einen neuen Zweig dafür. Wenn der Zweig voll funktionsfähig ist, kannst du die neue Funktion mit den folgenden Methoden wieder in den Masterzweig einfügen.

Es gibt zwei Möglichkeiten, einen neuen Zweig zu erstellen, der sich vom aktuellen Stand deines Projekts abspaltet:

git branch newleaf       # Create a new branch...
git checkout newleaf     # then check out the branch you just created.
    # Or execute both steps at once with the equivalent:
git checkout -b newleaf

Nachdem du den neuen Zweig erstellt hast, wechsle zwischen den Spitzen der beiden Zweige über git checkout master und git checkout newleaf.

Auf welchem Zweig bist du gerade? Finde es heraus mit:

git branch

die alle Zweige auflistet und ein * bei dem Zweig einfügt, der gerade aktiv ist.

Was würde passieren, wenn du eine Zeitmaschine bauen, in die Zeit vor deiner Geburt zurückreisen und deine Eltern töten würdest? Wenn wir etwas aus der Science-Fiction gelernt haben, dann, dass sich die Gegenwart nicht ändert, wenn wir die Geschichte ändern, sondern eine neue, alternative Geschichte entsteht. Wenn du also eine alte Version auscheckst, Änderungen vornimmst und ein neues Commit-Objekt mit deinen neuen Änderungen eincheckst, hast du jetzt einen neuen Zweig, der sich vom Master-Zweig unterscheidet. Über git branch findest du heraus, dass du auf (no branch) bist, wenn sich die Vergangenheit auf diese Weise gabelt. Nicht gekennzeichnete Zweige können zu Problemen führen. Wenn du also jemals feststellst, dass du auf (no branch) arbeitest, dann führe git branch -m new_branch_name um den Zweig zu benennen, in den du gerade gesplittet hast.

Zusammenführung

Bisher haben wir neue Commit-Objekte erzeugt, indem wir mit einem Commit-Objekt als Ausgangspunkt begonnen und eine Liste von Diffs aus dem Index angewendet haben. Ein Zweig ist auch eine Reihe von Diffs. Wenn wir also ein beliebiges Commit-Objekt und eine Liste von Diffs aus einem Zweig haben, sollten wir in der Lage sein, ein neues Commit-Objekt zu erstellen, in dem die Diffs des Zweigs auf das bestehende Commit-Objekt angewendet werden. Dies ist eine Zusammenführung. Um alle Änderungen, die im Laufe des Jahres newleaf wieder in master zusammenzuführen, wechsle zu master und benutze git merge:

git checkout master
git merge newleaf

Wenn du z.B. einen Zweig von master benutzt hast, um eine neue Funktion zu entwickeln, und diese schließlich alle Tests besteht, wird durch die Anwendung aller Diffs aus dem Entwicklungszweig auf master ein neues Commit-Objekt erstellt, in dem die neue Funktion fest verankert ist.

Nehmen wir an, du hast während der Arbeit an dem neuen Feature nie master ausgecheckt und somit keine Änderungen daran vorgenommen. Dann wäre das Anwenden der Diffs aus dem anderen Zweig einfach eine schnelle Wiederholung aller Änderungen, die in den einzelnen Commit-Objekten des Zweigs aufgezeichnet wurden, was Git einen Schnellvorlauf nennt.

Wenn du aber Änderungen an master vorgenommen hast, geht es nicht mehr nur um eine schnelle Anwendung aller Diffs. Nehmen wir zum Beispiel an, dass gnomes.c an der Stelle, an der sich der Zweig abgespalten hat, Folgendes enthielt:

short int height_inches;

In master hast du die abwertende Art entfernt:

int height_inches;

Der Zweck der newleaf war die Umrechnung ins metrische System:

short int height_cm;

An diesem Punkt ist Git ratlos. Um zu wissen, wie man diese Zeilen kombiniert, muss man wissen, was du als Mensch beabsichtigt hast. Die Lösung von Git besteht darin, deine Textdatei so zu ändern, dass sie beide Versionen enthält, etwa so:

<<<<<<< HEAD
int height_inches;
=======
short int height_cm;
>>>>>>> 3c3c3c

Die Zusammenführung wird auf Eis gelegt und wartet darauf, dass du die Datei so bearbeitest, dass sie die von dir gewünschte Änderung enthält. In diesem Fall würdest du wahrscheinlich das fünfzeilige Stück, das Git in der Textdatei übrig gelassen hat, reduzieren:

int height_cm;

Hier ist die Vorgehensweise für das Commit von Zusammenführungen in einem Non-Fast-Forward, was bedeutet, dass es in beiden Zweigen Änderungen gegeben hat, seit sie auseinandergegangen sind:

  1. Lauf git merge other_branch.

  2. Höchstwahrscheinlich wird dir gesagt, dass es Konflikte gibt, die du lösen musst.

  3. Überprüfe die Liste der nicht zusammengefassten Dateien mit git status.

  4. Wähle eine Datei aus, die du manuell überprüfen willst. Öffne sie in einem Texteditor und finde die Zusammenführungsmarkierungen, wenn es sich um einen inhaltlichen Konflikt handelt. Wenn es sich um einen Konflikt mit dem Dateinamen oder der Dateiposition handelt, verschiebe die Datei an ihren Platz.

  5. Lauf git add your_now_fixed_file.

  6. Wiederhole die Schritte 3-5, bis alle nicht gemischten Dateien eingecheckt sind.

  7. Führe git commit aus, um die Zusammenführung abzuschließen.

Tröste dich mit all dieser manuellen Arbeit. Git ist beim Zusammenführen konservativ und wird nichts automatisch tun, was in einer bestimmten Situation dazu führen könnte, dass du Arbeit verlierst.

Wenn du mit dem Zusammenführen fertig bist, sind alle relevanten Diffs, die im Seitenzweig aufgetreten sind, im endgültigen Commit-Objekt des zusammengeführten Zweigs enthalten:

git branch -d other_branch

Der other_branch Tag wird gelöscht, aber die Commit-Objekte, die zu diesem Tag geführt haben, sind immer noch im Repository zu deiner Information vorhanden.

Der Rebase

Angenommen, du hast einen Hauptzweig, von dem du am Montag einen Testzweig abspaltest. Dann nimmst du von Dienstag bis Donnerstag umfangreiche Änderungen sowohl am Haupt- als auch am Testzweig vor. Wenn du am Freitag versuchst, den Testzweig wieder mit dem Hauptzweig zusammenzuführen, hast du eine überwältigende Anzahl kleiner Konflikte zu lösen.

Beginnen wir die Woche von vorne. Am Montag hast du den Testzweig vom Hauptzweig abgetrennt, d.h. die letzten Commits in beiden Zweigen haben einen gemeinsamen Vorgänger, den Commit vom Montag im Hauptzweig. Am Dienstag gibt es einen neuen Commit auf dem Hauptzweig, zum Beispiel den Commit abcd123. Am Ende des Tages spielst du alle Diffs, die auf dem Hauptzweig stattgefunden haben, auf den Testzweig zurück:

git branch testing  # get on the testing branch
git rebase abcd123  # or equivalently: git rebase main

Mit dem Befehl rebase werden alle Änderungen, die auf dem Hauptzweig seit dem gemeinsamen Vorgänger gemacht wurden, auf dem Testzweig wiedergegeben. Es kann sein, dass du Dinge manuell zusammenführen musst, aber dadurch, dass wir nur die Arbeit eines Tages zusammenführen müssen, ist die Aufgabe des Zusammenführens hoffentlich überschaubarer.

Da nun alle Änderungen bis abcd123 in beiden Zweigen vorhanden sind, ist es so, als hätten sich die Zweige tatsächlich von diesem Commit abgespalten und nicht von dem Commit am Montag. Daher kommt auch der Name der Prozedur: Der Testzweig wurde so umgestellt, dass er sich von einem neuen Punkt des Hauptzweigs abspaltet.

Auch am Mittwoch, Donnerstag und Freitag führst du einen Rebase durch, und jeder dieser Rebases ist relativ schmerzlos, da der Testzweig die ganze Woche über mit den Änderungen im Hauptzweig Schritt gehalten hat.

Rebases werden oft als fortschrittliche Anwendung von Git dargestellt, weil andere Systeme, die nicht so gut mit Diffs umgehen können, diese Technik nicht haben. In der Praxis sind Rebase und Merging jedoch gleichwertig: Beide wenden Diffs von einem anderen Zweig an, um einen Commit zu erzeugen. Die einzige Frage ist, ob du die Enden zweier Zweige zusammenführen willst (in diesem Fall merge) oder ob die beiden Zweige noch eine Weile ihr getrenntes Leben weiterführen sollen (in diesem Fall rebase). In der Regel werden die Diffs aus dem Master-Zweig in den Side-Branch umbasiert und die Diffs aus dem Side-Branch in den Master-Zweig zusammengeführt, so dass in der Praxis eine Symmetrie zwischen den beiden besteht. Wie bereits erwähnt, können sich die Diffs auf mehreren Zweigen anhäufen, was den endgültigen Merge zu einem Problem machen kann, daher ist es ratsam, relativ häufig zu rebasen.

Entfernte Repositorien

Bis zu diesem Punkt hat sich alles innerhalb eines Baums abgespielt. Wenn du ein Repository von einem anderen geklont hast, haben du und der Ursprung zum Zeitpunkt des Klonens identische Bäume mit identischen Commit-Objekten. Du und deine Kolleginnen und Kollegen werden jedoch weiterarbeiten, sodass ihr alle neue und unterschiedliche Commit-Objekte hinzufügen werdet.

Dein Repository hat eine Liste von Remotes, also Verweisen auf andere Repositories, die mit diesem Repository irgendwo auf der Welt verbunden sind. Wenn du dein Repository über git clone erhalten hast, dann heißt das Repository, von dem du geklont hast, origin, soweit es das neue Repository betrifft. Im Normalfall ist dies das einzige Repository, das du jemals benutzen wirst.

Wenn du zum ersten Mal klonst und git branch aufrufst, siehst du nur einen einzigen Zweig, unabhängig davon, wie viele Zweige das ursprüngliche Repository hatte. Wenn du aber git branch -a aufrufst, um alle Zweige zu sehen, die Git kennt, siehst du sowohl die Zweige im entfernten als auch die im lokalen Repository. Wenn du ein Repository von Github und Co. geklont hast, kannst du damit überprüfen, ob andere Autoren andere Zweige in das zentrale Repository gepusht haben.

Diese Kopien der Zweige in deinem lokalen Repository sind auf dem Stand des ersten Pulls. Um die entfernten Zweige nächste Woche mit den Informationen aus dem Ursprungs-Repository zu aktualisieren, führe git fetch aus.

Da du nun aktuelle Kopien der entfernten Zweige in deinem Repository hast, kannst du einen davon mit dem lokalen Zweig, an dem du gerade arbeitest, zusammenführen, indem du den vollständigen Namen des entfernten Zweigs verwendest, zum Beispiel git merge remotes/origin/master.

Anstelle des zweistufigen git fetch; git merge remotes/origin/master kannst du den Zweig auch über

git pull origin master

der die Änderungen aus der Ferne holt und sie auf einmal in dein aktuelles Repository einfügt.

Der umgekehrte Fall ist push, mit dem du das entfernte Repository mit deinem letzten Commit aktualisieren kannst (nicht mit dem Status deines Index oder Arbeitsverzeichnisses). Wenn du an einem Zweig mit dem Namen bbranch arbeitest und einen Push an das entfernte Repository mit demselben Namen durchführen willst, verwende:

git push origin bbranch

Wenn du deine Änderungen pushst, stehen die Chancen gut, dass die Anwendung der Diffs von deinem Zweig auf den entfernten Zweig kein Fast-Forward ist (wenn doch, dann haben deine Kollegen nicht gearbeitet). Das Auflösen eines nicht-schnellen Zusammenschlusses erfordert in der Regel einen menschlichen Eingriff, und auf dem entfernten Zweig gibt es wahrscheinlich keinen Menschen. Deshalb lässt Git nur Fast-Forward-Pushes zu. Wie kannst du garantieren, dass dein Push ein Fast-Forward ist?

  1. Führe git pull origin bbranch um die Änderungen zu erhalten, die seit deinem letzten Pull vorgenommen wurden.

  2. Zusammenführen wie oben beschrieben, wobei du als Mensch die Änderungen auflöst, die ein Computer nicht auflösen kann.

  3. Lauf git commit -a -m "dealt with merges".

  4. Führe git push origin aus. bbranch, denn jetzt muss Git nur noch einen einzigen Diff anwenden, was automatisch geschehen kann.

Bisher bin ich davon ausgegangen, dass du dich in einem lokalen Zweig befindest, der denselben Namen trägt wie der entfernte Zweig (wahrscheinlich master auf beiden Seiten). Wenn du Namen kreuzt, gibst du ein durch Doppelpunkt getrenntes Paar von source:destination Zweignamen an.

git fetch origin new_changes:master #Merge remote new_changes into local master
git push origin my_fixes:version2  #Merge the local branch into a differently named remote.
git push origin :prune_me          #Delete a remote branch.
git fetch origin new_changes:      #Pull to no branch; create a commit named FETCH_HEAD.

Keine dieser Operationen ändert deinen aktuellen Zweig, aber einige erstellen einen neuen Zweig, zu dem du über die übliche git checkout wechseln kannst.

Die Struktur eines Git-Repositorys ist nicht besonders komplex: Es gibt Commit-Objekte, die die Änderungen seit dem übergeordneten Commit-Objekt darstellen und in einem Baum organisiert sind. Aber mit diesen Elementen kannst du mehrere Versionen deiner Arbeit organisieren, vertrauensvoll Dinge löschen, experimentelle Zweige erstellen und sie wieder mit dem Hauptstrang zusammenführen, wenn sie alle Tests bestanden haben, und die Arbeit deiner Kollegen mit deiner eigenen zusammenführen. Auf git help und in deiner Lieblings-Internetsuchmaschine findest du noch viele weitere Tricks und Möglichkeiten, um diese Dinge noch reibungsloser zu erledigen.

Get 21st Century C, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.