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

Rebase: spill arbeidet av på en ny base

Modul 4: Angre og skrive om · 13 min

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

Etter leksjonen kan du

  • forklare hvorfor git rebase gir nye commits med nye ID-er, og hva som skjer med originalene forstå
  • bruke git rebase main til å legge en branch oppå main, og så fast-forwarde main anvende
  • fortsette eller avbryte en rebase som stopper på en konflikt anvende
  • velge mellom merge og rebase ut fra den gylne regelen: rebase bare privat historikk, merge delt historikk vurdere

Forstå: Samme arbeid, ny forelder

Du jobber på feature og har laget to commits, B og C. I mellomtiden har noen lagt D til på main. Linjene har delt seg. Du vil at arbeidet ditt skal ligge oppå den nyeste main.

Ett svar kjenner du allerede: en merge. Git lager en merge-commit M med to foreldre. Ingenting skrives om, og grafen viser ærlig at arbeidet skjedde parallelt.

Tilbake til forgreningen: rebase går en annen vei. Git starter på den nyeste commiten på main og spiller av commitene dine én etter én, som om du hadde skrevet dem der. Endringen fra B blir en ny commit oppå D. Passer ikke en endring, stopper Git.

Når alle commitene er spilt av, flytter Git feature-lappen til den siste nye commiten. Historikken er nå én rett linje oppå D.

Se på de stiplede commitene. Det er originalene, B og C, og de er ikke endret. ID-en til en commit regnes ut fra snapshotet, forelderen, meldingen og forfatteren. Ny forelder gir ny ID. Rebase flytter altså aldri commits. Den lager kopier og lar de gamle ligge igjen.

For å få arbeidet inn i main kjører du git switch main og så git merge feature. Siden feature nå ligger rett foran, blir det en fast-forward. Ingen merge-commit, bare en rett linje.

Så den gylne regelen: Rebase aldri commits som andre allerede har. Tenk deg at du hadde pushet den gamle feature. Da lever originalene videre på origin, og det finnes to versjoner av det samme arbeidet. Rebase bare privat historikk, og bruk merge når den er delt.

Se demo: Legg en feature-branch oppå main

Vi står på feature. Grafen viser at linjene har delt seg: to commits på feature og én ny commit på main. Merk deg ID-ene til de to feature-commitene.

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

Nå kommer nøkkelkommandoen. git rebase main spiller av commitene fra feature oppå main. Følg med på ID-ene.

$ git rebase main

Grafen er nå en rett linje, med nye ID-er på feature-commitene. Git lagret den gamle tuppen i ORIG_HEAD, og loggen derfra viser originalene, helt uendret. Angrer du rebasen, tar git reset --hard ORIG_HEAD deg tilbake.

$ git log --oneline -2 ORIG_HEAD

Nå skal arbeidet inn i main. Først bytter vi til main.

$ git switch main

Å merge feature er nå en fast-forward. Git flytter bare main-lappen fram. Se etter ordet Fast-forward i utskriften.

$ git merge feature

Resultatet er én rett linje: først snapshotet, så dokumentasjonen, så de to login-commitene.

$ git log --oneline --graph

Oppgaver

  1. Vis alle brancher som en graf, med én linje per commit.
  2. Spill av feature oppå den nyeste main.
  3. Oppdater main med arbeidet fra feature, uten merge-commit.

Se demo: Når en avspilling gir konflikt

Begge branchene har endret den samme linjen i README.md. Vi rebaser greeting oppå main. Git spiller av commiten, treffer konflikten og stopper midt i.

$ git rebase main

La oss se på filen. Markørene er de samme som i en merge-konflikt. Men hvilken side heter HEAD denne gangen?

$ cat README.md

Vi løser den som enhver konflikt. Vi skriver versjonen vi vil ha, og markerer den som løst med git add.

$ echo 'Welcome to Git class, friend!' > README.md && git add README.md

Nå lager git rebase --continue den nye commiten og spiller av resten. Git åpner commit-meldingen slik at du kan justere den. Vi beholder den og lagrer.

$ git rebase --continue

Oppgaver

  1. Begynn å spille av greeting oppå main.
  2. Løs konflikten ved å kombinere begge linjene, og marker filen som løst.
  3. Fullfør rebasen.

Øv: Overfør: en butikk med tre brancher

Du står på main. Branchene docs, faq og pricing startet før main gikk videre. faq er pushet, og Ada jobber videre på den. De to andre finnes bare hos deg. Midt i en rebase setter git rebase --abort alt tilbake. Ingen demo denne gangen.

Oppgaver

  1. Få arbeidet fra docs inn i main som én rett linje, uten merge-commit.
  2. Få FAQ-en fra faq inn i main.
  3. Rebase pricing oppå main. Det blir konflikt. Du bestemmer deg for ikke å løse den nå. Gå tilbake, slik at pricing er nøyaktig som før.

Sjekk deg selv: Hent det fram igjen

Du pushet feature i går, og en medstudent har bygget videre på den. I dag har main gått videre. Hva er det trygge valget?

  • Rebase feature oppå main og force-push
  • Merge, slik at de delte commitene blir som de er
  • Rebase, det går fint så lenge du bruker git push --force-with-lease

Under en rebase-konflikt kjører du git checkout --ours README.md. Hvilken versjon får du i filen?

  • Versjonen fra din egen commit, den som spilles av
  • Versjonen fra den nye basen, med commitene som allerede er spilt av
  • Versjonen fra den felles forfaren

Midt i en rebase med konflikter skjønner du at du startet fra feil branch. Hva setter alt tilbake slik det var før?

  • git rebase --abort
  • git rebase --skip
  • git reset --hard

Repetisjon fra «Brancher og HEAD»: helt til slutt i en rebase flytter Git branchen feature. Hva er det Git egentlig flytter?

  • En kopi av alle filene på branchen
  • En peker til én commit, tuppen av branchen
  • En lagret liste over alle commitene på branchen

Vanlige misforståelser

  • «Rebase flytter commitene mine.» Den lager nye commits med nye ID-er. Originalene ligger uendret igjen til de etter hvert ryddes bort.
  • «Rebase er alltid best fordi historikken blir penere.» Skriver du om commits andre allerede har, får teamet doble versjoner og mye ekstraarbeid.
  • «I en rebase-konflikt er HEAD min egen endring.» Under en rebase står HEAD på den nye basen, pluss commitene som allerede er spilt av. Din commit er den som kommer inn.

Les mer

Hva git rebase main faktisk gjør

Git finner commitene som ligger på branchen din, men ikke på main (området main..HEAD). Den lagrer den gamle tuppen i ORIG_HEAD, sjekker ut den nyeste main på en løsrevet HEAD og legger commitene på én etter én, omtrent som en serie cherry-picks. Først når den siste er ferdig, flyttes branch-lappen din til den nye tuppen. Hver ny commit får en ny forelder og dermed en ny ID. Originalene ligger urørt igjen i repositoryet.

$ git rebase main
Successfully rebased and updated refs/heads/feature.
$ git log --oneline --graph --all
* d9c6470 Add login test
* d355c49 Add login form
* 6007fdb Add docs
* a5ea1be Initial snapshot
$ git reflog -5
d9c6470 HEAD@{0}: rebase (finish): returning to refs/heads/feature
d9c6470 HEAD@{1}: rebase (pick): Add login test
d355c49 HEAD@{2}: rebase (pick): Add login form
6007fdb HEAD@{3}: rebase (start): checkout main
0c10b5b HEAD@{4}: checkout: moving from main to feature
$ git rev-parse --short ORIG_HEAD
0c10b5b

Tre detaljer er verdt å kjenne til:

  • En commit der endringen allerede finnes på main (for eksempel fordi noen har cherry-picket den), hoppes over. Git skriver warning: skipped previously applied commit.
  • Merge-commits på branchen din droppes som standard, og historikken flates ut til én linje.
  • Branchen flyttes først helt til slutt. Derfor kan git rebase --abort alltid gå tilbake til nøyaktig samme utgangspunkt.

Når en commit ikke passer

En rebase stopper på den første commiten som gir konflikt. Løs den som en vanlig merge-konflikt, og velg så hvordan du går videre:

Kommando Virkning
git rebase --continue Committer den løste endringen og spiller av resten
git rebase --skip Utelater denne commiten fra den nye historikken (endringen forsvinner fra branchen)
git rebase --abort Stopper og setter branchen tilbake nøyaktig der den var

Under en rebase er sidene byttet om sammenlignet med en merge inn i din egen branch: HEAD og «ours» er den nye basen pluss commitene som allerede er spilt av, «theirs» er din commit som spilles av. git checkout --ours file gir deg altså ikke din egen versjon.

Sammenlign gammel og ny med git range-diff

Etter en rebase har du to versjoner av den samme serien. git range-diff parer dem og sammenligner endringene, ikke ID-ene:

$ git range-diff main ORIG_HEAD feature
1:  86eeba1 = 1:  d355c49 Add login form
2:  0c10b5b = 2:  d9c6470 Add login test

= betyr at endringen er identisk, ! at den er forskjellig (en indre diff følger), < at commiten bare finnes i den gamle serien, og > bare i den nye. Vil du ha et varig bokmerke i stedet for ORIG_HEAD, lager du en branch først: git branch feature-before før du rebaser, og deretter git range-diff main feature-before feature.

Flytt en del av en branch: --onto

git rebase --onto <newbase> <upstream> <branch> tar commitene i upstream..branch og spiller dem av på newbase. Klassikeren: du startet discount fra pricing ved en feil, og bare rabatt-commiten skal inn i main.

$ git log --oneline --graph --all
* 95dbcf3 Add student discount
* d3a55ee Show prices
| * 82af405 Add license
|/
* a5ea1be Initial snapshot
$ git rebase --onto main pricing discount
Successfully rebased and updated refs/heads/discount.
$ git log --oneline --graph --all
* 535e91d Add student discount
* 82af405 Add license
| * d3a55ee Show prices
|/
* a5ea1be Initial snapshot

Show prices blir liggende på pricing. Bare commiten etter den ble flyttet.

Rebase av noe du allerede har pushet

En rebaset branch inneholder ikke lenger den gamle tuppen, så en vanlig git push blir avvist som non-fast-forward. Å overskrive branchen på serveren er greit bare når branchen er din alene, typisk din egen pull request-branch som ingen andre har hentet eller bygget videre på, eller når teamet uttrykkelig har avtalt det. Aldri på main eller en annen delt branch.

Selv da bør du bruke --force-with-lease i stedet for --force. Den overskriver bare hvis branchen på serveren fortsatt står der origin/feature sier. Her pushet Ada en commit til branchen mens du rebaset:

$ git push --force-with-lease
 ! [rejected]        feature -> feature (stale info)

Leasen har et svakt punkt: Oppdaterer git fetch (eller en editor i bakgrunnen) origin/feature først, går leasen gjennom, og Adas commit blir overskrevet likevel. --force-if-includes legger til en sjekk av at du faktisk har tatt inn det du hentet:

$ git fetch
$ git push --force-with-lease --force-if-includes
 ! [rejected]        feature -> feature (remote ref updated since checkout)

Ingen av valgene gjør det trygt å skrive om historikk som andre bruker. De stopper bare noen uhell.

git pull --rebase

git pull --rebase henter og rebaser så de lokale commitene dine som ikke finnes på upstream-branchen, oppå den oppdaterte versjonen. Det er trygt så lenge de commitene er private, og derfor er det den rebasen de fleste bruker daglig. git config pull.rebase true gjør det til standard.

Merge eller rebase?

Situasjon Valg
Privat branch, du vil ha en rett linje før du deler rebase
Branch som andre har hentet eller bygget på merge
Oppdater dine egne upushede commits git pull --rebase
Historikken skal vise at arbeidet skjedde parallelt merge

Offisiell dokumentasjon