sequenceDiagram
actor A as Dev A
participant S as SVN Server
actor B as Dev B
S->>A: svn checkout project1:main
S->>B: svn checkout project1:main
S->>B: svn log
S->>A: svn diff
A->>S: svn commit
A->>S: svn commit
S->>B: svn update
B->>S: svn commit
Les 2: GitHub, Branches en Mergen
Herhaling Les 1
git init |
Nieuwe Git repository (repo) aanmaken in huidige directory (folder). |
git config |
Git configureren.git config --global : Configuratie voor alle repos.git config --global user.name "Fabrice Devaux" : Instellen.git config get user.name : Toon ingestelde waarde.git config --list --show-origin : Overzicht ingestelde configuratie. |
git status |
Informatie over huidige staat van de directory/repo. Belangrijkste commando. Vertelt meestal wat je moet of kan doen. |
git add <filename> |
Bestand(en) stagen.git add . : Stage alle aangepaste en nieuwe bestanden. |
git commit |
Commit alles wat gestaged is. Maakt een permanente nieuwe “versie”.git commit -m "message" : Geeft direct een commit message mee.git commit -a : Commit rechtstreeks alle aangepaste bestanden. |
git rm <filename> |
Verwijdert bestand(en) terug uit Git.git rm --cached : Bestand bewaren in working directory (untracked). |
git log [filename] |
Commit geschiedenis bekijken. Van nieuw naar oud. filename optie.git log --oneline : Compacte output. Één lijn per commit.git log --stat : Statistieken over aanpassingen in de commit.git log -p : Ook volledige diff (patch) van elke commit. |
git restore <filename> |
Nog niet gestaged bestand herstellen (naar laatste commit versie).git restore --staged : Reeds gestagede veranderingen herstellen. |
git diff [filename] |
Toont aanpassingen t.o.v. laatste commit.git diff commit1..commit2 : Toont aanpassingen tussen commit 1 en 2. |
git show [commit_id] |
Toont alle informatie, inclusief diff, van één commit.git restore --staged : Reeds gestagede veranderingen herstellen. |
git blame [filename] |
Volledig bestand met laatste commit, datum en auteur op elke lijn. |
Gedistribueerd Versiebeheer
Of Distributed Version Control.
Vergelijking met Gecentraliseerd
Traditioneel waren versiebeheersystemen gecentraliseerd (bvb. Subversion of svn). Dit wil zeggen dat alle versiegegevens enkel op een centrale server worden bijgehouden. De server wordt gebruikt tijdens vrijwel elke bewerking die een gebruiker uitvoert.
Het grote voordeel is dat dit conceptueel heel eenvoudig is. Daartegenover staat de beperkte flexibiliteit en netwerkafhankelijkheid. Veranderingen kunnen enkel in versiebeheer komen op de server zelf. Bijgevolg zijn veranderingen ook onmiddellijk ‘zichtbaar’ voor andere gebruikers.
Bij een gedistribueerd systeem, zoals Git, krijgt elke gebruiker een volledige kopie van de repository (inclusief alle commit historiek, ‘branches’, metadata, enz.). We gebruiken dan ook de term clone i.p.v. checkout. Eenmaal lokaal gekloond gebeurt vrijwel elke bewerking nu lokaal.
sequenceDiagram
actor A as Dev A
participant S as Git Server
actor B as Dev B
S->>A: git clone project1
S->>B: git clone project1
B->>B: git log
A->>A: git diff
A->>A: git commit
A->>A: git commit
A->>S: git push
B->>B: git commit
S->>B: git pull
Het enorme voordeel hierbij is dat iedereen onafhankelijke en snel lokaal kan werken. De architectuur is ook extreem veilig omdat iedereen een volledige kopie van de repo heeft. Er is letterlijk geen verschil tussen de repo op de server en die op het systeem van een gebruiker.
Het voornaamste nadeel is dat deze aanpak een stuk complexer kan zijn om te begrijpen met een wat steilere leercurve als gevolg.
Wat je heel goed moet onthouden uit bovenstaand schema is dat er bij Git dus maar drie commandos zijn waarbij informatie wordt uitgewisseld met een Git server: git clone, git push en git fetch/pull.
Een commando als git status gebruikt dus altijd enkel lokale informatie.
Een repo clonen
GitHub is ongetwijfeld de bekendste Git server en bevat bovendien heel wat extra functionaliteit. Zo is bijvoorbeeld de volledige web interface geen standaard onderdeel van Git.
We hebben het hier over een “Git Server” maar dat klopt niet helemaal. Er bestaat niet echt een concept van “server”. Er bestaan alleen verschillende kopieën van eenzelfde Git repository. Elke kopie bevat de volledige repo. Het is enkel een conventie dat een van die repos typisch wordt aanzien als een “source of truth”. Maar er is dus intrinsiek geen verschil tussen de repo aanwezig op GitHub, en je lokale clone van die repo.
Om een Git repo te clonen hebben we eerst en vooral een adres nodig. Git ondersteunt standaard twee protocollen om met server te communiceren: HTTP(S) en SSH. Om te starten gebruiken we HTTP met een eenvoudige test repo: https://github.com/dvx76/git-demo. Vanop een GitHub pagina kan je het Git adres vinden door op de Code knop te klikken.
In dit geval dus https://github.com/dvx76/git-demo.git.
Om een repo te clonen gebruik je het git clone commando.
Het git clone commando zal een nieuwe directory aanmaken met de naam van de repo (in bovenstaand geval dus git-demo). Zorg er dus voor dat je je niet reeds in een repo of directory bevindt.
$ git clone https://github.com/dvx76/git-demo.git
Cloning into 'git-demo'...
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 9 (delta 0), reused 9 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (9/9), done.
$ cd git-demo
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
$ git log --oneline
0a17da5 (HEAD -> main) Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.mdOp deze manier kan je elke (publieke) repo op GitHub clonen!
Remotes
Remote: Een andere repo waar je je lokale repo mee kan updaten (“pull”) en lokale aanpassingen (commits) naar kan sturen (“push”). Typisch gehost op een netwerk-verbonden systeem.
Technisch gezien kan een remote dus perfect gewoon een andere repo zijn op je eigen systeem.
Het git remote commando geeft een lijst met de gekende remote(s). Met de -v (verbose) optie krijg je voor elke remote ook de URL.
$ git remote
origin
$ git remote -v
origin https://github.com/dvx76/git-demo.git (fetch)
origin https://github.com/dvx76/git-demo.git (push)Origin (oorsprong) is de conventionele (en standaard) naam voor de remote van waar je de lokale repo gecloned hebt. De termen fetch en push komen zo meteen aan bod.
Het git status commando kennen we al, maar geeft nu wat extra informatie:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean- De lokale
mainbranch is gelinkt met demainbranch van deoriginremote. We zeggen dat de lokalemainbranch de remoteorigin/mainbranch trackt (volgt). - De lokale en de remote branch zijn actueel (up-to-date).
git push
Met het git push commando worden aanpassingen op de lokale branch naar de remote verstuurd.
Eerst wordt een verandering gecommit. Veranderingen die nog niet gecommit zijn kan je dus nooit rechtstreeks pushen!
We gebruiken onze editor om een module docstring toe te voegen in hello.py.
$ git add hello.py
$ git commit -m "Add module docstring in hello.py"
[main 22b1b17] Add module docstring in hello.py
1 file changed, 2 insertions(+)Met git commit -a kan je in één commando alle gewijzigde bestanden stagen (add) en committen. Bovenstaande stap wordt dan gewoon:
$ git commit -a -m "Add module docstring in hello.py"
[main 22b1b17] Add module docstring in hello.py
1 file changed, 2 insertions(+)Let op: met -a stage je enkel de gewijzigde bestanden die Git al kent! Nieuwe bestanden worden niet toegevoegd en moet je expliciet met git add stagen.
Als we nu opnieuw git status uitvoeren zien we iets nieuws. De lokale branch “loopt voor” (ahead) op de remote, en wel met 1 commit.
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree cleanZoals de output suggereert kan die commit met git push naar de remote verzonden worden.
$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 10 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 1.01 KiB | 1.01 MiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/dvx76/git-demo.git
0a17da5..22b1b17 main -> mainIn de git log --oneline output zien we de nieuwe commit, als ook de informatie dat deze commit de meest recent commit is zowel in de lokale main branch als in de main branch in de origin remote.
$ git log --oneline
22b1b17 (HEAD -> main, origin/main, origin/HEAD) Add module docstring in hello.py
0a17da5 Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.mdgit fetch en git pull
Stel dat een andere ontwikkelaar ook een lokale clone heeft van dezelfde repo, zonder deze nieuwe commit:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
$ git log --oneline
0a17da5 (HEAD -> main, origin/main, origin/HEAD) Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.mdMet git fetch zullen alle verandering in de remote gedownload worden maar worden lokale branches niet aangepast. Dit wordt iets duidelijker als we daarna ook git status uitvoeren.
$ git fetch
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (3/3), 1013 bytes | 506.00 KiB/s, done.
From https://github.com/dvx76/git-demo
0a17da5..22b1b17 main -> origin/main
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree cleanMet het commando git merge (samenvoegen) worden de veranderingen van de lokale kopie van de remote branch opgenomen in de lokale branch.
$ git merge
Updating 0a17da5..22b1b17
Fast-forward
hello.py | 2 ++
1 file changed, 2 insertions(+)
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
$ git log --oneline
22b1b17 (HEAD -> main, origin/main, origin/HEAD) Add module docstring in hello.py
0a17da5 Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.mdMeestal wil je zowel fetchen als mergen. Dit kan met één enkel commando: git pull.
$ git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (3/3), 1013 bytes | 337.00 KiB/s, done.
From https://github.com/dvx76/git-demo
0a17da5..22b1b17 main -> origin/main
Updating 0a17da5..22b1b17
Fast-forward
hello.py | 2 ++
1 file changed, 2 insertions(+)git pull is letterlijk equivalent met git fetch + git merge.
git pull en git fetch downloaden beide veranderingen van de remote. Daarentegen is git merge een lokaal commando (dus geen contact met de remote).
Een eigen repo op GitHub
GitHub Account
Heb je nog geen eigen (privé) GitHub account, maak er dan nu eerste een aan. Stel later zeker ook two-factor authentication in voor je account.
Een Repository Maken
Eenmaal ingelogd kan je een nieuwe repository maken via de + knop rechts bovenaan.
Hoewel dit niet verplicht is werkt het handiger als de namen van de lokale en de remote repo hetzelfde zijn, dus we gebruiken gewoon hello.
Voor de visibility behouden we Public. Op die manier kunnen we elkaars repos bekijken.
Omdat we al een lokale repo hebben die we later naar hier willen pushen kiezen we er voor geen initiele README, .gitignore of andere bestanden in de repo te plaatsen. We starten dus met een volledige lege repo (geen commits).
Als de repo is aangemaakt krijgen we van GitHub meteen de juiste commandos om de repo als een remote toe te voegen aan onze lokale repo.
De termen repository en project worden al eens door elkaar gebruikt.
Binnen Git zelf bestaat enkel repository. In GitHub betekende dit ooit hetzelfde (en in GitLab is dit nog steeds het geval), of bestond er alleszins een één-op-één verhouding tussen de twee. Een project kon je dan zien als een repo, plus extra elementen die niet strikt bij Git zelf horen, zoals bijvoorbeeld een issue tracker.
Ondertussen heeft GitHub een onderscheiding gemaakt tussen de twee termen en betekent project iets specifieks, los van een bepaalde repo.
Remote toevoegen en pushen
We bekijken de commandos die GitHub voorstelt één voor één.
git remote add origin https://github.com/dvx76/hello.gitHiermee voegen we een remote toe en geven deze de naam origin. Het resultaat is dan eigenlijk hetzelfde als wanneer je een remote cloned.
git branch -M mainMet -M move (verander) je de naam van de huidige branch naar een nieuwe naam. Omdat onze (enige) branch al main heet doet dit commando niets en kunnen we het overslaan.
git push -u origin mainHiermee pushen we de huidige lokale branch (main) naar de main branch in de remote genoemd origin. Als we enkel git push gebruiken weet git niet naar we we precies willen pushen.
Met -u (upstream) stellen we in dat dit de upstream (= op een remote) branch is voor onze lokale branch. Als we -u niet gebruiken moeten we telkens opnieuw origin main meegeven. Hierdoor kunnen we de volgende keer gewoon git push gebruiken, want Git weet nu dat origin main de juiste remote branch is.
Als je nooit eerder gepusht hebt naar GitHub zul je moeten authentificeren. Hoe dit gebeurt kan verschillen naargelang je besturingssysteem, Git installatie, editor en terminal.
Inlog Venster
Als je dit soort venster opent klik je op Sign in with your browser. Eenmaal in de browser moet je eerst inloggen, indien dit nog niet gebeurt is. Vervolgens klik je op Authorize git-ecosystem.
Foutmelding
$ git push -u origin main
Username for 'https://github.com': user
Password for 'https://user@github.com':
remote: Invalid username or token. Password authentication is not supported for Git operations.
fatal: Authentication failed for 'https://github.com/user/hello.git/'Sinds 2021 kan je niet meer je gewone GitHub username en password gebruiken. De eenvoudigste manier om verder te kunnen werken is met een zogenaamd access token. Later bekijken we andere en betere manieren.
- Vanop https://github.com klik op je profielfoto en kies je Settings (of rechtstreeks: https://github.com/settings/profile).
- Kies Developer Settings, helemaal onderaan.
- Kies Personal access tokens en dan Tokens (classic).
- Klik op Generate new token en dan Generate new token (classic).
- De
reposcope is genoeg om te kunnen pushen. - Bewaar het token, bvb. in een password manager.
Gebruik nu dit token i.p.v. je eigen wachtwoord.
Het kan gebeuren dat git push telkens opnieuw naar je username en password vraagt en dat wordt natuurlijk snel erg vervelend.
Er zijn een aantal manieren om dit te verhelpen, maar doe dit enkel als je telkens opnieuw je token moet ingeven.
$ git config --global credential.helper storeIs de makkelijkste maar minst veilige manier. Je login gegevens worden ongeëncrypteerd opgeslagen (in ~/.git-credentials). Gebruik dit enkel als de andere opties niet werken.
$ git config --global credential.helper cacheHiermee worden je login gegevens enkele uren in het geheugen opgeslagen. Na een tijdje moet je ze dus opnieuw ingeven.
Nogmaals, we zien later andere en betere manieren om jezelf the authentificeren.
Oefeningen
1. Bestaande repo pushen
Als je dit nog niet gedaan hebt, volg de stappen uit dit hoofdstuk om een lege hello repo te maken in je GitHub account. Stel deze repo in als origin in je lokale hello repo en push naar de remote. Bekijk de repo in GitHub om te bevestigen.
2. Nieuwe repo clonen
- Maak een nieuwe repo in je eigen GitHub account. Kies er deze keer voor om de repo wel met een
README.mdte initialiseren. - Clone deze repo lokaal.
- Voeg een eenvoudig Python programma toe. Gebruik bvb. iets uit je laatste les.
- Commit het nieuwe bestand.
- Push naar je remote.
- Bekijk het bestand en de commit op GitHub.
Branching en de Commit Tree
Hell is other people.
- Jean-Paul Sartre
Als je Git enkel voor je eigen projecten gebruikt en nooit met anderen moet samenwerken dan bevatten de vorige hoofdstukken in principe alles wat je moet weten.
In de praktijk is de kans natuurlijk groot dat je wel gaat samenwerken. Git leent zich hier uitstekend toe en biedt flexibele en krachtige opties op er op een efficiënte manier mee op te gaan.
Om hier verder op in te gaan zullen we eerste een aantal nodige en onderliggende concepten uitdiepen.
In dit hoofdstuk leggen we onze remote in GitHub even opzij en werken we opnieuw volledig lokaal!
Commits
Een commit is eerder beschreven als iets dat volgende informatie bevat:
- ID
- Auteur
- Datum
- Beschrijving
- Diff
Daarnaast bevat elke commit (behalve de allereerste commit) ook de ID van de voorgaande commit (we spreken van parent commit). In speciale gevallen kan een commit zelf meerdere parents hebben.
We spreken ook wel van een commit hash i.p.v. ID omdat het de hash waarde is van alle informatie in de commit. Een hash is een wiskundige functie die voor een bepaalde input een unieke string met vaste lengte (hier 40 karakters) geeft. Naast het identificeren van een bepaalde commit zorgt die hash vooral voor de data-integriteit van Git: door de hash opnieuw te berekenen kan Git data corruptie detecteren! Omdat de gebruikte hash functie SHA-1 is wordt er soms ook gesproken over een commit sha.
Om naar een bepaalde commit te refereren gebruiken we de commit hash. We hoeven niet de volledige hash van 40 karakters te gebruiken. Het begin van de hash is genoeg om een bepaalde commit uniek te identificeren. In onze kleine repo met slechts een handvol commits hebben we al genoeg met de eerste 7 karakters. Dit gebruikt het git commando zelf ook bvb. bij git log --oneline.
$ git log --oneline
22b1b17 (HEAD -> main, origin/main, origin/HEAD) Add module docstring in hello.py
0a17da5 Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.mdDat een commit de diff met zijn parent bevat is eigenlijk niet helemaal correct. Git bewaart elke versie van elk bestand. I.p.v. een diff bevat een commit dan eigenlijk een lijst van bestand-versies. Elke commit vormt op die manier een complete snapshot van je repo. Vele andere versiebeheersystemen werken precies omgekeerd en bewaren bij elke commit de diffs.
De diff die je te zien krijgt als je een commit bekijkt wordt dus eigenlijk bepaald door die commit met zijn parent te vergelijken.
$ git show 22b1b17
commit 22b1b17cd1b19d5da6f451fe3b066aee2dc0620d (HEAD -> main, origin/main, origin/HEAD)
Author: Fabrice Devaux <fabrice.devaux@gmail.com>
Date: Wed Aug 27 08:31:34 2025 +0200
Add module docstring in hello.py
diff --git a/hello.py b/hello.py
index e0f8470..13b7d2a 100644
--- a/hello.py
+++ b/hello.py
@@ -1,2 +1,4 @@
+"""Print a message to stdout."""
+
message = "Hello from my first Git repo"
print(message)Git slaat bestanden op als een blob (Binary Large OBject). Waarom en wat dat precies inhoud is minder belangrijk, maar onthoud dat een blob één bepaalde versie is van een bestand.
In de git show output hierboven zien we volgende lijn:
index e0f8470..13b7d2a 100644
100644 heeft betrekking om de permissies van het bestand en is hier minder belangrijk. e0f8470 en 13b7d2a zijn de (verkorte) hashes van de blobs van het bestand (hello.py) in de huidige en vorige commit. Met git cat-file kunnen we de inhoud van een blob (en andere Git objected) bekijken:
$ git cat-file blob e0f8470
message = "Hello from my first Git repo"
print(message)
$ git cat-file blob 13b7d2a
"""Print a message to stdout."""
message = "Hello from my first Git repo"
print(message)
Als je deze twee inhouden vergelijkt, dan merk je dat dit inderdaad precies de diff is die het git show commando laat zien.
TODO: commit and tree objecten
Een ketting van commits
Omdat elke commit de ID van de vorige commit bevat krijgen we dus een soort ketting van commits. We halen er opnieuw de output van git log --oneline bij:
$ git log --oneline
22b1b17 (HEAD -> main, origin/main, origin/HEAD) Add module docstring in hello.py
0a17da5 Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.md
Deze kunnen we grafisch voorstellen als een tijdlijn. De oudste commit links en de laatste commit rechts:
---
config:
theme: 'base'
---
gitGraph
commit id: "d8d6683"
commit id: "18152d6"
commit id: "0a17da5"
commit id: "22b1b17"
Hoe weet Git nu wat de “huidige” commit is die overeenkomt met de “echte” bestanden in de directory? Git gebruikt references (of kort: refs) om bepaalde commits aan te wijzen. HEAD is een speciale ref die wijst naar de huidige commit.
---
config:
theme: 'base'
---
gitGraph
commit id: "d8d6683"
commit id: "18152d6"
commit id: "0a17da5"
commit id: "22b1b17" tag: "HEAD"
Branches
Een branch in Git is gewoon een vertakking van de tijdlijn van je code. Je gebruikt branches zodat je veilig en overzichtelijk kunt werken:
- Isolatie – je kunt nieuwe features of fixes uitproberen zonder de stabiele code kapot te maken.
- Samenwerken – meerdere mensen kunnen tegelijk aan verschillende dingen werken zonder elkaar in de weg te zitten.
- Experimenteren – je kunt ideeën testen, en als het niks is, gooi je de branch gewoon weg.
De main branch
Conceptueel is een branch een welbepaalde reeks commits. Zoals de naam al doet vermoeden kunnen er meerdere branches (vertakkingen) zijn en vormen ze samen een tree (boom).
De “default” branch in Git was historisch altijd master. In huidige versie is dit nog steeds het geval. Echter is deze naam ondertussen configureerbaar. Tevens raadt het git init commando ook aan een andere naam te configureren. Tegenwoordig is de meest gebruikte naam main en is dit tevens de default naam op populaire diensten als GitHub en GitLab.
$ git config --global init.defaultBranch mainConceptueel dus een reeks commits. In werkelijkheid is een branch echter ook niets meer dan een ref (zoals HEAD). Met andere woorden, een verwijzing naar een bepaalde commit, namelijk de meest recente commit voor die branch. Meer is niet nodig aangezien elke commit zelf naar zijn parent verwijst.
---
config:
theme: 'base'
---
gitGraph
commit id: "d8d6683"
commit id: "18152d6"
commit id: "0a17da5"
commit id: "22b1b17" tag: "HEAD" tag: "main"
Branches maken
Tot nu hebben we enkel op de default of main branch gewerkt. Een nieuwe branch kan op twee manieren gemaakt worden:
git branch <name>maakt we een nieuwe branch maar blijft op de huidige branch in de working directorygit switch -c <name>maakt (-c= create) een nieuwe branch en switcht ook naar die branch
$ git branch cool-feature
$ git switch -c another-feature
Switched to a new branch 'another-feature'Met git status kunnen we controleren op welke branch we momenteel werken. Met git switch (zonder -c) kunnen we switchen naar een andere (bestaande) branch. Met git branch krijgen we een lijst met alle branches, voegen we -a toe dan krijgen we ook de gekende remote branches.
$ git status
On branch another-feature
nothing to commit, working tree clean
$ git switch cool-feature
Switched to branch 'cool-feature'
$ git branch -a
main
another-feature
* cool-feature
remotes/origin/HEAD -> origin/main
remotes/origin/mainMerk op dat er intern in de Git repo weinig verandert is. Er zijn twee refs gemaakt voor de twee nieuwe branches en beiden verwijzen naar dezelfde commit die tevens de commit is waar de main branch naar verwijst. Een nieuwe branch “start” dus altijd vanaf de commit waar de huidige branch naar verwijst.
In oudere documentatie of tutorials wordt het git checkout commando gebruikt i.p.v. git switch. Het probleem met git checkout is dat het voor heel wat verschillende zaken gebruikt kan worden, wat erg verwarrend is. Door de jaren heen is git uitgebreid met nieuwe commandos om dit te verhelpen. De git checkout commandos kan je wel nog gebruiken maar het is aan te raden direct de nieuwere commandos te leren.
git switch cool-feature=git checkout cool-featuregit switch -c cool-feature=git checkout -b cool-feature
Branches en Commits
Als we nu een nieuwe commit aanmaken terwijl we op een andere branch werken, gebeurt er het volgende:
- De nieuwe commit verwijst (parent) naar de vorige commit van de branch.
- De branch reference wordt aangepast en verwijst nu naar de nieuwste commit.
- De HEAD ref wordt op dezelfde manier aangepast. Alle andere refs veranderen niet.
$ echo "# Tests for the hello module" > test_hello.py
$ git status
On branch cool-feature
Untracked files:
(use "git add <file>..." to include in what will be committed)
test_hello.py
nothing added to commit but untracked files present (use "git add" to track)
$ git add test_hello.py
$ git commit -m "Add placeholder test for hello module"
[cool-feature 24e6e1a] Add placeholder test for hello module
1 file changed, 1 insertion(+)
create mode 100644 test_hello.py
$ git log --oneline
24e6e1a (HEAD -> cool-feature) Add placeholder test for hello module
22b1b17 (origin/main, origin/HEAD, main, another-feature) Add module docstring in hello.py
0a17da5 Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.md---
config:
theme: 'base'
---
gitGraph
commit id: "d8d6683"
commit id: "18152d6"
commit id: "0a17da5"
commit id: "22b1b17" tag: "main" tag: "another-feature"
commit id: "24e6e1a" tag: "HEAD" tag: "cool-feature"
Nog geen boom
We hebben nog steed niet echt een boom-structuur met onze commits.
Als we aanpassingen committen op another-feature branch komt daar verandering in.
$ git switch another-feature
Switched to branch 'another-feature'
$ echo 'print("Goodbye")' > goodbye.py
$ git add goodbye.py
$ git commit -m "Add goodbye module"
[another-feature 19f7b7a] Add goodbye module
1 file changed, 1 insertion(+)
create mode 100644 goodbye.py---
config:
theme: 'base'
---
gitGraph
commit id: "d8d6683"
commit id: "18152d6"
commit id: "0a17da5"
commit id: "22b1b17" tag: "main"
branch cool-feature
checkout cool-feature
commit id: "24e6e1a" tag: "cool-feature"
checkout main
branch another-feature
checkout another-feature
commit id: "19f7b7a" tag: "HEAD" tag: "another-feature"
Met git log --oneline --graph --decorate krijg je ook een grafische voorstelling van de commit tree. Voeg je ook --all toe dan omvat het de volledige commit tree i.p.v. enkel de huidige branch.
Met volgende tool krijg je een interactieve visualisatie van we er eigenlijk gebeurt wanneer je verschillende git commandos uitvoert. Dit kan heel erg nuttig zijn om eenvoudig te experimenteren en de verschillende concepten volledige onder de knie te krijgen.
https://onlywei.github.io/explain-git-with-d3/
Jammer genoeg is het git switch commando niet beschikbaar en moet je dus git checkout en git checkout -b gebruiken i.p.v. git switch en git switch -c.
Een recenter project https://learngitbranching.js.org/ is wel up to date met de nieuwste commandos en standaarden.
Er wordt meestal gesproken over de git commit tree als we het hebben over de volledige structuur van commits in een Git repo.
Iets technischer moeten we het eigenlijk hebben over een “Directed Acyclical Graph” - kortweg DAG.
- Een graph is een model van knooppunten (hier, de commits) en verbindingen tussen die punten
- Bij een directed graph hebben die verbindingen een richting
- Bij een acyclical graph zijn er geen lussen mogelijk - je kan dus nooit terug op dezelfde knoop uitkomen
Een tree is eigenlijk een speciaal geval van een DAG waarbij elke knoop maar één ouder (parent) heeft en er één enkel startpunt (root) is. Zoals we later zullen zien kan een Git commit meerdere parents hebben.
Oefeningen
- Clone de repo van https://github.com/dvx76/git-this.
- Bekijk de lijst met branches.
- Bekijk voor elke branch de lijst met commits.
- Probeer op de blad papier de volledige commit tree uit te tekenen
- Simuleer wat er gebeurt met https://onlywei.github.io/explain-git-with-d3/, en/of maak level 2 in de Introduction Sequence reeks op https://learngitbranching.js.org/.
Als je een repo clonet krijg je automatisch alle remote branches. Maar lokaal krijg je enkel de main branch. Met git switch <branch-name> (dus zonder -c) krijg je lokale versie van een remote branch.
We gaan hier later verder op in.
Mergen
Net als in het vorige hoofdstuk werken we opnieuw volledig lokaal! In het volgende hoofdstuk komt onze remote op GitHub terug in de kijker.
De aanpassingen (commits) op een branch zullen in principe op een bepaald moment naar de main branch gebracht moeten worden. Dit proces heet mergen (samenvoegen).
Bij een merge spreken we altijd over:
- Een source branch - dit is de branch met de commits die op een andere branch terecht moeten komen
- Een target branch
De source branch wordt in de target branch gemerged. De commit vanaf waar de tween branches out elkaar lopen (divergen) noemen we het branchpunt (branchpoint) of ook wel merge base of most recent common ancestor.
Kijken we terug naar de laatste visualisatie uit het vorige hoofdstuk, als we de cool-feature branch in de main branch willen mergen, dan is:
cool-featurede source branchmainde target branch22b1b17de merge base
---
config:
theme: 'base'
---
gitGraph
commit id: "d8d6683"
commit id: "18152d6"
commit id: "0a17da5"
commit id: "22b1b17" tag: "main"
branch cool-feature
checkout cool-feature
commit id: "24e6e1a" tag: "cool-feature"
checkout main
branch another-feature
checkout another-feature
commit id: "19f7b7a" tag: "HEAD" tag: "another-feature"
Om te mergen gebruiken we het git merge commando, met als parameter de source branch. Het commando wordt uitgevoerd _vanop de target branch_.
Er zijn drie basis scenarios die kunnen voorkomen bij een merge:
- Fast-Forward Merge
- True (of three-way) Merge
- Merge Conflict
Fast-Forward Merge
Een fast-forward merge is de eenvoudigste vorm. Dit komt voor als er op de target branch geen nieuwe commits zijn na het branchpunt.
Het volgende voorbeeld merget de cool-feature branch in de main branch.
$ git merge cool-feature
Updating 22b1b17..24e6e1a
Fast-forward
test_hello.py | 1 +
1 file changed, 1 insertion(+)
create mode 100644 test_hello.py
$ git log --oneline
24e6e1a (HEAD -> main, cool-feature) Add placeholder test for hello module
22b1b17 (origin/main, origin/HEAD) Add module docstring in hello.py
0a17da5 Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.md---
config:
theme: 'base'
---
gitGraph
commit id: "d8d6683"
commit id: "18152d6"
commit id: "0a17da5"
commit id: "22b1b17"
branch cool-feature
checkout cool-feature
commit id: "24e6e1a"
checkout main
branch another-feature
checkout another-feature
commit id: "19f7b7a" tag: "another-feature"
checkout main
merge cool-feature tag: "HEAD" tag: "main" tag: "cool-feature"
Deze visualisatie is een beetje verwarrend. Het lijkt also er een commit bijkomt maar eigenlijk worden gewoon de HEAD en main refs aangepast naar commit 24e6e1a.
Met explain-git-with-d3 krijgen we een duidelijker beeld.
Nadat de cool-feature branch in main gemerged is hebben we de branch niet meer nodig. Een branch deleten doe je met git branch -d. Git weet welke branches al gemerged zijn. Als je een niet-gemergede branch probeert te deleting krijg je een foutmelding!
$ git branch -d cool-feature
Deleted branch cool-feature (was 24e6e1a).
$ git branch -d another-feature
error: the branch 'another-feature' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D another-feature'
hint: Disable this message with "git config set advice.forceDeleteBranch false"True (three-way) Merge
Wat gebeurt er als er, vanaf het het branchpunt (de aftakking) nieuwe commits zijn op zowel de source als de target branch? In dit geval kan Git niet zomaar de source branch ref aanpassen, want dan zouden we de tussenliggende commits op de source branch verliezen.
Eerste voegen we een commit toe op de main branch.
$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ echo 'print("Goodbye")' >> hello.py
$ git add hello.py
$ git commit -m "Add goodbye message"
[main 7c13567] Add goodbye message
1 file changed, 1 insertion(+)De commit graph ziet er op dat moment als volgt uit:
---
config:
theme: 'base'
---
gitGraph
commit id: "d8d6683"
commit id: "18152d6"
commit id: "0a17da5"
commit id: "22b1b17"
commit id: "24e6e1a"
branch another-feature
checkout another-feature
commit id: "19f7b7a" tag: "another-feature"
checkout main
commit id: "7c13567" tag: "HEAD" tag: "main"
Het git commando kan ook een visuele representatie geven van de commit graph:
$ git log --oneline --graph --decorate --all
* 7c13567 (HEAD -> main) Add goodbye message
* 24e6e1a Add placeholder test for hello module
| * 19f7b7a (another-feature) Add goodbye module
|/
* 22b1b17 (origin/main, origin/HEAD) Add module docstring in hello.py
* 0a17da5 Improve hello flexibility
* 18152d6 Add initial hello module
* d8d6683 Add README.mdDan mergen we de another-feature branch in de main branch.
$ git merge another-feature
*
Merge made by the 'ort' strategy.
goodbye.py | 1 +
1 file changed, 1 insertion(+)
create mode 100644 goodbye.py* Git opent nu de tekst editor om een nieuwe commit message te schrijven! Standaard zal dit Merge branch 'another-feature' zijn en dit kunnen we zo laten.
Wat is er gebeurt? Omdat er een commit is op de main branch (voorbij het another-feature branchpoint) kan Git niet gewoon een fast-forward merge gebruiken en “doorspoelen” naar de laatste commit op de another-feature branch. Om de aanpassingen van de another-feature branch op de main branch te krijgen worden die aanpassingen (commits) één voor één (in volgorde) uitgevoerd boven op de main branch. Op het einde worden alle aanpassingen samen vastgelegd in een nieuwe commit op de main branch met een zogenaamde merge commit. Die commit bevat dus alle aanpassingen van alle commits van de another-feature branch. Die merge commit krijg ook twee parent commits: de laatste commit van de main branch, en de laatste commit van de another-feature branch. Op die manier blijft het achteraf duidelijk wat er precies gebeurd is. Alle commits op de another-feature branch blijven bestaan (ook als we die branch deleten!).
De commit graph ziet er nu zo uit:
---
config:
theme: 'base'
---
gitGraph
commit id: "d8d6683"
commit id: "18152d6"
commit id: "0a17da5"
commit id: "22b1b17"
commit id: "24e6e1a"
branch another-feature
checkout another-feature
commit id: "19f7b7a" tag: "another-feature"
checkout main
commit id: "7c13567"
merge another-feature tag: "HEAD" tag: "main"
$ git log --oneline --graph --decorate --all
* 02c5545 (HEAD -> main) Merge branch 'another-feature'
|\
| * 19f7b7a (another-feature) Add goodbye module
* | 7c13567 Add goodbye message
* | 24e6e1a Add placeholder test for hello module
|/
* 22b1b17 (origin/main, origin/HEAD) Add module docstring in hello.py
* 0a17da5 Improve hello flexibility
* 18152d6 Add initial hello module
* d8d6683 Add README.mdOpnieuw kan een simulatie met explain-git-with-d3 een beter beeld geven van wat er precies gebeurt.
In de output van het git merge commando zien we Merge made by the 'ort' strategy.
Een merge strategy bepaald welke logica Git volgt om te beslissen welke code overblijft bij het mergen van verschillende branches. Dit is vooral van toepassing bij ingewikkelde merges waar de source en/of target branches zelf meerdere parents hebben.
ort staat voor Ostensibly Recursive’s Twin en is sinds Git 2.34 de default strategie. Vroeg was dit de recursive strategie.
In deze cursus gaan we niet verder in op merge strategieën.
Waarom noemen we dit ook een three-way merge?
Simpelweg omdat er gewerkt wordt vanuit drie punten (commits) uit de commit graph:
- De gemeenschappelijke voorouder. M.a.w. het aftakkingspunt waar beide branches uit vertrekken.
- De tip (laatste commit) van de source branch.
- De tip van de target branch.
Merge Conflicts
Deze derde en laatste mogelijkheid is eigenlijk een speciaal geval van de vorige three-way merge. Zoals de naam doet vermoeden gaat het hier om de situatie waarom er conflicten zijn, tussen de source en de target branch, die Git niet automatisch kan oplossen.
We starten een nieuwe branch, feature-42. In een eerste commit passen we goodbye.py aan:
goodbye.py
def say_goodbye():
print("Goodbye")$ git switch -c feature-42
Switched to a new branch 'feature-42'
$ git commit -a -m "Refactor goodbye as a function"
[feature-42 67fae1f] Refactor goodbye as a function
1 file changed, 2 insertions(+), 1 deletion(-)In een tweede commit passen we hello.py aan:
hello.py
"""Print a message to stdout."""
from goodbye import say_goodbye
message = "Hello from my first Git repo"
print(message)
say_goodbye()$ git commit -a -m "Use goodbye in hello.pyx"
[feature-42 e8ba000] Use goodbye in hello.py
Date: Thu Sep 4 08:42:38 2025 +0200
1 file changed, 2 insertions(+), 1 deletion(-)Terwijl er aan deze feature is gewerkt is op de main branch hello.py ook aangepast in een nieuwe commit:
hello.py
"""Print a message to stdout."""
message = "Hello from my first Git repo"
print(message)
print("Goodbye and thanks for all the fish!")$ git switch main
$ git commit -a -m "Make goodbye message more fishy"
[main 6fb8518] Make goodbye message more fishy
1 file changed, 1 insertion(+), 1 deletion(-)De commit graph ziet er nu zo uit (de oudste commits laten we voor de duidelijkheid achterwege):
---
config:
theme: 'base'
---
gitGraph
commit id: "19f7b7a"
commit id: "02c5545"
branch feature-42
checkout feature-42
commit id: "67fae1f"
commit id: "e8ba000" tag: "feature-42"
checkout main
commit id: "6fb8518" tag: "HEAD" tag: "main"
$ git log --oneline --graph --decorate --all 02c5545^..
* 6fb8518 (HEAD -> main) Make goodbye message more fishy
| * e8ba000 (feature-42) Use goodbye in hello.py
| * 67fae1f Refactor goodbye as a function
|/
* 02c5545 Merge branch 'another-feature'
* 19f7b7a Add goodbye moduleTot zover geen verschillende met een gewone merge. Als we nu echter de feature-42 branch willen mergen in main krijgen we het volgende probleem:
$ git merge feature-42
Auto-merging hello.py
CONFLICT (content): Merge conflict in hello.py
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch main
Your branch is ahead of 'origin/main' by 5 commits.
(use "git push" to publish your local commits)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
modified: goodbye.py
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: hello.pyGit kon er niet in slagen de commits van feature-42 automatisch te mergen in main. Op dat moment stopt Git met de merge en geeft de controle terug over aan jou om het probleem (conflict) op te lossen. Er wordt ook aangegeven in welke bestand (of bestanden) er problemen zijn.
Met git status krijgen we nog wat extra details.
Als we het bestand in kwestie nu bekijken krijgen we iets vreemds te zien:
hello.py
"""Print a message to stdout."""
from goodbye import say_goodbye
message = "Hello from my first Git repo"
print(message)
<<<<<<< HEAD
print("Goodbye and thanks for all the fish!")
=======
say_goodbye()
>>>>>>> feature-42Dit is Git’s manier om aan te geven waar in het bestand er een conflict is, en wat het conflict precies is. Elke block tussen <<<<<<< en >>>>>>> is één conflict dat opgelost moet worden. Tussen <<<<<<< en ======= zien we de code zoals die bestaat op de target branch. Tussen ======= en >>>>>>> de code uit de source branch.
Het is nu aan ons om te beslissen hoe we dit willen oplossen. Uit de source branch is het duidelijke dat we nu een functie willen gebruiken uit de goodbye module, dus we passen hello.py als volgt aan:
hello.py
"""Print a message to stdout."""
from goodbye import say_goodbye
message = "Hello from my first Git repo"
print(message)
say_goodbye()Maar nu zijn we de aanpassingen van de eigenlijke tekst kwijt. Om hetzelfde resultaat te behouden passen we goodbye.py aan:
goodbye.py
def say_goodbye():
print("Goodbye and thanks for all the fish!")Onze status ziet er nu zo uit:
$ git status
On branch main
Your branch is ahead of 'origin/main' by 5 commits.
(use "git push" to publish your local commits)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
modified: goodbye.py
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: hello.py
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: goodbye.pyRest nog alle aanpassingen samen te committen in een merge-commit. Hierbij krijgen we opnieuw een voorgestelde commit message in de editor, net zoals bij een gewone merge.
$ git commit -a
De uiteindelijke commit graph ziet er dan net zo uit als na een gewone three-way commit.
Oefeningen
- Clone de repo van https://github.com/dvx76/git-this
- Bekijk de commit history
- Merge de branch
more-citiesinmain - Merge de branch
strings-docinimprove-strings - Merge de branch
bigfloatinimprove-floats - Extra: Welke branches hebben commits die nog niet op
mainbestaan? Merge deze branches inmain.
Gebruik git status als je even de weg kwijt bent!



