en snygg, linjär Git-historia

en av de saker som förbises i många Git-baserade projekt är värdet av en linjär begå historia. Faktum är att många verktyg och riktlinjer avskräcker Git-arbetsflöden som syftar till en linjär historia. Jag tycker att det här är sorgligt, eftersom en snygg historia är mycket värdefull, och det finns ett rakt fram arbetsflöde som säkerställer en linjär historia.

linjär vs icke-linjär historia

en linjär historia är helt enkelt en Git-historia där alla åtaganden kommer efter varandra. Dvs. du kommer inte hitta några sammanslagningar av filialer med oberoende begå historier.

1 - nonlinear-vs-linear

Varför vill du ha en linjär historia?

förutom att vara snygg och logisk, kommer en linjär historia till nytta när:

  • titta på historien. En icke-linjär historia kan vara mycket svår att följa – ibland till den punkten att historien bara är obegriplig.
  • Backtracking förändringar. Till exempel: “har funktionen en introduceras före eller efter buggfix B?”.
  • spåra buggar. Git har en mycket snygg funktion som kallas Git bisect, som kan användas för att snabbt hitta vilken commit som introducerade en bugg eller regression. Men med en icke-linjär historia blir Git bisect svår eller till och med omöjlig att använda.
  • återställa ändringar. Säg att du hittade ett åtagande som orsakade en regression, eller om du vill ta bort en funktion som inte skulle gå ut i en specifik utgåva. Med lite tur(om din kod inte har ändrats för mycket) kan du helt enkelt återställa oönskade åtaganden med Git revert. Men om du har en icke-linjär historia, kanske med massor av tvärgrenar, blir det betydligt svårare.

det finns förmodligen en annan handfull situationer där en linjär historia är mycket värdefull, beroende på hur du använder Git.

poängen är: ju mindre linjär din historia är, desto mindre värdefull är den.

orsaker till en icke-linjär historia

kort sagt, varje sammanfognings commit är en potentiell källa till en icke-linjär historia. Det finns dock olika typer av sammanslagningar.

sammanfoga från en ämnesgren till master

när du är klar med din ämnesgren och vill integrera den i master är en vanlig metod att slå samman ämnesgrenen till master. Kanske något i linje:

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

en trevlig egenskap med den här metoden är att du bevarar informationen om vilka åtaganden som var en del av din ämnesgren (ett alternativ skulle vara att lämna ut “–no-ff”, vilket skulle göra det möjligt för Git att göra en snabbspolning istället för en sammanslagning, i vilket fall det kanske inte är så klart vilket av åtagandena som faktiskt tillhörde din ämnesgren).

problemet med att slå samman till master uppstår när din ämnesgren är baserad på en gammal master istället för det senaste tipset på master. I det här fallet kommer du oundvikligen att få en icke-linjär historia.

2 - sammanslagning-old-branch

huruvida detta kommer att vara ett vanligt problem eller inte beror till stor del på hur aktivt Git-arkivet är, hur många utvecklare som arbetar samtidigt etc.

sammanfoga från master till en ämnesgren

ibland vill du uppdatera din gren så att den matchar den senaste master (t. ex. det finns några nya funktioner på master som du vill komma in i din ämnesgren, eller så upptäcker du att du inte kan slå samman din ämnesgren i master eftersom det finns konflikter).

en vanlig metod, som till och med rekommenderas av vissa, är att slå samman spetsen av master i din ämnesgren. Detta är en viktig källa till icke-linjär historia!

3 - sammanslagning-master-into-topic

lösningen: Rebase!

Git rebase är en mycket användbar funktion som du bör använda om du vill ha en linjär historia. Vissa tycker att begreppet rebasing är besvärligt, men det är egentligen ganska enkelt: spela upp ändringarna (förbinder) i din gren ovanpå en ny begå.

du kan till exempel använda Git rebase för att ändra roten till din ämnesgren från en gammal mästare till toppen av den senaste mästaren. Förutsatt att du har din ämnesgren utcheckad kan du göra:

git fetch origingit rebase origin/master

4 - rebasing

Observera att motsvarande sammanfogningsoperation skulle vara att slå samman spetsen av master i din ämnesgren (som avbildad i föregående figur). Det resulterande innehållet i filerna i ämnesgrenen skulle vara detsamma, oavsett om du gör en rebase eller en sammanslagning. Historien är dock annorlunda (linjär vs icke-linjär!).

detta låter allt bra och bra. Det finns dock ett par varningar med rebase som du bör vara medveten om.

Caveat 1: Rebase skapar nya åtaganden

Rebasing en gren kommer faktiskt att skapa nya åtaganden. De nya åtagandena kommer att ha olika SHA: s än de gamla åtagandena. Detta är vanligtvis inte ett problem, men du kommer att stöta på problem om du baserar om en filial som finns utanför ditt lokala arkiv (t.ex. om din filial redan finns på origin).

om du skulle driva en ombyggd gren till ett fjärrförvar som redan innehåller samma gren (men med den gamla historiken) skulle du:

  1. git push --force-with-lease), eftersom Git inte tillåter dig att driva en ny historia till en befintlig gren annars. Detta ersätter effektivt historiken för den givna fjärrgrenen med en ny historia.
  2. gör eventuellt någon annan väldigt olycklig, eftersom deras lokala version av den givna filialen inte längre matchar filialen på fjärrkontrollen, vilket kan leda till alla slags problem.

undvik i allmänhet att skriva över en filials historia på en fjärrkontroll (det enda undantaget är att skriva om historiken för en filial som är under kodgranskning – beroende på hur ditt kodgranskningssystem fungerar – men det är en annan diskussion).

om du behöver basera om en gren som delas med andra på en fjärrkontroll, är ett enkelt arbetsflöde att skapa en ny gren som du baserar om, istället för att basera om den ursprungliga grenen. Förutsatt att du har my-topic-branch checkat ut kan du göra det:

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

…och berätta sedan för personer som arbetar med my-topic-branch att fortsätta arbeta med my-topic-branch-2 istället. Den gamla grenen är då effektivt föråldrad och bör inte slås samman till mästaren.

Caveat 2: att lösa konflikter i en rebase kan vara mer arbete än i en sammanfogning

om du får en konflikt i en sammanfogningsoperation kommer du att lösa alla konflikter som en del av den sammanfogningsbegäran.

men i en rebase-operation kan du potentiellt få en konflikt för varje åtagande i den gren som du baserar om.

faktum är att många gånger kommer du att upptäcka att om du får en konflikt i ett åtagande kommer du att stöta på relaterade (mycket liknande) konflikter i efterföljande åtaganden i din gren, helt enkelt för att åtaganden i en ämnesgren tenderar att vara relaterade (t.ex. ändra samma delar av koden).

det bästa sättet att minimera konflikter är att hålla reda på vad som händer i master och undvika att låta en ämnesgren springa för länge utan att rebasera. Att hantera små konflikter på framsidan då och då är lättare än att hantera dem alla i en stor lycklig konfliktrör i slutet.

några varningar för GitHub användare

GitHub är bra på många saker. Det är fantastiskt för Git hosting, och det har ett underbart webbgränssnitt med kodbläddring, fin Markdown-funktionalitet, Gist, etc.

Pull-förfrågningar har å andra sidan några funktioner som aktivt motverkar linjär Git-historia. Det skulle vara mycket välkommet om GitHub faktiskt fixade dessa problem, men tills dess borde du vara medveten om bristerna:

“Merge pull request” tillåter icke-linjära sammanslagningar att behärska

det kan vara frestande att trycka på den gröna, vänliga “Merge pull request” – knappen för att slå samman en ämnesgren i master. Särskilt som det står ” denna gren är uppdaterad med basgrenen. Sammanslagning kan utföras automatiskt”.

 GitHub merge pull request

vad GitHub verkligen säger här är att filialen kan slås samman för att behärska utan konflikter. Det kontrollerar inte om pull request-filialen är baserad på den senaste mästaren eller inte.

med andra ord, om du vill ha en linjär historia måste du se till att pull request-grenen är ombyggd ovanpå den senaste mästaren själv. Såvitt jag kan säga är ingen sådan information tillgänglig via GitHub-webbgränssnittet (om du inte använder “skyddade grenar” – se nedan), så du måste göra det från din lokala Git-klient.

även om pull-begäran är korrekt ombyggd finns det ingen garanti för att sammanslagningsoperationen i GitHub-webbgränssnittet kommer att vara atomär (dvs. någon kan driva ändringar till master innan din sammanslagningsoperation går igenom-och GitHub kommer inte att klaga).

så egentligen är det enda sättet att vara säker på att dina grenar är ordentligt ombyggda ovanpå den senaste mästaren att göra sammanslagningsoperationen lokalt och trycka på den resulterande mästaren manuellt. Något längs linjerna:

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

om du är otur och någon lyckas driva ändringar till master mellan dina pull-och push-operationer, kommer din push-operation att nekas. Detta är dock bra eftersom det garanterar att din operation är atomisk. Bara git reset --hard origin/master och upprepa ovanstående steg tills det går igenom.

Obs: respektera dina projektriktlinjer med kodgranskning och testning. T. ex.om du kör automatiska tester (bygger, statisk analys, enhetstester, …) som en del av en pull-förfrågan, bör du förmodligen skicka in din rebased-filial (antingen med git push-f eller genom att öppna en ny PR) istället för att bara uppdatera huvudgrenen manuellt.

Protected branches funktionalitet uppmuntrar sammanslagningar från master

om du använder skyddade grenar och statuskontroller i ditt GitHub-projekt får du faktiskt skydd mot att slå samman en pull request-filial till master om den inte är baserad på den senaste master (jag tror att motiveringen är att statuskontroller som utförs på PR-filialen fortfarande ska vara giltiga efter sammanslagning till master).

men … om pull request-filialen inte är baserad på den senaste master, presenteras du med en vänlig knapp som heter “Update branch”, med texten “den här filialen är föråldrad med basgrenen. Slå samman de senaste ändringarna från master till den här filialen”.

github-update-branch

vid det här tillfället är ditt bästa alternativ att ombasera grenen lokalt och tvinga den till dragförfrågan. Förutsatt att du har my-pullrequest-branch checkat ut, gör:

git fetch origingit rebase origin/mastergit push -f

tyvärr spelar GitHub pull-förfrågningar inte bra med kraftpressar, så en del av kodgranskningsinformationen kan gå vilse i processen. Om det inte är acceptabelt, överväg att skapa en ny pull-begäran (dvs. tryck din ombyggda filial till en ny fjärrgren och skapa en pull-begäran från den nya filialen).

slutsats

om du bryr dig om en linjär historia:

  • Rebase din ämnesgren ovanpå den senaste mästaren innan du slår samman den för att behärska.
  • slå inte ihop master i ämnesgrenen. Rebase istället.
  • när du delar din ämnesgren med andra, skapa en ny gren när du behöver ombasera den (my-topic-branch-2, my-topic-branch-3, …).
  • om du använder GitHub pull-förfrågningar, var medveten om:
    • “Merge” – knappen garanterar inte att PR-filialen är baserad på den senaste master. Rebase manuellt vid behov.
    • om du använder skyddade grenar med statuskontroller, Tryck aldrig på knappen “Uppdatera gren”. Rebase manuellt istället.

om du inte bryr dig för mycket om en linjär Git – historia-lycklig sammanslagning!

önskelista för GitHub

till dig fina människor som arbetar på GitHub: Lägg till stöd för rebase-arbetsflöden i pull-förfrågningar (dessa kan vara opt-in-lagringsalternativ).

  • Lägg till möjligheten att inaktivera knappen Merge/operation i pull-förfrågningar om filialen inte är baserad på den senaste mästaren (detta bör inte kräva användning av statuskontroller).
  • Lägg till en “Rebase på senaste master” – knappen. I många fall bör detta vara en konfliktfri operation som enkelt kan göras via webbgränssnittet.
  • bevara pull begäran historia (begår, kommentarer, …) efter en rebase / force push.

Leave a Reply