Ga naar inhoud

Git Remotes, Branches en Mergen

NOTES

Voorbereiding

Herhaling

Commando Beschrijving
git init Nieuwe Git repository (repo) aanmaken in de 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 de 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) uit Git.
git rm --cached : Bestand bewaren in working directory (untracked).
git log [filename] Commitgeschiedenis bekijken. Van nieuw naar oud. Met de filename-optie enkel voor dat bestand.
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> Een nog niet gestaged bestand herstellen (naar de versie van de laatste commit).
git restore --staged : Reeds gestagede veranderingen herstellen.
git diff [filename] Toont aanpassingen t.o.v. de 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 blame [filename] Toont het volledige bestand met op elke lijn de laatste commit, datum en auteur.

Gedistribueerd Versiebeheer

NOTES

  • Centraal vs. gedistribueerd, voor- en nadelen
  • GitHub, just-another-repo, clonen (git-demo)
  • Remote (= andere repo, typisch op een netwerkverbonden systeem)
  • clone, fetch, pull
  • Eigen GitHub account en repo, auth, push

In het eerste hoofdstuk hebben we Git en GitHub al gebruikt en spraken we over gedistribueerd versiebeheer (Distributed Version Control). We komen hier eerst even wat dieper op dit concept terug.

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.

Tip

In dit geval is er geen of weinig informatie over de versies en aanpassingen lokaal aanwezig op je computer. Het is alsof alle informatie die Git lokaal bewaart (in de .git/-directory) enkel op een server bestaat.

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

Het grote voordeel is dat dit conceptueel heel eenvoudig is. Daartegenover staan de beperkte flexibiliteit en netwerkafhankelijkheid. Veranderingen kunnen enkel op de server zelf in versiebeheer komen. 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 commithistoriek, 'branches', metadata, enz.). We gebruiken dan ook de term clone i.p.v. checkout. Na het klonen 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 onafhankelijk 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.

Belangrijk

Wat je heel goed moet onthouden uit bovenstaand schema is dat er bij Git dus maar drie commando's 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.

Just another repo

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 beschouwd als een "source of truth". Maar er is dus intrinsiek geen verschil tussen de repo op GitHub en je lokale clone van die repo.

Om een Git repo te clonen (met de Git CLI) hebben we eerst en vooral een adres nodig. Git ondersteunt standaard twee protocollen om met de server te communiceren: HTTP(S) en SSH. SSH vereist enige voorkennis en extra configuratie. We kiezen dus doelbewust voor HTTP. Na het gebruik van GitHub Desktop in het eerste hoofdstuk zou bovendien authenticatie via HTTP al functioneel moeten zijn.

Op een GitHub-pagina kan je het Git-adres vinden door op de groene <> Code knop te klikken. Bijvoorbeeld vanaf deze eenvoudige test repo: https://github.com/dvx76/git-demo.

In dit geval dus https://github.com/dvx76/git-demo.git.

Om een repo te clonen gebruik je het git clone commando.

Tip

GitHub Desktop vraagt bij het clonen naar de lokale directory (map) die je voor de repo wilt gebruiken.

Het git clone commando doet dit niet en maakt een nieuwe directory aan 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 project bevindt.

git clone https://github.com/dvx76/git-demo.gitCloning 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-demogit statusOn branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
git log --oneline0a17da5 (HEAD -> main) Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.md

Op deze manier kan je elke (publieke) repo op GitHub lokaal clonen!

Remotes

NOTES

Niet per se meevolgen. In het volgende hoofdstuk maken we elk een eigen remote op GitHub.

Extra voorbeeldje voor het concept van een remote:

$ mkdir ../foo
$ cd ../foo
$ git init
$ git remote add origin ../hello
$ git fetch
$ git pull
$ git remote -v
$ cd ../hello
$ rm -rf ../foo

Remote

Remote: Een andere repo waarmee je je lokale repo kan updaten (pull) en waarnaar je lokale aanpassingen (commits) kan sturen (push). Typisch gehost op een netwerkverbonden systeem.

In principe kan een remote dus perfect gewoon een andere repo (directory) 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 remoteorigin
git remote -vorigin https://github.com/dvx76/git-demo.git (fetch)
origin https://github.com/dvx76/git-demo.git (push)

Origin

origin (oorsprong) is de conventionele (en standaard) naam voor de remote waarvandaan je de lokale repo gecloned hebt.

Een belangrijk inzicht is dat origin niets meer dan een naam is voor een bepaalde remote en die naam ook gewoon aangepast kan worden.

(De termen fetch en push komen zo meteen aan bod.)

Het git status commando kennen we al, maar het geeft nu wat extra informatie:

git statusOn branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
  1. De lokale main branch is "gelinkt" met de main branch van de origin remote. We zeggen dat de lokale main branch de remote origin/main branch trackt (volgt).
  2. De lokale en de remote branch zijn actueel (up-to-date).

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 iets aan te passen in hello.py (bvb. een module-docstring toe te voegen).

git add hello.pygit commit -m "Add module docstring in hello.py"[main 22b1b17] Add module docstring in hello.py
1 file changed, 2 insertions(+)

Tip: add en commit combineren

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; die moet je expliciet met git add stagen.

Als we nu opnieuw git status uitvoeren zien we iets nieuws. De lokale branch "loopt voor" (is ahead) op de remote, en wel met 1 commit.

git statusOn 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 clean

Zoals de output suggereert kan die commit met git push naar de remote verzonden worden.

Authenticatie

In principe niet meer nodig wegens het eerdere gebruik van GitHub Desktop.

Het is je misschien opgevallen dat we nooit hebben moeten inloggen. Dat komt omdat de repo die we clonen publiek is. Toegankelijk voor iedereen dus - ook zonder GitHub account! Er is dus geen authenticatie aan te pas gekomen.

Daarna hebben we lokaal een aanpassing gecommit en ook daar is nooit authenticatie voor nodig.

Nu willen we echter aanpassingen naar de repo op GitHub doorsturen (pushen). Dat kan natuurlijk niet door iedereen!

Omdat we een HTTP-remote gebruiken zou je in principe telkens (bij elk git push commando) authenticatiegegevens moeten ingeven. Omdat dit al snel erg vervelend zou worden kan Git werken met een zogenaamde credential helper om op een veilige manier je authenticatiegegevens lokaal te bewaren en hergebruiken.

Welke helper gebruikt wordt en hoe het authenticatieproces precies verloopt, hangt af van het besturingssysteem.

De Git Credential Manager (GCM) wordt automatisch (samen met de Git CLI) geïnstalleerd en gebruikt.

Wanneer je voor de eerste keer het git push commando gebruikt zal deze helper je voorstellen om via je browser in te loggen op GitHub. Vervolgens moet je in de browser de helper (Git Credential Manager) toegang verlenen tot je GitHub account. (Dit hele proces is gekend als OAuth 2.0 en krijg je misschien te zien tijdens de module Security)

Op macOS moet de Git Credential Manager apart geïnstalleerd worden. Dit doe je best via Homebrew.

brew install --cask git-credential-manager

Eenmaal geïnstalleerd kom je bij het eerste git push commando in hetzelfde "OAuth" scenario terecht als op Windows.

Op Linux hangen de mogelijkheden een beetje af van de gebruikte Linux distributie (en je eigen ervaring met het systeem).

Indien beschikbaar is het git-credentials-oauth package een goede optie.

sudo apt install git-credentials-oauth

git-credentials-oauth config

Hierna kom je bij het eerste git push commando in hetzelfde "OAuth" scenario terecht als op Windows.

git pushinfo: please complete authentication in your browser...
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 -> main

In de git log --oneline output zien we de nieuwe commit, alsook de informatie dat deze commit de meest recente commit is, zowel in de lokale main branch als in de main branch van de origin remote.

git log --oneline22b1b17 (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

Fetch en Pull

Stel dat een andere ontwikkelaar ook een lokale clone heeft van dezelfde repo, zonder deze nieuwe commit te hebben. Zijn of haar lokale repo ziet er dan als volgt uit:

git statusOn branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
git log --oneline0a17da5 (HEAD -> main, origin/main, origin/HEAD) Improve hello flexibility
18152d6 Add initial hello module
d8d6683 Add README.md

Met het git fetch commando zullen alle veranderingen op de remote gedownload worden, maar lokale branches worden niet aangepast. Dit wordt iets duidelijker als we daarna ook git status uitvoeren.

git fetchremote: 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 statusOn 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 clean

Met het commando git merge (samenvoegen) worden de veranderingen in de lokale kopie van de remote branch opgenomen in de lokale branch.

git mergeUpdating 0a17da5..22b1b17
Fast-forward
hello.py | 2 ++
1 file changed, 2 insertions(+)
git statusOn branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
git log --oneline22b1b17 (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

Meestal wil je zowel fetchen als mergen. Dit kan met één enkel commando: git pull.

git pullremote: 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(+)

Tip

git pull is letterlijk hetzelfde als 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).

Extra: 'Meet the remote'

https://wizardzines.com/comics/meet-the-remote/

Van de lokale repo naar GitHub

In het eerste hoofdstuk hebben we een GitHub account gemaakt en een repo rechtstreeks op GitHub aangemaakt.

In het volgende deel hebben we met de Git CLI een volledig lokale repo (hello) gemaakt. We willen nu die repo ook op GitHub krijgen.

Een 'lege' GitHub repo toevoegen

Ingelogd op github.com 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 hierheen willen pushen kiezen we ervoor geen initiële README, .gitignore of andere bestanden in de repo te plaatsen. We starten dus met een volledig lege repo (geen commits).

Als de repo is aangemaakt krijgen we van GitHub meteen de juiste commando's om de repo als remote toe te voegen aan onze lokale repo.

Extra: Repository vs. Project

De termen repository en project worden al eens door elkaar gebruikt.

Binnen Git zelf bestaat enkel repository. Op GitHub betekende dit ooit hetzelfde. Een project kon je dan zien als een repo, plus extra elementen die niet strikt bij Git zelf horen, zoals de issue tracker.

Ondertussen heeft GitHub een onderscheid gemaakt tussen de twee termen en betekent project iets specifieks, los van één bepaalde repo.

Remote toevoegen en pushen

We bekijken de commando's die GitHub voorstelt één voor één.

git remote add origin https://github.com/dvx76/hello.git

Hiermee voegen we een remote toe en geven deze de naam origin. Het resultaat is dan eigenlijk hetzelfde als wanneer je een remote clonet.

git branch -M main

Met -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 main

Hiermee pushen we de huidige lokale branch (main) naar de main branch van de origin remote. Als we enkel git push gebruiken weet Git niet waarnaar we precies willen pushen.

Met -u (upstream) stellen we in dat dit de upstream (= op een remote) branch is van 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.

Oefening

  1. Volg de stappen uit dit hoofdstuk om een lege hello repo te maken op je GitHub account.
  2. Stel deze repo in als origin van je lokale hello repo.
  3. Push naar de remote.
  4. Bekijk de repo in GitHub om te bevestigen dat de push gelukt is.

Branching en de Commit Tree

NOTES

  • Parent Commit ID
  • Commit Hash of SHA
  • Geen diff in commit maar ref naar objecten met volledig bestand
  • Blobs en git cat-file
  • Kennen? Nee. Handig om te weten hoe het werkt: ja!
  • Ketting van commits, refs (HEAD, main)
  • Branches (isolatie, samenwerken, experimenteren)
  • Wordt een echte tree als commits op elke branch
  • D3!

Terug volledig lokaal

We leggen onze remote in GitHub even opzij en werken opnieuw volledig lokaal!

Commits

Een commit is eerder beschreven als iets dat de volgende informatie bevat:

  • Een 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.

Commit Hash

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 datacorruptie detecteren! Omdat de gebruikte hashfunctie SHA-1 is wordt er soms ook gesproken over een commit sha.

Op https://www.shasum.com kan je zelf het berekenen van een SHA1 hash uitproberen. Voer wat tekst in (bvb. de inhoud van hello.py) en bekijk de hash. Pas dan één karakter aan en herbekijk de hash. Wat valt op?

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. Dat doet het Git-commando zelf ook, bvb. bij git log --oneline.

git log --oneline22b1b17 (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

Commit Diff

Dat 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 bestandsversies. 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 22b1b17commit 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)
Extra: Blobs en Diffs

Git slaat bestanden op als een blob (Binary Large OBject). Waarom en wat dat precies inhoudt is minder belangrijk, maar onthoud dat een blob één bepaalde versie is van een bestand.

In de git show output hierboven zien we de volgende lijn:

index e0f8470..13b7d2a 100644

100644 heeft betrekking op 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-objecten) bekijken:

git cat-file blob e0f8470message = "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 versies vergelijkt, dan merk je dat dit inderdaad precies de diff is die het git show-commando laat zien.

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 --oneline22b1b17 (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:

Branches

Een branch in Git is gewoon een vertakking van de tijdlijn van je code. Je gebruikt branches zodat je veilig en overzichtelijk kan werken:

  • Isolatie – je kan nieuwe features of fixes uitproberen zonder de stabiele code te breken.
  • Samenwerken – meerdere mensen (of AI agents) kunnen tegelijk aan verschillende dingen werken zonder elkaar in de weg te zitten.
  • Experimenteren – je kan ideeën testen, en als het niets is, gooi je de branch gewoon weg.

"Refs" en 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) of graph.

master of main?

De "default" branch in Git was historisch altijd master. In de huidige versie is dit nog steeds het geval. Echter is deze naam ondertussen configureerbaar. Tevens raadt het git init commando aan een andere naam te configureren. Tegenwoordig is de meest gebruikte naam main en is dit ook de default naam op populaire diensten als GitHub en GitLab.

$ git config --global init.defaultBranch main

De main branch is heilig

De main branch wordt altijd gezien als de stabiele en “officiële” versie van de code. Die code moet altijd functioneel zijn en vormt ook de basis waaruit alle ontwikkelaars werken.

Meestal hebben slechts een beperkt aantal ontwikkelaars de nodige rechten om in main te werken.

Omdat elke commit naar zijn voorganger (parent) verwijst hoeft Git voor een branch eigenlijk enkel te onthouden wat de laatste commit is voor die branch. Git gebruikt hiervoor references (of kort: refs).

Daarom zien we in de output van git log --oneline by the laatste commit main staan (en ook origin/main aangezien ook op de remote dit de laatste commit is op de main branch).

In diezelfde output zien we ook dit staan: HEAD -> main.

HEAD is een speciale reference en verwijst normaal gezien naar de naam van de huidige branch.

Extra: Refs zijn bestanden

Net als al de rest in Git zijn refs ook gewone bestanden in de .git/ directory. Een ref-bestand bevat dus de commit sha waar die bepaalde ref naar verwijst.

cat .git/HEADref: refs/heads/main
cat .git/refs/heads/main22b1b17cd1b19d5da6f451fe3b066aee2dc0620d

Branches maken

Tot nu toe hebben we enkel op de default of main branch gewerkt. Een nieuwe branch kan op twee manieren gemaakt worden:

  • git branch <name> maakt een nieuwe branch maar blijft op de huidige branch in de working directory
  • git switch -c <name> maakt (-c = create) een nieuwe branch en switcht ook naar die branch
git branch cool-featuregit switch -c another-featureSwitched 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 statusOn branch another-feature
nothing to commit, working tree clean

git switch cool-featureSwitched to branch 'cool-feature'

git branch -a main
cool-feature
* cool-feature
remotes/origin/HEAD -> origin/main
remotes/origin/main

Merk op dat er intern in de Git repo weinig veranderd is! Er zijn twee refs gemaakt voor de twee nieuwe branches en beide verwijzen naar dezelfde commit, namelijk de commit waarnaar ook de main branch verwijst. Een nieuwe branch "start" dus altijd vanaf de commit waar de huidige branch naar verwijst.

.git/HEAD
ref: refs/heads/cool-feature
.git/refs/heads/main
22b1b17cd1b19d5da6f451fe3b066aee2dc0620d
.git/refs/heads/cool-feature
22b1b17cd1b19d5da6f451fe3b066aee2dc0620d
.git/refs/heads/another-feature
22b1b17cd1b19d5da6f451fe3b066aee2dc0620d
Extra: git checkout

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 commando's om dit te verhelpen. De git checkout-commando's kan je wel nog gebruiken, maar het is aan te raden direct de nieuwere commando's te leren.

  • git switch cool-feature = git checkout cool-feature
  • git 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:

  1. De nieuwe commit verwijst (parent) naar de vorige commit van de branch.
  2. De branch reference wordt aangepast en verwijst nu naar de nieuwste commit.
  3. De HEAD ref wordt op dezelfde manier aangepast. Alle andere refs veranderen niet.

Na aanmaken van een nieuw bestand:

test_hello.py
# Tests for the hello module
git statusOn 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.pygit 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 --oneline24e6e1a (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

Nog geen boom

We hebben nog steeds niet echt een "boomstructuur" met onze commits.

Als we aanpassingen committen op de another-feature branch, dan komt daar verandering in.

git switch another-featureSwitched to branch 'another-feature'
echo 'print("Goodbye")' > goodbye.pygit add goodbye.pygit commit -m "Add goodbye module"[another-feature 19f7b7a] Add goodbye module
1 file changed, 1 insertion(+)
create mode 100644 goodbye.py

Note

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.

Explain Git with D3

Met de volgende tools krijg je een interactieve visualisatie van wat er eigenlijk gebeurt wanneer je verschillende Git-commando's uitvoert. Dit kan heel erg nuttig zijn om eenvoudig te experimenteren en de verschillende concepten volledig onder de knie te krijgen.

Extra: Directed Acyclic Graph

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 Acyclic 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 acyclic 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

  1. Clone de repo van https://github.com/dvx76/git-this.
  2. Bekijk de lijst met branches.
  3. Bekijk voor elke branch de lijst met commits.
  4. Probeer op een blad papier de volledige commit tree uit te tekenen.
  5. Simuleer wat er gebeurt met https://dvx76.github.io/explain-git-with-d3/, en/of maak level 2 in de reeks Introduction Sequence op https://learngitbranching.js.org/.

Tip

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 de lokale versie van een remote branch.

Hier komen we later nog op terug.

Mergen

NOTES

  • Lokaal!
  • source, target
  • 3 mogelijkheden:
    • FF, branch -d
    • 3-Way, commit msg, editor, commits afspelen, --graph
    • Conflict
  • D3 gebruiken!

Nog steeds volledig lokaal

Net als in het vorige deel werken we opnieuw volledig lokaal! In het volgende hoofdstuk komt 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 waar de twee branches uit elkaar lopen (divergeren), noemen we het branchpunt (branchpoint), of merge base (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-feature de source branch
  • main de target branch
  • 22b1b17 de merge base

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 basisscenario's die zich bij een merge kunnen voordoen:

  1. Fast-Forward Merge
  2. True (of three-way) Merge
  3. 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-featureUpdating 22b1b17..24e6e1a
Fast-forward
test_hello.py | 1 +
1 file changed, 1 insertion(+)
create mode 100644 test_hello.py
git log --oneline24e6e1a (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

explain-git-with-d3 kan de operatie duidelijker in beeld brengen.

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 deleten, krijg je een foutmelding!

git branch -d cool-featureDeleted branch cool-feature (was 24e6e1a).

git branch -d another-featureerror: 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 branchpunt (de aftakking) nieuwe commits zijn op zowel de source branch 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.

Na de vorige fast-forward merge zitten we eigenlijk al in die situatie!

Extra: Commit graph commando

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.md

Dan mergen we de another-feature branch in de main branch.

git switch main
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 teksteditor om een nieuwe commit message te schrijven! Standaard zal dit Merge branch 'another-feature' zijn en dit kunnen we zo laten. Met de --no-edit optie kunnen we de voorgestelde commit message direct aanvaarden.

Wat is er gebeurd?

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) toegepast op de main branch. Op het einde worden alle aanpassingen samen vastgelegd in een nieuwe commit op de main branch. Dit noemen we een merge commit. Die commit bevat dus alle aanpassingen van alle commits van de another-feature branch.

Die merge commit krijgt ook twee parent commits: de vorige 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:

Opnieuw kan een simulatie met explain-git-with-d3 een beter beeld geven van wat er precies gebeurt.

Extra: Mergestrategieën

In de output van het git merge commando zien we Merge made by the 'ort' strategy.

Een merge strategy bepaalt 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. Vroeger was dit de recursive strategie.

In deze cursus gaan we niet verder in op mergestrategieën.

Extra: Three-way?

Waarom noemen we dit ook een three-way merge?

Simpelweg omdat er gewerkt wordt vanuit drie punten (commits) uit de commit graph:

  1. De gemeenschappelijke voorouder (branching point).
  2. De tip (laatste commit) van de source branch.
  3. 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 waarin er tussen de source branch en de target branch conflicten zijn 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-42Switched 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.py"[feature-42 e8ba000] Use goodbye in hello.py
1 file changed, 2 insertions(+), 1 deletion(-)

Terwijl er aan deze feature werd gewerkt, is hello.py op de main branch 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 maingit 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 oudere commits laten we voor de duidelijkheid achterwege):

git log --oneline --graph --decorate --all 24e6e1a..* 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'
* 24e6e1a Add goodbye module

Tot zover geen verschillen met een gewone merge. Als we nu echter de feature-42 branch willen mergen in main, krijgen we het volgende probleem:

git merge feature-42Auto-merging hello.py
CONFLICT (content): Merge conflict in hello.py
Automatic merge failed; fix conflicts and then commit the result.

git statusOn 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

Git 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 aan jou om het probleem (conflict) op te lossen. Er wordt ook aangegeven in welk bestand (of welke bestanden) er problemen zijn.

Dit is typisch een van de momenten waarop nieuwe Git-gebruikers in paniek raken. De situatie is echter meestal vrij eenvoudig op te lossen en zoals gewoonlijk wijst git status je onmiddellijk de weg.

Als we het bestand in kwestie nu bekijken zien we het volgende:

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-42

Dit is de manier waarop Git aangeeft waar er in het bestand een conflict is, en wat het conflict precies is. Elk blok tussen <<<<<<< en >>>>>>> is één conflict dat opgelost moet worden. Tussen <<<<<<< en ======= zien we de code zoals die bestaat op de target branch. Tussen ======= en >>>>>>> staat de code uit de source branch.

Extra: 'Merge Conflict'

https://wizardzines.com/comics/oh-shit-merge-conflict/

Het is nu aan ons om te beslissen hoe we dit willen oplossen. Uit de source branch blijkt 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 aan de oorspronkelijke tekst kwijt. Om hetzelfde resultaat te behouden passen we ook goodbye.py aan:

goodbye.py
def say_goodbye():
    print("Goodbye and thanks for all the fish!")

Onze status ziet er nu als volgt uit:

git statusOn 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.py

Rest nog: alle aanpassingen samen 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 merge.

Oefening

  1. Clone de repo van https://github.com/dvx76/git-this
  2. Bekijk de commit history
  3. Merge de branch more-cities in main
  4. Merge de branch strings-doc in improve-strings
  5. Merge de branch bigfloat in improve-floats
  6. Extra: Welke branches hebben commits die nog niet op main bestaan? Merge deze branches in main.

Tip

Gebruik git status als je even de weg kwijt bent!


Samenvatting

Extra: 'Meet the merge'

https://wizardzines.com/comics/meet-the-merge/

Samenvatting

  • Git is gedistribueerd: elke gebruiker heeft een volledige kopie van de repo, inclusief de volledige geschiedenis.
  • Slechts drie commando's wisselen informatie uit met een Git server: git clone, git push en git fetch/git pull. Alle andere commando's werken volledig lokaal.

  • Een remote is een andere kopie van de repo (bvb. GitHub). origin is de conventionele naam voor de remote waarvandaan je de repo hebt gecloned (git clone <url>); git remote -v toont alle gekende remotes met hun URL.
  • git push stuurt je gecommitte aanpassingen naar de remote - aanpassingen die nog niet gecommit zijn kan je dus nooit rechtstreeks pushen.
  • git fetch downloadt nieuwe aanpassingen zonder je lokale branches te wijzigen; git merge voegt ze daarna lokaal samen.
  • git pull = git fetch + git merge.
  • Een bestaande lokale repo koppel je aan GitHub met git remote add origin <url> en git push -u origin main.

  • Een commit is een snapshot van alle bestanden. De commit hash (SHA-1) identificeert de commit én bewaakt de data-integriteit.
  • Elke commit verwijst naar zijn parent(s); een merge commit heeft twee parents. Zo ontstaat de commit tree (of commit graph).
  • Refs wijzen commits aan: HEAD is de huidige commit, een branch is een ref naar de laatste commit van die branch. Een nieuwe branch maken betekent dus enkel een extra ref naar de huidige commit - de commits zelf veranderen niet!
  • Branches geven isolatie, samenwerking (ook met AI agents) en experimenteerruimte: maken met git branch <naam> of git switch -c <naam>, switchen met git switch <naam>, overzicht met git branch -a, verwijderen met git branch -d.

  • Mergen brengt de commits van een source branch in een target branch. Het branchpoint (merge base) is de gemeenschappelijke voorouder waar de branches uiteenlopen.
  • Drie scenario's: fast-forward (de target heeft geen nieuwe commits - de branch-ref schuift gewoon door naar de laatste commit), three-way merge (beide branches hebben nieuwe commits - een merge commit met twee parents) en merge conflict (Git kan niet automatisch beslissen).
  • Bij een conflict plaatst Git conflictmarkers in het bestand (<<<<<<<, ======= en >>>>>>>). Oplossen, git add, committen als merge-commit - of alles terugdraaien met git merge --abort.

Belangrijke nieuwe commando's:

git clone <url>

git remote add origin <url>

git remote -v

git pull

git push
git push -u origin main

git merge

git branch <naam>
git switch -c <naam>
git switch <naam>

Wat moet je kunnen?

  • Je begrijpt het verschil tussen gecentraliseerd en gedistribueerd versiebeheer en waarom er bij Git maar drie commando's met een server communiceren.

  • Je kan een repo clonen en uitleggen wat een remote en origin zijn.
  • Je kan de remote-informatie in de output van git status interpreteren (up to date, ahead, behind).
  • Je kan lokale commits pushen en aanpassingen van een remote fetchen of pullen.

  • Je begrijpt de opbouw van een commit: hash, parent(s) en snapshot.
  • Je begrijpt wat refs zijn en welke rol HEAD en branches daarin spelen; je kan uitleggen dat een nieuwe branch maken niets aan de commits zelf verandert.

  • Je kan branches maken, wisselen en verwijderen (git branch, git switch, git switch -c, git branch -d).
  • Je kan de commit tree interpreteren, bvb. met git log --oneline --graph --decorate --all.

  • Je weet wat een source branch, een target branch en het branchpoint zijn; je kan een branch mergen met git merge.
  • Je herkent de drie merge-scenario's en kan ze uitleggen:
    • fast-forward als de target branch geen nieuwe commits heeft
    • three-way merge met een merge commit als beide branches nieuwe commits hebben
    • merge conflict als Git de aanpassingen niet automatisch kan samenvoegen
  • Je kan een merge conflict oplossen: de conflictmarkers begrijpen, de juiste code kiezen, git add, committen als merge-commit - of git merge --abort om terug te keren.

  • Je weet dat git status je bij elke stap de weg wijst, ook midden in een merge!