Git
Chapters ▾ 2nd Edition

3.6 Клонове в Git - Пребазиране на клонове

Пребазиране на клонове

В Git съществуват два основни начина за интегриране на промени от един клон код в друг: сливане (merge) и пребазиране (rebase). В тази секция ще научите какво е пребазирането, как да го правите, защо е мощен инструмент и кои са случаите, в които не бихте искали да го използвате.

Просто пребазиране

Ако се върнете назад до по-ранния пример в Сливане, можете да си припомните, че работата ви се разклонява и вие правихте къмити в два различни клона.

Проста история на разклоняването.
Figure 35. Проста история на разклоняването

Най-лесният начин за интегрирането на клоновете, както вече разгледахме, беше командата merge. Тя изпълнява трипосочно сливане между най-новите snapshot-и от клоновете (C3 и C4) и най-близкия им общ предшественик (C2) създавайки нов snapshot (и къмит).

Сливане за интегриране на разклонена работна история.
Figure 36. Сливане за интегриране на разклонена работна история

Обаче, съществува и друг начин да направите това: можете да вземете patch на промените, които са въведени с C4 и да ги приложите върху C3. В Git това се нарича пребазиране, rebasing. С командата rebase, вие вземате всички промени къмитнати в един клон и ги пускате в друг такъв.

В този пример, ще извлечете клона experiment и ще го пребазирате върху master по следния начин:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Това става, като се намери най-близкия общ предшестващ къмит на двата клона (този върху, който сте в момента и този, който ще пребазирате), вземат се разликите въведени от всеки къмит на клона, върху който сте, разликите се записват във временни файлове, текущият клон се ресетва към същия къмит, в който е клона, който ще се пребазира, и накрая се прилага всяка промяна поред.

Пребазиране на промяната от `C4` в `C3`.
Figure 37. Пребазиране на промяната от C4 в C3

В този момент, можете да се върнете към master клона и да направите fast-forward сливане.

$ git checkout master
$ git merge experiment
Fast-forwarding на клона master.
Figure 38. Fast-forwarding на клона master

Сега snapshot-ът, към който сочи C4' е точно същият като този, към който сочеше C5 в the merge example. Няма разлика в крайния резултат от интеграцията, но пребазирането прави историята по-чиста. Ако изследвате лога на един пребазиран клон код, той ще прилича на линейна история, цялата работа изглежда като случила се на серии, дори когато в действителност е била паралелна.

Често ще правите това за да се уверите, че вашите къмити се прилагат безпроблемно върху отдалечен клон — вероятно проект, към който се опитвате да допринесете, но който не поддържате като автор. В този случай, вие вършите своята дейност в собствен клон и след това пребазирате работата си върху origin/master, когато сте готови да изпратите своите поправки в основния проект. По този начин, поддръжащият проекта разработчик не трябва да върши никаква работа по интеграцията на промените ви — просто ще направи fast-forward.

Отбележете отново, че snapshot-ът, към който сочи финалния получил се къмит (бил той последния от пребазираните къмити за rebase или пък новосъздадения в резултат от merge) е един и същи и в двата случая — разликата е само в историята. Пребазирането прилага промените от една линия на разработка в друга по реда, в който те са били направени, докато сливането взема двата края на два клона и ги слива в едно.

Други интересни пребазирания

Едно пребазиране може да бъде приложено и върху друг освен върху целевия му клон. Например, вижте фигурата История с topic клон произлизащ от друг topic клон. Създали сте един topic клон (server) за да добавите някаква сървърна функционалност към проекта си и сте направили къмит. След това, ползвайки този клон за отправна точка, сте създали нов такъв (client) за да направите някакви промени по клиентската част и сте къмитнали няколко пъти. Накрая, върнали сте се обратно към сървърния клон и сте направили още няколко къмита.

История с topic клон произлизащ от друг topic клон.
Figure 39. История с topic клон произлизащ от друг topic клон

Да кажем, че решавате да слеете клиентските промени в master клона за публикуване, но желаете да си запазите server-side промените за допълнително тестване. Можете да вземете промените в клиентската част, които не присъстват на сървъра (C8 and C9) и да ги приложите върху master клона чрез параметъра --onto на командата git rebase:

$ git rebase --onto master server client

Това звучи така, “Вземи клона client, разбери кои са промените в него след момента, в който този клон се е разклонил от server клона и ги приложи отново в него сякаш той е бил базиран първоначално на master клона.” Изглежда доста объркано, но резултатът е впечатляващ.

Пребазиране на topic клон от друг topic branch.
Figure 40. Пребазиране на topic клон от друг topic branch

Сега можете да направите fast-forward на master клона (виж Fast-forwarding на master клона за включване на промените от клона client):

$ git checkout master
$ git merge client
Fast-forwarding на master клона за включване на промените от клона client.
Figure 41. Fast-forwarding на master клона за включване на промените от клона client

Нека кажем, че решите да интегрирате и промените от server клона. Можете да пребазирате server клона върху master без да се налага да превключвате към него изпълнявайки git rebase <basebranch> <topicbranch> –- което вместо вас ще превключи към topicbranch (в този случай server) и ще го приложи върху basebranch (в случая master):

$ git rebase master server

Това прилага вашите server промени върху master клона както е показано в Пребазиране на server клона в master клона.

Пребазиране на server клона в master клона.
Figure 42. Пребазиране на server клона в master клона

След което, можете да превъртите основния клон (master):

$ git checkout master
$ git merge server

Сега можете да изтриете клоновете client и server, защото те са интегрирани и не ви трябват повече, което ще направи историята на целия процес да изглежда така Историята на финалния къмит:

$ git branch -d client
$ git branch -d server
Историята на финалния къмит.
Figure 43. Историята на финалния къмит

Опасности при пребазиране

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

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

Послушате ли този съвет, ще сте ОК. Не го ли направите, ще ви намразят.

Когато пребазирате неща, вие зарязвате съществуващи къмити и създавате нови, които са подобни, но не съвсем същите. Ако изпратите къмитите някъде и други ваши колеги ги издърпат и използват като изходна точка за тяхната работа, а след това вие ги презапишете с git rebase и ги изпратите отново, то колегите ви ще трябва да слеят отново тяхната работа и нещата бързо ще придобият грозен вид, когато пък вие се опитате да изтеглите тяхната работа обратно за ваше ползване.

Нека видим един пример, как пребазирането на публична работа може да доведе до проблеми. Да допуснем, че клонирате от централен сървър и вършите някаква работа след това. Историята на къмитите ви изглежда по подобен начин:

Клонирайте хранилище и направете промени по него.
Figure 44. Клонирайте хранилище и направете промени по него

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

Издърпвате още къмити и ги сливате във вашата работа.
Figure 45. Издърпвате още къмити и ги сливате във вашата работа

След това човекът, който е изпратил слетите си промени, решава да се върне назад и да пребазира работата си изпълнявайки git push --force за да презапише историята на сървъра. Вие от своя страна, издърпвате отново от сървъра получавайки новите къмити.

Някой изпраща към сървъра пребазирани къмити, изоставяйки тези, върху които вие сте базирали работата си.
Figure 46. Някой изпраща към сървъра пребазирани къмити, изоставяйки тези, върху които вие сте базирали работата си

Сега и двамата сте в бъркотия. Ако направите git pull, ще се създаде merge commit, който включва и двете линни история и хранилището ви ще изглежда така:

Вие сливате в същата работа отново
Figure 47. Вие сливате в същата работа отново, в нов merge commit

Ако пуснете git log в история като тази, ще видите два къмита с един и същи автор, дата и съобщение, което ще е доста смущаващо. По-нататък, ако изпратите тази история обратно към сървъра, ще запишете допълнително в него всички тези пребазирани къмити, което би объркало допълнително хората в бъдеще. Безопасно е да предположите, че другият ви колега не желае C4 и C6 да присъстват в историята - в края на краищата това е причината той да направи пребазирането.

Пребазиране по време на пребазиране

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

Оказва се, че в допълнение към SHA-1 чексумата на къмита, Git също така калкулира и чексума, която е базирана на поправката (patch-ът) въведена от него. Това се нарича “patch-id”.

Ако издърпате работа, която е била пренаписана и я пребазирате върху новите къмити на вашия колега, Git често може успешно да определи кое е изцяло ваше и да ги приложи обратно върху новия клон.

Например, в предишния сценарий, ако вместо да правим сливане докато сме в момента показан на фигурата Някой изпраща към сървъра пребазирани къмити, изоставяйки тези, върху които вие сте базирали работата си, изпълним git rebase teamone/master, Git ще направи следното:

  • Ще определи коя работа е уникална за нашия клон (C2, C3, C4, C6, C7)

  • Ще определи кои от къмитите не са (C2, C3, C4)

  • Ще определи кои къмити не са били презаписани в целевия клон (само C2 и C3, защото C4 е същият patch като C4')

  • Ще приложи тези къмити върху teamone/master

Така вместо резултатите, които виждаме във Вие сливате в същата работа отново, в нов merge commit, ще получим нещо приличащо повече на Пребазиране върху форсирано изпратена пребазирана работа.

Пребазиране върху форсирано изпратена пребазирана работа.
Figure 48. Пребазиране върху форсирано изпратена пребазирана работа.

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

Можете също така да опростите това изпълнявайки git pull --rebase, вместо нормален git pull. Или можете да го направите ръчно с git fetch последвана от git rebase teamone/master в този случай.

Ако използвате git pull и искате да включите --rebase аргумента по подразбиране, можете да зададете pull.rebase конфигурационната стойност с git config --global pull.rebase true.

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

Ако вие или ваш колега установи към даден момент, че това е необходимо - уверете се, че поне всеки знае и пуска git pull --rebase за да се намалят неудобствата, когато проблемите дойдат.

Пребазиране или сливане

Сега, когато видяхте в действие пребазирането и сливането, може би се питате кое от двете е по-добро. Преди да отговорим, нека се върнем малко назад и да поговорим за това какво означава историята.

Една възможна дефиниция е, че историята на къмитите във вашето хранилище е запис на това какво в действителност се е случвало. Това е исторически документ, ценен сам по себе си, който не бива да се модифицира. Погледнато по този начин, излиза че промяната на историята на къмитите е почти богохулство - вие лъжете за това какво се е случвало всъщност. А какво ще е ако имахте разхвърляни серии от merge къмити? Ами - просто така са протекли нещата и хранилището следва да пази този факт за идните поколения.

Обратната гледна точка приема историята като хронология на това как проектът ви е бил създаден. Например, не бихте публикували първата чернова на една книга и ръководството за това как да поддържате вашия софтуер заслужава внимателно редактиране. Това е гледната точка на поддръжниците на инструменти като rebase и filter-branch, които искат да разкажат историята на проекта не буквално каквато е била, а каквато би била най-полезна за тези, които ще го поемат в бъдеще.

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

Ако се колебаете, но искате да се възползвате от предимствата и на двата подхода, просто помнете това правило - спокойно използвайте пребазиране за локални промени, които сте направили, но не са публични. Така ще си подредите историята в чист вид. Но никога не пребазирайте нещо, което вече сте изпратили за споделено ползване.