Git
Chapters ▾ 2nd Edition

7.6 Git инструменти - Манипулация на историята

Манипулация на историята

Често в работата си с Git може да поискате да ревизирате историята на локалните къмити. Една от чудесните страни на Git е, че ви позволява да вземате решения в последния възможен момент. Вие решавате кои файлове в кои къмити да отидат непосредствено преди къмитване с индексната област, вие може да решите, че в момента отлагате работата по даде проблем с git stash и също така, можете да презапишете предишни къмити така че да изглеждат сякаш са се случили по различен начин. Това може да включва смяна на реда на къмитите, смяна на съобщенията или модифициране на файлове в къмит, комбиниране на няколко в един или разделяне на къмит на части или пък изтриване на къмит — всичко това преди да споделите работата си с колегите.

В тази част от книгата ще видим как се осъществяват такива задачи, така че да накарате историята на работата ви да изглежда по начина, по който вие искате преди да я споделите с другите.

Note
Не публикувайте работата си преди да сте я довършили докрай

Едно от кардиналните правила на Git е това, че понеже вършите повечето работа локално във вашето копие на хранилището, вие разполагате с почти неограничена свобода да преправяте локалната си история. Обаче, след като веднъж сте публикували работата си, нещата значително се променят и е редно да гледате на нея като на финална версия, освен ако нямате наистина добра причина да промените това. Накратко, би следвало да избягвате да публикувате работата си до момента в който решите, че тя е достатъчно добра за да бъде споделена с другите.

Промяна на последния къмит

Промяната на най-последния къмит вероятно е най-често срещания вид манипулация на историята. Две неща ще искате да правите най-често: просто да редактирате къмит съобщението или да промените съдържанието на къмита добавяйки, изтривайки или променяйки файлове.

Ако става дума само за съобщението на последния къмит, това е лесно:

$ git commit --amend

Командата зарежда съобщението на къмита в текстовия редактор, където можете да го редактирате, да запишете промените и да излезете. Когато затворите редактора, той записва нов къмит с редактираното съобщение и го прави последен къмит.

Ако, от друга страна, искате да промените нещо по действителното съдържание на къмита, процесът в общи линии работи по същия начин — първо направете желаните промени, индексирайте ги и след това git commit --amend командата ще замени последния записан къмит с вашите нови, коригирани данни.

Трябва да внимавате с тази техника, понеже тя променя SHA-1 хеша на къмита. Това е като много малко пребазиране — не променяйте последния къмит, ако вече сте го публикували!

Tip
Редактиран къмит може да се нуждае, но може и да не се нуждае от amended къмит съобщение

Когато редактирате къмит, имате възможност да смените както съобщението, така и съдържанието на данните в къмита. Ако променяте данните по същество, почти винаги е добре да обновите и съобщението, така че да отразява корекциите.

От друга страна, ако промените са тривиални (например поправка на правописна грешка или добавяне на файл, който сте забравили да индексирате) и оригиналното съобщение си е съвсем на място, можете просто да направите промените и да прескочите стъпката с редактора изцяло:

$ git commit --amend --no-edit

Смяна на повече къмит съобщения

За да промените къмит, който е назад в историята, ще ви трябват по-сложни инструменти. Git не разполага с инструмент за модифициране на историята, но можете да използвате rebase за да пребазирате серия от къмити върху HEAD-а, на който са били първоначално базирани, вместо да ги премествате към друг. С интерактивния инструмент за пребазиране след това можете да спирате на всеки къмит, който искате и да редактирате съобщението му, да добавяте файлове и т.н. Можете да стартирате rebase в интерактивен режим с флага -i на командата git rebase. Трябва да посочите колко назад искате да презаписвате къмити указвайки на командата върху кой къмит да пребазира.

Например, ако искате да промените последните три къмит съобщения или кое да е съобщение в тази група, вие подавате като аргумент на git rebase -i родителя на последния къмит, който искате да редактирате, което е HEAD~2^ или HEAD~3. Може да е по-лесно да използвате ~3, понеже опитвате да редактирате последните три къмита, но имайте предвид, че всъщност посочвате четири къмита назад, родителя на последния къмит, който искате да промените:

$ git rebase -i HEAD~3

Подчертаваме отново, че това е пребазираща команда — всеки къмит в обхвата HEAD~3..HEAD ще бъде презаписан независимо дали променяте съобщението или не. Не включвайте никакъв къмит, който вече сте изпратили на централния сървър — правейки това ще смутите другите разработчици, защото осигурявате алтернативна версия на една и съща промяна.

Изпълнявайки командата, получавате списък къмити в текстовия си редакто, подобно на това:

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added 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 added cat-file
310154e updated README formatting and added blame
f7f3f6d changed my name a bit

Забележете обърнатия ред. Интерактивното пребазиране ви дава скрипта, който ще изпълни. То ще започне от къмита, който указвате на командния ред (HEAD~3) и ще извърши промените въведени във всеки от тези къмити отгоре надолу. Най-старият се показва най-отгоре, защото той е първият, който ще бъде приложен.

Трябва да редактирате скрипта така, че да спре на къмита, който желаете да редактирате. За да направите това, променете думата ‘pick’ на ‘edit’ за всеки от къмитите, след които искате скрипта да спре. Например, за да промените само третото къмит съобщение, променяте файла да изглежда така:

edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

Когато запишете и излезете от редактора, Git ви превърта обратно до последния къмит в този списък и ви връща в командния ред със следното съобщение:

$ git rebase -i HEAD~3
Stopped at f7f3f6d... changed 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 на повече редове, можете да повторите тези стъпки за всеки съответен къмит. Всеки път Git ще спира, ще ви позволи да промените къмита и ще продължи, когато сте готови.

Пренареждане на къмити

Можете да използвате интерактивното пребазиране за да размествате или изцяло да премахвате къмити. Ако искате да премахнете къмита “added cat-file” и да смените реда, в който останалите два се прилагат, може да смените rebase скрипта от това

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

към това:

pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit

Когато запишете и затворите редактора, Git превърта клона назад до родителя на тези къмити, прилага 310154e следван от f7f3f6d и спира. Сега редът на двата останали къмита е сменен, а “added cat-file” е изцяло премахнат.

Обединяване на къмити

Възможно е също така да вземете няколко къмита и да ги обедините в един единичен. Процесът е известен като Squashing и също може да се направи с инструмента за интерактивно пребазиране. Скриптът ви дава напътствия в rebase съобщението:

#
# 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”, Git прилага текущата промяната и промяната непосредствено преди нея и ви позволява да слеете къмит съобщенията заедно. Така че, ако искате да направите единичен къмит от горните три, може да редактирате скрипта си така:

pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file

Когато затворите редактора, Git прилага всичките три промени и пуска редактора още веднъж, за сливане на трите съобщения:

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

# This is the 2nd commit message:

updated README formatting and added blame

# This is the 3rd commit message:

added cat-file

Записвайки това, вие получавате единичен къмит, който съдържа промените от всичките три предишни.

Разделяне на къмит

Разделянето ще отмени даден къмит и след това частично ще индексира и къмитне толкова пъти, колкото укажете. Например, решавате да разделите втория от трите къмита по-горе. Вместо “updated README formatting and added blame”, искате да го разделите в два къмита със съобщения “updated README formatting” за първия и “added blame” за втория. Можете да го постигнете с rebase -i скрипта променяйки инструкцията на втория къмит на “edit”:

pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

След това, когато скриптът ви върне в командния ред, вие reset-вате къмита, вземате промените, които са били отменени и създавате няколко къмита от тях. Когато запишете и затворите редактора, Git превърта назад до родителя на първия къмит в списъка, прилага първия къмит (f7f3f6d), прилага втория (310154e) и ви връща в конзолата. Там можете да направите mixed reset на този къмит с командата git reset HEAD^, което на практика отменя къмита и оставя модифицираните файлове извън индекса. Сега можете да индексирате и къмитвате файлове докато получите колкото желаете къмита и след това да изпълните git rebase --continue за да завършите процеса:

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

Git ще приложи и последния къмит от скрипта (a5f4a0d) и сега историята ви изглежда така:

$ git log -4 --pretty=format:"%h %s"
1c002dd added cat-file
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit

Както вече забелязахте, този процес променя SHA-1 стойностите за всички къмити в списъка, така че се убедете, че никой от тях не е бил публикуван в споделено хранилище.

Мощната алтернатива: 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 от всеки един snapshot без значение дали присъства или не. Ако искате да премахнете всички случайно къмитнати backup файлове от вашия редактор, може да изпълните нещо като git filter-branch --tree-filter 'rm -f *~' HEAD.

Ще видите как Git пренаписва дърветата и къмитите и след това премества указателя на клона в края. Добра идея е да направите това пробно в тестов клон и ако резултатите ви устройват, да го приложите и в master клона. Можете да пуснете filter-branch и върху всички клонове с опцията --all към командата.

Превръщане на под-директория в Root директория

Да допуснем, че сте импортирали проект от друга source control система и имате под-директории, които са излишни (trunk, tags и т.н.). Ако искате да направите директорията trunk корен за проекта ви за всеки къмит, също може да използвате filter-branch:

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

Сега trunk е новата основна директория. Git автоматично ще премахне къмитите, които не касаят тази директория.

Смяна на имейл адрес глобално

Друга възможна ситуация е да сте забравили да пуснете 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 данни на своите родители, това също означава, че командата ще смени хешовете на всички къмити в историята, включително и на тези, които не съдържат вашия имейл.