rss

Zarządzanie repozytorium kodu w projektach Agile

Wszyscy wiemy jak prowadzić projekt Agile, ale kiedy przychodzi co do czego pojawiają się problemy, których żaden „coach” nie wytłumaczył wcześniej. Jak poukładać repozytorium kodu? Jak zapewnić, że w chwili wydania iteracji kod nie będzie zawierał niedokończonych historyjek?

Michał Paluchowski, 16 sierpnia 2009

Opiszę strategię zarządzania repozytorium, którą wypracowaliśmy w trakcie dotychczasowych projektów. Nie jest to ani strategia najlepsza ani jedyna, a w chwili publikacji tego tekstu pewnie nawet nie jest aktualna, bo cały czas uczymy się na błędach.

Żeby skorzystać na poniższym artykule przyda się podstawowa wiedza na temat zasad Agile, którą dla usystematyzowania krótko przypomnę. Typowy projekt zarządzany w oparciu o reguły Agile jest opisany w formie historyjek użytkownika, z których każda (mniej lub bardziej szczegółowo) opisuje pojedynczą funkcję, jaką ma posiadać program, sposób jej wykorzystania i oczekiwany wynik. Kilkanaście albo kilkadziesiąt takich historyjek planowanych jest do wykonania w ramach jednej „iteracji” – ściśle ograniczonego czasowo przedziału, na koniec którego musi zostać wydana pośrednia wersja programu, obejmująca tyle historyjek ile udało się ukończyć i żadnego kodu pochodzącego z historyjek, które nie zostały napisane, przetestowane oraz zrefaktoryzowane.

Układ repozytorium kodu źródłowego

Do zarządzania kodem na ogół korzystamy z Subversion. Zdecydowaliśmy więc, że zastosujemy standardowy (czyt. „większość ludzi tak to robi”) układ folderów, a więc trzy „nieśmiertelne” foldery:

/branches/
/tags/
/trunk/

W folderze „trunk” mamy główną gałąź kodu. Gdyby do zespołu przyszedł człowiek z zewnątrz i poprosił o „aktualny” kod, dostałby właśnie zawartość trunka. „branches” zawiera gałęzie, a więc kopie kodu z trunka, na których pracujemy. „tags” z kolei będzie zawierał kopie kodu z trunka lub gałęzi, stworzone w określonych momentach. Na przykład tagi oznaczają stan kodu w chwili zakończenia iteracji.

Większość naszych projektów to systemy składające się z wielu odosobnionych modułów, w których zmiany dokonane w jednym mają wpływ na działanie innych. W tej sytuacji tworzymy osobny katalog dla każdego modułu wewnątrz katalogu „trunk”:

/branches/
/tags/
/trunk/modul1/
      /modul2/
      /...

Dzięki takiemu układowi możemy jednym poleceniem „svn cp” stworzyć tag lub gałąź zachowując wszystkie zależności pomiędzy modułami.

Rzadziej zdarza nam się tworzyć projekty złożone z wielu niezależnych modułów. Mamy również repozytoria, w których przechowujemy wiele niezależnych projektów. W takich sytuacjach działamy na odwrót i powyższe 3 standardowe katalogi umieszczamy wewnątrz odrębnych katalogów modułów lub projektów:

/modul1/branches/
       /tags/
       /trunk/
/projekt2/branches/
         /...

Ostateczna decyzja co do układu katalogów zależy więc przede wszystkim od istnienia lub braku zależności pomiędzy modułami tworzonego systemu. Czasem w repozytorium trzyma się także pliki nie będące częścią programu – dokumentację, schematy itp. Decyzję, w którym miejscu je umieścić podejmujemy w identyczny sposób – jeśli są powiązane z kodem, tzn. zmiana w dokumentach wymusza zmiany w kodzie i na odwrót, korzystamy z pierwszej opcji, inaczej z drugiej.

Oczywiście zdarzają się i projekty, w których tworzony jest pojedynczy program bez wyodrębnionych modułów. W takich sytuacjach powyższy dylemat znika i zostajemy przy samych „branches”, „tags” i „trunk”.

Stabilny „trunk”

Trzymamy się zasady, według której kod znajdujący się w folderze „trunk” pozostaje w każdej chwili stabilny. Nie znaczy to wcale, że nie zawiera błędów czy też broń Boże nie można go uznać za odpowiedni do oddania użytkownikom. Oznacza to jedynie, że:

Celem takiego podejścia jest mieć możliwość zakończenia iteracji przez wydanie kodu, który zawiera jedynie dokończone historyjki. Kiedy przychodzi moment demonstracji wyników iteracji nie zdarzają nam się wypowiedzi typu „ten przycisk nie działa, bo nie zdążyliśmy dokończyć tej funkcji”.

Osobna gałąź dla każdej historyjki

Miejscem, w którym odbywa się praca nad nowymi funkcjami są gałęzie. Kiedy programista zaczyna nową historyjkę, najpierw tworzy gałąź na bazie kodu z folderu „trunk”. W nazwie gałęzi umieszczamy liczbowy identyfikator historyjki (każda posiada unikalny w danym projekcie) oraz krótki opis jej zawartości, na przykład:

/branches/14-admin-usuwa-uzytkownika/...

Następnie programista pracuje w pocie czoła nad napisaniem całości kodu spełniającego wymogi historyjki i jego przetestowaniem, zarówno ręcznie jak i automatycznie. Po wykonaniu tej pracy następuje przygotowanie do połączenia tejże gałęzi z folderem „trunk”. Przechodzimy wtedy przez listę punktów kontrolnych, która zawiera co najmniej:

Lista powyższa jest zazwyczaj dłuższa, zależne od projektu. Na przykład dla projektów WAR (aplikacje webowe w Javie) dodajemy wymóg ich poprawnej instalacji na serwerach typu Tomcat.

Gdy wszystkie wymagania z listy zostaną spełnione, przeprowadzamy przegląd kodu, tzn. autor oddaje owoc swojej pracy do sprawdzenia przez innych programistów w zespole. Korzystamy do tego z systemu ReviewBoard, publikując w nim plik diff ze wszystkimi zmianami od chwili utworzenia nowej gałęzi do jej aktualnej wersji.

Członkowie zespołu przesyłają swoje uwagi do zmian w kodzie. Te są przyjmowane lub odrzucane, dyskutowane, kod jest poprawiany, ponownie testowany i oddawany do przeglądu. Dopiero kiedy nie ma żadnych dalszych komentarzy do kodu uznaje się jego przegląd za zakończony i można przystąpić do łączenia z trunkiem.

Łączenie zaczyna się od synchronizacji gałęzi ze zmianami, które od jej utworzenia zostały wprowadzone w trunku. W czasie pracy nad tą historyjką inny programista mógł zakończyć prace nad swoją. Robimy więc „merge” z folderu trunk do folderu z aktualną gałęzią. To ważny krok, którego ominięcie może spowodować wiele konfliktów w chwili łączenia kodu z gałęzi z trunkiem, a nawet zniknięcie całych partii napisanego dopiero co kodu.

Gdy gałąź jest zsychronizowana, wykonujemy ponownie operację „merge”, tym razem o typie „reintegracja” z aktualnej gałęzi do folderu „trunk”. W tej chwili nie powinny wystąpić żadne konflikty zmian. Na koniec usuwamy niepotrzebną już gałąź i procedura zaczyna się od początku z nową historyjką.

Zgodnie z zasadami Agile każda historyjka użytkownika powinna być możliwie mała, tak żeby można było ją wykonać szybko, nawet w ciągu kilku godzin. Dzięki temu każda taka gałąź ma bardzo krótki okres istnienia i stąd ilość konfliktów występujących przy łączeniu jej na powrót z trunkiem jest prawie zawsze minimalna.

Tag na koniec iteracji

Kiedy przyjdzie czas na zakończenie iteracji na ogół wszystkie zaplanowane wcześniej historyjki zostały wykonane. Nawet jeśli którejś nie zdążyliśmy skończyć, jej kod jest odizolowany w osobnej gałęzi, podczas gdy wyniki iteracji możemy przedstawić na bazie kodu z trunka. Tworzymy tag będący kopią trunka i oznaczający wynik iteracji:

/tags/release-1.0-iteration-1/...

W nazwie taga podajemy również numer wersji wydania („release”) programu, której częścią jest kończona iteracja. W ten sposób przy produkcji kolejnej wersji możemy numerować iteracje od początku.

Wydanie wersji końcowej

Według zasad Agile na wydanie jednej wersji programu składa się zestaw iteracji. Gdy więc zakończymy ostatnią iterację zaplanowaną dla aktualnie opracowywanej wersji przechodzimy do przygotowania jej wydania. Takie wydanie i oddanie „stabilnego” programu użytkownikom końcowym wymaga zdecydowanie więcej testów i dodatkowej pracy nad zapewnieniem odpowiedniej jakości.

Często jednocześnie prowadzimy też prace nad kolejną wersją programu, która ma zostać wydana z nowym zestawem wprowadzonych historyjek. Chcemy więc utrzymać „trunk” jako miejsce zawierające najbardziej aktualny, stabilny kod, a jednocześnie mieć możliwość usuwania błędów znalezionych w kodzie wydawanej wersji. Tworzymy nową gałąź „stabilizacyjną” kodu na bazie trunka:

/branches/release-1.0-maintenance/...

Kod znajdujący się w tej gałęzi przechodzi dodatkowe ręczne i automatyczne testy jakościowe, oraz akceptacyjne testy klienta (jeśli przygotowujemy program na zamówienie). Wszystkie znalezione błędy są usuwane w tej gałęzi, a później łączone z powrotem z trunkiem. W momencie kiedy jakość kodu w tej gałęzi stanie się „wystarczająco dobra”, tzn. nie wiemy o żadnych większych błędach, tworzymy z niej tag z wydaniem wersji końcowej:

/tags/release-1.0-final/...

Gałęzi „maintenance” jednakże nie usuwamy. Dopiero po oddaniu programu w ręce użytkowników, najsurowszych testerów na świecie, zaczynają pojawiać się raporty o istotnych błędach. Wszystkie usuwamy właśnie na gałęzi „release-1.0-maintenance”, z której co jakiś czas wydajemy pośrednie wersje „naprawcze”, odpowiednio tagowane:

/tags/release-1.0.1-final/...
/tags/release-1.1-final/...
/tags/release-1.2-final/...

Trunk tymczasem wykorzystywany jest przez cały czas do wprowadzania nowych, znakomitych funkcji i ulepszeń.

Zgodność z Agile

Podejście, które stosujemy jest w miarę proste i w dużej mierze zgodne z duchem Agile:

A jak wy zarządzacie repozytoriami?