Time to read: 9 min read

Git Ultimate Short

# Disclaimer

Dieses Schreiben ist das Werk von Martin Tinich, ich habe es lediglich von ungarisch auf deutsch übersetzt.

# Einführung

Git ist ein Version Tracker. Meistens clonen wir ein bestehendes Repository mit git clone <url>

Lokal können wir einen Ordner mit git init anfangen zu verfolgen. beides kommt mit einem versteckten .git Ordner, den wir aber nie anfassen müssen. Name und Email werden beim commit verlangt, damit man weiß wen man blamieren muss.

# Commit

Es gibt 3 wichtige Orte im Konzept:

  • Working Directory (WD)
  • Staging Area (SA)
  • Repository (REPO)

WD ist der Ordner in dem ich arbeite. Manuell würde man Backups folgender weise machen: Ordner zippen, und irgendwo verstauchen. Ich werde diese Analogie benutzen, aber im Hintergrund ist git viel schlauer. Nach dem ersten commit merkt er sich lediglich Deltas (zeilenweise Änderungen), deshalb brauchen alle späteren Backups kaum Speicherplatz über der WD.

Git sieht welche Files ich geändert habe, und listet sie mir durch git status. Dies differenziert auch zwischen staged und unstaged Files, zeigt sie in unterschiedlichen farben. Die angezeigten Änderungen werden relativ zum letzten commit gezeigt.

Wenn ich 3 Files im Ordner habe, aber mit dem dritten noch nicht fertig bin, trotzdem bereits einen Backup erstellen möchte, schiebe ich nur die ersten zwei Files in die SA mit git add <filename>. Wenn beide Files im SA sind, kann ich committen: git commit -m "message". Nach dem Commit macht git einen zip wo die zwei Files neu sind, und das dritte noch vom älteren Backup stammt. Manchmal merkt man, dass nach einem commit noch Rechtschreibfehler drin sind, dann kann man die kleinere Änderungen durch git commit --amend dem letzten commit dranhängen, sodass kein neues commit mit neuer message erstellt wird.

Häufige Probleme: "Ich möchte ein file nicht tracken, hab es im gitignore drin, es wird aber trotzdem verfolgt. Ich habe die .gitignore Datei auch committet" Wenn ein file irgendwann schon mal verfolgt wurde, und man es nachträglich ins gitignore schreibt, wird es nicht ignoriert. Man muss die Verfolgung der Datei stoppen mit git rm <file> --cached. Die Datei selbst wird im WD nicht gelöscht, aber sie wird vom REPO entfernt, und von da an nichtmehr verfolgt.

"Okay, aber ich will die Datei beibehalten, bloß meine lokale Änderungen nicht mitnehmen" git update-index --assume-unchanged <file>

"Ich habe etwas committet, habe aber etwas vermasselt und möchte den commit löschen" Commits löschen wir nie. Wenn möglich, reparieren wir den Fehler und committen erneut. Wenn es um zu viele falsche Files geht, dann können wir die letzten Commits mit git log ansehen, und mit git revert <commit> zu einem früheren commit zurückkehren. Hierbei wird ein neuer commit erstellt, der eine neue message erwartet, dies kann man mit git revert --no-edit <commit> umgehen. Wenn ich noch vor dem commit etwas ändern möchte, dann git revert --no-commit <commit>, Änderungen anlegen, und danach git commit...

# Branching

Manchmal möchte ich meine Änderungen in einen eigenen backup Ordner schieben, ich will aber nicht dass andere darauf ungewollt zugreifen. Da mach ich halt einen Branch mit git checkout -b <branch_name>. Um zu checken in welchem branch ich bin, einfach git branch. Als Ausgang für einen Branch kann jeder commit dienen. Um auf ein bereits existierendes Branch zu wechseln einfach git checkout <branch_name>. Dabei kommt man auf das absolut neueste commit von dem angegebenen Branch.

Beispiel: Ich hab meinen Armin Branch, und Angelina Jolie hat ihr Angelina Branch. Sie arbeitet an einer feature, und speichert alles schön, macht regelmäßig Commits, und ich arbeite an miener, und mache regelmäßig Commits. Nun brauch ich ihre Hilfe. Sie macht einen checkout auf meinen Branch, und plötzlich, wie Magie, ändern sich alle Files in ihrem WD auf die die in meinem letzten commit drin waren. Sie macht ihr Ding, commitet (und pusht), und checkt wieder ihren eigenen branch aus um da weiter zu machen. Ich mach einen git pull (dazu später), et voilá, ich habe alle ihre Änderungen nun in meiner WD.

Hier kommt HEAD ins gespräch: HEAD ist immer das allerneueste commit in dem jeweiligen Branch wo ich gerade bin. Dies ist auch unter .git/HEAD in einer Variable gespeichert. Man kann diese Variable auf was anderes umstellen, dann spricht man von einem DETACHED HEAD. Das macht man indem man "checkout" nicht auf einen Branch, sondern auf ein Commit durchführt.

Im Beispiel von vorhin: Wenn Angelina nicht meinen Branch auscheckt, sondern meinen letzten commit, dann kann sie mir nicht weiterhelfen. Sie ist dann in einem Detached Head. Sobald sie einen Branch auscheckt, sodass sie einen aktiven Branch hat, wechseln sich all ihre Files in der WD auf die von dem Branch, und ihre Änderungen gehen verloren.

Kurz und gut: ein detached head heißt es gibt gerade keinen aktiven Branch, die WD representiert nicht den neuesten Stand von einem Branch. Einfach einen branch auschecken, und erst dann anfangen Dinge zu ändern.

Man kann Branches auch aufeinander resetten mit git reset --hard <branch>. Bitte nur mit Obacht, es können wesentliche Unterschiede im Code verloren gehen.

# Merge und Rebase

Sagen wir mal ich hab meinen Feature Branch, da Dinge geändert, in der zwischenzweit ist der Master branch auch vorangekommen, und ich möchte nun meine Feature in den Master einbauen.

1 -> 2 -> 3 -> 5  Master 
     |
     ˇ--> 4 -> 6  Feature

Es gibt zwei Möglichkeiten:

  • Den Master branch auschecken und den Feature Branch reinmergen.
  • Den Feature branch "rebasen", und zwar auf das neueste commit vom Master Branch.

Beim merge schaut sich git an ob es Konflikte gibt (Wenn die selbe Zeile in beiden Branches unterschiedlich geändert wurde). Merge Conflicts müssen gelöst werden, dazu gibt es tolle tools, wie "git tree compare" in VSCode. Wenn es keine conflicts (mehr) gibt, wird ein neues commit erzeugt, das von sowohl dem Master als auch dem Feature Branch abstammt. Im Graph waren die Branches abgezweigt, und nun sind sie zusammengewachsen und eins geworden.

git checkout master

git merge <branch>

1 -> 2 -> 3 -> 5 ------> 7  Master 
     |              ^  
     ˇ--> 4 -> 6 ---|       Feature

Beim rebase werden die Zweige straffgezogen, sodass es am ende nur noch eins gibt. Wir sagen dem Master brach, dass er und seine neue Commits sich nicht auf das alte abgezweigte commit basieren soll, sondern dass sich diese Commits lieber auf die Spitze von meinem Feature Branch draufbauen sollen. Kein Beispiel, da ausdrücklich verboten.

1 -> 2 -> 4 -> 6 -> 3 -> 5 -> 7  Master

Hier sieht man was ein Rebase macht, aber rebase ist ABSOLUT VERBOTEN. Den master, den wir mit anderen teilen möchten, den darf man NIE mit rebase anrühren. Alles was nicht ausschließlich bei uns lokal bleibt, darf man NIE mit rebase anrühren. Die Probleme die man für andere erzeugt wenn sie gerade am master arbeiten, welches wir rebasen, und sie versuchen zu commiten und zu pushen.... sind legendär.

Okay, wozu dient dann ein rebase?

Es kann sein, dass meine Feature von einem commit von gestern abzweigt, dass es heute aber im master eine Funktion gibt die ich auch bräuchte, aber keine Lust habe manuell alles reinzukopieren. Ein anderer Fall ist es dass ich meine Feature gegen einen neuren Stand des masters testen möchte. In diesem Fall kann ich meine Feature auf den heutigen commit rebasen, sodass diese Funktion bei mir dann bereits drin ist. und meine Änderungen auch alle bleiben. Das geht leider nicht immer, oft muss man einige conflicts manuell lösen.

# Remote

Hier kommt github ins Spiel. Github dient dazu, dass unsere lokale Commits aufs internet kommen, sodass andere sie auschecken können. Synkronisation ist in diesen Fall nicht automatisch, sondern man muss sie manuell mit PUSH hochladen, und mit FETCH herunterladen. Beim PULL wird im Hintergrund eigentlich ein FETCH und ein MERGE durchgeführt. Der Grund dafür ist dass ich vielleicht auf dem gleichen Branch was geändert habe wie jemand anderes. Mein branch ist lokal abgezweigt von dem stand des remote branch, deswegen muss natürlich ein merge passieren, um alles auszubügeln.

Um dem lokalen git repo zu erklären zu welchem remote github remot er gehört, muss man git remote add origin <remote_url> ausführen. Die remote repository hört auf den namen "origin", dehalb beim push: git push -u origin master, bzw. git push -u origin <branch>

damit man sich nicht jedes Mal einzeln einloggen muss, kann man git config --global credential.helper store durchführen, sodass git sich während dem nächsten Login die Zugangsdaten speichert.

# Nützlich

Stash ist nützlich, wenn man noch nicht commiten möchte, aber auf ein anderes branch wechseln will. In dem Fall kann man die WD mit git stash einfach lokal in eine sichere Schublade verstecken, und demnächst mit git stash pop wieder hervorbringen. Git wird nicht zulassen dass ich einen anderen branch auschecke solange es ungespeicherte Änderungen in meiner WD gibt.

Wenn git installiert ist, sollte man als erstes name und email angeben:

git config --global user.name "name"

git config --global user.email "email"

configs auszulisten: git config --list

help: git help xyz z.B. git help commit

Um eine Repository zu erzeugen, damit git den Ordner anfängt zu verfolgen, geht man in den Ordner cd Ordner und initialisiert mit git init Um eine Basis zu haben, macht man den ersten commit, und dafür sagt man welche Files da drin sein sollen (welche in die staging area (SA) sollen). Wenn alles, dann git add ., danach git commit -m "message". Wenn sowieso alle Files mitgehen sollen, kann man die zwei steps zusammen ausführen mit git commit -am "message".

Die Commits auslisten: git log Commits auf author filtern: git log --author="name" Den graph aunzusehen: git log --graph

git status zeigt welche Files in der WD relativ zum letzten commit geändert wurden, und welche bereits in der SA sind.

git diff <file> zeigt die zeilenweise Unterschiede innerhalb des Files.

git rm <file> löscht die Datei, und stellt die Verwerfung auch in die SA, sodass die Datei auch im nächsten commit fehlen wird.

git mv <file> <file> dies ist die UNIX weise der Umbenennung. In Windows wird eine Umbenennung einer Datei als löschen der alten und Erzeugung der neuen behandelt.

git checkout -- <file> ladet die eine Datei von der repo herunter und wechselt sie in der WD aus.

git checkout <commit_id> -- <file> bringt die Datei von der angegebenen commit in die WD.

git reset HEAD <file> bringt eine Datei zurück von der SA in die WD. Das Gegenteil von git add <file>