-
Notifications
You must be signed in to change notification settings - Fork 3
🧙♂️ Git Strategy
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.
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.
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.
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.
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.
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.
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
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.
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.
Deze branches kun je vervolgens weer samenvoegen door de git merge
command. Je hebt 3 verschillende soorten merges:
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".
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.
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.
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.
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.
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.
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.
Ditzelfde principe werkt ook tussen twee commits. Door i.p.v de branch namen de commit hashes in te voeren.
[Git Kraken] VS [CLI]
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.
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.
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!
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
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.
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?
Als back-up en om goed samen te werken wordt er vrijwel altijd gebruik gemaakt van Remote Repositories zoals deze op Github.
git clone <repo>
is een commando die je gebruikt om een repository van het internet te downloaden naar jouw eigen machine.
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
.
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.
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 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,=.
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.
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.
Éé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.
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 wordt gebruikt voor twee doeleinden: Als een alternatief voor merging én voor het opschonen van je repository.
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.
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.
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.
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. git tag -a <name>
Die metadata bevatten, net als een commit: Auteur, email, de datum, en een tagging message.
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 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!
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>
.