Ga naar inhoud

Samenwerken met Git en GitHub

NOTES

Voorbereiding

Start van de les

  • Vragen over vorige les? Problemen?

Herhaling

Commando Beschrijving
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 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 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 clone [remote] Maakt een lokale kopie van een remote repo. Niet in repo uitvoeren.
git remote -v Lijst met alle gekende remotes.
git push Stuur nieuwe commits op de huidige branch naar de remote.
git fetch Download nieuwe commits vanaf de remote. Update de lokale branch niet.
git pull [filename] Fetch + update (merge) in de lokale branch.

Fast-Forward Merge

True (3-Way) Merge

Samenwerken

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 om er op een efficiënte manier mee om te gaan.

In Remotes hebben we reeds de basisconcepten van remotes bekeken.

Belangrijk!

Je lokale repo bevat een kopie van de remote(s) zoals die was op het moment van de laatste fetch (of op het moment van de initiële clone). Als je samenwerkt met verschillende ontwikkelaars kan het natuurlijk gebeuren dat iemand anders de remote wijzigt (push) sinds jouw laatste fetch.

Basis

Bob en Alice werken samen aan een nieuwe feature. Hiervoor hebben ze een aparte branch aangemaakt (new-feat) en naar de gemeenschappelijke Git server gepusht. De startsituatie voor Bob ziet er als volgt uit (andere branches zoals main worden voor de eenvoud niet weergegeven):

De main branch is heilig

Waarom zouden Bob en Alice niet gewoon rechtstreeks op de main branch werken?

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. Het is dus erg belangrijk dat de main branch stabiel blijft.

Door op aparte branches te werken kan nieuwe code ook eerst nagekeken (review) worden door andere ontwikkelaars. Als alles OK is, kan zo'n branch in de main branch gemergd worden.

Meestal hebben slechts een beperkt aantal ontwikkelaars de nodige rechten om in main te mergen (laat staan om rechtstreeks te pushen naar main).

Bob

In Bobs lokale repo heeft hij natuurlijk een new-feat branch. Dit is een lokale branch waar Bob rechtstreeks in kan werken.

Bob heeft een lokale representatie van de new-feat branch zoals die bestaat in de remote. Dit is een remote branch. Remote branches hebben altijd een naam in de vorm <remote-name>/<branch-name>. In dit geval dus origin/new-feat.

Als Bob iets commit in zijn lokale new-feat branch zal er niets veranderen in zijn origin/new-feat branch.

Met het git branch -a commando (-a voor all) krijgen we zowel de lokale als de remote branches te zien. Het * teken geeft aan op welke lokale branch we momenteel zitten.

git branch -a main
* new-feat
remotes/origin/HEAD -> origin/main
remotes/origin/main
remotes/origin/new-feat

Met git status krijgen we informatie over de verhouding tussen de (huidige) lokale branch en een eventuele remote branch.

git statusOn branch new-feat
Your branch is up to date with 'origin/new-feat'.

nothing to commit, working tree clean

De lokale new-feat branch is gelinkt met de remote origin/new-feat branch. We zeggen dat new-feat origin/new-feat trackt (tracking). We zien ook dat de lokale new-feat up-to-date is met de remote origin/new-feat.

Let op!

Bij het uitvoeren van git status (en de meeste andere git commando's) wordt er geen contact gemaakt met de server. De lokale new-feat is dus up-to-date met origin/new-feat zoals die bestond op het moment van de laatste git fetch!

Met git remote show krijgen we nog meer informatie over één bepaalde remote.

git remote show origin* remote origin
Fetch URL: https://github.com/dvx76/git-demo.git
Push URL: https://github.com/dvx76/git-demo.git
HEAD branch: main
Remote branches:
main tracked
new-feat tracked
Local branches configured for 'git pull':
main merges with remote main
new-feat merges with remote new-feat
Local refs configured for 'git push':
main pushes to main (up to date)
new-feat pushes to new-feat (up to date)

Alice

Bobs collega, Alice, heeft ook een lokale clone van dezelfde repo. Alice vindt de benaming origin niet zo duidelijk en heeft de remote hernoemd naar server.

Alice commit een aanpassing op haar lokale new-feat branch. Hierdoor komt haar lokale new-feat voor te staan (ahead) op de remote server/new-feat. Een git status vertelt Alice het volgende:

git statusOn branch new-feat
Your branch is ahead of 'server/new-feat' by 1 commit.
(use "git push" to publish your local commits)

nothing to commit, working tree clean

new-feat is dus 1 commit ahead op de remote.

Met git push update Alice de remote.

Bob moet fetchen

Bob loopt nu eigenlijk achter op de server, maar weet het nog niet. Vanuit zijn standpunt is hij nog perfect up-to-date! Met het git fetch commando wordt zijn lokale representatie van de remote geüpdatet.

Met git fetch is enkel origin/new-feat lokaal geüpdatet! De lokale new-feat branch is (nog) niet aangepast en verwijst nog steeds naar dezelfde commit. Zoals gewoonlijk geeft git status alle informatie die we nodig hebben:

git statusOn branch new-feat
Your branch is behind 'origin/new-feat' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

nothing to commit, working tree clean
  • Bobs lokale branch zit 1 commit achter (behind) op de remote
  • Hij kan fast-forwarden, omdat hij zelf geen nieuwe commits op new-feat heeft
  • Met git pull (of git merge) update hij de lokale branch

Oefening

Vorm groepen van twee en kies wie Alice en wie Bob is.

  1. Clone de repo https://github.com/dvx76/git-demo.git
  2. Switch naar de branch team-1 voor groep 1, ...
  3. Alice maakt een aanpassing aan een bestand en commit deze
  4. Alice pusht naar de remote
  5. Bob fetcht van de remote
  6. Bob update de lokale branch
  7. Bob maakt een aanpassing aan een bestand en commit deze
  8. Bob pusht naar de remote
  9. Alice update de lokale branch

Wat zal er gebeuren als Bob en Alice "tegelijk" een nieuwe commit proberen te pushen?

Mergen

Tot nu toe was het voor Bob erg eenvoudig om de commit(s) van Alice op zijn eigen lokale branch te krijgen. Hij had namelijk zelf nog niets gecommit op diezelfde branch!

In de praktijk gaat dit zelden zo gemakkelijk. Bob zit natuurlijk niet te wachten op Alice en heeft zelf al een of meer commits op zijn lokale new-feat branch staan.

Opnieuw heeft Alice haar nieuwe commit al gepusht naar de server. Bob heeft ondertussen op zijn lokale branch 2 commits toegevoegd.

Bob probeert te pushen

Bob is tevreden met zijn commits en wil ze naar de server pushen. Hij krijgt de volgende foutmelding:

git pushTo https://github.com/dvx76/git-demo.git
! [rejected] new-feat -> new-feat (non-fast-forward)
error: failed to push some refs to 'https://github.com/dvx76/git-demo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

The tip of your current branch is behind its remote counterpart. Git merkt dat Bobs lokale branch achterloopt op de remote. Daardoor kan Bob niet pushen, want anders zou de commit van Alice verloren gaan. Bob moet dus zijn lokale branch eerst updaten.

Maar helaas:

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), 1.00 KiB | 513.00 KiB/s, done.
From https://github.com/dvx76/git-demo
629f30e..4c62d28 new-feat -> origin/new-feat
hint: Diverging branches can't be fast-forwarded, you need to either:
hint:
hint: git merge --no-ff
hint:
hint: or:
hint:
hint: git rebase
hint:
hint: Disable this message with "git config set advice.diverging false"

Diverging branches can't be fast-forwarded. Git kan niet automatisch de nieuwe commits uit de remote op Bobs lokale branch overzetten omdat zijn eigen commits in de weg staan.

De situatie ziet er ongeveer als volgt uit voor Bob:

De foutmelding van het git pull commando geeft ook twee mogelijke opties:

  • git merge, of
  • git rebase

Merge

Mergen (three-way merge, zonder fast-forward) kennen we al. Bij het git merge commando stelt Git automatisch de commit message "Merge remote-tracking branch 'refs/remotes/origin/new-feat' into new-feat" voor die we gewoon kunnen aanvaarden.

git mergeMerge made by the 'ort' strategy.
hello.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
git statusOn branch new-feat
Your branch is ahead of 'origin/new-feat' by 3 commits.
(use "git push" to publish your local commits)

nothing to commit, working tree clean

git log --oneline9c72952 (HEAD -> new-feat) Merge remote-tracking branch 'refs/remotes/origin/new-feat' into new-feat
5cc2948 Add helper function for goodbye
9cfcf72 Add initial goodbye module
4c62d28 (origin/new-feat) Use a function to say hello
629f30e Hello from Alice
22b1b17 (origin/main, origin/HEAD, main) Add module docstring in hello.py

Wat opvalt is dat we nu drie commits voorlopen op de remote, terwijl we maar twee nieuwe commits hebben om te pushen. Dit komt door de merge die een extra merge-commit heeft toegevoegd.

Bob kan nu naar de remote pushen met een gewoon git push commando.

Oefening

We werken verder met de Alice-Bob-teams en de https://github.com/dvx76/git-demo.git repo.

  1. Alice maakt een aanpassing, commit deze en pusht naar de remote.
  2. Bob maakt een aanpassing maar niet in hetzelfde bestand en commit deze.
  3. Bob probeert te pushen.
  4. Bob probeert te pullen.
  5. Bob merget de remote branch in zijn lokale branch en pusht dan.
  6. Alice pullt opnieuw en beiden bekijken de commit history.

Conflicten

Bob en Alice werkten in verschillende bestanden. Daardoor kon Git zonder tussenkomst de commits van Alice in Bobs lokale branch mergen. Maar wat als beiden binnen hetzelfde bestand hebben gewerkt? Of om specifiek te zijn: op dezelfde lijnen binnen hetzelfde bestand!

De startsituatie is opnieuw dezelfde, alleen bevat Bobs nieuwe commit nu een aanpassing in hetzelfde bestand als Alices commit...

Alles verloopt hetzelfde tot het moment dat Bob probeert de remote in zijn lokale branch te mergen:

git mergeAuto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

git statusOn branch new-feat
Your branch and 'origin/new-feat' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)

You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)

Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

Git kon er niet in slagen de commit(s) van de remote branch automatisch te mergen in de lokale branch. 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 welk(e) bestand(en) er problemen zijn.

Het oplossen van dit conflict verloopt op dezelfde manier als bij een lokale merge, zoals we reeds gezien hebben.

Ter herhaling, als we het bestand in kwestie nu bekijken, krijgen we het volgende te zien:

README.md
A dummy repo to use during Git courses.
<<<<<<< HEAD
Bob was here
=======
Alice was here.
>>>>>>> refs/remotes/origin/new-feat

Dit is de manier waarop Git aangeeft waar 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 (in dit geval Bobs lokale branch). Tussen ======= en >>>>>>> de code uit de source branch (in dit geval de remote branch).

Het is nu aan Bob om te beslissen hoe hij dit wil oplossen. Er zijn eigenlijk 4 mogelijkheden:

  1. Gebruik Bobs versie (Current Change).
  2. Gebruik Alices versie (Incoming Change).
  3. Gebruik beide (na elkaar).
  4. Maak zelf een aangepaste versie.

Bob kiest voor optie 4 en maakt er het volgende van:

README.md
A dummy repo to use during Git courses.
Bob and Alice were here.

Na het opslaan van het bestand krijgt Bob deze status:

git statusOn branch new-feat
Your branch and 'origin/new-feat' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)

You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)

Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

Het enige dat nog rest, is 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

Oefening

We werken verder met de Alice-Bob-teams en de https://github.com/dvx76/git-demo.git repo.

  1. Switch naar de branch team-1 voor groep 1, ...
  2. Alice maakt een aanpassing, commit deze en pusht naar de remote.
  3. Bob maakt een aanpassing op dezelfde lijn als Alice en commit deze.
  4. Bob probeert te pushen.
  5. Bob probeert te pullen.
  6. Bob merget de remote branch in zijn lokale branch en pusht dan.
  7. Alice pullt opnieuw en beiden bekijken de commit history.

Feature branches & Pull Requests

Een Feature Branch is niets meer dan een gewone branch waar nieuwe functionaliteit in wordt ontwikkeld. Eerder hebben we het gehad over samenwerken binnen zo'n feature branch. Uiteindelijk zal de nieuwe code af zijn en is het de bedoeling dat die code een weg vindt naar de main branch.

We zouden een ontwikkelaar met de nodige machtiging kunnen vragen om de code in de feature branch te bekijken, feedback te geven en uiteindelijk in main te mergen. Zo'n proces is bijzonder onhandig en maakt het bovendien moeilijk om verschillende personen te betrekken. Bij grote projecten verlies je ook nog eens snel het overzicht.

Een Pull Request (of kort "PR") is eigenlijk een formele implementatie van dit proces. Met een pull request vraag (request) je om de aanpassingen uit jouw feature branch te pullen naar de main branch. GitHub voorziet hiervoor een specifieke interface waarmee je een pull request kan openen. Iedereen kan dan de veranderingen in de request bekijken en rechtstreeks feedback geven op specifieke stukken code. Zo nodig wordt de nieuwe code dan verder aangepast. Als iedereen tevreden is wordt de pull request aanvaard en automatisch gemerged in main.

Pull Request is GitHub, niet Git

Pull Requests zijn een functionaliteit van GitHub en niet van Git zelf. Er bestaat dus bvb. geen git pull-request commando.

  • Git wordt gebruikt om nieuwe code te committen in een branch en te pushen naar GitHub.
  • GitHub wordt gebruikt om een pull request te openen. Die pull request gebruikt Git om bvb. een diff te tonen tussen de feature branch en de main branch.
  • GitHub gebruikt ook Git om uiteindelijk de feature branch te mergen in main.

Pull Request of Merge Request

Sommige andere hostingplatformen zoals GitLab gebruiken de term Merge Request i.p.v. Pull Request voor hetzelfde proces en dezelfde functionaliteit. De term merge request is op zich logischer aangezien de feature branch wordt gemerged via de merge request. GitHub is echter zo populair dat er doorgaans vooral over pull requests wordt gesproken.

Een Pull Request openen

We werken verder vanuit de git-demo repo (https://github.com/dvx76/git-demo). We willen een nieuwe feature branch starten met de bedoeling deze via een pull request in main te mergen.

Vermijd conflicten...

Om de kans op extra merges en conflicten te minimaliseren is het belangrijk eerst de lokale main branch te updaten voordat we de nieuwe branch maken.

$ git switch main
$ git pull
$ git switch -c string-examples

De git-demo repo bevat een python_examples directory waaraan we voorbeelden van eenvoudige Python-syntax en -functies willen toevoegen. Als voorbeeld voegen we iets toe in strings.py.

python_examples/strings.py
"""Voorbeelden: werken met strings."""

# String "slicen": Met string_var[x:y] krijg je het deel van de string
# van karakter x t.e.m. karakter y. Het eerste karakter is 0!
string_var = "Welkom bij Syntra"
print(string_var[3:6])
# output: kom
git diffdiff --git a/python_examples/strings.py b/python_examples/strings.py
index 65aa152..88b317a 100644
--- a/python_examples/strings.py
+++ b/python_examples/strings.py
@@ -1 +1,7 @@
"""Voorbeelden: werken met strings."""
+
+# String "slicen": Met string_var[x:y] krijg je het deel van de string
+# van karakter x t.e.m. karakter y. Het eerste karakter is 0!
+string_var = "Welkom bij Syntra"
+print(string_var[3:6])
+# output: kom

git statusOn branch string-examples
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: python_examples/strings.py

no changes added to commit (use "git add" and/or "git commit -a")

git commit -a -m "Add string example"[string-examples 99ff974] Add string example
1 file changed, 6 insertions(+)

git statusOn branch string-examples
nothing to commit, working tree clean

Om te pushen moeten we Git eerst vertellen naar welke remote en branch we willen pushen.

Bij het pushen krijgen we al direct van GitHub (en niet Git zelf, maar wel via Git) een behulpzame link om rechtstreeks een pull request voor deze nieuwe branch te openen!

git pushfatal: The current branch string-examples has no upstream branch.
To push the current branch and set the remote as upstream, use

git push --set-upstream origin string-examples

To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.
git push --set-upstream origin string-examplesEnumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 10 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 1.12 KiB | 1.12 MiB/s, done.
Total 4 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
remote:
remote: Create a pull request for 'string-examples' on GitHub by visiting:
remote: https://github.com/dvx76/git-demo/pull/new/string-examples
remote:
To https://github.com/dvx76/git-demo.git
* [new branch] string-examples -> string-examples
branch 'string-examples' set up to track 'origin/string-examples'.

Naast deze rechtstreekse URL zijn er nog een aantal andere manieren om een pull request te openen. Op de homepage van het project (https://github.com/dvx76/git-demo) klik je op Pull requests en ga je daarna door naar New pull request. Aan de linkerkant moet je de target branch (hier main) selecteren, en rechts de source branch (hier string-examples). Vervolgens krijgen we een overzicht van de pull request: 1 commit, 1 aangepast bestand, enz.

Als we op Create pull request klikken kunnen we verdere details invullen:

  • De titel van de PR hou je best kort en duidelijk, net zoals de eerste lijn van een commit message.
  • De beschrijving: hier leg je uit wat de bedoeling is van de aanpassingen in de PR.
  • Andere velden en opties zijn optioneel en die laten we nog leeg.

Als de PR eenmaal is aangemaakt krijgt hij een unieke ID en kan je hem terugvinden in de lijst met PRs (https://github.com/dvx76/git-demo/pulls).

Een Pull Request reviewen

We bekijken samen hoe je feedback kan geven op de PR, zowel algemeen als op specifieke stukken nieuwe code.

Eenmaal tevreden met de inhoud van de PR wordt deze goedgekeurd (approve) en gemerged.

Oefening

Werk voor deze oefening individueel.

  1. Zorg dat je lokale main branch van de git-demo repo up-to-date is.
  2. Kies een van de bestanden uit python_examples waar je iets aan kan toevoegen. Je mag ook altijd een nieuw bestand toevoegen.
  3. Maak een nieuwe branch aan. Kies de naam in functie van wat je van plan bent toe te voegen.
  4. Commit je verandering en push de branch.
  5. Open een pull request om je branch in main te mergen.
  6. Als je klaar bent bekijk dan de andere pull requests en geef feedback. Bekijk de feedback op je eigen pull request.
  7. We bekijken samen elke pull request en lossen eventuele conflicten op.

Forken

Wanneer je een repo van iemand anders clonet, dan zou je een eigen kopie van die repo kunnen maken onder je eigen GitHub account door:

  1. Onder je eigen account een nieuwe (lege) repo te maken met dezelfde naam.
  2. Deze repo lokaal toevoegen als extra remote (bvb. git remote add bob git@github.com:bob/git-demo.git)
  3. Pushen naar die remote (bvb. git push bob main)

GitHub voorziet een eenvoudige manier om dat allemaal automatisch in één stap te doen en met bijkomende voordelen! GitHub noemt zo'n kopie een fork. Het maken van zo'n kopie noemen we een repo forken.

Je kan nu rechtstreeks je eigen repo lokaal clonen.

GitHub slaat de fork-relatie intern op. Zo staat er onder de naam van de repo een link naar het oorspronkelijke project, bvb. forked from fastapi/fastapi.

Een bijkomende eigenschap van forks is dat Pull Requests standaard geopend worden met de oorspronkelijke repo als de target.

Het proces ziet er dan als volgt uit:

  1. In je lokale clone van je eigen fork maak je een nieuwe branch met daarin een of meerdere nieuwe commits.
  2. Je pusht deze branch naar je eigen fork.
  3. Wanneer je nu een pull request opent (bvb. via de link die je krijgt bij de push) zal de oorspronkelijke repo geselecteerd zijn.

Forks zijn enorm populair bij open source projecten op GitHub. Het belangrijkste voordeel is dat je eender wie kan toelaten een pull request te openen zonder specifieke toegang te geven aan de repo.

Oefening:

Cowsay is een bekende kleine applicatie waarmee je een ASCII-tekening van een koe met een gekozen bericht in je terminal laat verschijnen.

Koezeg is een eenvoudige Python implementatie van hetzelfde programma.

  1. Neem online de README door van de https://github.com/dvx76/koezeg repo.
  2. Fork de repo naar je eigen GitHub account.
  3. Clone je fork lokaal.
  4. Kies een van de issues uit die je wil aanpakken en implementeer lokaal een oplossing. Je mag je zeker door AI laten ondersteunen hiervoor - de oefening draait immers niet rond de code zelf.
  5. Commit je aanpassing lokaal op een feature branch
  6. Push je aanpassing naar je GitHub fork en open een pull request
  7. Als je klaar bent bekijk dan de andere pull requests en geef feedback. Bekijk de feedback op je eigen pull request.

Git Rebase

Om conflicten op te lossen en een feature branch te updaten met de main branch hebben we tot nu toe merge gebruikt. Het is je misschien opgevallen dat daardoor de commit history nogal slordig en onoverzichtelijk wordt, ongeveer zoals dit:

git log --oneline --no-decorate --graph* b71af35 Another strings example
* d5130be Merge branch 'main' into new-feat
|\
| * dda5919 Updates to numbers
* | b82b0de Another feature
* | b4df073 Merge branch 'main' into new-feat
...

Dat komt omdat er telkens een merge commit wordt toegevoegd (met 2 parents dus, de oorspronkelijke laatste commit op de feature branch en de laatste commit op main).

Rebase

Een rebase is een manier om de nieuwe commits op de feature branch (new-feat) opnieuw te maken vanaf de laatste commit op de source branch (main).

Als je dit handmatig doet zou je de volgende stappen moeten doorlopen:

  1. Ga terug naar het branchpunt (git reset --hard 629f30e)
  2. Pull/merge de source branch, dit kan nu wel met fast-forward (git pull)
  3. Maak de aanpassingen van elke verloren commit opnieuw en maak nieuwe commits (git commit -a)
Extra: git reset

Met het git reset <commit-id> commando forceer je in één stap de huidige branch naar eender welke commit. Hierdoor kunnen we dus als het ware terug in de tijd gaan, door te resetten naar een oudere commit.

De aanpassingen uit de commits die hierdoor verloren gaan blijven (ongestaged) wel staan. Om ook die aanpassingen te vernietigen gebruik je het git reset --hard <commit-id> commando.

Oefen op explain-git-with-d3 om meer inzicht te krijgen in de werking van dit commando.

Veel werk, zelfs voor eenvoudige commits. git rebase doet dit allemaal automatisch.

Rebase kan gebruikt worden in verschillende situaties.

  • Bij pull requests, waarbij de target branch (meestal main) nieuwe commits heeft gekregen sinds we de feature branch hebben aangemaakt. In dit geval rebasen we de feature branch op main.
  • Tijdens het samenwerken op eenzelfde remote branch (zoals met Bob en Alice in de vorige hoofdstukken). In dat geval rebasen we de lokale branch op de remote.
git rebaseSuccessfully rebased and updated refs/heads/new-feat.

git statusOn branch new-feat
Your branch is ahead of 'origin/new-feat' by 2 commits.
(use "git push" to publish your local commits)

nothing to commit, working tree clean
git log --oneline8077d15 (HEAD -> new-feat) Add helper function for goodbye
0320454 Add initial goodbye module
4c62d28 (origin/new-feat) Use a function to say hello
629f30e Hello from Alice

Ditmaal hebben we slechts onze 2 nieuwe commits te pushen.

Nieuwe commits

Merk op dat de twee commits ook effectief nieuw zijn. De aanpassingen in elke commit zullen nog wel hetzelfde zijn, maar de commit ID is veranderd, want ook de parent van elke commit is veranderd!

Video

Boot.dev is een (betalend) online leerplatform met o.a. een goede module over Git. De auteur doorloopt zelf de volledige module op zijn YouTube kanaal. Je hoeft dus geen account of abonnement te nemen op boot.dev.

We bekijken samen een stukje over Git rebase:

Oefening

Rebase is een erg krachtig concept en wordt veel gebruikt, maar kan in het begin moeilijk te begrijpen zijn. Een oefening op https://learngitbranching.js.org/, met visualisatie van de commit tree, kan hierbij helpen.

De volgende oefeningen zijn interessant in het kader van rebase:

  • Main - Introduction Sequence - 4 (Rebase introduction)
  • Remote - Push & Pull - 7 (Diverged History)

Merge of Rebase?

Mergen of rebasen? Elk heeft zijn voor- en nadelen. In de volgende tabel krijg je een overzicht van de belangrijkste punten:

Voordelen Nadelen
Merge - Veilig: behoudt de volledige geschiedenis, niets wordt herschreven
- Eenvoudiger uit te voeren en te begrijpen
- Minder overzichtelijke geschiedenis
- Geen lineaire commit log
Rebase - Schone geschiedenis, lineaire commit log
- Geen rommelige en verwarrende merge-commits
- Geschiedenis herschrijven kan problematisch zijn bij samenwerking.
- Complexer voor beginners

Geschiedenis herschrijven

Omdat een rebase nieuwe commits met nieuwe commit IDs aanmaakt, wordt de geschiedenis van de branch herschreven. Op een eigen feature branch - waar jij als enige op werkt - is dat geen probleem, en zelfs de bedoeling.

Op de main branch of op een gedeelde branch is dit echter een groot probleem: andere ontwikkelaars hebben die commits al in hun lokale repo. De geschiedenis op de remote en in hun lokale repo's loopt na een rebase uit elkaar, met verwarring en fouten bij git pull en git push tot gevolg.

Rebase dus je eigen feature branches, maar herschrijf nooit de geschiedenis van main of van een gedeelde branch.

Rebase Conflicts

Net zoals bij een merge kunnen bij een rebase ook conflicten optreden.

We keren terug naar de vorige situatie waarbij Bob twee nieuwe commits heeft. Hij voegt een derde commit toe die ook hello.py aanpast, net op dezelfde lijnen als de reeds gepushte commit van Alice.

Als Bob nu probeert te rebasen gebeurt het volgende:

git rebaseAuto-merging hello.py
CONFLICT (content): Merge conflict in hello.py
error: could not apply 9465e01... Use goodbye module in hello
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply 9465e01... Use goodbye module in hello

Git kon de eerste twee commits (9cfcf72, 5cc2948) zonder problemen toepassen, maar bij de derde commit krijgen we een conflict dat we zelf moeten oplossen.

Net als bij een gewoon merge conflict moeten we de bestanden met conflicten handmatig oplossen. Als we daarmee klaar zijn kunnen we de rebase laten verdergaan met git rebase --continue. We krijgen dan ook nog de kans om de commit message aan te passen indien nodig. We krijgen dus geen merge-commit! Zoals gebruikelijk geeft git status ons altijd een duidelijk beeld van wat er gaande is en wat er moet gebeuren.

git statusinteractive rebase in progress; onto 4c62d28
Last commands done (3 commands done):
pick 5cc2948 Add helper function for goodbye
pick 9465e01 Use goodbye module in hello
(see more in file .git/rebase-merge/done)
No commands remaining.
You are currently rebasing branch 'new-feat' on '4c62d28'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: goodbye.py

Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: hello.py

git add hello.pygit rebase --continue[detached HEAD 896d701] Use goodbye module in hello
2 files changed, 4 insertions(+), 1 deletion(-)
Successfully rebased and updated refs/heads/new-feat.

Het kan zeker voorkomen dat na het oplossen van een conflict er tijdens het toepassen van de resterende commits opnieuw een conflict optreedt.

Zoals git rebase en git status al aangeven kunnen we er ook voor kiezen om de problematische commit helemaal over te slaan, met het git rebase --skip commando. Dit kan bijvoorbeeld voorkomen als de wijzigingen in de commit niet meer relevant zijn.


Samenvatting

Samenvatting

  • Je lokale repo bevat een kopie van de remote zoals die was op het moment van de laatste git fetch (of de initiële clone).
  • Bijna alle Git-commando's werken volledig lokaal, zonder contact met de server.
  • Naast je lokale branches houdt Git ook remote branches bij: <remote>/<branch> (bvb. origin/new-feat). git branch -a toont beide; het * teken geeft de huidige branch aan.
  • Een lokale branch trackt (meestal) één remote branch. git status toont de verhouding: up-to-date, ahead of behind (met het aantal commits). git remote show origin geeft nog meer details over de tracking.
  • git fetch downloadt nieuwe commits en updatet enkel je remote branches. Lokale branches veranderen niet. git pull = git fetch + merge. git push stuurt je commits naar de remote.

  • De main branch is heilig: die blijft altijd stabiel en functioneel. Nieuwe code hoort op een feature branch.
  • Als je lokale branch achterloopt wordt een git push geweigerd (non-fast-forward).
  • Als beide partijen nieuwe commits hebben, zijn de branches gedivergeerd en kan Git niet fast-forwarden. Je kiest dan merge of rebase.
  • git merge voegt de remote branch samen met je lokale branch via een merge-commit (three-way merge); daarna kan je pushen.
  • Een merge-conflict (beiden wijzigden dezelfde lijnen) los je lokaal op, net als bij een gewone merge. git add en committen als merge-commit. Met git merge --abort draai je alles terug.

  • Een Pull Request (PR) is de formele weg om je feature branch in main te krijgen: openen, reviewen, feedback geven op specifieke code, aanpassen en uiteindelijk approven en mergen. De PR toont het verschil tussen de source branch (jouw feature branch) en de target branch (main).
  • Pull Requests zijn GitHub, niet Git. Git doet de commits en de push; GitHub biedt de interface voor de PR.
  • Een nieuwe branch push je met git push --set-upstream origin <branch>.

  • Een fork is een eigen kopie van iemands repo onder je eigen account. GitHub houdt de fork-relatie bij (forked from ...).

  • Rebase herspeelt jouw commits bovenop de laatste commit van de target (bvb. main) in plaats van te mergen. Het resultaat is een lineaire geschiedenis zonder merge-commits - maar de commit IDs veranderen.
  • Ook bij een rebase kan een conflict optreden: oplossen, git add en verdergaan met git rebase --continue. Met git rebase --skip sla je een commit over, met git rebase --abort keer je terug naar de beginsituatie.

Belangrijke nieuwe commando's:

git branch -a

git remote show origin

git fetch
git pull

git push
git push --set-upstream origin <branch>

git merge

git rebase

Wat moet je kunnen?

  • Je begrijpt dat je lokale repo een kopie van de remote en niet automatisch geüpdatet wordt.

  • Je kan het verschil tussen lokale en remote branches uitleggen en ze tonen met git branch -a.
  • Je begrijpt wat tracking is en kan een nieuwe branch aan de remote koppelen.
  • Je kan de output van git status interpreteren: up-to-date, ahead en behind.
  • Je kan aanpassingen van de remote ophalen naar je lokale repo.
  • Je begrijpt waarom een git push geweigerd wordt (non-fast-forward) en hoe je dat oplost.

  • Je kan mergen bij divergerende branches en weet waarom dit een extra merge-commit oplevert.
  • Je kan een conflict bij samenwerking oplossen:.

  • Je kan een feature branch maken, pushen en er een pull request voor openen op GitHub.
  • Je kan feedback geven op een pull request en begrijpt hoe een PR gereviewd en gemerged wordt.
  • Je kan uitleggen wat een fork is.

  • Je begrijpt het verschil tussen merge en rebase en de gevolgen voor de commit history.