een nette, lineaire Git geschiedenis

een van de dingen die over het hoofd wordt gezien in veel op Git gebaseerde projecten is de waarde van een lineaire commit geschiedenis. In feite ontmoedigen veel tools en richtlijnen Git workflows die gericht zijn op een lineaire geschiedenis. Ik vind dit triest, omdat een nette geschiedenis is zeer waardevol, en er is een rechttoe rechtaan workflow die zorgt voor een lineaire geschiedenis.

Lineaire vs niet-lineaire geschiedenis

een lineaire geschiedenis is gewoon een Git geschiedenis waarin alle commits na elkaar komen. Dat wil zeggen: je zult geen merges vinden van branches met onafhankelijke commit geschiedenissen.

1 - niet-lineair-vs-lineair

Waarom wilt u een lineaire geschiedenis?

behalve netjes en logisch is een lineaire geschiedenis handig wanneer:

  • ik kijk naar de geschiedenis. Een niet-lineaire geschiedenis kan heel moeilijk te volgen zijn-soms tot het punt dat de geschiedenis gewoon onbegrijpelijk is.
  • wijzigingen in Backtracking. Bijvoorbeeld: “heeft feature a get introduced voor of na bugfix B?”.
  • bugs opsporen. Git heeft een zeer nette functie genaamd Git bisect, die gebruikt kan worden om snel te vinden welke commit een bug of regressie introduceerde. Echter, met een niet-lineaire geschiedenis wordt Git bisect moeilijk of zelfs onmogelijk te gebruiken.
  • wijzigingen ongedaan maken. Stel dat je een commit hebt gevonden die een regressie veroorzaakte, of je wilt een functie verwijderen die niet in een specifieke release had moeten verschijnen. Met wat geluk(als je code niet te veel veranderd is) kun je simpelweg de ongewenste commit (s) terugdraaien met Git revert. Echter, als je een niet-lineaire geschiedenis hebt, misschien met veel cross-branch merges, zal dit aanzienlijk moeilijker zijn.

er zijn waarschijnlijk nog een handvol situaties waarin een lineaire historie erg waardevol is, afhankelijk van hoe je Git gebruikt.

punt is: hoe minder lineair je geschiedenis is, hoe minder waardevol het is.

oorzaken van een niet-lineaire geschiedenis

kortom, elke merge commit is een Potentiële Bron van een niet-lineaire geschiedenis. Er zijn echter verschillende soorten merge commits.

Merge from a topic branch into master

wanneer u klaar bent met uw topic branch en deze wilt integreren in master, is een veelgebruikte methode om de topic branch samen te voegen in master. Misschien iets in de trant:

git checkout mastergit pullgit merge --no-ff my-topic-branch

een mooie eigenschap met deze methode is dat je de informatie bewaart over welke commits deel uitmaakten van je topic branch (een alternatief zou zijn om “–no-ff” weg te laten, wat Git zou toestaan om een fast-forward te doen in plaats van een merge, in welk geval het misschien niet zo duidelijk is welke van de commits eigenlijk tot je topic branch behoorde).

het probleem met mergen naar master ontstaat wanneer je topic branch gebaseerd is op een oude master in plaats van de laatste tip van master. In dit geval krijg je onvermijdelijk een niet-lineaire geschiedenis.

2 - merge-old-branch

of dit een veel voorkomend probleem is of niet hangt grotendeels af van hoe actief de git repository is, hoeveel ontwikkelaars gelijktijdig werken, etc.

Merge from master in a topic branch

soms wilt u uw branch bijwerken om overeen te komen met de laatste master (bijv. er zijn een aantal nieuwe functies op master die je in je topic branch wilt krijgen, of je merkt dat je je topic branch niet in master kunt mergen omdat er conflicten zijn).

een veelgebruikte methode, die zelfs door sommigen wordt aanbevolen, is om de tip van master in je topic branch te mergen. Dit is een belangrijke bron van niet-lineaire geschiedenis!

3 - merging-master-into-topic

de oplossing: Rebase!

Git rebase is een zeer handige functie die je zou moeten gebruiken als je een lineaire historie wilt. Sommigen vinden het concept van rebasen onhandig, maar het is echt heel eenvoudig: speel de wijzigingen (commits) in je branch opnieuw af bovenop een nieuwe commit.

bijvoorbeeld, je kunt Git rebase gebruiken om de root van je topic branch te veranderen van een oude master naar de tip van de laatste master. Ervan uitgaande dat je je topic branch hebt uitgecheckt, kun je:

git fetch origingit rebase origin/master

4 - rebasen

merk op dat de bijbehorende merge operatie zou zijn om de tip van master in uw topic branch samen te voegen (zoals afgebeeld in de vorige figuur). De resulterende inhoud van de bestanden in je topic branch zou hetzelfde zijn, ongeacht of je een rebase of merge doet. Echter, de geschiedenis is anders (lineair vs niet-lineair!).

dit klinkt allemaal prima. Er zijn echter een paar kanttekeningen bij rebase waar je je bewust van moet zijn.

Caveat 1: Rebase maakt nieuwe commits

een branch Rebasen zal in feite nieuwe commits maken. De nieuwe commits zullen andere SHA:s hebben dan de oude commits. Dit is meestal geen probleem, maar je komt in de problemen als je een branch rebase die buiten je lokale repository bestaat (bijvoorbeeld als je branch al bestaat op origin).

als je een rebased branch pusht naar een remote repository die al dezelfde branch bevat (maar met de oude geschiedenis), zou je:

  1. je moet de branch forceren (bijvoorbeeld git push --force-with-lease), omdat Git je anders niet zal toestaan om een nieuwe historie naar een bestaande branch te pushen. Dit vervangt effectief de geschiedenis voor de gegeven remote branch door een nieuwe geschiedenis.
  2. maakt mogelijk iemand anders erg ongelukkig, omdat hun lokale versie van de gegeven branch niet meer overeenkomt met de branch op de remote, wat tot allerlei problemen kan leiden.

in het algemeen, vermijd het overschrijven van de geschiedenis van een branch op een remote (de enige uitzondering is het herschrijven van de geschiedenis van een branch die onder code review staat – afhankelijk van hoe je code review systeem werkt – maar dat is een andere discussie).

als je een branch moet rebasen die gedeeld wordt met anderen op een remote, dan is een eenvoudige workflow om een nieuwe branch te maken die je rebase, in plaats van de oorspronkelijke branch te rebasen. Ervan uitgaande dat u my-topic-branch uitgecheckt hebt, kunt u:

git checkout -b my-topic-branch-2git fetch origingit rebase origin/mastergit push -u origin my-topic-branch-2

…en vertel mensen die aan my-topic-branch werken in plaats daarvan verder te werken aan my-topic-branch-2. De oude branch is dan effectief verouderd, en zou niet terug gemerged moeten worden naar master.

Caveat 2: conflicten oplossen in een rebase kan meer werk zijn dan in een merge

als je een conflict krijgt in een merge operatie, zul je alle conflicten oplossen als onderdeel van die merge commit.

echter, in een rebase operatie, kun je potentieel een conflict krijgen voor elke commit in de branch die je rebase.

eigenlijk zul je vaak merken dat als je een conflict krijgt in een commit, je gerelateerde (zeer vergelijkbare) conflicten zult tegenkomen in volgende commits in je branch, simpelweg omdat commits in een topic branch gerelateerd zijn (bijvoorbeeld het wijzigen van dezelfde delen van de code).

de beste manier om conflicten te minimaliseren is om bij te houden wat er gebeurt in master, en te voorkomen dat een topic branch te lang draait zonder rebasen. Af en toe is het makkelijker om kleine conflicten van tevoren aan te pakken dan ze allemaal in één grote, gelukkige conflictwoede aan het eind te verwerken.

sommige waarschuwingen voor GitHub gebruikers

GitHub is goed in veel dingen. Het is fantastisch voor Git hosting, en het heeft een prachtige webinterface met code browsen, mooie Markdown functionaliteit, Gist, etc.

Pull requests, aan de andere kant, heeft een paar functies die actief lineaire Git geschiedenis dwarsbomen. Het zou zeer welkom zijn als GitHub deze problemen daadwerkelijk heeft opgelost, maar tot dan moet u zich bewust zijn van de tekortkomingen:

” Merge pull request “staat niet-lineaire merges toe naar master

het kan verleidelijk zijn om op de groene, vriendelijk ogende” Merge pull request ” knop te drukken om een topic branch in master samen te voegen. Vooral als er staat: “deze branch is up-to-date met de basis branch. Samenvoegen kan automatisch worden uitgevoerd”.

GitHub merge pull request

wat GitHub hier eigenlijk zegt, is dat de branch zonder conflicten kan worden gemerged tot master. Het controleert niet of de pull request branch gebaseerd is op de laatste master.

met andere woorden, als je een lineaire geschiedenis wilt, moet je ervoor zorgen dat de pull request branch zelf wordt gerebased bovenop de laatste master. Voor zover ik kan zien, is dergelijke informatie niet beschikbaar via de GitHub webinterface (tenzij je “protected branches” gebruikt – zie hieronder), dus je moet het doen vanaf je lokale Git client.

zelfs als de pull request correct is gerebased, is er geen garantie dat de merge operatie in de GitHub webinterface atomisch zal zijn (d.w.z. iemand kan wijzigingen naar master pushen voordat je merge operatie doorgaat – en GitHub zal niet klagen).

dus echt, de enige manier om er zeker van te zijn dat je branches goed gerebased zijn bovenop de laatste master is om de merge operatie lokaal uit te voeren en de resulterende master handmatig te pushen. Iets langs de lijnen:

git checkout mastergit pullgit checkout my-pullrequest-branchgit rebase mastergit checkout mastergit merge --no-ff my-pullrequest-branchgit push origin master

als je pech hebt en iemand erin slaagt om wijzigingen naar master te pushen tussen je push-en push-operaties, zal je push-operatie worden geweigerd. Dit is echter een goede zaak, omdat het garandeert dat uw operatie atomisch is. Gewoon git reset --hard origin/master en herhaal de bovenstaande stappen tot het door gaat.

opmerking: respecteer uw project richtlijnen w. r. t. code herziening en testen. Bijvoorbeeld als je automatische tests uitvoert (builds, statische analyse, unit tests,…) als onderdeel van een pull request, moet je waarschijnlijk je rebased branch opnieuw indienen (ofwel met git push-f, of door een nieuwe PR te openen) in plaats van alleen de master branch handmatig bij te werken.

Protected branches functionaliteit moedigt merges aan vanaf master

als je protected branches en statuscontroles gebruikt in je GitHub project, krijg je eigenlijk bescherming tegen het mergen van een pull request branch in master, tenzij het gebaseerd is op de laatste master (ik denk dat de reden is dat statuscontroles uitgevoerd op de PR branch nog steeds geldig moeten zijn na het mergen naar master).

echter … als de pull request branch niet gebaseerd is op de laatste master, krijg je een vriendelijke knop genaamd “Update branch” te zien, met de tekst “Deze branch is verouderd met de basis branch. Merge de laatste wijzigingen van master in deze branch”.

github-update-branch

op dit punt is je beste optie om de branch lokaal te rebasen en deze te forceren-pushen naar de pull request. Ervan uitgaande dat u my-pullrequest-branch uitgecheckt hebt, doe:

git fetch origingit rebase origin/mastergit push -f

helaas spelen GitHub pull requests niet goed met force pushes, dus sommige van de code review informatie kan verloren gaan tijdens het proces. Als dat niet acceptabel is, overweeg dan om een nieuwe pull request te maken (dwz push je gerebased branch naar een nieuwe remote branch en maak een pull request van de nieuwe branch).

conclusie

Als u geeft om een lineaire geschiedenis:

  • Rebase je topic branch op de laatste master voordat je het mergt naar master.
  • merge master niet in uw topic branch. Rebase in plaats daarvan.
  • wanneer u uw topic branch deelt met anderen, maak dan een nieuwe branch wanneer u deze opnieuw moet baseren(my-topic-branch-2, my-topic-branch-3, …).
  • als je GitHub pull requests gebruikt, let dan op:
    • de “Merge” knop garandeert niet dat de PR branch gebaseerd is op de laatste master. Rebase handmatig indien nodig.
    • als je beveiligde branches gebruikt met statuscontroles, druk dan nooit op de” Update branch ” knop. Rebase handmatig in plaats daarvan.

als je niet te veel geeft om een lineaire Git geschiedenis – gelukkig mergen!

Wishlist voor GitHub

aan u fijne mensen die bij GitHub werken: voeg ondersteuning toe voor rebase workflows in pull requests (dit kunnen opt-in repository opties zijn).

  • voeg de mogelijkheid toe om de Merge knop/operatie uit te schakelen in pull requests als de branch niet gebaseerd is op de laatste master (dit zou het gebruik van statuscontroles niet nodig moeten hebben).
  • voeg een” Rebase on latest master ” knop toe. In veel gevallen moet dit een conflictvrije operatie zijn die gemakkelijk kan worden gedaan via de webinterface.
  • Preserve pull request history (commits, comments, …) na een rebase / force push.

Leave a Reply