Courtesy of Pixabay

Git-Vertiefung

Weiterführende Funktionen

Die Arbeit mit Git bietet noch viel mehr Möglichkeiten als in den einführenden Tutorials beschrieben. In dieser Session beschreiben wir euch ein paar der nützlichsten Funktionen (unter anderem auch wie man den letzten Commit rückgängig machen kann, wenn man einen Fehler gemacht hat). Darüber hinaus sind auch in SmartGit noch weitere Features integriert, die wir hier vorstellen wollen.

Discard

Über discard, können wir Änderungen, die wir an den Dateien vorgenommen haben, verwerfen. Wir würden dadurch wieder zu der Version der Datei zurückkehren, die im letzten commit enthalten ist. Wichtig ist, dass es somit hier um Änderungen an der Datei geht, die noch nicht in einem commit verarbeitet wurden.

Um das Ganze zu testen, müssen wir zunächst eine Änderung an einer lokalen Datei vornehmen. Beispielsweise fügen wir in unsere Auswertungsdatei eine neue Zeile Code ein.

Die Änderung wird nun in SmartGit angezeigt, indem die Datei als “Modified” bezeichnet wird.

Wir betrachten im Tutorial die Nutzung von Discard nur mit Hilfe von SmartGit. Wer lieber mit dem Terminal arbeitet, klickt hier.

Die durchgeführte Änderung an der Datei könnten wir in einen commit packen und dann auch einen push durchführen. Hier gehen wir nun aber davon aus, dass wir bemerken, dass unsere Änderung nicht gut ist und wieder rückgängig gemacht werden sollte. Natürlich könnte man die Datei öffnen und die Zeile einfach wieder raus löschen. Das ist in diesem Fall recht simpel, kann aber bei komplexeren Änderungen sehr langwierig sein. Hier kann man über discard nun also sehr schnell zum Stand des letzten commit zurückkehren. Der Button für den discard ist auch in der Werkzeugsleiste enthalten. Am besten wählt ihr vor dem Klicken die betreffende/n Datei/en bereits aus.

Bei der Auswahl gibt es im Dialogfenster dann zwei Unteroptionen:

  1. Discard: Wenn die Veränderungen nicht mehr benötigt werden, reicht diese Option aus. Hier werden alle Änderungen rückgängig gemacht und die Datei ist wieder auf dem alten Stand des letzten commits.
  2. Discard to Stash: Hierbei wird die veränderte Datei nicht unwiderruflich gelöscht, sondern dem Stash - quasi einem Papierkorb - hinzugefügt. Von hier aus können die veränderten Dateien wieder abgerufen werden, falls man sie doch noch benötigt.

Die bessere Option müsst ihr also von Fall zu Fall selbst wählen. Da es bei der Auswahl vom reinen discard keine weiteren Schritte geht, wählen im Tutorial aus Demonstrationszwecken jetzt Discard to Stash. Es folgt eine kleine Änderung in der SmartGit-Ansicht und zwar im Fenster zu den Branches. Hier erscheint der neue Punkt Stashes. Wenn wir auf diesen draufklicken, werden Datum und Uhrzeit angezeigt.

Mit einem Rechtsklick werden die Optionen zum Umgang mit dem stash angezeigt. Man kann sich den Inhalt nochmal mit Show Content in Log anschauen oder den Stash mittels Rename Stash… umbenennen, um sich für spätere Verwendung eine bessere Gedankenstütze zu bauen. Anhand von Apply Stash… kann die verworfenen Änderung wieder vorgenommen werden - darauf drücken wir zu Demonstrationszwecken.

Hier wird nochmal darin unterschieden, ob der stash nach dem Wiedereinfügen bestehen bleibt (Apply) (also wir uns auch später nochmal anschauen können, was wir in der Vergangenheit in den Stash gepackt haben) oder ob der Eintrag dann verworfen wird (Apply & Drop). Da der Stash ja nur zur Demonstration erstellt wurde, klicken wir auf Apply & Drop.

Wir sehen nun, dass die Datei zur Datenauswertung wieder als “Modified” angezeigt wird - die ursprünglich geschriebene neue Zeile wird jetzt wieder als Teil der Datei angesehen. Weiterhin ist der stash aus der Anzeige der Branches verschwunden (dies wäre bei nur Apply nicht passiert). Da die Änderung an unserer Auswertungsdatei aber nicht wichtig ist, wollen wir sie jetzt endgültig rückgäng machen. Wir wählen also wieder Discard und als Option diesmal das reine discard. Damit ist die Datei wieder auf dem Stand des letzten commit.

git revert

Dieser Befehl ist eine sichere Methode, um die Änderung eines Commit rückgängig zu machen. Was genau passiert, werden wir uns in einem Beispiel ansehen. Nehmen wir an, dass wir in unsere Datenauswertung etwas Falsches reingeschrieben haben:

Das ist uns aber erst aufgefallen, als wir die Änderung schon in einen Commit gepackt und diesen ausgeführt haben. Außerdem führen wir auch den Push durch.

Jetzt wollen wir diesen Commit rückgängig machen. Mit SmartGit geht es wie folgt, falls ihr lieber mit dem Terminal arbeitet, klickt hier.

In SmartGit müsst ihr oben in der Toolleiste auf Branch und dann Revert gehen. Hier seht ihr nun ganz oben euren letzten Commit, den wir rückgängig machen wollen. Dafür müsst ihr ihn auswählen und unten im Fenster auf “Revert & Commit” klicken.

Schaut ihr euch nun den Commit-Verlauf an, werdet ihr ganz oben den Commit sehen, der euren Alten rückgängig gemacht hat.

Der ungewollte Commit wird also nicht gelöscht, sondern das Repository wird einfach wieder auf den Stand zurückgesetzt, der vor dem ungewollten Commit existiert hat. Der “Revert”-Commit, den wir durch unser Vorgehen erlangt haben, repräsentiert denselben Status unseres Projekts wie der Commit vor unserem ungewollten Commit.

Diese Logik, dass der Commit nicht aus dem Log entfernt wird, bietet den Vorteil, dass wir doch noch mit dem ungewollten Commit arbeiten zu können, falls wir unsere Meinung ändern sollten.

Momentan wäre der ungewollte Commit nur auf unserer lokalen Version wieder rückgängig gemacht. Um das ganze auch im online Repository auf GitHub zu erhalten, müssen wir einen push durchführen.

git reset

Mit einem Reset kann man Commits auch rückgängig machen. Aber was ist der Unterschied zu dem eben betrachteten Vorgang über Revert? Wie im vorherigen Absatz beschrieben erstellt Revert einen Neuen Commit, der den alten umkehrt und wieder auf den Stand davor bringt (schaut euch dazu auch nochmal das Flowchart für git revert an). Der HEAD-Zeiger bewegt sich also vorwärts. Bei einem git Reset bewegt sich der HEAD-Zeiger rückwärts. Der ungewollte Commit wird vollständig gelöscht, auch aus dem Log. Ihr kehrt tatsächlich zu eurem alten Commit zurück und nicht nur auf eine “Kopie” davon. Ein Nachteil wird gleich beschrieben, weshalb wir das Vorgehen auch nur oberflächlich beschreiben und nicht an einem Beispiel orientieren.

Wer wissen will, wie man das im Terminal macht, klickt hier. In SmartGit funktioniert es recht einfach. Ihr klickt mit einem Rechtsklick auf den Commit zu dem ihr resetten wollt.

So könnt ihr auch ganz einfach mehrere Commits rückgängig machen, indem ihr einfach auf den Commit klickt, den ihr “ganz oben” haben wollt. Anschließend müsst ihr das noch bestätigen und euer Repository ist auf dem Stand des gewollten Commits.

Diese Option ist aber ungeeignet, wenn ihr euren falschen Commit schon auf GitHub gepushed habt. Versucht ihr nämlich nach dem Reset zu pushen, wird euch SmartGit Folgendes anzeigen:

SmartGit lässt auch nicht pushen, weil eventuell schon andere Leute auf dem Repository mit euren Commits arbeiten könnten. Wenn ihr aber alleine Zugriff auf euer Repository habt oder euch sicher seid, dass noch keiner mit diesen Commits gearbeitet hat, könnt ihr die Option: “Allow modifying pushed commits” anstellen. Ansonsten empfehlen wir die Arbeit mit Revert.

Fetch

Fetch ist euch vielleicht schon beim Befehl pull als Option aufgefallen. Dort konntet ihr euch zwischen Pull und Fetch Only entscheiden.

Der Unterschied zwischen den beiden Optionen besteht darin, dass beim Fetch lediglich die Informationen darüber abgerufen werden, ob seit unserem letzten push Veränderungen an dem Projekt vorgenommen wurden und welche genau das sind. Bei pull hingegen werden, wie wir es gesehen haben, sowohl diese Information abgerufen als auch alle Veränderungen auf unseren lokalen Ordner übertragen. Somit arbeiten wir direkt mit den veränderten Dateien weiter, während wir bei Fetch noch auf unserem eigenen letzten Stand bleiben.

Mit fetch können wir also einen Überblick über die Veränderungen erhalten, die seit unserem letzten pull von den anderen Kollaborator:innen vorgenommen wurden und auf GitHub gepushed wurden. Wir können dann nach der Betrachtung überlegen, ob wir die Änderungen auch übernehmen möchten. Beachtet jedoch, dass es zu Konflikten kommen kann, wenn ihr eine Datei nicht aktualisiert, sie dann auf andere Weise selbst verändert und versucht, einen push durchzuführen. Dann muss in mühsamer Kleinarbeit der Konflikt gelöst werden. Daher ist der Einsatz von fetch in unserer Anwendung eher beschränkt.

gitignore

Eine weitaus nützlichere Funktionalität ist die Verwendung von gitignore, die wir euch anhand der stets präsenten .Rhistory erläutern. Wie wir bereits beschrieben haben, ist die Datei .Rhistory eine eigene Historien-Dokumentation über durchgeführte Befehle in R, aber für unsere Arbeit unnötig. Trotzdem ist sie stets in der Anzeige Files in SmartGit enthalten, wo sie als “Untracked” angezeigt wird. Bisher haben wir sie stets manuell ignoriert, was aber keine zufriedenstellende Lösung ist. Weiterhin kann man Dateien mit Rechtsklick und Delete einfach löschen, aber bei der nächsten Arbeit mit R würde sie wieder auftauchen. Wir müssen Git also berichten, dass dies eine unwichtige Datei ist, die in alle Aktionen nicht einbezogen werden soll, wofür gitignore gemacht ist.

In einer Datei mit dem Namen .gitignore können Benennungen von Dateien festgelegt werden, die von Git - wie es der Name sagt - ignoriert werden. Es ist im Endeffekt eine Liste mit vielen Einträgen. Man kann die .gitignore-Datei mit einem normalen Texteditor erstellen, wobei dabei manchmal Probleme auftauchen, da keine Zeichen vor dem . im Dateinamen sind oder die Datei als .gitignore.txt abgespeichert wird, wodurch sie nicht funktioniert. Auch GitHub bietet eine Option zum Erstellen dieser Datei. Wir wollen aber betrachten, wie SmartGit uns dabei behilflich sein kann.

  1. Wir machen einen Rechtsklick auf die Datei .Rhistory und wählen jetzt die Option Ignore.
  2. In dem Fenster, was sich jetzt öffnet, haben wir zwei Optionen. Wir können entweder die Datei spezifisch zum Ignorieren auswählen (Ignore explicitly (e.g. ‘Makefile’)) oder alle Dateien mit ähnlichem Muster. Um die Muster kümmern wir uns später nochmal und wählen jetzt erstmal spezifisch die Datei. Wir lassen auch den Rest auf den Standardeinstellungen und bestätigen.

3. Im Files-Feld verschwindet nun die Datei .Rhistory und die Datei .gitignore wird angezeigt. Wenn wir die Datei .gitignore mit Linksklick anwählen, sehen wir in den Changes auch ihren Inhalt. Dort werden sowohl .gitignore als auch .Rhistory angezeigt. (Anmerkung: Falls die neue Datei .gitignore nicht direkt angezeigt, kann es sein, dass sie “sich selbst ignoriert”. Um sie in diesem Fall zu sehen, wählen wir erst View und dann Show ignored Files. In der File Übersicht werden nun sowohl .gitignore als auch .Rhistory angezeigt.)

3.1 Der folgende Teil ist nur entscheidend, wenn die Datei nicht direkt angezeigt wurde - also als untracked unter Files erschienen ist. Wie bereits erwähnt bedeutet das nicht anzeigen, dass die Datei sich selbst ignoriert. Das heißt auch, dass Änderungen nicht getracked werden und sie auch nicht in einem push auf GitHub enthalten sein kann. Jede:r Teilnehmende am Projekt hätte demnach entweder eine eigene Version der Datei oder auch gar keine, wenn er:sie keine erstellt hat. Wir möchten die Datei jedoch ins Tracking mit aufnehmen. Daher machen wir einen Rechtsklick auf die Datei und klicken Open, wodurch sie in einem Texteditor geöffnet wird. Dort entfernen wir dann die Zeile, in der .gitignore genannt ist und speichern. Der Status der Datei ist nun nicht mehr “Ignored”, sondern “Untracked”.

Wir sollten außerdem die ignorierten Dateien über die Auswahl in View wieder verstecken, damit SmartGit übersichtlich bleibt.

  1. Abschließend wollen wir die .gitignore-Datei in einen Dommit packen und diesen auch direkt über Commit & Push mit GitHub synchronisieren. Damit ist die Erstellung der Datei abgeschlossen.

Die Liste von ignorierten Dateien kann stets geändert werden. Dabei beginnt jeder Name einer Datei in einer neuen Zeile. Wir haben schon angemerkt, dass man neben präzisen Dateinamen auch Muster ausschließen kann. Beispielsweise sind Datensätze teilweise sehr große Objekte oder enthalten Daten, die nicht online gestellt werden sollten. Trotzdem wollen wir sie meist am selben Ort wie die Auswertungsskripts haben. Eine Aufnahme in die Liste der ignorierten Dateien ist hierbei optimal. Nehmen wir an, die Daten heißen “a.RData” und “b.RData”. Nun könnte man natürlich einfach beide Namen in die .gitignore-Datei eintragen. Man kann aber auch den Stern * als Platzhalter nutzen.

*.RData

Somit werden alle Dateien mit dieser Endung ignoriert. Nehmen wir an, dass wir eine weitere Datei “c.RData” haben, die keine persönlichen Informationen enthält und auch nicht zu groß ist. Diese würde nun auch ignoriert werden, obwohl sie es nicht soll. Ausnahmen in .gitignore können über ein Ausrufezeichen gesteuert werden !.

!c.RData

Nun wird nur die eine Daten-Datei getracked. Manchmal gibt es in einem Repository auch Unterordner, die alle Dateien erhalten, die für das Tracking nicht wichtig sind. Diese können dann gemeinsam über den zugehörigen Ordnernamen ausgeschlossen werden.

Ordnername/*

.gitignore ist also hilfreich zum Ordnung halten und kann gleichzeitig persönliche Daten vor einem Upload schützen.

Conflict Solver

Der Conflict Solver kommt - wie der Name schon verrät - bei der Entstehung eines Konfliktes zum Einsatz. Diese können bspw. entstehen, wenn zwei Personen diesselbe(n) Zeile(n) derselben Datei(en) ändern oder eine Person die Datei löscht, während ein andere diese ändert.

Für Git ist natürlich nicht direkt klar, welche der beiden Änderungen nun richtig ist. Dies wird durch das Anzeigen eines Konfliktes gelöst. Im Kontext der Arbeit mit GitHub muss die Person, die als zweites Änderungen an einer Datei pushen will, den Konflikt lösen. Dies ist generell erstmal logisch, da bei den ersten Änderungen natürlich kein Konflikt auftritt und Git nicht in die Zukunft schauen kann. Mit SmartGit wird das Lösen eines Konfliktes zum Glück erleichtert - wir wollen dies an einem Beispiel betrachten.

Konflikt erstellen

Um einen Konflikt lösen zu können, muss man natürlich erstmal einen erstellen. Dafür braucht ihr ein lokales Git-Repository, das mit einem Remote-Repository auf GitHub verbunden ist. Wir werden jetzt einen Konflikt in der Datei “Datenauswertung” erzeugen. Das funktioniert wie folgt:

  1. Unsere Datei “Datenauswertung” existiert lokal und auf GitHub. Natürlich könntet ihr auch eine neue Datei erstellen, aber in diesem Fall bleiben wir bei unserem Beispiel. Wichtig ist nur, dass die Datei lokal und auf GitHub auf demselben Stand vorliegt.

  2. Geht auf GitHub und ändert etwas an der Datei (hier in Zeile 21) und speichert diese Änderungen. Wir gehen jetzt gedanklich davon aus, dass eine andere Person im Team, einen shapiro.test() machen möchte und diesen auch schon remote hinterlegt hat (es muss von uns also auch ein Commit auf GitHub durchgeführt werden um diese Situation zu simulieren).

  1. Jetzt ändern wir lokal etwas in der Datei (auch in Zeile 21). Ihr selbst wollt einen t.test() mit den Daten durchführen.

4. Öffnet SmartGit und führt nur einen Commit durch, aber noch keinen Push.

5. Versucht jetzt einen Push durchzuführen. Es müsste euch folgende Warnung angezeigt werden:


SmartGit sagt uns, dass wir zuerst einen Pull durchführen sollen (hint: to the same ref. You may want to first integrate the remote changes). Wenn ihr das jetzt macht, ist ein Konflikt entstanden.

Konflikt Lösen

Nachdem SmartGit den Konflikt in der Datei erkannt hat und ihr diese auswählt, wird es euch in den Changes unten eine Leiste zur möglichen Konfliktlösung anzeigen.

Die einfachsten zwei Optionen zwischen denen man wählen kann, um den Konflikt zu lösen sind: Take Ours oder Take Theirs. Dabei entscheidet man sich entweder komplett für seine eigene Version oder die der andere Person.

Das ist natürlich problematisch, wenn man sowohl die Eigenen als auch die des Anderen behalten will. Hier kommt jetzt der Conflict Solver ins Spiel. Den Button dazu findet ihr in dem Bereich unten rechts, dort wo ihr auch schon die Optionen Take Ours und Take Theirs aufgefunden habt.

Nachdem ihr den Conflict Solver gestartet habt, öffnet sich ein weiteres Fenster. Links sieht man unsere eigenen Veränderungen, rechts die der anderen Person und in der Mitte eine mögliche Lösung des Konflikts.

Hier werden beide Veränderungen zusammen in der Datei angezeigt. Falls noch nicht genau ersichtlich ist, wer welche Veränderungen getätigt hat, geht man links oben auf Base Changes. Damit erhält man bei großen Veränderungen einen besseren Überblick. Bei unserem Beispiel ist das nicht nötig.

Wir wollen jetzt eine Version der Datei, die ausschließlich den shapiro.test() enthält, weil wir das für angemessen halten. Dafür klicken wir auf den Doppelpfeil neben der Zeile, wodurch diese Line in die Mitte übernommen wird.

Die Datei in der Mitte sieht jetzt so aus, wie wir sie haben wollten. Also gehen wir auf Save und schließen dann den Conflict Solver. Dabei taucht folgende Meldung auf:

Hier klicken wir auf Mark Resolved, da wir den Konflikt gelöst haben. Man gelangt wieder auf die übliche Oberfläche von SmartGit und sieht, dass die Datei, die wir resolved haben, nun als staged angezeigt wird (zur Erinnerung aus dem Git-Intro: Stage bereitet die Datei für einen Commit vor, lädt sie also ins Staging Environment).

Wichtig ist also, dass wir jetzt noch einen Commit und Push durchführen, um die Datei mit gelöstem Konflikt auch auf GitHub zu haben. Ansonsten droht euch ein größerer Konflikt, wenn jemand anderes noch weiter an derselben Datei arbeitet. Hierbei handelt es sich um einen besonderen Commit, da ein Merge durchgeführt wird. Was das genau bedeutet, wird erst in einem späteren Tutorial behandelt. Wir stellen auf jeden Fall fest, dass bereits eine Message im Fenster geschrieben ist, die die gelösten Konflikte aufführt (bei uns also die Datei Datenauswertung.R).

Nach dem Push wurde der Konflikt erfolgreich gelöst.

Fazit und Ausblick

Wie ihr sicherlich gesehen habt, sind Git und SmartGit umfangreicher als es auf den ersten Blick scheint. Denn auch dieses Kapitel hat das ganze Ausmaß nur angerissen. Falls ihr euch mal einen Überblick über alle Funktionen von Git schaffen wollt, schaut in die offizielle Git-Dokumentation rein. Wir werden im nächsten Tutorial auf die Nutzung von Branches eingehen.


Appendix A

Discard - Terminal

Ihr wechselt im Terminal auf euer Git-Repository. Mit git status könnt ihr euch die Änderungen in eurem Repository anschauen. Wir gehen hier wieder davon aus, dass wir in der Datenauswertung einfach eine Zeile hinzugefügt haben.

Wie ihr auf dem Bild sehen könnt, zeigt uns das Terminal auch mit “modified” an, dass etwas an unserer Datei geändert wurde.

Hier gibt es jetzt dieselben zwei Optionen, wie bei SmartGit.

und dann

ist genau dasselbe wie Discard in SmartGit. Eure Änderungen werden permanent gelöscht und ihr kehrt wieder auf den Stand des letzten commit zurück. Der erste Befehl löscht eure Git-Historie und der zweite “säubert” euren Working Tree, indem er Dateien löscht, die sich momentan nicht unter Versionskontrolle befinden.

  1. git stash: entspricht Discard to Stash. Eure Veränderungen werden also erstmal einem “Papierkorb” hinzugefügt und nicht direkt gelöscht.

Revert - Terminal

Hierzu öffnen wir über das Terminal unser Git-Repository und führen den Befehl git revert HEAD mit “Strg” + Entertaste aus. Danach gibt euch Git folgendes aus:

Git hat also einen neuen Commit erstellt, der wieder den Stand vor dem ungewollten Commit beinhaltet.

Im Log wird das Ganze vielleicht noch etwas deutlicher:

Deswegen heißt der neuste Commit auch: Revert “Add ungewollte Änderung”.


Reset - Terminal

Wenn ihr den Befehl $git reset --hard [Commit] in eurem Git-Repository ausführt, wird der Verlauf auf diesen ausgewählten Commit zurückgesetzt. Im Gegensatz zu oben bei git discard stehen hier vor dem hard zwei “- -”. Das liegt daran, dass die “-” - Option nur den Index im Working Directory ersetzt und die “- -”- Option direkt zum ausgewählten Commit springt. Die ungewollten Commits kann man im Log nicht mehr sehen (wie es oben der Fall ist). Das Log könnt ihr euch mit git log anzeigen lassen, mit “q” (quit) könnt ihr es wieder verlassen. es sieht so aus:

Hier könnt ihr euch einen Überblick über eure getätigten Commits verschaffen. Wie ihr seht, sind die zwei obersten Einträge unsere “Ungewollte Änderung” und der revert davon. Das kann irgendwann unübersichtlich werden und euer Log sehr lang machen. Deswegen wollen wir jetzt hier einen reset durchführen.

Ihr nehmt den Befehl von oben und setzt in die Klammer die “Nummer” des Commits ein auf den ihr zurückkehren wollt, hier “Add Header Tutorial”. Diese “Nummer” erfahrt ihr über das Log unter This reverts commit und sieht so aus: a6e1f5ecf72a9ff3c13060567875331f1da0822e. Wenn ihr nach dem Ausführen des Befehls nun wieder in euer Log schaut, sind die letzten zwei Commits verschwunden und der oberste Commit ist “Add Header Tutorial”.