English ▾ Themen ▾ Neueste Version ▾ gitcore-tutorial zuletzt aktualisiert in 2.43.1

NAME

gitcore-tutorial - Ein Git-Core-Tutorial für Entwickler

SYNOPSIS

git *

BESCHREIBUNG

Dieses Tutorial erklärt, wie man die "Core" Git-Befehle verwendet, um ein Git-Repository einzurichten und damit zu arbeiten.

Wenn du Git nur als Versionskontrollsystem nutzen möchtest, beginne vielleicht besser mit "A Tutorial Introduction to Git" (gittutorial[7]) oder dem Git-Benutzerhandbuch.

Ein Verständnis dieser Low-Level-Tools kann jedoch hilfreich sein, wenn du die Interna von Git verstehen möchtest.

Das Core Git wird oft als "Plumbing" bezeichnet, mit den schöneren Benutzeroberflächen darüber, die als "Porcelain" bezeichnet werden. Möglicherweise möchtest du das Plumbing nicht sehr oft direkt verwenden, aber es ist gut zu wissen, was die Plumbing-Schicht tut, wenn das Porcelain nicht spült.

Als dieses Dokument ursprünglich geschrieben wurde, waren viele Porcelain-Befehle Shell-Skripte. Der Einfachheit halber werden sie hier noch als Beispiele verwendet, um zu veranschaulichen, wie Plumbing-Elemente zusammengefügt werden, um Porcelain-Befehle zu bilden. Der Quellbaum enthält einige dieser Skripte in contrib/examples/ zur Referenz. Obwohl diese nicht mehr als Shell-Skripte implementiert sind, ist die Beschreibung, was die Befehle der Plumbing-Schicht tun, weiterhin gültig.

Hinweis
Tiefere technische Details sind oft als Hinweise markiert, die du bei deiner ersten Lektüre überspringen kannst.

Ein Git-Repository erstellen

Das Erstellen eines neuen Git-Repositorys könnte nicht einfacher sein: Alle Git-Repositories beginnen leer, und das Einzige, was du tun musst, ist, ein Unterverzeichnis zu finden, das du als Arbeitsverzeichnis (working tree) verwenden möchtest – entweder ein leeres für ein komplett neues Projekt oder ein bestehendes Arbeitsverzeichnis, das du in Git importieren möchtest.

Für unser erstes Beispiel starten wir ein komplett neues Repository von Grund auf, ohne bereits existierende Dateien, und wir nennen es git-tutorial. Zum Starten erstelle ein Unterverzeichnis dafür, wechsle in dieses Unterverzeichnis und initialisiere die Git-Infrastruktur mit git init

$ mkdir git-tutorial
$ cd git-tutorial
$ git init

worauf Git antworten wird

Initialized empty Git repository in .git/

was einfach nur Git's Art zu sagen ist, dass du nichts Seltsames getan hast und dass es ein lokales .git-Verzeichnis für dein neues Projekt eingerichtet hat. Du wirst jetzt ein .git-Verzeichnis haben, und du kannst es mit ls inspizieren. Für dein neues leeres Projekt sollte es dir unter anderem drei Einträge anzeigen

  • eine Datei namens HEAD, die ref: refs/heads/master enthält. Dies ist ähnlich einem symbolischen Link und verweist auf refs/heads/master relativ zur HEAD-Datei.

    Mach dir keine Gedanken darüber, dass die Datei, auf die der HEAD-Link zeigt, noch gar nicht existiert – du hast den Commit noch nicht erstellt, der deinen HEAD-Entwicklungszweig beginnen wird.

  • ein Unterverzeichnis namens objects, das alle Objekte deines Projekts enthalten wird. Du solltest niemals einen wirklichen Grund haben, die Objekte direkt anzusehen, aber du könntest wissen wollen, dass diese Objekte all die wirklichen Daten in deinem Repository enthalten.

  • ein Unterverzeichnis namens refs, das Referenzen auf Objekte enthält.

Insbesondere das Unterverzeichnis refs wird zwei weitere Unterverzeichnisse enthalten, die jeweils heads und tags heißen. Sie tun genau das, was ihre Namen vermuten lassen: Sie enthalten Referenzen auf beliebig viele verschiedene Entwicklungszweige (auch Branches genannt) und auf Tags, die du erstellt hast, um bestimmte Versionen in deinem Repository zu benennen.

Eine Anmerkung: Der spezielle master-Branch ist der Standard-Branch, weshalb die .git/HEAD-Datei erstellt wurde und darauf zeigt, auch wenn er noch nicht existiert. Grundsätzlich soll der HEAD-Link immer auf den Branch zeigen, an dem du gerade arbeitest, und du beginnst immer in der Erwartung, am master-Branch zu arbeiten.

Dies ist jedoch nur eine Konvention, und du kannst deinen Branches beliebige Namen geben und musst nicht einmal einen master-Branch haben. Eine Reihe von Git-Tools wird jedoch davon ausgehen, dass .git/HEAD gültig ist.

Hinweis
Ein Objekt wird durch seinen 160-Bit SHA-1-Hash, auch Objektname genannt, identifiziert, und eine Referenz auf ein Objekt ist immer die 40-stellige Hex-Darstellung dieses SHA-1-Namens. Die Dateien im Unterverzeichnis refs werden erwartet, diese Hex-Referenzen zu enthalten (normalerweise mit einem abschließenden \n am Ende), und du solltest daher erwarten, eine Anzahl von 41-Byte-Dateien zu sehen, die diese Referenzen in diesen refs-Unterverzeichnissen enthalten, wenn du anfängst, deinen Baum zu füllen.
Hinweis
Ein fortgeschrittener Benutzer möchte vielleicht gitrepository-layout[5] nach Abschluss dieses Tutorials durchsehen.

Du hast nun dein erstes Git-Repository erstellt. Da es leer ist, ist das natürlich nicht sehr nützlich, also lass uns anfangen, es mit Daten zu füllen.

Ein Git-Repository füllen

Wir halten es einfach und dumm, also füllen wir es zunächst mit ein paar trivialen Dateien, nur um ein Gefühl dafür zu bekommen.

Beginne damit, beliebige zufällige Dateien zu erstellen, die du in deinem Git-Repository verwalten möchtest. Wir fangen mit ein paar schlechten Beispielen an, nur um ein Gefühl dafür zu bekommen, wie das funktioniert

$ echo "Hello World" >hello
$ echo "Silly example" >example

du hast jetzt zwei Dateien in deinem Arbeitsverzeichnis (aka working directory) erstellt, aber um deine harte Arbeit tatsächlich einzuchecken, musst du zwei Schritte durchlaufen

  • fülle die Index-Datei (aka Cache) mit den Informationen über den Zustand deines Arbeitsverzeichnisses.

  • committe diese Index-Datei als Objekt.

Der erste Schritt ist trivial: Wenn du Git über Änderungen in deinem Arbeitsverzeichnis informieren möchtest, verwendest du das Programm git update-index. Dieses Programm nimmt normalerweise nur eine Liste von Dateinamen entgegen, die du aktualisieren möchtest, aber um triviale Fehler zu vermeiden, weigert es sich, neue Einträge zum Index hinzuzufügen (oder bestehende zu entfernen), es sei denn, du sagst ihm ausdrücklich, dass du einen neuen Eintrag mit dem Flag --add hinzufügst (oder einen Eintrag mit dem Flag --remove entfernst).

Um den Index also mit den beiden gerade erstellten Dateien zu füllen, kannst du Folgendes tun

$ git update-index --add hello example

und du hast Git jetzt mitgeteilt, diese beiden Dateien zu verfolgen.

Tatsächlich, als du das getan hast, wenn du jetzt in dein Objektverzeichnis schaust, wirst du feststellen, dass Git zwei neue Objekte zur Objektdatenbank hinzugefügt hat. Wenn du genau die obigen Schritte ausgeführt hast, solltest du jetzt Folgendes tun können

$ ls .git/objects/??/*

und zwei Dateien sehen

.git/objects/55/7db03de997c86a4a028e1ebd3a1ceb225be238
.git/objects/f2/4c74a2e500f5ee1332c86b94199f52b1d1d962

die den Objekten mit den Namen 557db... bzw. f24c7... entsprechen.

Wenn du möchtest, kannst du git cat-file verwenden, um diese Objekte anzusehen, aber du musst den Objektnamen verwenden, nicht den Dateinamen des Objekts

$ git cat-file -t 557db03de997c86a4a028e1ebd3a1ceb225be238

wobei -t git cat-file anweist, dir den "Typ" des Objekts mitzuteilen. Git wird dir sagen, dass du ein "blob"-Objekt hast (d.h. nur eine normale Datei), und du kannst den Inhalt mit

$ git cat-file blob 557db03

sehen, was "Hello World" ausgibt. Das Objekt 557db03 ist nichts anderes als der Inhalt deiner Datei hello.

Hinweis
Verwechsle dieses Objekt nicht mit der Datei hello selbst. Das Objekt sind buchstäblich nur diese spezifischen Inhalte der Datei, und egal wie sehr du später die Inhalte in der Datei hello änderst, das Objekt, das wir gerade betrachtet haben, wird sich nie ändern. Objekte sind unveränderlich.
Hinweis
Das zweite Beispiel zeigt, dass du den Objektnamen an den meisten Stellen auf die ersten paar Hexadezimalziffern abkürzen kannst.

Wie auch immer, wie wir bereits erwähnt haben, siehst du normalerweise nie die Objekte selbst an, und das Tippen langer 40-stelliger Hex-Namen ist nichts, was du normalerweise tun möchtest. Die obige Abschweifung diente nur dazu zu zeigen, dass git update-index etwas Magisches getan hat und tatsächlich den Inhalt deiner Dateien in der Git-Objektdatenbank gespeichert hat.

Das Aktualisieren des Index hat auch noch etwas anderes bewirkt: Es hat eine Datei .git/index erstellt. Dies ist der Index, der dein aktuelles Arbeitsverzeichnis beschreibt, und etwas, dessen du dir sehr bewusst sein solltest. Auch hier beschäftigst du dich normalerweise nie mit der Indexdatei selbst, aber du solltest dir bewusst sein, dass du deine Dateien bisher noch nicht wirklich in Git "eingecheckt" hast, du hast Git nur von ihnen erzählt.

Da Git sie jedoch kennt, kannst du jetzt einige der grundlegendsten Git-Befehle verwenden, um die Dateien zu manipulieren oder ihren Status anzuzeigen.

Lass uns insbesondere die beiden Dateien noch nicht in Git einchecken, sondern wir fügen zuerst eine weitere Zeile zu hello hinzu

$ echo "It's a new day for git" >>hello

und du kannst jetzt, da du Git über den vorherigen Zustand von hello informiert hast, Git fragen, was sich im Baum im Vergleich zu deinem alten Index geändert hat, indem du den Befehl git diff-files verwendest

$ git diff-files

Ups. Das war nicht sehr lesbar. Es hat einfach seine eigene interne Version eines diff ausgegeben, aber diese interne Version sagt dir nur, dass es bemerkt hat, dass "hello" geändert wurde und dass die alten Objektinhalte durch etwas anderes ersetzt wurden.

Um es lesbar zu machen, können wir git diff-files anweisen, die Unterschiede als Patch auszugeben, indem wir das Flag -p verwenden

$ git diff-files -p
diff --git a/hello b/hello
index 557db03..263414f 100644
--- a/hello
+++ b/hello
@@ -1 +1,2 @@
 Hello World
+It's a new day for git

d.h. der Diff der Änderung, die wir durch Hinzufügen einer weiteren Zeile zu hello verursacht haben.

Mit anderen Worten, git diff-files zeigt uns immer den Unterschied zwischen dem, was im Index aufgezeichnet ist, und dem, was sich derzeit im Arbeitsverzeichnis befindet. Das ist sehr nützlich.

Eine gebräuchliche Abkürzung für git diff-files -p ist einfach git diff zu schreiben, was dasselbe tut.

$ git diff
diff --git a/hello b/hello
index 557db03..263414f 100644
--- a/hello
+++ b/hello
@@ -1 +1,2 @@
 Hello World
+It's a new day for git

Git-Zustand committen

Nun wollen wir zur nächsten Stufe in Git übergehen, nämlich die Dateien, die Git im Index kennt, zu nehmen und sie als echten Baum zu committen. Dies tun wir in zwei Phasen: Erstellen eines Tree-Objekts und Committen dieses Tree-Objekts als Commit-Objekt zusammen mit einer Erklärung, worum es in diesem Baum ging, sowie Informationen darüber, wie wir zu diesem Zustand gekommen sind.

Das Erstellen eines Baumobjekts ist trivial und wird mit git write-tree durchgeführt. Es gibt keine Optionen oder andere Eingaben: git write-tree nimmt den aktuellen Indexzustand und schreibt ein Objekt, das diesen gesamten Index beschreibt. Mit anderen Worten, wir binden nun alle verschiedenen Dateinamen mit ihren Inhalten (und ihren Berechtigungen) zusammen und erstellen das Äquivalent eines Git-"Verzeichnis"-Objekts

$ git write-tree

und dies gibt einfach den Namen des resultierenden Baums aus, in diesem Fall (wenn du genau wie ich beschrieben hast) sollte es sein

8988da15d077d4829fc51d8544c097def6644dbb

was ein weiterer unverständlicher Objektname ist. Auch hier kannst du, wenn du möchtest, git cat-file -t 8988d... verwenden, um zu sehen, dass diesmal das Objekt kein "blob"-Objekt ist, sondern ein "tree"-Objekt (du kannst auch git cat-file verwenden, um den rohen Objektinhalt tatsächlich auszugeben, aber du siehst hauptsächlich eine binäre Unordnung, also ist das weniger interessant).

Normalerweise würdest du jedoch niemals git write-tree allein verwenden, da du normalerweise immer einen Baum mit dem Befehl git commit-tree in ein Commit-Objekt committest. Tatsächlich ist es einfacher, git write-tree gar nicht allein zu verwenden, sondern sein Ergebnis einfach als Argument an git commit-tree zu übergeben.

git commit-tree nimmt normalerweise mehrere Argumente entgegen – es muss wissen, was der Elternteil eines Commits war, aber da dies der erste Commit in diesem neuen Repository ist und er keine Eltern hat, müssen wir nur den Objektnamen des Baums übergeben. git commit-tree möchte jedoch auch eine Commit-Nachricht über seine Standardeingabe erhalten und schreibt den resultierenden Objektnamen für den Commit auf seine Standardausgabe.

Und hier erstellen wir die Datei .git/refs/heads/master, auf die von HEAD verwiesen wird. Diese Datei soll die Referenz auf die Spitze des Master-Branches enthalten, und da dies genau das ist, was git commit-tree ausgibt, können wir dies mit einer Abfolge von einfachen Shell-Befehlen tun

$ tree=$(git write-tree)
$ commit=$(echo 'Initial commit' | git commit-tree $tree)
$ git update-ref HEAD $commit

In diesem Fall wird ein komplett neuer Commit erstellt, der mit nichts anderem verbunden ist. Normalerweise macht man das nur einmal für ein Projekt überhaupt, und alle späteren Commits werden auf einen früheren Commit aufbauen.

Normalerweise würdest du dies auch nie von Hand tun. Es gibt ein hilfreiches Skript namens git commit, das all das für dich erledigt. Du hättest also einfach git commit schreiben können, und es hätte die obige magische Skripterstellung für dich durchgeführt.

Eine Änderung vornehmen

Erinnerst du dich, wie wir git update-index für die Datei hello ausgeführt und dann hello danach geändert haben und den neuen Zustand von hello mit dem im Index gespeicherten Zustand vergleichen konnten?

Erinnerst du dich weiter, wie ich sagte, dass git write-tree den Inhalt der Index-Datei in den Baum schreibt und damit das, was wir gerade committed haben, tatsächlich der ursprüngliche Inhalt der Datei hello war, nicht die neuen. Wir haben das absichtlich getan, um den Unterschied zwischen dem Indexzustand und dem Zustand im Arbeitsverzeichnis zu zeigen und wie sie nicht übereinstimmen müssen, selbst wenn wir Dinge committen.

Wie zuvor, wenn wir git diff-files -p in unserem git-tutorial-Projekt ausführen, sehen wir immer noch denselben Unterschied, den wir letztes Mal gesehen haben: Die Indexdatei hat sich durch das Committen von nichts geändert. Nachdem wir jedoch etwas committet haben, können wir auch ein neues Kommando lernen: git diff-index.

Im Gegensatz zu git diff-files, das den Unterschied zwischen der Indexdatei und dem Arbeitsverzeichnis anzeigte, zeigt git diff-index die Unterschiede zwischen einem committeten Baum und entweder der Indexdatei oder dem Arbeitsverzeichnis an. Mit anderen Worten, git diff-index benötigt einen Baum, gegen den verglichen werden soll, und bevor wir den Commit durchgeführt haben, konnten wir das nicht tun, weil wir nichts zum Vergleichen hatten.

Aber jetzt können wir tun

$ git diff-index -p HEAD

(wobei -p dieselbe Bedeutung hat wie bei git diff-files), und es wird uns denselben Unterschied zeigen, aber aus einem völlig anderen Grund. Jetzt vergleichen wir das Arbeitsverzeichnis nicht mit der Indexdatei, sondern mit dem Baum, den wir gerade geschrieben haben. Es ist offensichtlich, dass diese beiden identisch sind, also erhalten wir dasselbe Ergebnis.

Auch hier kann dies, da es eine häufige Operation ist, mit einer Abkürzung geschrieben werden mit

$ git diff HEAD

was am Ende das obige für dich tut.

Mit anderen Worten, git diff-index vergleicht normalerweise einen Baum mit dem Arbeitsverzeichnis, aber wenn ihm das Flag --cached gegeben wird, wird angewiesen, stattdessen nur mit den Index-Cache-Inhalten zu vergleichen und den aktuellen Arbeitsverzeichnis-Zustand vollständig zu ignorieren. Da wir gerade die Indexdatei in HEAD geschrieben haben, sollte die Ausführung von git diff-index --cached -p HEAD somit ein leeres Diff-Ergebnis liefern, und genau das tut es.

Hinweis

git diff-index verwendet wirklich immer den Index für seine Vergleiche, und die Aussage, dass er einen Baum mit dem Arbeitsverzeichnis vergleicht, ist daher nicht ganz korrekt. Insbesondere die Liste der zu vergleichenden Dateien (die "Metadaten") stammt immer aus der Indexdatei, unabhängig davon, ob das Flag --cached verwendet wird oder nicht. Das Flag --cached bestimmt wirklich nur, ob die zu vergleichenden Dateiinhalte aus dem Arbeitsverzeichnis stammen oder nicht.

Das ist nicht schwer zu verstehen, sobald man erkennt, dass Git einfach nie von Dateien weiß (oder sich darum kümmert), von denen ihm nicht ausdrücklich erzählt wurde. Git wird niemals nach Dateien suchen, um sie zu vergleichen, es erwartet, dass du ihm sagst, welche Dateien es sind, und dafür ist der Index da.

Unser nächster Schritt ist jedoch, die Änderung zu committen, die wir vorgenommen haben. Um zu verstehen, was passiert, behalte den Unterschied zwischen "Arbeitsverzeichnis-Inhalt", "Indexdatei" und "committetem Baum" im Hinterkopf. Wir haben Änderungen im Arbeitsverzeichnis, die wir committen wollen, und wir müssen immer über die Indexdatei arbeiten. Das erste, was wir tun müssen, ist also, den Index-Cache zu aktualisieren

$ git update-index hello

(beachte, wie wir diesmal das Flag --add nicht benötigt haben, da Git die Datei bereits kannte).

Beachte, was mit den verschiedenen git diff-* Versionen hier passiert. Nachdem wir hello im Index aktualisiert haben, zeigt git diff-files -p jetzt keine Unterschiede mehr an, aber git diff-index -p HEAD zeigt immer noch an, dass sich der aktuelle Zustand von dem Zustand, den wir committet haben, unterscheidet. Tatsächlich zeigt git diff-index nun denselben Unterschied an, unabhängig davon, ob wir das Flag --cached verwenden oder nicht, da der Index nun kohärent mit dem Arbeitsverzeichnis ist.

Da wir hello im Index aktualisiert haben, können wir die neue Version committen. Wir könnten dies tun, indem wir den Baum erneut von Hand schreiben und den Baum committen (diesmal müssten wir das Flag -p HEAD verwenden, um Commit mitzuteilen, dass HEAD der Elternteil des neuen Commits war und dass dies kein initialer Commit mehr war), aber das hast du bereits einmal gemacht, also lass uns diesmal einfach das hilfreiche Skript verwenden

$ git commit

was einen Editor startet, in dem du die Commit-Nachricht schreiben kannst, und dir einiges darüber erzählt, was du getan hast.

Schreibe die gewünschte Nachricht. Alle Zeilen, die mit # beginnen, werden entfernt, und der Rest wird als Commit-Nachricht für die Änderung verwendet. Wenn du dich entscheidest, zum jetzigen Zeitpunkt doch nichts zu committen (du kannst weiterhin Dinge bearbeiten und den Index aktualisieren), kannst du einfach eine leere Nachricht hinterlassen. Andernfalls wird git commit die Änderung für dich committen.

Du hast nun deinen ersten echten Git-Commit erstellt. Und wenn du daran interessiert bist, was git commit wirklich tut, kannst du es gerne untersuchen: Es sind einige sehr einfache Shell-Skripte, um die hilfreichen (?) Commit-Nachrichten-Header zu generieren, und einige Einzeiler, die den Commit tatsächlich durchführen (git commit).

Änderungen inspizieren

Während das Erstellen von Änderungen nützlich ist, ist es noch nützlicher, wenn du später sagen kannst, was sich geändert hat. Das nützlichste Werkzeug dafür ist ein weiteres der diff-Familie, nämlich git diff-tree.

git diff-tree kann zwei beliebige Bäume erhalten, und es wird dir die Unterschiede zwischen ihnen mitteilen. Vielleicht noch häufiger kann man ihm jedoch nur ein einzelnes Commit-Objekt geben, und es ermittelt selbst den Elternteil dieses Commits und zeigt den Unterschied direkt an. Um also denselben Diff zu erhalten, den wir bereits mehrmals gesehen haben, können wir jetzt tun

$ git diff-tree -p HEAD

(wieder bedeutet -p, den Unterschied als menschenlesbaren Patch anzuzeigen), und es wird zeigen, was der letzte Commit (in HEAD) tatsächlich geändert hat.

Hinweis

Hier ist eine ASCII-Grafik von Jon Loeliger, die veranschaulicht, wie verschiedene diff-*-Befehle Dinge vergleichen.

            diff-tree
             +----+
             |    |
             |    |
             V    V
          +-----------+
          | Object DB |
          |  Backing  |
          |   Store   |
          +-----------+
            ^    ^
            |    |
            |    |  diff-index --cached
            |    |
diff-index  |    V
            |  +-----------+
            |  |   Index   |
            |  |  "cache"  |
            |  +-----------+
            |    ^
            |    |
            |    |  diff-files
            |    |
            V    V
          +-----------+
          |  Working  |
          | Directory |
          +-----------+

Interessanter ist, dass du git diff-tree auch das Flag --pretty geben kannst, was ihm anweist, auch die Commit-Nachricht, den Autor und das Datum des Commits anzuzeigen, und du kannst ihm sagen, "silent" zu sein und die Diffs gar nicht anzuzeigen, sondern nur die tatsächliche Commit-Nachricht. Alternativ kannst du ihm sagen, "silent" zu sein und die Diffs gar nicht anzuzeigen, sondern nur die tatsächliche Commit-Nachricht.

Tatsächlich wird git diff-tree zusammen mit dem Programm git rev-list (das eine Liste von Revisionen generiert) zu einer wahren Quelle von Änderungen. Du kannst git log, git log -p usw. mit einem einfachen Skript emulieren, das die Ausgabe von git rev-list an git diff-tree --stdin weiterleitet, was genau die Art und Weise ist, wie frühe Versionen von git log implementiert waren.

Eine Version taggen

In Git gibt es zwei Arten von Tags: einen "leichten" und einen "kommentierten Tag".

Ein "leichter" Tag ist technisch nichts anderes als ein Branch, nur dass wir ihn im Unterverzeichnis .git/refs/tags/ platzieren, anstatt ihn einen head zu nennen. Die einfachste Form eines Tags besteht daher nur aus

$ git tag my-first-tag

was einfach nur das aktuelle HEAD in die Datei .git/refs/tags/my-first-tag schreibt, danach kannst du diesen symbolischen Namen für diesen bestimmten Zustand verwenden. Du kannst zum Beispiel tun

$ git diff my-first-tag

um deinen aktuellen Zustand gegen diesen Tag zu diffen, was zu diesem Zeitpunkt offensichtlich ein leerer Diff sein wird, aber wenn du weiter entwickelst und Dinge committest, kannst du deinen Tag als "Ankerpunkt" verwenden, um zu sehen, was sich geändert hat, seit du ihn getaggt hast.

Ein "kommentierter Tag" ist tatsächlich ein echtes Git-Objekt und enthält nicht nur einen Verweis auf den zu taggenden Zustand, sondern auch einen kleinen Tag-Namen und eine Nachricht sowie optional eine PGP-Signatur, die besagt, dass du diesen Tag tatsächlich erstellt hast. Du erstellst diese kommentierten Tags entweder mit dem Flag -a oder -s für git tag

$ git tag -s <tagname>

was das aktuelle HEAD signieren wird (aber du kannst ihm auch ein weiteres Argument übergeben, das angibt, was getaggt werden soll, z. B. hättest du den aktuellen Punkt von mybranch taggen können, indem du git tag <tagname> mybranch verwendest).

Signierte Tags verwendest du normalerweise nur für Hauptveröffentlichungen oder ähnliches, während leichte Tags für jede Markierung nützlich sind, die du vornehmen möchtest – wann immer du entscheidest, dass du einen bestimmten Punkt festhalten möchtest, erstelle einfach einen privaten Tag dafür, und du hast einen schönen symbolischen Namen für den Zustand an diesem Punkt.

Repositories kopieren

Git-Repositories sind normalerweise vollständig eigenständig und verlagerbar. Im Gegensatz zu CVS zum Beispiel gibt es keine separate Vorstellung von "Repository" und "Arbeitsverzeichnis". Ein Git-Repository ist normalerweise das Arbeitsverzeichnis, mit den lokalen Git-Informationen, die im Unterverzeichnis .git versteckt sind. Sonst nichts. Was du siehst, ist das, was du hast.

Hinweis
Du kannst Git anweisen, die Git-internen Informationen vom Verzeichnis zu trennen, das es verfolgt, aber das ignorieren wir vorerst: Das ist nicht, wie normale Projekte funktionieren, und es ist wirklich nur für spezielle Zwecke gedacht. Das mentale Modell "die Git-Informationen sind immer direkt mit dem Arbeitsverzeichnis verbunden, das sie beschreiben" ist also möglicherweise nicht technisch zu 100 % korrekt, aber es ist ein gutes Modell für alle normalen Anwendungen.

Dies hat zwei Implikationen

  • Wenn du dich des von dir erstellten Tutorial-Repositorys langweilst (oder einen Fehler gemacht hast und von vorne beginnen möchtest), kannst du einfach

    $ rm -rf git-tutorial

    tun und es wird weg sein. Es gibt kein externes Repository, und es gibt keine Historie außerhalb des von dir erstellten Projekts.

  • Wenn du ein Git-Repository verschieben oder duplizieren möchtest, kannst du das tun. Es gibt den Befehl git clone, aber wenn du nur eine Kopie deines Repositorys erstellen möchtest (mit der vollständigen Historie, die damit einherging), kannst du dies mit einem regulären cp -a git-tutorial new-git-tutorial tun.

    Beachte, dass nach dem Verschieben oder Kopieren eines Git-Repositorys deine Git-Indexdatei (die verschiedene Informationen zwischenspeichert, insbesondere einige "stat"-Informationen für die beteiligten Dateien) wahrscheinlich aktualisiert werden muss. Nach einem cp -a zur Erstellung einer neuen Kopie möchtest du also Folgendes tun

    $ git update-index --refresh

    im neuen Repository, um sicherzustellen, dass die Indexdatei auf dem neuesten Stand ist.

Beachte, dass der zweite Punkt auch auf verschiedenen Maschinen gilt. Du kannst ein entferntes Git-Repository mit jedem regulären Kopierwerkzeug duplizieren, sei es scp, rsync oder wget.

Beim Kopieren eines entfernten Repositorys möchtest du zumindest den Index-Cache aktualisieren, und insbesondere bei Repositorys anderer Leute möchtest du oft sicherstellen, dass der Index-Cache in einem bekannten Zustand ist (du weißt nicht, was sie getan und noch nicht eingecheckt haben), also wirst du normalerweise der git update-index ein

$ git read-tree --reset HEAD
$ git update-index --refresh

voranstellen, das einen vollständigen Neuaufbau des Index aus dem von HEAD referenzierten Baum erzwingt. Es setzt den Inhalt des Index auf HEAD zurück, und dann sorgt git update-index dafür, dass alle Indexeinträge mit den ausgecheckten Dateien übereinstimmen. Wenn das ursprüngliche Repository uncommitted Änderungen in seinem Arbeitsverzeichnis hatte, bemerkt git update-index --refresh diese und teilt dir mit, dass sie aktualisiert werden müssen.

Das obige kann auch einfach geschrieben werden als

$ git reset

und tatsächlich können viele der gängigen Git-Befehlskombinationen mit den git xyz-Schnittstellen skriptet werden. Du kannst Dinge lernen, indem du einfach nachsiehst, was die verschiedenen Git-Skripte tun. Zum Beispiel war git reset früher die oben genannten zwei Zeilen, implementiert in git reset, aber Dinge wie git status und git commit sind etwas komplexere Skripte rund um die grundlegenden Git-Befehle.

Viele (die meisten?) öffentlichen entfernten Repositorys enthalten keine der ausgecheckten Dateien oder sogar eine Indexdatei und enthalten nur die tatsächlichen Git-Kern-Dateien. Ein solches Repository hat normalerweise nicht einmal das Unterverzeichnis .git, sondern hat alle Git-Dateien direkt im Repository.

Um deine eigene lokale Live-Kopie eines solchen "rohen" Git-Repositorys zu erstellen, würdest du zuerst dein eigenes Unterverzeichnis für das Projekt erstellen und dann den rohen Repository-Inhalt in das Verzeichnis .git kopieren. Zum Beispiel, um deine eigene Kopie des Git-Repositorys zu erstellen, würdest du Folgendes tun

$ mkdir my-git
$ cd my-git
$ rsync -rL rsync://rsync.kernel.org/pub/scm/git/git.git/ .git

gefolgt von

$ git read-tree HEAD

um den Index zu füllen. Du hast den Index nun gefüllt und alle internen Git-Dateien, aber du wirst feststellen, dass du tatsächlich keine der Arbeitsverzeichnisdateien zum Bearbeiten hast. Um diese zu erhalten, würdest du sie mit

$ git checkout-index -u -a

auschecken, wobei das Flag -u bedeutet, dass du möchtest, dass der Checkout den Index auf dem neuesten Stand hält (damit du ihn danach nicht aktualisieren musst), und das Flag -a bedeutet "alle Dateien auschecken" (wenn du eine veraltete Kopie oder eine ältere Version eines ausgecheckten Baums hast, benötigst du möglicherweise auch zuerst das Flag -f, um git checkout-index anzuweisen, alte Dateien erzwungen zu überschreiben).

Auch das kann vereinfacht werden mit

$ git clone git://git.kernel.org/pub/scm/git/git.git/ my-git
$ cd my-git
$ git checkout

was am Ende all das für dich erledigt.

Du hast nun erfolgreich die entfernte Repository von jemand anderem (meinem) kopiert und ausgecheckt.

Einen neuen Branch erstellen

Branches in Git sind eigentlich nichts anderes als Zeiger in die Git-Objektdatenbank innerhalb des Unterverzeichnisses .git/refs/, und wie wir bereits besprochen haben, ist der HEAD-Branch nichts weiter als ein Symlink zu einem dieser Objektzeiger.

Du kannst jederzeit einen neuen Branch erstellen, indem du einfach einen beliebigen Punkt in der Projektgeschichte auswählst und den SHA-1-Namen dieses Objekts in eine Datei unter .git/refs/heads/ schreibst. Du kannst jeden beliebigen Dateinamen verwenden (und tatsächlich auch Unterverzeichnisse), aber die Konvention ist, dass der "normale" Branch master genannt wird. Das ist jedoch nur eine Konvention, und nichts erzwingt sie.

Um dies als Beispiel zu zeigen, gehen wir zurück zum git-tutorial-Repository, das wir zuvor verwendet haben, und erstellen dort einen Branch. Dies tust du einfach, indem du sagst, dass du einen neuen Branch auschecken möchtest

$ git switch -c mybranch

erstellt einen neuen Branch, der an der aktuellen HEAD-Position basiert, und wechselt dorthin.

Hinweis

Wenn Sie sich entscheiden, Ihren neuen Zweig an einem anderen Punkt in der Historie als dem aktuellen HEAD zu beginnen, können Sie dies tun, indem Sie git switch einfach mitteilen, was die Basis des Checkouts wäre. Mit anderen Worten, wenn Sie einen früheren Tag oder Zweig haben, würden Sie einfach

$ git switch -c mybranch earlier-commit

und es würde den neuen Zweig mybranch am früheren Commit erstellen und den Zustand zu diesem Zeitpunkt auschecken.

Sie können jederzeit einfach zu Ihrem ursprünglichen master-Zweig zurückspringen, indem Sie

$ git switch master

(oder jeden anderen Zweignamen, was das betrifft) und wenn Sie vergessen haben, auf welchem Zweig Sie sich gerade befinden, eine einfache

$ cat .git/HEAD

wird Ihnen sagen, wohin es zeigt. Um die Liste der Zweige zu erhalten, die Sie haben, können Sie sagen

$ git branch

was früher nichts weiter war als ein einfaches Skript um ls .git/refs/heads. Vor dem Zweig, auf dem Sie sich gerade befinden, steht ein Sternchen.

Manchmal möchten Sie vielleicht einen neuen Zweig erstellen, ohne ihn tatsächlich auszuchecken und zu ihm zu wechseln. Wenn ja, verwenden Sie einfach den Befehl

$ git branch <branchname> [startingpoint]

was einfach den Zweig erstellt, aber nichts weiter tut. Sie können dann später — sobald Sie entscheiden, dass Sie tatsächlich an diesem Zweig entwickeln möchten — mit einem regulären git switch mit dem Zweig Namen als Argument zu diesem Zweig wechseln.

Zwei Zweige zusammenführen

Eine der Ideen, einen Zweig zu haben, ist, dass Sie darin (möglicherweise experimentelle) Arbeit erledigen und sie schließlich wieder in den Hauptzweig zusammenführen. Angenommen, Sie haben den oben genannten mybranch erstellt, der am Anfang identisch mit dem ursprünglichen master-Zweig war. Stellen wir sicher, dass wir uns auf diesem Zweig befinden und dort etwas Arbeit erledigen.

$ git switch mybranch
$ echo "Work, work, work" >>hello
$ git commit -m "Some work." -i hello

Hier haben wir gerade eine weitere Zeile zu hello hinzugefügt und eine Kurzform für die Ausführung von sowohl git update-index hello als auch git commit verwendet, indem wir den Dateinamen direkt an git commit übergeben, mit der Option -i (sie weist Git an, diese Datei zusätzlich zu dem, was Sie bisher im Index getan haben, in den Commit aufzunehmen). Die Option -m dient dazu, die Commit-Log-Nachricht von der Befehlszeile aus zu übergeben.

Um es nun etwas interessanter zu gestalten, nehmen wir an, dass jemand anderes etwas Arbeit im ursprünglichen Zweig leistet, und simulieren dies, indem wir zum master-Zweig zurückkehren und dieselbe Datei dort anders bearbeiten

$ git switch master

Nehmen Sie sich hier einen Moment Zeit, um den Inhalt von hello zu betrachten und zu bemerken, dass er nicht die Arbeit enthält, die wir gerade in mybranch geleistet haben — denn diese Arbeit hat im master-Zweig überhaupt nicht stattgefunden. Führen Sie dann

$ echo "Play, play, play" >>hello
$ echo "Lots of fun" >>example
$ git commit -m "Some fun." -i hello example

da der master-Zweig offensichtlich in einer viel besseren Stimmung ist.

Nun haben Sie zwei Zweige und entscheiden, dass Sie die geleistete Arbeit zusammenführen möchten. Bevor wir das tun, führen wir ein cooles grafisches Werkzeug ein, das Ihnen hilft zu sehen, was vor sich geht

$ gitk --all

zeigt Ihnen grafisch beide Ihrer Zweige (dafür steht --all: normalerweise zeigt es nur Ihren aktuellen HEAD) und ihre Historien. Sie können auch genau sehen, wie sie aus einer gemeinsamen Quelle entstanden sind.

Wie auch immer, beenden wir gitk (^Q oder das Menü Datei) und entscheiden, dass wir die Arbeit, die wir im mybranch-Zweig geleistet haben, in den master-Zweig zusammenführen möchten (der derzeit auch unser HEAD ist). Dazu gibt es ein schönes Skript namens git merge, das wissen möchte, welche Zweige Sie auflösen möchten und worum es bei der Zusammenführung geht

$ git merge -m "Merge work in mybranch" mybranch

wobei das erste Argument als Commit-Nachricht verwendet wird, wenn die Zusammenführung automatisch aufgelöst werden kann.

In diesem Fall haben wir absichtlich eine Situation geschaffen, in der die Zusammenführung von Hand behoben werden muss. Git wird also so viel wie möglich automatisch tun (was in diesem Fall nur das Zusammenführen der example-Datei ist, die im mybranch-Zweig keine Unterschiede hatte) und sagen

	Auto-merging hello
	CONFLICT (content): Merge conflict in hello
	Automatic merge failed; fix conflicts and then commit the result.

Es sagt Ihnen, dass es eine "automatische Zusammenführung" durchgeführt hat, die aufgrund von Konflikten in hello fehlgeschlagen ist.

Keine Sorge. Es hat den (trivialen) Konflikt in hello in der gleichen Form hinterlassen, wie Sie es wahrscheinlich kennen, wenn Sie jemals CVS verwendet haben. Öffnen wir also hello in unserem Editor (was auch immer das sein mag) und beheben Sie es irgendwie. Ich würde vorschlagen, es einfach so zu machen, dass hello alle vier Zeilen enthält

Hello World
It's a new day for git
Play, play, play
Work, work, work

und sobald Sie mit Ihrer manuellen Zusammenführung zufrieden sind, führen Sie einfach ein

$ git commit -i hello

was sehr laut davor warnt, dass Sie jetzt eine Zusammenführung committen (was korrekt ist, also machen Sie sich keine Sorgen), und Sie können eine kleine Zusammenführungsnachricht über Ihre Abenteuer im git merge-Land schreiben.

Wenn Sie fertig sind, starten Sie gitk --all, um grafisch zu sehen, wie die Historie aussieht. Beachten Sie, dass mybranch immer noch existiert und Sie dorthin wechseln und weiter damit arbeiten können, wenn Sie möchten. Der mybranch-Zweig wird die Zusammenführung nicht enthalten, aber wenn Sie ihn das nächste Mal vom master-Zweig aus zusammenführen, wird Git wissen, wie Sie ihn zusammengeführt haben, sodass Sie diese Zusammenführung nicht erneut durchführen müssen.

Ein weiteres nützliches Werkzeug, besonders wenn Sie nicht immer in einer X-Window-Umgebung arbeiten, ist git show-branch.

$ git show-branch --topo-order --more=1 master mybranch
* [master] Merge work in mybranch
 ! [mybranch] Some work.
--
-  [master] Merge work in mybranch
*+ [mybranch] Some work.
*  [master^] Some fun.

Die ersten beiden Zeilen zeigen an, dass sie die beiden Zweige mit den Titeln ihrer Top-of-the-tree-Commits anzeigt, Sie befinden sich gerade auf dem master-Zweig (beachten Sie das Sternchen *), und die erste Spalte für die späteren Ausgabezeilen wird verwendet, um Commits zu zeigen, die im master-Zweig enthalten sind, und die zweite Spalte für den mybranch-Zweig. Drei Commits werden zusammen mit ihren Titeln angezeigt. Alle haben in der ersten Spalte nicht leere Zeichen (* zeigt einen gewöhnlichen Commit im aktuellen Zweig, - ist ein Merge-Commit), was bedeutet, dass sie jetzt Teil des master-Zweigs sind. Nur der "Some work"-Commit hat das Pluszeichen + in der zweiten Spalte, da mybranch nicht zusammengeführt wurde, um diese Commits aus dem Master-Zweig zu übernehmen. Die Zeichenkette in Klammern vor der Commit-Log-Nachricht ist ein kurzer Name, mit dem Sie den Commit benennen können. Im obigen Beispiel sind master und mybranch Branch-Heads. master^ ist das erste Elternteil des master-Branch-Heads. Bitte siehe gitrevisions[7], wenn Sie komplexere Fälle sehen möchten.

Hinweis
Ohne die Option --more=1 würde git show-branch den Commit [master^] nicht ausgeben, da der Commit [mybranch] ein gemeinsamer Vorfahre beider master und mybranch Tipps ist. Bitte siehe git-show-branch[1] für Details.
Hinweis
Wenn es nach der Zusammenführung weitere Commits im master-Zweig gäbe, würde der Merge-Commit selbst von git show-branch standardmäßig nicht angezeigt werden. Sie müssten die Option --sparse angeben, um den Merge-Commit in diesem Fall sichtbar zu machen.

Nehmen wir nun an, Sie sind derjenige, der die gesamte Arbeit in mybranch geleistet hat, und die Frucht Ihrer harten Arbeit wurde endlich in den master-Zweig zusammengeführt. Gehen wir zurück zu mybranch und führen Sie git merge aus, um die "Upstream-Änderungen" zurück in Ihren Zweig zu bekommen.

$ git switch mybranch
$ git merge -m "Merge upstream changes." master

Dies gibt etwas Ähnliches aus wie (die tatsächlichen Commit-Objektnamen wären anders)

Updating from ae3a2da... to a80b4aa....
Fast-forward (no commit created; -m option ignored)
 example | 1 +
 hello   | 1 +
 2 files changed, 2 insertions(+)

Da Ihr Zweig nichts mehr enthielt als das, was bereits in den master-Zweig zusammengeführt worden war, hat die Zusammenführungsoperation tatsächlich keine Zusammenführung durchgeführt. Stattdessen hat sie lediglich die Spitze des Baumes Ihres Zweigs auf die des master-Zweigs aktualisiert. Dies wird oft als Fast-Forward-Merge bezeichnet.

Sie können gitk --all erneut ausführen, um zu sehen, wie die Commit-Abstammung aussieht, oder show-branch ausführen, was Ihnen dies mitteilt.

$ git show-branch master mybranch
! [master] Merge work in mybranch
 * [mybranch] Merge work in mybranch
--
-- [master] Merge work in mybranch

Zusammenführen externer Arbeit

Es ist normalerweise viel üblicher, dass Sie mit jemand anderem zusammenführen als mit Ihren eigenen Zweigen. Daher lohnt es sich hervorzuheben, dass Git dies auch sehr einfach macht, und tatsächlich ist es nicht viel anders als ein git merge. Tatsächlich läuft eine Remote-Zusammenführung auf nichts anderes hinaus als darauf, "die Arbeit aus einem Remote-Repository in einen temporären Tag zu holen" gefolgt von einem git merge.

Das Abrufen aus einem Remote-Repository erfolgt, wie könnte es anders sein, mit git fetch

$ git fetch <remote-repository>

Einer der folgenden Transporte kann verwendet werden, um das Repository zu benennen, von dem heruntergeladen werden soll

SSH

remote.machine:/path/to/repo.git/ oder

ssh://remote.machine/path/to/repo.git/

Dieser Transport kann sowohl für das Hochladen als auch für das Herunterladen verwendet werden und erfordert, dass Sie über Anmeldeberechtigungen für ssh auf dem Remote-Computer verfügen. Er ermittelt den Satz von Objekten, den die andere Seite nicht hat, indem die Head-Commits ausgetauscht werden, die beide Seiten haben, und überträgt (fast) das minimale Set an Objekten. Dies ist bei weitem der effizienteste Weg, Git-Objekte zwischen Repositories auszutauschen.

Lokales Verzeichnis

/pfad/zu/repo.git/

Dieser Transport ist derselbe wie der SSH-Transport, verwendet aber sh, um beide Enden auf dem lokalen Computer auszuführen, anstatt das andere Ende über ssh auf dem Remote-Computer auszuführen.

Git Native

git://remote.machine/path/to/repo.git/

Dieser Transport wurde für anonyme Downloads entwickelt. Wie der SSH-Transport ermittelt er den Satz von Objekten, den die nachgelagerte Seite nicht hat, und überträgt (fast) das minimale Set an Objekten.

HTTP(S)

http://remote.machine/path/to/repo.git/

Der Downloader von HTTP- und HTTPS-URLs erhält zuerst den Namen des obersten Commit-Objekts von der Remote-Seite, indem er die angegebene Ref-Name unter dem Verzeichnis repo.git/refs/ betrachtet, und versucht dann, das Commit-Objekt zu erhalten, indem er von repo.git/objects/xx/xxx... unter Verwendung des Objektnamens dieses Commit-Objekts herunterlädt. Dann liest es das Commit-Objekt, um seine Eltern-Commits und das zugehörige Baum-Objekt zu ermitteln; es wiederholt diesen Prozess, bis es alle notwendigen Objekte erhalten hat. Aufgrund dieses Verhaltens werden sie manchmal auch als Commit-Walker bezeichnet.

Die Commit-Walker werden manchmal auch als Dumb-Transports bezeichnet, da sie keinen Git-bewussten Smart-Server wie den Git Native-Transport benötigen. Jeder normale HTTP-Server, der nicht einmal eine Verzeichnisindizierung unterstützt, würde ausreichen. Sie müssen jedoch Ihr Repository mit git update-server-info vorbereiten, um die Dumb-Transport-Downloader zu unterstützen.

Sobald Sie aus dem Remote-Repository abgerufen haben, merge Sie dies mit Ihrem aktuellen Zweig.

Es ist jedoch so üblich, dass man fetch und dann sofort merge ausführt, dass dies git pull genannt wird, und Sie können einfach tun

$ git pull <remote-repository>

und optional einen Zweignamen für das Remote-Ende als zweites Argument angeben.

Hinweis
Sie könnten darauf verzichten, überhaupt Zweige zu verwenden, indem Sie so viele lokale Repositories behalten, wie Sie Zweige haben möchten, und zwischen ihnen mit git pull zusammenführen, so wie Sie zwischen Zweigen zusammenführen. Der Vorteil dieses Ansatzes ist, dass Sie einen Satz von Dateien für jeden ausgecheckten branch behalten können und es möglicherweise einfacher finden, hin und her zu wechseln, wenn Sie mehrere Entwicklungsstränge gleichzeitig jonglieren. Natürlich zahlen Sie den Preis für mehr Festplattenplatz, um mehrere Arbeitsverzeichnisse zu speichern, aber Festplattenspeicher ist heutzutage billig.

Es ist wahrscheinlich, dass Sie von Zeit zu Zeit aus demselben Remote-Repository pullen werden. Als Kurzform können Sie die Remote-Repository-URL in der Konfigurationsdatei des lokalen Repositorys wie folgt speichern

$ git config remote.linus.url https://git.kernel.org/pub/scm/git/git.git/

und verwenden Sie das Schlüsselwort "linus" mit git pull anstelle der vollständigen URL.

Beispiele.

  1. git pull linus

  2. git pull linus tag v0.99.1

Die obigen sind äquivalent zu

  1. git pull http://www.kernel.org/pub/scm/git/git.git/ HEAD

  2. git pull http://www.kernel.org/pub/scm/git/git.git/ tag v0.99.1

Wie funktioniert das Zusammenführen?

Wir sagten, dieses Tutorial zeigt, was die Plumbing-Befehle tun, um Ihnen zu helfen, mit dem Porcelain umzugehen, das nicht spült, aber wir haben bisher nicht darüber gesprochen, wie das Zusammenführen wirklich funktioniert. Wenn Sie dieses Tutorial zum ersten Mal befolgen, schlage ich vor, zum Abschnitt "Ihre Arbeit veröffentlichen" zu springen und später hierher zurückzukehren.

Okay, immer noch bei mir? Um ein Beispiel zum Betrachten zu haben, gehen wir zurück zum früheren Repository mit den Dateien "hello" und "example" und bringen uns zurück in den Zustand vor dem Zusammenführen

$ git show-branch --more=2 master mybranch
! [master] Merge work in mybranch
 * [mybranch] Merge work in mybranch
--
-- [master] Merge work in mybranch
+* [master^2] Some work.
+* [master^] Some fun.

Denken Sie daran, dass vor dem Ausführen von git merge unser master-Head beim Commit "Some fun." war, während unser mybranch-Head beim Commit "Some work." war.

$ git switch -C mybranch master^2
$ git switch master
$ git reset --hard master^

Nach dem Zurückspulen sollte die Commit-Struktur wie folgt aussehen

$ git show-branch
* [master] Some fun.
 ! [mybranch] Some work.
--
*  [master] Some fun.
 + [mybranch] Some work.
*+ [master^] Initial commit

Jetzt sind wir bereit, das Zusammenführen von Hand zu experimentieren.

Der Befehl git merge verwendet beim Zusammenführen von zwei Zweigen den 3-Wege-Merge-Algorithmus. Zuerst findet er den gemeinsamen Vorfahren zwischen ihnen. Der Befehl, den er verwendet, ist git merge-base

$ mb=$(git merge-base HEAD mybranch)

Der Befehl schreibt den Commit-Objektnamen des gemeinsamen Vorfahren in die Standardausgabe, daher haben wir seine Ausgabe in einer Variablen erfasst, da wir sie im nächsten Schritt verwenden werden. Übrigens ist der gemeinsame Vorfahren-Commit in diesem Fall der "Initial commit". Sie können das erkennen an

$ git name-rev --name-only --tags $mb
my-first-tag

Nachdem ein gemeinsamer Vorfahren-Commit gefunden wurde, ist der zweite Schritt dieser

$ git read-tree -m -u $mb HEAD mybranch

Dies ist derselbe git read-tree-Befehl, den wir bereits gesehen haben, aber er nimmt drei Bäume entgegen, im Gegensatz zu früheren Beispielen. Dies liest den Inhalt jedes Baumes in verschiedene Stages der Indexdatei (der erste Baum geht zu Stage 1, der zweite zu Stage 2 usw.). Nachdem drei Bäume in drei Stages gelesen wurden, werden die Pfade, die in allen drei Stages gleich sind, zu Stage 0 kollabiert. Auch Pfade, die in zwei von drei Stages gleich sind, werden zu Stage 0 kollabiert, wobei die SHA-1-Werte entweder aus Stage 2 oder Stage 3 übernommen werden (d.h. nur eine Seite hat sich vom gemeinsamen Vorfahren unterschieden).

Nach dem Kollabieren bleiben Pfade, die in den drei Bäumen unterschiedlich sind, in nicht-null Stages übrig. An diesem Punkt können Sie die Indexdatei mit diesem Befehl inspizieren

$ git ls-files --stage
100644 7f8b141b65fdcee47321e399a2598a235a032422 0	example
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1	hello
100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2	hello
100644 cc44c73eb783565da5831b4d820c962954019b69 3	hello

In unserem Beispiel mit nur zwei Dateien hatten wir keine unveränderten Dateien, daher führte nur example zum Kollabieren. Aber in realen großen Projekten, wenn nur eine kleine Anzahl von Dateien in einem Commit geändert wird, neigt dieses Kollabieren dazu, die meisten Pfade ziemlich schnell trivial zusammenzuführen, und nur eine Handvoll echter Änderungen in nicht-null Stages zu hinterlassen.

Um nur nicht-null Stages anzuzeigen, verwenden Sie die Option --unmerged

$ git ls-files --unmerged
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1	hello
100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2	hello
100644 cc44c73eb783565da5831b4d820c962954019b69 3	hello

Der nächste Schritt des Zusammenführens besteht darin, diese drei Versionen der Datei mithilfe des 3-Wege-Merges zusammenzuführen. Dies geschieht, indem der Befehl git merge-one-file als eines der Argumente für den Befehl git merge-index übergeben wird

$ git merge-index git-merge-one-file hello
Auto-merging hello
ERROR: Merge conflict in hello
fatal: merge program failed

Das Skript git merge-one-file wird mit Parametern aufgerufen, die diese drei Versionen beschreiben, und ist dafür verantwortlich, die Zusammenführungsergebnisse im Arbeitsverzeichnis zu hinterlassen. Es ist ein ziemlich geradliniges Shell-Skript und ruft schließlich das merge-Programm aus der RCS-Suite auf, um einen 3-Wege-Merge auf Dateiebene durchzuführen. In diesem Fall erkennt merge Konflikte, und das Zusammenführungsergebnis mit Konfliktmarkierungen wird im Arbeitsverzeichnis hinterlassen. Dies kann gesehen werden, wenn Sie zu diesem Zeitpunkt erneut ls-files --stage ausführen

$ git ls-files --stage
100644 7f8b141b65fdcee47321e399a2598a235a032422 0	example
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1	hello
100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2	hello
100644 cc44c73eb783565da5831b4d820c962954019b69 3	hello

Dies ist der Zustand der Indexdatei und der Arbeitsdatei, nachdem git merge die Kontrolle wieder an Sie zurückgegeben hat und die widersprüchliche Zusammenführung zur Behebung überlassen hat. Beachten Sie, dass der Pfad hello immer noch nicht zusammengeführt ist und was Sie zu diesem Zeitpunkt mit git diff sehen, sind Unterschiede seit Stage 2 (d. h. Ihre Version).

Ihre Arbeit veröffentlichen

Wir können also die Arbeit von jemand anderem aus einem Remote-Repository verwenden, aber wie bereiten **Sie** ein Repository vor, damit andere Leute es abrufen können?

Sie erledigen Ihre eigentliche Arbeit in Ihrem Arbeitsverzeichnis, das Ihr primäres Repository als Unterverzeichnis .git darunter hat. Sie **könnten** dieses Repository remote zugänglich machen und Leute bitten, es abzurufen, aber in der Praxis geschieht dies normalerweise nicht. Ein empfohlener Weg ist, ein öffentliches Repository zu haben, es für andere erreichbar zu machen und, wenn die in Ihrem primären Arbeitsverzeichnis vorgenommenen Änderungen gut vorbereitet sind, das öffentliche Repository von dort aus zu aktualisieren. Dies wird oft als Pushen bezeichnet.

Hinweis
Dieses öffentliche Repository könnte weiter gespiegelt werden, und so werden Git-Repositories auf kernel.org verwaltet.

Das Veröffentlichen der Änderungen von Ihrem lokalen (privaten) Repository in Ihr Remote- (öffentliches) Repository erfordert Schreibprivilegien auf dem Remote-Computer. Sie benötigen dort ein SSH-Konto, um einen einzigen Befehl auszuführen: git-receive-pack.

Zuerst müssen Sie ein leeres Repository auf dem Remote-Computer erstellen, das Ihr öffentliches Repository beherbergen wird. Dieses leere Repository wird später beim Pushen gefüllt und auf dem neuesten Stand gehalten. Offensichtlich muss diese Repository-Erstellung nur einmal erfolgen.

Hinweis
git push verwendet ein Paar von Befehlen, git send-pack auf Ihrem lokalen Computer und git-receive-pack auf dem Remote-Computer. Die Kommunikation zwischen den beiden über das Netzwerk nutzt intern eine SSH-Verbindung.

Das Git-Verzeichnis Ihres privaten Repositorys ist normalerweise .git, aber Ihr öffentliches Repository wird oft nach dem Projektnamen benannt, d.h. <project>.git. Erstellen wir ein solches öffentliches Repository für das Projekt my-git. Nachdem Sie sich auf dem Remote-Computer angemeldet haben, erstellen Sie ein leeres Verzeichnis

$ mkdir my-git.git

Machen Sie dann dieses Verzeichnis zu einem Git-Repository, indem Sie git init ausführen, aber diesmal, da sein Name nicht das übliche .git ist, machen wir die Dinge etwas anders

$ GIT_DIR=my-git.git git init

Stellen Sie sicher, dass dieses Verzeichnis für andere, von denen Sie möchten, dass sie Ihre Änderungen über den Transport Ihrer Wahl abrufen, verfügbar ist. Außerdem müssen Sie sicherstellen, dass das Programm git-receive-pack in $PATH vorhanden ist.

Hinweis
Viele SSHD-Installationen rufen Ihre Shell nicht als Login-Shell auf, wenn Sie Programme direkt ausführen. Das bedeutet, wenn Ihre Login-Shell bash ist, wird nur .bashrc gelesen und nicht .bash_profile. Als Workaround stellen Sie sicher, dass .bashrc $PATH so einrichtet, dass Sie das Programm git-receive-pack ausführen können.
Hinweis
Wenn Sie planen, dieses Repository über HTTP zu veröffentlichen, sollten Sie zu diesem Zeitpunkt mv my-git.git/hooks/post-update.sample my-git.git/hooks/post-update ausführen. Dies stellt sicher, dass jedes Mal, wenn Sie in dieses Repository pushen, git update-server-info ausgeführt wird.

Ihr "öffentliches Repository" ist nun bereit, Ihre Änderungen zu empfangen. Gehen Sie zurück zu dem Computer, auf dem Sie Ihr privates Repository haben. Führen Sie von dort aus diesen Befehl aus

$ git push <public-host>:/path/to/my-git.git master

Dies synchronisiert Ihr öffentliches Repository, um dem benannten Zweig-Head (d.h. in diesem Fall master) und den von ihnen erreichbaren Objekten in Ihrem aktuellen Repository zu entsprechen.

Als reales Beispiel, so aktualisiere ich mein öffentliches Git-Repository. Das Kernel.org-Spiegelnetzwerk kümmert sich um die Weiterleitung an andere öffentlich sichtbare Maschinen

$ git push master.kernel.org:/pub/scm/git/git.git/

Ihr Repository packen

Zuvor sahen wir, dass für jedes von Ihnen erstellte Git-Objekt eine Datei unter dem Verzeichnis .git/objects/??/ gespeichert wird. Diese Darstellung ist effizient, um atomar und sicher zu erstellen, aber nicht so bequem über das Netzwerk zu transportieren. Da Git-Objekte nach ihrer Erstellung unveränderlich sind, gibt es eine Möglichkeit, die Speicherung zu optimieren, indem sie "zusammengepackt" werden. Der Befehl

$ git repack

tut dies für Sie. Wenn Sie den Tutorial-Beispielen gefolgt sind, hätten Sie bisher etwa 17 Objekte in den Verzeichnissen .git/objects/??/ angesammelt. git repack teilt Ihnen mit, wie viele Objekte es gepackt hat, und speichert die gepackte Datei im Verzeichnis .git/objects/pack.

Hinweis
Sie sehen zwei Dateien, pack-*.pack und pack-*.idx, im Verzeichnis .git/objects/pack. Sie hängen eng zusammen und wenn Sie sie aus irgendeinem Grund von Hand in ein anderes Repository kopieren, sollten Sie darauf achten, sie zusammen zu kopieren. Die erstere enthält alle Daten der Objekte in der Packung, und die letztere enthält den Index für den zufälligen Zugriff.

Wenn Sie paranoid sind, würde die Ausführung des Befehls git verify-pack erkennen, ob Sie eine beschädigte Packung haben, aber machen Sie sich nicht zu viele Sorgen. Unsere Programme sind immer perfekt ;-).

Sobald Sie Objekte gepackt haben, müssen Sie die ungepackten Objekte, die in der Packungsdatei enthalten sind, nicht mehr aufbewahren.

$ git prune-packed

würde sie für Sie entfernen.

Sie können versuchen, find .git/objects -type f vor und nach der Ausführung von git prune-packed auszuführen, wenn Sie neugierig sind. Außerdem würde git count-objects Ihnen mitteilen, wie viele ungepackte Objekte in Ihrem Repository vorhanden sind und wie viel Speicherplatz sie verbrauchen.

Hinweis
git pull ist für den HTTP-Transport etwas umständlich, da ein gepacktes Repository relativ wenige Objekte in einer relativ großen Packung enthalten kann. Wenn Sie viele HTTP-Pulls aus Ihrem öffentlichen Repository erwarten, möchten Sie vielleicht oft neu packen und bereinigen oder nie.

Wenn Sie git repack erneut ausführen, wird es sagen "Nichts Neues zum Packen.". Sobald Sie Ihre Entwicklung fortsetzen und Änderungen ansammeln, erstellt die erneute Ausführung von git repack eine neue Packung, die Objekte enthält, die seit dem letzten Packen Ihres Repositorys erstellt wurden. Wir empfehlen, Ihr Projekt bald nach dem anfänglichen Import zu packen (es sei denn, Sie beginnen Ihr Projekt von Grund auf neu) und dann git repack ab und zu auszuführen, abhängig davon, wie aktiv Ihr Projekt ist.

Wenn ein Repository über git push und git pull synchronisiert wird, werden gepackte Objekte im Quell-Repository normalerweise ungepackt im Ziel-Repository gespeichert. Dies ermöglicht es Ihnen zwar, auf beiden Seiten unterschiedliche Packungsstrategien zu verwenden, bedeutet aber auch, dass Sie beide Repositories ab und zu neu packen müssen.

Mit anderen zusammenarbeiten

Obwohl Git ein wirklich verteiltes System ist, ist es oft praktisch, Ihr Projekt mit einer informellen Hierarchie von Entwicklern zu organisieren. Die Entwicklung des Linux-Kernels wird so durchgeführt. Es gibt eine schöne Illustration (Seite 17, "Merges to Mainline") in Randy D**unlap's Präsentation (Randy Dunlap's presentation).

Es sollte betont werden, dass diese Hierarchie rein informell ist. Es gibt nichts Fundamentales in Git, das die von dieser Hierarchie implizierte "Kette des Patch-Flusses" erzwingt. Sie müssen nicht nur aus einem Remote-Repository pullen.

Ein empfohlener Workflow für einen "Projektleiter" sieht wie folgt aus

  1. Bereiten Sie Ihr primäres Repository auf Ihrem lokalen Computer vor. Dort erledigen Sie Ihre Arbeit.

  2. Bereiten Sie ein öffentliches Repository vor, das für andere zugänglich ist.

    Wenn andere Leute über Dumb-Transportprotokolle (HTTP) aus Ihrem Repository pullen, müssen Sie dieses Repository Dumb-Transport-freundlich halten. Nach git init enthält $GIT_DIR/hooks/post-update.sample, kopiert aus den Standardvorlagen, einen Aufruf an git update-server-info, aber Sie müssen den Hook manuell aktivieren, indem Sie mv post-update.sample post-update. Dies stellt sicher, dass git update-server-info die notwendigen Dateien auf dem neuesten Stand hält.

  3. Pushen Sie aus Ihrem primären Repository in das öffentliche Repository.

  4. Führen Sie git repack für das öffentliche Repository aus. Dies erstellt eine große Packung, die den anfänglichen Satz von Objekten als Basislinie enthält, und möglicherweise git prune, wenn der für das Pulling aus Ihrem Repository verwendete Transport gepackte Repositories unterstützt.

  5. Arbeiten Sie weiter in Ihrem primären Repository. Ihre Änderungen umfassen eigene Modifikationen, per E-Mail erhaltene Patches und Zusammenführungen, die sich aus dem Pulling der "öffentlichen" Repositories Ihrer "Subsystem-Wartungsleute" ergeben.

    Sie können dieses private Repository jederzeit neu packen, wenn Sie möchten.

  6. Pushen Sie Ihre Änderungen in das öffentliche Repository und kündigen Sie sie öffentlich an.

  7. Packen Sie das öffentliche Repository gelegentlich neu. Gehen Sie zurück zu Schritt 5 und fahren Sie mit der Arbeit fort.

Ein empfohlener Arbeitszyklus für einen "Subsystem-Wartungsleute", der an diesem Projekt arbeitet und ein eigenes "öffentliches Repository" hat, sieht wie folgt aus

  1. Bereiten Sie Ihr Arbeitsrepository vor, indem Sie das öffentliche Repository des "Projektleiters" mit git clone klonen. Die URL, die für das anfängliche Klonen verwendet wird, wird in der Konfigurationsvariablen remote.origin.url gespeichert.

  2. Bereiten Sie ein öffentliches Repository vor, das für andere zugänglich ist, genau wie die Person "Projektleiter".

  3. Kopieren Sie die gepackten Dateien aus dem öffentlichen Repository des "Projektleiters" in Ihr öffentliches Repository, es sei denn, das Repository des "Projektleiters" befindet sich auf demselben Computer wie Ihres. In letzterem Fall können Sie die Datei objects/info/alternates verwenden, um auf das Repository zu verweisen, von dem Sie ausleihen.

  4. Pushen Sie aus Ihrem primären Repository in das öffentliche Repository. Führen Sie git repack aus und möglicherweise git prune, wenn der für das Pulling aus Ihrem Repository verwendete Transport gepackte Repositories unterstützt.

  5. Arbeiten Sie weiter in Ihrem primären Repository. Ihre Änderungen umfassen eigene Modifikationen, per E-Mail erhaltene Patches und Zusammenführungen, die sich aus dem Pulling der "öffentlichen" Repositories Ihres "Projektleiters" und möglicherweise Ihrer "Sub-Subsystem-Wartungsleute" ergeben.

    Sie können dieses private Repository jederzeit neu packen, wenn Sie möchten.

  6. Pushen Sie Ihre Änderungen in Ihr öffentliches Repository und bitten Sie Ihren "Projektleiter" und möglicherweise Ihre "Sub-Subsystem-Wartungsleute", davon zu pullen.

  7. Packen Sie das öffentliche Repository gelegentlich neu. Gehen Sie zurück zu Schritt 5 und fahren Sie mit der Arbeit fort.

Ein empfohlener Arbeitszyklus für einen "individuellen Entwickler", der kein "öffentliches" Repository hat, ist etwas anders. Er sieht wie folgt aus

  1. Bereiten Sie Ihr Arbeitsrepository vor, indem Sie das öffentliche Repository des "Projektleiters" (oder eines "Subsystem-Wartungsleiters", wenn Sie an einem Subsystem arbeiten) mit git clone klonen. Die URL, die für das anfängliche Klonen verwendet wird, wird in der Konfigurationsvariablen remote.origin.url gespeichert.

  2. Erledigen Sie Ihre Arbeit in Ihrem Repository auf dem master-Zweig.

  3. Führen Sie von Zeit zu Zeit git fetch origin aus dem öffentlichen Repository Ihres Upstreams aus. Dies erledigt nur die erste Hälfte von git pull, aber es wird nicht zusammengeführt. Der Head des öffentlichen Repositorys wird in .git/refs/remotes/origin/master gespeichert.

  4. Verwenden Sie git cherry origin, um zu sehen, welche Ihrer Patches akzeptiert wurden, und/oder verwenden Sie git rebase origin, um Ihre nicht zusammengeführten Änderungen auf den aktualisierten Upstream zu portieren.

  5. Verwenden Sie git format-patch origin, um Patches für die E-Mail-Einreichung an Ihren Upstream vorzubereiten und senden Sie sie aus. Gehen Sie zurück zu Schritt 2 und fahren Sie fort.

Mit anderen zusammenarbeiten, Shared Repository Stil

Wenn Sie aus einem CVS-Hintergrund kommen, mag der im vorherigen Abschnitt vorgeschlagene Kooperationsstil neu für Sie sein. Sie müssen sich keine Sorgen machen. Git unterstützt auch den "gemeinsamen öffentlichen Repository"-Stil der Zusammenarbeit, mit dem Sie wahrscheinlich vertrauter sind.

Details finden Sie in gitcvs-migration[7].

Ihre Arbeit bündeln

Es ist wahrscheinlich, dass Sie sich gleichzeitig mit mehr als einer Sache beschäftigen werden. Es ist einfach, diese mehr oder weniger unabhängigen Aufgaben mit Branches in Git zu verwalten.

Wir haben bereits gesehen, wie Branches zuvor funktionieren, mit dem Beispiel "Spaß und Arbeit" unter Verwendung von zwei Branches. Die Idee ist dieselbe, wenn es mehr als zwei Branches gibt. Nehmen wir an, Sie sind vom "master" Head ausgegangen und haben einige neue Codes im "master"-Branch und zwei unabhängige Fixes in den Branches "commit-fix" und "diff-fix".

$ git show-branch
! [commit-fix] Fix commit message normalization.
 ! [diff-fix] Fix rename detection.
  * [master] Release candidate #1
---
 +  [diff-fix] Fix rename detection.
 +  [diff-fix~1] Better common substring algorithm.
+   [commit-fix] Fix commit message normalization.
  * [master] Release candidate #1
++* [diff-fix~2] Pretty-print messages.

Beide Fixes sind gut getestet, und an diesem Punkt möchten Sie beide einfügen. Sie könnten zuerst diff-fix und dann commit-fix wie folgt zusammenführen:

$ git merge -m "Merge fix in diff-fix" diff-fix
$ git merge -m "Merge fix in commit-fix" commit-fix

Was zu Folgendem führen würde:

$ git show-branch
! [commit-fix] Fix commit message normalization.
 ! [diff-fix] Fix rename detection.
  * [master] Merge fix in commit-fix
---
  - [master] Merge fix in commit-fix
+ * [commit-fix] Fix commit message normalization.
  - [master~1] Merge fix in diff-fix
 +* [diff-fix] Fix rename detection.
 +* [diff-fix~1] Better common substring algorithm.
  * [master~2] Release candidate #1
++* [master~3] Pretty-print messages.

Es gibt jedoch keinen besonderen Grund, zuerst einen Branch und dann den anderen zusammenzuführen, wenn Sie eine Reihe von wirklich unabhängigen Änderungen haben (wenn die Reihenfolge wichtig wäre, dann sind sie per Definition nicht unabhängig). Sie könnten stattdessen diese beiden Branches gleichzeitig in den aktuellen Branch zusammenführen. Lassen Sie uns zuerst rückgängig machen, was wir gerade getan haben, und von vorne beginnen. Wir möchten den Master-Branch vor diesen beiden Zusammenführungen erhalten, indem wir ihn auf master~2 zurücksetzen.

$ git reset --hard master~2

Sie können sicherstellen, dass git show-branch mit dem Zustand vor diesen beiden git merge übereinstimmt, die Sie gerade durchgeführt haben. Anstatt dann zwei git merge Befehle hintereinander auszuführen, würden Sie diese beiden Branch Heads zusammenführen (dies wird als Octopus erstellen bezeichnet).

$ git merge commit-fix diff-fix
$ git show-branch
! [commit-fix] Fix commit message normalization.
 ! [diff-fix] Fix rename detection.
  * [master] Octopus merge of branches 'diff-fix' and 'commit-fix'
---
  - [master] Octopus merge of branches 'diff-fix' and 'commit-fix'
+ * [commit-fix] Fix commit message normalization.
 +* [diff-fix] Fix rename detection.
 +* [diff-fix~1] Better common substring algorithm.
  * [master~1] Release candidate #1
++* [master~2] Pretty-print messages.

Beachten Sie, dass Sie einen Octopus nicht nur deshalb tun sollten, weil Sie es können. Ein Octopus ist ein gültiges Vorgehen und erleichtert oft die Betrachtung der Commit-Historie, wenn Sie mehr als zwei unabhängige Änderungen gleichzeitig zusammenführen. Wenn Sie jedoch Merge-Konflikte mit einem der Branches haben, die Sie zusammenführen, und diese manuell auflösen müssen, ist dies ein Hinweis darauf, dass die Entwicklung in diesen Branches nicht unabhängig war. Sie sollten dann zwei auf einmal zusammenführen, dokumentieren, wie Sie die Konflikte gelöst haben, und den Grund, warum Sie Änderungen auf einer Seite gegenüber der anderen bevorzugt haben. Andernfalls würde dies die Projektgeschichte erschweren, nicht erleichtern.

GIT

Teil der git[1] Suite