Wstęp do rozproszonej kontroli wersji na przykładzie Git’a

post_img

W standardowych (centralnych) systemach wersjonowania mamy zazwyczaj jedno główne repozytorium kodu, z którego korzysta cały zespół. Praca w projekcie wiąże się z ciągłym pobieraniem i wgrywaniem zmian i w związku z tym wymaga stałego połączenia z jednym centralnym serwerem. Cała historia zmian trzymana jest w centralnym repozytorium, członkowie zespołu lokalnie posiadają jedynie kopię źródeł.

Rozproszona kontrola wersji jest zorganizowana w zupełnie inny sposób. Repozytoriów jest tyle ilu członków zespołu. Każdy z nich lokalnie posiada pełne źródła projektu wraz z historią zmian. Wszystkie repozytoria są równe – żadne z nich nie jest w żaden sposób wyróżnione. To, z którego repozytorium na przykład zbudujemy wersję jest sprawą czysto umowną.

Rozpoczęcie pracy z projektem wiąże się ze stworzeniem nowej kopii projektu przez sklonowanie wybranego istniejącego repozytorium. Dalsza praca może odbywać się lokalnie. W wybranych momentach następuje scalanie zmian dokonywanych przez różnych członków zespołu.

Co zyskujemy dzięki rozproszeniu?

Przede wszystkim pracujemy lokalnie – tworzymy branche wtedy kiedy my ich potrzebujemy, nazywamy je tak jak chcemy (nie ma konfliktów nazw często spotykanych w repozytoriach centralnych), commitujemy zmiany kiedy uznamy jakąś część pracy za wykonaną. Pomimo pracy lokalnej jesteśmy cały czas pod kontrolą wersji – możemy zarządzać nawet małymi lokalnymi zmianami, które potencjalnie mogą zawierać błędy.

Zarządzamy zmianami a nie wersjami. Wbrew pozorom to bardzo duża zmiana w podejściu do zarządzania kodem – zamiast zastanawiać się jak uaktualnić jedną wersję tak, żeby była taka sama jak inna – zarządzamy zmianami – mówimy więc ‘daj mi swój zestaw zmian’. Przy takim podejściu mergowanie zmian staje się codziennością w związku z czym musi być wydajne i bezproblemowe i takie właśnie jest. Dzięki temu pośrednio zyskujemy też silne wsparcie dla nieliniowego rozwoju projektu.

Kompletny projekt wraz z historią zmian istnieje w wielu miejscach. Podczas zakładania (klonowania) projektu dostajemy pełną kopię repozytorium (wraz z historią zmian). Dzięki temu nie mamy jednego newralgicznego punktu, ale te same źródła w wielu miejscach.

Systemy rozproszonego zarządzania wersjami

Na rynku istnieje kilka systemów DVCS (Distributed Version Control System). Do głównych należą darmowe Mercurial, Bazaar, Git oraz komercyjny BitKeeper. Nie ma dużych różnic projektowych pomiędzy nimi (być może poza sposobem synchronizacji), są za to znaczące różnice w implementacji, które wpływają na wydajność i komfort pracy. Ciekawostką jest to, ze Bazaar może pracować zarówno jako repozytorium rozproszone jak i w modelu z centralnym repozytorium.

Ze względu na rosnącą popularność Git’a proponuję pouczyć się co to jest DVCS na jego przykładzie.

Git

Git jest szybkim rozproszonym systemem kontroli wersji dostępnym na licencji open source. Został zaprojektowany i pierwotnie stworzony przez Linusa Torvaldsa na potrzeby zarządzania źródłami jądra Linuxa.

Wśród głównych cech Git’a autorzy wymieniają to, że jest systemem rozproszonym, wspiera nieliniowy development, jest wydajny w dużych projektach oraz zapewnia spójność repozytorium przez zastosowanie algorytmów szyfrujących (SHA1).

Instalacja

Większość systemów linuxowych ma już gita w swoich repozytoriach pakietów (git-core). Wersję zbudowaną lub źródła można pobrać ze strony projektu (http://git-scm.com/download). Instalacja jest prosta i nie powinna przysporzyć problemów.

Po zainstalowaniu Git jest gotowy do zarządzania naszym kodem.

Jedyne co powinniśmy ustawić, to swoją nazwę użytkownika i e-mail:

git config --global user.name Michal Laguna
git config --global user.email michal.laguna@atena.pl

Utworzenie repozytorium

Po założeniu katalogu z projektem wykonujemy polecenie git init. Spowoduje to utworzenie repozytorium – w katalogu z projektem powinien powstać katalog .git.

$git init
Initialized empty Git repository in /home/atena/repo1/.git

Za pomocą polecenia git status możemy sprawdzić aktualny stan naszego repozytorium. Powinniśmy dostać odpowiedź podobną do tej:

$git status
# On branch master
nothing to commit (working directory clean)

Dodanie/zmiana plików

Pliki dodajemy i zmieniamy w repozytorium za pomocą polecenia add:

$git add.
$git status
# On branch master
# Changes to be commited:
…
# new file: test1.txt

Zatwierdzenie zmian

Wprowadzone zmiany zatwierdzamy za pomocą polecenia commit (uwaga: commit zatwierdza zmiany tylko w lokalnym repozytorium):

$git commit –m ‘Poprawa bledu Abcd1234’
1 files changed, 1 insertion(+), 0 deletion(-)
create mode 100644 test1.txt

Historia zmian

W dowolnej chwili można podejrzeć historię zmian za pomocą polecenia log:

$git log

Możemy podejrzeć również listę różnic:

$git log –p

Branchowanie i mergowanie

Stworzenie brancha jest tak proste jak wykonanie instrukcji git branch [nazwa brancha]. Żeby przełączyć się na brancha wykonujemy polecenie git checkout [nazwa brancha].

$git branch xyz123
$git checkout xyz123
Switched to branch ‘xyz123’

W dowolnej chwili możemy sprawdzić w jakim branchu aktualnie się znajdujemy (ten oznaczony gwiazdką):

$git branch
master
* xyz123

Dokonajmy teraz zmiany w naszym branchu xyz123 i dodajmy zmianę do brancha:

$git add –m ‘Zmiana w branchu’
$git commit

Powyższe dwie instrukcje możemy też zastąpić pojedynczą:

$git commit –a –m ‘Zmiana w branchu’

Po przejściu z powrotem do brancha głównego widzimy, że nie ma w nim wprowadzonych zmian:

$git checkout master
$git log

Możemy teraz dociągnąć zmiany za pomocą polecenia merge:

$git merge xyz123

Na koniec możemy podejrzeć drzewko zmian w narzędziu gitk

$gitk

I usunąć gałąź xyz123

$git branch –d xyz123

Tagowanie

Założenie taga to wykonanie polecenia Tag:

$git tag 0.1.15

Lista tagów:

$git tag -l

Następnie możemy stworzyć brancha z dowolnego taga:

$git branch stable 0.1.15

Praca w zespole

Jak już wiemy w pracy z rozproszonym systemem kontroli wersji wszystkie repozytoria są równe, a wskazanie repozytorium, z którego na przykład budujemy wersję lub uruchamiany testy jest tylko kwestią umowy.

Załóżmy, że pracujemy w dwuosobowym zespole (user1, user2). User2 tworzy swoje lokalne repozytorium przez sklonowanie repozytorium należącego do user1:

$git clone git-repo-user1 git-repo-user2
Cloning into git-repo-user2…
done.

Następnie user2 edytuje lokalnie jakieś pliki umieszcza je w swoim repozytorium:

cd git-repo-user2
git commit –a –m ‘Zmiany user2’

Gdy skończy może powiadomić usera1 o możliwości zaciągnięcia zmian – wtedy user1 może dociągnąć zmiany z repozytorium usera2 za pomocą polecenia pull:

$git pull /home/atena/git-repo-user2 master

Polecenie pull powoduje pobranie zmian ze wskazanego repozytorium i zmergowanie ich z naszym kodem (fetch + merge). Wskazane jest zacommitowanie wszystkich lokalnych zmian przed wykonaniem operacji pull – pozwoli to na bezproblemowe przeprowadzenie merge’a.

Istnieje możliwość ściągnięcia zmian z innego repozytorium bez ich mergowania – służy do tego polecenie fetch:

$git fetch /home/atena/git-repo-user2 master
$git log –p HEAD..FETCH_HEAD
$git merge FETCH_HEAD

Dla ułatwienia pracy można nadać alias zdalnemu repozytorium, z którym pracujemy:

$git remote add user2 /home/atena/git-repo-user2

Z poziomu swojego repozytorium możemy też ‘wepchać’ zmiany do repozytorium zdalnego – praca z Gitem przypomina wtedy pracę z repozytorium centralnym. Służy do tego polecenie push:

$git push user2 master

Co ważne – Git domyślnie nie pozwoli na wepchnięcie zmian do zdalnego repozytorium jeśli wskazany branch został w nim zacheckoutowany. Dostaniemy wtedy taki komunikat:

error: refusing to update checked out branch: refs/heads/master
error: By default, updating the current branch in a non-bare repository
error: is denied, because it will make the index and work tree inconsistent
error: with what you pushed, and will require 'git reset --hard' to match
error: the work tree to HEAD.

Możemy wtedy zrobić dwie rzeczy – albo w zdalnym repozytorium przejść (checkout) na inny branch lub przekształcić zdalne repozytorium w repozytorium centralne (bare), czyli takie, które zawiera jedynie katalog .git i nie zawiera żadnych zacheckoutowanych plików

git config --bool core.bare true

Rebase

Niniejszy artykuł jest jedynie wprowadzeniem do zagadnienia rozproszonego kontrolowania źródeł, ale ponieważ za przykład wzięliśmy Git’a nie sposób nie wspomnieć o jeszcze jednej bardzo wartościowej i silnej funkcjonalności którą dostarcza – rebase.

Rebase to w uproszczeniu przesunięcie miejsca utworzenia brancha. Załóżmy, że mamy w projekcie branch główny oraz jakiś dodatkowy z funkcjonalnością przygotowywaną dla konkretnego klienta. Jeśli chcemy aby branch dedykowany dla klienta zawierał wszystkie zmiany wrzucane do brancha głównego musimy dokonywać merge’y.

Git daje nam możliwość wykonania operacji rebase, która spowoduje przesunięcie miejsca utworzenia brancha:

$git checkout clientX_branch
$git rebase origin

Po takiej operacji Git zapisze wszystkie commity od momentu utworzenia brancha jako patche w katalogu .git/rebase, zaktualizuje brancha tak, żeby wskazywał aktualną wersję w branchu głównym i na koniec zaaplikuje odłożone patche. Dzięki temu historia zmian będzie serią commitów bez merge’y.

Podsumowanie

Rozproszone systemy zarządzania wersjami moim zdaniem w pewnym sensie definiują na nowo podejście do zarządzania kodem źródłowym. Skupienie się na zmianach zamiast na wersjach rozwiązało bardzo wiele problemów spotykanych w pracy z SVN’em czy CVS’em – branchowanie i mergowanie staje się standardowym elementem prowadzenia developmentu, a dzięki rozproszeniu repozytorium towarzyszy nam od pierwszej linijki kodu.

Z drugiej strony rozproszone systemy repozytorium kodu nie są żadną nowością, a moim zdaniem wciąż nie zyskały bardzo dużej popularności w projektach komercyjnych. Wydaje mi się, że z punktu widzenia firmy rozproszenie repozytorium nie jest czymś niezbędnym – najczęściej przecież i tak potrzebujemy repozytorium centralnego, z którego automatycznie budujemy aktualną wersję i na przykład puszczamy testy jednostkowe. Tworzenie oprogramowania w firmie jest zazwyczaj prowadzone w sposób bardziej usystematyzowany niż w projektach tworzonych przez otwartą społeczność. Zakłada się, że wszyscy deweloperzy wykonują przydzielone zadania i wyniki ich pracy będą musiały się znaleźć w wersji. Czasem może nawet nie chcemy, żeby szeregowy developer mógł pobrać projekt wraz z kilkuletnią historią jego rozwoju.
Pewnym problemem może być też wsparcie systemu kontroli wersji przez IDE. SVN czy CVS jest wszechobecny – większość narzędzi do projektowania i wytwarzania aplikacji bez względu na język programowania czy modelowania ma albo natywną obsługę albo przynajmniej wtyczki do obsługi najpopularniejszych systemów kontroli wersji. Niestety żaden z rozproszonych systemów kontroli wersji nie należy jeszcze do tych najpopularniejszych.

Jeśli chodzi o Git’a to jest bez wątpienia bardzo silnym narzędziem. Na dzień dzisiejszy wydaje się być dopracowanym projektem gotowym do użycia w komercyjnym projekcie. Nie jest jednak jedynym DVCS’em – moim zdaniem przed rozpoczęciem projektu warto rozpoznać konkurencję pod kątem naszych konkretnych potrzeb (np. chociażby pluginów do wybranych IDE).

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *