Git-exercises-2
I den tidligere Git-øvelse arbejdede du med:
- Working Directory
- Staging Area
- Local Repository
- Remote Repository
- commits
- push og pull
I denne øvelse skal du arbejde sammen med en anden udvikler (altså en af dine medstuderende) om det samme repository.
Undervejs vil I opleve nogle af de problemer, der opstår, når flere udviklere arbejder på den samme kodebase.
Målet er både at lære Git bedre at kende og at undersøge, hvorfor Continuous Integration – hyppig integration af små ændringer i en fælles kodebase – kan gøre samarbejdet lettere.
Arbejd sammen to og to.
Vi kalder jer i resten af øvelsen:
- Udvikler A
- Udvikler B
Udvikler A opretter et nyt Java-projekt og et GitHub-repository.
Projektet skal mindst indeholde:
src/
Main.java
Customer.java
Order.java
README.md
Opret et første commit og push projektet til GitHub.
Udvikler B skal nu have sin egen lokale kopi af projektet.
Find ud af, hvordan Udvikler B får projektet ned på sin computer.
Når I begge har projektet, skal I undersøge:
git status
git log --oneline
git remote -v
Tegn eller forklar situationen.
Hvor mange repositories findes der nu?
Overvej især:
- Har A og B samme Working Directory?
- Har A og B samme Local Repository?
- Hvilket repository deler de?
- Indeholder de lokale repositories på dette tidspunkt den samme historik?
Se svar
Udvikler B kan clone repositoryet:
git clone URL-TIL-REPOSITORY
Der findes nu mindst tre Git-repositories:
A's Local Repository
↕
GitHub Repository
↕
B's Local Repository
A og B har hver deres Working Directory og Local Repository.
GitHub-repositoryet er det fælles Remote Repository.
Umiddelbart efter clone og før nye ændringer vil de tre repositories normalt indeholde samme commit-historik.
Nu skal I arbejde samtidig.
Udvikler A:
Foretag en ændring i Customer.java.
Udvikler B:
Foretag en ændring i Order.java.
Begge skal:
- kontrollere ændringerne
- stage dem
- committe dem lokalt
I må ikke pushe endnu.
Undersøg jeres lokale historik.
Er de to historikker stadig ens?
Hvor findes A’s ændring?
Hvor findes B’s ændring?
Nu pusher A sit commit til GitHub.
Derefter forsøger B at pushe sit commit.
B’s push bliver afvist.
Undersøg Git’s fejlmeddelelse.
Der er ingen merge-konflikt.
Hvorfor vil Git alligevel ikke acceptere B’s push?
Hvad er der sket med Remote Repository, siden B sidst hentede det?
Find ud af, hvordan B kan hente og integrere A’s ændring.
Undersøg bagefter:
git status
git log --oneline
Kontrollér også:
- Findes A’s ændring i
Customer.java? - Findes B’s ændring stadig i
Order.java?
B skal til sidst pushe resultatet.
Se svar
A har pushed et commit, som B ikke har i sit lokale repository.
Derfor kan B ikke umiddelbart pushe sin egen historik oven på remote.
B kan hente og integrere ændringerne med:
git pull
Git kan normalt integrere ændringerne automatisk, fordi A og B har ændret forskellige filer.
B kan derefter pushe:
git push
Den vigtige pointe er:
Et afvist push er ikke det samme som en merge-konflikt.
Sørg først for, at begge udviklere har den nyeste version:
git pull
Nu skal begge ændre Customer.java.
Antag fx at klassen indeholder:
public class Customer {
private String name;
private String email;
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
Udvikler A ændrer eller tilføjer kode omkring getName().
Udvikler B ændrer eller tilføjer kode omkring getEmail().
Begge laver et lokalt commit.
A pusher først.
B forsøger derefter at pushe.
Løs situationen på samme måde som tidligere.
Begge har denne gang ændret:
Customer.java
Alligevel kan Git sandsynligvis integrere ændringerne automatisk.
Hvorfor?
Se svar
En merge-konflikt opstår ikke nødvendigvis, bare fordi to udviklere har ændret den samme fil.
Git forsøger at kombinere ændringerne.
Hvis ændringerne ligger forskellige steder i filen og ikke er i konflikt med hinanden, kan Git normalt merge dem automatisk.
Sørg igen for, at begge har den nyeste version.
Find den samme metode i Customer.java.
Den kunne fx være:
public String getDisplayName() {
return name;
}
Nu skal I bevidst ændre den samme kode forskelligt.
Udvikler A ændrer fx til:
public String getDisplayName() {
return "Customer: " + name;
}
Udvikler B ændrer fx til:
public String getDisplayName() {
return name.toUpperCase();
}
Begge laver et lokalt commit.
A pusher først.
B forsøger derefter at pushe.
B skal forsøge at løse problemet ved at hente ændringerne fra GitHub.
Denne gang kan Git ikke automatisk afgøre, hvordan de to ændringer skal kombineres.
Undersøg:
git status
Åbn derefter den konfliktramte fil.
Find markeringer, der ligner:
<<<<<<<
=======
>>>>>>>
Hvad tror du, de betyder?
Løs ikke konflikten med det samme.
Stop og undersøg filen.
Find:
- B’s egen kode
- A’s kode fra GitHub
- konfliktmarkørerne
Hvorfor vælger Git ikke bare den nyeste ændring?
Hvilken af de to løsninger er korrekt?
Kan Git vide det?
Se svar
Git kan registrere, at begge commits har ændret det samme område forskelligt.
Git kan derimod ikke afgøre, hvilken programlogik teamet ønsker.
Konfliktmarkørerne viser de alternative versioner.
En merge-konflikt betyder derfor ikke, at Git er gået i stykker.
Det betyder:
Git kan ikke automatisk afgøre, hvordan ændringerne skal kombineres. En udvikler skal træffe beslutningen.
B skal nu løse konflikten.
Tal sammen om, hvordan getDisplayName() faktisk skal fungere.
I behøver ikke vælge enten A’s eller B’s løsning. Den korrekte løsning kan også være en kombination.
Ret filen, så:
- den indeholder den ønskede kode
- konfliktmarkørerne er væk
- programmet stadig kan compile og køre
Brug:
git status
undervejs.
Find derefter ud af, hvordan du fortæller Git:
Konflikten er nu løst, og denne version af filen er den version, vi ønsker.
Afslut integrationen og push resultatet til GitHub.
Begge udviklere skal til sidst have samme version.
Kontrollér med:
git status
git log --oneline
og ved at undersøge koden.
Se svar
Når konflikten er løst manuelt, stages filen:
git add Customer.java
Kontrollér:
git status
Derefter afsluttes merge-processen med et commit, hvis Git kræver det:
git commit
Til sidst:
git push
Den anden udvikler kan derefter hente den integrerede version:
git pull
Den præcise historik og behovet for et særskilt merge-commit kan afhænge af Git-konfigurationen.
Det centrale workflow er:
Opdag konflikt
↓
Undersøg konflikten
↓
Beslut korrekt kode
↓
Ret filen
↓
Stage den løste fil
↓
Afslut integrationen
↓
Push
Nu skal I fremprovokere endnu en merge-konflikt.
Denne gang skal konflikten løses ved hjælp af IntelliJs Git-værktøjer.
Gentag princippet fra den foregående opgave:
- Begge starter med nyeste version.
- Begge ændrer samme område forskelligt.
- Begge committer lokalt.
- A pusher.
- B forsøger at integrere A’s ændringer.
- Der opstår konflikt.
Brug nu IntelliJs værktøj til at undersøge og løse konflikten.
Sammenlign med den manuelle løsning.
Hvad hjælper IntelliJ jer med?
Hvad skal I stadig selv beslutte?
Har IntelliJ en anden slags Git-repository end Git Bash?
Se svar
IntelliJ gør det lettere visuelt at sammenligne de forskellige versioner og vælge eller kombinere ændringer.
Men IntelliJ kan ikke nødvendigvis afgøre, hvilken programlogik der er rigtig.
Det er stadig udviklerens ansvar.
IntelliJ og Git Bash arbejder med det samme Git-repository.
Nu skal I undersøge betydningen af, hvor ofte et team integrerer sit arbejde.
Begge udviklere starter med samme version.
Forestil jer følgende arbejdsdag:
09:00 Begge henter nyeste kode
09:05 A begynder at programmere
B begynder at programmere
10:00 Begge arbejder videre
11:00 Begge arbejder videre
12:00 Begge arbejder videre
13:00 Begge arbejder videre
14:00 Begge forsøger at integrere deres arbejde
I løbet af dagen har begge ændret flere filer og måske noget af den samme kode.
Hvad kan være sket med forskellen mellem A’s og B’s kode i løbet af de fem timer?
Hvordan påvirker det:
- risikoen for merge-konflikter?
- størrelsen på konflikterne?
- hvor svært det er at forstå konflikterne?
- hvor svært det er at finde ud af, hvilken løsning der er korrekt?
Se forslag til svar
Jo længere udviklerne arbejder uafhængigt af hinanden, desto større kan forskellen mellem deres kode blive.
Det kan betyde:
- flere ændringer skal integreres på én gang
- større risiko for at ændre de samme områder
- større og mere komplekse konflikter
- vanskeligere fejlfinding
- vanskeligere at afgøre, hvilken ændring der har introduceret et problem
Problemet er altså ikke kun om man integrerer, men også hvor ofte man gør det.
Prøv nu en anden arbejdsform.
Arbejd igen på det samme projekt.
Denne gang gælder følgende regel:
Lav kun en lille sammenhængende ændring ad gangen.
Når ændringen virker, skal I følge denne cyklus:
ændre kode
↓
køre/teste programmet
↓
commit
↓
integrere med nyeste fælles kode
↓
push
Gentag cyklussen flere gange, mens I begge arbejder.
Lav mindst tre små integrationer hver.
Før hver push skal du sikre dig, at dit arbejde er integreret med den nyeste fælles kode.
Sammenlign denne arbejdsform med Del 7.
Hvilken arbejdsform giver de største forskelle mellem udviklernes lokale kode?
Hvornår opdager I, at jeres ændringer påvirker hinanden?
Hvornår er en konflikt lettest at forstå:
- efter 10 minutters arbejde?
- efter en hel dags arbejde?
- efter en uges arbejde?
Se forslag til svar
Når små ændringer integreres ofte, holdes forskellen mellem udviklernes kode typisk mindre.
Det betyder, at integrationsproblemer opdages tidligere.
Hvis en konflikt opstår kort efter en lille ændring, er det også ofte lettere at forstå:
- hvad der er blevet ændret
- hvorfor det blev ændret
- hvordan konflikten bør løses
Hyppig integration garanterer ikke, at konflikter aldrig opstår. Den hjælper med at forhindre, at integration vokser til en stor og vanskelig opgave.
I har nu prøvet eller diskuteret to forskellige arbejdsformer.
Arbejd længe
Arbejd længe
Arbejd længe
Arbejd længe
Integrér
Lille ændring
Test
Integrér
Lille ændring
Test
Integrér
Lille ændring
Test
Integrér
Den anden arbejdsform er central i Continuous Integration.
Formulér sammen med din makker jeres egen forklaring på:
Hvad betyder Continuous Integration?
Forklar derefter, hvorfor hyppig integration kan reducere problemerne ved samarbejde om en fælles kodebase.
Er målet med Continuous Integration at undgå alle merge-konflikter?
Eller er målet snarere at opdage integrationsproblemer tidligt og mens de stadig er små?
Se svar
Continuous Integration er en udviklingspraksis, hvor udviklere ofte integrerer små ændringer i en fælles kodebase.
I stedet for at udviklere arbejder isoleret i lang tid og først senere forsøger at samle deres arbejde, integreres ændringer løbende.
Det betyder blandt andet, at:
- forskellen mellem udviklernes kode holdes mindre
- integrationsproblemer opdages tidligere
- konflikter typisk bliver lettere at forstå
- fejl opdages tættere på den ændring, der introducerede dem
Målet er ikke nødvendigvis, at merge-konflikter aldrig opstår.
Pointen er, at integration bliver en hyppig og normal del af udviklingsarbejdet frem for en stor aktivitet, der sker efter lang tids uafhængigt arbejde.
A ændrer en metode i Customer.java.
B ændrer en metode i Order.java.
Git kan uden problemer merge ændringerne.
Der opstår ingen merge-konflikt.
Efter integrationen starter I programmet.
Programmet virker ikke længere korrekt.
Hvordan kan det ske?
Hvis Git kunne merge koden uden konflikt, betyder det så, at programmet er korrekt?
Hvad kontrollerer Git?
Hvad kontrollerer Git ikke?
Forestil jer fx:
- A ændrer navnet eller betydningen af en metode.
- B skriver kode, som afhænger af den tidligere opførsel.
Git kan måske godt kombinere tekstændringerne.
Men fungerer systemet stadig?
Hvad kunne teamet gøre efter hver integration for hurtigere at opdage sådanne problemer?
Se svar
Git arbejder med ændringer i filer og historik.
At Git kan merge to ændringer automatisk betyder ikke, at det samlede program fungerer korrekt.
Der kan opstå en logisk integrationsfejl uden en Git merge-konflikt.
Derfor kombineres Continuous Integration normalt med automatiseret:
- build
- test
Efter integration skal teamet hurtigt kunne opdage, hvis den fælles kodebase ikke længere virker.
For hver situation skal I:
- forklare, hvad der er sket
- beskrive, hvad udvikleren bør gøre
- forklare, hvordan situationen hænger sammen med Continuous Integration
Du har lavet et commit.
Du forsøger:
git push
Git afviser dit push, fordi remote indeholder commits, du ikke har lokalt.
Der er endnu ingen merge-konflikt.
Hvad gør du?
Du henter ændringerne fra remote.
Git integrerer dem automatisk.
Hvorfor kunne Git gøre det?
Hvordan kontrollerer du bagefter, at programmet stadig virker?
Du henter ændringerne.
Git skriver:
CONFLICT
og git status viser en konfliktramt fil.
Hvad gør du?
Hvem bestemmer, hvordan den endelige kode skal se ud – Git eller udvikleren?
Git merger uden konflikter.
Programmet kan stadig compile.
En test fejler.
Var integrationen succesfuld?
Forklar forskellen mellem:
- en Git merge-konflikt
- en integrationsfejl i programmet
To udviklere har arbejdet hver for sig i fem dage.
Begge har ændret mange af de samme filer.
De skal nu samle deres arbejde.
Et andet team har integreret små ændringer flere gange om dagen.
Hvilket team forventer I får de største integrationsproblemer?
Hvorfor?
Hvilken udviklingspraksis forsøger at undgå, at integration bliver en stor særskilt aktivitet?
Se forslag til svar
Remote-repositoryet indeholder ændringer, som ikke findes lokalt.
Udvikleren skal hente og integrere de fælles ændringer, før arbejdet kan pushes.
Et naturligt første skridt er:
git pull
Git kunne automatisk merge ændringerne, fordi ændringerne ikke var tekstmæssigt i konflikt.
Det betyder ikke nødvendigvis, at programmet fungerer.
Programmet bør derfor bygges og testes efter integrationen.
Udvikleren skal:
- undersøge konflikten
- forstå begge ændringer
- beslutte den korrekte samlede løsning
- rette filen
- stage den løste fil
- afslutte integrationen
- teste resultatet
- pushe
Git opdager konflikten.
Udvikleren afgør den korrekte løsning.
Git-integrationen er gennemført teknisk, men den samlede software fungerer ikke korrekt.
En merge-konflikt betyder, at Git ikke automatisk kan kombinere tekstændringer.
En integrationsfejl betyder, at dele af systemet ikke fungerer korrekt sammen.
Der kan derfor sagtens være integrationsfejl uden merge-konflikter.
Teamet, der har arbejdet uafhængigt i fem dage, har sandsynligvis større integrationsrisiko.
Continuous Integration forsøger netop at gøre integration til en hyppig del af det daglige udviklingsarbejde.
Små ændringer integreres løbende, så problemer opdages tidligt.
I denne øvelse har I selv gjort noget i retning af:
Kode
↓
Test
↓
Commit
↓
Integrér
↓
Test
↓
Push
Men der er et problem:
Hvad hvis udvikleren glemmer at køre testene?
Et naturligt næste skridt er derfor at lade en server automatisk:
Push til GitHub
↓
Hent kode
↓
Build projekt
↓
Kør tests
↓
Rapportér resultat
Det kan fx gøres med GitHub Actions.
Automatiseringen understøtter Continuous Integration ved hurtigt at kontrollere, om den kode, der løbende integreres, stadig kan bygges og består testene.
I denne øvelse får I især brug for kommandoer, I allerede kender:
git status
git diff
git add
git commit
git log --oneline
git pull
git push
Når noget går galt, så start ikke med tilfældigt at prøve forskellige Git-kommandoer.
Start med:
git status
Læs, hvad Git fortæller jer, og find derefter ud af, hvilket problem I faktisk skal løse.