Den här artikeln är en introduktion till distribuerade versionshanteringsverktyg i allmänhet, och till git i synnerhet. Den kommer att ge en översikt av de grundläggande kommandona för att komma igång och jobba med verktyget. Jag kommer också beröra de anledningar, som jag ser det, som gör att git och andra distribuerade verktyg är överlägsna de traditionella. Artikeln läses med fördel från början till slutet, men du kan naturligtvis hoppa direkt till avsnittet om olika sätt att jobba med git, om du vill.
En av de mest grundläggande komponenterna i en systemutvecklares verktygslåda är versionshanteringsverktyget. Tidigare stod valet oftast mellan CVS, Subversion och nåt kommersiellt (men tyvärr ofta sämre) alternativ som Perforce, ClearCase eller Visual SourceSafe. Grundprincipen för alla dessa är väldigt lik: filerna som versionshanteras ligger i ett centraliserat repository på en server. Alla som jobbar i projektet måste ha tillgång till detta repository och läs- eller skrivrättigheter till det beroende på vilken roll de har.
När en projektmedlem ska arbeta med filerna hämtar han/hon hem dem till sin dator (”checkar ut” projektet) och jobbar med dem där (i en ”arbetskatalog” som ligger lokalt). När projektmedlemmen sedan är klar med en del av det som ska göras checkar han in (eller ”committar”) filerna tillbaka till repositoryt. Under projektets gång kan han regelbundet hämta hem det senaste som checkats in i repositoryt (”göra en update”).
Vid mer eller mindre förutbestämda tillfällen under projektets gång kan man välja att sätta en stämpel (”tagga”) på ett visst känt läge och/eller ”brancha” repositoryt så att man får två parallella spår att gå på. Det sker oftast i samband med övergång mellan projektfaser, som vid slutet av en scrum-sprint, vid delleveranser till kund, start av acceptanstestperioder och naturligtvis vid slutleveransen av projektet.
Det finns inget direkt hinder för att tagga eller brancha mitt under ett projekt, men i praktiken görs det väldigt sällan. Anledningen till att man drar sig för detta kan vara att det oftast upplevt som ett potentiellt väldigt komplicerat arbete att sedan återföra (”merga”) förändringar man gjort i en branch tillbaka till huvudspåret när man är klar, och även att alla taggar och brancher påverkar hela projektet och måste således antingen förankras bland alla eller följa strikta projektkonventioner. (Eftersom branchningen görs i repositoryt är den ”global”. Om du gör en branch och ger den namnet ”optimizing” så kommer alla att se den branchen och ingen annan kan efter detta använda det namnet för en tagg eller branch.)
Om du som läser detta är en van CVS/Subversion-användare så tycker du nog att det är självklarheter jag berättar om. Antagligen har du inte ens ifrågasatt varför arbetssättet går till som ovan utan bara accepterat att det är så det funkar. Men på senare tid har det dykt upp ett flertal mer eller mindre obskyra verktyg som möjliggör ett helt annat sätt att jobba på: distribuerade versionshanterare!
Distribuerad versionshantering
Den avgörande skillnaden mellan traditionella och distribuerade versionshanteringssystem är att de senare inte har något centralt repository som alla kontinuerligt arbetar mot. Istället kan samma projekt ha ett stort antal repositoryn som, i alla fall rent tekniskt, är helt likvärdiga.
De största open source-baserade distribuerade versionshanteringssystemen idag är Git, Mercurial, Bazaar, Darcs och säkert några till. I praktiken är det dock Git och Mercurial som fått mest hype och flest användare och som dessutom får en extra skjuts av att de används av några andra kända open source-projekt. Mercurial har OpenJDK (open source-varianten av Java) som referensprojekt, medan Git stoltserar med så vitt skilda projekt som Linux Kernel, Ruby on Rails och Javascript-biblioteket Prototype.
Resten av den här artikeln kommer att fokusera på Git, men Mercurial är snarlik i sin arkitektur och skiljer sig nog mest på lågnivådetaljerna och kringverktygen som finns.
Git i praktiken
Git fungerar bra på Linux, Mac och Windows (med vissa brasklappar för det sistnämnda), men jag tänkte inte gå igenom hur man installerar på respektive plattform, där får jag hänvisa till dokumentationen på Gits hemsida. På förekommen anledning vill jag dock påpeka att paketnamnet på Ubuntu (och säkert på andra Debian-baserade linux-distributioner) inte är ”git”, utan ”git-core”. Paketet ”git” är nåt helt annat, vilket kan leda till en del förvirring.
Jag tänkte gå igenom några fiktiva användningssteg med git. Vi börjar med ett arbetssätt som liknar det sätt man jobbar med Subversion etc, för att sedan utforska andra sätt att jobba som i praktiken kräver ett distribuerat versionshanteringssystem för att överhuvudtaget vara möjligt.
Skapa en arbetskatalog från ett befintligt projekt
Läget är att det finns ett kod-repository som du vill använda. Det kan vara ett publikt repository, eller ett internt i det projekt du för närvarande jobbar. Du vill ”checka ut” projektet till din lokala dator för att kunna utveckla vidare på det där.
Git-kommandot för detta heter ”clone” och används på detta sätt:
git clone robu@projectserver.cygni.se:/var/lib/git/ourproject.git
Kommandot ovan skapar en ny katalog direkt under din aktuella katalog med namnet ”ourproject”, och checkar sedan ut projektets innehåll i den. Det förutsätter att du har en användare på servern med namnet ”robu” som kan logga in via ssh. För att det ska funka någorlunda smidigt bör du också se till att kunna logga in med kryptonyckel istället för lösenord.
Gå in i katalogen ”ourproject” och bekräfta att alla projektfiler ligger där. Nästa steg är att uppdatera en fil och checka in ändringen i repositoryt igen. Vi antar att projektet innehåller en textfil med namnet README som du lägger till en rad i. Prova nu att köra kommandot ”git status”. Du får en utskrift med bland annat texten ”modified: README”. Det indikerar alltså att du har en ändring i den filen som inte commitats till repositoryt ännu. Dags att göra det:
git commit -a -m "added a line. just testing, actually."
Prova köra ”git status” igen direkt efter commiten, så får du se att allt numera är incheckat. Commit-raden ovan är väldigt lik motsvarande kommando för subversion, som skulle ha sett ut så här:
svn commit -m "just testing"
Det finns dock två skillnader. Den ena kan verka vara finlir: ”-a” i git-kommandot säger åt git att committa alla förändrade filer. Det är ju det beteendet som Subversion har som default, men vi får återkomma till vad git gör istället, om man utelämnar den flaggan.
Den andra skillnaden är något mer dramatisk. Om du, ”checkar ut” (med git clone som tidigare) projektet på nytt nu, från samma repository men på ett annat ställe på din hårddisk, så kommer du att märka att din förändring inte finns med! Så — vad hände med incheckningen som du nyss gjorde…?
En arbetskatalog – ett repository
Nu märker vi den första riktigt viktiga skillnaden mellan git och Subversion. Till skillnad från Subversion, som alltid använder sig av ett centraliserat repository för ett projekt så jobbar git alltid med lokala repositoryn. Alltså: om du har en arbetskatalog så finns det också ett lokalt repository knutet till detta. Du ser repositoryt som en (dold) katalog i roten av arbetskatalogen, med namnet ”.git”. På gits hemsida finns en tutorial som bland annat går igenom innehållet i repository-katalogerna. Intressant läsning, men vi kommer inte att gå in på det mer i den här artikeln, utan betrakta det som en ”black box” som innehåller alla versioner av alla filer och kataloger i projektet.
Din arbetskatalog har alltså ett helt eget repository, där du kan göra de vanliga versionshanteringsmomenten: checka in, tagga, brancha, etc. Den mest uppenbara skillnaden i handhavandet på den här nivån, jämfört med i Subversion, är att allt detta går på bråkdelen av en sekund istället för en tålamodsprövande väntan vid varje steg. Det beror både på att allt ju görs lokalt, men även på att git är oerhört mycket snabbare på de allra flesta funktioner även när man kör över nätet.
Men om man nu vill att ens förändringar faktiskt ska återföras till det centrala projekt-repositoryt, hur gör man då?
Gits distribuerade natur gör det möjligt att synka förändringar mellan repositoryn på ett smidigt sätt. För att skicka tillbaka dina förändringar till projektrepositoryt på servern kör du kommandot ”push”, så här:
git push robu@projectserver.cygni.se:/var/lib/git/ourproject.git
Väldigt bra grej. Men med tanke på hur vanligt det är att man vill göra just det så kan det kännas som om det finns potential för förenkling här. Och enklare blir det. Modellen ovan med den kompletta URLen till repositoryt kan användas för att synka med vilket repository som helst. Men om du vill synka med just det repository som är ditt repositorys ursprung, dvs det som du klonade ovan, så kan du helt enkelt köra:
git push
Motsvarande kommando för att hämta hem de senaste förändringarna från servern till ditt lokala repository heter analogt:
git pull
Vi kan ta en liten utökad titt på den här finessen. Om du skriver kommandot ”git remote” får du en lista över de ställen som ditt repository känner till, med kortnamn. I det läge vi befinner oss just nu kommer bara ett namn att dyka upp: ”origin”. Detta är ett speciellt namn som ges till det repository man har klonat lokalt. Om man kör ”push” eller ”pull” utan att ange repository (som vi såg ovan) så används det repository som pekas ut av ”origin”. Det intressanta här är att man när som helst kan ändra vilket repository som är ”origin”.
Så, om ni är ett team som då och då vill synka sinsemellan varandra (istället för att gå via ett centralt repository), så kan du lägga till namn på vart och ett av dina teammedlemmars repositoryn:
git remote add kalle guest@kalle-laptop:/home/kalle/projects/ourproject<br></br>
git remote add pelle guest@pelle-desktop:/home/pelle/proj/theproject
När Pelle sedan säger till dig att han vill att du ska prova hans förändringar han gjort i projektet så mergar du in hans kod med ett enkelt handgrepp:
git pull pelle
Brancha lättviktigt
Nu när vi har fått en kort intro till hur distribuerade repositoryn hänger ihop så ska vi återgå till en klassisk versionshanteringsfeature: branchning. En branch är ett parallellt spår av samma källa. Vid branchningstillfället skapas konceptuellt en kopia av hela innehållet i repositoryt, som man sedan kan växla mellan.
Användningsområdena för branchning är lite olika. Ofta vill man göra en branch när man har släppt en version av en produkt till produktion. På så sätt kan man underhålla den produktionssatta branchen och fixa kritiska buggar där, samtidigt som man fortsätter med utvecklingen av nästa version i huvudspåret. Ett annat användningsområde är ”experimentell utveckling”. Om du vill göra en större förändring i ett system, t.ex. byta ut en av de grundläggande ramverken som systemet vilar på, så kan det vara bra att göra det i ett isolerat utvecklingsspår. På så sätt är det lätt att backa om det visar sig att man valt fel väg och den stora förändringen störs inte heller av den övriga utvecklingen som pågår parallellt.
I båda exemplena ovan är det viktigt att man kan återföra förändringar man har gjort i branchen till huvudspåret. Antingen fortlöpande, som när man fixar kritiska buggar i en release-branch, eller vid ett specifikt tillfälle, när man är färdig med experimentet och vill applicera det på huvudspåret.
Vi börjar med att kolla vilka branchar som finns för närvarande:
git branch
Kommandot ”branch” utan parametrar listar alla lokala branchar. Har vi inte skapat några sådana explicit så finns åtminstone branchen ”master” där, som är den som motsvarar huvudspåret och fungerar som default för de kommandon som tar en branch som parameter.
Dags att prova göra en ny branch! För att skapa en ny branch och sedan byta till den branchen i arbetskatalogen kan man antingen köra dessa två kommandon:
git branch vitestar<br></br>
git checkout vitestar
...eller kombinera de båda stegen till ett:
git checkout -b vitestar
Börja gärna med att notera hur blixtsnabbt operationen ovan gick! Enligt välinformerade källor ska operationen ”skapa en branch” i git, motsvara att skriva en ny fil med 43 bytes. En ny listning av aktuella branchar (”git branch”) visar att vi nu har en ny branch med namnet ”vitestar”, som är markerad med en asterisk för att markera att det är den aktuella branchen.
För att byta till en annan lokal branch används ”checkout”-kommandot:
git checkout master
En enkel övning i det här läget är att switcha fram och tillbaka mellan dessa brancher och ändra i samma fil och observera hur de två spåren hanteras parallellt. När du sedan vill merga in ändringar från en branch till en annan gör du så här (från ”vitestar” till ”master”):
git checkout master<br></br>
git merge vitestar
Git är bra på att merga, och en mergning går även den oerhört snabbt. Men git kan inte trolla, så självklart blir det konflikt när olika ändringar gjorts på samma ställe. Det hanteras i princip på samma sätt som i CVS och Subversion: konfliktmarkeringar läggs in i filen. Sedan är det bara att manuellt korrigera filen och checka in den på nytt.
Förbered incheckningen med gits index
En av gits egenheter som jag har slätat över lite ovan, men som är viktig att känna till, är ”indexet”. Man kan säga att en ”commit” i git egentligen görs i två steg. Först läggs förändringarna som ska commitas, in i en ”temporär databas” som kallas för ”index”. När man sedan kör ”git commit” är det bara de förändringar som ligger i indexet som tas med i committen.
För att lägga till saker i indexet används ”git add”. En genväg med detta är att använda parametern ”-a” till commit, för att automatiskt köra ”add” på alla filer som har ändrats (och sedan committa dem). Men då måste man istället komma ihåg att ”-a”-parametern bara lägger till filer som redan finns i repositoryt i indexet. Helt nya filer måste man alltså explicit göra en ”git add” på innan man committar.
Om du vill komma igång utan att kunna alla nyanser av git så räcker det att du lägger till ”-a” på alla commit-kommandon, och kommer ihåg att göra ”git add” på alla nya filer. Men det är viktigt att känna till indexets existens och betydelse, eftersom det hänvisas till på flera ställen i gits dokumentation. Det gör det till exempel mycket enklare att förstå (den faktiskt ganska informativa) utskriften från ”git status”.
Skapa ett git-projekt från scratch
Allt hittills har varit grunderna för att göra ”det dagliga arbetet” med git, men det bygger från början på att någon skapat ett git-repository som du vill klona och jobba mot. När du börjar från scratch, eller med ett befintligt projekt som inte ligger i något git-repository ännu, ser uppstartsprocessen ut ungefär såhär:
- Ställ dig i projektkatalogen (roten för projektet)
- Kör:
git init
Du har nu skapat ett tomt git-repository som ligger under ”.git”. - Skapa filen ”.gitignore”, där du kan lista filer (med jokertecken) som inte ska komma med i repositoryt – nu eller senare. Den kan innehålla rader som ”.log”, ”.class”, och så vidare.
- Kör:
git add .
Nu har alla filer och alla underkataloger markerats som redo att läggas in i repositoryt. - Kör:
git commit -m "Initial checkin"
Nu är alla befintliga filer incheckade i repositoryt!
Skapa ett centralt git-repository
Alla repositoryn i git är tekniskt sett likvärdiga. Ni kan mycket väl klara er utan ett centralt repository, bara genom att pusha till och pulla från varandra. Men ibland kan det finns skäl att lägga upp ett repository på en server. Det kan vara för att man helt enkelt föredrar att jobba mot en central server, eller för att man vill ha ett ”officiellt” repository som finns tillgängligt för allmänheten. Många (alla?) git-baserade open source-projekt har ett sådant repository nånstans.
Om man har ett repository på en server finns det i praktiken ingen anledning att ha en arbetskatalog knuten till det. Ett sådant repository kallas för ”bare”, och skapas med flaggan ”–bare” till clone-kommandot. Det är precis ett likadant repository som brukar ligga under ”.git” i en arbetskatalog, men när man skapar det utan arbetskatalog är konventionen sådan att det skapas i en katalog som heter ”projektnamn.git”.
Alltså, vi provar! Vi har en katalog, ”~/projects/newproject”, där vi har en massa källkod som ska versionshanteras. Vi har skapat ett lokalt repository för den arbetskatalogen, enligt beskrivningen i förra avsnittet. Nu vill vi lägga upp det på en server för att göra den tillgänglig till alla, även om min dator skulle vara avstängd.
Vi ställer oss i ~/temp och kör följande:
git clone --bare ~/projects/newproject newproject.git
Nu har vi ett repository under katalogen ”newproject.git” som inte har någon arbetskatalog knuten till sig. Nästa steg visar på enkelheten med git: repositoryt är helt ”self contained” och kan fungera oavsett var man placerar det. Vi kopierar helt enkelt upp det till servern, precis som det ligger:
scp -r newproject.git robu@projectserver:/var/lib/git
Vi går sedan tillbaka till vår ursprungliga projektkatalog och pekar ut det nya serverrepositoryt som ”origin”, så att vi enkelt kan pusha och pulla mot det hädanefter:
cd ~/projects/newproject<br></br>
git remote add origin robu@projectserver:/var/lib/git/newproject.git
Om vi hade velat köra med hängslen och livrem (men vem vill det...?) så skulle vi förstås istället kunna flytta undan den ursprungliga arbetskatalogen och hämta hem en ny klon av serverrepositoryt:
cd ~/projects<br></br>
mv newproject newproject.BAK
git clone robu@projectserver:/var/lib/git/newproject.git
Resultatet borde bli detsamma (förutom att vi har en backup kvar av den ursprungliga projektkatalogen).
Arbetssätt med git
Hittills har vi sett att git visserligen är annorlunda, men ändå kan användas på samma sätt som traditionella versionshanteringssystem. Väljer man att jobba på det sättet finns det ändå stora fördelar jämfört med CVS/Subversion:
- Prestanda. Både CVS och Subversion är notoriskt långsamma för många olika typer av operationer, medan git så gott som alltid levererar på en bråkdel av en sekund.
- Arbeta offline. Eftersom du alltid har ditt repository med dig så kan du jobba som vanligt oavsett om du befinner dig utanför internet-täckning, på flygplan, på solsemester eller på lantstället.
- Flexibilitet. Eftersom det är trivialt enkelt att peka om ”origin” från en server till en annan så blir det inte lika viktigt att allt blir 100% rätt från början. Repositoryn kan enkelt flyttas utan att det medför en väsentlig störning för utvecklingen i projektet.
Jag ser faktiskt egentligen ingen nackdel med git jämfört med CVS/Subversion vad gäller funktionalitet i verktyget. Det enda som skulle kunna motivera de traditionella verktygen framför git är om det finns något kringverktyg som absolut kräver CVS/Subversion och där git helt enkelt inte fungerar.
Men gits distribuerade natur möjliggör helt andra arbetssätt som inte alls fungerar (eller i varje fall inte särskilt bra) med traditionella verktyg. Här är ett par exempel:
Ett arbetssätt som lämpar sig särskilt bra för stora open source-projekt (t.ex Linux Kernel, och det är också på det här sättet som de jobbar), är att bygga upp ett informellt ”network of trust”. Den person som har hand om det officiella, publika repositoryt har delat upp systemet i ca ett dussin olika ansvarsområden. Var och ett av dessa områden hanteras av en person som centralfiguren (låt oss kalla honom för ”Linus”) litar på. Var och en av dessa har i sin tur ett dussintal egna medarbetare som hjälper dem att koda på deras delsystem och som de i sin tur litar på.
När en kodare har gjort en ny feature eller fixat en bugg säger han till den person som ansvarar för hans grupp att göra en ”git pull” från hans repository. Den ansvarige kan dra in förändringen till en egen lokal branch och granska den separat, eller bara göra en ”git pull” rakt av och låta git hantera mergningen. Blir det en konflikt i mergen kan han antingen lösa den själv eller backa tillbaka pull-kommandot och låta kodaren själv göra en pull från administratören och fixa konflikten där innan han skickar en ny ”pull request” igen (dvs ber att administratören mergar in hans nya kod i sitt repository med ”git pull”).
På toppnivån går allt till på precis samma sätt, men med troligen lite större kodförändringar att hantera. Men Linus kräver av sina närmaste att de gjort en pull från hans repository innan de skickar en ”pull request” till honom. Även Linus kan dra in den nya koden i en lokal branch för att snabbt se vilka förändringar som kommer in. Han gör säkert stickprov på områden som intresserar och/eller oroar honom extra mycket, men i stort sett bygger det på att han har ett stort förtroende för de medarbetare han delegerat ut delsystemen till.
Git, med dess lokala repository och snabba och enkla branchar, uppmuntrar också till andra sätt att arbeta. Det finns t.ex. ingen anledning att inte checka in något, bara för att det t.ex. inte kompilerar eller att testsviten inte går att köra. Självklart finns det goda anledningar att lägga in sådana regler för vissa branchar och för vissa speciella repositoryn (t.ex. master-branchen på ett officiellt projektrepository för nåt stort open source-projekt), men ”under brinnande utveckling” kan du fortfarande checka in precis när du behagar. Om du inte pushar ut repositoryt så är det ingen annan som berörs, eller ens märker av, hur du väljer att brancha och checka in kod.
Faktum är att (och här skiljer sig nog git från de flesta versionshanteringssystem) du kan gå tillbaka till tidigare incheckningar och ändra dem i efterhand. Kanske har du gjort en ”för stor” commit, som egentligen inkluderar flera orelaterade förändringar. Du kan då gå tillbaka och göra om den till flera separata incheckningar. Du kan till och med bryta upp förändringar som är gjorda i samma fil till flera separata incheckningar! Exakt hur det går till tror jag får vänta till en senare artikel.
Tips och tricks
Jag vill nämna ett par smågrejer som gör det enklare att komma igång med att prova på git. Den första av dessa är verktyget ”git-svn”. Med hjälp av git-svn kan du använda dig av ett lokalt git-repository som synkar med ett konventionellt centralt subversion-repository. Du kan alltså använda dig av git och få många av dess fördelar även om resten av projektet fortfarande sitter bara med subversion. (Stackarna!)
Jag kan också rekommendera alla som hanterar sitt git-repository från ett bash-shell att lägga in aktuell branch i bash-prompten. Att skapa och hoppa mellan brancher är en del av det dagliga arbetet när man jobbar med git och det är praktiskt att alltid ha en tydlig markeringom vilken branch man befinner sig i just nu.