Nachdem wir Git eingerichtet haben und die nötigsten Optionen gesetzt sind, wollen wir nun versuchen die Grundlagen der Git-Nutzung mit einem Beispielprojekt zu lernen.
Dazu sollten wir zunächst ein paar Grundbegriffe über die Benutzung klären.
Habt ihr ein Projekt, liegt es höchstwahrscheinlich als Verzeichnis vor, indem einige Dateien und unterverzeichnisse liegen. Wird dieses Verzeichnis mit einem Versionsverwaltungssystem verwaltet, nennt man es „Arbeitskopie“.
Eine Arbeitskopie ist - wie der Name schon sagt - eine Kopie, mit der ihr Arbeiten könnt. Dort könnt ihr Dateien ändern, hinzufügen oder löschen, ihr könnt die Quellcodedateien zum Kompilieren verwenden oder auch sonst alles machen.
In einem verteilten Versionsverwaltungssystem wie Git 1) es ist ist, besitzt jede Arbeitskopie auch ein sogenanntes „Repository“. Dies ist bei zentralen Versionsverwaltungssystemen, wie CVS und Subversion nicht so: das Repository liegt auf einem entfernten Server.
Repository ist Englisch für Lagerstätte oder Aufbewahrungsort, und das wird dort auch gemacht: alle Änderungen und Dateien die ihr tätigt werden dort gespeichert und aufbewahrt. In diesem Tutorial werde ich dafür das englische Wort „Repository“ verwenden.
Um also unser Projekt zu verfolgen, müssen wir zuerst ein Repository anlegen. Dazu legen wir ein Verzeichnis an, und wechseln dort hinein
mkdir helloworld cd helloworld
Anschließend erstellen - oder besser gesagt initiieren - wir ein Git-Repository:
git init
Und schon ist es erstellt! Überprüfen könnt ihr das, indem ihr mit 'ls -a
'
alle Unterverzeichnisse anzeigen lasst. Das Repository ist das
.git
-Unterverzeichnis.
Jetzt wollen wir mit der Arbeit beginnen. Dazu werden wir das typische „Hello World“- Programm schreiben, und es anschließend eintragen. Ihr könnt die Datei entweder mit einem Editor erstellen, oder ihr kopiert einfach das Folgende in eure Konsole:
cat > helloworld.c << EOF #include <stdio.h> int main() { printf ("Hello World!\n"); return 0; } EOF
Wollt ihr diese Datei nun eintragen, müsst ihr Git erstmal bekannt geben, dass es eure Datei verfolgen soll. Dies geschieht mit dem Befehl add:
git add helloworld.c
Anschließend müssen wir die Änderungen commiten (das bedeutet Eintragen). Dies geschieht mit dem commit-Befehl:
git commit
Dies startet euren eingestellten Editor, mit dem ihr den Commit beschreiben müsst: schreibt eine Art kleinen Logbucheintrag über eure Änderungen. Diese Nachricht wird gespeichert, und soll als erklärung der Änderung dienen, damit man auch in Zukunft nachvollziehen kann was ihr warum gemacht habt. Haltet euch kurz, bleibt präzise und beschreibt mit so wenig Worten wie möglich was ihr wozu getan habt.
Das Format ist dabei zwar völlig frei Wählbar, es empfiehlt sich allerdings ein bestimmtes Format einzuhalten, damit ihr die Änderungen bequem lesen könnt:
diese sollten aber nicht länger als 76 Zeichen sein.
Für den ersten Commit hat es sich eingebürgert einfach nur „Initial commit“ als Nachricht zu verwenden.
Habt ihr eure Nachricht eingetragen, speichert die offene Datei und schließt den Editor. Als Antwort bekommt ihr nun, welcher Zweig aktualisiert wurde, und eine kleine Zusammenfassung der Änderungen. Bei mir sieht das Beispielsweise so aus:
[master (Basis-Version) 0b78177] Initial commit 1 Datei geändert, 9 Zeilen hinzugefügt(+) create mode 100644 helloworld.c
Gratulation! Damit habt ihr euren ersten Commit getätigt!
Da das jetzt so schön war, fügen wir noch eine Datei names Makefile
hinzu:
.PHONY: clean all: helloworld helloworld: helloworld.c clean: $(RM) helloworld
git add Makefile git commit -m "Makefile hinzugefügt"
Dem add-Befehl könnt ihr auch ganze Verzeichnisse hizufügen:
git add <verzeichnis>/
Das tut aber nicht das was ihr wahrscheinlich denkt: es fügt alle Dateien in diesem Verzeichnis und allen Unterverzeichnisse hinzu, und damit zwar automatisch das angegebene Verzeichnis selbst. Leere Verzeichnisse können allerdings nicht hinzugefügt werden!
Begründet wird das damit dass Git ja ein „Content“-Tracker ist, und Verzeichnisse keine Inhalte sind, sondern nur Organisationsstrukturen.
Wie auch immer, ihr könnt leider keine leeren Verzeichnisse in Git verfolgen.
Es gibt allerdings einen Trick, um das zu erreichen: erstellt eine Datei
namens .gitignore
(siehe unten) in dem
Verzeichnis das ihr verfolgen wollt. Dann ist es zwar nicht ganz leer, enthält
aber nur eine versteckte, leere Datei.
Nun werden wir versuchen, eine vorhandene Datei zu Ändern, und diese Änderungen einzutragen.
Vorher möchte ich allerdings eure Aufmerksamkeit auf einen neuen Befehl lenken: der status-Befehl:
git status
Dieser Befehl gibt euch Auskunft über den derzeitigen Status eurer Arbeitskopie: auf welchem Zweig ihr euch befindet, welche Dateien geändert sind, und welche zum Eintragen markiert sind. Kurz: er gibt Auskunft darüber „was gerade abgeht“.
Dementsprechend würde ich euch empfehlen, dass ihr ihn immer dann eingebt, wenn ihr nicht sicher seid in welchen Zustand sich eure Arbeitskopie gerade befindet. Dies wird am Anfang noch öfter nötig sein, mit der Erfahrung bekommt ihr aber ein Gefühl dafür.
Gebt nun diesen Befehl ein, und seht euch die Ausgabe an. Bei mir sieht das so aus:
# Auf Zweig master nichts zum Eintragen (Arbeitsverzeichnis sauber)
Klar. Wir befinden uns auf dem Zweig „master“ (unser „Haupt“-Zweig), und wir haben seit unserem Letzten Commit nichts geändert (deswegen ist das Arbeitsverzeichnis „sauber“) und auch nichts mit add hinzugefügt (deswegen haben wir „nichts zum Eintragen“).
Jetzt werden wir unser Programm ändern. Das geschieht wie auch immer ihr das
normalerweise macht. Sagen wir, wir wollen nun nicht mehr die Welt grüßen,
sondern nur proggen.org. Ändert also die 5. Zeile in der Datei helloworld.c
und speichert die Datei. Wenn ihr zu faul seid, kopiert einfach folgendes
Zauberkommando in eure Kommandozeile:
sed -i -e 's/World!/proggen.org!/g' helloworld.c
So. Nun rufen wir noch einmal git status
auf. Die Ausgabe sieht bei mir so
aus:
# Auf Zweig master # Änderungen, die nicht zum Eintragen bereitgestellt sind: # (benutze "git add <Datei>..." zur Aktualisierung der Eintragung) # (benutze "git checkout -- <Datei>..." um die Änderungen im Arbeitsverzeichnis zu verwerfen) # # geändert: helloworld.c # keine Änderungen zum Eintragen hinzugefügt (benutze "git add" und/oder "git commit -a")
Wir befinden uns immer noch auf dem Zweig „master“, haben aber jetzt Änderungen
in unserem Arbeitsverzeichnis. Habt ihr color.ui
eingeschalten, wird die Datei helloworld.c
rot angezeigt.
Achtung: wenn ihr jetzt commiten würdet, würde das gar nicht gehn, da keine Änderungen bereitgestellt sind! Dies ist ein Unterschied zu anderen Versionsverwaltungssystemen (wie z.B. Subversion), denn dort würden alle Änderungen an bekannten Dateien eingetragen werden.
Nicht so bei Git. Hier müsst ihr die Änderungen zuerst „Bereitstellen“, und zwar mit dem Befehl add. Dieser fügt die Änderungen an einer Datei in die sogenannte „staging Area“ oder kurz „Index“ hinzu.
Wenn ihr den commit-Befehl aufruft, werden immer nur diejenigen Änderungen commitet, die sich im Index befinden. Für Novizen sieht das erstmal wie Mehrarbeit im Vergleich zu SVN aus, ihr werdet diese Funktionsweise aber mit der Zeit zu schätzen lernen!
Nun gut, jetzt tragen wir unsere Änderungen ein:
git add helloworld.c
Sehen wir uns den status an:
git status
Die Ausgabe ist:
# Auf Zweig master # zum Eintragen bereitgestellte Änderungen: # (benutze "git reset HEAD <Datei>..." zum Herausnehmen aus der Bereitstellung) # # geändert: helloworld.c #
Wie ihr nun seht, sind die Änderungen die wir gemacht haben
„zum Eintragen bereitgestellt“ - die Datei helloworld.c
ist nun grün.
Man sagt, die Änderungen befinden sich „im Index“. Der „Index“ ist der Zustand
der Arbeitskopie, den sie beim nächsten Commit haben soll.
Beachtet, dass wenn ihr helloworld.c jetzt noch einmal bearbeiten würdet, würden die neuen Änderungen nicht eingetragen werden. Wollt ihr die Datei weiter bearbeiten und die Gesamtänderungen commiten, müsst ihr sie nach dem Ändern noch einmal mit add hinzufügen.
Wir könnten jetzt einen Commit tätigen, und die Änderungen würden aufgezeichnet werden. Das machen wir allerdings noch nicht.
Wir sehen uns zuerst nochmal an, was passiert wenn man weitere Änderungen
tätigt. Ändern wir die Makefile
-Datei zu folgendem:
.PHONY: clean all: helloworld helloworld: helloworld.c $(CC) $(CFLAGS) -o $@ $< clean: $(RM) helloworld
Sehen wir uns den jetzigen Status an:
git status
# Auf Zweig master # zum Eintragen bereitgestellte Änderungen: # (benutze "git reset HEAD <Datei>..." zum Herausnehmen aus der Bereitstellung) # # geändert: helloworld.c # # Änderungen, die nicht zum Eintragen bereitgestellt sind: # (benutze "git add <Datei>..." zur Aktualisierung der Eintragung) # (benutze "git checkout -- <Datei>..." um die Änderungen im Arbeitsverzeichnis zu verwerfen) # # geändert: Makefile #
Jetzt sehen wir, dass helloworld.c
bereitgestellt (im Index) ist, und das
Makefile
nicht. Würdet ihr jetzt den Commit tätigen, würde nur die
helloworld.c
-Datei eingetragen werden. Das seht ihr auch an der Farbe:
helloworld.c
ist grün und Makefile
ist rot.
Wir können uns die Änderungen auch detailliert anzeigen lassen. Wollt ihr euch die Änderungen ansehen, die beim nächsten commit eingetragen werden, benutzt folgenden Befehl:
git diff --cached
Wollt ihr die Änderungen zwischen dem Index und dem jetzigen Zustand der Arbeitskopie anzeigen (also alle noch nicht bereitgestellten Änderungen), benutzt Folgendes:
git diff
Wollt ihr die gesamten Änderungen, die seit dem letzten Commit geschehen sind anzeigen lassen, benutzt
git diff HEAD
mit diesen Befehlen wird das jeweilige diff in einem Pager angezeigt. Die Navigation in der Ausgabe könnt ihr hier nachlesen: In der Ausgabe scrollen.
Näheres zu diff und HEAD findet ihr im nächsten Kapitel.
Bevor man die Änderungen tatsächlich commitet, sollte man stets überprüfen das
nur die richtigen Änderungen, die ihr eintragen wollt auch tatsächlich
bereitgestellt sind. Überprüft mit 'git status
', ob auch die richtigen
Dateien markiert sind, und eventuell mit 'git diff --cached
' was eingetragen
wird. Wenn ihr euch vergewissert habt, dass alles in Ordnung ist, könnt ihr
weitermachen.
Stellen wir nun alle Änderungen bereit und erstellen den Commit:
git add Makefile git commit
Als Commit-Nachricht geben wir „Grüße nur proggen.org statt der Welt“ ein.
Überprüft ihr jetzt noch einmal den Status, sollte das Arbeitsverzeichnis wieder als „sauber“ angezeigt werden.
Selbstverständlich kann man in Git auch Dateien löschen. Dies geschieht mit dem Befehl rm:
git rm <datei>
Man kann auch ganze Verzeichnisse löschen, dazu übergibt man den Schalter
'-r
':
git rm -r <verzeichnis>
Das werden wir jetzt an unserem Beispielprojekt üben: zunächst erstellen wir
eine Kopie der Datei helloworld.c
, und löschen dann die Ursprungsdatei:
cp helloworld.c hello.c git rm helloworld.c
Ein git status
zeigt nun folgendes an:
# Auf Zweig master # zum Eintragen bereitgestellte Änderungen: # (benutze "git reset HEAD <Datei>..." zum Herausnehmen aus der Bereitstellung) # # gelöscht: helloworld.c # # Unverfolgte Dateien: # (benutze "git add <Datei>..." zum Einfügen in die Eintragung) # # hello.c
Das Löschen der Datei ist nun bereitgestellt (im Index), und mit ls
könnt
ihr Überprüfen, dass die Datei auch von eurem Dateisystem gelöscht ist.
Jetzt werden wir die Kopie der gelöschten Datei wieder zu unserem Projekt hinzufügen, das geschieht in der gewohnten Weise mit
git add hello.c
Ein 'git status
' zeigt nun folgendes:
# Auf Zweig master # zum Eintragen bereitgestellte Änderungen: # (benutze "git reset HEAD <Datei>..." zum Herausnehmen aus der Bereitstellung) # # umbenannt: helloworld.c -> hello.c #
Wie ihr seht, hat Git, obwohl wir die Datei kopiert, das Original gelöscht und die Kopie hinzugefügt haben die Operation als „Umbennen“ erkannt.
Tatsächlich ist es so, dass es unter Git kein richtiges „Umbennen“ gibt! Es gibt zwar den mv-Befehl der auch funktioniert wie man es erwarten würde, dieser tut aber nichts anderes als wir gerade gemacht haben - und zwar die alte Datei löschen und die gleiche Datei mit anderem Namen hinzufügen.
Das Erkennen ob es sich dabei um Umbennenen handelt, übernimmt Git. Das mag auf den ersten Blick zwar Sinnfrei erscheinen, aber: in der Philosophie von Git zählt nur eines: der Inhalt. Für Git bewegen sich nur Codezeilen, wie die dazugehörigen Dateien heißen oder wie diese auf dem Dateisystem repräsentiert sind ist Git egal. Das ermöglicht vielen Git-Befehlen so effektiv wie möglich zu sein.
Um unsere Änderungen abzuschließen, sollten wir noch eine kleinigkeit im Makefile ändern: und zwar alle vorkomnisse von „helloworld“ auf „hello“ zu ändern. Das geht mit folgendem Zauberkommando:
sed -i -e 's/helloworld/hello/g' Makefile
Diese Änderungen stellen wir ebenfalls bereit:
git add Makefile
Bevor wir commiten, sollten wir uns zumindest mit 'git status
' ansehen, ob
alle Änderungen so eingetragen werden, wie wir uns das gedacht haben. Eventuell
könnt ihr die Änderungen mit 'git diff --cached
' überprüfen. Erst wenn ihr
sicher seid, dass alles in Ordnung ist, tragen wir die Änderungen ein:
git commit
Als Nachricht geben wir dabei „helloworld.c umbenannt“ ein.
Damit würde ich dieses Kapitel gerne beenden, denn das Wichtigste dazu wie man Geschichte schreibt und einträgt ist gesagt. Allerdings gäbe es noch einige weitere interessante Punkte, die ich gerne ansprechen würde. Diese werden zwar nützlich sein, sind aber nicht zwingend für die Benutzung von Git erforderlich.
Aus diesem Grund werden diese Punkte in dem Abschnitt „Fortgeschrittene Themen“ angesprochen. Wenn ihr mit dem bisher gelernten überfordert sein, rate ich euch diesen Teil zu überspringen und später darauf zurückzukommen. Ist euer Kopf noch frisch, könnt ihr ruhig weitermachen.
Ist die Commit-Nachricht kurz, könnt ihr sie auch direkt als
Kommandozeilenparameter angeben. Dazu überreicht den Parameter --message
oder -m
und danach die Nachricht in Anführungszeichen:
git commit -m "Initial commit"
Damit spart ihr euch, dass der Editor aufgerufen wird.
Mit 'git add -u
' könnt ihr alle Änderungen an jeder Datei, die Git bereits
kennt in den Index stellen.
Außerdem könnt alle Änderungen an jeder Datei, die Git bereits kennt eintragen
lassen und damit das manuelle eintragen in den Index umgehen. Dies geschieht mit
dem Schalter -a
. Ihr könnt ihn auch zusammen mit -m
verwenden:
git commit -am "Einiges geändert"
Dazu ein kleiner Tip für euch: FINGER WEG VON DIESER OPTION! Obwohl das quasi das „Standardverhalten“ von SVN wäre, würde ich euch raten das nicht zu tun. Es ist (mir selbst, aber auch Anderen) schon oft genug passiert, dass deswegen Dateien fehlen, unbeabsichtigt hinzugefügt, oder die falschen Änderungen eingetragen wurden. Tut euch selbst einen Gefallen, und lasst es.
Jedes mal, wenn ihr git status
aufruft, werden alle Dateien, die Git nicht
bekannt sind als „nicht verfolgt“ („untracked“) angezeigt. Dies stellt
eigentlich kein Problem dar, denn solange ihr diese Dateien nicht manuell
hinzufügt, wird es keine Probleme geben.
Sind das allerdings viele Dateien, die öfters auftauchen und die die Ausgabe von
git status
nicht zumüllen sollen, gibt es eine Möglichkeit diese dauerhaft
auszublenden.
Das ist zum Beispiel für temporäre Dateien des Bau-Systems, für kompilierte Dateien oder für Backupdateien eures Editors nützlich.
Diese Dateien und Ordner könnt ihr nach einem bestimmten Muster entweder
in die Datei .gitignore
oder in die Datei .git/info/exclude
eintragen.
Wohin ihr eure ignorierten Dateien eintragt ist eigentlich egal, allerdings wird
.git/info/exclude
für „private“ Dateien (also Dateien, die nur euren eigenen
Arbeitsfluss betreffen) und .gitignore
für „öffentliche“ ignorierte Dateien
(also Dateien, die bei allen Entwicklern ignoriert werden sollten) verwendet.
Dementsprechend muss .gitignore
(im Gegensatz zu .git/info/exclude) auch
zur Geschichte hinzugefügt und eingetragen werden.
Das Muster für die Erkennung der ingorierten Dateien ist relativ einfach:
.gitignore
-Datei steht).
Die Datei .gitignore
könnt ihr in jedes Verzeichnis platzieren. Dabei werden
bestimmte Muster (siehe oben) relativ zum Pfad der der .gitignore
-Datei
ignoriert.
Diese Datei könntet ihr vielleicht auch dazu nutzen, um „leere“ Verzeichnisse in
Git zu erzwingen. Legt dazu einfach in dem leeren Verzeichnis die
.gitignore
-Datei an, fügt diese hinzu und commitet die Änderung.
Selbstverständlich ist das Verzeichnis dabei nicht tatsächlich Leer, sondern
enthält nur eine einzige versteckte Datei.
Wir werden nun in unserem Projekt alle Dateien ignorieren, die aus dem Quellcode gebaut werden können:
printf 'hello\n' >> .gitignore printf '*.o\n' >> .gitignore git add .gitignore git commit -m ".gitignore hinzugefügt"
Erzeugt euer Editor noch gerne Backupdateien, die z.B. mit einer Tilde ('~')
enden, sollten diese ebenfalls ignoriert werden. Dieses Muster kommt nach
.git/info/exclude
:
printf '*~\n' >> .git/info/exclude
Für die Details und eine vielleicht klarere Erklärung darüber wie Git Dateien
ignoriert, seht euch bitte die Git-Hilfe zu .gitnigore mit
git help ignore
an.
Der Sinn des Index' besteht unter anderem darin, dass man an einer Arbeitskopie mehrere Änderungen hintereinander ausführen kann, und diese dann in der Geschichte als „schrittweise“ Änderungen, in mehreren Commits darstellen kann.
Mit dem bisher Gelernten kann man nur ganze Dateien zum Index hinzufügen, allerdings könnte es sein dass man an einer Datei mehrere Änderungen gemacht hat, die man gerne in verschiedenen Commits hätte.
Für diesen Fall bietet Git eine Lösung an: man kann die Änderung an einer oder mehreren Dateien „stückweise“ hinzufügen. Ein „Stück“ ist in diesem Fall ein sogenannter Hunk, ein Teil eines Patches.
Seht ihr euch nämlich ein Diff an, werdet ihr bemerken, dass ein Patch aus mehreren Bereichen besteht: den hinzugefügten Zeilen, den entfernten Zeilen und dem „Kontext“, der um die geänderten Zeilen ist. Einen solchen Bereich bezeichnet man als Hunk.
Näheres zum Format von Patches findet ihr in der Wiki-Seite von Diff/Patch.
Mit Git hat man die Möglichkeit, die Änderungen an einer Datei in diesen „Hunks“
in den Index zu stellen, und dazu übergebt ihr git add
den Schalter -p
oder --patch
:
git add -p <Pfade> ...
Tut ihr das, kommt ihr in eine Art „interaktiven Hinzufügemodus“, bei dem euch jeder Hunk gezeigt wird und ihr entscheiden müsst, ob ihr ihn bereitstellen wollt oder nicht.
Mit y könnt ihr ihn hinzufügen, mit n ablehnen und mit q das Hinzufügen beenden.
Ihr könnt außerdem den Hunk bis zu einer gewissen Mindestgröße weiter Teilen, und zwar mit der Taste s. Wollt ihr einen Hunk kleiner als diese Mindestgröße bereitstellen, müsst ihr den Hunk manuell eingben (wählt dazu die Option e). Ich muss allerdings zugeben, dass ich es noch nie geschafft habe, einen solchen manuellen Hunk ordnungsgemäß hinzuzufügen 2).
Dabei gibt es noch weitere Optionen, die ihr mit der Taste ? abfragen könnt.
Seid ihr mit dem stückweisen Hinzufügen fertig (indem ihr entweder alle Hunks
abgearbeitet habt oder die Abfrage vorzeitig verlassen habt), dann sollten
bei einem git status
die Dateien doppelt angezeigt werden
(außer ihr habt keinen oder alle Hunks angenommen):
als Dateien mit bereitgestellten und mit nicht bereitgestellten Änderungen
- klar, ihr habt ja nur einen Teil der Änderungen bereitgestellt und den
anderen nicht.
Hat dir dieser Artikel geholfen? Wenn nicht oder wenn du Verbesserungs- bzw. Erweiterungsvorschläge hast, dann schreib das bitte einfach auf die Diskussionsseite.