Git
Chapters ▾ 2nd Edition

7.1 Git и други системи - Git като клиент

Светът не е перфектен. Обикновено не можете безпрепятствено да прехвърлите всеки проект, който имате към Git. Понякога имате проект, който ползва друга VCS, и бихте искали да е Git. Ще отделим първата част на тази глава за да научим начините за използване на Git като клиент, когато проектът, по който работите е на друга система.

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

Git като клиент

Git е толкова полезен, че много разработчици са намерили начин да го използват на работните си станции макар останалите им колеги от екипа да ползват съвсем различни VCS. Съществуват много налични адаптери за това, известни като “bridges”. Тук ще посочим някои от тях, с които е вероятно да си имате работа.

Git и Subversion

Голяма част от проектите с отворен код, а също така и голям брой корпоративни проекти използват Subversion за управление на сорс кода. Subversion съществува повече от десетилетие и през повечето време от своя живот беше де факто първи избор за контрол на open source проекти. Тя много прилича на CVS, която пък преди това беше най-популярната система за контрол.

Една от най-съществените възможности на Git е двупосочния bridge към Subversion известен като git svn. Този инструмент позволява да използвате Git като валиден клиент на Subversion сървър, така че можете да ползвате всички локални възможности на Git и след това да публикувате работатата си на Subversion сървъра така сякаш ползвате Subversion и локално. Това означава, че можете да имате локални клонове и сливания, да използвате индексната област, да правите rebasing/cherry-picking и т.н. докато колегите ви продължават да си работят по стария начин. Това е добър начин за вмъкване на Git в корпоративни среди както и за привличане на повече разработчици (а защо не и на цели корпоративни звена) да преминат на Git. Subversion бриджът е ключ към DVCS света.

git svn

Основната команда в Git за всички Subversion функционалности е git svn. Тя приема множество подкоманди и ще покажем най-използваните от тях симулирайки няколко прости работни процеса.

Важно е да помним, че използвайки git svn ние комуникираме със Subversion, система функционираща по много различен начин от Git. Въпреки че можете да правите локални клонове и сливания, в общи линии е най-добре да поддържате историята си възможно най-праволинейна използвайки пребазирания и избягвайки да правите неща като едновременна комуникация с отдалечени Git хранилища.

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

Настройка

За целите на демонстрацията, се нуждаем от стандартно SVN хранилище с права за писане в него. Ако искате да копирате следващите примери, ще трябва да направите writeable копие на тестово Subversion хранилище. Най-лесно ще направите това с инструмента svnsync, който се разпространява със Subversion.

Първо, създаваме ново локално Subversion хранилище:

$ mkdir /tmp/test-svn
$ svnadmin create /tmp/test-svn

След това, позволете всички да могат да променят revprops — лесният начин за това е да добавите скрипт pre-revprop-change, който винаги излиза с exit 0:

$ cat /tmp/test-svn/hooks/pre-revprop-change
#!/bin/sh
exit 0;
$ chmod +x /tmp/test-svn/hooks/pre-revprop-change

Сега можете да синхронизирате този проект в локалната машина изпълнявайки svnsync init с целево и изходно хранилища.

$ svnsync init file:///tmp/test-svn \
  http://your-svn-server.example.org/svn/

Това настройва параметрите за стартиране на синхронизацията. След това може да клонирате кода с:

$ svnsync sync file:///tmp/test-svn
Committed revision 1.
Copied properties for revision 1.
Transmitting file data .............................[...]
Committed revision 2.
Copied properties for revision 2.
[…]

Въпреки, че тази операция може да отнеме само няколко минути, ако се опитате да копирате оригиналното хранилище към друго отдалечено такова (вместо към локално), процесът ще продължи близо час при все че има по-малко от 100 къмита. Subversion ще трябва да клонира една версия в един момент и след това да я публикува обратно в друго хранилище — това е тотално неефективно, но е единственият наличен начин.

Начало на работа

След като вече имате Subversion хранилище, до което имате права за писане, можете да изпълните един типичен работен процес. Ще започнем с командата git svn clone, която импортира цяло Subversion хранилище в локално Git такова. Да не забравяме, че импортираме от реално хостнато Subversion хранилище, така че трябва да заменим израза file:///tmp/test-svn с URL-а на вашето Subversion хранилище:

$ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags
Initialized empty Git repository in /private/tmp/progit/test-svn/.git/
r1 = dcbfb5891860124cc2e8cc616cded42624897125 (refs/remotes/origin/trunk)
    A	m4/acx_pthread.m4
    A	m4/stl_hash.m4
    A	java/src/test/java/com/google/protobuf/UnknownFieldSetTest.java
    A	java/src/test/java/com/google/protobuf/WireFormatTest.java
…
r75 = 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae (refs/remotes/origin/trunk)
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/my-calc-branch, 75
Found branch parent: (refs/remotes/origin/my-calc-branch) 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae
Following parent with do_switch
Successfully followed parent
r76 = 0fb585761df569eaecd8146c71e58d70147460a2 (refs/remotes/origin/my-calc-branch)
Checked out HEAD:
  file:///tmp/test-svn/trunk r75

Това всъщност изпълнява еквивалента на две команди, git svn init последвана от git svn fetch към URL-а посочен от вас. Процесът може да отнеме доста време. Ако например тестовият проект имаше само около 75 къмита и сорс кодът не е много голям, Git независимо от всичко ще трябва да извлече всяка една версия индивидуално и да я къмитне индивидуално всеки път. За проект със стотици или хиляди къмити, това буквално може да отнеме часове или дори дни.

Частта от командата -T trunk -b branches -t tags указва на Git, че това Subversion хранилище следва основните branching и tagging конвенции. Ако вие именувате вашия trunk, клонове или тагове по различен начин, можете да промените тези опции. Понеже това е толкова често ползвано, можете да замените целия израз просто с флага -s, което означава standart layout и прилага трите горни опции. Така че тази команда е еквивалентна:

$ git svn clone file:///tmp/test-svn -s

В този момент вече трябва да разполагате с валидно Git хранилище с импортирани тагове и клонове:

$ git branch -a
* master
  remotes/origin/my-calc-branch
  remotes/origin/tags/2.0.2
  remotes/origin/tags/release-2.0.1
  remotes/origin/tags/release-2.0.2
  remotes/origin/tags/release-2.0.2rc1
  remotes/origin/trunk

Отбележете как инструментът управлява Subversion таговете като отдалечени референции. Нека погледнем в повече дълбочина с plumbing командата на Git show-ref:

$ git show-ref
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/heads/master
0fb585761df569eaecd8146c71e58d70147460a2 refs/remotes/origin/my-calc-branch
bfd2d79303166789fc73af4046651a4b35c12f0b refs/remotes/origin/tags/2.0.2
285c2b2e36e467dd4d91c8e3c0c0e1750b3fe8ca refs/remotes/origin/tags/release-2.0.1
cbda99cb45d9abcb9793db1d4f70ae562a969f1e refs/remotes/origin/tags/release-2.0.2
a9f074aa89e826d6f9d30808ce5ae3ffe711feda refs/remotes/origin/tags/release-2.0.2rc1
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/remotes/origin/trunk

Git не прави това при клониране от Git сървър, ето как изглежда прясно клонирано хранилище с тагове:

$ git show-ref
c3dcbe8488c6240392e8a5d7553bbffcb0f94ef0 refs/remotes/origin/master
32ef1d1c7cc8c603ab78416262cc421b80a8c2df refs/remotes/origin/branch-1
75f703a3580a9b81ead89fe1138e6da858c5ba18 refs/remotes/origin/branch-2
23f8588dde934e8f33c263c6d8359b2ae095f863 refs/tags/v0.1.0
7064938bd5e7ef47bfd79a685a62c1e2649e2ce7 refs/tags/v0.2.0
6dcb09b5b57875f334f61aebed695e2e4193db5e refs/tags/v1.0.0

Git издърпва таговете директно в refs/tags, вместо да ги третира като отдалечени клонове.

Къмитване обратно в Subversion

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

$ git commit -am 'Adding git-svn instructions to the README'
[master 4af61fd] Adding git-svn instructions to the README
 1 file changed, 5 insertions(+)

Следващата стъпка е да публикуваме тази промяна. Забележете как това променя начина ви на работа със Subversion — можете да направите повече от един локален къмит офлайн и след това да изпратите всички наведнъж към Subversion сървъра. За да направите това, използвайте командата git svn dcommit:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	README.txt
Committed r77
    M	README.txt
r77 = 95e0222ba6399739834380eb10afcd73e0670bc5 (refs/remotes/origin/trunk)
No changes between 4af61fd05045e07598c553167e0f31c84fd6ffe1 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

Командата взема всички ваши новоизвършени къмити, създава Subversion къмит за всеки от тях и след това редактира локалния ви Git къмит за да включи уникален идентификатор. Това е важно, защото означава, че всичките ви SHA-1 чексуми на вашите къмити се променят. Отчасти поради тази причина, да работите с Git базирани отдалечени версии на проектите едновременно със Subversion такива не е добра идея. Ако погледнете последния къмит, може да видите новодобавения идентификатор git-svn-id:

$ git log -1
commit 95e0222ba6399739834380eb10afcd73e0670bc5
Author: ben <ben@0b684db3-b064-4277-89d1-21af03df0a68>
Date:   Thu Jul 24 03:08:36 2014 +0000

    Adding git-svn instructions to the README

    git-svn-id: file:///tmp/test-svn/trunk@77 0b684db3-b064-4277-89d1-21af03df0a68

Забелязваме също така, че SHA-1 чексумата, която първоначално започваше с 4af61fd, когато къмитнахме сега започва с 95e0222. Ето защо, ако все пак настоявате да публикувате и към двата вида сървъри, трябва първо да направите това (dcommit) към Subversion сървъра, понеже това променя чексумата.

Издърпване на нови промени

При съвместната работа в един момент неизбежно се стига до опит за публикуване на промени предизвикващи конфликт. Промяната с конфликт ще бъде отхвърлена докато не слеете работата на другия колега преди това. В git svn, това изглежда така:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...

ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: d5837c4b461b7c0e018b49d12398769d2bfc240a and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 f414c433af0fd6734428cf9d2a9fd8ba00ada145 c80b6127dd04f5fcda218730ddf3a2da4eb39138 M	README.txt
Current branch master is up to date.
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.

За да се измъкнем от подобна ситуация, използваме git svn rebase, която издърпва от сървъра всички промени, които все още нямаме локално и след това пребазира текущата ни работа върху тях:

$ git svn rebase
Committing to file:///tmp/test-svn/trunk ...

ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: eaa029d99f87c5c822c5c29039d19111ff32ef46 and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 65536c6e30d263495c17d781962cfff12422693a b34372b25ccf4945fe5658fa381b075045e7702a M	README.txt
First, rewinding head to replay your work on top of it...
Applying: update foo
Using index info to reconstruct a base tree...
M	README.txt
Falling back to patching base and 3-way merge...
Auto-merging README.txt
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.

Сега всичката ви работа е пребазирана върху последната изтеглена от Subversion сървъра, така че можете успешно да направите dcommit:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	README.txt
Committed r85
    M	README.txt
r85 = 9c29704cc0bbbed7bd58160cfb66cb9191835cd8 (refs/remotes/origin/trunk)
No changes between 5762f56732a958d6cfda681b661d2a239cc53ef5 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

Отбележете, че за разлика от Git, при който трябва да слеете upstream промените, които ви липсват локално преди да можете да публикувате, git svn ви кара да правите това само ако промените предизвикват конфликт (точно както работи Subversion). Казано с други думи, ако някой друг публикува промени по един файл и след това вие публикувате промени по друг файл, dcommit ще си работи без проблем:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	configure.ac
Committed r87
    M	autogen.sh
r86 = d8450bab8a77228a644b7dc0e95977ffc61adff7 (refs/remotes/origin/trunk)
    M	configure.ac
r87 = f3653ea40cb4e26b6281cec102e35dcba1fe17c4 (refs/remotes/origin/trunk)
W: a0253d06732169107aa020390d9fefd2b1d92806 and refs/remotes/origin/trunk differ, using rebase:
:100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 e757b59a9439312d80d5d43bb65d4a7d0389ed6d M	autogen.sh
First, rewinding head to replay your work on top of it...

Това е важен за запомняне момент, защото резултатът ще е статус на проекта, който не съществува и на двата локални компютъра. Ако промените са несъвместими (но не правят конфликт) може да се окажете с проблеми, които са трудни за установяване. При един Git сървър това не е така — можете изцяло да тествате статуса на проекта на локалната ви машина преди да го публикувате, докато при Subversion дори не можете да сте сигурни, че статусите преди и след къмитването ви са идентични.

Трябва да използвате тази команда за изтегляне на промени от Subversion сървъра дори все още да не сте готови да къмитвате вашите. Можете да изпълните и git svn fetch за сваляне на новите данни, но git svn rebase изтегля и обновява локалните ви къмити с една команда.

$ git svn rebase
    M	autogen.sh
r88 = c9c5f83c64bd755368784b444bc7a0216cc1e17b (refs/remotes/origin/trunk)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/origin/trunk.

Изпълнявайте git svn rebase регулярно, за да сте сигурни, че локалния ви код е актуален. Обаче трябва да сте сигурни, че работната ви директория е в чист вид, когато пуснете командата. Ако имате некъмитнати промени, трябва или да ги маскирате (stash) или временно да ги къмитнете преди изпълнението на git svn rebase, иначе командата ще спре ако види, че пребазирането ще доведе до merge конфликт.

Проблеми с Git клонове

Когато започнете да се чувствате удобно с похватите на работа на Git, доста вероятно е да започнете да правите topic клонове, да работите в тях и да ги сливате. Ако публикувате към Subversion сървър с git svn, може да искате да пребазирате работата си върху единичен клон всеки път вместо да сливате клонове. Причината да предпочетете пребазирането е, че Subversion поддържа линейна история и не обработва сливанията така, както го прави Git. Така че git svn следва само първия родител при конвертирането на snapshot-ите в Subversion къмити.

Да допуснем, че историята ни изглежда така: създали сме experiment клон, направили сме два къмита и след това сме го слели обратно в master. При изпълнение на dcommit, ще видим нещо такова:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	CHANGES.txt
Committed r89
    M	CHANGES.txt
r89 = 89d492c884ea7c834353563d5d913c6adf933981 (refs/remotes/origin/trunk)
    M	COPYING.txt
    M	INSTALL.txt
Committed r90
    M	INSTALL.txt
    M	COPYING.txt
r90 = cb522197870e61467473391799148f6721bcf9a0 (refs/remotes/origin/trunk)
No changes between 71af502c214ba13123992338569f4669877f55fd and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

Изпълнението на dcommit от клон със слята история работи добре с изключение на факта, че когато погледнете в историята на Git проекта, ще установите че двата къмита от клона experiment не са пренаписани, вместо това всички тези промени се появяват в SVN версията на единичния сливащ къмит.

Когато някой друг клонира тази работа, всичко което ще види е merge къмита ви с всичката работа обединена в него все едно сте изпълнили git merge --squash, но никакви подробности откъде и кога са промените направени от вас в experiment.

Subversion клонове

Клоновете в Subversion не са същите като в Git и ако можете да ги избягвате ще е най-добре. Но при все това, с git svn можете да създавате и да къмитвате в Subversion клонове.

Създаване на нов SVN клон

За да създадете нов клон в Subversion, използвайте git svn branch [new-branch]:

$ git svn branch opera
Copying file:///tmp/test-svn/trunk at r90 to file:///tmp/test-svn/branches/opera...
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/opera, 90
Found branch parent: (refs/remotes/origin/opera) cb522197870e61467473391799148f6721bcf9a0
Following parent with do_switch
Successfully followed parent
r91 = f1b64a3855d3c8dd84ee0ef10fa89d27f1584302 (refs/remotes/origin/opera)

Това прави еквивалента на svn copy trunk branches/opera командата в Subversion и работи на Subversion сървъра. Важно е да посочим, че това не ви прехвърля автоматично в този клон и ако сега къмитнете, къмитът ще отиде в клона trunk на сървъра, вместо в opera.

Превключване на активни клонове

Git определя в кой клон отиват вашите dcommits гледайки върховете на всички ваши Subversion клонове в историята ви — трябва да имате само един и той трябва да е последния с git-svn-id в текущата ви branch история.

Ако искате да работите по повече от един клон едновременно, можете да настроите локалните клонове да правят dcommit към специфични Subversion клонове стартирайки ги от импортирания Subversion къмит за този клон. Ако искате клон opera, в който да работите отделно, може да изпълните:

$ git branch opera remotes/origin/opera

Сега, ако желаете да слеете вашия opera клон в trunk (вашия master клон), можете да го направите с нормална команда git merge. Но трябва да предоставите описателно къмит съобщение (чрез -m) или сливането ще гласи “Merge branch opera” вместо нещо полезно.

Помнете, че независимо че използвате git merge за тази операция и че сливането вероятно ще е много по-лесно отколкото би било в Subversion (защото Git автоматично ще установи подходящата merge база за вас), това не е стандартен Git merge къмит. Ще трябва да изпратите тези данни към Subversion сървър, който не може да обработва къмит следящ повече от един родител и когато го направите, те ще изглеждат като единичен къмит обединяващ цялата извършена работа от друг клон. След като слеете един клон в друг, не можете лесно да се върнете и да продължите да работите по този клон, както нормално бихте могли в Git. Командата dcommit изтрива всяка информация казваща кой клон е бил слят, така че евентуалните следващи merge-base изчисления ще са погрешни — dcommit прави така, че резултатът от git merge да изглежда като от git merge --squash. За жалост няма добър начин за избягване на такава ситуация — Subversion не може да пази тази информация и винаги ще бъдете ограничавани, когато го използвате като сървър. За да си спестите проблеми, добре е да изтривате локалния клон (в този случай opera) след като го слеете в trunk.

Subversion команди

Инструментите на git svn осигуряват множество команди, които подпомагат по-лесното минаване към Git, предоставяйки функционалности подобни на тези в Subversion. Ето няколко команди, които ви дават това, което и Subversion.

История в SVN стил

Ако сте свикнали със Subversion и искате да видите историята си в SVN стил, може да използвате git svn log:

$ git svn log
------------------------------------------------------------------------
r87 | schacon | 2014-05-02 16:07:37 -0700 (Sat, 02 May 2014) | 2 lines

autogen change

------------------------------------------------------------------------
r86 | schacon | 2014-05-02 16:00:21 -0700 (Sat, 02 May 2014) | 2 lines

Merge branch 'experiment'

------------------------------------------------------------------------
r85 | schacon | 2014-05-02 16:00:09 -0700 (Sat, 02 May 2014) | 2 lines

updated the changelog

Две неща са важни с git svn log. Първо, тя работи офлайн, за разлика от реалната svn log команда, която пита Subversion сървъра за данни. Второ, тя показва само къмитите, които са били публикувани на Subversion сървъра. Локалните Git къмити, които не сте публикували, не се показват — нито пък тези, които други разработчици евентуално са публикували на Subversion сървъра междувременно. Получавате нещо като last known статус на къмитите от сървъра.

SVN анотация

Точно както git svn log симулира командата svn log офлайн, можете да изпълните еквивалента на svn annotate с git svn blame [FILE]. Изходът изглежда така:

$ git svn blame README.txt
 2   temporal Protocol Buffers - Google's data interchange format
 2   temporal Copyright 2008 Google Inc.
 2   temporal http://code.google.com/apis/protocolbuffers/
 2   temporal
22   temporal C++ Installation - Unix
22   temporal =======================
 2   temporal
79    schacon Committing in git-svn.
78    schacon
 2   temporal To build and install the C++ Protocol Buffer runtime and the Protocol
 2   temporal Buffer compiler (protoc) execute the following:
 2   temporal

Отново, вашите къмити и междувременно публикуваните от други хора промени в Subversion сървъра няма да се покажат.

Инфомация за SVN Server

Можете също да получите информацията, която svn info предоставя с git svn info:

$ git svn info
Path: .
URL: https://schacon-test.googlecode.com/svn/trunk
Repository Root: https://schacon-test.googlecode.com/svn
Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029
Revision: 87
Node Kind: directory
Schedule: normal
Last Changed Author: schacon
Last Changed Rev: 87
Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009)

Данните подобно на blame и log са офлайн и са актуални към момента, когато последно сте комуникирали със Subversion сървъра.

Игнориране на това, което Subversion игнорира

Ако клонирате Subversion хранилище, което има зададени svn:ignore настройки, вероятно бихте искали да създадете съответни .gitignore файлове, така че да не къмитнете по невнимание ненужни неща. git svn има две команди за случая. Първата е git svn create-ignore, която автоматично създава съответстващите .gitignore файлове, така че следващият ви къмит може да ги включи.

Втората команда е git svn show-ignore, която печата на stdout редовете, които трябва да вмъкнете в .gitignore файл така че бихте могли да пренасочите изхода ѝ в exclude файла си и да не правите индивидуални .gitignore:

$ git svn show-ignore > .git/info/exclude

По този начин не се налага да пълните проекта с .gitignore файлове. Това е добра опция, ако сте единствения Git потребител в Subversion екип и колегите ви не желаят да виждат .gitignore файлове в съвместния проект.

Git-Svn обобщение

Инструментите на git svn са полезни, ако си имате работа със Subversion сървър по една или друга причина. Би следвало да гледате на Subversion като на орязан Git или ще си имате проблеми в превода, предизвикващи объркване у вас и у колегите ви. За да си спестите главоболия, опитайте да спазвате тези правила:

  • Поддържайте линейна Git история, която не съдържа сливащи къмити направени с git merge. Пребазирайте всяка работа, която сте извърили извън главния си клон обратно върху него, не я сливайте в него.

  • Не работете паралелно по един и същи проект в Git сървър и Subversion сървър. По изключение може да имате един такъв за да ускорите клониранията за новите разработчици, но не публикувайте в него нищо, което не съдържа git-svn-id елемент. Може дори да искате да добавите pre-receive hook, който да проверява всяко къмит съобщение за наличие на git-svn-id поле и да отказва публикуванията с къмити, в които то липсва.

Спазвате ли тези съвети, работата ви със Subversion сървъри би могла да бъде по-поносима. Обаче, ако е налична възможност да преминете към реален Git сървър, това ще даде на екипа ви много повече позитиви.

Git и Mercurial

DVCS вселената не включва само Git. В действителност, налични са много други системи, всяка от която със собствени възгледи за това как трябва да се прави разпределен version control. Като изключим Git, най-популярната сред тях е Mercurial и двете са подобни в много аспекти.

Добрите новини са, че ако предпочитате да използвате Git локално, но се налага да работите по проект, чийто сорс код се контролира с Mercurial, съществува начин Git да функционира като клиент на Mercurial хранилища. Понеже Git използва remotes за комуникация със сървърни хранилища, не е изненадващо, че въпросният бридж е имплементиран като remote helper. Името на проекта е git-remote-hg, и може да се намери на https://github.com/felipec/git-remote-hg.

git-remote-hg

Първо, трябва да инсталираме git-remote-hg. Практически това се изчерпва с копирането на файла някъде в пътя ви:

$ curl -o ~/bin/git-remote-hg \
  https://raw.githubusercontent.com/felipec/git-remote-hg/master/git-remote-hg
$ chmod +x ~/bin/git-remote-hg

…допускаме че ~/bin е включен в пътищата на променливата $PATH. Git-remote-hg има и още една друга зависимост: библиотеката mercurial за Python. Ако имате инсталиран Python, това е лесно:

$ pip install mercurial

Ако нямате Python, посетете https://www.python.org/ и го инсталирайте преди това.

Последното нещо, от което се нуждаем, е Mercurial клиента. Изтеглете го и го инсталирайте от https://www.mercurial-scm.org/.

Сега сте готови за работа. Имаме нужда от Mercurial хранилище, в което можем да публикуваме. За късмет, всяко Mercurial хранилище може да работи по такъв начин, така че ще използваме стандартното "hello world", което всички използват за да учат Mercurial:

$ hg clone http://selenic.com/repo/hello /tmp/hello

Начало

След като имаме подходящо “server-side” хранилище, можем да преминем през един стандартен работен процес. Както ще видите, тези две системи са доста подобни и не би трябвало да имате големи трудности.

Както винаги с Git, първо клонираме:

$ git clone hg::/tmp/hello /tmp/hello-git
$ cd /tmp/hello-git
$ git log --oneline --graph --decorate
* ac7955c (HEAD, origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master, master) Create a makefile
* 65bb417 Create a standard 'hello, world' program

Веднага забелязваме, че при работата с Mercurial хранилища се използва стандартната git clone команда. Това е така, понеже git-remote-hg работи на сравнително ниско ниво, използвайки механизъм подобен на този, който Git използва за HTTP/S комуникация (remote helpers). И Git и Mercurial са проектирани така, че всеки клиент да има пълно копие на историята на хранилището и тази команда ви осигурява цялостно клониране, включващо цялата история на проекта — при това сравнително бързо.

Командата log показва два къмита, към последния от които сочи цял куп референции. Оказва се, че някои от тях реално не са там. Нека видим какво действително съдържа директорията .git:

$ tree .git/refs
.git/refs
├── heads
│   └── master
├── hg
│   └── origin
│       ├── bookmarks
│       │   └── master
│       └── branches
│           └── default
├── notes
│   └── hg
├── remotes
│   └── origin
│       └── HEAD
└── tags

9 directories, 5 files

Git-remote-hg се опитва да прави нещата в стила на Git, но зад кулисите тя управлява концептуалното свързване между две леко различаващи се системи. Директорията refs/hg съдържа действителните отдалечени референции. Например, refs/hg/origin/branches/default е Git ref файл, който съдържа SHA-1 започваща с “ac7955c”, което е къмита, към който сочи master. По този начин refs/hg директорията е нещо като фалшива refs/remotes/origin, но също така може да различава bookmarks и branches.

Файлът notes/hg е изходната точка за това как git-remote-hg съотнася хешовете на Git къмитите с changeset идентификаторите на Mercurial. Нека погледнем по-подробно:

$ cat notes/hg
d4c10386...

$ git cat-file -p d4c10386...
tree 1781c96...
author remote-hg <> 1408066400 -0800
committer remote-hg <> 1408066400 -0800

Notes for master

$ git ls-tree 1781c96...
100644 blob ac9117f...	65bb417...
100644 blob 485e178...	ac7955c...

$ git cat-file -p ac9117f
0a04b987be5ae354b710cefeba0e2d9de7ad41a9

И така refs/notes/hg сочи към дърво, което в базата данни с обекти на Git е представено като списък от други обекти с имена. git ls-tree отпечатва режима, типа, хеш стойността и името на файла за елементите в дървото. Ако поразгледаме един от елементите на дървото, откриваме, че вътре има blob наречен “ac9117f” (SHA-1 хеша на къмита, към който сочи master), със съдържание “0a04b98” (което е ID-то на Mercurial changeset-а на върха на клона default).

Добрата новина е, че почти не се налага да се занимаваме с всичко това. Типичният работен процес няма да е много различен от този, при който работим с Git remote.

Трябва да обърнем внимание на още едно нещо преди да продължим, игнориранията. Mercurial и Git използват много подобен механизъм за това, но е вероятно да не искате да публикувате .gitignore файл в Mercurial хранилище. За щастие Git има начин да игнорира файлове, които са локални за хранилище на диска и форматът на Mercurial е съвместим с този на Git, така че просто трябва да копирате съответния файл:

$ cp .hgignore .git/info/exclude

Файлът .git/info/exclude работи точно като .gitignore, но не влиза в къмитите.

Работен процес

Да допуснем, че сме извършили някаква работа в проекта, имаме няколко къмита в клона master и сме готови да публикуваме промените. Ето как изглежда хранилището ни сега:

$ git log --oneline --graph --decorate
* ba04a2a (HEAD, master) Update makefile
* d25d16f Goodbye
* ac7955c (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Create a makefile
* 65bb417 Create a standard 'hello, world' program

Нашият master клон е с два къмита напред спрямо origin/master, но те са само на локалната машина. Нека проверим дали някой друг не е публикувал някаква важна промяна междувременно:

$ git fetch
From hg::/tmp/hello
   ac7955c..df85e87  master     -> origin/master
   ac7955c..df85e87  branches/default -> origin/branches/default
$ git log --oneline --graph --decorate --all
* 7b07969 (refs/notes/hg) Notes for default
* d4c1038 Notes for master
* df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
| * ba04a2a (HEAD, master) Update makefile
| * d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard 'hello, world' program

Понеже използвахме флага --all, виждаме референциите “notes”, които се използват вътрешно от git-remote-hg, можем да ги игнорираме. Останалото е каквото се очаква; origin/master е обновен с един къмит и историята ни сега е разклонена (diverged). За разлика от другите системи, които преглеждаме в тази глава, Mercurial може да обработва сливания, така че няма да се налага да правим нищо необичайно.

$ git merge origin/master
Auto-merging hello.c
Merge made by the 'recursive' strategy.
 hello.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --decorate
*   0c64627 (HEAD, master) Merge remote-tracking branch 'origin/master'
|\
| * df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
* | ba04a2a Update makefile
* | d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard 'hello, world' program

Перфектно. Пускаме си тестовете, те минават добре и сме готови да публикуваме промените си на сървъра:

$ git push
To hg::/tmp/hello
   df85e87..0c64627  master -> master

Това е всичко! Ако погледнете Mercurial хранилището, ще видите че Push операцията е направила каквото се очаква:

$ hg log -G --style compact
o    5[tip]:4,2   dc8fa4f932b8   2014-08-14 19:33 -0700   ben
|\     Merge remote-tracking branch 'origin/master'
| |
| o  4   64f27bcefc35   2014-08-14 19:27 -0700   ben
| |    Update makefile
| |
| o  3:1   4256fc29598f   2014-08-14 19:27 -0700   ben
| |    Goodbye
| |
@ |  2   7db0b4848b3c   2014-08-14 19:30 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard 'hello, world' program

Changeset-ът с номер 2 е създаден от Mercurial, а тези с номера 3 и 4 са направени от git-remote-hg, в резултат от публикуването на къмитите от Git.

Branches и Bookmarks

Git има само един вид клон: референция, която се премества с правенето на къмити. В Mercurial, този тип референция се нарича “bookmark,” и тя се държи почти по същия начин като Git клон.

Концепцията на Mercurial за понятието “branch” е по-различна. Клонът, върху който е създаден changeset се записва с changeset-a, което значи че той винаги ще бъде в историята на хранилището. Ето пример за къмит направен в клона develop:

$ hg log -l 1
changeset:   6:8f65e5e02793
branch:      develop
tag:         tip
user:        Ben Straub <ben@straub.cc>
date:        Thu Aug 14 20:06:38 2014 -0700
summary:     More documentation

Забележете реда, който започва с “branch”. Git не може в действителност да пресъздаде това (а и не се нуждае, и двата типа клонове могат да се представят като Git референции), но git-remote-hg трябва да разбира разликата, защото Mercurial ѝ обръща внимание.

Създаването на Mercurial bookmarks е лесно колкото създаването на Git клонове. На Git страната:

$ git checkout -b featureA
Switched to a new branch 'featureA'
$ git push origin featureA
To hg::/tmp/hello
 * [new branch]      featureA -> featureA

Това е всичко по въпроса. От страната на Mercurial това изглежда така:

$ hg bookmarks
   featureA                  5:bd5ac26f11f9
$ hg log --style compact -G
@  6[tip]   8f65e5e02793   2014-08-14 20:06 -0700   ben
|    More documentation
|
o    5[featureA]:4,2   bd5ac26f11f9   2014-08-14 20:02 -0700   ben
|\     Merge remote-tracking branch 'origin/master'
| |
| o  4   0434aaa6b91f   2014-08-14 20:01 -0700   ben
| |    update makefile
| |
| o  3:1   318914536c86   2014-08-14 20:00 -0700   ben
| |    goodbye
| |
o |  2   f098c7f45c4f   2014-08-14 20:01 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard 'hello, world' program

Забележете новия таг [featureA] на revision 5. Тези работят точно както Git клонове от страната на Git с едно изключение: не можете да изтриете bookmark от страната Git (това е ограничение на remote helpers).

Освен с Mercurial bookmarks, можете да работите и с “heavyweight” Mercurial клонове: просто ги създавате в branches namespace:

$ git checkout -b branches/permanent
Switched to a new branch 'branches/permanent'
$ vi Makefile
$ git commit -am 'A permanent change'
$ git push origin branches/permanent
To hg::/tmp/hello
 * [new branch]      branches/permanent -> branches/permanent

Ето как ще изглежда това от страната на Mercurial:

$ hg branches
permanent                      7:a4529d07aad4
develop                        6:8f65e5e02793
default                        5:bd5ac26f11f9 (inactive)
$ hg log -G
o  changeset:   7:a4529d07aad4
|  branch:      permanent
|  tag:         tip
|  parent:      5:bd5ac26f11f9
|  user:        Ben Straub <ben@straub.cc>
|  date:        Thu Aug 14 20:21:09 2014 -0700
|  summary:     A permanent change
|
| @  changeset:   6:8f65e5e02793
|/   branch:      develop
|    user:        Ben Straub <ben@straub.cc>
|    date:        Thu Aug 14 20:06:38 2014 -0700
|    summary:     More documentation
|
o    changeset:   5:bd5ac26f11f9
|\   bookmark:    featureA
| |  parent:      4:0434aaa6b91f
| |  parent:      2:f098c7f45c4f
| |  user:        Ben Straub <ben@straub.cc>
| |  date:        Thu Aug 14 20:02:21 2014 -0700
| |  summary:     Merge remote-tracking branch 'origin/master'
[...]

Името на клона “permanent” беше записано с changeset-а маркиран като 7.

От гледна точка на Git, работата с тези два вида клонове е една и съща: превключвате, къмитвате, изтегляте, сливате и публикувате както нормално се прави в Git. Нещо, което трябва да отбележим е, че Mercurial не поддържа презапис на историята, към нея може само да се добавя. Ето как ще изглежда нашето Mercurial хранилище след интерактивно пребазиране последвано от force-push:

$ hg log --style compact -G
o  10[tip]   99611176cbc9   2014-08-14 20:21 -0700   ben
|    A permanent change
|
o  9   f23e12f939c3   2014-08-14 20:01 -0700   ben
|    Add some documentation
|
o  8:1   c16971d33922   2014-08-14 20:00 -0700   ben
|    goodbye
|
| o  7:5   a4529d07aad4   2014-08-14 20:21 -0700   ben
| |    A permanent change
| |
| | @  6   8f65e5e02793   2014-08-14 20:06 -0700   ben
| |/     More documentation
| |
| o    5[featureA]:4,2   bd5ac26f11f9   2014-08-14 20:02 -0700   ben
| |\     Merge remote-tracking branch 'origin/master'
| | |
| | o  4   0434aaa6b91f   2014-08-14 20:01 -0700   ben
| | |    update makefile
| | |
+---o  3:1   318914536c86   2014-08-14 20:00 -0700   ben
| |      goodbye
| |
| o  2   f098c7f45c4f   2014-08-14 20:01 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

Changeset-ите 8, 9, и 10 са създадени и принадлежат към клона permanent, но старите changesets са все още там. Това може да е много смущаващо за колегите ви използващи Mercurial, така че се старайте да го избягвате.

Mercurial обобщение

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

Git и Bazaar

Друга популярна DVCS система е Bazaar. Bazaar е безплатна система с отворен код, част от проекта GNU Project. Тя работи по много различен начин от Git. Понякога, за да направите едно и също нещо като в Git, ще трябва да използвате различна ключова дума и също така някои често използвани ключови думи нямат значението, което може би бихте очаквали. По-специално, управлението на клонове е много различно и може да предизвика конфуз, особено сред Git потребителите. Но въпреки всичко, да се работи с Bazaar хранилище от Git такова, е възможно.

Съществуват много проекти, които позволяват да използвате Git като Bazaar клиент. Тук ще използваме проекта на Felipe Contreras, който можете да намерите на https://github.com/felipec/git-remote-bzr. За да го инсталирате, трябва просто да свалите git-remote-bzr в папка от пътя ви:

$ wget https://raw.github.com/felipec/git-remote-bzr/master/git-remote-bzr -O ~/bin/git-remote-bzr
$ chmod +x ~/bin/git-remote-bzr

Ще ви трябва и инсталиран Bazaar. Това е всичко.

Създаване на Git хранилище от Bazaar хранилище

Инструментът е лесен за ползване. Достатъчно е да клонирате Bazaar хранилище като му дадете префикс bzr::. Понеже Git и Bazaar правят пълно клониране върху локалния компютър, възможно е да прикачите Git копие към локалното ви Bazaar копие на хранилище, въпреки че това не е препоръчително. Много по-лесно е да закачите вашето Git копие към същото място, към което е закачено Bazaar копието — към централното хранилище.

Приемаме, че работим с отдалечено хранилище на адрес bzr+ssh://developer@mybazaarserver:myproject. Клонираме го така:

$ git clone bzr::bzr+ssh://developer@mybazaarserver:myproject myProject-Git
$ cd myProject-Git

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

$ git gc --aggressive

Bazaar клонове

Bazaar позволява само да клонирате клонове, но едно хранилище може да има много такива и git-remote-bzr може да ги клонира всички. Например, за да клонирате клон:

$ git clone bzr::bzr://bzr.savannah.gnu.org/emacs/trunk emacs-trunk

За да клонирате цялото хранилище:

$ git clone bzr::bzr://bzr.savannah.gnu.org/emacs emacs

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

$ git config remote-bzr.branches 'trunk, xwindow'

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

$ git init emacs
$ git remote add origin bzr::bzr://bzr.savannah.gnu.org/emacs
$ git config remote-bzr.branches 'trunk, xwindow'
$ git fetch

Игнориране с .bzrignore

Понеже работите по Bazaar-контролиран проект, не трябва да създавате .gitignore файл, защото можете по невнимание да го включите в контрола и тогава другите хора работещи с Bazaar ще бъдат объркани. Решението е да създадете .git/info/exclude файл като символна връзка или като нормален такъв. По-късно ще видим как да се справим с това.

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

  1. "!!" позволява да игнорирате определени файлови маски дори ако те са указани с "!" правило.

  2. "RE:" в началото на реда позволява да укажете като маска Python регулярен израз (Git позволява само шел globs).

В резултат на това, възникват две различни ситуации:

  1. Ако .bzrignore файлът не съдържа някой от двата префикса, тогава просто можете да направите символна връзка към него в хранилището: ln -s .bzrignore .git/info/exclude.

  2. В противен случай, трябва да създадете файл .git/info/exclude и да го адаптирате така, че да игнорира същите файлове, които игнорира и .bzrignore.

Във всички случаи трябва да бъдете внимателни за всяка бъдеща промяна по .bzrignore за да сте сигурни, че .git/info/exclude винаги отразява коректно съдържанието му. Ако .bzrignore се промени и се появят редове започващи с "!!" или "RE:", то Git няма да може да ги интерпретира и ще трябва да адаптирате вашия .git/info/exclude файл да прави същото като .bzrignore. Освен това, ако .git/info/exclude преди това е бил символна връзка, първо ще трябва да я изтриете, да копирате .bzrignore в .git/info/exclude и да го редактирате за коректна адаптация. Обаче, бъдете внимателни със създаването на файла, защото с Git не е възможно да включите повторно файл, ако родителската директория на този файл е изключена.

Издърпване на промени от отдалечено хранилище

За издърпване на промени се използват стандартните Git команди. Подразбирайки, че промените ви са в master клона, вие сливате или пребазирате работата си върху origin/master клона:

$ git pull --rebase origin

Публикуване в отдалечено хранилище

Bazaar също като Git поддържа концепцията за merge къмити, така че няма да има проблем с публикуването на такива. Можете спокойно да работите по отделен клон, да го слеете в master и след това да публикувате. Създавате клонове, тествате и къмитвате както обикновено. Накрая изпращате работата си в Bazaar хранилището:

$ git push origin master

Недостатъци

Remote helper-ите на Git си имат някои ограничения. По-специално, следните команди не работят:

  • git push origin :branch-to-delete (Bazaar не може да приема изтриване на референции по този начин)

  • git push origin old:new (ще се публикува old)

  • git push --dry-run origin branch (ще се направи действително публикуване)

Обобщение

Моделите на Git и Bazaar си приличат и по тази причина съвместната им работа не е толкова проблемна. Стига да съблюдавате ограниченията и винаги да помните, че отдалеченото хранилище не е реално Git такова, не би следвало да срещате съществени трудности.

Git и Perforce

Perforce е много популярна version-control система в корпоративните среди. Съществува от 1995 г. и това я прави най-старата от всички, които разглеждаме в тази глава. Като такава, тя е проектирана съобразно нуждите от онези времена и подразбира, че сте свързани към единичен централен сървър и само една версия се пази на локалния диск. За да сме справедливи, нейните функционалности и ограничения обслужват много добре някои специфични задачи, но на практика съществуват много проекти, които използват Perforce а биха работили много по-добре на Git.

Ако искате да съчетаете използването на Perforce и Git, налични са две опции. Първата, която ще опишем е “Git Fusion” бриджа създаден от авторите на Perforce, който ви позволява да третирате subtrees от Perforce депо като read-write Git хранилища. Втората, git-p4, е клиентски бридж, който позволява да използвате Git като Perforce клиент без да се налага каквато и да било промяна в настройките на Perforce сървъра.

Git Fusion

Perforce предоставя продукта Git Fusion (наличен на http://www.perforce.com/git-fusion), който синхронизира Perforce сървър с Git хранилища от страна на сървъра.

Настройка

За нашите примери ще използваме най-лесния метод за инсталиране на Git Fusion, виртуална машина, в която работят Perforce демона и Git Fusion. Можете да свалите имидж за виртуалната машина от http://www.perforce.com/downloads/Perforce/20-User, и след това да го стартирате в предпочитания от вас виртуализационен софтуер (в случая ползваме VirtualBox).

След стартирането си, виртуалната машина изисква задаване на пароли за три Linux потребителя (root, perforce и git) и предоставя име на инстанцията, което може да се използва за различаване на конкретната инсталация от другите в същата мрежа. Когато всичко това приключи ще видите следното:

Стартов екран на виртуалната машина на Git Fusion
Фигура 145. Стартов екран на виртуалната машина на Git Fusion

Запомнете показания тук IP адрес, ще го ползваме по-късно. След това, ще създадем потребител за Perforce. Изберете опцията “Login” в долния край на екрана и натиснете enter (или влезте в машината през SSH), след това влезте като root. След това изпълнете тези команди, за да създадете новия потребител:

$ p4 -p localhost:1666 -u super user -f john
$ p4 -p localhost:1666 -u john passwd
$ exit

Първата ще отвори редактора Vi за настройка на потребителя, можете да приемете настройките по подразбиране изпълнявайки :wq и enter. Следващата команда ще ви пита два пъти за паролата. Това е всичко, което правим в конзолата, така че можете да излезете от сесията.

Следващото нещо, което следва да направим, е да кажем на Git да не проверява SSL сертификатите. Виртуалната машина на Git Fusion идва със сертификат за домейн, който няма да съвпада с IP адреса на виртуалната машина и Git ще отхвърля HTTPS връзката. Ако това ще бъде перманентна инсталация, можете да погледнете документацията на Git Fusion и да видите как да инсталирате различен сертификат. За целите на демонстрацията това няма да е нужно:

$ export GIT_SSL_NO_VERIFY=true

Сега можем да тестваме дали всичко работи.

$ git clone https://10.0.1.254/Talkhouse
Cloning into 'Talkhouse'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 630, done.
remote: Compressing objects: 100% (581/581), done.
remote: Total 630 (delta 172), reused 0 (delta 0)
Receiving objects: 100% (630/630), 1.22 MiB | 0 bytes/s, done.
Resolving deltas: 100% (172/172), done.
Checking connectivity... done.

Имиджът идва с инсталиран примерен проект, който може да се клонира. Тук клонираме през HTTPS като потребител john, който създадохме преди малко. Git ще пита за име и парола за конекцията първия път, но после credential кешът ще ви спести това неудобство.

Конфигурация на Fusion

След като имате инсталиран Git Fusion, ще променим леко конфигурацията. Това е сравнително лесно с предпочитания от вас Perforce клиент, просто асоциирайте //.git-fusion директорията на Perforce сървъра в работното си пространство. Файловата структура изглежда така:

$ tree
.
├── objects
│   ├── repos
│   │   └── [...]
│   └── trees
│       └── [...]
│
├── p4gf_config
├── repos
│   └── Talkhouse
│       └── p4gf_config
└── users
    └── p4gf_usermap

498 directories, 287 files

Директорията objects се използва служебно от Git Fusion за асоцииране на Perforce обекти с Git и обратно, няма да се налага да пипате нищо в нея. В нея има глобален p4gf_config файл, както и по един такъв за всяко хранилище — това са конфигурационните файлове, които определят поведението на Git Fusion. Нека видим файла в главната директория:

[repo-creation]
charset = utf8

[git-to-perforce]
change-owner = author
enable-git-branch-creation = yes
enable-swarm-reviews = yes
enable-git-merge-commits = yes
enable-git-submodules = yes
preflight-commit = none
ignore-author-permissions = no
read-permission-check = none
git-merge-avoidance-after-change-num = 12107

[perforce-to-git]
http-url = none
ssh-url = none

[@features]
imports = False
chunked-push = False
matrix2 = False
parallel-push = False

[authentication]
email-case-sensitivity = no

Няма да задълбаваме в подробности за всички флагове тук, просто отбележете, че това е INI-форматиран текстов файл, подобен на тези, които и Git използва за конфигурации. Този файл задава глобалните опции, които могат да бъдат презаписани от съответния файл в конкретно хранилище като например repos/Talkhouse/p4gf_config. Ако отворите подобен такъв, ще видите секцията [@repo] с няколко настройки, които се различават от глобалните. Ще видите и секции като:

[Talkhouse-master]
git-branch-name = master
view = //depot/Talkhouse/main-dev/... ...

Това е мапинг между Perforce клон и Git клон. Тази секция може да се казва както желаете, стига да е уникална. git-branch-name позволява да конвертирате пътя на депо, който може да е объркващ под Git към нещо по-разбираемо. Настройката view определя как Perforce файловете се съотнасят в Git хранилището, използва се стандартния view mapping синтаксис. Както е видно, може да се задава повече от един мапинг:

[multi-project-mapping]
git-branch-name = master
view = //depot/project1/main/... project1/...
       //depot/project2/mainline/... project2/...

По този начин, ако нормалният ви workspace мапинг включва промени в структурата на директориите, може да ги отразите в Git хранилището.

Последният файл, на който обръщаме внимание е users/p4gf_usermap, който съотнася Perforce потребители на Git потребители, и от който може въобще да не се нуждаете. Когато конвертира от Perforce changeset към Git къмит, Get Fusion по подразбиране ще потърси Perforce потребителя и ще използва имейл адреса и пълното му име за съответните author/committer полета в Git. В обратната посока, подразбиращото се поведение е да се потърси Perforce потребител с имейл адрес съответстващ на author полето на Git къмита и changeset-а да се запише като направен от този потребител (като се вземат предвид и правата за достъп). В повечето случаи тази схема работи коректно, но погледнете този мапинг файл:

john john@example.com "John Doe"
john johnny@appleseed.net "John Doe"
bob employeeX@example.com "Anon X. Mouse"
joe employeeY@example.com "Anon Y. Mouse"

Всеки ред следва формата <user> <email> "<full name>" и дефинира единично съответствие за потребител. Първите две полета асоциират два отделни имейл адреса с един и същи Perforce потребителски акаунт. Това е полезно, ако сте създали Git къмити с различни имейл адреси (или пък сте сменили адреса), но искате да ги присъедините към същия Perforce потребител. Когато създавате Git къмит от Perforce changeset, първият ред съответстващ на Perforce потребителя се използва за попълване на информацията за автора в Git.

Последните два реда маскират действителните имена и имейл адреси на Bob и Joe, от създадените Git къмити. Това е хубаво, в случай че решите да отворите кода на вътрешен проект, но не искате да разкриете списъка с разработчици в компанията ви на останалия свят. Запомнете, че имейл адресите и пълните имена трябва да са уникални, освен ако не искате всички Git къмити да са с фиктивен единичен автор.

Работен процес

Git Fusion е двупосочен бридж между Perforce и Git. Да видим как изглежда работата от гледната точка на Git. Ще допуснем, че сме асоциирали проекта “Jam” с конфигурационен файл като показания по-горе. Клонираме го така:

$ git clone https://10.0.1.254/Jam
Cloning into 'Jam'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 2070, done.
remote: Compressing objects: 100% (1704/1704), done.
Receiving objects: 100% (2070/2070), 1.21 MiB | 0 bytes/s, done.
remote: Total 2070 (delta 1242), reused 0 (delta 0)
Resolving deltas: 100% (1242/1242), done.
Checking connectivity... done.
$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/rel2.1
$ git log --oneline --decorate --graph --all
* 0a38c33 (origin/rel2.1) Create Jam 2.1 release branch.
| * d254865 (HEAD, origin/master, origin/HEAD, master) Upgrade to latest metrowerks on Beos -- the Intel one.
| * bd2f54a Put in fix for jam's NT handle leak.
| * c0f29e7 Fix URL in a jam doc
| * cc644ac Radstone's lynx port.
[...]

Първият път, когато направите това, може да отнеме повече време. Това, което се случва е, че Git Fusion конвертира всички приложими changesets в Perforce историята в Git къмити. Това се случва локално на сървъра, така че е сравнително бързо, но все пак зависи от мащаба на историята. Последващите изтегляния правят инкрементално конвертиране, така че наподобяват скоростта на Git, с която сме свикнали.

Както може да видите, хранилището ни изглежда точно като всяко друго с Git. Имаме три клона и Git услужливо е създал локален master клон, който проследява origin/master. Нека направим малко промени и създадем няколко къмита:

# ...
$ git log --oneline --decorate --graph --all
* cfd46ab (HEAD, master) Add documentation for new feature
* a730d77 Whitespace
* d254865 (origin/master, origin/HEAD) Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]

Сега имаме два нови къмита. Следва да проверим дали някой друг колега не е публикувал нови промени:

$ git fetch
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://10.0.1.254/Jam
   d254865..6afeb15  master     -> origin/master
$ git log --oneline --decorate --graph --all
* 6afeb15 (origin/master, origin/HEAD) Update copyright
| * cfd46ab (HEAD, master) Add documentation for new feature
| * a730d77 Whitespace
|/
* d254865 Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]

Изглежда има такива. От този изглед няма как да разберете това, но къмитът 6afeb15 е създаден в действителност от Perforce клиент. От гледна точка на Git, този къмит изглежда като нормален Git къмит и точно това е идеята. Нека да видим как Perforce сървърът управлява merge къмит:

$ git merge origin/master
Auto-merging README
Merge made by the 'recursive' strategy.
 README | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Perforce: 100% (3/3) Loading commit tree into memory...
remote: Perforce: 100% (5/5) Finding child commits...
remote: Perforce: Running git fast-export...
remote: Perforce: 100% (3/3) Checking commits...
remote: Processing will continue even if connection is closed.
remote: Perforce: 100% (3/3) Copying changelists...
remote: Perforce: Submitting new Git commit objects to Perforce: 4
To https://10.0.1.254/Jam
   6afeb15..89cba2b  master -> master

Според Git нещата работят. Нека видим историята на README файла от гледната точка на Perforce с revision graph опцията на p4v:

Perforce revision графика след Git push
Фигура 146. Perforce revision графика след Git push

Ако никога преди не сте виждали такъв изглед, той може да изглежда смущаващо, но показва същите концепции както графичния инструмент на Git за разглеждане на историята. Ние гледаме историята на файла README така че дървото горе вляво показва само него както се вижда в различните клонове. Горе вдясно виждаме как се свързват различните версии на файла, а общата картина на тази графика е долу вдясно. Останалата част от картинката е за детайлите на избраната версия (2 в този случай).

Едно от нещата, които се виждат е, че графиката изглежда точно както тази в Git историята. Perforce няма именуван клон за съхранение на къмитите 1 и 2, така че е създал “anonymous” клон в директорията .git-fusion за тях. Същото ще се случи за именувани Git клонове, които не съвпадат с именувани Perforce клонове (и после можете да ги мапнете към Perforce клон с конфигурационния файл).

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

Git-Fusion обобщение

Ако имате достъп (или може да получите такъв) към вашия Perforce сървър, Git Fusion е чудесен начин да накарате Git и Perforce да си сътрудничат. Има известна работа по конфигурацията, но като цяло не е толкова сложно. Това е една от малкото секции в тази глава, в които няма да видите предупреждения за използването на пълната сила на Git. Това не означава, че Perforce ще е щастлив с всичко, което му изпратите — ако се опитате да промените история, която вече е била публикувана, Git Fusion ще откаже това, но иначе се опитва да бъде максимално близък до Git. Можете дори да използвате Git модули (въпреки, че те ще изглеждат странно за Perforce потребителите) и да сливате клонове (това ще се запише като integration от страната на Perforce).

Ако не успеете да убедите администратора на сървъра ви да инсталира Git Fusion, все пак съществува възможност да използвате двете системи заедно.

Git-p4

Git-p4 е двупосочен бридж между Git и Perforce. Той работи изцяло във вашето Git хранилище, така че не ви трябва никакъв достъп до Perforce сървър (като изключим потребителските име и парола, разбира се). Git-p4 не е толкова гъвкаво и пълно решение като Git Fusion, но ви позволява да правите повечето неща, които бихте желали, без да се налага да пипате по сървъра.

Забележка

Трябва да поставите p4 инструмента някъде в пътищата на PATH променливата, за да работите с git-p4. По времето на писането на този текст, той е наличен на http://www.perforce.com/downloads/Perforce/20-User.

Настройка

За целите на примерите, ще използваме Perforce сървъра от Git Fusion OVA както е показано по-горе, но ще заобикаляме Git Fusion сървъра и ще комуникираме директно с Perforce version control системата.

За да използваме p4 конзолния клиент (от който зависи работата на git-p4), трябва да настроим няколко променливи на обкръжението:

$ export P4PORT=10.0.1.254:1666
$ export P4USER=john
Начало

Както всичко в Git, първата команда е за клониране:

$ git p4 clone //depot/www/live www-shallow
Importing from //depot/www/live into www-shallow
Initialized empty Git repository in /private/tmp/www-shallow/.git/
Doing initial import of //depot/www/live/ from revision #head into refs/remotes/p4/master

Това създава, според терминологията на Git, “shallow” копие; само последната Perforce версия се импортира в Git; спомнете си, че Perforce не е проектиран да предоставя всяка версия на всеки потребител. Това е достатъчно да използваме Git като Perforce клиент, но за други цели не е.

След края на командата, имаме пълнофункционално Git хранилище:

$ cd myproject
$ git log --oneline --all --graph --decorate
* 70eaf78 (HEAD, p4/master, p4/HEAD, master) Initial import of //depot/www/live/ from the state at revision #head

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

$ git remote -v

Не се показват никакви отдалечени референции. Git-p4 е създал няколко референции за да пресъздаде статуса на сървъра и за git log те изглеждат като отдалечени такива. Обаче не се управляват от Git и не можете да публикувате в тях.

Работен процес

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

$ git log --oneline --all --graph --decorate
* 018467c (HEAD, master) Change page title
* c0fb617 Update link
* 70eaf78 (p4/master, p4/HEAD) Initial import of //depot/www/live/ from the state at revision #head

Имаме два нови къмита и искаме да ги изпратим на Perforce сървъра. Проверяваме за междувременно въведени промени от някой друг:

$ git p4 sync
git p4 sync
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12142 (100%)
$ git log --oneline --all --graph --decorate
* 75cd059 (p4/master, p4/HEAD) Update copyright
| * 018467c (HEAD, master) Change page title
| * c0fb617 Update link
|/
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

Изглежда, че такива има и клоновете master и p4/master са разделени. Системата за клонове на Perforce няма нищо общо с тази на Git, така че изпращането на merge къмити няма никакъв смислъл. Git-p4 препоръчва да пребазирате къмитите си и дори предлага кратък път за това:

$ git p4 rebase
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
No changes to import!
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
Applying: Update link
Applying: Change page title
 index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Вероятно и сами сте забелязали от резултата, но все пак да кажем, че git p4 rebase е съкратен вариант на git p4 sync последвана от git rebase p4/master. Командата дори е една идея по-умна, особено при работа с много клонове, но засега това ни е достатъчно.

Сега историята ни отново е линейна и сме готови да изпратим промените си към Perforce. Командата git p4 submit ще се опита да създаде нова Perforce ревизия за всеки Git къмит между p4/master и master. Изпълнението ѝ ще ни отвори текстовия редактор със следното съдържание:

# A Perforce Change Specification.
#
#  Change:      The change number. 'new' on a new changelist.
#  Date:        The date this specification was last modified.
#  Client:      The client on which the changelist was created.  Read-only.
#  User:        The user who created the changelist.
#  Status:      Either 'pending' or 'submitted'. Read-only.
#  Type:        Either 'public' or 'restricted'. Default is 'public'.
#  Description: Comments about the changelist.  Required.
#  Jobs:        What opened jobs are to be closed by this changelist.
#               You may delete jobs from this list.  (New changelists only.)
#  Files:       What opened files from the default changelist are to be added
#               to this changelist.  You may delete files from this list.
#               (New changelists only.)

Change:  new

Client:  john_bens-mbp_8487

User: john

Status:  new

Description:
   Update link

Files:
   //depot/www/live/index.html   # edit


######## git author ben@straub.cc does not match your p4 account.
######## Use option --preserve-user to modify authorship.
######## Variable git-p4.skipUserNameCheck hides this message.
######## everything below this line is just the diff #######
--- //depot/www/live/index.html  2014-08-31 18:26:05.000000000 0000
+++ /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/index.html   2014-08-31 18:26:05.000000000 0000
@@ -60,7 +60,7 @@
 </td>
 <td valign=top>
 Source and documentation for
-<a href="http://www.perforce.com/jam/jam.html">
+<a href="jam.html">
 Jam/MR</a>,
 a software build tool.
 </td>

Това в голямата си част е същото съдържание, което ще видите при изпълнение на p4 submit, с изключение на нещата в края, които git-p4 услужливо е вмъкнала. Git-p4 се опитва да съблюдава вашите Git и Perforce настройки индивидуално, когато трябва да даде име за къмит или changeset, но в някои случаи ще искате да го редактирате. Например, ако Git къмитът, който импортирате, е бил създаден от потребител, който няма Perforce потребителски акаунт, може все още да искате полученият changeset да изглежда така сякаш е създаден от този потребител (не от вас).

Git-p4 удобно е вмъкнал съобщението от Git къмита като съдържание за този Perforce changeset, така че просто трябва да запишем и да излезем, два пъти (по веднъж за всеки къмит). Резултатът на екрана ще изглежда подобно:

$ git p4 submit
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Synchronizing p4 checkout...
... - file(s) up-to-date.
Applying dbac45b Update link
//depot/www/live/index.html#4 - opened for edit
Change 12143 created with 1 open file(s).
Submitting change 12143.
Locking 1 files ...
edit //depot/www/live/index.html#5
Change 12143 submitted.
Applying 905ec6a Change page title
//depot/www/live/index.html#5 - opened for edit
Change 12144 created with 1 open file(s).
Submitting change 12144.
Locking 1 files ...
edit //depot/www/live/index.html#6
Change 12144 submitted.
All commits applied!
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12144 (100%)
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
$ git log --oneline --all --graph --decorate
* 775a46f (HEAD, p4/master, p4/HEAD, master) Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

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

По време на този процес всеки Git къмит се превръща в Perforce changeset; ако искате да ги съчетаете в единичен changeset, може да го направите с интерактивно пребазиране преди да изпълните git p4 submit. Също така отбележете, че SHA-1 хешовете на всички изпратени като changesets къмити са променени, това е защото git-p4 добавя по ред в края на всеки конвертиран къмит:

$ git log -1
commit 775a46f630d8b46535fc9983cf3ebe6b9aa53145
Author: John Doe <john@example.com>
Date:   Sun Aug 31 10:31:44 2014 -0800

    Change page title

    [git-p4: depot-paths = "//depot/www/live/": change = 12144]

Какво се случва, ако опитате да изпратите merge къмит? Нека опитаме. Ето ситуацията, в която попадаме:

$ git log --oneline --all --graph --decorate
* 3be6fd8 (HEAD, master) Correct email address
*   1dcbf21 Merge remote-tracking branch 'p4/master'
|\
| * c4689fc (p4/master, p4/HEAD) Grammar fix
* | cbacd0a Table borders: yes please
* | b4959b6 Trademark
|/
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

Историята на Git и Perforce се разделя след 775a46f. Git страната има два къмита, след това merge къмит с Perforce head, и след това още един къмит. Ще се опитаме да изпратим всичко това върху единичен changeset от Perforce страната. Какво се случва, ако опитаме да публикуваме:

$ git p4 submit -n
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would synchronize p4 checkout in /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would apply
  b4959b6 Trademark
  cbacd0a Table borders: yes please
  3be6fd8 Correct email address

Флагът -n е съкратено за --dry-run и се опитва да докладва какво би се случило, ако submit командата беше реална. В този случай, изглежда че ще се опитаме да създадем три Perforce changeset-а, съответстващи на трите non-merge къмита, които все още не съществуват на Perforce сървъра. Звучи като това, което искаме — нека изпълним реалната команда:

$ git p4 submit
[…]
$ git log --oneline --all --graph --decorate
* dadbd89 (HEAD, p4/master, p4/HEAD, master) Correct email address
* 1b79a80 Table borders: yes please
* 0097235 Trademark
* c4689fc Grammar fix
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

Историята ни стана линейна, точно както ако бяхме пребазирали преди публикуването (което в действителност е реалният резултат). Това значи, че може да се чувствате свободни да създавате, работите, изтривате и сливате клонове локално в Git без да се тревожите, че историята ви ще стане несъвместима с Perforce. Всичко, което можете да пребазирате, може да се изпрати към Perforce сървъра.

Клонове

Ако вашият Perforce проект има много клонове, не е страшно, git-p4 може да се справи с тази ситуация по начин, по който сте свикнали с Git. Нека кажем, че вашето Perforce депо изглежда така:

//depot
  └── project
      ├── main
      └── dev

И нека приемем, че имате dev клон с view spec подобен на това:

//depot/project/main/... //depot/project/dev/...

Git-p4 може автоматично да установи ситуацията и да направи правилното:

$ git p4 clone --detect-branches //depot/project@all
Importing from //depot/project@all into project
Initialized empty Git repository in /private/tmp/project/.git/
Importing revision 20 (50%)
    Importing new branch project/dev

    Resuming with change 20
Importing revision 22 (100%)
Updated branches: main dev
$ cd project; git log --oneline --all --graph --decorate
* eae77ae (HEAD, p4/master, p4/HEAD, master) main
| * 10d55fb (p4/project/dev) dev
| * a43cfae Populate //depot/project/main/... //depot/project/dev/....
|/
* 2b83451 Project init

Отбележете “@all” указателя в пътя на депото, той казва на git-p4 да клонира не само най-новия changeset за това под-дърво, но също и всички changesets, които някога са докосвали тези пътища. Това е близко до концепцията на Git за клон, но може да отнеме време при проекти с голяма история.

Флагът --detect-branches казва на git-p4 да използва branch-спецификациите на Perforce за да мапва клоновете с Git референции. Ако тези мапинги не са налични на Perforce сървъра (което е съвсем валиден начин за използване на Perforce), можете вие да кажете на git-p4 какви са мапингите и ще получите същия резултат:

$ git init project
Initialized empty Git repository in /tmp/project/.git/
$ cd project
$ git config git-p4.branchList main:dev
$ git clone --detect-branches //depot/project@all .

Ако за конфигурационната променлива git-p4.branchList зададем стойност main:dev, това казва на git-p4, че “main” и “dev” са клонове и че вторият произлиза от първия.

Ако сега изпълним git checkout -b dev p4/project/dev и направим няколко къмита, git-p4 е достатъчно умен да определи кой е правилния клон при изпълнение на git p4 submit. За съжаление, git-p4 не може да смесва shallow копия и множество клонове, ако имате голям проект и искате да работите по повече от един клон, ще трябва да изпълните git p4 clone по веднъж за всеки клон, в който искате да публикувате.

За създаването и интеграцията на клонове ще трябва да използвате Perforce клиент. Git-p4 може да синхронизира и публикува към съществуващи клонове и само с един линеен changeset в даден момент. Ако слеете два клона в Git и се опитате да публикувате новия changeset, всичко което ще бъде записано ще множество файлови промени, метаданните с информация за това кои клонове са участвали в интеграцията, ще бъдат загубени.

Git и Perforce - обобщение

Инструментът Git-p4 прави възможно използването на Git работен процес при работа с Perforce сървър и е добър в това. Обаче, важно е да се помни, че Perforce обработва сорс кода и Git се използва само за локална работа. Просто бъдете наистина внимателни при споделяне на Git къмити, ако имате отдалечена референция ползвана и от други хора, не публикувайте никакви къмити, които преди това не са изпратени към Perforce сървъра.

Ако искате свободно да смесвате използването на Perforce и Git като клиенти за сорс контрол и ако успеете да убедите системния ви администратор да го инсталира, то Git Fusion превръща Git в първокласен version-control клиент за Perforce сървър.

scroll-to-top