Chapters ▾ 2nd Edition

7.7 Git-verktyg - Nollställning förklarad

Nollställning förklarad

Innan vi går vidare till mer specialiserade verktyg, låt oss prata om Gits kommandon reset och checkout. Dessa kommandon är två av de mest förvirrande delarna av Git när du stöter på dem första gången. De gör så många saker att det verkar hopplöst att verkligen förstå dem och använda dem korrekt. För detta rekommenderar vi en enkel metafor.

De tre träden

Ett enklare sätt att tänka på reset och checkout är genom att se Git som en innehållsförvaltare av tre olika träd. Med "träd" menar vi egentligen "samling av filer", inte specifikt datastrukturen. Det finns några fall där indexet inte riktigt beter sig som ett träd, men för våra syften är det enklast att tänka så här just nu.

Git som system hanterar och manipulerar tre träd i sin normala drift:

Träd Roll

HEAD

Senaste incheckningsögonblicksbild, nästa förälder

Index

Föreslagen nästa incheckningsögonblicksbild

Arbetskatalog

Sandlåda

HEAD

HEAD är pekaren till den aktuella grenreferensen, som i sin tur pekar på den senaste incheckningen som gjorts på den grenen. Det betyder att HEAD blir förälder till nästa incheckning som skapas. Det är i allmänhet enklast att se HEAD som ögonblicksbilden av din senaste incheckning på den grenen.

Faktum är att det är ganska lätt att se hur den ögonblicksbilden ser ut. Här är ett exempel på hur du hämtar den faktiska kataloglistan och SHA‑1-kontrollsummorna för varje fil i HEAD-ögonblicksbilden:

$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon  1301511835 -0700
committer Scott Chacon  1301511835 -0700

initial commit

$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152...   README
100644 blob 8f94139338f9404f2...   Rakefile
040000 tree 99f1a6d12cb4b6f19...   lib

Kommandona cat-file och ls-tree i Git är lågnivåkommandon som används för lågnivåuppgifter och inte riktigt till vardags, men de hjälper oss att se vad som händer här.

Index

Indexet är din föreslagna nästa incheckning. Vi har också kallat det här begreppet Gits "köyta" eftersom det är vad Git tittar på när du kör git commit.

Git fyller indexet med en lista över allt filinnehåll som senast lades ut till din arbetskatalog och hur de såg ut när de ursprungligen lades ut. Sedan ersätter du några av dessa filer med nya versioner av dem, och git commit omvandlar det till trädet för en ny incheckning.

$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0	README
100644 8f94139338f9404f26296befa88755fc2598c289 0	Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0	lib/simplegit.rb

Återigen använder vi här git ls-files, som mer är ett bakom kulisserna-kommando som visar hur indexet ser ut just nu.

Indexet är tekniskt sett inte en trädstruktur – det är faktiskt implementerat som ett platt manifest – men för våra syften räcker det gott.

Arbetskatalogen

Till sist har du din arbetskatalog (som ofta också kallas "arbetsträd"). De andra två träden lagrar sitt innehåll på ett effektivt men opraktiskt sätt, inne i katalogen .git. Arbetskatalogen packar upp dem till faktiska filer, vilket gör det mycket enklare för dig att redigera dem. Tänk på arbetskatalogen som en sandlåda, där du kan prova ändringar innan du checkar in dem i din köyta (index) och sedan i historiken.

$ tree
.
├── README
├── Rakefile
└── lib
    └── simplegit.rb

1 directory, 3 files

Arbetsflödet

Gits typiska arbetsflöde är att spela in ögonblicksbilder av ditt projekt i successivt bättre tillstånd, genom att manipulera dessa tre träd.

Gits typiska arbetsflöde
Figur 135. Gits typiska arbetsflöde

Låt oss visualisera processen: säg att du går in i en ny katalog med en enda fil. Vi kallar detta filens v1, och vi markerar den i blått. Nu kör vi git init, vilket skapar ett Git‑kodförråd med en HEAD-referens som pekar på den ännu ofödda master-grenen.

Nyinitialiserat Git‑kodförråd med ej köad fil i arbetskatalogen
Figur 136. Nyinitialiserat Git‑kodförråd med ej köad fil i arbetskatalogen

I det här läget har bara arbetskatalogträdet något innehåll.

Nu vill vi checka in den här filen, så vi använder git add för att ta innehållet i arbetskatalogen och kopiera det till indexet.

Fil kopieras till index vid `git add`
Figur 137. Filen kopieras till index vid git add

Sedan kör vi git commit, som tar innehållet i indexet och sparar det som en permanent ögonblicksbild, skapar ett incheckningsobjekt som pekar på den ögonblicksbilden och uppdaterar master till att peka på den incheckningen.

Steget `git commit`
Figur 138. Steget git commit

Om vi kör git status ser vi inga ändringar, eftersom alla tre träd är desamma.

Nu vill vi göra en ändring i den filen och checka in den. Vi går igenom samma process; först ändrar vi filen i vår arbetskatalog. Vi kallar detta filens v2, och markerar den i rött.

Git‑kodförråd med ändrad fil i arbetskatalogen
Figur 139. Git‑kodförråd med ändrad fil i arbetskatalogen

Om vi kör git status just nu ser vi filen i rött som "Ändringar som inte är köade för incheckning", eftersom den posten skiljer sig mellan indexet och arbetskatalogen. Nästa steg är att köa den med git add.

Köar ändring till index
Figur 140. Köar ändring till index

Vid det här laget, om vi kör git status, ser vi filen i grönt under "Ändringar att checka in" eftersom indexet och HEAD skiljer sig — det vill säga, vår föreslagna nästa incheckning skiljer sig nu från vår senaste incheckning. Till sist kör vi git commit för att färdigställa incheckningen.

Steget `git commit` med ändrad fil
Figur 141. Steget git commit med ändrad fil

Nu kommer git status inte att ge någon utdata, eftersom alla tre träd är identiska igen.

Att byta grenar eller klona går igenom en liknande process. När du växlar till en gren ändrar det HEAD till att peka på den nya grenreferensen, fyller ditt index med ögonblicksbilden av den incheckningen och kopierar sedan innehållet i indexet till din arbetskatalog.

Nollställning-kommandots roll

Kommandot reset blir mer begripligt när man ser det i detta sammanhang.

För att exemplen ska bli tydligare, låt oss säga att vi har ändrat file.txt igen och checkat in den en tredje gång. Så nu ser historiken ut så här:

Git‑kodförråd med tre incheckningar
Figur 142. Git‑kodförråd med tre incheckningar

Låt oss nu gå igenom exakt vad reset gör när du anropar det. Det manipulerar direkt dessa tre träd på ett enkelt och förutsägbart sätt. Det gör upp till tre grundläggande operationer.

Steg 1: flytta HEAD

Det första reset gör är att flytta vad HEAD pekar på. Det är inte samma sak som att ändra HEAD självt (vilket är vad checkout gör); reset flyttar grenen som HEAD pekar på. Det innebär att om HEAD är inställd på grenen master (det vill säga att du för närvarande står på master), kommer git reset 9e5e6a4 att börja med att göra att master pekar på 9e5e6a4.

Mjuk återställning
Figur 143. Mjuk återställning

Oavsett vilken variant av reset med en incheckning du anropar är detta det första den alltid försöker göra. Med reset --soft stannar den helt enkelt där.

Ta nu en sekund och titta på diagrammet och inse vad som hände: den ångrade i praktiken det senaste git commit-kommandot. När du kör git commit skapar Git en ny incheckning och flyttar grenen som HEAD pekar på till den. När du gör reset tillbaka till HEAD~ (föräldern till HEAD) flyttar du grenen tillbaka till där den var, utan att ändra indexet eller arbetskatalogen. Du kan nu uppdatera indexet och köra git commit igen för att åstadkomma det som git commit --amend skulle ha gjort (se Ändra den senaste incheckningen).

Steg 2: uppdatera indexet (--mixed)

Observera att om du kör git status nu ser du i grönt skillnaden mellan indexet och vad den nya HEAD är.

Nästa sak reset gör är att uppdatera indexet med innehållet i den ögonblicksbild som HEAD nu pekar på.

Blandad återställning
Figur 144. Blandad återställning

Om du anger --mixed-flaggan stannar reset vid denna punkt. Det är också standard, så om du inte anger någon flagga alls (i det här fallet bara git reset HEAD~) stannar kommandot här.

Ta nu en sekund till att titta på diagrammet och inse vad som hände: det ångrade fortfarande din senaste incheckning, men gjorde också allting okölagt. Du rullade tillbaka till tiden innan du körde dina git add- och git commit-kommandon.

Steg 3: uppdatera arbetskatalogen (--hard)

Den tredje saken reset gör är att få arbetskatalogen att se ut som indexet. Om du använder --hard fortsätter den till detta steg.

Hård återställning
Figur 145. Hård återställning

Så låt oss tänka igenom vad som hände. Du ångrade din senaste incheckning, git add och git commit-kommandona, och allt arbete du gjorde i din arbetskatalog.

Det är viktigt att notera att flaggan (--hard) är det enda sättet att göra kommandot reset farligt, och ett av de få tillfällen där Git faktiskt kan förstöra data.

Alla andra anrop av reset kan relativt enkelt ångras, men flaggan --hard kan det inte, eftersom den med tvång skriver över filer i arbetskatalogen. I det här fallet har vi fortfarande v3-versionen av vår fil i en incheckning i vår Git‑databas, och vi skulle kunna få tillbaka den genom att titta i vår reflog, men om vi inte hade checkat in den skulle Git ändå ha skrivit över filen och den hade varit omöjlig att återställa.

Sammanfattning

Kommandot reset skriver över dessa tre träd i en specifik ordning och stannar där du säger till:

  1. Flytta grenen som HEAD pekar på (stanna här om --soft).

  2. Få indexet att se ut som HEAD (stanna här om inte --hard).

  3. Få arbetskatalogen att se ut som indexet.

Nollställning med en sökväg

Det täcker beteendet hos reset i sin grundform, men du kan också ange en sökväg som kommandot ska påverka. Om du anger en sökväg kommer reset att hoppa över steg 1 och begränsa resten av sina åtgärder till en specifik fil eller uppsättning filer. Det är faktiskt ganska rimligt — HEAD är bara en pekare, och du kan inte peka på en del av en incheckning och en annan del av en annan. Men indexet och arbetskatalogen kan uppdateras delvis, så reset fortsätter med steg 2 och 3.

Så, anta att vi kör git reset file.txt. Den formen (eftersom du inte angav ett inchecknings-SHA‑1 eller en gren, och du inte angav --soft eller --hard) är en förkortning för git reset --mixed HEAD file.txt, vilket:

  1. Flytta grenen som HEAD pekar på (hoppa över) .

  2. Få indexet att se ut som HEAD (stanna här) .

Så den kopierar i praktiken bara file.txt från HEAD till indexet.

Blandad återställning med sökväg
Figur 146. Blandad återställning med sökväg

Detta har den praktiska effekten att filen avköas. Om vi tittar på diagrammet för det kommandot och tänker på vad git add gör är de exakta motsatser.

Köar fil till index
Figur 147. Köar fil till index

Det är anledningen till att utdata från git status föreslår att du kör det här för att avköa en fil (se Ta bort en köad fil för mer om detta).

Vi skulle lika gärna kunna hindra Git från att anta att vi menade "dra data från HEAD" genom att ange en specifik incheckning att hämta den filversionen från. Vi skulle då köra något som git reset eb43bf file.txt.

Mjuk återställning med sökväg till en specifik incheckning
Figur 148. Mjuk återställning med sökväg till en specifik incheckning

Det gör i praktiken samma sak som om vi hade återställt innehållet i filen till v1 i arbetskatalogen, kört git add på den, och sedan återställt den tillbaka till v3 igen (utan att faktiskt gå igenom alla de stegen). Om vi kör git commit nu kommer det att registrera en ändring som återställer filen till v1, även om vi aldrig faktiskt hade den i arbetskatalogen igen.

Det är också intressant att notera att likt git add accepterar kommandot reset flaggan --patch för att avköa innehåll diffstycke för diffstycke. Så du kan selektivt avköa eller återställa innehåll.

Sammanfoga incheckningar

Låt oss titta på hur du kan göra något intressant med den här nyfunna kraften — sammanfoga incheckningar.

Säg att du har en serie incheckningar med meddelanden som "oops.", "WIP" och "forgot this file". Du kan använda reset för att snabbt och enkelt sammanfoga dem till en enda incheckning som får dig att se riktigt smart ut. Sammanfoga incheckningar visar ett annat sätt att göra detta, men i det här exemplet är det enklare att använda reset.

Låt oss säga att du har ett projekt där den första incheckningen har en fil, den andra incheckningen lade till en ny fil och ändrade den första, och den tredje incheckningen ändrade den första filen igen. Den andra incheckningen var ett arbete på gång och du vill sammanfoga den.

Git‑kodförråd
Figur 149. Git‑kodförråd

Du kan köra git reset --soft HEAD~2 för att flytta HEAD-grenen tillbaka till en äldre incheckning (den senaste incheckningen du vill behålla):

Flyttar HEAD med mjuk återställning
Figur 150. Flyttar HEAD med mjuk återställning

Och sedan helt enkelt köra git commit igen:

Git‑kodförråd med sammanfogad incheckning
Figur 151. Git‑kodförråd med sammanfogad incheckning

Nu kan du se att din nåbara historik, den historik du skulle skicka, nu ser ut som att du hade en incheckning med file-a.txt v1, och sedan en andra som både ändrade file-a.txt till v3 och lade till file-b.txt. Incheckningen med v2-versionen av filen finns inte längre i historiken.

Växla till det

Till sist kanske du undrar vad skillnaden är mellan checkout och reset. Likt reset manipulerar checkout de tre träden, och det är lite olika beroende på om du ger kommandot en sökväg eller inte.

Utan sökvägar

Att köra git checkout [gren] är ganska likt att köra git reset --hard [gren] genom att det uppdaterar alla tre träd så att de ser ut som [gren], men det finns två viktiga skillnader.

För det första, till skillnad från reset --hard, är checkout säkert för arbetskatalogen; den kontrollerar att den inte skriver över filer som har ändringar i sig. Faktum är att den är lite smartare än så — den försöker göra en trivialsammanfogning i arbetskatalogen, så alla filer du inte har ändrat blir uppdaterade. reset --hard kommer däremot helt enkelt ersätta allt utan att kontrollera.

Den andra viktiga skillnaden är hur checkout uppdaterar HEAD. Medan reset flyttar grenen som HEAD pekar på, flyttar checkout själva HEAD till att peka på en annan gren.

Till exempel, säg att vi har grenarna master och develop som pekar på olika incheckningar, och att vi står på develop (så HEAD pekar på den). Om vi kör git reset master kommer develop nu att peka på samma incheckning som master gör. Om vi i stället kör git checkout master flyttar sig inte develop, det gör däremot HEAD självt. HEAD kommer nu att peka på master.

Så i båda fallen flyttar vi HEAD till att peka på incheckning A, men hur vi gör det är väldigt olika. reset flyttar grenen som HEAD pekar på, checkout flyttar själva HEAD.

`git checkout` och `git reset`
Figur 152. git checkout och git reset

Med sökvägar

Det andra sättet att köra checkout är med en sökväg, vilket, precis som reset, inte flyttar HEAD. Det är precis som git reset [gren] fil i att det uppdaterar indexet med den filen från den incheckningen, men det skriver också över filen i arbetskatalogen. Det skulle vara exakt som git reset --hard [gren] fil (om reset skulle låta dig köra det) — det är inte säkert för arbetskatalogen och flyttar inte HEAD.

Dessutom, precis som git reset och git add, accepterar checkout flaggan --patch för att låta dig selektivt återställa filinnehåll diffstycke för diffstycke.

Sammanfattning

Förhoppningsvis förstår du nu och känner dig mer bekväm med kommandot reset, men du är sannolikt fortfarande lite förvirrad över hur exakt det skiljer sig från checkout och kan knappast minnas alla regler för de olika varianterna.

Här är en fusklapp för vilka kommandon som påverkar vilka träd. Kolumnen "HEAD" visar "REF" om det kommandot flyttar referensen (grenen) som HEAD pekar på, och "HEAD" om det flyttar HEAD självt. Var särskilt uppmärksam på kolumnen "AK säker?" — om den säger NEJ, stanna upp och fundera innan du kör kommandot.

HEAD Index Arbetskatalog AK säker?

Incheckningsnivå

reset --soft [commit]

REF

NEJ

NEJ

JA

reset [commit]

REF

JA

NEJ

JA

reset --hard [commit]

REF

JA

JA

NEJ

checkout <commit>

HEAD

JA

JA

JA

Filnivå

reset [commit] <paths>

NEJ

JA

NEJ

JA

checkout [commit] <paths>

NEJ

JA

JA

NEJ