Göm menyn

Versionshantering och git

De flesta programmeringsprojekt utförs av mer än en person, och då blir det viktigt att ha ett bra sätt att synkronisera arbetet. Detta kallas versionshantering och det finns en mängd olika verktyg för det där det absolut mest vanliga heter git.

Svårigheterna med synkronisering av programkod

Om två personer arbetar med samma kodbas vill man se till att de kan arbeta parallelt, att deras ändringar inte påverkar varandra och att det går enkelt att slå ihop ändringarna när de är klara. Om man till exempel skulle använda zip-filer och email för att synkronisera koden skulle det vara ett stort jobb att slå ihop två separata ändringar. Varje fil skulle behöva jämföras och slås ihop manuellt.

Ett alternativ skulle kunna vara något i stil med Google Drive där alla ändringar synkroniseras i realtid. Det skulle dock också bidra till problem då hela koden måste vara fri från syntaxfel för att python ska kunna köra den. Om en utvecklare är mitt i en rad så skulle den andra utvecklaren inte kunna testa sin kod.

Versionshanteringsverktyg som SVN och Git löser detta genom att hålla koll på alla versioner av projektet och genom att hjälpa till med att slå ihop ändringarna. När en utvecklare ska börja implementera en ny "funktion" börjar den med att hämta (eng. checkout) senaste versionen av projektet från en central källa, oftast en server. Utvecklaren gör ändringar för att implementera funktionen och när det är klart skapar hen en commit som innehåller alla andra ändringar. Den nya committen slås ihop (eng. merge) med den centrala versionen som då får den nya funktionaliteten.

En grafisk beskrivning av det kan ses här:

Grafisk beskrivning av centraliserade versionshanterare

Decentralisering

Git, som vi kommer att använda i kursen, fungerar inte exakt som den tidigare beskrivningen eftersom att det är ett så kallat decentraliserat versionshanteringssystem. Varje utvecklare har en egen kopia av hela projektet lokalt på sin dator och det behövs ingen central server, checkouts, merges och commits görs alltså alltid lokalt på datorn.

Oftast vill man dock synkronisera ändringar mellan flera utvecklare och datorer och då brukar ändå en central server användas. För att kommunicera med den "pushas" och "pullas" kod till och från servern. Om man tidigare arbetat med ett icke-decentraliserat versionshanteringssystem där commit direkt skickar ändringarna till servern så kan detta kännas lite ovant. I git uppdaterar nästan alla kommandon den lokala kopian och endast push och pull används för synkronisering mellan olika klienter och servrar.

En grafisk beskriving av decentraliserad versionshantering kan ses här Grafisk beskrivning av decentraliserade versionshanterare

Att använda git

Setup

När man använder git har man nästan alltid en central server som har en kopia av all kod i projektet. I den här kursen, och många andra IDA-kurser kommer vi att använda LiUs gitlab-server för detta. Du kan komma dit på gitlab.liu.se.

För att kunna skriva och testa koden måste du först skapa en lokal kopia av git-repositoryt på din dator. Detta görs enklast med kommandot git clone <url>. På gitlab som kommer att användas i den här kursen hittar man URLen på projektets huvudsida som kan ses här.

Det finns två alternativa sätt att kommunicera med gitlabservern, HTTPS och SSH. Med HTTPS används användarnamn och lösenord varje gång man ska autentisera sig med servern medan SSH använder en SSH nyckel som gör det automatiskt. SSH är alltså lite smidigare att använda men setupen första gången tar någon minut extra.

SSH

För att kunna autentisera sig via SSH måste man skapa en SSH-nyckel och ge den till gitlab-servern så att de vet att nyckeln är associerad med ditt konto.

En guide för det finns på https://docs.gitlab.com/ce/ssh/README.html

Välj sedan alternativet SSH när du hämtar URLen som ska användas till git clone.

Om git säger Agent admitted failure to sign using the key när man klonar med SSH måste man följa instruktionerna här: https://help.github.com/articles/error-agent-admitted-failure-to-sign/.

Fallgropar med SSH-nycklar

Ibland föreslår någon att man ska göra följande i sin .bashrc. Gör inte det.

eval "$(ssh-agent -s)"  # Don't do this
ssh-add                 # Don't do this

Den första raden startar en "ssh-agent" som håller reda på SSH-nycklarna så att man inte behöver skriva sin passphrase hela tiden. Fönstersystemet ska redan göra detta, och det enda man åstadkommer är att man får flera olika ssh-agenter att hålla reda på.

Den andra raden lägger till nycklar i ssh-agenten. Den behöver då läsa in din passphrase (som du ska ha) från terminalfönstret, och kör du detta via Thinlinc hänger sig sessionen direkt eftersom det inte finns något terminalfönster att läsa från. Kör du inte från Thinlinc blir det kanske bara en del onödiga frågor varje gång du startar ett nytt terminalfönster.

HTTPS

När HTTPS används behöver du inte göra någon inital setup, använd istället bara HTTPS-URLen med git clone

Göra ändringar

När du har gjort några ändringar i din kod som du är nöjd med så ska du pusha dessa ändringar till huvud-repositoryt för projektet så att andra projektmedlemmar kan ta del av dem.

För att se vilka filer som har ändrats sedan senaste gången du commitade används git status

Detta kommer att skriva något i stil med

On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   studiematerial/git.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    labbar/projekt/instructions.html

Filerna som listas under 'changes not staged for commit' är alla filer som versionshanteras och som du har ändrat på. 'Untracked files' är andra filer som ligger i mappen, men som inte hanteras av git.

För att spara ändringarna som har gjorts till en fil används git add <filnamn>.

Efter att du kört det på alla filer du vill ändra och kör git status igen kommer du att få något i stil med:

On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   studiematerial/git.md

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   studiematerial/git.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    labbar/projekt/instructions.html

Om det ser bra ut och alla filer du vill ha med är under 'changes to be committed' kan du använda git commit -m '<commitmeddelande>' för att spara ändringarna. Det är viktigt att skriva ett bra committmeddelande eftersom det låter dig och andra utvecklare i projektet se du ändrade och varför. Se mer om det under rubriken 'historik'

Commit sparar ändringarna du gjort lokalt och låter dig se dem i historiken, men antagligen vill du också skicka ändringarna till huvudservern så att andra i projektet också kan ta del av dem.

Detta görs genom att skriva git push

Om det lyckas kommer git att skriva något i stil med

Counting objects: 10, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (10/10), done.
Writing objects: 100% (10/10), 2.00 KiB | 0 bytes/s, done.
Total 10 (delta 6), reused 0 (delta 0)
To gitlab.liu.se:frask812/python-next-generation.git
   5f065f5..3b402ac  master -> master

Pull/Merge

Ibland kan den dock ge en felutskrift som ser ut så här:

To gitlab.liu.se:frask812/python-next-generation.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'git@gitlab.liu.se:frask812/python-next-generation.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Detta betyder att någon annan har gjort ändringar i samma git-repository och att du måste se till att era ändringar slås ihop korrekt.

För att hämta ändringarna från centrala repositoryt används kommandot git pull vilket kan få lite olika resultat.

Om git kan slå ihop era ändringar automatiskt, vilket oftast går så kommer en editor att öppnas och du får skriva ett commmitmeddelande för mergen. Gör det, spara och stäng editorn (om du hamnat i en konstig editor du inte känner igen, se rubriken om vim). Du kommer då att se något i stil med:

Merge made by the 'recursive' strategy.
 labbar/projekt/instructions.md  | 11 +++++++++--
 labbar/projekt/skeleton/main.py |  4 ++--
 2 files changed, 11 insertions(+), 4 deletions(-)

Detta betyder att allt är ok. Om du har opushade ändringar borde du nu kunna köra git push igen.

Merge conflicts

I vissa fall, om du och någon annan i projektet har gjort ändringar på samma ställe i en fil så kan git inte automatiskt slå ihop era ändringar. Du kommer då istället få ett meddelande som säger automatic merge failed och en fil som inte lyckades merga. Om du kör git status kommer du att se några filer under 'changes to be committed' och några under 'changes not staged for commit'.

Filerna som inte är staged for commit är de filer med konflikter och du måste manuellt öppna dem i en editor för att lösa konflikterna.

Om du gör det kommer du att se vissa ställen där git har ändrat koden till

...
<<<<<<<<<<<<<<HEAD
# Den här koden finns på servern
def do_something():
==============
# Den här koden finns lokalt
def dont_do_something():
>>>>>>>>>>>>>>"commit id"
    ...

Ändra den delen av koden så att allt viktigt från både din version och serverns version kommer med och spara filen. Kör igenom koden när du har fixat alla sådana block, så att du ser att allt löstes korrekt. Commita sedan resultatet.

Local changes would be overwritten by merge

remote: Counting objects: 42, done.
remote: Compressing objects: 100% (24/24), done.
remote: Total 24 (delta 16), reused 0 (delta 0)
Unpacking objects: 100% (24/24), done.
From gitlab.liu.se:frask812/python-next-generation
   3d19779..5f065f5  master     -> origin/master
error: Your local changes to the following files would be overwritten by merge:
    studiematerial/git.md
Please commit your changes or stash them before you merge.
Aborting

Ibland när git pull körs kan det här meddelandet visas. Det betyder att en fil du har ändrat men inte committat också har ändrats på servern. Om något skulle bli fel när git mergar skulle git inte kunna lösa det och den låter dig därför inte merga eller pulla. För att lösa detta måste du antingen committa dina ändringar till den filen och sedan pulla eller göra git stash.

Stash

git stash sparar dina ändringar lokalt och sätter tillbaka filerna till hur de såg ut vid den senaste committen. Om du kommer på att du vill använda ändringarna i framtiden kan du göra git stash apply.

Git stash är ett bra sätt att slänga bort ändringar du inte vill ha, eller att spara ändringar temporärt om du vill pulla andras kod men inte är redo att committa din kod.

Historik och 'blame'

En väldigt användbar funktion i git och andra versionshanterare är att man kan gå tillbaka till gamla versioner och se hur koden såg ut då. Detta kan vara bra om man hittar en bugg och vill hitta en gammal version där buggen inte fanns för att se vad som har förändrats

För att titta vilka gamla commits som har gjorts kan du köra kommandot git log, vilket ger en lista med commits och lite information om dem. git log ger ett långt commit-id, datumet och vem som gjorde committen och committmeddelandet för committen. I många fall vill man bara veta ett kortare commit-id och commitmeddelandet och då kan man lägga till flaggan --oneline till git log.

Här visas även vikten av bra commitmeddelanden, om man letar efter en speciell ändring är det väldigt svårt att hitta den om alla commits heter hej eller asdasd.

När du hittat en commit som du vill veta mer om kan kan du göra flera saker. Med kommandot git checkout <id> kan du titta på hela repot som det såg ut efter den committen. För att komma tillbaka till nuvarande versionen av repot används git checkout master.

Du kan även se alla ändringar som har gjorts sedan en commit genom att köra git diff <id>. I både checkout och diff kan man även använda HEAD^n istället för id för att hämta ändringar från n commits bakåt i tiden.

git diff kan även användas för att se vilka ändringar som har gjorts sedan senaste gången du committade genom att inte skriva något commit-id. Om du bara vill se ändringarna i en viss fil kan du även använda git diff <filnamn>

Användbara hjälpmedel i git

Här är några funktioner i git som kan vara bra att känna till och använda för att göra arbetet enklare

.gitignore

Om filen .gitignore finns i ett repository kommer alla filer som står med där att ignorerars när git status, git add och liknande körs. Python skapar till exempel .pyc-filer som man inte bör commita. För att få git att strunta i dem kan man då lägga in *.pyc i .gitignore vilket ignorerar alla filer med ett namn som slutar på .pyc.

commit -am

Väldigt ofta när man arbetar med git vill man göra git add på alla unstaged ändringar till filer som redan är med i repoot. git commit -am "commitmeddelande" gör just det och commit i ett och samma kommando.

Vim

Ibland när man arbetar med git ber den en att göra ändringar i en extern texteditor. Vilken texteditor som används beror på vad variabeln $EDITOR är satt till och default är vim. Vim kan vara lite svår att använda om man inte är van vid det och det är speciellt svårt att ta sig ur.

För att ta sig ur används :q vilket borde skrivas nere i vänstra hörnet. Om filen har ändrats av misstag, kanske när man testar andra, vanligare kommandon, kan man behöva göra :q! vilket säger till editorn att strunta i ändringarna. Om man har gjort ändringar man faktiskt vill spara används :wq eller :x.

Om kommandona man skriver skrivs in i texten istället eller inte fungerar är man antagligen i fel "mode". Om det står till exempel -- INSERT -- nere i vänstra hörnet betyder det att man är i texteditor-mode och det borde gå att skriva text som vanligt. För att komma ur de andra moderna används Escape.

Om inget av det fungerar kan man vara i någon mer obskyr mode, till exempel ex och en dålig men fungerande lösning är då att köra pkill vim i en annan terminal.

Alternativa UI

I den här kursen används git från terminalen men det finns en mängd andra gitinterface man också kan använda.

Github windowsklient

På windows går det att använda githubs gitklient som låter en göra commits och liknande via ett GUI istället för terminalen. Det går självklart också att installera terminalversionen av git på windows.

IDEer

Många IDEer har inbyggda GUI:n för git. I javakursen kommer ni till exempel att få prova IntelliJ IDEAs gitinterface.

Git på egen dator

På Debian, Ubuntu eller Linux mint kan git installeras med sudo apt install git.

På mac och windows kan man hämta git på https://git-scm.com/download


Sidansvarig: Peter Dalenius
Senast uppdaterad: 2021-12-03