Git
Chapters ▾ 2nd Edition

3.6 Git förgreningar - Grenflytt

Grenflytt

I Git finns i huvusak två sätt att integrera ändringar från en gren in i en annan: sammanslagning (merge) eller grenflytt (rebase). I detta avsnitt kokker du får lära dig vad en grenflytt är, hur man gör det och varför det är ett ganska häpnadsväckande verktyg, samt i vilka fall du inte vill använda det.

Den grundläggande grenflytten

Om du går tillbaks till ett tidigare exempel från Grundläggande sammanslagning, kan du se att du divergerade ditt arbete och gjorde versioner på två olika grenar.

Enkel divergent historik.
Figur 35. Enkel divergent historik

Det änklaste sättet att integrera grenar är, som vi redan gått igenom, kommandot merge. Den genomför en trevägssammanslagning mellan de två senaste ögonblicksbilderna (C3 och C4) och den senaste gemensamma versionen av de två grenarna (C2) och skapar en ny ögonblicksbild (och version).

Sammanslagning för att integrera divergerad versionshistorik.
Figur 36. Sammanslagning för att integrera divergerad versionshistorik

Det finns emellertid ett annat sätt: Du kan ta ändringarna som introducerades i C4 och tillämpa den på toppen av C3. I Git kallas detta för grenflytt (eng. rebasing). Med kommandot rebase kan du ta alla ändringar som sparats i en gren och spela upp dem på en annan gren.

I detta exemplet kommer du checka ut experiment-grenen och sedan flytta grenen till master-grenen som följer:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Denna operation fungerar genom att hitta den senaste gemensamma versionen för de två grenarna (den du står på och den du skall flytta din gren till), ta reda på skillnaderna som introducerats i varje version av den gren du står på, spara dessa i temporära filer, peka om den aktuella grenen till toppen av den gren som du skall flytta din aktuella gren till, och sedan applicera ändringarna i turordning.

Flytta ändringarna som introducerats i `C4` på toppen av `C3`.
Figur 37. Flytta ändringarna som introducerats i C4 på toppen av C3

Nu kan du gå tillbaka till master-grenen och göra en sammanslagning via snabbspolning.

$ git checkout master
$ git merge experiment
Snabbspolning av master-grenen.
Figur 38. Snabbspolning av master-grenen

Ögonblicksbilden som C4' pekar på är exakt samma som den som C5 pekade på i the merge example. Det finns ingen skillnad i slutprodukten av integrationen, men att flytta grenen gör att historiken blir renare. Om du undersöker historiken av en flyttad gren kommer den vara linjär: det verkar som att allt arbete har skett sekvensiellt, trots att det egentligen skedde parallellt.

Ofta vill du göra detta för att säkerställa att dina versioner kan läggas till rent på en fjärrgren — kanske i ett projekt som du försöker bidra till men som du inte underhåller. I detta fall gör du ditt arbete i en gren och sedan flyttar ditt arbet in i origin/master när du är redo att publicera dina ändringar till huvudprojektet. På detta vis behöver inte den som underhåller projektet göra något integrationsarbete — endast snabbspola eller lägga till ändringarna rent.

Notera att ögonblicksbilden som pekas på av den slutliga versionen, oavsett om det är den senaste av de flyttade versionerna för en grenflytt eller den slutliga versionen efter en sammanslagning är samma ögonblicksbild — det är bara historiken som skiljer. Grenflytt spelar upp ändringarna från en arbetshistorik på toppen av en annan i samma ordning de introducerades, medan sammanslagning tar ändpunkterna på varje gren och slår ihop dem.

Mer intressanta grenflyttar

Du kan också spela upp din historik på något annat än den ursprungliga basgrenen. Ta en historik som Historik med en gren från en annan gren, till exempel. Du gjorde en gren (server) för att lägga till lite serverfunktionalitet till ditt projekt och skapade en ny version. Därefter gjorde du en gren från denna för att göra motsvarande ändringar hos klienten (client) och gjorde några nya versioner. Slutligen gick du tillvaks till din servergren och gjorde några fler versioner.

Historik med en gren från en annan gren.
Figur 39. Historik med en gren från en annan gren

Antag att du beslutar att du vill slå samman din klientfunktionalitet till ditt huvudspår för att frisläppa dem, men att du vill avvakta serverändringarna tills dessa är testade. Du kan ta klietändringarna som inte är på server (C8 och C9) och spela upp dem på din master gren genom att använda flaggan --onto till git rebase:

$ git rebase --onto master server client

Detta betyder i praktiken “Ta client grenen, ta reda på de patchar sedan den divergerade från server grenen, och spela upp dem på klient delen som om de vore baserade direkt från master grenen istället.” Det är lite komplext, men resultatet är rätt häftigt.

Flytta en gren från en annan gren.
Figur 40. Flytta en gren från en annan gren
$ git checkout master
$ git merge client
Snabbspola din master-gren till att inkludera klientgrenens ändringar.
Figur 41. Snabbspola din master-gren till att inkludera klientgrenens ändringar

Säg att du beslutar att dra in din servergren också. Du kan spela upp servergrenen på master grenen utan att behöva checka ut den först genom att köra git rebase <basgren> <stickspår> — vilket checkar ut stickspåret (server i detta fall) för dig och spelar up den på basgrenen (master):

$ git rebase master server

Detta spelar upp ditt arbete i server ovan på ditt arbete i master, som synes i Flytta din servergren til toppen av din mastergren.

Flytta din servergren til toppen av din mastergren.
Figur 42. Flytta din servergren til toppen av din mastergren

Därefter kan du snabbspola din basgren (master):

$ git checkout master
$ git merge server

Du kan ta bort grenarna client och server eftersom allt arbete är integrerat, vilket ger dig en historik för denna process likt Slutlig versionshistorik:

$ git branch -d client
$ git branch -d server
Slutlig versionshistorik.
Figur 43. Slutlig versionshistorik

Farorna med grenflyttar

Ahh, lyckan med grenflytt är inte helt utan nackdelar, vilka kan sammanfattas i en mening:

Flytta inte versioner som existerar utanför ditt lokala repo som andra kan ha baserat sitt arbete på.

Följer du det tipset så kommer allt gå bra. Om inte, kommer folk hata dig och du kommer att hånas av dina vänner och familj.

När du flyttar om saker överger du existerande versioner och skapar nya som är lika, men annorlunda. Om du publicerar versioner någonstans och andra hämtar dem och baserar arbete på dem och du sedan skriver om historiken med git rebase och publicerar dessa ändringarna igen, kommer dina medarbetare att behöva återintegrera sitt arbete och saker kommer bli krånligt när du försöker integrera deras ändringar i dina.

Låt oss ta ett exempel på hur det kan uppstå problem om du skriver om arbete som du gjort publikt. Antag att du klonar från en central server och sedan gör lite arbete på det. Din versionshistorik ser ut såhär:

Klona ett repo och gör lite jobb på det.
Figur 44. Klona ett repo och gör lite jobb på det

Nu gör någon annan mer arbete som inkluderar en sammanslagning och publicerar det arbetet till den centrala servern. Du hämtar det och slår ihop fjärrgrenen in i ditt arbete vilket gör att din versionshistorik ser ut ungefär såhär:

Hämta fler versioner och slå ihop ändringarna i ditt arbete.
Figur 45. Hämta fler versioner och slå ihop ändringarna i ditt arbete

Sedan bestämmer sig personen som publicerade ändringarna att gå tillbaks och skriva om sin historik istället; de gör git push --force för att skriva över den historik som finns på servern. Du hämtar sedan från den server, och får hem de nya versionerna.

Någon publicerar omskriven historik, och överger versioner på vilka du baserat arbete.
Figur 46. Någon publicerar omskriven historik, och överger versioner på vilka du baserat arbete

Nu sitter ni båda i skiten. Om du gör git pull kommer du skapa en sammanslagningsversion som inkluderar båda versionstidslinjerna, och ditt repo kommer se ut såhär:

Du integrerar samma arbete igen i en ny sammanslagningsversion.
Figur 47. Du integrerar samma arbete igen i en ny sammanslagningsversion

Om du kör git log när din historik ser ut såhär kommer du se två versioner som har samma författare, datum och meddelande, vilket kommer vara förvirrande. Vidare, om du publicerar denna historik tillbaks till servern, kommer du återintroducera alla de tidigare omskrivna versionerna vilket kan förvirra andra också. Man kan vara ganska säker på att den andra utvecklaren inte vill att C4 och C6 skall vara i historiken; det är därför de skrev om historiken från början.

Flytta en gren när du flyttar en gren

Om du däremot finner att du är i en liknande sitiation, så har Git lite ytterligare magi som kan komma väl till pass. Om någon i ditt team trycker ut en ändring som skriver över arbete som du baserar arbete på, blir din utmaning att ta reda på vad som är ditt och vad de har skrivit om.

Det faller sig så att utöver till versionens SHA-1 checksimma, beräknar Git också en checksumma baserat på just den patch som introducerades med versionen. Denna kallas för “patch-id”.

Om du hämtar hem arbete som var omskrivet och gör en egen omskrivning på toppen av versionerna från din kollega, kan Git ofta lista ut vad som är unikt ditt och och applicera dina ändringar ovanpå den nya grenen.

Till exempel, i föregående scenario, om du istället för att slå ihop ändringarna vid Någon publicerar omskriven historik, och överger versioner på vilka du baserat arbete och kör git rebase teamone/master, kommer Git att:

  • Ta reda på vilket arbete som är unikt för vår gren (C2, C3, C4, C6, C7)

  • Ta reda på vilka som inte är sammanslagningsversioner (C2, C3, C4)

  • Ta reda på vad som inte har skrivits om i målgrenen (bara C2 och C3, eftersom C4 är samma patch som C4')

  • Applicera de ändringarna ovanpå teamone/master

Grenflytt till toppen av en tvingande publicering av omskriven historik.
Figur 48. Grenflytt till toppen av en tvingande publicering av omskriven historik

Detta fungerar bara om C4 och C4' som din kollega gjort är näst intill samma patch. Annars kommer Git inte kunna avgöra att de är duplikat och kommer lägga till ytterligare en C4-lik patch (som förmodligen inte kommer gå att applicera rent, eftersom ändringarna bitvis redan är på plats).

Du kan också förenklad detta genom att köra git pull --rebase istället för en vanlig git pull. Eller så kan du göra det manuellt genom git fetch följt av git rebase teamone/master i detta fallet.

Använder du git pull och vill göra --rebase till normalfallet, kan du sätta konfigureringsparametern pull.rebase med något liknande git config --global pull.rebase true.

Om du nägonsin skriver om historik som bara finns lokalt på din dator kommer du vara helt säker. Om du skriver om historik som är publicerade, men som ingen annan baserat versioner på, kommer du också vara helt säker. Om du skriver om versionshistorik som redan har publicerats publikt, och som folk har baserat arbete på, kommer du hamna i frustrerande trubbel och hånas av dina teammedlemmar.

Om du eller en kollega anser det vara nödvändigt vid något tillfälle, se till att alla vet om att de skall köra git pull --rebase för att göra den efterföljande pinan något lättare.

Omskrivning vs. Sammanslagning

Nu när du sett hur omskrivning och sammanslagning fungerar, kanske du undrar vilken som är bäst. Innan vi kan svara på det, låt oss ta ett steg tillbaka och prata om vad historik betyder.

En infallsvinkel på det är att dit repos versionshistorik är en beskrivning över vad som faktiskt hände. Det är ett historiskt dokument, värdefull i sig själv, och skall inte manipuleras. Med denna vinkel är ändring av versionshistoriken närmast blasfemi; du ljuger om vad som faktisk skedde. Vad gör det om det är en stökig historik av sammanslagningsversioner? Det var så det hände, och repot skall bevara det för eftervärlden.

En motstående infallsvinkel är att versionshistoriken är berättelsen av hur ditt projekt skapades. Du publicerar inte första utkastet av en bok, och manualen för hur du underhåller din mjukvara förtjänar noggrann redigering. Detta är lägret som använder verktyg som omskrivning och filter-grenar för att berätta historien som bäst lämpar sig för framtida läsare.

Nu till frågan huruvida sammanslagning eller omskrivning är bättre: förhoppningsvis inser du att det inte är så enkelt. Git är ett kraftfullt verktyg och tillåter dig att göra många saker med din historik, men alla team och alla projekt är olika. Nu när du vet hur båda dessa verktyg fungerar, är det upp till dig att avgöra vilken metod som är bäst lämpad i din specifika situation.

I allmänhet, för att få det bästa från två världar, är att skriva om lokala ändringar du gjort men ännu inte delat innan du publicerar dem i syfte att rensa upp din historik, men att aldrig skriva om historik du publicerat nånstans.

scroll-to-top