Meny for Per-Arne's Git Good!
Fag og prosjekt Per-Arne's Git Good!

Treveis merge: når begge linjene har flyttet seg

Modul 2: Branches og merge · 12 min

Den interaktive leksjonen trenger JavaScript. Teksten under er hele leksjonen.

Etter leksjonen kan du

  • forklare hva en merge-base er, og finne den med git merge-base anvende
  • forutsi når en merge gir en ny merge-commit med to foreldre forstå
  • lese en merget historikk med git log --oneline --graph, og bruke HEAD^1 og HEAD^2 til å finne hver side av en merge anvende

Forstå: Tre snapshots, to foreldre

Nå har begge linjene fått nye commits etter A. main har B, og feature har C. Ingen av dem er stamfar til den andre.

Git leter bakover fra begge tuppene og finner den nærmeste felles stamfaren. Den kalles merge-base. Her er det A, der linjene skilte lag.

Så sammenligner Git tre snapshots: basen, vår side og deres side. En endring som bare er gjort på én side, blir med i resultatet. Derfor heter dette en treveis merge.

Resultatet lagres i en ny commit, en merge-commit, med to foreldre. main flytter til den. feature blir stående, og B og C er uendret.

Rekkefølgen på foreldrene betyr noe. Første forelder er commiten du stod på, altså B. Andre forelder er den du merget inn, altså C. Git kaller dem HEAD^1 og HEAD^2.

En merge uten feilmeldinger betyr bare at tekstene passet sammen. Den beviser ikke at programmet virker. Kjør testene før du deler resultatet.

Se demo: En ekte merge-commit

Vi står på main. Grafen deler seg: main og feature har hver sin nye commit etter den første.

$ git log --oneline --graph --all

Vi spør Git om merge-basen. Svaret er en commit-ID. Sammenlign med loggen: det er den første commiten, der linjene skilte lag.

$ git merge-base main feature

Nå merger vi inn feature. --no-edit godtar meldingen Git foreslår. Se grafen: en ny commit med to foreldre dukker opp, og main flytter dit.

$ git merge --no-edit feature

Les grafen ovenfra. Øverst står merge-commiten. Rett under deler streken seg i to: én vei til hver forelder. Nederst møtes de igjen i merge-basen.

$ git log --oneline --graph

Første forelder, HEAD^1, er der vi stod før mergen: Add contact page fra main.

$ git log --oneline -1 HEAD^1

Andre forelder, HEAD^2, er det vi merget inn: Add login form fra feature.

$ git log --oneline -1 HEAD^2

Oppgaver

  1. Vis grafen med alle brancher.
  2. Finn den felles stamfaren til main og feature.
  3. Merge feature inn i main.
  4. Vis den andre forelderen til merge-commiten.

Øv: Overfør: en ny merge

Både main og api har fått en ny commit. Du står på api.

Oppgaver

  1. Merge api inn i main.
  2. Lag en branch before-api som peker på commiten main stod på rett før mergen.

Sjekk deg selv: Hent det fram igjen

Hva er merge-basen?

  • Den nyeste commiten på main
  • Den nærmeste commiten som begge branchene har felles i historikken
  • Den aller første commiten i repositoryet

Du stod på main og kjørte git merge feature, og Git laget en merge-commit. Hva er HEAD^2 nå?

  • Commiten to steg tilbake på main
  • Den siste commiten fra feature, som ble merget inn
  • Commiten main pekte på før mergen

Hva skjer med commitene B og C når Git lager merge-commiten M?

  • De slås sammen til M og forsvinner
  • De er uendret. M peker på begge som foreldre.
  • De kopieres over på main

En merge gikk gjennom uten konflikt. Hva vet du da?

  • At programmet fungerer
  • At Git kunne kombinere tekstendringene. Programmet må fortsatt testes.
  • At ingen har endret de samme filene

Repetisjon: Begge sider endret README.md før mergen, og HEAD er merge-commiten. Du vil se README.md slik den var på main rett før mergen. Hvilken kommando viser det?

  • git show HEAD^2:README.md
  • git show HEAD^1:README.md
  • git diff HEAD^1 HEAD

Vanlige misforståelser

  • «En merge kopierer eller skriver om commitene.» De eksisterende commitene er uendret. Merge-commiten peker bare på begge.
  • «HEAD^2 betyr to commits tilbake.» ^2 velger den andre forelderen. Det er ~2 som går to generasjoner tilbake.
  • «En merge uten konflikt betyr at koden virker.» Git sjekker at tekstene kan kombineres, ikke at programmet oppfører seg riktig.

Les mer

Treveisregelen

Når begge linjene har flyttet seg, sammenligner Git tre snapshots for hver fil, og innenfor en fil for hvert endret område: merge-base, vår side (ours, branchen du står på) og deres side (theirs, branchen du merger inn).

Base Vår Deres Resultat
A A A A
A B A B — bare vi endret
A A C C — bare de endret
A B B B — samme endring på begge sider
A B C konflikt — neste leksjon

Det er basen som får dette til å virke. Med bare vår og deres side kunne Git se at versjonene er ulike, men ikke hvem som endret hva. Med basen vet Git hvilken side som gjorde endringen, og kan ta den med. Derfor merges endringer i ulike filer, og til og med i ulike deler av samme fil, vanligvis av seg selv. Ulike endringer på samme eller nabolinjer blir en konflikt.

Finne basen

$ git merge-base main feature
df6c7db3a1e0c9f5b8d2e7a4c6f1b9d0e3a5c7f2

Svaret er den nærmeste commiten begge brancher kan nå, målt langs grafen, ikke etter dato. git log --oneline main..feature lister det feature har og main mangler, altså det merge vil ta inn, og git diff main...feature med tre punktum viser endringene på feature siden basen. I uvanlige historikker der brancher er merget fram og tilbake, kan det finnes flere like gode baser. Gits standardstrategi, ort, merger da først disse basene. Det trenger du sjelden å tenke på.

Anatomien til en merge-commit

$ git merge feature
Merge made by the 'ort' strategy.
 login.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 login.txt
$ git show --no-patch --format='%h %p %s' HEAD
c3d971a cecf599 2379bbb Merge branch 'feature'

%p skriver ut foreldrene. Den første, cecf599, er der main var. Den andre, 2379bbb, er tuppen av feature. git cat-file -p HEAD viser det samme som én tree-linje fulgt av to parent-linjer: en merge-commit er en vanlig commit med ett snapshot, det samlede resultatet, og to foreldre. Commitene på begge brancher blir verken kopiert eller endret.

I en ekte terminal åpner Git editoren med meldingen «Merge branch 'feature'». Lagre for å godta den, bruk --no-edit for å hoppe over editoren, eller -m for å skrive din egen.

Navigere mellom foreldre: ^ og ~

Uttrykk Betyr
HEAD^ eller HEAD^1 første forelder: der du stod
HEAD^2 andre forelder: det du merget inn
HEAD~2 to steg tilbake, alltid via første forelder (samme som HEAD^^)
HEAD^2~1 ett steg tilbake langs branchen som ble merget inn

^ velger hvilken forelder. ~ går bakover i generasjoner.

Lese en merget historikk

*   c3d971a Merge branch 'feature'
|\
| * 2379bbb Style login form
| * 27a6985 Add login form
* | cecf599 Add docs
|/
* df6c7db Initial snapshot

Venstre kolonne er første-forelder-linjen: historien til main. Nyttige visninger:

  • git log --oneline --first-parent viser bare stegene main selv tok, her c3d971a, cecf599 og df6c7db.
  • git log --merges viser bare merge-commits.
  • git diff HEAD^1 HEAD viser hva merge brakte inn i main.

Test før du committer mergen

En merge uten konflikter betyr bare at tekstene passet sammen. Eksempel: på main gir noen funksjonen greet() nytt navn, welcome(). På feature legger en annen til et nytt kall til greet() i en annen fil. Git merger begge uten problemer, og programmet virker ikke lenger. Kjør testene etter hver merge, før du pusher.

Vil du se på eller teste resultatet før det lagres, bruk git merge --no-commit --no-ff feature. Git merger, stopper før commit og skriver Automatic merge went well; stopped before committing as requested. Test, og fullfør med git commit, eller trekk deg ut med git merge --abort. Uten --no-ff blir en mulig fast-forward likevel gjennomført.

Strategivalg, brukt med omhu

  • -X ours eller -X theirs: når et område er i konflikt, ta vår eller deres versjon automatisk. Endringer uten konflikt fra begge sider merges fortsatt. Praktisk, men du hopper over avgjørelsen en konflikt ber om.
  • -s ours er noe helt annet: den registrerer en merge, men beholder ditt snapshot uendret og kaster alle endringene deres. Sjelden det du vil.
  • Å merge flere brancher samtidig, git merge a b c, gir en «octopus»-merge med mer enn to foreldre. Den nekter hvis noe er i konflikt.

Angre en merge

  • Mens en merge fortsatt pågår, på grunn av konflikt eller --no-commit: git merge --abort.
  • En fullført merge du ikke har delt: ORIG_HEAD holder der branchen var; leksjonen om reset viser hvordan du går tilbake.
  • En merge som allerede er pushet: git revert -m 1 <merge> lager en ny commit som angrer den. -m 1 sier at første forelders side er den som skal beholdes. Se leksjonen om revert.

Offisiell dokumentasjon