Skip to content

🧙‍♂️ Git Strategy

Melvin Idema edited this page Mar 1, 2022 · 22 revisions

Niet voor niets heeft deze pagina een tovenaartje 🧙‍♂️ als emoji. Git kan namelijk nog wel eens aanvoelen als magie. In ieder geval voor mij. Zeker tijdens het lezen van de Git Best Practices kwam ik erachter dat Git meer is dan commits, branches en repositories. Diffing, stashing, rebasing, squashing en tags. Ik had werkelijk geen flauw benul.

Uitgelezen kans dus om eens wat dieper in Git te duiken. Aan de hand van een Udemy course: Git & Github Bootcamp en als mooie bron erbij Git from the bottom up.

Wat is Git eigenlijk?

Git is, zoals het zichzelf noemt: een fast, scalable, distributed revision control system. Oorspronkelijk geschreven door Linus Torvalds in amper een maandje tijd nadat de uber nerds flinke ruzie kregen over de Source Control Management (SCM) system die zij gebruikten voor het onderhouden van de Linux Kernel. BitKeeper, de SCM die zij toen ter tijd gebruikte, trok zijn gratis licentie terug na beschuldigen van het reverse engineeren van de code door Andrew Tridgell. [1]

Als echte programmeur stroopte Linus Torvalds daarom zijn mouwen op om eens flink te gaan typen. Na een maand op zijn - waarschijnlijk staandesk - te hebben gewerkt was daar dan Git. En de programmeurs waren blij. En alles was goed. Amper een paar maanden later gaf Linus zijn project door aan Junio Hamano die, tot op de dag van vandaag, nog steeds een belangrijke rol speelt in de development van Git.

Oke, maar wat is Git eigenlijk?

Met Git kun je dus revisies beheren in een zogehete repository. Een database van veranderingen. Git slaat niet het verschil van bestanden op; maar slaat voor elke aanpassing die gecommit wordt een hele nieuwe versie van het bestand op in de .git/objects map. Dat wordt ook wel een blob genoemd. Die blob wordt vervolgens gehashed met sha1 tot een unieke 40 karakter tellende tekenreeks. Deze tekenreeks kun je zelf maken met het git hash-object command. Probeer maar eens: echo "git" | git hash-object --stdin. Je krijgt dan de tekenreeks: 5664e303b5dc2e9ef8e14a0845d9486ec1920afd. Je creëert namelijk een unieke tekenreeks voor het woord "git".

Deze blob wordt vervolgens in een tree geplaatst. Een tree is eigenlijk heel simpel: Het is een lijst met bestanden en andere tree's. Eigenlijk precies zoals je map op een computer. Die bevat ook bestanden en andere mappen.

blob 5664e303b5dc2e9ef8e14a0845d9486ec1920afd git.txt
tree 3b18e512dba79e4c8300dd08aeb37f8e728b8dad css/

En deze tree, wordt weer gekoppeld aan een commit. Die dus eigenlijk niets meer is dan een verwijzing naar een tree met weer verwijzingen naar blobs. Maar dan met een stukje tekst erbij. Én, een verwijzing naar een vorige commit! Zo kan Git makkelijk de geschiedenis van commits uitvogelen. Want elke commit verwijst naar een vorige commit. En zo niet, dan is dat de allereerste commit.

En dan heb je nog branches

Hoe weet Git dan welke commit de meest recentste is? Daar komt branching bij kijken. Dat is een heel simpel bestandje met een verwijzing naar zo'n eerdergenoemde hash. Dat bestandje wordt opgeslagen in .git/refs/heads/ en die kun jij weer aanroepen met git switch <branchnaam>. Zo heb je eigenlijk een verwijzing naar een verwijzing naar een verwijzing naar een blob.

En dat is waarom Git soms ook wel "light-weight" genoemd wordt. En waarom Git eigenlijk rete simpel blijkt. Maar moeilijk lijkt door al die tientallen commands en vage termen als rebase, squashing, stashing etc.

Typische Git Workflow

Een typische Git workflow bestaat uit de 4 voornaamste commando's:

git status

Laat zien welke bestanden gewijzigd, verwijderd, toegevoegd of untracked zijn.

git add <filename>

Voegt bestanden toe aan de staging sectie van Git om later te committen.

git commit [-a] -m <short message in present tense>

Zet een checkpoint en voegt de commit toe aan de repository. [-a] kan gebruikt worden om de add stap over te slaan.

git log

Laat alle gedane commits zien.

Atomic Commits

Aangezien Git al meer dan 15 jaar bestaat en wij programmeurs standaarden fantastisch vinden ontkomt ook de git commit niet aan deze standaarddrang.

Zo moet een commit zich focussen op maar 1 ding: Een bug, een feature of een fix. Zodat, wanneer je bijvoorbeeld terug moet naar een commit of een commit moet ongedaan maken je niet allerlei randzaken daarbij ook ongedaan maakt.

De tegenwoordige tijd

Daarnaast is er veel discussie over het gebruik van de tegenwoordige tijd vs de verleden tijd. Make bar do foo of Made bar do foo. Git zelf gebruikt de tegenwoordige tijd; maar er zijn ook goede argumenten voor de verleden tijd. Zoals in dit artikel. Waarin de auteur pleit voor het gebruik van de verleden tijd. Aangezien Git zelf de tegenwoordige tijd gebruikt ga ik die ook gebruiken.

Change 🧙‍♂️ Git Strategy wiki page

Samenvatting Git and Github Bootcamp

Whoops, you've made a mistake

Door --amend (letterlijke vertaling: wijzigen) kun je de vorige commit wijzigen. Dit is handig bij het vergeten van een bestand of als er nog een kleine fix in je feature nodig was.

Branches

Met Git heb je toegang tot een 'branches' functionaliteit waarin je kunt aftakken van je codebase om features te maken, bugs op te lossen of experimenten te doen. De master of main branch wordt vaak gebruikt als de source of truth waarin de code perfect is.

Mergen

Deze branches kun je vervolgens weer samenvoegen door de git merge command. Je hebt 3 verschillende soorten merges:

Fast-Forward

Wanneer er niets nieuws gecommit is in de branch waar je naartoe merged, dan loopt deze als het ware gewoon achter op de branch die gemerged wordt. Git kan daarom gewoon de HEAD verplaatsen naar de laatste commit van de branch die gemerged wordt. Het "spoelt vooruit".

Merge Commit

Wanneer er wel iets gecommit is in zowel de branch waar je naartoe merged als de branch die gemerged wordt; maar geen aanpassingen die in conflict zijn met elkaar. Maakt Git een nieuwe commit voor je aan die wijst naar de laatste commit van beide branches.

Merge Conflicts

Wanneer in beide branches hetzelfde bestand is gewijzigd krijg je een merge conflict. Git kan niet voor jou bepalen wat hij moet houden en wat hij moet verwijderen. Daarom wordt je gevraagd dit handmatig te doen. In het conflicterende bestand staan zogenoemde markers

<<<<<<< HEAD:file.txt
Hello world
=======
Goodbye
>>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt

Bron: https://stackoverflow.com/questions/7901864/git-conflict-markers

Waarin de head de current branch is en het onderste de branch waarmee je probeert te mergen. Na het verwijderen van de markers kun je een nieuwe commit plaatsen om de merge te voltooien.

Diffing

git diff <filename> is ook weer zo'n command waar ik nog nooit van gehoord had. Maar echt ontzettend handig is. Met diff - voor difference - kun je namelijk het verschil zien tussen het bestand in je working directory (unstaged) en de vorige versie van een bestand.

git diff HEAD

HEAD is een optie die je mee kunt geven aan de diff command en laat alle veranderingen, staged of unstaged, zien vanaf de laatste commit.

git diff --staged

Waar HEAD alle veranderingen laat zien, staged of unstaged, laat je met de --staged optie alleen de veranderingen zien in de staged files. Dus niet die in je working directory.

Veranderingen tussen twee branches

Wat ook belachelijk handig is is het verschil zien tussen twee branches. Dit doe je simpel door git diff branch1..branch2 als command in te voeren. Vervolgens laat git alle verschillen zien tussen de twee branches.

Veranderingen tussen twee commits

Ditzelfde principe werkt ook tussen twee commits. Door i.p.v de branch namen de commit hashes in te voeren.

CLI vs. GUI

[Git Kraken] VS [CLI]

Stashing

Terwijl je bezig bent met een bepaalde feature, bugfix of experiment kan het nog wel eens voorkomen dat je nog niet klaar bent om te committen; maar wel iets anders moet gaan doen. Bijvoorbeeld naar een andere branch toe om wat te fixen. Of snel een kleine commit maken. Je kunt dan je unstaged én staged veranderingen opslaan in de stash. Door middel van: git stash sla je de veranderingen op en draait Git de veranderingen terug naar de laatste commit. Wanneer je klaar bent en weer je huidige werk terug wilt. Kun je git stash pop doen.

Time Travelling & Undoing Changes

Time Travelling

Wat Git natuurlijk zo'n mooi stukje software maakt is dat je al die checkpoints krijgt. Je kunt precies, door de tijd heen, zien welke veranderingen gedaan zijn en wanneer. En je kunt ook "terugspoelen" naar die veranderingen. Het enige wat je nodig hebt is de eerste 7 cijfers van de hash van de commit waar je naar toe wilt. Die je kunt vinden met git log --oneline. Of gewoon git log natuurlijk.

Vervolgens kun je, net als bij een branch, switchen naar die commit met de git checkout command.

Undoing Changes

Je kunt je complete working directory switchen naar een commit of een branch. Maar ook een specifiek bestand. Met diezelfde git checkout command. Als je bijvoorbeeld aanpassingen gedaan hebt aan je server.js en perongeluk er een enorm zooitje van gemaakt hebt. Geen paniek! git checkout HEAD server.js draait alle veranderingen terug naar de laatste commit. HEAD verwijst hierbij naar de laatste commit. Daar waar de HEAD naartoe wijst. Maar dit zou ook een branch kunnen zijn of een commit hash. Best handig dus!

Nieuwe Command

Git Restore

Git heeft recent een nieuwe command toegevoegd: git restore. Dit is hetzelfde als git checkout HEAD maar dan simpeler. Als je vanaf een commit of branch wilt restoren. Gebruik je de --source optie. git restore --source HEAD~1 server.js

Git Reset

Stel je voor. Je bent keihard bezig geweest en hebt netjes al je veranderingen gecommit. Met paniekerige ogen kijk je naar de terminal: "Shit! 😱 Ik zit in de production branch.". Daar hebben de slimme mensen bij Git ook aan gedacht. git reset <commit>. Hiermee verwijder je alle commits tot die bepaalde commit. Waarbij je werk wel opgeslagen blijft! Die zijn namelijk nog steeds in je working directory. Wil je dit niet en wil je echt alle veranderingen kwijt? Dan kun je de --hard optie gebruiken.

Maar mijn collega's dan?

Het probleem bij git reset is dat git doet alsof de commits niet gemaakt zijn. Dit kan problemen opleveren als je in een team werkt. En er ineens commits verdwenen zijn. Maar niet getreurd, git revert to the rescue! Die maakt namelijk een nieuwe commit waarin de veranderingen van een andere commit ongedaan gemaakt worden. Snap jij het nog?

Remote Repositories

Als back-up en om goed samen te werken wordt er vrijwel altijd gebruik gemaakt van Remote Repositories zoals deze op Github.

Git Clone

git clone <repo> is een commando die je gebruikt om een repository van het internet te downloaden naar jouw eigen machine.

Git Push

git push <remote> <branch> upload je git repository naar waar die op het internet opgeslagen staat. Zoals op Github. Waar <remote> de bijnaam van je URL is en de branch die je wilt pushen. <branch> hoeft in dit geval niet altijd dezelfde te zijn als op je lokale machine. door met een : te werken kun je andere namen gebruiken. Bijvoorbeeld je lokale experimental-feature naar je remote cool-feature branch. experimental-feature:cool-feature.

Shortcut

Je kunt ook de upstream van je branch zetten met de -u optie. git push -u origin master. Hiermee verbind je de lokale master branch met de remote master branch. Waarna je gewoon git push kunt gebruiken.

Fetching & Pulling

git fetch haalt de veranderingen van de remote repository op maar integreert deze niet in je working directory. Dus je lokale repository weet er vanaf; maar de veranderingen zijn niet toegepast.

Dit in tegenstelling tot de git pull command. Deze update de HEAD branch, of te wel de working directory. Het is eigenlijk git fetch + git merge.

Voordat je pushed

Voordat je pusht naar een remote repository is het verstandig om altijd eerst te pullen. Zo weet je of er conflicts ontstaan en kun je die gelijk oplossen,=.

Git(hub) Workflows

The Centralized Workflow

The Centralized Workflow is een manier van (samen)werken waarbij iedereen in het team op dezelfde branch werkt. De meest basale workflow mogelijk. Het fijne aan deze workflow is dat het heel simpel is; je werkt immers maar op één branch. Het nadeel is dat je een hoop tijd kwijt bent aan het oplossen van merge conflicts en je geen code kunt pushen tenzij het werkend is. Anders sloop je de hele codebase. Dus een teamgenoot laten kijken naar je code kan eigenlijk niet. Eigenlijk alleen mogelijk als je team heel klein is.

To the rescue: Feature Branches

Niemand werkt op de default - master / main - branch. Maar werkt op een aparte feature branch. Bouw je een input component? input-component branch. Werk je aan een login feature? login branch! Hierbij is de main branch vrijgesteld van kapotte code en je kunt goed samenwerken met je team op meerdere features.

Pull Requests

Één van de dingen die helpen bij de feature branches workflow zijn pull requests. Een methode waarbij je andere developers op de hoogte stelt van je nieuwe code. Je kunt er vervolgens over discussiëren, de code kan nog veranderd worden en vervolgens kan de feature branch gemerged worden in de main branch. Dit is geen feature van Git maar van tools als Github en BitBucket.

Fork & Clone Workflow

In plaats van één gecentraliseerde repository op Github; heeft elke developer een eigen repository op Github. Developers maken aanpassingen in hun eigen repository en pushen hun forks naar de main repo door middel van pull requests. Deze workflow wordt vaak gebruikt in hele grote open-source projecten zoals React. Ook Forking is geen git feature, maar van Github zelf om samenwerking te faciliteren.

Rebasing

Rebasing wordt gebruikt voor twee doeleinden: Als een alternatief voor merging én voor het opschonen van je repository.

Merging: Het verschil tussen git rebase en git merge

Stel dat je werkt aan een zeer actief project waar meerdere keren per dag commits verschijnen op de main branch terwijl jij werkt aan een feature op een andere branch. Elke keer dat je merged, ontstaan er op jouw branch nieuwe merge commits die je commit geschiedenis vervuilen. Rebasing, in plaats van merging, herschrijft de geschiedenis en propt je commits vooraan de commits van een andere branch. Je hebt nog steeds jouw commits met jouw aanpassingen, maar nu in een lineaire commit geschiedenis. master-1, master-2, master-3, feature-1, feature-2, feature-3. I.P.V: master-1, feature-1-merge, master-2, feature-2-merge, master-3, feature-3

Eigenlijk worden je commits automatisch opnieuw gedaan door Git vanaf het eind van de andere branch. Dit herschrijft geschiedenis; dus zolang niemand anders de oude commits heeft kun je dit gebruiken. Anders wordt het een nachtmerrie.

Opschonen: Interactive Rebasing

Maar het herschrijven van je git geschiedenis hoeft niet alleen te gebeuren bij het mergen van twee branches. Je kunt namelijk met de -i optie ook commits herschrijven, bestanden toevoegen of commits verwijderen. Mocht je dus vreselijke commits geschreven hebben, of je strategie aanpassen in de toekomst. Kun je terug in de tijd gaan om je commits te herschrijven. zolang niemand anders deze commits op hun lokale repository hebben staan.

Git Tags

Een tag is een verwijzer, een soort sticky note, die verwijst naar een commit. Vaak gebruikt voor versie releases. Zoals bijvoorbeeld v3.0.0. Je markeert belangrijke momenten in je Git history. Je hebt twee soorten tags: lighweight tags en annotated tags.

Lightweight Tags

git tag <name> Dit soort tags zijn, zoals de naam al doet vermoeden, lightweight. Ze bevatten alleen een naam en een verwijzing. In tegenstelling tot...

Annotated tags

...Annotated Tags. git tag -a <name> Die metadata bevatten, net als een commit: Auteur, email, de datum, en een tagging message.

Versioning

Git tags worden het meest gebruikt voor versioning. Semantic Versioning om precies te zijn. Een standaard protocol gebruikt in heel veel repositories om versies aan te geven. Meer hierover lees je in dit artikel. In het kort volgt het ...

Waarin PATCH een kleine bug fix of andere veranderingen aangeeft die geen impact hebben op hoe de code gebruikt wordt.

MINOR daar en tegen beduid een nieuwe feature of functionaliteit die toegevoegd is. Er is iets toegevoegd, maar het breekt niets aan de oude code. Een gebruiker hoeft dus niks te veranderen aan haar code. De PATCH wordt weer teruggezet naar 0.

MAJOR releases zijn voor significante veranderingen die de code kunnen breken. Features zijn verwijderd of substantieel veranderd.

Reflogs

Reflogs staat voor Reference Logs. Logs die Git bijhoudt wanneer References worden geüpdate. Dus wanneer een tag, de HEAD of een branch veranderd. Bijvoorbeeld bij een nieuwe commit en het switchen van branches.

Je kunt dit doen door eerst git reflog show <ref> te doen. Waarbij je de log in je terminal te zien krijgt. En vervolgens git reflow <ref>@{x} waarbij je dan een aantal keer terug kan gaan. Stel je voor dat je een commit doet. Switched van branch. Nog een commit. Weer switched van branch en vervolgens terug wilt naar je eerste commit. Dan zou je git checkout HEAD@{1} kunnen doen. Wil je bijvoorbeeld het verschil tussen de eerste en tweede commit zien. Dan kun je dit met een diff doen: git diff HEAD@{1} HEAD{3}.

Echt handig wordt het bij het gebruik van tijd. Je kunt namelijk letterlijk tijdreizen! Je kunt git checkout master${1.week.ago} doen om een week terug in de tijd te gaan en je werk toen ter tijd te bekijken!

Verdwenen Commits

Stel je voor dat je een aantal commits gedaan hebt en toch niet blij bent met je aanpassingen. Gelukkig hebben we geleerd over de git reset command. Je doet git log en pakt een commit waarbij alles nog goed was. Je doet vervolgens git reset --hard 93e33cf en gaat terug naar die commit.

Als je je nu toch weer bedenkt. Is die commit foetsie! git log geeft namelijk die commit niet meer. Gelukkig hebben we nu dus git reflog show om de verdwenen commit te vinden. En vervolgens git reset --hard <verdwenen commit>.

Certificaat