Versionshantering
När du arbetar med något som förändras över tid är det användbart att kunna spåra ändringarna. Det finns flera skäl: du får en historik över vad som ändrats, hur du ångrar det, vem som ändrade det, och ibland även varför. Versionshanteringssystem (VCS) ger dig den förmågan. De låter dig checka in ändringar i en uppsättning filer, med ett meddelande som beskriver ändringen, samt granska och ångra tidigare ändringar.
De flesta VCS stöder delning av incheckningshistorik mellan flera användare. Det möjliggör smidigt samarbete. Du kan se ändringarna jag gjort, och jag kan se ändringarna du gjort. Eftersom VCS spårar ändringar kan systemet ofta (men inte alltid) räkna ut hur våra ändringar kan kombineras, så länge de rör relativt separata delar.
Det finns väldigt många VCS-system, och de skiljer sig mycket i vad de stödjer, hur de fungerar, och hur man interagerar med dem. Här fokuserar vi på git, ett av de vanligaste, men jag rekommenderar att du också tittar på Mercurial.
Med det sagt, nu till snabbversionen.
Är git mörk magi?
Inte riktigt. Du behöver förstå datamodellen. Vi hoppar över vissa detaljer, men i grova drag är den centrala “saken” i git en incheckning.
- varje incheckning har ett unikt namn, en “revisionshash”
en lång hash som
998622294a6c520db718867354bf98348ae3c7e2förkortas ofta till ett kort (ungefär unikt) prefix:9986222 - en incheckning har författare + incheckningsmeddelande
- den har också hashen för eventuella förfäder oftast bara hashen för föregående incheckning
- en incheckning representerar också en diff, alltså en beskrivning av hur man går från incheckningens förfäder till incheckningen (t.ex. ta bort den här raden i en fil, lägg till de här raderna i en annan, byt namn på en fil, osv.)
- i praktiken lagrar git hela tillståndet före och efter
- du vill troligen inte lagra stora filer som ändras ofta
Initialt har ett kodförråd (ungefär mappen som git hanterar) inget innehåll och inga incheckningar. Låt oss sätta upp det:
$ git init hackers
$ cd hackers
$ git status
Utdatan här ger faktiskt en bra utgångspunkt. Låt oss gå igenom den och se till att vi förstår allt.
Först: “On branch master”.
- man vill inte arbeta med hashar hela tiden
- grenar är namn som pekar på hashar
- master är traditionellt namnet för den “senaste” incheckningen varje gång en ny incheckning skapas flyttas master till den nya incheckningens hash
- det särskilda namnet
HEADbetyder “aktuellt” namn - du kan också skapa egna namn med
git branch(ellergit tag) vi kommer tillbaka till det
Vi hoppar över “No commits yet” eftersom det är självförklarande.
Sedan: “nothing to commit”.
- varje incheckning innehåller en diff med alla ändringar du gjort men hur byggs den diffen från början?
- man skulle kunna checka in alla ändringar sedan senaste incheckning
- ibland vill du bara checka in en del (t.ex. inte
TODOs) - ibland vill du dela upp en ändring i flera incheckningar för att ge separata incheckningsmeddelanden
- ibland vill du bara checka in en del (t.ex. inte
- git låter dig mellanlagra ändringar för att konstruera en incheckning
- lägg till ändringar i en eller flera filer till mellanlagret med
git add - lägg till bara vissa ändringar i en fil med
git add -p - utan argument arbetar
git addpå “alla kända filer” - ta bort en fil och mellanlagra borttagningen med
git rm - töm mängden mellanlagrade ändringar med
git reset - notera att detta inte ändrar några filer det betyder bara att inga ändringar tas med i nästa incheckning
- för att ta bort bara vissa mellanlagrade ändringar:
git reset FILEellergit reset -p - se mellanlagrade ändringar med
git diff --staged
- lägg till ändringar i en eller flera filer till mellanlagret med
- se återstående ändringar med
git diff- när du är nöjd med mellanlagret, skapa en incheckning medgit commit- om du vill checka in alla ändringar direkt:
git commit -a git help addhar mer hjälpsam information
- om du vill checka in alla ändringar direkt:
Medan du testar ovan,
försök köra git status för att se vad git tycker att du gör.
Det är förvånansvärt hjälpsamt.
En incheckning säger du…
Okej, vi har en incheckning. Vad nu?
- vi kan titta på senaste ändringarna:
git log(ellergit log --oneline) - vi kan titta på fullständiga ändringar:
git log -p - vi kan visa en specifik incheckning:
git show master- eller med
-pför full diff/patch
- eller med
- vi kan gå tillbaka till tillståndet vid en incheckning med
git checkout NAME- om
NAMEär en incheckningshash säger git att vi är “detached” det betyder bara att ingetNAMEpekar på incheckningen, så om vi gör incheckningar är det ingen som känner till dem
- om
- vi kan återställa en ändring med
git revert NAME- det applicerar diffen i incheckningen vid
NAMEi omvänd riktning
- det applicerar diffen i incheckningen vid
- vi kan jämföra en äldre version med den här via
git diff NAME.. a..bär ett incheckningsintervall. Om någon sida utelämnas betyder detHEAD.- vi kan visa alla incheckningar mellan två punkter med
git log NAME.. -pfungerar här också- vi kan flytta
mastertill en viss incheckning (och i praktiken ångra allt efter den) medgit reset NAME: - va?
var inte
resettill för mellanlagret?resethar en “andra” form (segit help reset) som sätterHEADtill incheckningen som namnet pekar på - notera att detta inte ändrar några filer,
git diffvisar nu i praktikengit diff NAME..
Vad betyder ett namn?
Namn är uppenbart viktiga i git.
De är nyckeln till att förstå mycket av vad som händer i git.
Hittills har vi pratat om incheckningshashar,
master,
och HEAD.
Men det finns mer.
- du kan skapa egna grenar (som master) med
git branch b- det skapar ett nytt namn,
b, som pekar på incheckningen vidHEAD
- det skapar ett nytt namn,
- du är fortfarande “på” master,
så om du gör en ny incheckning uppdateras master men inte
b- byt till gren medgit checkout b- incheckningar du gör nu uppdaterar namnet
b - byt tillbaka till master med
git checkout master- då “försvinner” ändringarna i
bur sikte
- då “försvinner” ändringarna i
- detta är ett smidigt sätt att testa ändringar
- incheckningar du gör nu uppdaterar namnet
- taggar är andra namn som aldrig ändras och som har egna meddelanden används ofta för utgåvor och ändringsloggar
NAME^betyder “incheckningen föreNAME”- kan appliceras rekursivt:
NAME^^^ - du menar oftast
~när du använder den typen av notation~är “temporal”, medan^går via förfäder~~är samma som^^- med
~kan du också skrivaX~3för “3 incheckningar äldre änX” - du vill inte ha
^3
git diff HEAD^-betyder “föregående namn”- de flesta kommandon arbetar på
HEADom du inte anger annat argument
Städa upp historiken
Din incheckningshistorik kommer väldigt ofta att se ut så här:
lägg till funktion x– kanske till och med med ett bra incheckningsmeddelande omxforgot to add filefix bugtypotypo2actually fixactually actually fixtests passfix example codetypoxxxx
Det är okej för git, men inte särskilt hjälpsamt för ditt framtida jag eller för andra som vill förstå vad som ändrats. git låter dig städa upp detta:
git commit --amend: vik in mellanlagrade ändringar i föregående incheckning- notera att detta ändrar föregående incheckning och ger den en ny hash
git rebase -i HEAD~13är mycket kraftfullt för varje incheckning i de senaste 13 väljer du vad som ska göras:- standard är
pick: gör inget -r: ändra incheckningsmeddelande -e: ändra incheckning (lägg till eller ta bort filer) -s: slå ihop incheckning med föregående och redigera incheckningsmeddelandet -f: “fixup” – slå ihop med föregående och kasta incheckningsmeddelandet - i slutet pekarHEADpå det som nu är sista incheckning - kallas ofta för att slå ihop incheckningar - det som faktiskt händer är:
backa
HEADtill rebasens startpunkt, och återapplicera incheckningar i ordning enligt dina val git reset --hard NAME: återställ alla filer till tillståndet iNAME(ellerHEADom inget namn anges) praktiskt för att ångra ändringar
Arbeta med andra
Ett vanligt användningsfall för versionshantering är att låta flera personer göra ändringar i samma filsamling utan att trampa varandra på tårna. Eller rättare sagt, att säkerställa att om de gör det, så skrivs ändringarna inte bara över tyst.
git är ett distribuerat VCS. Alla har en lokal kopia av hela kodförrådet (eller åtminstone allt andra har valt att publicera). Vissa VCS är centraliserade (t.ex. subversion): en server har alla incheckningar, och klienter har bara filerna de har “checkat ut”. I princip har de bara de aktuella filerna och måste fråga servern för allt annat.
Varje kopia av ett git-kodförråd kan listas som ett fjärrförråd.
Du kan kopiera ett befintligt kodförråd med git clone ADDRESS
(i stället för git init).
Detta skapar ett fjärrförråd som heter origin och pekar på ADDRESS.
Du kan hämta namn och incheckningar de pekar på från ett fjärrförråd med git fetch REMOTE.
Alla namn på remoten blir tillgängliga som REMOTE/NAME,
och du kan använda dem som lokala namn.
Om du har skrivåtkomst till ett fjärrförråd
kan du ändra namn på fjärren så att de pekar på incheckningar du skapat via git push.
Till exempel,
låt oss få master på fjärren origin att peka på samma incheckning
som vår lokala master pekar på:
git push origin master:master- för bekvämlighet kan du sätta
origin/mastersom standardmål förgit pushfrån aktuell gren med-u - fundera: vad gör
git push origin master:HEAD^?
Ofta använder du GitHub,
GitLab,
BitBucket,
eller något annat som fjärrförråd.
Det är inget “särskilt” ur gits perspektiv.
Det är bara namn och incheckningar.
Om någon ändrar master och flyttar github/master till sin incheckning
(vi återkommer till det strax),
kan du efter git fetch github se deras ändringar med git log github/master.
Samarbete i praktiken
Hittills verkar grenar ganska meningslösa. Du kan skapa dem, jobba i dem, men sedan då? Till slut flyttar du väl ändå master till dem, eller?
- vad händer om du måste fixa något medan du arbetar på en stor funktion?
- vad händer om någon annan under tiden gör en ändring i master?
Förr eller senare måste du slå samman ändringar i en gren med ändringar i en annan,
oavsett om ändringarna gjorts av dig eller någon annan.
git gör detta med git merge NAME.
merge kommer att:
- hitta senaste punkt där
HEADochNAMEdelade incheckningsförfader (alltså där de divergerade) - försöka applicera alla dessa ändringar på aktuell
HEAD - skapa en incheckning som innehåller alla ändringar
och listar både
HEADochNAMEsom förfäder - sätta
HEADtill den incheckningens hash
När din stora funktion är klar kan du slå samman dess gren till master, och git ser till att du inte tappar ändringar från någon gren.
Om du har använt git tidigare känner du kanske igen merge under ett annat namn: pull.
När du kör git pull REMOTE BRANCH händer följande:
git fetch REMOTEgit merge REMOTE/BRANCH- där
REMOTEochBRANCH, liksom medpush, ofta utelämnas och då används den spårade fjärrgrenen (minns-u)
Det här fungerar oftast bra så länge ändringarna i grenarna är separata. Om de inte är det får du en sammanslagningskonflikt. Det låter läskigt.
- en sammanslagningskonflikt betyder bara att git inte vet hur den slutliga diffen ska se ut
- git pausar och ber dig slutföra mellanlagringen av sammanslagningsincheckningen
- öppna den konfliktande filen i redigeraren och leta efter många vinkelparenteser (
<<<<<<<) texten ovanför=======är ändringen iHEADsedan gemensam förfader texten under=======är ändringen iNAMEsedan samma förfader git mergetoolär praktiskt, eftersom det öppnar ett diffverktyg- när du löst konflikten genom att bestämma hur filen ska se ut, mellanlagra ändringarna med
git add - när alla konflikter är lösta, avsluta med
git commit- du kan avbryta med
git merge --abort
- du kan avbryta med
Du har just löst din första git-sammanslagningskonflikt.
\o/
Nu kan du publicera dina färdiga ändringar med git push.
När världar krockar
När du pushar kontrollerar git att ingen annans arbete går förlorat när du uppdaterar namnet på fjärren du pushar till.
Det görs genom att kontrollera att fjärrnamnets nuvarande incheckning är en förfader till incheckningen du pushar.
Om så är fallet kan git säkert uppdatera namnet.
Det kallas snabb framflyttning (fast-forwarding).
Om inte vägrar git uppdatera fjärrnamnet och säger att det har tillkommit ändringar.
Om din push nekas, vad gör du då?
- slå samman ändringar från fjärren med
git pull(alltsåfetch+merge) - tvinga push med
--forceDå förloras andras ändringar.- det finns också
--force-with-lease, som bara tvingar om fjärrnamnet inte ändrats sedan senastefetch; det är klart säkrare - om du har rebasat lokala incheckningar som du tidigare pushat (historikomskrivning, gör helst inte det) måste du tvångspusha
- det finns också
- försök återapplicera dina ändringar “ovanpå” fjärrändringarna
- det är en ombasering (
rebase)- backa alla lokala incheckningar sedan gemensam förfader
- fast-forward
HEADtill incheckningen vid fjärrnamnet - applicera lokala incheckningar i ordning
- konflikter kan uppstå och behöva lösas manuellt
- använd
git rebase --continueeller--abort
- mer här
git pull --rebasestartar processen åt dig- om man bör slå samman eller basera om är en het diskussion några bra läsningar:
- det är en ombasering (
Vidare läsning
- Learn git branching
- How to explain git in simple words
- Git from the bottom up
- Git for computer scientists
- Oh shit, git!
- The Pro Git book
Övningar
-
I ett kodförråd, prova att ändra en befintlig fil. Vad händer när du kör
git stash? Vad ser du medgit log --all --oneline? Körgit stash popför att ångra det du gjorde medgit stash. I vilket scenario kan detta vara användbart? -
Ett vanligt misstag när man lär sig git är att checka in stora filer som inte bör hanteras av git, eller att råka lägga till känslig information. Prova att lägga till en fil i ett kodförråd, skapa några incheckningar, och ta sedan bort filen ur historiken (du kan titta på det här). Om du faktiskt vill låta git hantera stora filer, titta på Git-LFS.
- Git är väldigt bra för att ångra ändringar,
men man behöver känna till även ovanliga lägen.
- Om en fil råkar ändras i en incheckning kan den återställas med
git revert. Men om incheckningen innehåller flera ändringar ärrevertkanske inte bästa val. Hur kan vi användagit checkoutför att återställa en filversion från en specifik incheckning? - Skapa en gren,
gör en incheckning i den,
och ta sedan bort branchen.
Kan du fortfarande återställa incheckningen?
Titta på
git reflog. (Obs: återställ “hängande” saker snabbt, git städar periodiskt bort incheckningar som inget pekar på.) - Om man är för snabb med
git reset --hardi stället förgit resetkan ändringar lätt gå förlorade. Eftersom ändringarna var mellanlagrade kan de dock återställas. (Titta pågit fsck --lost-foundoch.git/lost-found.)
- Om en fil råkar ändras i en incheckning kan den återställas med
-
I valfritt git-kodförråd, titta i mappen
.git/hooks. Där finns skript som slutar på.sample. Om du byter namn på dem och tar bort.samplekörs de enligt sitt namn. Till exempel körspre-commitföre en incheckning. Experimentera med dem. -
Liksom många kommandoradsverktyg har
giten konfigurationsfil (dotfile) som heter~/.gitconfig. Skapa ett alias i~/.gitconfigså attgit graphger samma utdata somgit log --oneline --decorate --all --graph(det här är ett bra kommando för att snabbt visualisera incheckningsgrafen). -
Git låter dig också definiera globala ignore-mönster i
~/.gitignore_global. Det är användbart för att förebygga vanliga misstag, som att lägga till RSA-nycklar. Skapa en~/.gitignore_global-fil, lägg till mönstret*rsa, och testa att det fungerar i ett kodförråd. -
När du blir mer van vid
gitkommer du märka återkommande uppgifter, som att redigera.gitignore. git extras erbjuder många småverktyg som integrerar medgit. Till exempel läggergit ignore PATTERNtill mönstret i kodförrådets.gitignore, ochgit ignore-io LANGUAGEhämtar vanliga ignore-mönster för språket från gitignore.io. Installeragit extrasoch testa verktyg somgit aliasellergit ignore. -
Git-GUI-program kan ibland vara mycket användbara. Prova att köra gitk i ett kodförråd och utforska gränssnittets olika delar. Kör sedan
gitk --all. Vilka skillnader ser du? - När man väl vant sig vid kommandoradsprogram kan GUI-verktyg kännas tunga. En bra kompromiss är ncurses-baserade verktyg, som kan navigeras från kommandoraden men fortfarande erbjuder ett interaktivt gränssnitt. Git har tig. Prova att installera det och köra det i ett kodförråd. Du hittar användningsexempel här.
Licensed under CC BY-NC-SA.
