Git
Chapters ▾ 2nd Edition

7.6 Orodja Git - Prepisovanje zgodovine

Prepisovanje zgodovine

Pri delu z Gitom se pogosto zgodi, da želite spremeniti zgodovino lokalnih potrditev. Ena od prednosti Gita je, da vam omogoča sprejemanje odločitev v zadnjem trenutku. Lahko se odločite, katere datoteke gredo v katero potrditev, tik preden jih potrdite s področjem priprave, lahko se odločite, da še ne želite delati na določenem delu z git stash in prepišete lahko potrditve, ki so se že zgodile, tako da izgledajo, kot da so se zgodile drugače. To lahko vključuje spreminjanje vrstnega reda potrditev, spreminjanje sporočil ali datotek v potrditvi, stiskanje skupaj ali razcepitev potrditev, ali pa popolno odstranjevanje potrditev — vse to, preden svoje delo delite z drugimi.

V tem razdelku boste videli, kako izvesti te naloge, tako da lahko svojo zgodovino potrditev uredite po želji, preden jo delite z drugimi.

Opomba
Ne potisnite svojega dela dokler niste z njim zadovoljni

Ena izmed ključnih pravil Gita je, da imate zaradi tega, ker je veliko dela lokalno v vašem klonu, veliko svobode pri spreminjanju vaše zgodovine lokalno. Vendar pa je to povsem druga zgodba, ko enkrat potisnete svoje delo, in takrat morate objavljeno delo obravnavati kot končno, razen če imate dober razlog za njegovo spreminjanje. Skratka, izogibati se morate potiskanju svojega dela, dokler niste z njim zadovoljni in ga pripravljeni deliti z ostalim svetom.

Spreminjanje zadnje potrditve

Spreminjanje vaše zadnje potrditve je verjetno najpogostejše prepisovanje zgodovine, ki ga boste naredili. Pogosto boste želeli narediti dve osnovni stvari na svoji zadnji potrditvi: preprosto spremeniti sporočilo potrditve, ali pa spremeniti njeno dejansko vsebino z dodajanjem, odstranjevanjem in spreminjanjem datotek.

Če želite preprosto spremeniti sporočilo zadnje potrditve, je to precej enostavno:

$ git commit --amend

Zgoraj navedeni ukaz naloži sporočilo prejšnje potrditve v sejo urejevalnika, kjer lahko spremenite sporočilo, shranite spremembe in izstopite. Ko shranite in zaprete urejevalnik, se ustvari nova potrditev s posodobljenim sporočilom in postane vaša nova zadnja potrditev.

Če pa želite spremeniti dejansko vsebino zadnje potrditve, postopek deluje na enak način — najprej naredite spremembe, ki ste jih pozabili, shranite te spremembe in nato z uporabo ukaza git commit --amend zamenjate zadnjo potrditev z vašo novo in izboljšano potrditvijo.

Pri tej tehniki morate biti previdni, saj spremeni vrednost SHA-1 potrditve. Gre za zelo majhno ponovno baziranje — ne spreminjajte zadnje potrditve, če ste jo že potisnili.

Namig
Spremenjena potrditev lahko (ali pa ne) potrebuje spremenjeno sporočilo potrditve

Ko popravljate potrditev, imate možnost spremeniti tako sporočilo potrditve kot tudi njeno vsebino. Če bistveno spremenite vsebino potrditve, bi morali skoraj zagotovo posodobiti njeno sporočilo, da odraža to spremenjeno vsebino.

Po drugi strani če so vaše spremembe ustrezno nepomembne (popravljanje neumne tipkarske napake ali dodajanje datoteke, ki ste jo pozabili dati v področje priprave), tako da je prejšnje sporočilo potrditve v redu, lahko preprosto opravite spremembe, jih postavite v področje priprave in se v celoti izognete nepotrebnemu urejevalniku z:

$ git commit --amend --no-edit

Spreminjanje več sporočil potrditev

Če želite spremeniti potrditev, ki je dlje nazaj v zgodovini, morate uporabiti bolj zapletena orodja. Git nima orodja za spreminjanje zgodovine, vendar lahko uporabite orodje ponovnega baziranja, da na novo nanesete serijo potrditev na HEAD, na kateri so bile prvotno zasnovane, namesto da jih premikate na drugo glavo. Z interaktivnim orodjem ponovnega baziranja se lahko nato ustavite po vsaki potrditvi, ki jo želite spremeniti, in spremenite sporočilo, dodate datoteke, ali karkoli drugega želite narediti tam. Ponovno baziranje lahko poganjate interaktivno tako, da dodate ukazu git rebase možnost -i. Morali boste navesti, kako daleč nazaj želite preoblikovati potrditve, tako da ukazu poveste, na katero potrditev naj se ponovno baziranje izvaja.

Na primer, če želite spremeniti zadnja tri sporočila potrditev, ali katero koli sporočilo v tej skupini, argumentu git rebase -i podate nadrejeno zadnje potrditve, ki jo želite urediti, kar je HEAD~2^ ali HEAD~3. Lahko si zapomnite ~3, ker poskušate urediti zadnje tri potrditve, vendar imejte v mislih, da dejansko določate štiri potrditve nazaj, nadrejeno zadnje potrditve, ki jo želite urediti:

$ git rebase -i HEAD~3

Ponovno poudarjamo, da gre za ukaz ponovnega baziranja — vsaka potrditev v območju HEAD~3..HEAD s spremenjenim sporočilom in vsi njeni potomci bodo preoblikovani. Ne vključite nobene potrditve, ki ste jo že potisnili na osrednji strežnik — to bo druge razvijalce zmedlo z zagotavljanjem alternativne različice iste spremembe.

Zagon tega ukaza vam da seznam potrditev v urejevalniku besedil, ki je videti nekako takole:

pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file

# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Pomembno je opozoriti, da so te potrditve navedene v obratnem vrstnem redu, kot jih običajno vidite z uporabo ukaza log. Če zaženete log, boste videli nekaj takega:

$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d Add cat-file
310154e Update README formatting and add blame
f7f3f6d Change my name a bit

Opazite obratni vrstni red. Interaktivno ponovno baziranje vam da skript, ki ga bo izvedel. Začelo se bo pri potrditvi, ki jo določite v ukazni vrstici (HEAD~3) in bo od vrha navzdol ponovilo spremembe, uvedene v vsaki od teh potrditev. Najstarejšo potrditev na seznamu postavi na vrh, namesto najnovejše, ker jo bo najprej ponovilo.

Skript morate urediti, da se ustavi pri potrditvi, ki jo želite urediti. Da bi to naredili, spremenite besedo pick v besedo edit za vsako potrditev, za katero želite, da se skript ustavi po njej. Na primer, če želite spremeniti samo sporočilo tretje potrditve, spremenite datoteko, da je videti tako:

edit f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file

Ko shranite in zapustite urejevalnik, vas Git previje nazaj na zadnjo potrditev na seznamu in vas postavi v ukazno vrstico z naslednjim sporočilom:

$ git rebase -i HEAD~3
Stopped at f7f3f6d... Change my name a bit
You can amend the commit now, with

       git commit --amend

Once you're satisfied with your changes, run

       git rebase --continue

Ta navodila vam povedo točno, kar morate narediti. Vpišite:

$ git commit --amend

Spremenite sporočilo potrditve in zapustite urejevalnik. Nato poženite:

$ git rebase --continue

Ta ukaz bo samodejno uporabil drugi dve potrditvi in potem ste končali. Če na več vrsticah spremenite pick v edit, lahko za vsako potrditev, ki jo spremenite v edit, ponovite te korake. Vsakič se bo Git ustavil, vam dovolil popraviti potrditev in nadaljeval, ko končate.

Preurejanje potrditev

Interaktivna ponovna baziranja lahko uporabite tudi za preureditev ali popolno odstranitev potrditev. Če želite odstraniti potrditev »Add cat-file« in spremeniti vrstni red, v katerem sta uvedeni drugi dve potrditvi, lahko spremenite skript ponovnega baziranja iz tega:

pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file

na to:

pick 310154e Update README formatting and add blame
pick f7f3f6d Change my name a bit

Ko shranite in zaprete urejevalnik, Git previje vašo vejo nazaj na nadrejeno teh potrditev, uporabi 310154e in nato f7f3f6d ter se nato ustavi. Učinkovito ste spremenili vrstni red teh potrditev in popolnoma odstranili potrditev »Add cat-file«.

Stiskanje potrditev skupaj

Z interaktivnim orodjem ponovnega baziranja je možno združiti tudi serijo potrditev v eno samo potrditev. Skript v sporočilu ponovnega baziranja vključuje uporabna navodila:

#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Če namesto pick ali edit navedete squash, Git uporabi tako spremembo kot tudi spremembo neposredno pred njo in vas prosi, da združite sporočila potrditev. Če želite na primer ustvariti eno samo potrditev iz teh treh potrditev, lahko skript oblikujete na ta način:

pick f7f3f6d Change my name a bit
squash 310154e Update README formatting and add blame
squash a5f4a0d Add cat-file

Ko shranite in zapustite urejevalnik, bo Git uporabil vse tri spremembe in vas nato vrnil nazaj v urejevalnik, da združite tri sporočila potrditev.

# This is a combination of 3 commits.
# The first commit's message is:
Change my name a bit

# This is the 2nd commit message:

Update README formatting and add blame

# This is the 3rd commit message:

Add cat-file

Ko to shranite, boste imeli eno potrditev, ki vsebuje spremembe vseh treh prejšnjih potrditev.

Razdelitev potrditve

Razdelitev (angl. splitting) potrditve razveljavi potrditev in jo delno doda v področje priprave ter nato potrdi tolikokrat s kolikor potrditev želite končati. Na primer, če želite razdeliti srednjo potrditev iz svojih treh potrditev. Namesto »Update README formatting and add blame« jo želite razdeliti na dve potrditvi: »Update README formatting« za prvo in »Add blame« za drugo. To lahko storite v skriptu rebase -i, tako da spremenite navodila na potrditvi, ki jo želite razdeliti, na edit:

pick f7f3f6d Change my name a bit
edit 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file

Nato, ko vas skript spusti v ukazno vrstico, ponastavite to potrditev, vzamete spremembe, ki so bile ponastavljene, in iz njih ustvarite več potrditev. Ko shranite in zaprete urejevalnik, se Git premakne nazaj na nadrejeno prve potrditve na vašem seznamu, uporabi prvo potrditev (f7f3f6d), uporabi drugo (310154e) in vas spusti v konzolo. Tam lahko naredite mešano ponastavitev te potrditve z git reset HEAD^, ki učinkovito razveljavi to potrditev in pusti spremenjene datoteke izven področja priprave. Sedaj lahko označite in potrdite datoteke, dokler ne dobite več potrditev, in ko končate, zaženete git rebase --continue:

$ git reset HEAD^
$ git add README
$ git commit -m 'Update README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'Add blame'
$ git rebase --continue

Git uporabi zadnjo potrditev v skriptu (a5f4a0d) in vaša zgodovina je videti takole:

$ git log -4 --pretty=format:"%h %s"
1c002dd Add cat-file
9b29157 Add blame
35cfb2b Update README formatting
f7f3f6d Change my name a bit

To spremeni SHA-1 treh najnovejših potrditev na vašem seznamu, zato poskrbite, da nobena spremenjena potrditev, ki se pojavi na tem seznamu, že ni bila potisnjena v skupno rabo. Opazite, da je zadnja potrditev na seznamu (f7f3f6d) nespremenjena. Kljub temu, da je ta potrditev prikazana v skriptu, ker je bila označena kot pick in je bila uporabljena pred vsemi spremembami ponovnega baziranja, Git pusti potrditev nespremenjeno.

Brisanje potrditve

Če želite odstraniti potrditev, jo lahko izbrišete s pomočjo skripta rebase -i. Na seznamu potrditev pred tisto, ki jo želite izbrisati, vnesite besedo drop (ali pa samo izbrišite to vrstico iz skripta za ponovno baziranje):

pick 461cb2a This commit is OK
drop 5aecc10 This commit is broken

Zaradi načina, kako Git gradi objekte potrditev, bo brisanje ali spreminjanje potrditve povzročilo ponovno pisanje vseh potrditev, ki sledijo. Bolj, ko se pomikate nazaj v zgodovino svojega repozitorija, več potrditev bo treba ustvariti. Če imate v zaporedju veliko potrditev, ki so odvisne od tiste, ki ste jo pravkar izbrisali, lahko to povzroči veliko konfliktov med združevanjem.

Če se nekje na sredini ponovnega baziranja odločite, da to ni dobra ideja, se lahko vedno ustavite. Vpišite git rebase --abort in vaš repozitorij se bo povrnil v stanje, v katerem je bil pred začetkom ponovnega baziranja.

Če končate ponovno baziranje in se odločite, da to ni to, kar želite, lahko uporabite git reflog, da obnovite prejšnjo različico svoje veje. Glejte razdelek Obnovitev podatkov za več informacij o ukazu reflog.

Opomba

Drew DeVault je pripravil praktični vodnik z vajami, ki vam pomaga se naučiti uporabljati git rebase. Najdete ga na: https://git-rebase.io/

Jedrska možnost: filter-branch

Obstaja še ena možnost spreminjanja zgodovine, ki jo lahko uporabite, če morate na neki skriptni način spremeniti večje število potrditev — na primer globalno spremeniti svoj e-poštni naslov ali odstraniti datoteko iz vsake potrditve. Ukaz je filter-branch in lahko ponovno napiše ogromne dele vaše zgodovine, zato ga verjetno ne bi smeli uporabljati, razen če vaš projekt še ni javen in drugi ljudje še niso temeljili na delu, ki ste ga ravno nameravali spremeniti. Vendar pa je lahko zelo uporaben. Naučili se boste nekaj običajnih uporab, da boste dobili predstavo o nekaterih stvareh, ki jih lahko naredi.

Pozor

git filter-branch ima veliko pasti in ni več priporočen način za spreminjanje zgodovine. Namesto tega razmislite o uporabi skripta Python git-filter-repo, ki v večini aplikacij, kjer bi se običajno obrnili na filter-branch, opravi boljšo nalogo. Dokumentacija in izvorna koda sta na voljo na spletnem naslovu https://github.com/newren/git-filter-repo.

Odstranjevanje datoteke iz vsake potrditve

To se zgodi pogostokrat. Nekdo naključno doda veliko binarno datoteko z nepremišljenim git add . in želite jo odstraniti povsod. Morda ste nenamerno dali v repozitorij datoteko, ki vsebuje geslo, in želite svoj projekt narediti odprtokodni. filter-branch je orodje, ki ga verjetno želite uporabiti, da očistite celotno zgodovino. Če želite iz celotne zgodovine odstraniti datoteko z imenom passwords.txt, lahko uporabite možnost --tree-filter v filter-branch:

$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten

Možnost --tree-filter po vsakem izvleku projekta izvede navedeni ukaz in nato ponovno potrdi rezultate. V tem primeru odstranite datoteko passwords.txt iz vsakega posnetka, ne glede na to, ali obstaja ali ne. Če želite odstraniti vse naključno dodane datoteke varnostnih kopij urejevalnika, lahko zaženete nekaj takega kot git filter-branch --tree-filter 'rm -f *~' HEAD.

Lahko spremljate Git, ko ponovno piše drevesa in potrditve, nato pa premaknete kazalnik veje na konec. Na splošno je dobra ideja to storiti v testni veji in nato trdo ponastaviti vašo vejo master, ko ugotovite, da je rezultat tisto, kar resnično želite. Če želite zagnati filter-branch na vseh vejah, lahko ukazu podate --all.

Izdelava podimenika v novi koren

Recimo, da ste uvozili projekt iz drugega sistema za nadzor nad izvorno kodo in imate poddirektorije, ki nimajo smisla (trunk, tags itd.). Če želite narediti poddirektorij trunk za nov koren projekta za vsako potrditev, vam lahko filter-branch pomaga tudi pri tem:

$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten

Zdaj je vaš novi projekt osnovna mapa, ki je bila vsakič v podmapi trunk. Git bo samodejno odstranil tudi potrditve, ki niso vplivale na podmapo.

Globalno spreminjanje e-poštnih naslovov

Še en pogost primer je, da ste pozabili zagnati git config, da bi nastavili vaše ime in e-poštni naslov, preden ste začeli delati, ali pa morda želite objaviti projekt na delovnem mestu in spremeniti vse delovne e-poštne naslove v svoj osebni naslov. V vsakem primeru lahko s filter-branch spremenite e-poštne naslove v več potrditvah naenkrat. Paziti morate, da spremenite samo tiste e-poštne naslove, ki so vaši, zato uporabite --commit-filter:

$ git filter-branch --commit-filter '
        if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
        then
                GIT_AUTHOR_NAME="Scott Chacon";
                GIT_AUTHOR_EMAIL="schacon@example.com";
                git commit-tree "$@";
        else
                git commit-tree "$@";
        fi' HEAD

To gre skozi in prepiše vsako potrditev z vašim novim naslovom. Ker potrditve vsebujejo vrednosti SHA-1 njihovih nadrejenih, ta ukaz spremeni vrednosti SHA-1 vsake potrditve v vaši zgodovini, ne samo tistih, ki imajo prilegajoči se naslov e-pošte.

scroll-to-top