Subsections of Versionierung mit Git

Intro: Versionskontrolle in der Softwareentwicklung

TL;DR

In der Softwareentwicklung wird häufig ein Versionsmanagementsystem (VCS) eingesetzt, welches die Verwaltung von Versionsständen und Änderungen ermöglicht. Ein Repository sammelt dabei die verschiedenen Änderungen (quasi wie eine Datenbank der Software-Versionsstände). Die Software Git ist verbreiteter Vertreter und arbeitet mit dezentralen Repositories.

Ein neues lokales Repository kann man mit git init anlegen. Der Befehl legt den Unterordner .git/ im aktuellen Ordner an, darin befindet sich das lokale Repository und weitere von Git benötigte Dateien (FINGER WEG!). Die Dateien und anderen Unterordner im aktuellen Ordner können nun der Versionskontrolle hinzugefügt werden.

Den lokal vorliegenden (Versions-) Stand der Dateien im aktuellen Ordner nennt man auch "Workingcopy".

Ein bereits existierendes Repo kann mit git clone <url> geklont werden.

GitHub ist nicht Git, sondern ein kommerzieller Anbieter, der das Hosten von Git-Repositories und weitere Features anbietet.

Videos (HSBI-Medienportal)
Lernziele
  • (K1) Varianten der Versionierung
  • (K1) Begriffe Workingcopy und Repository
  • (K2) Github ist nicht Git
  • (K2) Erstellung von lokalen Git-Repositories
  • (K3) Umgang mit entsprechenden Git-Befehlen auf der Konsole

Typische Probleme bei SW-Entwicklung

  • Was hat wer wann (und wo) geändert? Und warum?
  • Ich brauche den Stand von gestern/letzter Woche/...
  • Ich will schnell mal eine neue Idee ausprobieren ...
  • Ich arbeite an mehreren Rechnern (Synchronisation)
  • Wir müssen gemeinsam an der gleichen Codebasis arbeiten.
  • Wir arbeiten am Release v42, aber Kunde braucht schnell einen Fix für v40

Folgen SW-Entwicklung ohne Versionsverwaltung

  • Filesystem müllt voll mit manuell versionierten Dateien/Sicherungen ala file_20120507_version2_cagi.txt
  • Ordner/Projekte müssen dupliziert werden für neue Ideen
  • Code müllt voll mit auskommentierten Zeilen ("Könnte ja noch gebraucht werden")
  • Unklar, wann welche Änderung von wem warum eingeführt wurde
  • Unbeabsichtigtes Überschreiben mit älteren Versionen beim Upload in gemeinsamen Filesharing-Bereich

Prinzip Versionsverwaltung

  • Repository: Datenbank mit verschiedenen Versionsständen, Kommentaren, Tags etc.

  • Workingcopy: Arbeitskopie eines bestimmten Versionsstandes

Varianten: Zentrale Versionsverwaltung (Beispiel SVN)

Es gibt ein zentrales Repository (typischerweise auf einem Server), von dem die Developer einen bestimmten Versionsstand "auschecken" (sich lokal kopieren) und in welches sie Änderungen wieder zurück "pushen".

Zur Abfrage der Historie und zum Veröffentlichen von Änderungen benötigt man entsprechend immer eine Verbindung zum Server.

Varianten: Verteilte Versionsverwaltung (Beispiel Git)

In diesem Szenario hat jeder Developer nicht nur die Workingcopy, sondern auch noch eine Kopie des Repositories. Zusätzlich kann es einen oder mehrere Server geben, auf denen dann nur das Repository vorgehalten wird, d.h. dort gibt es normalerweise keine Workingcopy. Damit kann unabhängig voneinander gearbeitet werden.

Allerdings besteht nun die Herausforderung, die geänderten Repositories miteinander abzugleichen. Das kann zwischen dem lokalen Rechner und dem Server passieren, aber auch zwischen zwei "normalen" Rechnern (also zwischen den Developern).

Hinweis: GitHub ain't no Git! Git ist eine Technologie zur Versionsverwaltung. Es gibt verschiedene Implementierungen und Plugins für IDEs und Editoren. GitHub ist dagegen ein Dienstleister, wo man Git-Repositories ablegen kann und auf diese mit Git (von der Konsole oder aus der IDE) zugreifen kann. Darüber hinaus bietet der Service aber zusätzliche Features an, beispielsweise ein Issue-Management oder sogenannte Pull-Requests. Dies hat aber zunächst mit Git nichts zu tun. Weitere populäre Anbieter sind beispielsweise Bitbucket oder Gitlab oder Gitea, wobei einige auch selbst gehostet werden können.

Versionsverwaltung mit Git: Typische Arbeitsschritte

  1. Repository anlegen (oder clonen)

  2. Dateien neu erstellen (und löschen, umbenennen, verschieben)

  3. Änderungen einpflegen ("committen")

  4. Änderungen und Logs betrachten

  5. Änderungen rückgängig machen

  6. Projektstand markieren ("taggen")

  7. Entwicklungszweige anlegen ("branchen")

  8. Entwicklungszweige zusammenführen ("mergen")

  9. Änderungen verteilen (verteiltes Arbeiten, Workflows)

(Globale) Konfiguration

Minimum:

  • git config --global user.name <name>
  • git config --global user.email <email>

Diese Konfiguration muss man nur einmal machen.

Wenn man den Schalter --global weglässt, gelten die Einstellungen nur für das aktuelle Projekt/Repo.

Zumindest Namen und EMail-Adresse muss man setzen, da Git diese Information beim Anlegen der Commits speichert (== benötigt!).

Aliase:

  • git config --global alias.ci commit
  • git config --global alias.co checkout
  • git config --global alias.br branch
  • git config --global alias.st status
  • git config --global alias.ll 'log --all --graph --decorate --oneline'

Zusätzlich kann man weitere Einstellungen vornehmen, etwa auf bunte Ausgabe umschalten: git config --global color.ui auto oder Abkürzungen (Aliase) für Befehle definieren: git config --global alias.ll 'log --all --oneline --graph --decorate' ...

Git (und auch GitHub) hat kürzlich den Namen des Default-Branches von master auf main geändert. Dies kann man in Git ebenfalls selbst einstellen: git config --global init.defaultBranch <name>.

Anschauen kann man sich die Einstellungen in der Textdatei ~/.gitconfig oder per Befehl git config --global -l.

Neues Repo anlegen

  • git init

    => Erzeugt neues Repository im akt. Verzeichnis

  • git clone <url>

    => Erzeugt (verlinkte) Kopie des Repos unter <url>

Wrap-Up

  • Git: Versionsmanagement mit dezentralen Repositories
  • Anlegen eines lokalen Repos mit git init
  • Clonen eines existierenden Repos mit git clone <url>
Quellen

Basics der Versionsverwaltung mit Git (lokale Repos)

TL;DR

Änderungen an Dateien (in der Workingcopy) werden mit git add zum "Staging" (Index) hinzugefügt. Dies ist eine Art Sammelbereich für Änderungen, die mit dem nächsten Commit in das Repository überführt werden. Neue (bisher nicht versionierte Dateien) müssen ebenfalls zunächst mit git add zum Staging hinzugefügt werden.

Änderungen kann man mit git log betrachten, dabei erhält man u.a. eine Liste der Commits und der jeweiligen Commmit-Messages.

Mit git diff kann man gezielt Änderungen zwischen Commits oder Branches betrachten.

Mit git tag kann man bestimmte Commits mit einem "Stempel" (zusätzlicher Name) versehen, um diese leichter finden zu können.

Wichtig sind die Commit-Messages: Diese sollten eine kurze Zusammenfassung haben, die aktiv formuliert wird (was ändert dieser Commit: "Formatiere den Java-Code entsprechend Style"; nicht aber "Java-Code nach Style formatiert"). Falls der Kommentar länger sein soll, folgt eine Leerzeile auf die erste Zeile (Zusammenfassung) und danach ein Block mit der längeren Erklärung.

Videos (HSBI-Medienportal)
Lernziele
  • (K3) Umgang mit Dateien: Hinzufügen zum und Löschen aus Repo
  • (K3) Umgang mit Änderungen: Hinzufügen zum Staging und Commit
  • (K3) Herausfinden von Unterschieden, Ansehen der Historie
  • (K3) Ignorieren von Dateien und Ordnern

Versionsverwaltung mit Git: Typische Arbeitsschritte

  1. Repository anlegen (oder clonen)

  2. Dateien neu erstellen (und löschen, umbenennen, verschieben)

  3. Änderungen einpflegen ("committen")

  4. Änderungen und Logs betrachten

  5. Änderungen rückgängig machen

  6. Projektstand markieren ("taggen")

  7. Entwicklungszweige anlegen ("branchen")

  8. Entwicklungszweige zusammenführen ("mergen")

  9. Änderungen verteilen (verteiltes Arbeiten, Workflows)

Dateien unter Versionskontrolle stellen

  1. git add . (oder git add <file>)

    => Stellt alle Dateien (bzw. die Datei <file>) im aktuellen Verzeichnis unter Versionskontrolle

  2. git commit

    => Fügt die Dateien dem Repository hinzu

Abfrage mit git status

Änderungen einpflegen

  • Abfrage mit: git status
  • "Staging" von modifizierten Dateien: git add <file>
  • Committen der Änderungen im Stage: git commit

Anmerkung: Alternativ auch mit git commit -m "Kommentar", um das Öffnen des Editors zu vermeiden ... geht einfach schneller ;)

Das "staging area" stellt eine Art Zwischenebene zwischen Working Copy und Repository dar: Die Änderungen sind temporär "gesichert", aber noch nicht endgültig im Repository eingepflegt ("committed").

Man kann den Stage dazu nutzen, um Änderungen an einzelnen Dateien zu sammeln und diese dann (in einem Commit) gemeinsam einzuchecken.

Man kann den Stage in der Wirkung umgehen, indem man alle in der Working Copy vorliegenden Änderungen per git commit -a -m "Kommentar" eincheckt. Der Schalter "-a" nimmt alle vorliegenden Änderungen an bereits versionierten Dateien, fügt diese dem Stage hinzu und führt dann den Commit durch. Das ist das von SVN bekannte Verhalten. Achtung: Nicht versionierte Dateien bleiben dabei außen vor!

Letzten Commit ergänzen

  • git commit --amend -m "Eigentlich wollte ich das so sagen"

    Wenn keine Änderungen im Stage sind, wird so die letzte Commit-Message geändert.

  • git add <file>; git commit --amend

    Damit können vergessene Änderungen an der Datei <file> zusätzlich im letzten Commit aufgezeichnet werden.

    In beiden Fällen ändert sich die Commit-ID!

Weitere Datei-Operationen: hinzufügen, umbenennen, löschen

  • Neue (unversionierte) Dateien und Änderungen an versionierten Dateien zum Staging hinzufügen: git add <file>
  • Löschen von Dateien (Repo+Workingcopy): git rm <file>
  • Löschen von Dateien (nur Repo): git rm --cached <file>
  • Verschieben/Umbenennen: git mv <fileAlt> <fileNeu>

Aus Sicht von Git sind zunächst alle Dateien "untracked", d.h. stehen nicht unter Versionskontrolle.

Mit git add <file> (und git commit) werden Dateien in den Index (den Staging-Bereich, d.h. nach dem Commit letztlich in das Repository) aufgenommen. Danach stehen sie unter "Beobachtung" (Versionskontrolle). So lange, wie eine Datei identisch zur Version im Repository ist, gilt sie als unverändert ("unmodified"). Eine Änderung führt entsprechend zum Zustand "modified", und ein git add <file> speichert die Änderungen im Stage. Ein Commit überführt die im Stage vorgemerkte Änderung in das Repo, d.h. die Datei gilt wieder als "unmodified".

Wenn eine Datei nicht weiter versioniert werden soll, kann sie aus dem Repo entfernt werden. Dies kann mit git rm <file> geschehen, wobei die Datei auch aus der Workingcopy gelöscht wird. Wenn die Datei erhalten bleiben soll, aber nicht versioniert werden soll (also als "untracked" markiert werden soll), dann muss sie mit git rm --cached <file> aus der Versionskontrolle gelöscht werden. Achtung: Die Datei ist dann nur ab dem aktuellen Commit gelöscht, d.h. frühere Revisionen enthalten die Datei noch!

Wenn eine Datei umbenannt werden soll, geht das mit git mv <fileAlt> <fileNeu>. Letztlich ist dies nur eine Abkürzung für die Folge git rm --cached <fileAlt>, manuelles Umbenennen der Datei in der Workingcopy und git add <fileNeu>.

Commits betrachten

  • Liste aller Commits: git log

    • git log -<n> oder git log --since="3 days ago" Meldungen eingrenzen ...
    • git log --stat Statistik ...
    • git log --author="pattern" Commits eines Autors
    • git log <file> Änderungen einer Datei
  • Inhalt eines Commits: git show

Änderungen und Logs betrachten

  • git diff [<file>]

    Änderungen zwischen Workingcopy und letztem Commit (ohne Stage)

    Das "staging area" wird beim Diff von Git behandelt, als wären die dort hinzugefügten Änderungen bereits eingecheckt (genauer: als letzter Commit im aktuellen Branch im Repo vorhanden). D.h. wenn Änderungen in einer Datei mittels git add <datei> dem Stage hinzugefügt wurden, zeigt git diff <datei> keine Änderungen an!

  • git diff commitA commitB

    Änderungen zwischen Commits

  • Blame: git blame <file>

    Wer hat was wann gemacht?

Dateien ignorieren: .gitignore

  • Nicht alle Dateien gehören ins Repo:
    • generierte Dateien: .class
    • temporäre Dateien
  • Datei .gitignore anlegen und committen
    • Wirkt auch für Unterordner
    • Inhalt: Reguläre Ausdrücke für zu ignorierende Dateien und Ordner
    # Compiled source #
    *.class
    *.o
    *.so

    # Packages #
    *.zip

    # All directories and files in a directory #
    bin/**/*

Zeitmaschine

  • Änderungen in Workingcopy rückgängig machen

    • Änderungen nicht in Stage: git checkout <file> oder git restore <file>
    • Änderungen in Stage: git reset HEAD <file> oder git restore --staged <file>

    => Hinweise von git status beachten!

  • Datei aus altem Stand holen:

    • git checkout <commit> <file>, oder
    • git restore --source <commit> <file>
  • Commit verwerfen, Geschichte neu: git revert <commit>

Hinweis: In den neueren Versionen von Git ist der Befehl git restore hinzugekommen, mit dem Änderungen rückgängig gemacht werden können. Der bisherige Befehl git checkout steht immer noch zur Verfügung und bietet über git restore hinaus weitere Anwendungsmöglichkeiten.

  • Stempel (Tag) vergeben: git tag <tagname> <commit>
  • Tags anzeigen: git tag und git show <tagname>

Wann und wie committen?

Jeder Commit stellt einen Rücksetzpunkt dar!

Typische Regeln:

  • Kleinere "Häppchen" einchecken: ein Feature oder Task (das nennt man auch atomic commit: das kleinste Set an Änderungen, die gemeinsam Sinn machen und die ggf. gemeinsam zurückgesetzt werden können)
  • Logisch zusammenhängende Änderungen gemeinsam einchecken
  • Projekt muss nach Commit compilierbar sein
  • Projekt sollte nach Commit lauffähig sein

Ein Commit sollte in sich geschlossen sein, d.h. die kleinste Menge an Änderungen enthalten, die gemeinsam einen Sinn ergeben und die (bei Bedarf) gemeinsam zurückgesetzt oder verschoben werden können. Das nennt man auch atomic commit.

Wenn Sie versuchen, die Änderungen in Ihrem Commit zu beschreiben (siehe nächste Folie "Commit-Messages"), dann werden Sie einen atomic commit mit einem kurzen Satz (natürlich im Imperativ!) beschreiben können. Wenn Sie mehr Text brauchen, haben Sie wahrscheinlich keinen atomic commit mehr vor sich.

Lesen Sie dazu auch How atomic Git commits dramatically increased my productivity - and will increase yours too.

Schreiben von Commit-Messages: WARUM?!

Schauen Sie sich einmal einen Screenshot eines git log --oneline 61e48f0..e2c8076 im Dungeon-CampusMinden/Dungeon an:

Nun stellen Sie sich vor, Sie sind auf der Suche nach Informationen, suchen einen bestimmten Commit oder wollen eine bestimmte Änderung finden ...

Wenn man das genauer analysiert, dann stören bestimmte Dinge:

  • Mischung aus Deutsch und Englisch
  • "Vor-sich-hin-Murmeln": "Layer system 5"
  • Teileweise werden Tags genutzt wie [BUG], aber nicht durchgängig
  • Mischung zwischen verschiedenen Formen: "Repo umbenennen", "Benenne Repo um", "Repo umbenannt"
  • Unterschiedliche Groß- und Kleinschreibung
  • Sehr unterschiedlich lange Zeilen/Kommentare

Das Beachten einheitlicher Regeln ist enorm wichtig!

Leider sagt sich das so leicht - in der Praxis macht man es dann doch schnell wieder unsauber. Dennoch, auch im Dungeon-Repo gibt es einen positiven Trend (git log --oneline 8039d6c..7f49e89):

Typische Regeln und Konventionen tauchen überall auf, beispielsweise in [Chacon2014] oder bei Tim Pope (siehe nächstes Beispiel) oder bei "How to Write a Git Commit Message".

Short (50 chars or less) summary of changes

More detailed explanatory text, if necessary.  Wrap it to about
72 characters or so.  In some contexts, the first line is treated
as the subject of an email and the rest of the text as the body.
The blank line separating the summary from the body is critical
(unless you omit the body entirely); tools like rebase can get
confused if you run the two together.

Further paragraphs come after blank lines.

 - Bullet points are okay, too
 - Typically a hyphen or asterisk is used for the bullet, preceded
   by a single space, with blank lines in between, but conventions
   vary here

Quelle: "A Note About Git Commit Messages" by Tim Pope on tbaggery.com

Denken Sie sich die Commit-Message als E-Mail an einen zukünftigen Entwickler, der das in fünf Jahren liest!

Vom Aufbau her hat eine E-Mail auch eine Summary und dann den eigentlichen Inhalt ... Erklären Sie das "WARUM" der Änderung! (Das "WER", "WAS", "WANN" wird bereits automatisch von Git aufgezeichnet ...)

Lesen (und beachten) Sie unbedingt auch "How to Write a Git Commit Message"!

Ausflug "Conventional Commits"

Die Commit-Messages dienen vor allem der Dokumentation und werden von Entwicklern gelesen.

Wenn man die Messages ein wenig stärker formalisieren würde, dann könnte man diese aber auch mit Tools verarbeiten und beispielsweise automatisiert Changelogs oder Release-Texte verfassen!

Betrachten Sie einmal das Projekt ConventionalCommits.org. Dies ist ein solcher Versuch, die Commit-Messages (a) einheitlicher und lesbarer zu gestalten und (b) auch eine Tool-gestützte Auswertung zu erlauben.

Das Projekt schlägt als Erweitung der üblichen Regeln zum Formatieren von Commit-Messages vor, dass in der ersten Zeile der Summary noch eine Abkürzung für die in diesem Commit erfolgte Änderung (Bug-Fix, neues Feature, ...) vorangestellt wird. Dieser Abkürzung kann in Klammern noch der Scope der Änderung hinzugefügt werden, beispielsweise den Bereich im Projekt, der von diesem Commit berührt wird. Wenn es eine breaking change ist, also alter Code nach dieser Änderung sich anders verhält oder vielleicht sogar nicht mehr kompiliert, wird noch ein "!" hinter dem Typ der Änderung ergänzt.

Beispiel: Stellen Sie sich vor, im Dungeon-Projekt wurde ein neues Verhalten hinzugefügt.

  1. Normalerweise hätten Sie vielleicht diese Message geschrieben (angepasste Version aus Dungeon-CampusMinden/Dungeon/pull/469):

    add fight skill
    
    -   `DamageProjectileSkill` creates a new entity which causes `HealthDamage` when hitting another entity
    -   `FireballSkill` is a more concrete implementation of this
    -   Melee skills can be created with `DamageProjectileSkill` using a customised range
        -   Example: the `FireballSkill` has a range of 10, a melee would have a considerably smaller range
    
    fixes #24
    fixes #126
    fixes #224
    
  2. Mit ConventionalCommits.org könnte das dann so aussehen:

    feat: add fight skill
    
    -   `DamageProjectileSkill` creates a new entity which causes `HealthDamage` when hitting another entity
    -   `FireballSkill` is a more concrete implementation of this
    -   Melee skills can be created with `DamageProjectileSkill` using a customised range
        -   Example: the `FireballSkill` has a range of 10, a melee would have a considerably smaller range
    
    fixes #24
    fixes #126
    fixes #224
    

    Da es sich um ein neues Feature handelt, wurde der Summary in der ersten Zeile ein feat: vorangestellt.

    Die zu verwendenden Typen/Abkürzungen sind im Prinzip frei definierbar. Das Projekt ConventionalCommits.org schlägt eine Reihe von Abkürzungen vor. Auf diese Weise sollen in möglichst allen Projekten, die Conventional Commits nutzen, die selben Abkürzungen/Typen eingesetzt werden und so eine Tool-gestützte Auswertung möglich werden.

  3. Oder zusätzlich mit dem Scope der Änderung:

    feat(game): add fight skill
    
    -   `DamageProjectileSkill` creates a new entity which causes `HealthDamage` when hitting another entity
    -   `FireballSkill` is a more concrete implementation of this
    -   Melee skills can be created with `DamageProjectileSkill` using a customised range
        -   Example: the `FireballSkill` has a range of 10, a melee would have a considerably smaller range
    
    fixes #24
    fixes #126
    fixes #224
    

    Der Typ feat wurde hier noch ergänzt um einen frei definierbaren Identifier für den Projektbereich. Dieser wird in Klammern direkt hinter den Typ notiert (hier feat(game):).

    Im Beispiel habe ich als Bereich "game" genommen, weil die Änderung sich auf den Game-Aspekt des Projekts bezieht. Im konkreten Projekt wären andere Bereiche eventuell "dsl" (für die im Projekt entwickelte Programmiersprache plus Interpreter) und "blockly" (für die Integration von Google Blockly zur Programmierung des Dungeons mit LowCode-Ansätzen). Das ist aber letztlich vom Projekt abhängig und weitestgehend Geschmackssache.

  4. Oder zusätzlich noch als Auszeichnung "breaking change" (hier mit scope, geht aber auch ohne scope):

    feat(game)!: add fight skill
    
    -   `DamageProjectileSkill` creates a new entity which causes `HealthDamage` when hitting another entity
    -   `FireballSkill` is a more concrete implementation of this
    -   Melee skills can be created with `DamageProjectileSkill` using a customised range
        -   Example: the `FireballSkill` has a range of 10, a melee would have a considerably smaller range
    
    fixes #24
    fixes #126
    fixes #224
    

    Angenommen, das neue Feature muss in der API etwas ändern, so dass existierender Code nun nicht mehr funktionieren würde. Dies wird mit dem extra Ausrufezeichen hinter dem Typ/Scope kenntlich gemacht (hier feat(game)!:).

    Zusätzlich kann man einen "Footer" in die Message einbauen, also eine extra Zeile am Ende, die mit dem String "BREAKING CHANGE:" eingeleitet wird. (vgl. Conventional Commits > Examples)

Es gibt noch viele weitere Initiativen, Commit-Messages lesbarer zu gestalten und zu vereinheitlichen. Schauen Sie sich beispielsweise einmal gitmoji.dev an. (Mit einem Einsatz in einem professionellen Umfeld wäre ich hier aber sehr ... vorsichtig.)

Wrap-Up

  • Änderungen einpflegen zweistufig (add, commit)
  • Status der Workingcopy mit status ansehen
  • Logmeldungen mit log ansehen
  • Änderungen auf einem File mit diff bzw. blame ansehen
  • Projektstand markieren mit tag
  • Ignorieren von Dateien/Ordnern: Datei .gitignore
Challenges

Versionierung 101

  1. Legen Sie ein Repository an.
  2. Fügen Sie Dateien dem Verzeichnis hinzu und stellen Sie einige davon unter Versionskontrolle.
  3. Ändern Sie eine Datei und versionieren Sie die Änderung.
  4. Was ist der Unterschied zwischen "git add .; git commit" und "git commit -a"?
  5. Wie finden Sie heraus, welche Dateien geändert wurden?
  6. Entfernen Sie eine Datei aus der Versionskontrolle, aber nicht aus dem Verzeichnis!
  7. Entfernen Sie eine Datei komplett (Versionskontrolle und Verzeichnis).
  8. Ändern Sie eine Datei und betrachten die Unterschiede zum letzten Commit.
  9. Fügen Sie eine geänderte Datei zum Index hinzu. Was erhalten Sie bei git diff <datei>?
  10. Wie können Sie einen früheren Stand einer Datei wiederherstellen? Wie finden Sie überhaupt den Stand?
  11. Legen Sie sich ein Java-Projekt in Ihrer IDE an an. Stellen Sie dieses Projekt unter Git-Versionskontrolle. Führen Sie die vorigen Schritte mit Ihrer IDE durch.
Quellen

Git Branches: Features unabhängig entwickeln und mit Git verwalten

TL;DR

Die Commits in Git bauen aufeinander auf und bilden dabei eine verkettete "Liste". Diese "Liste" nennt man auch Branch (Entwicklungszweig). Beim Initialisieren eines Repositories wird automatisch ein Default-Branch angelegt, auf dem die Commits dann eingefügt werden.

Weitere Branches kann man mit git branch anlegen, und die Workingcopy kann mit git switch oder git checkout auf einen anderen Branch umgeschaltet werden. Auf diese Weise kann man an mehreren Features parallel arbeiten, ohne dass die Arbeiten sich gegenseitig stören.

Zum Mergen (Vereinigen) von Branches gibt es git merge. Dabei werden die Änderungen im angegebenen Branch in den aktuell in der Workingcopy ausgecheckten Branch integriert und hier ggf. ein neuer Merge-Commit erzeugt. Falls es in beiden Branches inkompatible Änderungen an der selben Stelle gab, entsteht beim Mergen ein Merge-Konflikt. Dabei zeigt Git in den betroffenen Dateien jeweils an, welche Änderung aus welchem Branch stammt und man muss diesen Konflikt durch Editieren der Stellen manuell beheben.

Mit git rebase kann die Wurzel eines Branches an eine andere Stelle verschoben werden. Dies wird später bei Workflows eine Rolle spielen.

Videos (HSBI-Medienportal)
Lernziele
  • (K3) Erzeugen von Branches
  • (K3) Mergen von Branches, Auflösen möglicher Konflikte
  • (K3) Rebasen von Branches

Neues Feature entwickeln/ausprobieren

A---B---C  master
  • Bisher nur lineare Entwicklung: Commits bauen aufeinander auf (lineare Folge von Commits)
  • master ist der (Default-) Hauptentwicklungszweig
    • Pointer auf letzten Commit
    • Default-Name: "master" (muss aber nicht so sein bzw. kann geändert werden)

Anmerkung: Git und auch Github haben den Namen für den Default-Branch von master auf maingeändert. Der Name an sich ist aber für Git bedeutungslos und kann mittels git config --global init.defaultBranch <name> geändert werden. In Github hat der Default-Branch eine gewisse Bedeutung, beispielsweise ist der Default-Branch das automatische Ziel beim Anlegen von Pull-Requests. In Github kann man den Default-Namen global in den User-Einstellungen (Abschnitt "Repositories") und für jedes einzelne Repository in den Repo-Einstellungen (Abschnitt "Branches") ändern.

Entwicklung des neuen Features soll stabilen master-Branch nicht beeinflussen => Eigenen Entwicklungszweig für die Entwicklung des Features anlegen:

  1. Neuen Branch erstellen: git branch wuppie
  2. Neuen Branch auschecken: git checkout wuppie oder git switch wuppie

Alternativ: git checkout -b wuppie oder git switch -c wuppie (neuer Branch und auschecken in einem Schritt)

A---B---C  master, wuppie

Startpunkt: prinzipiell beliebig (jeder Commit in der Historie möglich).

Die gezeigten Beispiel zweigen den neuen Branch direkt vom aktuell ausgecheckten Commit/Branch ab. Also aufpassen, was gerade in der Workingcopy los ist!

Alternativ nutzen Sie die Langform: git branch wuppie master (mit master als Startpunkt; hier kann jeder beliebige Branch, Tag oder Commit genutzt werden).

Nach Anlegen des neuen Branches zeigen beide Pointer auf den selben Commit.

Anmerkung: In neueren Git-Versionen wurde der Befehl "switch" eingeführt, mit dem Sie in der Workingcopy auf einen anderen Branch wechseln können. Der bisherige Befehl "checkout" funktioniert aber weiterhin.

Arbeiten im Entwicklungszweig ...

          D  wuppie
         /
A---B---C  master
  • Entwicklung des neuen Features erfolgt im eigenen Branch: beeinflusst den stabilen master-Branch nicht
  • Wenn in der Workingcopy der Feature-Branch ausgecheckt ist, gehen die Commits in den Feature-Branch; der master bleibt auf dem alten Stand
  • Wenn der master ausgecheckt wäre, würden die Änderungen in den master gehen, d.h. der master würde sich ab Commit C parallel zu wuppie entwickeln

Problem: Fehler im ausgelieferten Produkt

          D  wuppie
         /
A---B---C  master

Fix für master nötig:

  1. git checkout master
  2. git checkout -b fix
  3. Änderungen in fix vornehmen ...

Das führt zu dieser Situation:

          D  wuppie
         /
A---B---C  master
         \
          E  fix

git checkout <branchname> holt den aktuellen Stand des jeweiligen Branches in die Workingcopy. (Das geht in neueren Git-Versionen auch mit git switch <branchname>.)

Man kann weitere Branches anlegen, d.h. hier im Beispiel ein neuer Feature-Branch fix, der auf dem master basiert. Analog könnte man auch Branches auf der Basis von wuppie anlegen ...

Fix ist stabil: Integration in master

          D  wuppie
         /
A---B---C  master
         \
          E  fix
  1. git checkout master
  2. git merge fix => fast forward von master
  3. git branch -d fix

Der letzte Schritt entfernt den Branch fix.

          D  wuppie
         /
A---B---C---E  master
  • Allgemein: git merge <branchname> führt die Änderungen im angegebenen Branch <branchname> in den aktuell in der Workingcopy ausgecheckten Branch ein. Daraus resultiert für den aktuell ausgecheckten Branch ein neuer Commit, der Branch <branchname> bleibt dagegen auf seinem bisherigen Stand.

    Beispiel:

    • Die Workingcopy ist auf A
    • git merge B führt A und B zusammen: B wird in A gemergt
    • Wichtig: Der Merge-Commit (sofern nötig) findet hierbei in A statt!

    In der Abbildung ist A der master und B der fix.

  • Nach dem Merge existieren beide Branches weiter (sofern sie nicht explizit gelöscht werden)

  • Hier im Beispiel findet ein sogenannter "Fast forward" statt.

    "Fast forward" ist ein günstiger Spezialfall beim Merge: Beide Branches liegen in einer direkten Kette, d.h. der Zielbranch kann einfach "weitergeschaltet" werden. Ein Merge-Commit ist in diesem Fall nicht notwendig und wird auch nicht angelegt.

Feature weiter entwickeln ...

          D---F  wuppie
         /
A---B---C---E  master
  1. git switch wuppie
  2. Weitere Änderungen im Branch wuppie ...

git switch <branchname> holt den aktuellen Stand des jeweiligen Branches in die Workingcopy. Man kann also jederzeit in der Workingcopy die Branches wechseln und entsprechend weiterarbeiten.

Hinweis: Während der neue git switch-Befehl nur Branches umschalten kann, funktioniert git checkout sowohl mit Branchnamen und Dateinamen - damit kann man also auch eine andere Version einer Datei in der Workingcopy "auschecken". Falls gleiche Branch- und Dateinamen existieren, muss man für das Auschecken einer Datei noch "--" nutzen: git checkout -- <dateiname>.

Feature ist stabil: Integration in master

          D---F  wuppie                            D---F  wuppie
         /                     =>                 /     \
A---B---C---E  master                    A---B---C---E---G  master
  1. git checkout master
  2. git merge wuppie => Kein fast forward möglich: Git sucht nach gemeinsamen Vorgänger

Hier im Beispiel ist der Standardfall beim Mergen dargestellt: Die beiden Branches liegen nicht in einer direkten Kette von Commits, d.h. hier wurde parallel weitergearbeitet.

Git sucht in diesem Fall nach dem gemeinsamen Vorgänger beider Branches und führt die jeweiligen Änderungen (Differenzen) seit diesem Vorgänger in einem Merge-Commit zusammen.

Im master entsteht ein neuer Commit, da kein fast forward beim Zusammenführen der Branches möglich!

Anmerkung: git checkout wuppie; git merge master würde den master in den wuppie mergen, d.h. der Merge-Commit wäre dann in wuppie.

Beachten Sie dabei die "Merge-Richtung":

  • Die Workingcopy ist auf A
  • git merge B führt A und B zusammen: B wird in A gemergt
  • Wichtig: Der Merge-Commit (sofern nötig) findet hierbei in A statt!

In der Abbildung ist A der master und B der wuppie.

Achtung: Richtung beachten! git checkout A; git merge B führt beide Branches zusammen, genauer: führt die Änderungen von B in A ein, d.h. der entsprechende Merge-Commit ist in A!

Konflikte beim Mergen

(Parallele) Änderungen an selber Stelle => Merge-Konflikte

$ git merge wuppie
Auto-merging hero.java
CONFLICT (content): Merge conflict in hero.java
Automatic merge failed; fix conflicts and then commit the result.

Git fügt Konflikt-Marker in die Datei ein:

<<<<<<< HEAD:hero.java
public void getActiveAnimation() {
    return null;
=======
public Animation getActiveAnimation() {
    return this.idleAnimation;
>>>>>>> wuppie:hero.java
  • Der Teil mit HEAD ist aus dem aktuellen Branch in der Workingcopy
  • Der Teil aus dem zu mergenden Branch ist unter wuppie notiert
  • Das ======= trennt beide Bereiche

Merge-Konflikte auflösen

Manuelles Editieren nötig (Auflösung des Konflikts):

  1. Entfernen der Marker
  2. Hinzufügen der Datei zum Index
  3. Analog für restliche Dateien mit Konflikt
  4. Commit zum Abschließen des Merge-Vorgangs

Alternativ: Nutzung graphischer Oberflächen mittels git mergetool

Rebasen: Verschieben von Branches

          D---F  wuppie                            D---F  wuppie
         /                     =>                 /     \
A---B---C---E  master                    A---B---C---E---G  master

Bisher haben wir Branches durch Mergen zusammengeführt. Dabei entsteht in der Regel ein extra Merge-Commit (im Beispiel G), außer es handelt sich um ein fast forward. Außerdem erkennt man in der Historie sehr gut, dass hier in einem separaten Branch gearbeitet wurde, der irgendwann in den master gemergt wurde.

Leider wird dieses Vorgehen in großen Projekten recht schnell sehr unübersichtlich. Außerdem werden Merges in der Regeln nur von besonders berechtigten Personen (Manager) durchgeführt, die im Falle von Merge-Konflikten diese dann selbst auflösen müssten (ohne aber die fachliche Befähigung zu haben). Hier greift man dann häufig zur Alternative Rebase. Dabei wird der Ursprung eines Branches auf einen bestimmten Commit verschoben. Im Anschluss ist dann ein Merge mit fast forward, also ohne die typischen rautenförmigen Ketten in der Historie und ohne extra Merge-Commit möglich. Dies kann aber auch als Nachteil gesehen werden, da man in der Historie den früheren Branch nicht mehr erkennt! Ein weiterer schwerwiegender Nachteil ist, dass alle Commits im verschobenen Branch umgeschrieben werden und damit neue Commit-IDs bekommen. Das verursacht bei der Zusammenarbeit in Projekten massive Probleme! Als Vorteil gilt, dass man mögliche Merge-Konflikte bereits beim Rebasen auflösen muss, d.h. hier muss derjenige, der den Merge "beantragt", durch einen vorherigen Rebase den konfliktfreien Merge sicherstellen. Mehr dazu in “Branching-Strategien” und “Workflows”.

git rebase master wuppie

führt zu

              D'---F'  wuppie
             /
A---B---C---E  master

Nach dem Rebase von wuppie auf master sieht es so aus, als ob der Branch wuppie eben erst vom master abgezweigt wurde. Damit ist dann ein fast forward Merge von wuppie in den master möglich, d.h. es gibt keine Raute und auch keinen extra Merge-Commit (hier nicht gezeigt).

Man beachte aber die Änderung der Commit-IDs von wuppie: Aus D wird D'! (Datum, Ersteller und Message bleiben aber erhalten.)

Don't lose your HEAD

  • Branches sind wie Zeiger auf letzten Stand (Commit) eines Zweiges

  • HEAD: Spezieller Pointer

    • Zeigt auf den aktuellen Branch der Workingcopy
  • Früheren Commit auschecken (ohne Branch): "headless state"

    • Workingcopy ist auf früherem Commit

    • Kein Branch => Änderungen gehen verloren!

      Eventuelle Änderungen würden ganz normal als Commits auf dem HEAD-Branch aufgezeichnet. Sobald man aber einen anderen Branch auscheckt, wird der HEAD auf diesen anderen Branch gesetzt, so dass die eben gemachten Commits "in der Luft hängen". Sofern man die SHA's kennt, kommt man noch auf die Commits zurück. Allerdings laufen von Zeit zu Zeit interne Aufräum-Aktionen, so dass die Chance gut steht, dass die "kopflosen" Commits irgendwann tatsächlich verschwinden.

Wrap-Up

  • Anlegen von Branches mit git branch
  • Umschalten der Workingcopy auf anderen Branch: git checkout oder git switch
  • Mergen von Branches und Auflösen von Konflikten: git merge
  • Verschieben von Branches mit git rebase
Challenges

Branches und Merges

  1. Legen Sie in Ihrem Projekt einen Branch an. Ändern Sie einige Dateien und committen Sie die Änderungen. Checken Sie den Master-Branch aus und mergen Sie die Änderungen. Was beobachten Sie?

  2. Legen Sie einen weiteren Branch an. Ändern Sie einige Dateien und committen Sie die Änderungen. Checken Sie den Master-Branch aus und ändern Sie dort ebenfalls:

    • Ändern Sie eine Datei an einer Stelle, die nicht bereits im Branch modifiziert wurde.
    • Ändern Sie eine Datei an einer Stelle, die bereits im Branch manipuliert wurde.

    Committen Sie die Änderungen.

    Mergen Sie den Branch jetzt in den Master-Branch. Was beobachten Sie? Wie lösen Sie Konflikte auf?

Mergen am Beispiel

Sie verwalten Ihr Projekt mit Git. Es existieren zwei Branches: master (zeigt auf Commit $C$) und feature (zeigt auf Version $F$). In Ihrer Workingcopy haben Sie den Branch feature ausgecheckt:

(1) Mit welcher Befehlsfolge können Sie den Branch feature in den Branch master mergen, so dass nach dem Merge die im folgenden Bild dargestellte Situation entsteht?

(Der Merge läuft ohne Konflikte ab. Es ist irrelevant, welcher Branch am Ende in der Workingcopy ausgecheckt ist.)

(2) Wie können Sie erreichen, dass es keinen Merge-Commit gibt, sondern dass die Änderungen in $D$ und $F$ im master als eine lineare Folge von Commits erscheinen?

Interaktive Git-Tutorials: Schaffen Sie die Rätsel?

Quellen

Branching-Strategien mit Git

TL;DR

Das Erstellen und Mergen von Branches ist in Git besonders einfach. Dies kann man sich in der Entwicklung zunutze machen und die einzelnen Features unabhängig voneinander in eigenen Hilfs-Branches ausarbeiten.

Es haben sich zwei grundlegende Modelle etabliert: "Git-Flow" und "GitHub Flow".

In Git-Flow gibt es ein umfangreiches Konzept mit verschiedenen Branches für feste Aufgaben, welches sich besonders gut für Entwicklungmodelle mit festen Releases eignet. Es gibt zwei langlaufende Branches: master enthält den stabilen veröffentlichten Stand, in develop werden die Ergebnisse der Entwicklung gesammelt. Features werden in kleinen Feature-Branches entwickelt, die von develop abzweigen und dort wieder hineinmünden. Für Releases wird von develop ein eigener Release-Branch angelegt und nach Finalisierung in den master und in develop gemergt. Fixes werden vom master abgezweigt, und wieder in den master und auch nach develop integriert. Dadurch stehen auf dem master immer die stabilen Release-Stände zur Verfügung, und im develop sammeln sich die Entwicklungsergebnisse.

Der GitHub Flow basiert auf einem deutlich schlankeren Konzept und passt gut für die kontinuierliche Entwicklung ohne echte Releases. Hier hat man auch wieder einen master als langlaufenden Branch, der die stabilen Release-Stände enthält. Vom master zweigen direkt die kleinen Feature-Branches ab und werden auch wieder direkt in den master integriert.

Videos (YouTube)
Videos (HSBI-Medienportal)
Lernziele
  • (K3) Einsatz von Themenbranches in der Entwicklung
  • (K3) Git-Flow-Modell anwenden
  • (K3) GitHub Flow-Modell anwenden

Nutzung von Git in Projekten: Verteiltes Git (und Workflows)

Git ermöglicht ein einfaches und schnelles Branchen. Dies kann man mit entsprechenden Branching-Strategien sinnvoll für die SW-Entwicklung einsetzen.

Im Folgenden sollen also die Frage betrachtet werden: Wie setze ich Branches sinnvoll ein?

Umgang mit Branches: Themen-Branches

                I---J---K  wuppieV1
               /
          D---F  wuppie
         /
A---B---C---E  master
             \
              G---H  test

Branchen ist in Git sehr einfach und schnell. Deshalb wird (gerade auch im Vergleich mit SVN) gern und viel gebrancht.

Ein häufiges anzutreffendes Modell ist dabei die Nutzung von Themen-Branches: Man hat einen Hauptzweig (master). Wann immer eine neue Idee oder ein Baustein unabhängig entwickelt werden soll/kann, wird ein entsprechender Themen-Branch aufgemacht. Dabei handelt es sich normalerweise um kleine Einheiten!

Themenbranches haben in der Regel eine kurze Lebensdauer: Wenn die Entwicklung abgeschlossen ist, wird die Idee bzw. der Baustein in den Hauptzweig integriert und der Themenbranch gelöscht.

  • Vorteil: Die Entwicklung im Themenbranch ist in sich gekapselt und stört nicht die Entwicklung in anderen Branches (und diese stören umgekehrt nicht die Entwicklung im Themenbranch).

  • Nachteil:

    • Mangelnder Überblick durch viele Branches
    • Ursprung der Themenbranches muss überlegt gewählt werden, d.h. alle dort benötigten Features müssen zu dem Zeitpunkt im Hauptzweig vorhanden sein

Umgang mit Branches: Langlaufende Branches

A---B---D  master
     \
      C---E---I  develop
           \
            F---G---H  topic

Häufig findet man in (größeren) Projekten Branches, die über die gesamte Lebensdauer des Projekts existieren, sogenannte "langlaufende Branches".

Normalerweise gibt es einen Branch, in dem stets der stabile Stand des Projekts enthalten ist. Dies ist häufig der master. In diesem Branch gibt es nur sehr wenige Commits: normalerweise nur Merges aus dem develop-Branch (etwa bei Fertigstellung einer Release-Version) und ggf. Fehlerbehebungen.

Die aktive Entwicklung findet in einem separaten Branch statt: develop. Hier nutzt man zusätzlich Themen-Branches für die Entwicklung einzelner Features, die nach Fertigstellung in den develop gemergt werden.

Kleinere Projekte kommen meist mit den zwei langlaufenden Branches in der obigen Darstellung aus. Bei größeren Projekten finden sich häufig noch etliche weitere langlaufende Branches, beispielsweise "Proposed Updates" etc. beim Linux-Kernel.

  • Vorteile:
    • Mehr Struktur im Projekt durch in ihrer Semantik wohldefinierte Branches
    • Durch weniger Commits pro Branch lässt sich die Historie leichter verfolgen (u.a. auch aus bestimmter Rollen-Perspektive: Entwickler, Manager, ...)
  • Nachteile: Bestimmte "ausgezeichnete" Branches; zusätzliche Regeln zum Umgang mit diesen beachten

Komplexe Branching-Strategie: Git-Flow

A---B---------------------G---J1  master
     \                   / \ /
      \                 /   X  fix
       \               /     \
        C-------------F----I--J2  develop
         \           / \  /
          \         /   H1  featureB
           \       /
            D1----D2  featureA
             \
              E1---E2---E3---E4---E5  featureC

Das Git-Flow-Modell von Vincent Driessen (nvie.com/posts/a-successful-git-branching-model) zeigt einen in der Praxis überaus bewährten Umgang mit Branches. Lesen Sie an der angegebenen Stelle nach, besser kann man die Nutzung dieses eleganten Modells eigentlich nicht erklären :-)

Git-Flow: Hauptzweige master und develop

A---B-------E---------------J  master
     \     /               /
      C---D---F---G---H---I---K  develop

Bei Git-Flow gibt es zwei langlaufende Branches: Den master, der immer den stabilen Stand enthält und in den nie ein direkter Commit gemacht wird, sowie den develop, wo letztlich (ggf. über Themenbranches) die eigentliche Entwicklung stattfindet.

Änderungen werden zunächst im develop erstellt und getestet. Wenn die Features stabil sind, erfolgt ein Merge von develop in den master. Hier kann noch der Umweg über einen release-Branch genommen werden: Als "Feature-Freeze" wird vom develop ein release-Branch abgezweigt. Darin wird das Release dann aufpoliert, d.h. es erfolgen nur noch kleinere Korrekturen und Änderungen, aber keine echte Entwicklungsarbeit mehr. Nach Fertigstellung wird der release dann sowohl in den master als auch develop gemergt.

Git-Flow: Weitere Branches als Themen-Branches

A---B---------------------I-------------K  master
     \                   /             /
      C------------F----H-------------J---L  develop
       \          / \  /             /
        \        /   G1  featureB   /
         \      /                  /
          D1---D2  featureA       /
           \                     /
            E1---E2---E3---E4---E5  featureC

Für die Entwicklung eigenständiger Features bietet es sich auch im Git-Flow an, vom develop entsprechende Themenbranches abzuzweigen und darin jeweils isoliert die Features zu entwickeln. Wenn diese Arbeiten eine gewisse Reife haben, werden die Featurebranches in den develop integriert.

Git-Flow: Merging-Detail

---C--------E  develop
    \      /                 git merge --no-ff
     D1---D2  featureA

vs.

---C---D1---D2  develop      git merge

Wenn beim Mergen ein "fast forward" möglich ist, würde Git beim Mergen eines (Feature-) Branches in den develop (oder allgemein in einen anderen Branch) keinen separaten Commit erzeugen (Situation rechts in der Abbildung).

Damit erscheint der develop-Branch wie eine lineare Folge von Commits. In manchen Projekten wird dies bevorzugt, weil die Historie sehr übersichtlich aussieht.

Allerdings verliert man die Information, dass hier ein Feature entwickelt wurde und wann es in den develop integriert wurde (linke Seite in obiger Abbildung). Häufig wird deshalb ein extra Merge-Commit mit git merge --no-ff <branch> (extra Schalter "--no-ff") erzwungen, obwohl ein "fast forward" möglich wäre.

Anmerkung: Man kann natürlich auch über Konventionen in den Commit-Kommentaren eine gewisse Übersichtlichkeit erzwingen. Beispielsweise könnte man vereinbaren, dass alle Commit-Kommentare zu einem Feature "A" mit "feature a:" starten müssen.

Git-Flow: Umgang mit Fehlerbehebung

A---B---D--------F1  master
     \   \      /
      \   E1---E2  fix
       \        \
        C1-------F2  develop

Wenn im stabilen Branch (also dem master) ein Problem bekannt wird, darf man es nicht einfach im master fixen. Stattdessen wird ein extra Branch vom master abgezweigt, in dem der Fix entwickelt wird. Nach Fertigstellung wird dieser Branch sowohl in den master als auch den develop gemergt, damit auch im Entwicklungszweig der Fehler behoben ist.

Dadurch entspricht jeder Commit im master einem Release.

Vereinfachte Braching-Strategie: GitHub Flow

A---B---C----D-----------E  master
     \   \  /           /
      \   ta1  topicA  /
       \              /
        tb1---tb2---tb3  topicB

Github verfolgt eine deutlich vereinfachte Strategie: "GitHub Flow" (vgl. "GitHub Flow" (S. Chacon) bzw. "GitHub flow" (GitHub, Inc.)).

Hier ist der stabile Stand ebenfalls immer im master. Features werden ebenso wie im Git-Flow-Modell in eigenen Feature-Branches entwickelt.

Allerdings zweigen Feature-Branches immer direkt vom master ab und werden nach dem Test auch immer dort wieder direkt integriert (es gibt also keine weiteren langlaufenden Branches wie develop oder release).

In der obigen Abbildung ist zu sehen, dass für die Entwicklung eines Features ein entsprechender Themenbranch vom master abgezweigt wird. Darin erfolgt dann die Entwicklung des Features, d.h. mehrere Commits. Das Mergen des Features in den master erfolgt dann aber nicht lokal, sondern mit einem "Pull-Request" auf dem Server: Sobald man im Feature-Branch einen "diskussionswürdigen" Stand hat, wird ein Pull-Request (PR) über die Weboberfläche aufgemacht (streng genommen gehört dies in die Kategorie “Zusammenarbeit” bzw. “Workflows”; außerdem gehört ein PR nicht zu Git selbst, sondern zum Tooling von Github). In einem PR können andere Entwickler den Code kommentieren und ergänzen. Jeder weitere Commit auf dem Themenbranch wird ebenfalls Bestandteil des Pull-Requests. Parallel laufen ggf. automatisierte Tests etc. und durch das Akzeptieren des PR in der Weboberfläche erfolgt schließlich der Merge des Feature-Branches in den master.

Diskussion: Git-Flow vs. GitHub Flow

In der Praxis zeigt sich, dass das Git-Flow-Modell besonders gut geeignet ist, wenn man tatsächlich so etwas wie "Releases" hat, die zudem nicht zu häufig auftreten.

Das GitHub-Flow-Vorgehen bietet sich an, wenn man entweder keine Releases hat oder diese sehr häufig erfolgen (typisch bei agiler Vorgehensweise). Zudem vermeidet man so, dass die Feature-Branches zu lange laufen, womit normalerweise die Wahrscheinlichkeit von Merge-Konflikten stark steigt. Achtung: Da die Feature-Branches direkt in den master, also den stabilen Produktionscode gemergt werden, ist es hier besonders wichtig, vor dem Merge entsprechende Tests durchzuführen und den Merge erst zu machen, wenn alle Tests "grün" sind.

Hier ein paar Einstiegsseiten für die Diskussion, die teilweise sehr erbittert (und mit ideologischen Zügen) geführt wird (erinnert an die Diskussionen, welche Linux-Distribution die bessere sei):

Wrap-Up

  • Einsatz von Themenbranches für die Entwicklung
  • Unterschiedliche Modelle:
    • Git-Flow: umfangreiches Konzept, gut für Entwicklung mit festen Releases
    • GitHub Flow: deutlich schlankeres Konzept, passend für kontinuierliche Entwicklung ohne echte Releases
Quellen

Arbeiten mit Git Remotes (dezentrale Repos)

TL;DR

Eine der Stärken von Git ist das Arbeiten mit verteilten Repositories. Zu jeder Workingcopy gehört eine Kopie des Repositories, wodurch jederzeit alle Informationen einsehbar sind und auch offline gearbeitet werden kann. Allerdings muss man für die Zusammenarbeit mit anderen Entwicklern die lokalen Repos mit den "entfernten" Repos (auf dem Server oder anderen Entwicklungsrechnern) synchronisieren.

Beim Klonen eines Repositories mit git clone <url> wird das fremde Repo mit dem Namen origin im lokalen Repo festgehalten. Dieser Name wird auch als Präfix für die Branches in diesem Repo genutzt, d.h. die Branches im Remote-Repo tauchen als origin/<branch> im lokalen Repo auf. Diese Remote-Branches kann man nicht direkt bearbeiten, sondern man muss diese Remote-Branches in einem lokalen Branch auschecken und dann darin weiterarbeiten. Es können beliebig viele weitere Remotes dem eigenen Repository hinzugefügt werden.

Änderungen aus einem Remote-Repo können mit git fetch <remote> in das lokale Repo geholt werden. Dies aktualisiert nur die Remote-Branches <remote>/<branch>! Die Änderungen können anschließend mit git merge <remote>/<branch> in den aktuell in der Workingcopy ausgecheckten Branch gemergt werden. (Anmerkung: Wenn mehrere Personen an einem Branch arbeiten, will man die eigenen Arbeiten in dem Branch vermutlich eher auf den aktuellen Stand des Remote rebasen statt mergen!) Eigene Änderungen können mit git push <remote> <branch> in das Remote-Repo geschoben werden.

Um den Umgang mit den Remote-Branches und den davon abgeleiteten lokalen Branches zu vereinfachen, gibt es das Konzept der "Tracking Branches". Dabei "folgt" ein lokaler Branch einem Remote-Branch. Ein einfaches git pull oder git push holt dann Änderungen aus dem Remote-Branch in den ausgecheckten lokalen Branch bzw. schiebt Änderungen im lokalen Branch in den Remote-Branch.

Videos (HSBI-Medienportal)
Lernziele
  • (K3) Erzeugen eines Clones von fremden Git-Repositories
  • (K3) Holen der Änderungen vom fremden Repo
  • (K3) Aktualisierung der lokalen Branches
  • (K3) Pushen der lokalen Änderungen ins fremde Repo
  • (K3) Anlegen von lokalen Branches vs. Anlegen von entfernten Branches
  • (K3) Anlegen eines Tracking Branches zum Vereinfachen der Arbeit

Nutzung von Git in Projekten: Verteiltes Git (und Workflows)

Git ermöglicht eine einfaches Zusammenarbeit in verteilten Teams. Nachdem wir die verschiedenen Branching-Strategien betrachtet haben, soll im Folgenden die Frage betrachtet werden: Wie arbeite ich sinnvoll über Git mit anderen Kollegen und Teams zusammen? Welche Modelle haben sich etabliert?

Clonen kann sich lohnen ...

https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture

---C---D---E  master

=> git clone https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture

./Prog2-Lecture/  (lokaler Rechner)

---C---D---E  master
           ^origin/master

Git-Repository mit der URL <URL-Repo> in lokalen Ordner <directory> auschecken:

  • git clone <URL-Repo> [<directory>]
  • Workingcopy ist automatisch über den Namen origin mit dem remote Repo auf dem Server verbunden
  • Lokaler Branch master ist mit dem remote Branch origin/master verbunden ("Tracking Branch", s.u.), der den Stand des master-Branches auf dem Server spiegelt

Für die URL sind verschiedene Protokolle möglich, beispielsweise:

  • "file://" für über das Dateisystem erreichbare Repositories (ohne Server)
  • "https://" für Repo auf einem Server: Authentifikation mit Username und Passwort (!)
  • "git@" für Repo auf einem Server: Authentifikation mit SSH-Key (diese Variante wird im Praktikum im Zusammenspiel mit dem Gitlab-Server im SW-Labor verwendet)

Eigener und entfernter master entwickeln sich weiter ...

https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture

---C---D---E---F---G  master

./Prog2-Lecture/  (lokaler Rechner)

---C---D---E---H  master
           ^origin/master

Nach dem Auschecken liegen (in diesem Beispiel) drei master-Branches vor:

  1. Der master auf dem Server,
  2. der lokale master, und
  3. die lokale Referenz auf den master-Branch auf dem Server: origin/master.

Der lokale master ist ein normaler Branch und kann durch Commits verändert werden.

Der master auf dem Server kann sich ebenfalls ändern, beispielsweise weil jemand anderes seine lokalen Änderungen mit dem Server abgeglichen hat (git push, s.u.).

Der Branch origin/master lässt sich nicht direkt verändern! Das ist lediglich eine lokale Referenz auf den master-Branch auf dem Server und zeigt an, welchen Stand man bei der letzten Synchronisierung hatte. D.h. erst mit dem nächsten Abgleich wird sich dieser Branch ändern (sofern sich der entsprechende Branch auf dem Server verändert hat).

Anmerkung: Dies gilt analog für alle anderen Branches. Allerdings wird nur der origin/master beim Clonen automatisch als lokaler Branch ausgecheckt.

Zur Abbildung: Während man lokal arbeitet (Commit H auf dem lokalen master), kann es passieren, dass sich auch das remote Repo ändert. Im Beispiel wurden dort die beiden Commits F und G angelegt (durch git push, s.u.).

Wichtig: Da in der Zwischenzeit das lokale Repo nicht mit dem Server abgeglichen wurde, zeigt der remote Branch origin/master immer noch auf den Commit E!

Änderungen im Remote holen und Branches zusammenführen

https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture

---C---D---E---F---G  master

=> git fetch origin

./Prog2-Lecture/  (lokaler Rechner)

---C---D---E---H  master
            \
             F---G  origin/master

Änderungen auf dem Server mit dem eigenen Repo abgleichen

Mit git fetch origin alle Änderungen holen

  • Alle remote Branches werden aktualisiert und entsprechen den jeweiligen Branches auf dem Server: Im Beispiel zeigt jetzt origin/master ebenso wie der master auf dem Server auf den Commit G.
  • Neue Branches auf dem Server werden ebenfalls "geholt", d.h. sie liegen nach dem Fetch als entsprechende remote Branches vor
  • Auf dem Server gelöschte Branches werden nicht automatisch lokal gelöscht; dies kann man mit git fetch --prune origin automatisch erreichen

Wichtig: Es werden nur die remote Branches aktualisiert, nicht die lokalen Branches!

master-Branch nach "git fetch origin" zusammenführen

  1. Mit git checkout master Workingcopy auf eigenen master umstellen
  2. Mit git merge origin/master Änderungen am origin/master in eigenen master mergen
  3. Mit git push origin master eigene Änderungen ins remote Repo pushen

https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture

---C---D---E---H---I  master
            \     /
             F---G

./Prog2-Lecture/  (lokaler Rechner)

---C---D---E---H---I  master
            \     /^origin/master
             F---G

Anmerkung: Schritt (2) kann man auch per git pull origin master erledigen ... Ein pull fasst fetch und merge zusammen (s.u.).

Anmerkung Statt dem merge in Schritt (2) kann man auch den lokalen master auf den aktualisierten origin/master rebasen und vermeidet damit die "Raute". Der pull kann mit der Option "--rebase" auf "rebase" umgestellt werden (per Default wird bei pull ein "merge" ausgeführt).

Auf dem Server ist nur ein fast forward merge möglich

Sie können Ihre Änderungen in Ihrem lokalen master auch direkt in das remote Repo pushen, solange auf dem Server ein fast forward merge möglich ist.

Wenn aber (wie in der Abbildung) der lokale und der remote master divergieren, müssen Sie den Merge wie beschrieben lokal durchführen (fetch/merge oder pull) und das Ergebnis wieder in das remote Repo pushen (dann ist ja wieder ein fast forward merge möglich, es sei denn, jemand hat den remote master in der Zwischenzeit weiter geschoben - dann muss die Aktualisierung erneut durchgeführt werden).

Branches und Remotes

  • Eigenen (neuen) lokalen Branch ins remote Repo schicken

    • git push <remote> <branch>
  • Neuer Branch im remote Repo

    • git fetch <remote> holt (auch) alle neuen Branches
    • Lokale Änderungen an remote Branches nicht möglich! => Remote Branch in lokalen Branch mergen (oder auschecken)

Zusammenfassung: Arbeiten mit Remotes

  1. Änderungen vom Server holen: git fetch <remote> => Holt alle Änderungen vom Repo <remote> ins eigene Repo (Workingcopy bleibt unangetastet!)

  2. Aktuellen lokalen Branch auffrischen: git merge <remote>/<branch> (oder alternativ git pull <remote> <branch>)

  3. Eigene Änderungen hochladen: git push <remote> <branch>

Anmerkung: push geht nur, wenn

  1. Ziel ein "bare"-Repository ist, und
  2. keine Konflikte entstehen

=> im remote Repo nur "fast forward"-Merge möglich

=> bei Konflikten erst fetch und merge, danach push

Anmerkung: Ein "bare"-Repository enthält keine Workingcopy, sondern nur das Repo an sich. Die ist bei Repos, die Sie auf einem Server wie Gitlab oder Github anlegen, automatisch der Fall. Sie können aber auch lokal ein solches "bare"-Repo anlegen, indem Sie beim Initialisieren den Schalter --bare mitgeben: git init --bare ...

Beispiel

git fetch origin           # alle Änderungen vom Server holen
git checkout master        # auf lokalen Master umschalten
git merge origin/master    # lokalen Master aktualisieren

... # Herumspielen am lokalen Master

git push origin master     # lokalen Master auf Server schicken

Vereinfachung: Tracking Branches

  • Tracking Branch: lokaler Branch, der remote Branch "verfolgt"

    • Beispiel: lokaler master-Branch folgt origin/master per Default
  • Vereinfachung im Workflow:

    • git pull entspricht
      1. git fetch <remote> plus
      2. git merge <remote>/<branch>
    • git push entspricht git push <remote> <branch>

Vorsicht: pull und push beziehen sich nur auf ausgecheckten Tracking Branch

Einrichten von Tracking Branches

  • git clone: lokaler master trackt automatisch origin/master

  • Remote Branch als Tracking Branch einrichten:

    1. Änderungen aus remote Repo holen: git fetch <remote>
    2. Tracking Branch anlegen: git checkout -t <remote>/<branch> (=> Option -t richtet den remote Branch als Tracking Branch ein)
  • Lokalen neuen Branch ins remote Repo schicken und als Tracking Branch einrichten:

    1. Lokalen Branch erzeugen: git checkout -b <branch>
    2. Lokalen Branch ins Repo schicken: git push -u <remote> <branch> (=> Option -u richtet den lokalen Branch als Tracking Branch ein)

Hinzufügen eines (weiteren) Remote Repository

Sie können einem Repo beliebig viele Remotes hinzufügen:

git remote add <name> <url>

Beispiel: git remote add andi git@github.com:andi/repo.git

  • Remote origin wird bei clone automatisch angelegt
  • Ansehen der Remotes mit git remote -v
  • fetch, push und pull jeweils über den vergebenen Namen

Beispiel: git fetch andi oder git push origin master

Wrap-Up

  • Synchronisierung des lokalen Repos mit anderen Repos

    • Repo kopieren: git clone <url>
    • Interner Name fürs fremde Repo: origin
    • Änderungen vom fremden Repo holen: git fetch <remote>
    • Änderungen in lokalen Branch einpflegen: git merge <remote>/<branch>
    • Eigene Änderungen ins fremde Repo schieben: git push <remote> <branch>
  • Tracking Branches (Konzept, Anwendung)

    • Remote Branches können lokal nicht verändert werden:
      • In lokale Branches mergen, oder
      • Tracking Branches anlegen => einfaches pull und push nutzen
    • Tracking Branches sind lokale Branches, die remote Branches verfolgen ("tracken")
Challenges

Synchronisierung mit Remote-Repos

Sie haben ein Repo von github.com geklont. Beide Repos, das Original auf dem Server als auch Ihre lokale Kopie, haben sich danach unabhängig voneinander weiter entwickelt (siehe Skizze).

Wie können Sie Ihre Änderung im lokalen Repo auf den Server pushen? Analysieren Sie die Situation und erklären Sie zwei verschiedene Lösungsansätze und geben Sie jeweils die entsprechenden Git-Befehle an.

Interaktive Git-Tutorials: Schaffen Sie die Rätsel?

Quellen

Zusammenarbeit: Git-Workflows und Merge-/Pull-Requests

TL;DR

Git erlaubt unterschiedliche Formen der Zusammenarbeit.

Bei kleinen Teams kann man einen einfachen zentralen Ansatz einsetzen. Dabei gibt es ein zentrales Repo auf dem Server, und alle Team-Mitglieder dürfen direkt in dieses Repo pushen. Hier muss man sich gut absprechen und ein vernünftiges Branching-Schema ist besonders wichtig.

In größeren Projekten gibt es oft ein zentrales öffentliches Repo, wo aber nur wenige Personen Schreibrechte haben. Hier forkt man sich dieses Repo, erstellt also eine öffentliche Kopie auf dem Server. Diese Kopie klont man lokal und arbeitet hier und pusht die Änderungen in den eigenen öffentlich sichtbaren Fork. Um die Änderungen ins Projekt-Repo integrieren zu lassen, wird auf dem Server ein sogenannter Merge-Request (Gitlab) bzw. Pull-Request (GitHub) erstellt. Dies erlaubt zusätzlich ein Review und eine Diskussion direkt am Code. Damit man die Änderungen im Hauptprojekt in den eigenen Fork bekommt, trägt man das Hauptprojekt als weiteres Remote in die Workingcopy ein und aktualisiert regelmäßig die Hauptbranches, von denen dann auch die eigenen Feature-Branches ausgehen sollten.

Videos (HSBI-Medienportal)
Lernziele
  • (K2) Git-Workflows für die Zusammenarbeit
  • (K2) Unterschied zwischen einem Pull/Merge und einem Pull/Rebase
  • (K2) Welche Commits werden Bestandteil eines Merge-Requests (und warum)
  • (K3) Einsatz des zentralisierten Workflows
  • (K3) Einsatz des einfachen verteilten Workflows mit unterschiedlichen Repos
  • (K3) Aktualisieren Ihres Clones und Ihres Forks mit Änderungen aus dem blessed Repo
  • (K3) Erstellen von Beiträgen zu einem Projekt über Merge-Requests
  • (K3) Aktualisierung von Merge-Requests
  • (K3) Diskussion über den Code in Merge-Requests

Nutzung von Git in Projekten: Verteiltes Git (und Workflows)

Git ermöglicht ein einfaches und schnelles Branchen. Dies kann man mit entsprechenden Branching-Strategien sinnvoll für die SW-Entwicklung einsetzen.

Auf der anderen Seite ermöglicht Git ein sehr einfaches verteiltes Arbeiten. Auch hier ergeben sich verschiedene Workflows, wie man mit anderen Entwicklern an einem Projekt arbeiten will/kann.

Im Folgenden sollen also die Frage betrachtet werden: Wie gestalte ich die Zusammenarbeit? Antwort: Workflows mit Git ...

Zusammenarbeit: Zentraler Workflow mit Git (analog zu SVN)

In kleinen Projektgruppen wie beispielsweise Ihrer Arbeitsgruppe wird häufig ein einfacher zentralisierter Workflow bei der Versionsverwaltung genutzt. Im Mittelpunkt steht dabei ein zentrales Repository, auf dem alle Teammitglieder gleichberechtigt und direkt pushen dürfen.

  • Vorteile:

    • Einfachstes denkbares Modell
    • Ein gemeinsames Repo (wie bei SVN)
    • Alle haben Schreibzugriff auf ein gemeinsames Repo
  • Nachteile:

    • Definition und Umsetzung von Rollen mit bestimmten Rechten ("Manager", "Entwickler", "Gast-Entwickler", ...) schwierig bis unmöglich (das ist kein Git-Thema, sondern hängt von der Unterstützung durch den Anbieter des Servers ab)
    • Jeder darf überall pushen: Enge und direkte Abstimmung nötig
    • Modell funktioniert meist nur in sehr kleinen Teams (2..3 Personen)

Zusammenarbeit: Einfacher verteilter Workflow mit Git

In großen und/oder öffentlichen Projekten wird üblicherweise ein Workflow eingesetzt, der auf den Möglichkeiten von verteilten Git-Repositories basiert.

Dabei wird zwischen verschiedenen Rollen ("Integrationsmanager", "Entwickler") unterschieden.

Sie finden dieses Vorgehen beispielsweise beim Linux-Kernel und auch häufig bei Projekten auf Github.

  • Es existiert ein geschütztes ("blessed") Master-Repo

    • Stellt die Referenz für das Projekt dar
    • Push-Zugriff nur für ausgewählte Personen ("Integrationsmanager")
  • Entwickler

    • Forken das Master-Repo auf dem Server und klonen ihren Fork lokal
    • Arbeiten auf lokalem Klon: Unabhängige Entwicklung eines Features
    • Pushen ihren Stand in ihren Fork (ihr eigenes öffentliches Repo): Veröffentlichung des Beitrags zum Projekt (sobald fertig bzw. diskutierbar)
    • Lösen Pull- bzw. Merge-Request gegen das Master-Repo aus: Beitrag soll geprüft und ins Projekt aufgenommen werden (Merge ins Master-Repo durch den Integrationsmanager)
  • Integrationsmanager

    • Prüft die Änderungen im Pull- bzw. Merge-Request und fordert ggf. Nacharbeiten an bzw. lehnt Integration ab (technische oder politische Gründe)
    • Führt Merge der Entwickler-Zweige mit den Hauptzweigen durch Akzeptieren der Pull- bzw. Merge-Requests durch: Beitrag der Entwickler ist im Projekt angekommen und ist beim nächsten Pull in deren lokalen Repos vorhanden

Den hier gezeigten Zusammenhang kann man auf weitere Ebenen verteilen, vgl. den im Linux-Kernel gelebten "Dictator and Lieutenants Workflow" (siehe Literatur).

Hinweis: Hier wird nur die Zusammenarbeit im verteilten Team dargestellt. Dazu kommt noch das Arbeiten mit verschiedenen Branches!

Anmerkung: In der Workingcopy wird das eigene (öffentliche) Repo oft als origin und das geschützte ("blessed") Master-Repo als upstream referenziert.

Anmerkungen zum Forken

Sie könnten auch das Original-Repo direkt clonen. Allerdings würden dann die push dort aufschlagen, was in der Regel nicht erwünscht ist (und auch nicht erlaubt ist).

Deshalb forkt man das Original-Repo auf dem Server, d.h. auf dem Server wird eine Kopie des Original-Repos im eigenen Namespace angelegt. Auf diese Kopie hat man dann uneingeschränkten Zugriff.

Anmerkungen zu den Namen für die Remotes: origin und upstream

Üblicherweise checkt man die Kopie lokal aus (d.h. erzeugt einen Clone). In der Workingcopy verweist dann origin auf die Kopie. Um Änderungen am Original-Repo zu erhalten, fügt man dieses unter dem Namen upstream als weiteres Remote-Repo hinzu. Dies ist eine nützliche Konvention.

Um Änderungen aus dem Original-Repo in den eigenen Fork (und die Workingcopy) zu bringen, führt man dann einfach folgendes aus (im Beispiel für den master):

$ git checkout master         # Workingcopy auf master
$ git pull upstream master    # Aktualisiere lokalen master mit master aus Original-Repo
$ git push origin master      # Pushe lokalen master in den Fork

Feature-Branches aktualisieren: Mergen mit master vs. Rebase auf master

Im Netz finden sich häufig Anleitungen, wonach man Änderungen im master mit einem Merge in den Feature-Branch holt, also sinngemäß:

$ git checkout master         # Workingcopy auf master
$ git pull upstream master    # Aktualisiere lokalen master mit master aus Vorgabe-Repo
$ git checkout feature        # Workingcopy auf feature
$ git merge master            # Aktualisiere feature: Merge master in feature
$ git push origin feature     # Push aktuellen feature ins Team-Repo

Das funktioniert rein technisch betrachtet.

Allerdings spielt in den meisten Git-Projekten der master üblicherweise eine besondere Rolle (vgl. Branching-Strategien) und ist üblicherweise stets das Ziel eines Merge, aber nie die Quelle! D.h. per Konvention geht der Fluß von Änderungen stets in den master (und nicht heraus).

Wenn man sich nicht an diese Konvention hält, hat man später möglicherweise Probleme, die Merge-Historie zu verstehen (welche Änderung kam von woher)!

Um die Änderungen im master in einen Feature-Branch zu bekommen, sollte deshalb ein Rebase (des Feature-Branches auf den master) vor einem Merge (des master in den Feature-Branch) bevorzugt werden.

Merk-Regel: Merge niemals nie den master in Feature-Branches!

Achtung: Ein Rebase bei veröffentlichten Branches ist problematisch, da Dritte auf diesem Branch arbeiten könnten und entsprechend auf die Commit-IDs angewiesen sind. Nach einem Rebase stimmen diese Commit-IDs nicht mehr, was normalerweise mindestens zu Verärgerung führt ... Die Dritten müssten ihre Arbeit dann auf den neuen Feature-Branch (d.h. den Feature-Branch nach dessen Rebase) rebasen ... vgl. auch "The Perils of Rebasing" in Abschnitt "3.6 Rebasing" in [Chacon2014].

Mögliches Szenario im Praktikum

Im Praktikum haben Sie das Vorgabe-Repo. Dieses könnten Sie als upstream in Ihre lokale Workingcopy einbinden.

Mit Ihrem Team leben Sie vermutlich einen zentralen Workflow, d.h. Sie binden Ihr gemeinsames Repo als origin in Ihre lokale Workingcopy ein.

Dann müssen Sie den lokalen master aus beiden Remotes aktualisieren. Zusätzlich wollen Sie Ihren aktuellen Themenbranch auf den aktuellen master rebasen.

$ git checkout master         # Workingcopy auf master
$ git pull upstream master    # Aktualisiere lokalen master mit master aus Vorgabe-Repo
$ git pull origin master      # Aktualisiere lokalen master mit master aus Team-Repo
$ git push origin master      # Pushe lokalen master in das Team-Repo zurück
$ git rebase master feature   # Rebase feature auf den aktuellen lokalen master
$ git push -f origin feature  # Push aktuellen feature ins Team-Repo ("-f" wg. geänderter IDs durch rebase)

Anmerkung: Dabei können in Ihrem master die unschönen "Rauten" entstehen. Wenn Sie das vermeiden wollen, tauschen Sie den zweiten und den dritten Schritt und führen den Pull gegen den Upstream-master als pull --rebase durch. Dann müssen Sie Ihren lokalen master allerdings auch force-pushen in Ihr Team-Repo und die anderen Team-Mitglieder sollten darüber informiert werden, dass sich der master auf inkompatible Weise geändert hat ...

Kommunikation: Merge- bzw. Pull-Requests

Mergen kann man auf der Konsole (oder in der IDE) und anschließend die (neuen) Branches auf den Server pushen.

Die verschiedenen Git-Server erlauben ebenfalls ein GUI-gestütztes Mergen von Branches: "Merge-Requests" (MR, Gitlab) bzw. "Pull-Requests" (PR, Github). Das hat gegenüber dem lokalen Mergen wichtige Vorteile: Andere Entwickler sehen den beabsichtigten Merge (frühzeitig) und können direkt den Code kommentieren und die vorgeschlagenen Änderungen diskutieren, aber auch allgemeine Kommentare abgeben.

Falls möglich, sollte man einen MR/PR immer dem Entwickler zuweisen, der sich weiter um diesen MR/PR kümmern wird (also zunächst ist man das erstmal selbst). Zusätzlich kann man einen Reviewer bestimmen, d.h. wer soll sich den Code ansehen.

Hier ein Screenshot der Änderungsansicht unseres Gitlab-Servers (SW-Labor):

Nachfolgend für den selben MR aus der letzten Abbildung noch die reine Diskussionsansicht:

Best Practices bei Merge-/Pull-Requests

  1. MR/PR so zeitig wie möglich aufmachen
    • Am besten sofort, wenn ein neuer Branch auf den Server gepusht wird!
    • Ggf. mit dem Präfix "WIP" im Titel gegen unbeabsichtigtes vorzeitiges Mergen sperren ... (bei GitHub als "Draft"-PR öffnen)
  2. Auswahl Start- und Ziel-Branch (und ggf. Ziel-Repo)
    • Es gibt verschiedene Stellen, um einen MR/PR zu erstellen. Manchmal kann man nur noch den Ziel-Branch einstellen, manchmal beides.
    • Bitte auch darauf achten, welches Ziel-Repo eingestellt ist! Bei Forks wird hier immer das Original-Repo voreingestellt!
    • Den Ziel-Branch kann man ggf. auch nachträglich durch Editieren des MR/PR anpassen (Start-Branch und Ziel-Repo leider nicht, also beim Erstellen aufpassen!).
  3. Titel (Summary): Das ist das, was man in der Übersicht sieht!
    • Per Default wird die letzte Commit-Message eingesetzt.
    • Analog zur Commit-Message: Bitte hier unbedingt einen sinnvollen Titel einsetzen: Was macht der MR/PR (kurz)?
  4. Beschreibung: Was passiert, wenn man diesen MR/PR akzeptiert (ausführlicher)?
    • Analog zur Commit-Message sollte hier bei Bedarf die Summary ausformuliert werden und beschreiben, was der MR/PR ändert.
  5. Assignee: Wer soll sich drum kümmern?
    • Ein MR/PR sollte immer jemanden zugewiesen sein, d.h. nicht "unassigned" sein. Ansonsten ist nicht klar, wer den Request durchführen/akzeptieren soll.
    • Außerdem taucht ein nicht zugewiesener MR/PR nicht in der Übersicht "meiner" MR/PR auf, d.h. diese MR/PR haben die Tendenz, vergessen zu werden!
  6. Diskussion am (und neben) dem Code
    • Nur die vorgeschlagenen Code-Änderungen diskutieren!
    • Weitergehende Diskussionen (etwa über Konzepte o.ä.) besser in separaten Issues erledigen, da sonst die Anzeige des MR/PR langsam wird (ist beispielsweise ein Problem bei Gitlab).
  7. Weitere Commits auf dem zu mergenden Branch gehen automatisch mit in den Request
  8. Weitere Entwickler kann man mit "@username" in einem Kommentar auf "CC" setzen und in die Diskussion einbinden

Anmerkung: Bei Gitlab (d.h. auch bei dem Gitlab-Server im SW-Labor) gibt es "Merge-Requests" (MR). Bei Github gibt es "Pull-Requests" (PR) ...

Wrap-Up

  • Git-Workflows für die Zusammenarbeit:

    • einfacher zentraler Ansatz für kleine Arbeitsgruppen vs.
    • einfacher verteilter Ansatz mit einem "blessed" Repo (häufig in Open-Source-Projekten zu finden)
  • Aktualisieren Ihres Clones und Ihres Forks mit Änderungen aus dem "blessed" Repo

  • Unterschied zwischen einem Pull/Merge und einem Pull/Rebase

  • Erstellen von Beiträgen zu einem Projekt über Merge-Requests

    • Welche Commits werden Bestandteil eines Merge-Requests (und warum)
    • Diskussion über den Code in Merge-Requests
Quellen