Versionshantering och Git
Versionshanteringssystem (VCS:er) är verktyg som används för att spåra ändringar i källkod (eller andra samlingar av filer och mappar). Som namnet antyder hjälper dessa verktyg till att bevara en historik över ändringar. Dessutom underlättar de samarbete. Logiskt sett spårar VCS:er ändringar i en mapp och dess innehåll som en serie ögonblicksbilder (snapshots), där varje ögonblicksbild kapslar in hela tillståndet för filer och mappar inom en toppnivåkatalog. VCS:er lagrar också metadata som vem som skapade varje ögonblicksbild, meddelanden kopplade till den och så vidare.
Varför är versionshantering användbart? Även när du arbetar själv kan det hjälpa dig att titta på gamla ögonblicksbilder av ett projekt, föra logg över varför vissa ändringar gjordes, arbeta på parallella utvecklingsgrenar och mycket mer. När du arbetar med andra är det ett ovärderligt verktyg för att se vad andra har ändrat, och för att lösa konflikter i samtidig utveckling.
Moderna VCS:er låter dig också enkelt (och ofta automatiskt) svara på frågor som:
- Vem skrev den här modulen?
- När redigerades den här specifika raden i den här specifika filen? Av vem? Varför redigerades den?
- Under de senaste 1000 revisionerna, när/varför slutade ett visst enhetstest att fungera?
Även om det finns andra VCS:er är Git i praktiken standarden för versionshantering. Den här XKCD-serien fångar Gits rykte:

Eftersom Gits gränssnitt är en läckande abstraktion kan det bli förvirrande att lära sig Git uppifrån och ner (med start i kommandoraden). Det går att memorera en handfull kommandon och behandla dem som magiska trollformler, och följa tillvägagångssättet i serien ovan när något går fel.
Även om Git onekligen har ett fult gränssnitt är den underliggande designen och idéerna vackra. Ett fult gränssnitt måste memoreras, men en vacker design kan förstås. Därför ger vi en förklaring nerifrån och upp av Git, med start i datamodellen och därefter kommandoraden. När datamodellen väl är förstådd blir kommandona lättare att förstå i termer av hur de manipulerar den underliggande datamodellen.
Gits datamodell
Gits genialitet ligger i dess välgenomtänkta datamodell som möjliggör alla fina funktioner i versionshantering, som att bevara historik, stödja grenar och möjliggöra samarbete.
Ögonblicksbilder
Git modellerar historiken för en samling filer och mappar inom någon toppnivåkatalog som en serie ögonblicksbilder.
I Git-terminologi kallas en fil en “blob”, och den är bara en hög med byte.
En katalog representeras av ett trädobjekt (tree), som mappar namn till blobbar eller andra trädobjekt (så kataloger kan innehålla andra kataloger).
En ögonblicksbild är det toppnivåträdobjekt (tree) som spåras.
Till exempel kan vi ha ett träd enligt följande:
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")
Toppnivåträdet innehåller två element, ett trädobjekt foo (som i sin tur innehåller ett element, en blob bar.txt) och en blob baz.txt.
Modellera historik: relatera ögonblicksbilder
Hur ska ett versionshanteringssystem relatera ögonblicksbilder? En enkel modell vore att ha en linjär historik. En historik vore då en lista av ögonblicksbilder i tidsordning. Av flera skäl använder Git inte en så enkel modell.
I Git är historik en riktad acyklisk graf (DAG) av ögonblicksbilder. Det kan låta som ett fint matematikord, men låt dig inte avskräckas. Allt det betyder är att varje ögonblicksbild i Git refererar till en uppsättning “föräldrar”, ögonblicksbilderna som kom före den. Det är en uppsättning föräldrar i stället för en enda förälder (som i en linjär historik), eftersom en ögonblicksbild kan härstamma från flera föräldrar, till exempel genom att slå samman två parallella utvecklingsgrenar.
Git kallar dessa ögonblicksbilder för incheckningar. En visualisering av en incheckningshistorik kan se ut ungefär så här:
o <-- o <-- o <-- o
^
\
--- o <-- o
I ASCII-bilden ovan motsvarar o:na individuella incheckningar (ögonblicksbilder).
Pilarna pekar på föräldern till varje incheckning (det är en “kommer före”-relation, inte “kommer efter”).
Efter den tredje incheckningen delar historiken upp sig i två separata grenar.
Detta kan till exempel motsvara två separata funktioner som utvecklas parallellt, oberoende av varandra.
I framtiden kan dessa grenar slås samman för att skapa en ny ögonblicksbild som innehåller båda funktionerna, och ge en ny historik som ser ut så här, med den nyskapade incheckningen för sammanslagningen i fetstil:
o <-- o <-- o <-- o <---- o
^ /
\ v
--- o <-- o
Incheckningar i Git är oföränderliga. Detta betyder dock inte att misstag inte kan rättas. Det betyder bara att “redigeringar” av incheckningshistoriken i själva verket skapar helt nya incheckningar, och referenser (se nedan) uppdateras för att peka på de nya.
Datamodell, som pseudokod
Det kan vara lärorikt att se Gits datamodell nedskriven i pseudokod:
// a file is a bunch of bytes
type blob = array<byte>
// a directory contains named files and directories
type tree = map<string, tree | blob>
// a commit has parents, metadata, and the top-level tree
type commit = struct {
parents: array<commit>
author: string
message: string
snapshot: tree
}
Det är en ren och enkel historikmodell.
Objekt och innehållsadressering
Ett “object” är en blob, tree eller commit:
type object = blob | tree | commit
I Gits datalager är alla objekt innehållsadresserade med sin SHA-1-hash.
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
Blobbar, trädobjekt (tree) och incheckningar förenas på detta sätt.
De är alla objekt.
När de refererar till andra objekt innehåller de dem inte i sin representation på disk, utan har en referens till dem via deras hash.
Till exempel ser trädobjektet (tree) för exempelkatalogstrukturen ovan (visualiserat med git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d) ut så här:
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
Trädobjektet innehåller pekare till sitt innehåll, baz.txt (en blob) och foo (ett trädobjekt).
Om vi tittar på innehållet som adresseras av hashen som motsvarar baz.txt med git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85, får vi följande:
git is wonderful
Referenser
Nu kan alla ögonblicksbilder identifieras med sina SHA-1-hashar. Det är opraktiskt, eftersom människor inte är bra på att minnas strängar med 40 hexadecimala tecken.
Gits lösning på detta problem är referenser: lättlästa namn som pekar på SHA-1-hashar.
Referenser är pekare till incheckningar.
Till skillnad från objekt, som är oföränderliga, är referenser föränderliga (de kan uppdateras för att peka på en ny incheckning).
Till exempel pekar referensen master vanligtvis på den senaste incheckningen i huvudgrenen för utveckling.
references = map<string, string>
def update_reference(name, id):
references[name] = id
def read_reference(name):
return references[name]
def load_reference(name_or_id):
if name_or_id in references:
return load(references[name_or_id])
else:
return load(name_or_id)
Med detta kan Git använda namn som “master” för att referera till en viss ögonblicksbild i historiken, i stället för en lång hexadecimal sträng.
En detalj är att vi ofta vill ha en uppfattning om “var vi befinner oss just nu” i historiken, så att vi när vi tar en ny ögonblicksbild vet vad den är relativ till (hur vi sätter fältet parents i incheckningen).
I Git är detta “var vi befinner oss” en särskild referens som kallas “HEAD”.
Kodförråd
Till sist kan vi definiera vad ett Git-kodförråd (ungefär) är.
Det är datan objects och references.
På disk är allt Git lagrar objekt och referenser.
Det är hela Gits datamodell.
Alla git-kommandon motsvarar någon manipulation av DAG:en för incheckningar genom att lägga till objekt och lägga till/uppdatera referenser.
Varje gång du skriver ett kommando, tänk på vilken manipulation kommandot gör i den underliggande grafdatastrukturen.
Omvänt, om du försöker göra en viss förändring i DAG:en för incheckningar, t.ex. “kasta bort icke-incheckade ändringar och låt refen ‘master’ peka på incheckningen 5d83f9e”, finns det sannolikt ett kommando för det (t.ex. i detta fall git checkout master; git reset --hard 5d83f9e).
Mellanlager
Det här är ett annat koncept som ligger vid sidan av datamodellen, men som ändå är en del av gränssnittet för att skapa incheckningar.
Ett sätt att förstå upplägget ovan är att tänka sig ett kommando som skapar en ny ögonblicksbild utifrån nuvarande tillstånd i arbetskatalogen. Vissa versionshanteringsverktyg fungerar så, men inte Git. Vi vill ha rena ögonblicksbilder, och det är inte alltid optimalt att skapa en ögonblicksbild från det nuvarande tillståndet. Tänk dig till exempel ett scenario där du implementerat två separata funktioner och vill skapa två separata incheckningar, där den första introducerar den första funktionen och nästa introducerar den andra funktionen. Eller tänk dig ett scenario där du lagt till felsökningsutskrifter över hela koden tillsammans med en felrättning. Du vill göra en incheckning med felrättningen samtidigt som du kastar bort alla utskriftssatser.
Git hanterar sådana scenarier genom att låta dig specificera vilka modifieringar som ska ingå i nästa ögonblicksbild via en mekanism som kallas indexet (staging area), alltså en köyta för vad som ska checkas in.
Git-kommandoradsgränssnitt
För att undvika att duplicera information kommer vi inte förklara kommandona nedan i detalj i dessa föreläsningsanteckningar. Se den varmt rekommenderade Pro Git för mer information, eller titta på föreläsningsvideon.
Grunder
git help <command>: få hjälp för ett git-kommandogit init: skapar ett nytt Git-kodförråd, med data lagrad i katalogen.gitgit status: berättar vad som pågårgit add <filename>: lägger till filer i indexetgit commit: skapar en ny incheckning- Skriv bra incheckningsmeddelanden!
- Ännu fler skäl att skriva bra incheckningsmeddelanden!
git log: visar en tillplattad historiklogggit log --all --graph --decorate: visualiserar historiken som en DAGgit diff <filename>: visa ändringar du gjort relativt till indexetgit diff <revision> <filename>: visar skillnader i en fil mellan ögonblicksbildergit checkout <revision>: uppdaterar HEAD (och aktuell gren om du checkar ut en gren)
Grenar och sammanslagning
git branch: visar grenargit branch <name>: skapar en grengit switch <name>: växlar till en grengit checkout -b <name>: skapar en gren och växlar till den- samma som
git branch <name>; git switch <name>
- samma som
git merge <revision>: slår samman med aktuell grengit mergetool: använd ett avancerat verktyg för att hjälpa till att lösa sammanslagningskonfliktergit rebase: basera om en uppsättning patchar på en ny bas
Fjärrförråd
git remote: lista fjärrförrådgit remote add <name> <url>: lägg till ett fjärrförrådgit push <remote> <local branch>:<remote branch>: skicka objekt till fjärrförråd och uppdatera fjärrreferensgit branch --set-upstream-to=<remote>/<remote branch>: sätt upp koppling mellan lokal och fjärrgrengit fetch: hämta objekt/referenser från ett fjärrförrådgit pull: samma somgit fetch; git mergegit clone: klona kodförråd från fjärrförråd
Ångra
git commit --amend: redigera en inchecknings innehåll eller meddelandegit reset <file>: avstaga en filgit restore: kasta bort ändringar
Avancerad Git
git config: Git är mycket anpassningsbartgit clone --depth=1: ytlig klon, utan hela versionshistorikengit add -p: interaktiv mellanlagringgit rebase -i: interaktiv ombaseringgit blame: visa vem som senast redigerade vilken radgit stash: ta tillfälligt bort modifieringar i arbetskatalogengit bisect: binärsök i historiken (t.ex. efter regressioner)git revert: skapa en ny incheckning som vänder effekten av en tidigare incheckninggit worktree: checka ut flera grenar samtidigt.gitignore: specificera avsiktligt ospårade filer som ska ignoreras
Övrigt
- GUI:er: det finns många GUI-klienter för Git. Vi använder dem inte personligen utan föredrar kommandoraden.
- Skalintegration: det är väldigt praktiskt att ha Git-status som en del av skalprompten (zsh, bash). Detta ingår ofta i ramverk som Oh My Zsh.
- Redigerarintegration: liknande ovan, praktiska integrationer med många funktioner. fugitive.vim är standardalternativet för Vim.
- Arbetsflöden: vi lärde ut datamodellen plus några grundläggande kommandon. Vi berättade inte vilka arbetssätt du ska följa i stora projekt (och det finns många olika angreppssätt).
- GitHub: Git är inte GitHub. GitHub har ett specifikt sätt att bidra kod till andra projekt, kallat ändringsförfrågningar (PR:er).
- Andra Git-leverantörer: GitHub är inte unikt. Det finns många värdar för Git-kodförråd, som GitLab och BitBucket.
Resurser
- Pro Git är starkt rekommenderad läsning. Att gå igenom kapitel 1–5 bör lära dig det mesta du behöver för att använda Git skickligt, nu när du förstår datamodellen. De senare kapitlen har intressant, avancerat material.
- Oh Shit, Git!?! är en kort guide för hur man återhämtar sig från vanliga Git-misstag.
- Git for Computer Scientists är en kort förklaring av Gits datamodell, med mindre pseudokod och fler avancerade diagram än dessa föreläsningsanteckningar.
- Git from the Bottom Up är en detaljerad förklaring av Gits implementationsdetaljer bortom datamodellen, för den nyfikne.
- How to explain git in simple words (hur man förklarar Git med enkla ord)
- Learn Git Branching är ett webbläsarbaserat spel som lär dig Git.
Övningar
- Om du inte har tidigare erfarenhet av Git, prova antingen att läsa de första kapitlen av Pro Git eller gå igenom en handledning som Learn Git Branching. När du arbetar igenom den, koppla Git-kommandon till datamodellen.
- Klona kodförrådet för kursens webbplats.
- Utforska versionshistoriken genom att visualisera den som en graf.
- Vem var den senaste personen som modifierade
README.md? (Tips: användgit logmed ett argument). - Vilket incheckningsmeddelande hörde till den senaste modifieringen av raden
collections:i_config.yml? (Tips: användgit blameochgit show).
- Ett vanligt misstag när man lär sig Git är att göra incheckningar med stora filer som inte borde hanteras av Git eller att lägga till känslig information. Prova att lägga till en fil i ett kodförråd, göra några incheckningar och sedan ta bort filen från historiken (inte bara senaste incheckningen). Du kanske vill titta på detta.
- Klona ett valfritt kodförråd från GitHub och modifiera en av dess befintliga filer.
Vad händer när du kör
git stash? Vad ser du när du körgit log --all --oneline? Körgit stash popför att ångra det du gjorde medgit stash. I vilket scenario kan detta vara användbart? - Liksom många kommandoradsverktyg tillhandahåller Git en konfigurationsfil (eller dotfil) kallad
~/.gitconfig. Skapa ett alias i~/.gitconfigså att när du körgit graphfår du utdata frångit log --all --graph --decorate --oneline. Du kan göra detta genom att direkt redigera filen~/.gitconfig, eller använda kommandotgit configför att lägga till aliaset. Information om git-alias finns här. - Du kan definiera globala ignore-mönster i
~/.gitignore_globalefter att ha körtgit config --global core.excludesfile ~/.gitignore_global. Detta sätter platsen för den globala ignore-filen som Git kommer använda, men du måste fortfarande skapa filen manuellt på den sökvägen. Sätt upp din globala gitignore-fil så att den ignorerar OS-specifika eller redigerarspecifika temporära filer, som.DS_Store. - Skapa en avgrening av kodförrådet för kursens webbplats, hitta ett stavfel eller någon annan förbättring du kan göra, och skicka en ändringsförfrågan (PR) på GitHub (du kanske vill titta på detta). Skicka bara ändringsförfrågningar som är användbara (spamma oss inte, tack!). Om du inte hittar någon förbättring att göra kan du hoppa över den här övningen.
- Öva på att lösa sammanslagningskonflikter genom att simulera ett samarbetsscenario:
- Skapa ett nytt kodförråd med
git initoch skapa en fil som heterrecipe.txtmed några rader (t.ex. ett enkelt recept). - Gör en incheckning av den, och skapa sedan två grenar:
git branch saltyochgit branch sweet. - I grenen
salty, modifiera en rad (t.ex. ändra “1 cup sugar” till “1 cup salt”) och gör en incheckning. - I grenen
sweet, modifiera samma rad på ett annat sätt (t.ex. ändra “1 cup sugar” till “2 cups sugar”) och gör en incheckning. - Växla nu till
masteroch provagit merge salty, sedangit merge sweet. Vad händer? Titta på innehållet irecipe.txt- vad betyder markörerna<<<<<<<,=======och>>>>>>>? - Lös konflikten genom att redigera filen för att behålla det innehåll du vill ha, ta bort konfliktmarkörerna och slutför merge med
git addochgit commit(ellergit merge --continue). Alternativt kan du prova att användagit mergetoolför att lösa konflikten med ett grafiskt eller terminalbaserat sammanslagningsverktyg. - Använd
git log --graph --onelineför att visualisera sammanslagningshistoriken du just skapade.
- Skapa ett nytt kodförråd med
Licensed under CC BY-NC-SA.