Einleitung

Was ist Git

Git ist eine Software, die die zu einem “Projekt” gehörenden Dateien verwalten kann.

Unter “Projekt” kann man z.B. verstehen

Ein Projekt, das von Git verwaltet wird, heißt Repository.

Git kann “Speicherpunkte” anlegen, die den Zustand des Projekts zu einem Zeitpunkt festhalten. Die Speicherpunkte heißen Commits.

Nutzen

Wenn man Git sinnvoll einsetzt, kann es einem nützen:

Theorie

Commits

Bestandteile eines Commits

Ein Commit repräsentiert den Zustand des Projekts zu einem bestimmten Zeitpunkt.

Zu jedem Commit speichert Git:

  • Name, Zugriffsrechte und Inhalt aller Dateien, die zum Projekt gehören

  • Zeitpunkt, zu dem der Commit erstellt wurde

  • Name desjenigen, der den Commit erstellt hat

  • eine Beschreibung des Commits (muss der Ersteller des Commits möglichst sinnvoll eingeben)

Jeder Commit erhält von Git automatisch einen Namen, der so gewählt ist, dass jeder Commit eindeutig identifiziert werden kann. (Nicht nur innerhalb eines Repositories, sondern praktisch über alle Repositories auf der Welt!)

Der Name eines Commits ist ein 40-stelliger Code aus Hexadezimalziffern und heißt auch Hash. z.B.: 64efe35c4535b472d2fe38a6f696adc56dc38ff4

Außerdem enthält jeder Commit:

Einen Verweis auf einen (oder mehrere) Vorgänger-Commit (“parent”)

(Nur der allererste Commit in einem Repository hat keinen Vorgänger)

Commit-Graph

Durch die Verweise auf die jeweiligen Vorgänger werden Commits untereinander verknüpft und repräsentieren gemeinsam den Entwicklungsfortschritt des Projekts.

Beispiele:

(A ← B heißt: A ist Vorgänger von B)

(links ist “alt”, rechts ist “neu”)

  1. Einfachster Fall, lineare Entwicklungsgeschichte:

     A ← B ← C ← D
  2. Verzweigung der Entwicklung:

     A ← B ← C ← D
         ↑
         └ E ← F ← G

    B ist Vorgänger von C und auch von E.

  3. Entwicklung hat unterschiedlichen Verlauf genommen und wird wieder zusammengeführt:

     A ← B ← C ←---┐
         ↑         |
         └ D ← E ← F
    • F hat zwei Vorgänger, nämlich C und E.
    • F ist ein “merge commit”.
  4. Dies ist nicht möglich:

     A ← B ← C
         ↓   ↑
         D --┘

    Ein Commit kann nicht sein eigener Vorfahre sein.

Im Allgemeinen gilt: Die Commits bilden einen gerichteten, azyklischen Graphen. (DAG).

Referenzen

Von einem bestimmten Commit aus betrachtet sind nur seine Vorfahren “sichtbar”/“erreichbar”:

A ← B ← C ← D
    ↑
    └ E ← F ← G

z.B. von E aus:

A ← B ← E

C, D, F, G sind von E aus nicht sichtbar/erreichbar.

Deshalb muss im Repository irgendwie gespeichert sein, welches der jeweils “neueste” Commit ist. (Als “Einstiegspunkt” in den Graphen)

Dazu gibt es Referenzen. Eine Referenz ist ein Verweis auf einen Commit.

Die gebräuchlichste Art von Referenz heißt Branch.

Branches

Analogie:

  • Lesezeichen im Webbrowser: Zuordnung Webseiten-Name → URL
  • Git Branch: Zuordnung Branch-Name → Commit-Hash

Beispiel:

A ← B [base]
    ↑
    ├ C ← D ← E [feature1]
    |
    └ F ← G [feature2] [feature3]
  • Es gibt 4 Branches: base, feature1, feature2, feature3
  • base verweist auf Commit B
  • feature1 verweist auf Commit E
  • sowohl feature2 als auch feature3 verweisen auf Commit G

Merke:

  • In einem Repository kann es beliebig viele Branches geben
  • Mehrere Branches können auf den selben Commit verweisen (z.B. oben: G)
  • Ein Branch muss nicht auf die “Spitze” der Entwicklungsgeschichte verweisen z.B. oben: base/B. Allerdings ist von einem Branch aus gesehen natürlich nur dessen “Vergangenheit” sichtbar (A ← B).

Mögliche Operationen mit Branches:

  • Umbenennen

    z.B. von

      ... C ← D ← E [feature1]

    zu

      ... C ← D ← E [my-feature]
  • Auf einen anderen Commit versetzen

    z.B. von

      A ← B [base]
          ↑
          ├ C ← D ← E [feature1]
          |
          └ F ← G [feature2] [feature3]

    zu

      A ← B [base] [feature3]
          ↑
          ├ C ← D ← E [feature1]
          |
          └ F ← G [feature2]

    (feature3 von G auf B versetzt; feature2 bleibt unberührt)

  • Löschen

    z.B. von

      ... F ← G [feature2]

    zu

      ... F ← G

Wichtig:

  • Die genannten Operationen mit Branches ändern nur eine Referenz (“Lesezeichen”) auf einen Commit.

  • Andere Branches und die Commits selbst bleiben unberührt.

  • Es kann allerdings sein, dass durch Versetzen oder Löschen eines Branchs ein Commit nicht mehr erreichbar ist (z.B. G oben)

    Solange der Hash eines unerreichbaren Commits noch bekannt ist, kann problemlos wieder ein neuer Branch angelegt werden, der auf den Commit verweist.

    Erst nach einiger Zeit (z.B. 2 Wochen) wird Git unerreichbare Commits tatsächlich löschen, z.B. von

      A ← B [base] [feature3]
          ↑
          ├ C ← D ← E [feature1]
          |
          └ F ← G

    zu

      A ← B [base] [feature3]
          ↑
          └ C ← D ← E [feature1]

    (F und G wurden von keinem Branch aus mehr erreicht und wurden schließlich gelöscht)

    Das Löschen von unerreichbaren Commits geschieht im Rahmen von “Aufräumarbeiten” (“garbage collection”), die Git regelmäßig im Repository durchführt, z.B. um Speicherplatz zu sparen.

Working Directory, Index

Wie entstehen neue Commits?

Git unterscheidet zu einem gegebenen Zeitpunkt drei konzeptuell verschiedene Zustände der zu einem Projekt gehörenden Dateien:

  • Den Zustand, der in dem Commit gespeichert ist, auf den HEAD gerade verweist (der “neueste Speicherpunkt”).

  • Den Zustand, in dem sich die Dateien im Arbeitsverzeichnis (“working directory”) gerade tatsächlich befinden (für den Benutzer ganz normal sichtbar).

  • Den sogenannten Index.

Der Index repräsentiert den Zustand des Projekts, der in einem neuen Commit gespeichert werden soll. (“staging area”)

Sind die drei Zustände HEAD, Arbeitsverzeichnis und Index identisch, ist das Repository “clean”. Dies ist normalerweise der Ausgangszustand, bevor man an dem Projekt weiterarbeitet, bzw. nachdem man einen Fortschritt am Projekt abgeschlossen hat.

Um einen neuen Commit zu erzeugen, müssen Inhalte zum Index hinzugefügt werden.

Das geschieht im einfachsten Fall mit folgendem, sich pro Commit wiederholendem, Ablauf:

  1. Repository “clean”

    Der Commit-Graph könnte so aussehen: HEAD verweist auf den “aktiven” Branch feature1, dieser wiederum auf den Commit, der den “aktuellen” Zustand C des Projekts speichert.

                  HEAD
                   ↓
     A ← B ← C [feature1]

    Arbeitsverzeichnis, Index und HEAD sind alle im gleichen Zustand C:

     Arbeitsverzeichnis     Index      HEAD
     ------------------     -----      ----
             C                C          C
  2. Der Inhalt des Arbeitsverzeichnisses wird vom Benutzer verändert (die eigentliche Arbeit an dem Projekt, d.h. neue Dateien oder Inhalte werden hinzugefügt, bestehendes wird verbessert, modifiziert, etc.).

    Nun sind die Zustände von Index und HEAD immer noch identisch, aber das Arbeitsverzeichnis ist davon verschieden. (“unstaged changes”)

    (Hier repräsentiert durch Zustand C → Zustand D).

     Arbeitsverzeichnis     Index     HEAD
     ------------------     -----     ----
             D                C         C
  3. Der geänderte Inhalt des Arbeitsverzeichnis wird in den Index übertragen.

     Arbeitsverzeichnis     Index     HEAD
     ------------------     -----     ----
             D          -->   D         C

    Jetzt sind Arbeitsverzeichnis und Index identisch (“staged changes”, “changes to be committed”), aber HEAD ist noch auf dem “alten” Stand.

  4. Ein neuer Commit wird erstellt, der den aktuellen Inhalt des Index speichert. Danach ist auch HEAD im Zustand D.

     Arbeitsverzeichnis     Index     HEAD
     ------------------     -----     ----
             D                D   -->   D

    Das Repository ist jetzt wieder “clean”.

Beim letzten Schritt (dem “committen”) führt Git intern mehrere Teilschritte aus, die sich anhand des Commit-Graphen darstellen lassen:

  • Ausgangssituation:

    HEAD verweist auf den “aktiven” Branch feature1, dieser wiederum auf den Commit, der den “aktuellen” Zustand C des Projekts speichert.

                   HEAD
                    ↓
      A ← B ← C [feature1]
  • Aus dem Inhalt des Index (Zustand D) wird ein neuer Commit erzeugt, als dessen Vorgänger der momentan aktuelle Commit eingetragen wird.

                   HEAD
                    ↓
      A ← B ← C [feature1]
              ↑
              └ D

    Der Branch feature1 und somit indirekt auch HEAD verweist zunächst noch auf Commit C.

  • Der Branch feature1 wird aktualisiert, so dass er nicht mehr auf Commit C, sondern auf Commit D verweist.

                       HEAD
                        ↓
      A ← B ← C ← D [feature1]

    HEAD wird selbst garnicht direkt modifiziert, sondern verweist die ganze Zeit auf den Branch-Namen feature1! Nur indirekt ändert sich der Branch, auf den HEAD verweist.

Vorteile des Index

Auf den ersten Blick erscheint der Index unnötig. Man könnte ja auch direkt den Zustand des Arbeitsverzeichnisses in einem Commit speichern.

Dank des Index ist es aber möglich, nur einen Teil der Änderungen aus dem Arbeitsverzeichnis in einen neuen Commit zu übernehmen, bzw. schließlich alle Änderungen auf mehrere Commits zu verteilen.

Das kann bedeuten, nur Änderungen aus bestimmten Dateien zu übernehmen, oder sogar nur bestimmte Änderungen, die man an einer einzelnen Datei gemacht hat.

Änderungen auf mehrere Commits aufzuteilen kann sinnvoll sein, wenn man noch einmal den Commit-Graphen als Repräsentation des Entwicklungsfortschritts eines Projekts betrachtet:

  • Man kann Änderungen, die konzeptuell voneinander unabhängig sind, in verschiedenen Commits speichern. Dadurch fällt es leichter, zu jedem Commit eine passende Beschreibung zu finden, und später lässt sich somit einfacher nachvollziehen, was man getan hat.

  • Vor allem im Zusammenhang mit Softwareprojekten ist es sinnvoll, wenn die in einem Commit enthaltenen Änderungen nur relativ kleine, in sich abgeschlossene Einheiten bilden. Sollte man später in der Software einen Fehler finden, lässt sich dann einfacher feststellen, durch welche Änderung genau der Fehler verursacht worden ist.

Beispiel (alternative zu oben):

Arbeitsverzeichnis     Index      HEAD
------------------     -----      ----
        C                C          C
        D                C          C
        D          -->   C1         C
        D                C1   -->   C1
        D          -->   C2         C1
        D                C2   -->   C2
        D          -->   D          C2
        D                D    -->   D

Resultierender Commit-Graph:

A ← B ← C ← C1 ← C2 ← D

Die Änderung von Zustand C auf Zustand D ist hier auf die Commits C1, C2 und D aufgeteilt.

Zusammenfassung Theorie

Praxis

Grundlagen

Git benutzt man durch Eingabe von Befehlen im Terminal.

Für die meisten Befehle (die sich auf ein bestimmtes Repository beziehen), muss das Arbeitsverzeichnis innerhalb des Repositories sein.

Die Befehle haben normalerweise die Form

$ git <Unterbefehl> <Argumente>

Die Dokumentation der Unterbefehle erhält man mit

$ man git <Unterbefehl>

oder auf https://git-scm.com/docs.

Grundeinstellungen

Bevor man Git das erste Mal auf einem Rechner benutzt, muss man u.U. noch ein paar Einstellungen machen:

  • Eigenen Namen und Mailadresse angeben (kann auch fiktiv sein, wird von Git in Commits eingetragen, die der Benutzer erstellt)

      $ git config --global user.name <Vorname> <Nachname>
      $ git config --global user.email <E-Mail-Adresse>
  • Farbige Ausgabe auf dem Terminal aktivieren (erhöht die Übersicht)

      $ git config --global color.ui auto

    Damit sieht die Ausgabe von Git übersichtlicher aus.

  • Texteditor festlegen

      $ git config --global core.editor <Name des Editors>

    Manchmal öffnet Git einen Texteditor um eine Eingabe vom Benutzer zu verlangen, der hier angegebene wird verwendet.

    Beispiele: gedit, nano

Neues Repository anlegen

Mit dem Befehl

$ git init

wird im Arbeitsverzeichnis ein neues Git-Repository angelegt.

Das kann man daran erkennen, dass ein Unterverzeichnis namens .git erzeugt wurde. Es enthält alle Daten, die zu dem Repository gehören.

(Falls man git init in einem schon bestehenden Repository ausführt, wird dadurch kein Schaden angerichtet.)

Bestehendes Repository klonen

Mit dem Befehl

$ git clone <Adresse>

wird im Arbeitsverzeichnis ein neues Unterverzeichnis angelegt und darin ein Klon eines vorhandenen Repositories erstellt.

Das vorhandene Repository wird an der angegebenen Adresse gesucht. Bei der Adresse kann es sich um einen Verzeichnispfad im eigenen Dateisystem oder um eine URL im Internet handeln, wie z.B.

https://sus.ziti.uni-heidelberg.de/Lehre/WS1718_Tools/GIT/uebung.git

Zustand des Repositories betrachten

$ git status

zeigt Informationen über den aktuellen Zustand des Repositories an, z.B.:

  • Wurden Dateien geändert oder gelöscht?
  • Wenn ja, welche?
  • Gibt es Dateien, die noch nicht zum Repository hinzugefügt wurden?
$ git log

zeigt vom aktuellen Commit ausgehend (worauf HEAD verweist) den Verlauf aller Vorgänger-Commits.

Einzelne Commits genauer betrachten

$ git show <Commit>

zeigt die Details zu einem Commit, und die Unterschiede der Dateien des Projekts im Vergleich zum Vorgänger-Commit.

Die Angabe des Commits kann ein Commit-Hash, oder ein Branch-Name (oder eine sonstige Referenz) sein.

Lässt man die Angabe des Commits weg (d.h. nur git show), zeigt Git die Details des aktuellen Commits (HEAD) an.

Anmerkung: Prinzipiell kann Git mit allen Arten von Dateien umgehen, aber alles was mit der Darstellung von Unterschieden zu tun hat, funktioniert nur mit reinen Textdateien (Quelltext in Programmiersprachen, LaTeX-Dokumente, etc.)

Die Darstellung der Unterschiede zwischen zwei Versionen einer Datei geschieht zeilenweise:

  • Zeilen, die vorher da waren und jetzt nicht mehr, wird ein - vorangestellt.

  • Zeilen, die vorher nicht da waren, und jetzt neu sind, wird ein + vorangestellt.

Wenn die Ausgabe länger als eine Bildschirmseite ist, verwendet Git automatisch less, d.h. man kann wie gewohnt darin navigieren und kommt mit q wieder heraus.

Unterschiede zwischen zwei Commits

$ git diff <Commit 1> <Commit 2>

zeigt die Unterschiede der Dateien des Projekts zwischen den beiden angegebenen Commits.

Nützliche Optionen für alle Varianten von git diff:

  • -w: ignoriert geänderte Leerzeichen

  • --color-words: zeigt Änderungen wortweise anstatt zeilenweise

Grafische Oberfläche

Für die Betrachtung des Entwicklungsverlaufs des Projekts, der Details einzelner Commits und der Unterschiede zwischen Commits gibt es auch eine grafische Benutzerobefläche. Sie wird geöffnet mit

$ gitk

(falls auf dem System installiert)

Mit $ gitk –all

bekommt man die Commits aller Branches angezeigt. Ohne --all nur die Commits des aktiven Branchs.

Mit dem Repository arbeiten

Mit dem Index umgehen

Inhalte aus dem Arbeitsverzeichnis werden mit dem Befehl

$ git add <Dateiname>

in den Index übertragen.

Mit dem Befehl

$ git reset <Dateiname>

werden Inhalte aus dem HEAD-Zustand in den Index übertragen (macht git add <Dateiname> wieder rückgängig).

Beispiel:

    Arbeitsverzeichnis     Index         HEAD
    ------------------     -----         ----
            D                C             C
            D      add -->   D             C
            D                C  <-- reset  C

Mit dem Befehl

$ git checkout -- <Dateiname>

wird der Zustand der Datei aus dem Index in das Arbeitsverzeichnis übertragen, d.h. die noch nicht im Repository gespeicherten Änderungen an der Datei werden rückgängig gemacht:

    Arbeitsverzeichnis     Index         HEAD
    ------------------     -----         ----
            D                C             C
            C  <-- checkout  C             C

Unterschiede betrachten

Git kann die Unterschiede zwischen Arbeitsverzeichnis, Index und Commits anzeigen:

$ git diff

zeigt den Unterschied zwischen Arbeitskopie und Index (“unstaged changes”, könnte noch mit git add zum Index hinzugefügt werden).

$ git diff <Commit>

zeigt den Unterschied zwischen Arbeitskopie und einem Commit (durch den Commit-Hash oder einen Branch-Namen oder eine andere Referenz angegeben).

$ git diff --cached [<Commit>]

zeigt den Unterschied zwischen Index und HEAD (“staged changes”, wäre der Unterschied zwischen dem aktuellen und einem neuen Commit), bzw. zwischen Index und einem anderen Commit als HEAD, falls angegeben.

    Arbeitsverzeichnis     Index                HEAD
    ------------------     -----                ----
            |                |                   |
            |<---- diff ---->|<- diff --cached ->|
            |                                    |
            |<---------- diff HEAD ------------->|

Commit erzeugen

Hat man mit git add Inhalte zum Index hinzugefügt (git diff --cached zeigt dann an, was der Unterschied zwischen dem aktuellen und einem neuen Commit wäre), wird mit

$ git commit

der Inhalt des Index zu einem neuen Commit gemacht. Git öffnet einen Texteditor, in dem man aufgefordert wird, die Commit-Message als Beschreibung der gemachten Änderungen einzugeben.

Um die eingegebene Beschreibung zu bestätigen, muss man die im Texteditor geöffnete Datei (namens COMMIT_EDITMSG) speichern und den Editor danach beenden. Erst dann schließt Git das Anlegen des Commits ab.

Mit der Option -m kann man die Message (in Anführungszeichen) auch direkt an den Befehl übergeben.

Erste Zusammenfassung

Ein Repository bekommen:

Ein Repository betrachten:

Oder gitk als grafische Oberfläche benutzen.

Index manipulieren und neue Commits erstellen:

Genauere Kontrolle über den Index

Die Befehle git add, git reset und git checkout haben die Option -p.

Damit kann man die jeweilige Aktion (Übertragung von Inhalt zwischen Index, Arbeitsverzeichnis und HEAD) nur für Teile der Änderungen in einer Datei interaktiv durchführen.

Die bestehenden Unterschiede werden in sogenannte “Hunks” aufgeteilt, und man bekommt für jeden Hunk die Möglichkeit, die o.g. Aktion durchzuführen (y) oder nicht (n). Durch die Antwort q bricht man die interaktive Abfrage ab.

Die Angabe eines Dateinamens (wie bei git add <Datei> ohne -p) ist hier möglich, um die Änderungen auf diese Datei zu beschränken, aber nicht nötig:

$ git add -p <Datei>

fragt nach den Änderungen in der Datei.

$ git add -p

fragt nach den Änderungen in allen Dateien.

Man kann die Hunks manchmal noch weiter aufteilen lassen, indem man die Antwort s gibt.

Mit Branches umgehen

Branches anlegen, umbenennen, versetzen, und löschen

In einem neu angelegten Repository gibt es standardmäßig einen Branch namens “master”.

Einen neuen Branch kann man mit dem Befehl

$ git branch <Name> <Commit>

erzeugen. Der neue Branch mit dem angegebenen Namen verweist dann auf den Commit, der durch seinen Hash, oder eine Referenz (z.B. existierender Branch-Name) angegeben wird.

(Oder in gitk im Kontextmenü durch Rechtsklick auf einen Commit.)

Eine Liste aller Branches bekommt man mit

$ git branch

Einen Branch kann man mit

$ git branch -m <alter Name> <neuer Name>

umbenennen. Worauf der Branch verweist, bleibt unverändert.

Einen Branch kann man mit

$ git branch -d <Branch>

löschen. Es wird nur die Referenz (“Lesezeichen”) auf einen Commit gelöscht, alle Commits selbst bleiben erhalten.

Falls durch das Löschen des Branchs Commits unerreichbar werden würden, verweigert Git das Löschen. Man kann es aber mit -D anstatt -d erzwingen.

Um einen Branch von einem Commit auf einen anderen Commit zu versetzen dient ebenfalls der Befehl git reset, in drei “Abstufungen”:

  1. Soft

     $ git reset --soft <Commit>
    • Versetzt den aktiven Branch (auf den HEAD gerade zeigt) auf den angegebenen Commit.

    • Index und Arbeitsverzeichnis bleiben unverändert.

  2. Mixed (ist Standard)

     $ git reset --mixed <Commit>
     $ git reset <Commit>
    • Versetzt den aktiven Branch (auf den HEAD gerade zeigt) auf den angegebenen Commit.

    • Überträgt den Zustand des angegebenen Commits in den Index.

    • Das Arbeitsverzeichnis bleibt unverändert.

  3. Hard

     $ git reset --hard <Commit>
    • Versetzt den aktiven Branch (auf den HEAD gerade zeigt) auf den angegebenen Commit.

    • Überträgt den Zustand des angegebenen Commits in den Index und ins Arbeitsverzeichnis.

Den aktiven Branch wechseln

Mit dem Befehl

$ git checkout <Branch>

wird HEAD auf den angegeben Branch gesetzt, sowie der Zustand des Projekts, der mit dem Commit gespeichert ist, auf den der Branch gerade verweist, sowohl in den Index als auch das Arbeitsverzeichnis übertragen.

(Das Repository sollte zuvor “clean” sein)

Somit kann man auch leicht eine beliebige Version des Projekts wiederherstellen:

$ git branch <Name> <gewünschte Version>
$ git checkout <Name>

Diese beiden Befehle kann man auch abkürzen mit:

$ git checkout -b <Name> <gewünschte Version>

Merge

Ein Commit kann mehr als einen Vorgänger haben.

Wie kommt man zu so einem Commit? Durch git commit wird immer nur ein Commit erstellt, der einen Vorgänger hat.

Einen Commit mit mehr als einem Vorgänger (meistens zwei) bekommt man unter Umständen mit dem Befehl

$ git merge <anderer Branch>

Einfacher Fall,

in dem kein neuer Commit erzeugt wird:

Wenn der aktuelle Branch (HEAD) ein Vorfahre des anderen Branchs ist, kann der aktuelle Branch einfach auf den neuesten Commit im anderen Branch vorgerückt werden (“fast-forward”):

Situation vorher, “master” ist der aktive Branch:


Wird durch

$ git merge hotfix

zu


(Bildquelle)

Allgemeiner Fall,

der eintritt, wenn der aktuelle Branch kein Vorfahre des anderen Branchs ist, wie z.B. (“master” ist aktiver Branch):


Dann wird mit

$ git merge iss53

ein neuer Commit erstellt, der auf

  • den bisher neuesten Commit im aktuellen Branch, und
  • den neuesten Commit im anderen Branch

als Vorgänger verweist.


(Bildquelle)

  • Der aktuelle Branch wird auf den neuen Commit vorgerückt,
  • der andere Branch bleibt von dem ganzen Vorgang unberührt.

Konflikte

Git versucht, die Änderungen, die in den Commits des anderen Branch seit dem gemeinsamen Vorfahren (im Beispiel: C3, C5) gemacht wurden, in den eigenen Branch zu übernehmen.

Wenn die Änderungen in beiden Branches sich nicht überlappen, klappt das normalerweise sehr gut.

Wenn in beiden Branches Änderungen an der selben Stelle in einer Datei gemacht worden sind, kann Git nicht wissen, welche Version die “richtige” sein soll.

Das nennt man einen Merge-Konflikt. Man muss als Benutzer selbst den Konflikt auflösen. Sieht man sich dazu nicht in der Lage, kann man mit

$ git merge --abort

den Merge-Vorgang abbrechen. Der Zustand des Repositories ist dann wieder wie vor dem versuchten Merge.

Die Auflösung eines Konflikts läuft so ab:

  • In git status ist zu sehen, welche Dateien von einem Konflikt betroffen sind.

  • Die betroffenen Dateien wurden von Git modifiziert, in dem die Stelle, an der die Datei in beiden Branches seit dem gemeinsamen Vorfahren verändert wurde, markiert ist:

      <<<<<<<
      Inhalt der Datei im aktuellen Branch
      =======
      Inhalt der Datei im anderen Branch
      >>>>>>>
  • Man muss die markierte Stelle durch den Inhalt ersetzen, den man als “gemeinsame Version” erachtet.

  • Mit git add fügt man den neuen Inhalt zum Index hinzu und teilt Git dadurch mit, dass der Konflikt aufgelöst ist.

  • Jetzt sollte man sich mit git status oder git diff --cached vergewissern, dass die Änderungen aus beiden Branches wie gewünscht im Index sind.

  • Mit git commit schließt man den Merge-Vorgang ab.

Hier ist es hilfreich, sich mit gitk das Ergebnis des Merges zu veranschaulichen.

Übung

Die Übungsaufgaben sind hier.

Generell: Versuche, Git in Zukunft sinnvoll einzusetzen. Lege für jedes scheinbar zu kleine “Projekt” ein Repository an und mache einen neuen Commit, wenn sich etwas geändert hat.