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

Reflog: redd arbeid som ser tapt ut

Modul 4: Angre og skrive om · 13 min

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

Etter leksjonen kan du

  • forklare hva reflogen registrerer, og hvorfor commits som ser tapt ut, som regel fortsatt finnes forstå
  • redde commits etter git reset --hard og etter git branch -D ved å lage en branch fra en linje i reflogen anvende
  • angre en rebase med ORIG_HEAD eller branchens egen reflog anvende
  • forklare hvorfor reflogen ikke er en backup forstå

Forstå: En borte peker er ikke en borte commit

Her er main med tre commits. Så kjører du git reset --hard HEAD~2 ved en feil. Du ville bare kaste en filendring, ikke to commits.

main hopper tilbake til A. B og C er nå stiplet: ingen branch når dem, og git log viser dem ikke lenger. Men de er ikke slettet. Commitene ligger fortsatt i repositoryet. Det var bare lappen som flyttet seg.

Hvordan finner du dem igjen? Git fører en dagbok som heter reflog. Hver gang HEAD eller en branch flytter seg, skriver Git en linje om hvor den gikk. HEAD@{0} er der HEAD er nå, og HEAD@{1} der den var ett steg før. Her er det C.

Før du gjør noe annet, gir du commiten et navn. git branch rescue HEAD@{1} lager en ny branch der. B og C kan nås igjen, og du kan se deg rundt i ro og fred. Etterpå kan du flytte main tilbake.

Å slette en branch er samme historie. git branch -D rescue fjerner bare lappen, og B og C blir stiplet igjen. Git skriver ut ID-en branchen hadde. Branchens egen reflog forsvinner, men reflogen til HEAD husker fortsatt commitene.

En rebase etterlater også commits som ingen peker på: originalene. Her droppet planen test-commiten ved en feil, så du vil ha branchen tilbake der den startet. Hvilket navn peker dit?

En rebase flytter HEAD mange ganger, så HEAD@{1} er bare det siste steget inne i rebasen. Bruk heller ORIG_HEAD, den gamle tuppen Git lagret før rebasen. ORIG_HEAD husker bare den siste reset, merge eller rebase, så etter det er branchens egen reflog, feature@{1}, veien tilbake.

Reflogen er et sikkerhetsnett, ikke en backup. Den finnes bare i din egen .git-mappe og blir aldri pushet. Linjer for commits som ingen branch når, går som standard ut 30 dager etter at de ble skrevet, og da kan Git slette commitene. Og den kjenner bare commits, aldri ucommittede endringer.

Se demo: Redning etter reset --hard

Her er uhellet. Vi resetter main to commits tilbake, med hard. Begge commitene forsvinner fra branchen, og filene deres forsvinner fra mappen.

$ git reset --hard HEAD~2

Loggen viser bare det første snapshotet. Det ser ut som arbeidet er borte. I grafen er de to commitene stiplet.

$ git log --oneline

Nå åpner vi dagboken. Se etter de to commitene som forsvant.

$ git reflog

Linjen rett under toppen, HEAD@{1}, er commiten Add budget. Vi gir den et navn med en gang, med en rescue-branch. De stiplede commitene blir hele igjen.

$ git branch rescue HEAD@{1}

main ligger rett bak rescue på samme linje, så en merge blir en fast-forward. Begge filene er tilbake.

$ git merge rescue

Alt er tilbake. Når du har sjekket at innholdet stemmer, kan du slette rescue.

$ git log --oneline

Oppgaver

  1. Finn ut hvor HEAD var før resetten.
  2. Lag en branch rescue på commiten HEAD pekte på rett før resetten.
  3. Flytt main tilbake til de reddede commitene.

Se demo: Angre en rebase som gikk galt

Vi vil rydde feature mens vi rebaser den oppå main. I planen sletter vi en linje ved en feil, den med testen. En linje som mangler, betyr drop, og Git advarer oss ikke.

$ git rebase -i main

Test-commiten er borte fra branchen. Vi vil ha feature tilbake nøyaktig slik den var før rebasen.

$ git log --oneline

La oss lese de fem siste linjene i reflogen. Legg merke til hvor mange linjer rebasen skrev.

$ git reflog -5

ORIG_HEAD peker på tuppen fra før rebasen. En hard reset flytter feature tilbake dit. Det er trygt her, fordi vi ikke har noe som ikke er committet.

$ git reset --hard ORIG_HEAD

Forgreningen er tilbake, med begge login-commitene. Nå kan vi rebase på nytt, og denne gangen beholde alle linjene i planen.

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

Oppgaver

  1. Se på de fem siste flyttene til HEAD.
  2. Sett feature tilbake nøyaktig slik den var før rebasen.

Øv: Overfør: en kafé med tre uhell

Et kafé-repository der tre ting gikk galt tidligere i dag. Du står på main. Les reflogen før du gjør noe.

Oppgaver

  1. Noen kjørte en hard reset ett steg tilbake, to ganger på rad. Flytt main tilbake til commiten Add price list.
  2. Tidligere i dag ble branchen experiment slettet med git branch -D. Gjenopprett den, med commiten Try dark theme.
  3. Tidligere i dag ble menu rebaset oppå main med feil område, og commiten Add menu forsvant fra branchen. Sett menu tilbake slik den var før rebasen.

Sjekk deg selv: Hent det fram igjen

git branch -D experiment skrev Deleted branch experiment (was f4edb90). Du angrer med en gang. Hva kjører du?

  • git reflog show experiment
  • git branch experiment f4edb90
  • Ingenting, -D slettet commitene

PC-en din dør. Kan en medstudent redde commitene du ikke har pushet, med git reflog i sin klone?

  • Ja, reflogen deles via origin
  • Nei, reflogen finnes bare i din egen .git-mappe
  • Ja, hvis medstudenten kjører git fetch først

Du kjørte git reset --hard mens du hadde endringer som aldri var staget eller committet. Hva kan reflogen gi tilbake?

  • Endringene, fra linjen før resetten
  • Ingenting av dem, fordi Git aldri lagret dem
  • Alt, hvis du er rask og handler innen 30 dager

Repetisjon fra «Brancher og HEAD»: du laget en commit med detached HEAD og byttet så til main. Hvordan tar du best vare på commiten?

  • Ingenting, Git tar vare på alle commits for alltid
  • Gi den en branch, for eksempel med git branch keep <id>
  • Bytt tilbake til den og lag en ny commit oppå

Vanlige misforståelser

  • «Etter git reset --hard er commitene slettet.» De finnes fortsatt. Ingen branch peker på dem, men reflogen gjør det.
  • «Reflogen er en backup.» Den er lokal, blir aldri pushet, går ut på dato og har aldri sett endringer du ikke committet.
  • «HEAD@{1} betyr alltid før min forrige kommando.» Det er HEADs forrige posisjon. En rebase flytter HEAD mange ganger.

Les mer

Hva reflogen registrerer

Hver gang en ref flytter seg, legger Git til en linje i loggen til den refen i .git/logs/: den gamle posisjonen, den nye og hvorfor. Det finnes én reflog for HEAD og én for hver lokale branch. git reflog viser HEADs, git reflog show main viser branchens, og git log -g viser de samme linjene i loggformat.

$ git reset --hard HEAD~2
HEAD is now at a5ea1be Initial snapshot
$ git reflog
a5ea1be HEAD@{0}: reset: moving to HEAD~2
f4edb90 HEAD@{1}: commit: Add budget
ae89efa HEAD@{2}: commit: Write project plan
a5ea1be HEAD@{3}: commit (initial): Initial snapshot
$ git reflog show main
a5ea1be main@{0}: reset: moving to HEAD~2
f4edb90 main@{1}: commit: Add budget
ae89efa main@{2}: commit: Write project plan
a5ea1be main@{3}: commit (initial): Initial snapshot

Navnene kan brukes overalt der Git forventer en commit:

Navn Betyr
HEAD@{2} der HEAD var for to flytt siden
main@{1} der main var før sitt siste flytt
main@{yesterday}, HEAD@{2.hours.ago} der refen var på det tidspunktet
@{-1} branchen du stod på før forrige bytte

HEAD flytter seg mye oftere enn en branch: ved hvert bytte og ved hvert steg i en rebase. Branchens egen reflog registrerer bare endringer i den branchen, så feature@{1} er ofte den enkleste veien tilbake etter en rebase.

En rolig redningsrutine

  1. Stopp. Ikke prøv flere kommandoer for å «fikse» det.
  2. Les git status, git log --oneline --graph --all og git reflog.
  3. Gi det du finner et navn før alt annet: git branch rescue <id>. En branch holder commitene nåbare uansett hva som skjer etterpå.
  4. Undersøk rescue, og flytt så branchen din tilbake med en merge, en reset eller en cherry-pick.
  5. Slett rescue først når du har sjekket resultatet.

Slettede brancher

git branch -D fjerner lappen og branchens egen reflog, men skriver ut den gamle ID-en, og reflogen til HEAD lister fortsatt hver commit HEAD har vært innom:

$ git branch -D rescue
Deleted branch rescue (was f4edb90).
$ git branch rescue f4edb90

ORIG_HEAD

Kommandoer som flytter HEAD kraftig, lagrer den gamle posisjonen i ORIG_HEAD først: git reset, git merge, git rebase og git am, og dermed også git pull. Vanlige git commit, git commit --amend, git switch, git cherry-pick og git revert rører den ikke. Det finnes bare én ORIG_HEAD, og neste slike kommando overskriver den, så bruk den med en gang. Merk at git reset --hard ORIG_HEAD selv er en reset, så etterpå peker ORIG_HEAD på der du nettopp var.

Når reflogen ikke har den: git fsck

Noen commits havner aldri i en reflog. En droppet stash er det klassiske eksempelet: git stash drop fjerner reflog-linjen til refs/stash, men stash-commiten blir liggende i objektdatabasen. git fsck lister slike dangling objekter, som ingenting peker på:

$ git stash
Saved working directory and index state WIP on main: a5ea1be Initial snapshot
$ git stash drop
Dropped refs/stash@{0} (11acfee0f6acd5aeedfa14a2f0f123603c56be6f)
$ git fsck --lost-found
dangling commit 11acfee0f6acd5aeedfa14a2f0f123603c56be6f
$ git log --oneline -1 11acfee
11acfee WIP on main: a5ea1be Initial snapshot
$ git stash apply 11acfee

En dangling commit er tuppen av en tapt kjede: ingenting peker på den, heller ikke en annen commit. Vanlig git fsck regner reflogen som en referanse, men --lost-found ser bort fra den, akkurat som --no-reflogs. Derfor lister den også commits som bare reflogen husker. Se på hver av dem med git log -1 før du handler.

Filer du har staget, men aldri committet, ligger også i objektdatabasen, som blobs. Etter en uforsiktig git reset --hard kan du fortsatt finne dem, men uten filnavn:

$ echo "Draft menu: soup, bread" > menu.txt && git add menu.txt
$ git reset --hard
HEAD is now at a5ea1be Initial snapshot
$ git fsck --lost-found
dangling blob e064b3ad67676a5cbae5998a99c9b253d261c4b2
dangling commit 11acfee0f6acd5aeedfa14a2f0f123603c56be6f
$ git show e064b3ad
Draft menu: soup, bread

Endringer du aldri kjørte git add på, har Git aldri lagret. Ingen kommando kan gi dem tilbake.

Utløp og søppeltømming

Gamle reflog-linjer fjernes av git gc, som Git også kjører automatisk av og til. Standardverdiene:

Innstilling Standard Gjelder
gc.reflogExpire 90 dager reflog-linjer for commits som fortsatt er på branchen
gc.reflogExpireUnreachable 30 dager linjer for commits branchen ikke lenger når
gc.pruneExpire 2 uker løse objekter som ingenting viser til lenger

Alderen regnes fra da linjen ble skrevet, ikke fra da commiten ble utilgjengelig. Du kan forlenge dem, for eksempel git config gc.reflogExpireUnreachable 90.days. Når en linje har gått ut og ingen ref når commiten, kan neste søppeltømming slette den for godt. git reflog expire --expire=now --all etterfulgt av git gc --prune=now sletter alt som ikke kan nås, med en gang. Det er siste steg når en hemmelighet allerede er skrevet ut av historikken, og nettopp derfor skal du aldri kjøre det i panikk: etterpå finnes ingen vei tilbake.

Hvorfor reflogen ikke er en backup

Reflogen finnes bare i din egen .git-mappe. Den blir ikke pushet, ikke hentet og ikke kopiert av git clone, og den går ut på dato. Den beskytter deg mot dine egne feil på din egen maskin. Mot en mistet PC trenger du en kopi på en annen maskin, for eksempel ved å pushe commitene dine til et remote.

Offisiell dokumentasjon