Chapters ▾ 2nd Edition

3.6 Git-grenar - Ombasering

Ombasering

I Git finns två huvudsätt att integrera ändringar från en gren in i en annan: merge och rebase. I det här avsnittet lär du dig vad ombasering är, hur man gör det, varför det är ett imponerande verktyg och när du inte ska använda det.

Grundläggande ombasering

Om du går tillbaka till exemplet i Grundläggande sammanslagning ser du att arbetet divergerade och att du gjorde incheckningar på två olika grenar.

Enkel divergerad historik
Figur 35. Enkel divergerad historik

Det enklaste sättet att integrera grenarna är, som vi redan gått igenom, merge. Det gör en trevägssammanslagning mellan de två senaste ögonblicksbilderna (C3 och C4) och deras senaste gemensamma förfader (C2) och skapar en ny ögonblicksbild (och incheckning).

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

Men det finns ett annat sätt: du kan ta ändringspatchen som introducerades i C4 och applicera den ovanpå C3. I Git kallas detta rebase(ombasera). Med rebase kan du ta alla ändringar som checkats in på en gren och spela upp dem på en annan.

I det här exemplet växlar du till experiment och flyttar den till master så här:

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

Detta fungerar genom att Git hittar den senaste gemensamma förfadern mellan de två grenarna (den du står på och den du flyttar till), tar fram diffen som varje incheckning i din gren introducerar, sparar dem som temporära ändringspatchar, flyttar den aktuella grenen till samma incheckning som basgrenen och applicerar ändringspatcharna i tur och ordning.

Flytta ändringen i `C4` ovanpå `C3`
Figur 37. Flytta ändringen i C4 ovanpå C3

Nu kan du gå tillbaka till master och göra en snabbspolad sammanslagning:

$ git checkout master
$ git merge experiment
Snabbspola `master`
Figur 38. Snabbspola master

Ögonblicksbilden som C4' pekar på är exakt densamma som den som C5 pekade på i the merge example. Slutresultatet är alltså detsamma, men ombasering ger en renare historik. Om du granskar loggen för en flyttad gren ser den linjär ut: allt verkar ha skett i serie även om arbetet egentligen skedde parallellt.

Ofta gör du detta för att säkerställa att dina incheckningar går att applicera rent på en fjärrgren – till exempel när du vill bidra till ett projekt du inte förvaltar. Då gör du arbetet i en gren och flyttar det till origin/master när du är redo att skicka in dina ändringspatchar. På så vis behöver förvaltaren bara snabbspola eller applicera ändringarna rent.

Notera att ögonblicksbilden som den sista incheckningen pekar på – oavsett om det är den sista flyttade incheckningen eller den sista sammanslagningsincheckningen – är densamma. Det är bara historiken som skiljer sig. Ombasering spelar upp ändringar i samma ordning som de introducerades, medan sammanslagning tar ändpunkterna och fogar samman dem.

Mer intressanta grenflyttar

Du kan också applicera en gren ovanpå något annat än basgrenen. Ta en historik som Historik med en ämnesgren från en annan. Du skapade en gren (server) för att lägga till serverspecifik funktionalitet och gjorde en incheckning. Sedan grenade du av för klientändringar (client) och checkade in några gånger. Till sist gick du tillbaka till server och gjorde fler incheckningar.

Historik med en ämnesgren från en annan ämnesgren
Figur 39. Historik med en ämnesgren från en annan

Anta att du vill sammanfoga klientändringarna till huvudlinjen för en utgåva, men vill vänta med serverändringarna tills de är mer testade. Du kan ta ändringarna på client som inte finns på server (C8 och C9) och spela upp dem på master genom flaggan --onto till git rebase:

$ git rebase --onto master server client

Det betyder i praktiken: "Ta client, ta fram ändringspatcharna sedan den divergerade från server och spela upp dem som om client hade baserats direkt på master." Det är lite komplext, men resultatet är riktigt snyggt.

Flytta en ämnesgren från en annan ämnesgren
Figur 40. Flytta en ämnesgren från en annan ämnesgren
$ git checkout master
$ git merge client
Snabbspola `master` så den inkluderar ändringarna från `client`
Figur 41. Snabbspola master så den inkluderar ändringarna från client

Anta att du också vill dra in server. Du kan flytta server till master utan att växla till den först genom att köra git rebase <basgren> <ämnesgren> – vilket växlar till ämnesgrenen (server) åt dig och spelar upp den ovanpå basgrenen (master):

$ git rebase master server

Detta applicerar arbetet i server ovanpå master, som visas i Flytta server ovanpå master.

Flytta `server` ovanpå `master`
Figur 42. Flytta server ovanpå master

Sedan kan du snabbspola basgrenen (master):

$ git checkout master
$ git merge server

Du kan ta bort grenarna client och server eftersom allt arbete är integrerat, vilket ger en historik som i Slutlig incheckningshistorik:

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

Farorna med grenflyttning

Att ombasera är inte utan nackdelar, vilket kan sammanfattas i en enda rad:

Flytta inte incheckningar som finns utanför ditt kodförråd och som andra kan ha baserat arbete på.

Följer du den regeln är du trygg. Om inte kommer folk bli irriterade och du får höra det.

När du flyttar om saker överger du befintliga incheckningar och skapar nya som är lika men ändå annorlunda. Om du skickar upp incheckningar, andra uppdaterar dem och baserar arbete på dem, och du sedan skriver om historiken med git rebase och skickar upp igen, måste dina medarbetare sammanfoga sitt arbete på nytt och det blir stökigt när du ska dra in deras ändringar.

Låt oss se ett exempel på hur det kan gå fel när du skriver om arbete du gjort publikt. Anta att du klonar från en central server och gör lite arbete. Historiken ser ut så här:

Klona ett kodförråd och basera arbete på det
Figur 44. Klona ett kodförråd och basera arbete på det

Nu gör någon annan mer arbete som inkluderar en sammanslagning och skickar upp det till den centrala servern. Du uppdaterar och sammanslår fjärrgrenen i ditt arbete, så att historiken ser ut så här:

Uppdatera fler incheckningar och sammanfoga dem i ditt arbete
Figur 45. Uppdatera fler incheckningar och slå samman dem i ditt arbete

Sedan bestämmer sig personen som skickade upp att gå tillbaka och flytta om sin historik och gör git push --force för att skriva över historiken på servern. Du uppdaterar från servern och får hem de nya incheckningarna.

Någon skickar upp omskriven historik och överger incheckningar du baserat arbete på
Figur 46. Någon skickar upp omskriven historik och överger incheckningar du baserat arbete på

Nu sitter ni båda i knipa. Om du kör git pull skapar du en sammanslagningsincheckning som innehåller båda historiklinjerna, och ditt kodförråd ser ut så här:

Du sammanfogar samma arbete igen i en ny sammanslagningsincheckning
Figur 47. Du sammanfogar samma arbete igen i en ny sammanslagningsincheckning

Om du kör git log i det här läget ser du två incheckningar med samma författare, datum och meddelande, vilket är förvirrande. Om du skickar upp den här historiken till servern återinför du dessutom de omskrivna incheckningarna, vilket kan förvirra andra. Det är rimligt att anta att den andra utvecklaren inte vill ha C4 och C6 i historiken; det var därför de flyttade om den från början.

Flytta när du flyttar

Om du är i en sådan situation har Git mer magi som kan hjälpa. Om någon i teamet tvingar upp ändringar som skriver över arbete du baserat dig på måste du ta reda på vad som är ditt och vad som skrivits om.

Utöver SHA-1-kontrollsumman beräknar Git också en kontrollsumma baserad på ändringspatchen som introducerades med incheckningen. Den kallas "ändringspatch-id".

Om du uppdaterar arbete som skrivits om och sedan flyttar dina egna incheckningar ovanpå de nya, kan Git ofta räkna ut vad som är unikt ditt och applicera det på den nya grenen.

Till exempel, i scenariot ovan, om du i stället för att sammanfoga vid Någon skickar upp omskriven historik och överger incheckningar du baserat arbete på kör git rebase teamone/master, kommer Git att:

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

  • Identifiera vilka som inte är sammanslagningsincheckningar (C2, C3, C4)

  • Identifiera vilka som inte redan finns omskrivna i målgrenen (bara C2 och C3, eftersom C4 har samma ändringspatch som C4')

  • Applicera dessa incheckningar ovanpå teamone/master

Ombasera ovanpå tvingat uppskriven historik
Figur 48. Ombasera ovanpå tvingat uppskriven historik

Detta fungerar bara om C4 och C4' som din kollega gjorde är nästan exakt samma ändringspatch. Annars kan en ombasering inte avgöra att det är en dubblett och kommer att lägga till ännu en C4-lik ändringspatch (som sannolikt inte går att applicera rent eftersom ändringarna redan finns där).

Du kan förenkla detta genom att köra git pull --rebase i stället för ett vanligt git pull. Eller så kan du göra det manuellt med git fetch följt av git rebase teamone/master.

Om du använder git pull och vill göra --rebase till standard kan du sätta pull.rebase med något i stil med git config --global pull.rebase true.

Om du bara flyttar incheckningar som aldrig lämnat din dator är det lugnt. Om du flyttar incheckningar som har skickats upp men som ingen annan baserat arbete på går det också bra. Men om du flyttar incheckningar som redan har skickats upp offentligt och andra har baserat arbete på dem kan du få frustrerande problem och sura kollegor.

Om du eller en kollega behöver göra det, se till att alla vet att de ska köra git pull --rebase för att minska smärtan efteråt.

Ombasera kontra sammanfoga

Nu när du har sett ombasering och sammanslagning i praktiken undrar du kanske vilket som är bäst. Innan vi kan svara behöver vi ta ett steg tillbaka och prata om vad historik betyder.

Ett perspektiv är att ditt kodförråds incheckningshistorik är en redogörelse för vad som faktiskt hände. Det är ett historiskt dokument, värdefullt i sig, och ska inte manipuleras. Ur det perspektivet är ändring av historiken nästan hädelse; du ljuger om vad som skedde. Vad gör det om det finns en stökig serie sammanslagningsincheckningar? Så var det, och kodförrådet ska bevara det.

Det motsatta perspektivet är att historiken är berättelsen om hur projektet skapades. Du publicerar inte första utkastet av en bok, så varför visa allt rörigt arbete? När du jobbar behöver du en logg över alla misslyckade spår, men när du ska visa upp resultatet kan du vilja berätta en mer sammanhängande historia om hur du gick från A till B. Personer i detta läger använder verktyg som rebase och filter-branch för att skriva om incheckningar innan de sammanfogas in i huvudlinjen, för att berätta historien på ett sätt som är bäst för framtida läsare.

Så frågan om sammanslagning eller ombasering är bättre har inget enkelt svar. Git är kraftfullt och låter dig göra mycket med historiken, men varje team och projekt är olika. Nu när du vet hur båda fungerar är det upp till dig att avgöra vad som passar din situation bäst.

Du kan få det bästa av två världar: flytta lokala ändringar innan du skickar upp dem för att städa historiken, men flytta aldrig sådant du redan har skickat någonstans.