Git 🌙
Chapters ▾ 2nd Edition

5.2 Verteiltes Git - An einem Projekt mitwirken

An einem Projekt mitwirken

Die größte Schwierigkeit bei der Beschreibung, wie man an einem Projekt mitwirkt, sind die zahlreichen Varianten, wie man das tun könnte. Da Git sehr flexibel ist, können die Beteiligten auf viele Arten zusammenarbeiten. Es ist nicht einfach zu beschreiben, wie du dazu beitragen solltest, da jedes Projekt ein wenig anders ist. Einige der wichtigen Unbekannten sind die Anzahl der aktiven Mitwirkenden, der ausgewählte Arbeitsablauf, deine Zugriffsberechtigung und möglicherweise auch die Methode, wie externe Beiträge verwaltet werden sollen.

Die erste Unbekannte ist die Anzahl der aktiven Mitwirkenden – wie viele Benutzer tragen aktiv Quelltext zu diesem Projekt bei und wie oft? In vielen Fällen hast du zwei oder drei Entwickler mit ein paar Commits pro Tag oder möglicherweise weniger für etwas schlummernde Projekte. Bei größeren Unternehmen oder Projekten kann die Anzahl der Entwickler in die Tausende gehen, wobei jeden Tag Hunderte oder Tausende von Commits getätigt werden können. Das ist deshalb von Bedeutung, da du mit einer wachsenden Anzahl von Entwicklern auch sicherstellen musst, dass sich der Code problemlos anwenden lässt und leicht zusammengeführt werden kann. Von dir übermittelte Änderungen können durch Arbeiten, die während deiner Arbeit oder während deiner Änderungen genehmigt oder eingearbeitet wurden, veraltet sein oder nicht mehr funktionieren. Wie kannst du deinen Quelltext konsistent auf dem neuesten Stand halten und dafür sorgen, dass deine Commits gültig sind?

Die nächste Unbekannte ist der für das Projekt verwendete Arbeitsablauf. Ist er zentralisiert, wobei jeder Entwickler den gleichen Schreibzugriff auf die Hauptentwicklungslinie hat? Verfügt das Projekt über einen Betreuer oder Integrationsmanager, der alle Patches überprüft? Sind alle Patches von Fachleuten geprüft und genehmigt? Bist du selbst in diesen Prozess involviert? Ist ein Leutnant System vorhanden und musst du deine Arbeit zuerst bei diesen einreichen?

Die nächste Unbekannte ist deine Zugriffsberechtigung. Der erforderliche Arbeitsablauf, um zu einem Projekt beizutragen, unterscheidet sich erheblich, wenn du Schreibzugriff auf das Projekt hast, als wenn du diesen nicht hast. Wenn du keinen Schreibzugriff hast, in welcher Reihenfolge erfolgt die Annahme von beigetragener Arbeit? Gibt es überhaupt eine Richtlinie? Wie umfangreich sind die Änderungen, die du jeweils beisteuerst? Wie oft trägst du etwas bei?

All diese Fragen können sich darauf auswirken, wie du effektiv zu einem Projekt beitragen und welche Arbeitsabläufe bevorzugt oder überhaupt für sie verfügbar sind. Wir werden jeden dieser Aspekte in einer Reihe von Anwendungsfällen behandeln, wobei wir mit simplen Beispielen anfangen und später komplexere Szenarios besprechen. Du solltest in der Lage sein, anhand dieser Beispiele die spezifischen Arbeitsabläufe zu erstellen, die du in der Praxis benötigst.

Richtlinien zur Zusammenführung (engl. Commits)

Bevor wir uns mit den spezifischen Anwendungsfällen befassen, findest du hier einen kurzen Hinweis zu Commit-Nachrichten. Ein guter Leitfaden zum Erstellen von Commits und das Befolgen desselben, erleichtert die Arbeit mit Git und die Zusammenarbeit mit anderen erheblich. Das Git-Projekt selber enthält ein Dokument, in dem einige nützliche Tipps zum Erstellen von Commits für die Übermittlung von Patches aufgeführt sind. Du findest diese Tipps im Git-Quellcode in der Datei Documentation/SubmittingPatches.

Deine Einsendungen sollten keine Leerzeichenfehler enthalten. Git bietet eine einfache Möglichkeit, dies zu überprüfen. Führe vor dem Commit git diff --check aus, um mögliche Leerzeichenfehler zu identifizieren und diese aufzulisten.

Ausgabe von `git diff --check`
Abbildung 56. Ausgabe von git diff --check

Wenn du diesen Befehl vor einem Commit ausführst, kannst du feststellen, ob Leerzeichen Probleme auftreten, die andere Entwickler stören könnten.

Als Nächstes, versuchst du, aus jedem Commit einen logisch getrennten Satz von Änderungen zu machen. Versuche deine Änderungen leicht verständlich zu machen. Arbeiten nicht ein ganzes Wochenende an fünf verschiedenen Features und übermittel all diese Änderungen in einem massiven Commit am Montag. Auch wenn du am Wochenende keine Commits durchführst, nutze am Montag die Staging-Area, um deine Änderungen aufzuteilen in wenigstens einen Commit für jeden Teilaspekt mit jeweils einer sinnvollen Nachricht. Wenn einige der Änderungen dieselbe Datei modifizieren, benutze die Anweisung git add --patch, um Dateien partiell zur Staging-Area hinzuzufügen (detailliert dargestellt im Abschnitt Interaktives Stagen). Der Schnappschuss vom Projekt am Ende des Branches ist der Selbe, ob du einen oder fünf Commits durchgeführt hast, solange nur all die Änderungen irgendwann hinzugefügt werden. Versuche also, die Dinge für deine Entwicklerkollegen zu vereinfachen, die deine Änderungen begutachten müssen.

Dieser Ansatz macht es außerdem einfacher, einen Satz von Änderungen zu entfernen oder rückgängig zu machen, falls das später nötig sein sollte. Den Verlauf umschreiben beschreibt eine Reihe nützlicher Git-Tricks zum Umschreiben des Verlaufs oder um interaktiv Dateien zur Staging-Area hinzuzufügen. Verwende diese Werkzeuge, um einen sauberen und leicht verständlichen Verlauf aufzubauen, bevor du deine Arbeit jemand anderem schickst.

Als letztes darf die Commit-Nachricht nicht vergessen werden. Macht man es sich zur Gewohnheit, qualitativ hochwertige Commit-Nachrichten zu erstellen, erleichtert dies die Verwendung und die Zusammenarbeit mit Git erheblich. In der Regel sollte deine Nachrichten mit einer einzelnen Zeile beginnen, die nicht länger als 50 Zeichen ist. Diese sollte deine Änderungen kurz und bündig beschreiben. Darauf folgen eine leere Zeile und eine ausführliche Erläuterung. Für das Git-Projekt ist es erforderlich, dass die ausführliche Erläuterung deine Motivation für die Änderung enthält. Außerdem sollte das Ergebnis deiner Implementierung mit dem vorherigen Verhalten des Projekts gegenüber gestellt werden. Dies ist eine gute Richtlinie, an die man sich halten sollte. Es empfiehlt sich außerdem, die Gegenwartsform des Imperativs in diesen Nachrichten zu benutzen. Mit anderen Worten, verwende Anweisungen. Anstatt „Ich habe Test hinzugefügt für“ oder „Tests hinzufügend für“ benutze „Füge Tests hinzu für“. Hier ist eine Vorlage, die du nutzen kannst. Wir haben sie leicht angepasst. Das Original findest du hier: E-Mail-Vorlage, die ursprünglich von Tim Pope geschrieben wurde:

Kurzfassung der Änderung (50 Zeichen oder weniger)

Gegebenenfalls ausführlicher, erläuternder Text. Pro Zeile etwa 72
Zeichen. In einigen Kontexten wird die erste Zeile wie der Betreff einer
E-Mail gehandelt und der Rest des Textes als Textkörper. Die Leerzeile,
welche die Zusammenfassung vom Text trennt ist von entscheidender Bedeutung
(es sei denn, du lässt den Textkörper ganz weg). Werkzeuge wie rebase
können Fehler machen, wenn du diese Leerzeile nicht einhältst.

Schreiben deine Commit Nachrichten im Imperativ: "Fix bug" und nicht
"Fixed bug" oder "Fixes bug". Diese Konvention stimmt mit Commit-Nachrichten
überein, die von Befehlen wie git merge und git revert generiert werden.

Weitere Absätze folgen nach Leerzeilen.

- Aufzählungszeichen sind auch in Ordnung

- In der Regel wird für das Aufzählungszeichen ein Bindestrich oder ein Sternchen verwendet, gefolgt von einem Leerzeichen.
  Zwischen den einzelnen Aufzählungen werden Leerzeilen eingefügt. Diese Konventionen variieren jedoch.

      Verwende einen hängenden Einzug

Wenn alle deine Commit-Nachrichten diesem Modell folgen, wird es für dich und die Entwickler, mit denen du zusammenarbeitest, viel einfacher sein. Das Git-Projekt selber enthält gut formatierte Commit-Nachrichten. Versuche, git log --no-merges auszuführen, um zu sehen, wie ein gut formatierter Commit-Verlauf des Projekts aussieht.

Anmerkung
Tue das, was wir sagen und nicht das, was wir tun.

Der Kürze halber haben viele der Beispiele in diesem Buch keine gut formatierten Commit-Nachrichten. Stattdessen verwenden wir einfach die Option -m, um git commit auszuführen.

Kurz gesagt, tue es wie wir es sagen und nicht wie wir es tun.

Kleines, privates Team

Das einfachste Setup, auf das du wahrscheinlich stoßen wirst, ist ein privates Projekt mit einem oder zwei anderen Entwicklern. Privat bedeutet in diesem Zusammenhang „closed source“ – es ist für die Außenwelt nicht öffentlich zugänglich. Du und die anderen Entwickler haben alle Schreibzugriff (Push-Zugriff) auf das Repository.

In dieser Umgebung kannst du einem Arbeitsablauf folgen, der dem ähnelt, den du mit Subversion oder einem anderen zentralisierten System ausführen würdest. Du hast immer noch die Vorteile von Dingen wie Offline-Commit und ein wesentlich einfacheres Verzweigungs- (engl.branching) und Zusammenführungsmodel (engl. merging), aber der Arbeitsablauf kann sehr ähnlich sein. Hauptunterschied ist, dass das Zusammenführen eher auf der Client-Seite stattfindet als auf dem Server beim Durchführen eines Commits. Mal sehen, wie es aussehen könnte, wenn zwei Entwickler beginnen, mit einem gemeinsam genutzten Repository zusammenzuarbeiten. Der erste Entwickler, John, klont das Repository, nimmt eine Änderung vor und commitet es lokal. Die Protokollnachrichten wurden in diesen Beispielen durch …​ ersetzt, um sie etwas zu verkürzen.

# John's Machine
$ git clone john@githost:simplegit.git
Cloning into 'simplegit'...
...
$ cd simplegit/
$ vim lib/simplegit.rb
$ git commit -am 'Remove invalid default value'
[master 738ee87] Remove invalid default value
 1 files changed, 1 insertions(+), 1 deletions(-)

Die zweite Entwicklerin, Jessica, tut dasselbe — sie klont das Repository, ändert etwas und führt einen Commit durch.

# Jessica's Machine
$ git clone jessica@githost:simplegit.git
Cloning into 'simplegit'...
...
$ cd simplegit/
$ vim TODO
$ git commit -am 'Add reset task'
[master fbff5bc] Add reset task
 1 files changed, 1 insertions(+), 0 deletions(-)

Nun lädt Jessica ihre Änderungen auf den Server hoch. Das funktioniert problemlos:

# Jessica's Machine
$ git push origin master
...
To jessica@githost:simplegit.git
   1edee6b..fbff5bc  master -> master

Die letzte Zeile der obigen Ausgabe zeigt eine nützliche Rückmeldung der Push Operation. Das Grundformat ist <oldref> .. <newref> fromref → toref, wobei oldref die alte Referenz bedeutet, newref die neue Referenz bedeutet, fromref der Name der lokalen Referenz ist, die übertragen wird, und toref ist der Name der entfernten Referenz, die aktualisiert werden soll. Eine ähnliche Ausgabe findst du weiter unten in den Diskussionen. Wenn du also ein grundlegendes Verständnis der Bedeutung dieser Angaben hast, dann kannst du die verschiedenen Zustände der Repositorys besser verstehen. Weitere Informationen dazu findest du in der Dokumentation für git-push.

Wenn wir mit diesem Beispiel fortfahren, nimmt John einige Änderungen vor, schreibt sie in sein lokales Repository und versucht, sie auf den gleichen Server zu übertragen:

# John's Machine
$ git push origin master
To john@githost:simplegit.git
 ! [rejected]        master -> master (non-fast forward)
error: failed to push some refs to 'john@githost:simplegit.git'

John ist es nicht gestattet, seine Änderungen hochzuladen, weil Jessica vorher ihre hochgeladen hat. Dies ist wichtig zu verstehen, wenn du an Subversion gewöhnt bist. Wie du sicherlich bemerkt hast, haben die beiden Entwickler nicht dieselbe Datei bearbeitet. Obwohl Subversion eine solche Zusammenführung automatisch auf dem Server durchführt, wenn verschiedene Dateien bearbeitet werden, musst du bei Git die Commits zuerst lokal zusammenführen. Mit anderen Worten, John muss zuerst Jessicas Änderungen abrufen und in seinem lokalen Repository zusammenführen, bevor ihm das Hochladen gestattet wird.

Als ersten Schritt holt John, Jessicas Änderungen (dies holt nur Jessicas Änderungen, diese werden noch nicht mit Johns Änderungen zusammengeführt):

$ git fetch origin
...
From john@githost:simplegit
 + 049d078...fbff5bc master     -> origin/master

Zu diesem Zeitpunkt sieht Johns lokales Repository ungefähr so aus:

Johns abzweigender Verlauf
Abbildung 57. Johns abzweigender Verlauf

Jetzt kann John, Jessicas abgeholte Änderungen, zu seinen eigenen lokalen Änderungen zusammenführen:

$ git merge origin/master
Merge made by the 'recursive' strategy.
 TODO |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

Wenn diese lokale Zusammenführung reibungslos verläuft, sieht der aktualisierte Verlauf von John nun folgendermaßen aus:

Johns Repository nach der Zusammenführung `origin/master`
Abbildung 58. Johns Repository nach der Zusammenführung origin/master

Zu diesem Zeitpunkt möchte John möglicherweise diesen neuen Code testen, um sicherzustellen, dass sich keine der Arbeiten von Jessica auf seine auswirkt. Wenn alles in Ordnung ist, kann er die neu zusammengeführten Änderungen schließlich auf den Server übertragen:

$ git push origin master
...
To john@githost:simplegit.git
   fbff5bc..72bbc59  master -> master

Am Ende sieht Johns Commit-Verlauf so aus:

Johns Verlauf nach Hochladen auf den `origin` Server
Abbildung 59. Johns Verlauf nach Hochladen auf den origin Server

In der Zwischenzeit hat Jessica einen neuen Branch mit dem Namen issue54 erstellt und drei Commits auf diesem Branch vorgenommen. Sie hat Johns Änderungen noch nicht abgerufen, daher sieht ihr Commit-Verlauf folgendermaßen aus:

Jessicas Featurebranch
Abbildung 60. Jessicas Featurebranch

Nun erfährt Jessica, dass John einige neue Arbeiten auf den Server geschoben hat und sie möchte sich diese ansehen. Sie kann alle neuen Inhalte von dem Server abrufen über die sie noch nicht verfügt:

# Jessica's Machine
$ git fetch origin
...
From jessica@githost:simplegit
   fbff5bc..72bbc59  master     -> origin/master

Dies zieht die Arbeit herunter (Pull), die John in der Zwischenzeit hochgeladen hat. Jessicas Verlauf sieht jetzt so aus:

Jessicas Verlauf nach dem Abholen von Johns Änderungen
Abbildung 61. Jessicas Verlauf nach dem Abholen von Johns Änderungen

Jessica denkt, dass ihr Feature Branch nun fertig ist. Sie möchte jedoch wissen, welchen Teil von Johns abgerufenen Arbeiten sie in ihre Arbeit einbinden muss, damit sie hochladen kann. Sie führt git log aus, um das herauszufinden:

$ git log --no-merges issue54..origin/master
commit 738ee872852dfaa9d6634e0dea7a324040193016
Author: John Smith <jsmith@example.com>
Date:   Fri May 29 16:01:27 2009 -0700

   Remove invalid default value

Die Syntax issue54..origin/master ist ein Logfilter, der Git anweist, nur die Commits anzuzeigen, die sich im letzterem Branch befinden (in diesem Fall origin/master) und nicht im ersten Branch (in diesem Fall issue54). Wir werden diese Syntax in Commit-Bereiche genauer erläutern.

Aus der obigen Ausgabe können wir sehen, dass es einen einzigen Commit gibt, den John gemacht hat, welchen Jessica nicht in ihre lokale Arbeit eingebunden hat. Wenn sie origin/master zusammenführt, ist dies der einzige Commit, der ihre lokale Arbeit verändert.

Jetzt kann Jessica ihre Arbeit in ihrem master Branch zusammenführen, Johns Arbeit (origin/master) in ihrem master Branch zusammenführen und dann wieder auf den Server hochladen.

Als erstes wechselt Jessica (nachdem sie alle Änderungen in ihrem FeatureBranch issue54 commitet hat) zurück zu ihrem master Branch, um diese Integration vorzubereiten:

$ git checkout master
Switched to branch 'master'
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.

Jessica kann entweder origin/master oder issue54 mit ihrem lokalem master zusammenführen – beide sind ihrem master vorgelagert. Daher spielt die Reihenfolge keine Rolle. Der finale Schnappschuss sollte unabhängig von der gewählten Reihenfolge identisch sein. Nur der Verlauf wird anders sein. Sie beschließt, zuerst den Branch issue54 zusammenzuführen:

$ git merge issue54
Updating fbff5bc..4af4298
Fast forward
 README           |    1 +
 lib/simplegit.rb |    6 +++++-
 2 files changed, 6 insertions(+), 1 deletions(-)

Es treten keine Probleme auf. Wie du siehst, handelte es sich um eine einfache Schnellvorlauf Zusammenführung (engl. Fast-Forward). Jessica schließt nun den lokalen Zusammenführungsprozess ab, indem sie Johns zuvor abgerufene Arbeit zusammenführt, die sich im Branch origin/master befindet:

$ git merge origin/master
Auto-merging lib/simplegit.rb
Merge made by the 'recursive' strategy.
 lib/simplegit.rb |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

Alles kann sauber zusammengeführt werden. Jessicas Verlauf sieht nun so aus:

Jessicas Verlauf nach Zusammenführung mit Johns Änderungen
Abbildung 62. Jessicas Verlauf nach Zusammenführung mit Johns Änderungen

Jetzt ist origin/master über Jessicas master Branch erreichbar, sodass sie erfolgreich pushen kann (vorausgesetzt, John hat in der Zwischenzeit keine weiteren Änderungen hochgeladen):

$ git push origin master
...
To jessica@githost:simplegit.git
   72bbc59..8059c15  master -> master

Jeder Entwickler hat einige Commits durchgeführt und die Arbeit des jeweils anderen erfolgreich zusammengeführt.

Jessicas Verlauf nach hochladen aller Änderungen auf den Server
Abbildung 63. Jessicas Verlauf nach hochladen aller Änderungen auf den Server

Das ist einer der einfachsten Arbeitsabläufe. Du arbeitest eine Weile (in der Regel in einem Feature Branch) und führst diese Arbeiten in deinem Branch master zusammen, sobald sie für die Integration bereit sind. Wenn du diese Arbeit teilen möchtest, rufst du deinen master von origin/master ab und führst ihn zusammen, falls er sich geändert hat. Anschließend pushst du ihn in den master Branch auf dem Server. Die allgemeine Reihenfolge sieht in etwa so aus:

Allgemeine Abfolge von Ereignissen für einen einfachen Arbeitsablauf mit mehreren Entwicklern
Abbildung 64. Allgemeine Abfolge von Ereignissen für einen einfachen Arbeitsablauf mit mehreren Entwicklern

Geführtes, privates Team

In diesem Szenario sehen wir uns die Rollen der Mitwirkenden in einem größeren, geschlossenen Team an. Du lernst, wie Du in einer Umgebung arbeitest, in der kleine Gruppen an der Entwicklung einzelner Funktionen zusammenarbeiten. Anschließend werden diese teambasierten Beiträge von einem anderen Beteiligten integriert.

Nehmen wir an, John und Jessica arbeiten gemeinsam an einem Feature (nennen wir dieses FeatureA), während Jessica und Josie, eine dritte Entwicklerin, an einem zweiten Feature arbeiten (sagen wir FeatureB). In diesem Fall verwendet das Unternehmen einen Arbeitsablauf mit Integrationsmanager. Bei diesem kann die Arbeit der einzelnen Gruppen nur von bestimmten Beteiligten integriert werden. Der Master-Branch des Haupt Repositorys kann nur von diesen Beteiligten aktualisiert werden kann. In diesem Szenario werden alle Arbeiten in teambasierten Branches ausgeführt und später vom Integrationsmanager zusammengeführt.

Folgen wir Jessicas Arbeitsablauf, während sie an ihren beiden Features tätig ist und parallel mit zwei verschiedenen Entwicklern in dieser Umgebung arbeitet. Wir nehmen an, sie hat ihr Repository bereits geklont. Zuerst beschließt sie an featureA zu arbeiten. Sie erstellt einen neuen Branch für das Feature und führt dort einige Änderungen aus:

# Jessica's Machine
$ git checkout -b featureA
Switched to a new branch 'featureA'
$ vim lib/simplegit.rb
$ git commit -am 'Add limit to log function'
[featureA 3300904] Add limit to log function
 1 files changed, 1 insertions(+), 1 deletions(-)

Zu diesem Zeitpunkt muss sie ihre Arbeit mit John teilen, also lädt sie ihre featureA Branch Commits auf den Server hoch. Jessica hat keinen Push-Zugriff auf den master Branch, nur die Integrationsmanager haben das. Sie muss daher auf einen anderen Branch hochladen, um mit John zusammenzuarbeiten:

$ git push -u origin featureA
...
To jessica@githost:simplegit.git
 * [new branch]      featureA -> featureA

Jessica schickt John eine E-Mail, um ihm mitzuteilen, dass sie einige Arbeiten in einen Branch mit dem Namen featureA hochgeladen hat. Er kann sie sich jetzt ansehen. Während sie auf Rückmeldung von John wartet, beschließt Jessica, mit Josie an featureB zu arbeiten. Zunächst startet sie einen neuen Feature-Branch, der auf dem master Branch des Servers basiert:

# Jessica's Machine
$ git fetch origin
$ git checkout -b featureB origin/master
Switched to a new branch 'featureB'

Jetzt macht Jessica ein paar Commits auf dem Branch featureB:

$ vim lib/simplegit.rb
$ git commit -am 'Make ls-tree function recursive'
[featureB e5b0fdc] Make ls-tree function recursive
 1 files changed, 1 insertions(+), 1 deletions(-)
$ vim lib/simplegit.rb
$ git commit -am 'Add ls-files'
[featureB 8512791] Add ls-files
 1 files changed, 5 insertions(+), 0 deletions(-)

Jessicas Repository sieht nun folgendermaßen aus:

Jessicas initialer Commit Verlauf
Abbildung 65. Jessicas initialer Commit Verlauf

Sie ist bereit, ihre Arbeit hochzuladen, erhält jedoch eine E-Mail von Josie, dass ein Branch mit einigen anfänglichen featureB Aufgaben bereits als featureBee Branch auf den Server übertragen wurde. Jessica muss diese Änderungen mit ihren eigenen zusammenführen, bevor sie ihre Arbeit auf den Server übertragen kann. Jessica holt sich zuerst Josies Änderungen mit git fetch:

$ git fetch origin
...
From jessica@githost:simplegit
 * [new branch]      featureBee -> origin/featureBee

Angenommen Jessica befindet sich noch in ihrem ausgecheckten featureB Branch. Dann kann sie nun Josies Arbeit mit git merge in diesen Branch zusammenführen:

$ git merge origin/featureBee
Auto-merging lib/simplegit.rb
Merge made by the 'recursive' strategy.
 lib/simplegit.rb |    4 ++++
 1 files changed, 4 insertions(+), 0 deletions(-)

Nun möchte Jessica die gesamte zusammengeführte Arbeit an featureB zurück auf den Server übertragen. Jedoch möchte sie nicht einfach ihren eigenen Branch featureB übertragen. Da Josie bereits einen Upstream Branch featureBee gestartet hat, möchte Jessica auf diesen Branch hochladen, was sie auch folgendermaßen tut:

$ git push -u origin featureB:featureBee
...
To jessica@githost:simplegit.git
   fba9af8..cd685d1  featureB -> featureBee

Dies wird als refspec bezeichnet. Unter Die Referenzspezifikation (engl. Refspec) findest du eine detailliertere Beschreibung der Git-Refspecs und der verschiedenen Möglichkeiten, die du damit hast. Beachte auch die -u Option. Dies ist die Abkürzung für --set-upstream, mit der die Branches so konfiguriert werden, dass sie später leichter gepusht und gepullt werden können.

Als nächstes erhält Jessica eine E-Mail von John, der ihr mitteilt, dass er einige Änderungen am Branch featureA vorgenommen hat, an dem sie zusammenarbeiten. Er bittet Jessica, sie sich anzusehen. Wieder führt Jessica ein einfaches git fetch durch, um alle neue Inhalte vom Server abzurufen, einschließlich (natürlich) Johns neuester Arbeit:

$ git fetch origin
...
From jessica@githost:simplegit
   3300904..aad881d  featureA   -> origin/featureA

Jessica kann sich Johns neue Arbeit ansehen, indem sie den Inhalt des neu abgerufenen Branches featureA mit ihrer lokalen Kopie desselben Branches vergleicht:

$ git log featureA..origin/featureA
commit aad881d154acdaeb2b6b18ea0e827ed8a6d671e6
Author: John Smith <jsmith@example.com>
Date:   Fri May 29 19:57:33 2009 -0700

    Increase log output to 30 from 25

Wenn ihr Johns neue Arbeit gefällt, kann sie sie mit ihrem lokalen Branch featureA zusammenführen:

$ git checkout featureA
Switched to branch 'featureA'
$ git merge origin/featureA
Updating 3300904..aad881d
Fast forward
 lib/simplegit.rb |   10 +++++++++-
1 files changed, 9 insertions(+), 1 deletions(-)

Schließlich möchte Jessica noch ein paar geringfügige Änderungen an dem gesamten, zusammengeführten Inhalt vornehmen. Sie kann diese Änderungen vornehmen, indem sie in ihren lokalen Branch featureA comittet und das Endergebnis zurück auf den Server überträgt (pusht):

$ git commit -am 'Add small tweak to merged content'
[featureA 774b3ed] Add small tweak to merged content
 1 files changed, 1 insertions(+), 1 deletions(-)
$ git push
...
To jessica@githost:simplegit.git
   3300904..774b3ed  featureA -> featureA

Jessicas commit Verlauf sieht nun in etwa so aus:

Jessicas Verlauf nach committen auf einem Feature Branch
Abbildung 66. Jessicas Verlauf nach committen auf einem Feature Branch

Irgendwann informieren Jessica, Josie und John die Integratoren, dass die Branches featureA und featureBee auf dem Server für die Integration in die Hauptlinie bereit sind. Nachdem die Integratoren diese Branches in der Hauptlinie zusammengeführt haben, holt ein Abruf den neuen Zusammenführungs-Commit ab, sodass der Verlauf wie folgt aussieht:

Jessicas Verlauf nach Zusammenführung ihrer beiden Featurebranches
Abbildung 67. Jessicas Verlauf nach Zusammenführung ihrer beiden Featurebranches

Viele Teams wechseln zu Git, da sie parallel arbeiten können und die verschiedenen Entwicklungslinien zu einem späteren Zeitpunkt zusammengeführt werden können. Ein großer Vorteil von Git besteht darin, dass man in kleinen Untergruppen eines Teams über entfernte Branches zusammenarbeiten kann, ohne notwendigerweise das gesamte Team zu involvieren oder zu behindern. Die Vorgehensweise dieses Arbeitsablaufes, sieht in etwa so aus:

Grundlegende Vorgehensweise bei einem geführten Team
Abbildung 68. Grundlegende Vorgehensweise bei einem geführten Team

Verteiltes, öffentliches Projekt

An öffentlichen Projekten mitzuwirken ist ein wenig anders. Da du nicht die Berechtigung hast, Branches im Projekt direkt zu aktualisieren, musst du deine Arbeit auf andere Weise an die Projektbetreuer weiterleiten. In diesem ersten Beispiel wird beschrieben, wie du auf Git Hosts, die einfaches „Forking“ unterstützen, via „Forking“ mitwirken kannst. Viele Hosting-Sites unterstützen dies (einschließlich GitHub, BitBucket, repo.or.cz und andere) und viele Projektbetreuer erwarten diese Art der Mitarbeit. Der nächste Abschnitt befasst sich mit Projekten, die bereitgestellte Patches bevorzugt per E-Mail akzeptieren.

Zunächst möchtest du wahrscheinlich das Hauptrepository klonen, einen Branch für den Patch oder die Patch Serien erstellen, die du beisteuern möchtest, und dort deine Arbeit erledigen. Der Prozess sieht im Grunde so aus:

$ git clone <url>
$ cd project
$ git checkout -b featureA
  ... work ...
$ git commit
  ... work ...
$ git commit
Anmerkung

Du kannst rebase -i verwenden, um deine Arbeit auf ein einzelnen Commit zu reduzieren. Du kannst auch die Änderungen in den Commits neu anordnen, damit der Betreuer den Patch einfacher überprüfen kann – siehe Den Verlauf umschreiben für weitere Informationen zum interaktiven Rebasing.

Wenn deine Arbeit am Branch abgeschlossen ist und du bereit bist, sie an die Betreuer weiterzuleiten, wechsel zur ursprünglichen Projektseite. Dort klickst du auf die Schaltfläche Fork, um deinen eigenen schreibbaren Fork des Projekts zu erstellen. Anschließend musst du diese Repository-URL als neue Remote-Adresse deines lokalen Repositorys hinzufügen. Nennen wir es in diesem Beispiel myfork:

$ git remote add myfork <url>

Anschließend musst du deine neue Arbeit in dieses Repository hochladen. Es ist am einfachsten, den Branch, an dem du arbeitest, in dein geforktes Repository hochzuladen, anstatt diese Arbeit in deinem ´master´-Branch zusammenzuführen und diesen hochzuladen. Der Grund dafür ist, dass du deinen master Branch nicht zurücksetzen musst, wenn deine Arbeit nicht akzeptiert bzw. nur teilweise übernommen (cherry-pick) wurde (die Git-Operation zum cherry-pick wird ausführlicher in Rebasing und Cherry-Picking Workflows behandelt). Wenn die Betreuer deine Arbeit per merge, rebase oder cherry-pick übernehmen, erhältst du deine Arbeit sowieso zurück, wenn du aus dem Repository der Betreuer pullst.

Auf jedem Fall kannst du deine Arbeit hochladen mit:

$ git push -u myfork featureA

Sobald deine Arbeit an deinem Fork des Repositorys hochgeladen wurde, musst du den Betreuern des ursprünglichen Projekts mitteilen, dass es Änderungen gibt, die sie zusammenführen möchten. Dies wird oft als Pull Request bezeichnet. Du generierst eine solche Anfrage entweder über die Website – GitHub hat einen eigenen „Pull-Request-Mechanismus“, den wir in GitHub behandeln werden, oder du kannst den Befehl git request-pull ausführen und die nachfolgende Ausgabe manuell per E-Mail an den Projektbetreuer senden.

Der Befehl git request-pull verwendet den Basis Branch, in den dein Feature Branch abgelegt werden soll. Außerdem wird die Git-Repository-URL angegeben aus dem er gezogen werden soll. Er erstellt damit eine Zusammenfassung aller Änderungen, um deren Übernahme du bittest. Wenn bspw. Jessica an John eine Pull Request senden möchte und sie zwei Commits für den gerade hochgeladenen Featurebranch ausgeführt hat, kann sie folgendes ausführen:

$ git request-pull origin/master myfork
The following changes since commit 1edee6b1d61823a2de3b09c160d7080b8d1b3a40:
Jessica Smith (1):
        Create new function

are available in the git repository at:

  https://githost/simplegit.git featureA

Jessica Smith (2):
      Add limit to log function
      Increase log output to 30 from 25

 lib/simplegit.rb |   10 +++++++++-
 1 files changed, 9 insertions(+), 1 deletions(-)

Diese Ausgabe kann an den Betreuer gesendet werden. Sie teilt ihm mit, von wo die Arbeit gebranched wurde, fasst die Commits zusammen und gibt an, von wo die neue Arbeit abgerufen werden soll.

Bei einem Projekt, für das du nicht der Betreuer bist, ist es im Allgemeinen einfacher, einen Branch wie master zu haben, der immer origin/master folgt. Deine Arbeit kannst du dann in Feature Branches erledigen, die du einfach verwerfen kannst, wenn deine Änderungen abgelehnt werden. Durch das Isolieren von Änderungen in Feature Branches wird es für dich auch einfacher, deine Arbeit neu zu strukturieren. Falls sich das Haupt-Repositorys in der Zwischenzeit weiter entwickelt hat und deine Commits nicht mehr sauber angewendet werden können. Wenn du beispielsweise ein zweites Feature an das Projekt senden möchtest, arbeitest du nicht weiter an dem Branch, den du gerade hochgeladen hast. Beginne erneut im master Branch des Haupt-Repositorys:

$ git checkout -b featureB origin/master
  ... work ...
$ git commit
$ git push myfork featureB
$ git request-pull origin/master myfork
  ... email generated request pull to maintainer ...
$ git fetch origin

Jetzt ist jedes deiner Features in einer Art Silo enthalten, ähnlich wie bei einer Patch-Warteschlange. Dieses kannst du umarbeiten, zurücksetzen oder ändern, ohne dass die Features sich gegenseitig stören oder voneinander abhängig sind.

Initialer Commit Verlauf mit `featureB` Änderungen
Abbildung 69. Initialer Commit Verlauf mit featureB Änderungen

Nehmen wir an, der Projektbetreuer hat eine Reihe weiterer Patches übernommen und deinen ersten Branch einfließen lassen, der jedoch nicht mehr ordnungsgemäß zusammengeführt werden kann. In diesem Fall kannst du versuchen, diesen Branch auf `origin/master' zu reorganisieren, die Konflikte für den Betreuer zu lösen und deine Änderungen erneut zu übermitteln:

$ git checkout featureA
$ git rebase origin/master
$ git push -f myfork featureA

Dadurch wird dein Verlauf so umgeschrieben, sodass er jetzt folgendermaßen aussieht Commit Verlauf nach featureA Änderungen.

Commit Verlauf nach `featureA` Änderungen
Abbildung 70. Commit Verlauf nach featureA Änderungen

Da du den Branch reorganisiert hast, musst du das -f für deinen Push-Befehl angeben, um den featureA Branch auf dem Server mit einen Commit ersetzen zu können, der nicht vom gegenwärtig letzten Commit des entfernten Branches abstammt. Eine Alternative wäre, diese neue Arbeit in einen anderen Branch auf dem Server hochzuladen (beispielsweise als featureAv2).

Schauen wir uns ein weiteres mögliches Szenario an: Der Betreuer hat sich die Arbeit in deinem zweiten Branch angesehen und mag dein Konzept, möchte aber, dass du ein Implementierungsdetail änderst. Du nutzt diese Gelegenheit, um deine Änderungen zu verschieben, damit diese auf dem aktuellen master Branch des Projektes basieren. Du startest einen neuen Branch, der auf den aktuellen Branch origin/master basiert und fasst die Änderungen an featureB dort zusammen. Dabei löst du etwaige Konflikte, machst die Implementierungsänderungen und lädst diese Arbeiten als neuen Branch hoch:

$ git checkout -b featureBv2 origin/master
$ git merge --squash featureB
  ... change implementation ...
$ git commit
$ git push myfork featureBv2

Mit der Option --squash wird die gesamte Arbeit an dem zusammengeführten Branch in einen Änderungssatz komprimiert. Dadurch wird ein Repository-Status erzeugt, als ob eine echter Commit stattgefunden hätte, ohne dass tatsächlich ein Merge-Commit durchgeführt wurde. Dies bedeutet, dass dein zukünftiger Commit nur einen übergeordneten Vorgänger hat. Das erlaubt dir alle Änderungen aus einem anderen Branch einzuführen und weitere Änderungen vorzunehmen, bevor du den neuen Commit aufnimmst. Auch die Option --no-commit kann nützlich sein, um den Merge-Commit im Falle des Standard-Merge-Prozesses zu verzögern.

Nun kannst du den Betreuer darüber informieren, dass du die angeforderten Änderungen vorgenommen hast und dass du diese Änderungen in deinem Branch featureBv2 findest.

Commit Verlauf nach getaner `featureBv2` Arbeit
Abbildung 71. Commit Verlauf nach getaner featureBv2 Arbeit

Öffentliche Projekte via Email

Viele Projekte haben fest definierte Prozesse, um Änderungen entgegenzunehmen. Du musst die spezifischen Regeln dieser Projekte kennen, da sie sich oft unterscheiden. Da es viele alte und große Projekte gibt, die Änderungen über eine Entwickler-Mailingliste akzeptieren, werden wir jetzt solch ein Beispiel durchgehen.

Der Workflow ähnelt dem vorherigen Anwendungsfall: Du erstellst Feature Branches für jede Patch Serie, an der du arbeitest. Der Unterschied besteht darin, wie du diese Änderungen an das Projekt sendest. Anstatt das Projekt zu forken und auf dein eigenes geforktes Repository hochzuladen, generierst du E-Mail-Versionen jeder Commit-Serie und sendest diese per E-Mail an die Entwickler-Mailingliste:

$ git checkout -b topicA
  ... work ...
$ git commit
  ... work ...
$ git commit

Jetzt hast du zwei Commits, die du an die Mailingliste senden kannst. Du verwendest git format-patch, um die mbox-formatierten Dateien zu generieren, die du anschließend per E-Mail an die Mailingliste sendest. Dabei wird jeder Commit in eine E-Mail-Nachricht umgewandelt. Die erste Zeile der Commit-Nachricht wird als Betreff verwendet. Der Rest der Commit-Nachricht plus den Patch, den der Commit einführt wird als Mail-Körper verwendet. Der Vorteil daran ist, dass durch das Anwenden eines Patches aus einer mit format-patch erstellten E-Mail alle Commit-Informationen ordnungsgemäß erhalten bleiben.

$ git format-patch -M origin/master
0001-add-limit-to-log-function.patch
0002-increase-log-output-to-30-from-25.patch

Der Befehl format-patch gibt die Namen der von ihm erstellten Patch-Dateien aus. Die -M Option weist Git an, nach Umbenennungen zu suchen. Die Dateien sehen am Ende folgendermaßen aus:

$ cat 0001-add-limit-to-log-function.patch
From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001
From: Jessica Smith <jessica@example.com>
Date: Sun, 6 Apr 2008 10:17:23 -0700
Subject: [PATCH 1/2] Add limit to log function

Limit log functionality to the first 20

---
 lib/simplegit.rb |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index 76f47bc..f9815f1 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -14,7 +14,7 @@ class SimpleGit
   end

   def log(treeish = 'master')
-    command("git log #{treeish}")
+    command("git log -n 20 #{treeish}")
   end

   def ls_tree(treeish = 'master')
--
2.1.0

Du kannst diese Patch-Dateien auch bearbeiten, um weitere Informationen für die E-Mail-Liste hinzuzufügen, die nicht in der Commit-Nachricht angezeigt werden sollen. Wenn du Text zwischen der Zeile --- und dem Beginn des Patches (der Zeile diff --git) einfügst, können die Entwickler diesen Text lesen. Der Inhalt wird jedoch vom Patch-Vorgang ignoriert.

Um dies nun per E-Mail an eine Mailingliste zu senden, kannst du die Datei entweder an eine Mail anhängen oder über ein Befehlszeilenprogramm direkt versenden. Das Einfügen von Text führt häufig zu Formatierungsproblemen, insbesondere bei „intelligenten“ Clients, bei denen Zeilenumbrüche und andere Leerzeichen nicht ordnungsgemäß beibehalten werden. Glücklicherweise bietet Git ein Tool, mit dem du ordnungsgemäß formatierte Patches über IMAP senden kannst, was einfacher für dich sein könnte. Wir zeigen dir, wie du einen Patch über Google Mail sendest. Dies ist der E-Mail-Agent, mit dem wir uns am besten auskennen. Detaillierte Anweisungen für eine Reihe von anderen Mail-Programmen findest du am Ende der oben genannten Datei Documentation/SubmittingPatches im Git-Quellcode.

Zuerst musst du den Abschnitt imap in deiner ~/.gitconfig Datei einrichten. Du kannst jeden Wert separat mit einer Reihe von git config Befehlen festlegen oder manuell hinzufügen. Am Ende sollte deine Konfigurationsdatei ungefähr so aussehen:

[imap]
  folder = "[Gmail]/Entwürfe"
  host = imaps://imap.gmail.com
  user = user@gmail.com
  pass = YX]8g76G_2^sFbd
  port = 993
  sslverify = false

Wenn dein IMAP-Server kein SSL verwendet, sind die letzten beiden Zeilen wahrscheinlich nicht erforderlich. Der Hostwert lautet dann imap:// anstelle von imaps://. Wenn dies eingerichtet ist, kannst du git imap-send verwenden, um die Patch-Reihe im Ordner Entwürfe des angegebenen IMAP-Servers abzulegen:

$ cat *.patch |git imap-send
Resolving imap.gmail.com... ok
Connecting to [74.125.142.109]:993... ok
Logging in...
sending 2 messages
100% (2/2) done

Zu diesem Zeitpunkt solltest du in der Lage sein, in deinem Entwurfsordner zu wechseln und dort das Feld An der generierten Email in die Mailinglist-Adresse zu ändern, an die du den Patch senden willst. Möglicherweise willst du auch den Betreuer oder die Person in Kopie nehmen, die für diesen Abschnitt verantwortlich ist. Anschließend kannst du die Mail versenden.

Du kannst die Patches auch über einen SMTP-Server senden. Wie zuvor kannst du jeden Wert separat mit einer Reihe von git config Befehlen festlegen oder manuell im Abschnitt sendemail in deiner ~/.gitconfig Datei hinzufügen:

[sendemail]
  smtpencryption = tls
  smtpserver = smtp.gmail.com
  smtpuser = user@gmail.com
  smtpserverport = 587

Danach kannst du deine Patches mit git send-email versenden:

$ git send-email *.patch
0001-add-limit-to-log-function.patch
0002-increase-log-output-to-30-from-25.patch
Who should the emails appear to be from? [Jessica Smith <jessica@example.com>]
Emails will be sent from: Jessica Smith <jessica@example.com>
Who should the emails be sent to? jessica@example.com
Message-ID to be used as In-Reply-To for the first email? y

Git gibt anschließend für jeden Patch, den du versendest, eine Reihe von Protokollinformationen aus, die in etwa so aussehen:

(mbox) Adding cc: Jessica Smith <jessica@example.com> from
  \line 'From: Jessica Smith <jessica@example.com>'
OK. Log says:
Sendmail: /usr/sbin/sendmail -i jessica@example.com
From: Jessica Smith <jessica@example.com>
To: jessica@example.com
Subject: [PATCH 1/2] Add limit to log function
Date: Sat, 30 May 2009 13:29:15 -0700
Message-Id: <1243715356-61726-1-git-send-email-jessica@example.com>
X-Mailer: git-send-email 1.6.2.rc1.20.g8c5b.dirty
In-Reply-To: <y>
References: <y>

Result: OK
Hinweis

Weitere Informationen zum Konfigurieren deines Systems und deiner E-Mail-Adresse, weitere Tipps und Tricks sowie eine Sandbox zum Senden eines Test-Patches per E-Mail findest du unter git-send-email.io.

Zusammenfassung

In diesem Abschnitt haben wir mehrere Workflows behandelt und über die Unterschiede zwischen der Arbeit an an Closed-Source-Projekten in einem kleinen Teams und der Mitarbeit an einem großen öffentlichen Projekt gesprochen. Du musst vor dem Committen nach White-Space-Fehlern suchen und kannst großartige Commit-Beschreibungen hinzufügen. Du hast gelernt, wie du Patches formatieren und per E-Mail an eine Entwickler-Mailingliste senden kannst. Der Umgang mit Merges wurde auch im Zusammenhang mit den verschiedenen Arbeitsabläufen behandelt. Du bist jetzt gut vorbereitet, an jedem Projekt mitzuarbeiten.

Als Nächstes erfährst du, wie du auf der anderen Seite arbeitest: als Verwalter (Maintainer) eines Git-Projektes. Du lernst, wie man als wohlwollender Diktator oder Integrationsmanager korrekt arbeitet.

scroll-to-top