Git
Chapters ▾ 2nd Edition

7.6 Гит алати - Поновно исписивање историје

Поновно исписивање историје

У многим ситуацијама, док радите са програмом Гит, можете пожелети да из неког разлога ревидирате своју историју комитова. Једна од одличних ствари увези програма Гит је што вам омогућава да доносите одлуке у последњем могућем тренутку. Стејџом можете одлучити који фајлови иду у које комитове непосредно пре него што направите комит, можете одлучити да још увек нисте желели да радите на нечему командом git stash и можете поново исписати историју комитова који су се већ догодили тако да изгледају као да су се догађали на неки другачији начин. Ово може значити измену редоследа комитова, измену порука или мењање фајлова у комиту, гњечење или растављање комитова, или потпуно уклањање комитова – све то пре него што рад поделите са другима.

У овом одељку ћете сазнати како се завршавају ови веома корисни послови тако да вашу историју комитова можете учинити да изгледа тачно онако како желите пре него што је поделите са другима.

Белешка
Не гурајте свој рад док нисте задовољни њиме

Пошто се заиста доста посла ради локално унутар вашег клона, једно од кључних правила програма Гит је да имате доста слободе код локалног преписивања своје историје. Међутим, једном када гурнете свој рад, прича је потпуно другачија и имајте на уму да би рад требало да гурнете као довршен, осим ако немате заиста добар разлог да га измените. Укратко, требало би да избегавате гурање свог рада све док не будете задовољни њиме и спремни да га делите са остатком света.

Измена последњег комита

Измена вашег последњег комита је поновно исписивање историје које ћете вероватно најчешће радити. Често ћете имати потребу да урадите две основне ствари вашем последњем комиту: измена комит поруке, или измена снимка који сте управо сачували тако што ћете додавати, мењати и уклањати фајлове.

Ако само желите да измените вашу последњу комит поруку, ствар је проста:

$ git commit --amend

Ова комада учитава претходну комит поруку у сесију едитора у којем поруку можете да измените, сачувате, па напустите едитор. Када измене сачувате и напустите едитор, он уписује нови комит који садржи ажурирану комит поруку и поставља га као ваш нови последњи комит.

С друге стране, ако желите да измените садржај вашег последњег комита, процес тече у основи на исти начин — најпре направите измене које мислите да сте заборавили, ставите их на стејџ, па git commit --amend замењује тај последњи комит вашим новим, унапређеним комитом.

Морате бити опрезни са овом техником јер мењање значи и измену SHA-1 вредности комита. То је као веома мало ребазирање — не мењајте свој последњи комит ако сте га већ гурнули.

Савет
Измењени комит може (али и не мора) захтевати измену комит поруке

Када мењате комит, имате могућност да промените и комит поруку и садржај самог комита. Ако је измена комита значајна, скоро обавезно би требало и да ажурирате комит поруку тако да обухвати и тај измењени садржај.

С друге стране, ако су ваше измене погодно тривијалне (исправка смешне грешке у куцању или додавање фајла који сте заборавили да ставите на стејџ) тако да ранија комит порука у потпуности важи, можете једноставно да направите измене, поставите их на стејџ и избегнете потпуно непотребну сесију едитора са:

$ git commit --amend --no-edit

Измена више комит порука одједном

Ако желите да измените комит који се налази даље у вашој историји, морате да употребите сложеније алате. Програм Гит нема алат за измену историје, али можете употребити rebase да ребазирате низ комитова на HEAD на којем су оригинално били базирани уместо да их премештате на неки други. Интерактивним rebase алатом затим можете да се зауставите након сваком комита који желите да измените и промените му поруку, додате фајлове или урадите штагод да желите. Ребазирање се извршава интерактивно додавањем опције -i команди git rebase. Морате навести колико далеко уназад желите да поново испишете комитове тако што команди задате на који комит да започне ребазирање.

На пример, ако желите измените последње три комит поруке, или било коју од порука из те групе, као аргумент команди git rebase -i наводите родитеља последњег комита који желите да уредите, што је HEAD~2^ или HEAD~3. ~3 је вероватно лакше за памћење, јер покушавате да уредите последња три комита, али имајте на уму да заправо наводите четврти претходни комит, тј. родитеља последњег комита четврти желите да уредите:

$ git rebase -i HEAD~3

Још једном, упамтите да је ово команда ребазирања — поново ће се исписати сваки комит из опсега HEAD~3..HEAD, без обзира да ли измените поруку или не. Немојте навести било који комит који је већ гурнут на сервер — збунићете остале програмере тиме што достављате алтернативне верзије једне те исте измене.

Извршавањем ове команде ћете у свом текст едитору добити листу комитова која личи на следећу:

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

Важно је приметити да се ови комитови наводе у супротном редоследу у односу на уобичајен приказ командом log. Ако извршите log, видећете нешто слично овоме:

$ 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

Уочите обрнути редослед. Интерактивно ребазирање вам приказује скрипту коју ће извршити. Почеће од комита који наведете у командној линији (HEAD~3) и поново ће проћи кроз измене које су уведене сваким од ових комитова, од врха ка дну. Уместо најновијег, на врху приказује најстарији комит јер је то први кроз који ће поново да прође.

Потребно је да уредите скрипту тако да стане на комиту који желите да уредите. Да бисте то урадили, измените реч ’pick’ у ’edit’ за сваки од комитова на којем желите да скрипта стане. На пример, ако желите да измените само трећу комит поруку, фајл треба да измените тако да изгледа овако:

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

Када сачувате и напустите едитор, програм Гит вас враћа назад на последњи комит у тој листи и приказује вам командну линију са следећом поруком:

$ 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

Ова упутства вам прецизно кажу шта да урадите. Откуцајте:

$ git commit --amend

Измените комит поруку и напустите едитор. Затим извршите:

$ git rebase --continue

Ова команда ће аутоматски применити наредна два комита и завршили сте. Ако на више линија измените pick у edit, ове кораке можете да поновите за сваки комит који сте променили у edit. Програм Гит ће се зауставити сваки пут, омогућити вам да измените комит и наставиће даље када то урадите.

Промена редоследа комитова

Интерактивна ребазирања можете употребити и за промену редоследа или потпуно уклањање комитова. Ако желите да уклоните „Add cat-file” комит и измените редослед у којем се уводе остала два комита, скрипту ребазирања можете променити из:

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

у:

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

Када то сачувате и напистите едитор, програм Гит премотава уназад вашу грану на родитеља ових комитова, примењује 310154e па затим f7f3f6d и онда се зауставља. Ефективно сте изменили редослед ових комитова и потпуно уклонили „Added cat-file” комит.

Сажимање комитова

Алатом за интерактивно ребазирање је могуће и да узмете низ комитова па да их згужвате у један. Скрипта у поруку ребазирања поставља корисна упутства:

#
# 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

Ако уместо „pick” или „edit” наведете „squash”, програм Гит примењује и ту и измену непосредно испред ње и даје вам да спојите комит поруке у једну. Дакле, ако желите да из ова три комита направите само један, преправите скрипту тако да изгледа овако:

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

Када скрипту сачувате и напустите едитор, програм Гит примењује све три измене, па вас затим враћа у едитор да спојите три комит поруке у једну:

# 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

Када то сачувате, имате један комит који је увео све измене из претходна три комита.

Подела комита

Подела комита поништава комит, па онда делимично ставља на стејџ и комитује онолико пута са колико комитова желите да завршите. На пример, рецимо да желите поделити средњи комит од ваша три. Уместо „Update README formatting and add blame” желите да га поделите на два комита: „Update README formatting” као први и „Add blame” као други. То можете урадити у rebase -i скрипти тако што ћете изменити упутство на комиту који желите да поделите у „edit”:

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

Затим када вас скрипта постави у командну линију, ресетујете тај комит, узмете ресетоване измене, па од њих направите више комитова. Када сачувате и напустите едитор, програм Гит премотава уназад на родитеља првог комита у вашој листи, примењује први комит (f7f3f6d), примењује други комит (310154e), па вас враћа у конзолу. Ту можете одрадити мешани ресет тог комита са git reset HEAD^, чиме се тај комит ефективно поништава и оставља измењене фајлове ван стејџа. Сада можете да стејџујете и комитујете фајлове све док не будете имали неколико комитова, па када завршите, извршите 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

Програм Гит примењује последњи комит у скрипти (a5f4a0d) и ваша историја изгледа овако:

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

Да поновимо, ово мења све SHA-1 вредности свих комитова у вашој листи, па будите сигурни да на листи није ниједан комит који сте већ раније гурнули на дељени репозиторијум. Приметите да је последњи комит на листи (f7f3f6d) остао неизмењен. Мада се овај комит појављује у скрипти, он је био обележен као „pick” и примењен је пре било каквих измена које уводи ребазирање, па га програм Гит не мења.

Брисање комита

Ако желите да се решите комита, можете га обрисати rebase -i скриптом. У листи комитова ставите реч „drop” испред комита који желите да обришете (или једноставно обришите ту линију из скрипте ребазирања):

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

Услед начина на који програм Гит изграђује комит објекте, брисање или измена комита ће изазвати поновно исписивање свих комитова који следе након њега. Што даље идете уназад кроз историју свог репозиторијума, више комитова ће морати поново да се креира. Ово може да изазове много конфликта при спајању ако касније у низу имате доста комитова који зависе од онога који сте управо обрисали.

Ако дођете на пола пута кроз овакво ребазирање и одлучите да то и није тако добра идеја, увек можете да се зауставите. Откуцајте git rebase --abort, и ваш репозиторијум се враћа на стање у којем је био пре него што сте покренули ребазирање.

Ако завршите ребазирање и схватите да то није оно што желите, можете употребите git reflog да вратите назад старију верзију своје гране. За више информација о команди reflog, погледајте Опоравак података.

Белешка

Дру Деволт је направила згодан практични водич са вежбама који помаже да научите како се користи git rebase. Можете га пронаћи на адреси: https://git-rebase.io/

Нуклеарна опција: filter-branch

Постоји још једна опција за поновно исписивање историје коју можете користити када је потребно да поново испишете велики број комитова на неки начин који може да се одради скриптом — на пример, глобална измена ваше имејл адресе или уклањање фајла из сваког комита. Команда filter-branch може поново да испише огромне откосе ваше историје, тако да вероватно не треба да је користите осим у случају када ваш пројекат још увек није јавни и неко други није свој рад базирао на комитовима које ћете управо да препишете. Ипак, може бити веома корисна. Научићете неколико уобичајених употреба тако да стекнете идеју о неколико од многих ствари које ова команда може да уради.

Уклањање фајла из сваког комита

Ово се јавља прилично често. Неко случајно комитује огроман бинарни фајл са непромишљеним git add ., па желите да га уклоните свуда. Можда сте случају комитовали фајл који је садржао лозинку, а желите да отворите кôд свог пројекта. filter-branch је алат који највероватније желите употребити да прочешљате своју комплетну историју. Ако из целе своје историје желите да уклоните фајл под именом passwords.txt, употребите --tree-filter опцију команде filter-branch:

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

Опција --tree-filter извршава наведену команду након сваког одјављивања пројекта, па затим поново комитује резултате. У овом случају, из сваког снимка уклањате фајл под именом passwords.txt, без обзира на то да ли он постоји у снимку или не. Ако желите да уклоните све случајно комитоване резервне фајлове едитора, можете да извршите нешто као што је git filter-branch --tree-filter 'rm -f *~' HEAD.

Видећете како програм Гит поново исписује стабла и комитове па на крају помера показивач гране. У општем случају је добра идеја да ово урадите у грани за тестирање, па да затим одрадите „hard-reset” ваше master гране када одредите да је исход заиста оно што желите. Ако filter-branch желите да извршите над свим вашим гранама, проследите јој --all.

Постављање поддиректоријума као новог корена

Рецимо да сте одрадили увоз из неког другог система за контролу изворног кода и имате поддиректоријуме који немају смисла (trunk, tags, и тако даље). Ако желите да trunk поддиректоријум постане нови корен пројекта за сваки комит, filter-branch вам такође долази у помоћ:

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

Сада је нови корен пројекта оно што се сваки пут налазило у trunk поддиректоријуму. Програм Гит ће такође да уклони све комитове који нису утицали на поддиректоријум.

Глобална измена имејл адресе

Још један уобичајени случај је да сте пре почетка рада заборавили да извршите git config и поставите своје име и имејл адресу, или да можда желите отворити кôд свог пројекта и промените своју пословну имејл адресу у приватну. У сваком случају, имејл адресе такође можете одједном променити у више комитова употребом команде filter-branch. Морате пазити да измените само своје имејл адресе, тако да користите --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

Ово пролази кроз све комитове и поново исписује сваки комит тако да има вашу нову имејл адресу. Пошто комитови садрже SHA-1 вредности својих родитеља, ова команда мења SHA-1 вредности свих комитова у вашој историји, а не само оних у којима се јавља ваша адреса коју замењујете.