CVS w Praktyce

Dariusz Cieślak

Dariusz Cieślak, absolwent informatyki na Politechnice Warszawskiej. Zainteresowania zawodowe obejmują programowanie obiektowe i techniki zapewnienia jakości w projektach informatycznych. Zwolennik Extreme Programming, programowania sterowanego testami w szczególności. Od ponad roku tworzy w firmie KF Studio system do zarządzania firmą korzystając z praktyk XP.

CVS (Concurrent Version System) to darmowa (Open Source) implementacja narzędzia do zarządzania kodem źródłowym. System kontroli wersji to bardzo ważne narzędzie w "przyborniku" każdego szanującego się programisty. Pozwala wygodnie zorganizować pracę w zespole, ale także programiście pracującemu w pojedynkę przyniesie dużo korzyści.

Oprogramowanie klienckie CVS

CVS jest zbudowane w architekturze klient - serwer. Dzięki takiej konstrukcji dorobił się wielu implementacji interfejsów klienta.

W artykule skupię się na oprogramowaniu klienckim działającym z linii poleceń. Pomimo, że jest ono trudniejsze w użyciu, znacznie lepiej nadaje się do automatyzacji. Jako narzędzie integracji zastosuję make, które wspaniale nadaje się do organizacji złożonych projektów.

Oczywiście oprócz interfejsu z linii poleceń CVS pozwala używać dużo przyjaźniejszego (dla początkującego użytkownika) interfejsu graficznego. Powstało wiele implementacji klienckich CVS dostępnych na różne platformy więc możemy np. efektywnie współpracować z grafikiem, który używa Mac-a bez przegrywania sobie plików mailem. CVS prawidłowo obsługuje zmiany końca linii dla plików tekstowych uwzględniając system klienta. Można zdefiniować, które pliki mają nie podlegać konwersji (Rysunek 1).


Rysunek 1. CVS - obsługa plików binarnych (MacCVS)

W artykule wspomnę o kilku z nich. Na platformie Windows polecam TortoiseCVS. Integruje się on z systemem i "koloruje" ikonki plików w zależności od stanu pliku (np. ikonki plików zmodyfikowanych i nie potwierdzonych pokazuje na czerwono). Jest dość intuicyjny i wygodny w użyciu.


Rysunek 2. Integracja Tortoise CVS z Windows

Podstawowe pojęcia

Na początku warto poznać terminologię jaką posługuje się CVS. Podaję również terminy angielskie by poprawnie interpretować komunikaty z CVS-u.

Repozytorium (ang. repository) - miejsce składowania danych dla systemu kontroli wersji. Zawiera informację o historii wszystkich projektów oraz informacje administracyjne jak np. informacje o utworzonych tagach.

Projekt (ang. project) - katalog w repozytorium mający swoją unikalną nazwę.

Lokalna kopia (ang. working copy) - katalog z plikami projektu które są bezpośrednio edytowane przez programistę. Dla jednego repozytorium może być wiele kopii roboczych (np. po jednej u każdego z programistów). Na podstawie lokalnej kopii można zatwierdzić zmiany i zapisać je do repozytorium (ang. commit, checkin) . Analogicznie lokalną kopię można tworzyć (ang. checkout) lub uaktualniać (ang. update) na podstawie repozytorium.

Wersja (ang. revision) - stan pliku / projektu. Nazwy wersji są generowane automatycznie i składają się z liczb oddzielonych kropkami. Znacznie bardziej praktycznie jest jednak używanie nazw wersji symbolicznych (ang. tag) - o nich opowiem w dalszej części artykułu.

Znacznik (ang. tag) jest symboliczną nazwą wersji. Zwykły znacznik opisuje moment w życiu projektu. Używany jest jako punkt odniesienia przy adresowaniu stanu projektu, także jest użyteczny przy łączeniu kilku wersji systemu.

Specjalnym (i bardzo użytecznym) rodzajem znacznika jest rozgałęzienie (ang. branch). O ile tag opisuje jeden moment w historii pliku, to rozgałęzienie opisuje ciąg zmian w pliku. Rozgałęzienia znakomicie nadają się do organizacji pracy wielu programistów.

Studium przypadku

Najlepiej CVS jest poznać na przykładzie. Poniżej pokażę kilka przykładów począwszy od najprostszej konfiguracji zespołu (jeden programista) aż do realizacji etapu kontroli jakości w dużym zespole programistów. Oczywiście techniki zastosowane w jednym przypadku mogą być przeniesione do innych - konfigurację systemu będę rozbudowywał poprzez dokładanie nowej funkcjonalności.

Jeden programista

Na początku poczynimy pewne założenia: CVS jest już zainstalowany i w repozytorium znajduje się już projekt o nazwie "mojafirma". Ponadto użytkownik ma skonfigurowane konto na maszynie z CVS-em, prawidłowo ustawioną zmienną środowiskową $CVSROOT i jest zalogowany do CVS-u (o możliwych opcjach konfiguracji opowiem w dalszej części artykułu).

Biorąc pod uwagę wygodę używania CVS-u warto jeszcze skonfigurować klienta cvs zapisując w pliku .cvsrc (w katalogu domowym) kilka przydatnych flag (Listing 1).

Listing 1. Zawartość pliku ~/.cvsrc
   cvs -q
   update -Pd
   checkout -P

Opcja -q zmniejszy nieco szum informacyjny generowany podczas operacji w CVS-ie. -P spowoduje kasowanie pustych katalogów (ang. prune empty directories), natomiast -d: ściąganie nowo powstałych katalogów. Po więcej możliwości konfiguracji odsyłam do dokumentacji CVS-u. Na rysunku 3 pokazano także ekran konfiguracji dla wizualnego klienta WinCVS dla Windows.


Rysunek 3. Konfiguracja klienta Win CVS

Najpierw więc tworzymy lokalną kopię projektu "mojafirma":

   $ cvs co mojafirma


Rysunek 4. Operacja checkout w Tortoise CVS z Windows

Tu co jest skrótem od "checkout". (Na rysunku 4 pokazana ta sama operacja w TortoiseCVS dla Windows, natomiast Rysunek 5 pokazuje SmartCVS zaimplementowanego w Javie) W katalogu roboczym pojawi się podkatalog mojafirma, który zawiera aktualną wersję projektu.


Rysunek 5. Klient SmartCVS (Java)

W przypadku jednego programisty przyjmujemy założenie, że nie stosujemy rozgałęzień, kod w repozytorium jest kodem działającym (lub przynajmniej kompilującym się).

W przypadku tak prostego modelu poruszamy się jedynie po jednej osi czasowej (nie ma rozgałęzień). Do identyfikacji poszczególnych wersji wystarczy więc data z godziną. Kiedy chcemy zobaczyć projekt, który istniał dwa dni temu można napisać:

   $ cvs up -D '2004-05-02 12:32'

Opcja up -D oznacza przeniesienie się w czasie do stanu z podanego dnia i godziny.

Dobrym zwyczajem, który warto sobie wyrobić jest wykonywanie aktualizacji lokalej kopii (update) przed zatwierdzeniem zmian (commit). Jeśli o tym zapomnimy CVS i tak nam o tym przypomni kiedy ktoś inny zmodyfikuje repozytorium.

Historia zmian (changelog)


Rysunek 6. Klient gcvs (GTK)

Bardzo użyteczną praktyką jest prowadzenie w projekcie historii zmian. Oglądając taki dziennik można szybko sprawdzić, co ostatnio działo się w projekcie, kto wprowadzał zmiany.

CVS pozwala na dodanie komentarza do zmodyfikowanych plików w fazie "commit". Po wydaniu polecenia cvs ci otwierany jest edytor, gdzie programista widzi listę zmienionych plików i może podać komentarz do swoich zmian. Czytelne komentarze stają się bardzo ważne wraz ze wzrostem ilości osób w zespole.

Aby wygodnie korzystać z takiego pliku z historią zmian dodamy kilka informacji: czas wprowadzenia zmiany oraz osobę, która ją wprowadziła. Na Listingu 2 pokazano fragment pliku Makefile z rzeczywistego projektu. Dodatkowo logujemy nazwę rozgałęzienia (o rozgałęzieniach będzie informacją w dalszej części artykułu).

Listing 2. Fragment Makefile - logowanie zmian
   cvs_commit:
      read -p "enter log text: " description;\
      test -z "$$description" && exit 1;\
      now=date +%Y-%m-%d %H:%M';\
      branch_name=`cvs st Makefile |\
         awk '/branch/{print "(" $$3 ") "};\
      info="$$now $$branch_name$$USER: $$description";\
      cp changelog.txt /tmp/changelog.txt;\
      echo "$$info" >> /tmp/changelog.txt;\
      sort /tmp/changelog.txt > changelog.txt;\
      rm /tmp/changelog.txt;\
      cvs ci -m "$$info";

Jeszcze jedno ważne zastosowanie ma plik changelog. Pozwala on łatwo odnaleźć w przeszłości interesujący nas stan projektu i go sobie obejrzeć nawet jeśli regularnie nie tworzymy tagów oznaczających kolejne wersje. Znajdujemy po prostu w changelog odpowiadającą nam datę i cofamy się w czasie za pomocą wcześniej omawianej operacji cvs up -D data').

Testowanie w stylu Extreme Programming (XP)


Rysunek 7. Klient dla Mac OS

O XP pisaliśmy już niejednokrotnie na naszych łamach. Podstawową zasadą w XP jest automatyczne testowanie projektu tak często jak to możliwe. W repozytorium nie powinien znaleźć się niedziałający kod.

Jeśli dysponujemy już przygotowanymi testami (załóżmy, że testy uruchamia plik test.sh i zwraca status poprzez kod błędu), to możemy przystąpić do konstrukcji wygodnego interfejsu dla programisty spełniające powyższe założenia.

Rozszerzmy więc plik Makefile o kilka nowych celi (targets) - Listing 3. Po wprowadzeniu zmian należy je potwierdzać w CVS-ie poprzez komendę make ci. Spowoduje to wykonanie wszystkich testów i jeśli wykonały się one poprawnie, to wykonywany jest commit. Zauważmy, że korzystamy z wcześniej zdefiniowanego celu cvs commit (Listing 2).

Listing 3. Testowanie przed fazą commit w Makefile
   ci: ready_for_commit cvs_commit
   ready_for_commit:
      sh test.sh

Jeszcze jednym użytym celem, który nie został wcześniej zdefiniowany jest ready_for_commit (Listing 4). Sprawdza on korzystając ze statusu plików w projekcie (up -u) czy nie ma plików nieaktualnych (P, U, bądź zawierających konflikt (C). Jeśli któryś z plików nie spełnia wymagań, to jest wypisywany komunikat o błędzie.

Listing 4. Sprawdzenie aktualności lokalnej kopii względem repozytorium
   ready_for_commit:
      @cvs -n up 2> /dev/null | awk '\
         /^P|^U|^\?|^C/ { print "update file:", $$0; result = 1; }\
         END { exit(result); }'

Aktualna kopia projektu

W pewnych sytuacjach korzystnie jest mieć na pewnym serwerze w określonym katalogu aktualną kopię projektu.

Może to się przydać, kiedy chcemy umożliwić programistom dodawanie uruchamiania powtarzalnych zadań w prosty sposób. Typowo do tego zadania w świecie unixowym używany jest cron. Ale co ma zrobić programista używający systemu Windows, który wyłącza swój komputer jak wychodzi z pracy?

Rozwiązanie problemu: po pierwsze tworzymy projekt w CVS-ie o nazwie cron.

   $ mkdir cron
   $ cd cron
   $ cvs import -m "" cron darek initial
   $ cd ..
   $ cvs co cron

Po objaśnienie poszczególnych opcji do polecenia cvs import odsyłam do dokumentacji. Dodajmy zadanie (założymy, że jedno zadanie to jeden plik) do projektu:

   $ cd cron
   $ echo "echo hello, world!" > hello_world.daily
   $ cvs add hello_world.daily
   $ cvs -m "plik testowy" ci

Przyjmijmy konwencję, że pliki będą skryptami shellowymi Bourne'a i rozszerzenia będą decydować o tym jak często mają być wykonywane. Tu skrypt będzie wykonywany raz dziennie (daily). Zalogujmy się na serwer cvs. Tu edytujemy tablicę komend dla demona cron poprzez polecenie:

   $ crontab -e

Na listingu 5 pokazano utworzone zadanie uruchamiane codziennie o piątej rano:

Listing 5. Plik crontab służący do uruchamiania zadań
   # minuta godzina dzień miesiąc dzień tygodnia
   # codziennie o piątej rano (daily)
   00 05 * * * cd /var/script/cron && for f in *.daily;\
      do sh $a; done
   # w każdy poniedziałek (weekly)
   00 05 * * 1 cd /var/script/cron && for f in *.weekly;\
      do sh $a; done

To zadanie wykonuje każdy plik w katalogu /var/script/cron z rozszerzeniem daily i uruchamia go. Jedyną rzeczą, którą należy teraz się zająć jest aktualizacja katalogu /var/script/cron po każdym commit wykonanym przez użytkowników.

Za wykonywanie akcji po commit odpowiada plik loginfo umieszczony w CVSROOT. CVSROOT jest specjalnym projektem w CVS-ie zawierającym pliki konfiguracyjne CVS-u. Modyfikacja plików z tego katalogu następuje tak samo jak każdego innego projektu. CVS trzyma historię zmian swoich plików konfiguracyjnych.

Na listingu 6 znajduje się plik loginfo, który po każdym commit aktualizuje skrypty uruchamiane potem przez mechanizm cron.

Listing 6. Plik CVSROOT/loginfo
   ^cron$ (sleep 1; cd /var/script;\
      test -d cron || cvs co cron;\
      cd cron && cvs up -PAd;) &

Inne zastosowania to tworzenie strony web. Chcemy, by nasze lokalne zmiany były widoczne po commit (nowe wersje plików powinny pokazać się natychmiast w katalogu /var/www). Obsługę tworzymy analogicznie.

Wielu programistów

Rozważmy teraz scenariusz gdzie jest kilku programistów pracujących nad tym samym projektem. Każdemu z nich przydzielamy konto w CVS-ie, każdy pracuje na swojej kopii roboczej projektu.

W uproszczonym modelu możemy założyć, że nie są tworzone rozgałęzienia, a zmiany są zatwierdzane bezpośrednio do głównej gałęzi. Taka polityka zmniejsza problemy z integracją kodu, ponieważ jest on integrowany "na bieżąco".

Typowym problemem w takiej konfiguracji jest brak możliwości izolacji "eksperymentalnego" kodu od kodu produkcyjnego. Jeśli założymy, że kod eksperymentalny będzie trzymany tylko w lokalnej kopii (żeby nie "psuć" głównej gałęzi), to pozbawiamy się dobrego systemy kopii zapasowych jakim jest przecież CVS.

W bardziej rozbudowanym modelu przewidujemy tworzenie rozgałęzień.

Proste użycie rozgałęzień

Jak nadmieniłem wcześniej rozgałęzienia mogą być używane do izolacji kodu eksperymentalnego od kodu produkcyjnego. Po pierwsze tworzymy rozgałęzienie (-b oznacza branch) dla wersji eksperymentalnej:

   $ cvs tag -b experimental

Przełączamy się na nie:

   $ cvs up -r experimental

Dobrym zwyczajem jest tagowanie ważnych punktów w projekcie. Utworzenie rozgałęzienia jest takim punktem (dalej pokaże jak można proces dodawania tagów zautomatyzować):

   $ cvs tag experimental-begin

Dodatkowe tagi w przypadku CVS-u będą pełnić ważną rolę w momencie wielokrotnego łączenia (merge) pomiędzy rozgałęzieniami. Powrót do głównej gałęzi można zrealizować poprzez:

   $ cvs up -A

Zmiany zatwierdzane na rozgałęzieniu nie są widoczne w innych rozgałęzieniach. Aby przenieść zmiany przeprowadzone w experimental do kodu produkcyjnego należy wykonać:

   $ cvs up -j experimental

Opcja -j oznacza "join". Teraz została zmodyfikowana lokalna kopia projektu (dołączono zmiany przeprowadzone w rozgałęzieniu experimental). Po takim połączeniu należy jeszcze kod zatwierdzić z głównym rozgałęzieniu:

   $ cvs ci

Dodajemy etap kontroli jakości (przeglądy)


Rysunek 8. Kto, kiedy zmienił którą linię (annotate - TkCVS)


Rysunek 9. Wizualna prezentacja rozgałęzień w TkCVS

Wraz z poznawaniem możliwości CVS-u wzrastają nasze możliwości zapanowania na chaosem jakim jest projekt. Aby zwiększyć jakość tworzonego systemu wprowadzimy etap przeglądu kodu (code review). Oczywiście można by przegląd wykonywać na lokalnej kopii, ale w takim przypadku programiści musieli by znajdować się w jednej lokalizacji (co nie zawsze jest wykonalne) lub przesyłać sobie wersję projektu podlegającą przeglądowi (co nie zawsze jest wygodne).

Znacznie lepszym rozwiązaniem jest zastosowanie rozgałęzień (branch). Dla każdej planowanej funkcjonalności tworzymy rozgałęzienie (Listing 7). Wykonanie make bcreate pyta się o nazwę nowego rozgałęzienia, zapisuje informację o utworzeniu rozgałęzieniu w pliku changelog.txt w głównej gałęzi projektu i przechodzi do rozgałęzienia. Na rysunku 9 pokazano wizualizację rozgałęzień w TkCVS.

Warto zauważyć, że forsujemy tu zasadę, iż rozgałęzienia można tworzyć tylko z głównej gałęzi projektu (cel not in_branch). Takie zasady możemy kształtować dowolnie w zależności od potrzeb projektu korzystając z możliwości jakie daje interfejs linii poleceń.

Listing 7. Tworzenie rozgałęzienia
   bcreate: not_in_branch
      @tail changelog.txt;\
      read -p "name of new branch [a-z_0-9]+: " branch_name;\
      now=date +%Y-%m-%d %H:%M';\
      info="$$now $$USER: ($$branch_name) branch starts from $$now";\
      echo "$$info" >> changelog.txt;\
      cvs ci -m "$$info" changelog.txt;\
      cvs tag -b "$$branch_name" > /dev/null;\
      cvs up -r "$$branch_name";\
      cvs tag -F "$$branch_name-start" > /dev/null;\
      cvs st Makefile;\

Listing 8. Sprawdzenie, czy nie jesteśmy na rozgałęzieniu
   not_in_branch: 
      cvs st Makefile | awk /branch/{exit 1}'

Na tak utworzonym rozgałęzieniu można swobodnie zatwierdzać zmiany. W każdej chwili można stwierdzić w jakim rozgałęzieniu się projekt znajduje poprzez cel st w Makefile (jest to skrót od słowa status) - Listing 9. Zauważmy, że korzystamy tu z CVS-u do pobrania informacji o jednym pliku - zakładamy, że wszystkie inne pliki w projekcie będą miały ten sam status (co, nawiasem mówiąc, w CVS-ie nie zawsze będzie prawdziwe).

Listing 9. Pokazanie statusu projektu (nazwy rozgałęzienia)
   st:
      cvs st Makefile

Innym ciekawym chwytem jest dodanie tagu na początku rozgałęzienia o nazwie składającej się z nazwy rozgałęzienia i postfiksu -start. Taki tag jest przydatny w momencie dołączania rozgałęzienia do głównej gałęzi, przydaje się też przy oglądaniu zmian.

Kiedy programista dokona już zmian na rozgałęzieniu i chce przekazać kod do kontroli jakości (do weryfikacji) tworzy nowy tag o nazwie składającej się z nazwy rozgałęzienia i postfiksa -verify (Listing 10).

Listing 10. Zgłoszenie rozgałęzienia do weryfikacji
   bverify: blist
      @read -p "name of existing branch to verify [a-z_0-9]+: "\
         branch_name;\
      cvs tag -Fr "$$branch_name" "$$branch_name-verify" > /dev/null;\

Istnienie takiego tagu dla osoby przeprowadzającej przegląd jest sygnałem, że można się tym rozgałęzieniem zająć. Teraz musimy przygotować jeszcze listę istniejących tagów w projekcie (Listing 11). Oglądając taką listę możemy szybko stwierdzić które rozgałęzienia jeszcze są w fazie opracowania a które czekają na etap kontroli jakości. Podobną informację można uzyskać wygodniej korzystając z wizualnych nakładek na CVS (Rysunek 10).


Rysunek 10. Lista nazw symbolicznych wersji w SmartCVS (Java)

Listing 11. Pokazanie listy rozgałęzień
   blist:
      @cvs log -h Makefile |\
         sed 's/:.*//g' |\
         sed -n '/\t/p' | sort

Kiedy kierownik projektu (lub ktoś, kto jest wyznaczony do przeprowadzania kontroli jakości) stwierdzi, że jedno z rozgałęzień zostało zgłoszone do kontroli jakości może przełączyć się na nie poprzez polecenie:

   $ cvs up -r nazwa-rozgałęzienia

W aktualnym katalogu dostanie on obraz projektu pochodzący z ostatniego zapisu. Może interaktywnie sprawdzić działanie rozgałęzienia.

Jednak testowanie interaktywne nie sprawdzi jakości kodu i możliwych pułapek np. dotyczących bezpieczeństwa, które pojawiły się w tym rozgałęzieniu. Cel bdiff zawarty na listingu 12 to wygodna (choć tekstowa) przeglądarka zmian. Zmiany są pokazywane w formacie diff -u, co pozwala zobaczyć także kontekst zmienionych linii kodu.

Listing 12. Pokazanie zmian w wybranym rozgałęzieniu
   bdiff: blist
      @read -p "name of existing branch [a-z_0-9]+: "\
         branch_name;\
      cvs diff -uNB -r "$$branch_name-start" -r "$$branch_name" \
      2> /dev/null | less

Dużą zaletą jest to, że nie musimy wyszukiwać w kodzie zmian tylko mamy podane je "na talerzu", co zwiększa efektywność przeglądu.

Oczywiście można tu użyć narzędzi graficznych prezentujących zmiany (np. tkdiff) lub wprowadzić inne procedury przeglądów - zależnie od potrzeb danego projektu.

Kiedy osoba sprawdzająca napotka błędy w rozgałęzieniu powinna usunąć tag -verify:

   $ cvs tag -d branch_name-verify

i wysłać mailem uwagi do programisty danego rozgałęzienia. Uważam, że jest to lepsza metoda niż poprawianie samemu błędów, ponieważ w ten sposób szkolimy programistów. Cykl można powtarzać do skutku (brak wykrytych błędów na etapie przeglądu).

Kiedy zdecydujemy się dołączyć zmiany dokonane na rozgałęzieniu do głównej gałęzi, wykonujemy następujące polecenia:

   $ cvs up -A
   $ make bmerge

Opcja -A powoduje przełączenie na główną gałąź (zwaną HEAD), natomiast cel bmerge (Listing 13) łączy zmiany z rozgałęzienia do HEAD. Podobną operację można przeprowadzić w środowisku graficznym (Na rys. 11 pokazano menu TortoiseCVS, gdzie jest dostępna operacja merge).


Rysunek 11. Operacja łączenia (merge) w TortoiseCVS (Windows)

Listing 13. Łączenie zmian z rozgałęzienia do HEAD
   bmerge: not_in_branch blist
      @read -p "name of existing branch\
         to merge [a-z_0-9]+: " branch_name;\
      cvs up -j "$$branch_name-start" -j "$$branch_name";\

Uwaga: połączenie zmian występuje tylko w lokalnej kopii. Po rozwiązaniu konfliktów i ponownym sprawdzeniu należy jeszcze zatwierdzić zmiany w HEAD (Listing 14). Jeśli wszystko jest OK, można usunąć rozgałęzienie z wszystkimi tagami:

   $ cvs tag -d nazwa_rozgalezienia-verify
   $ cvs tag -d nazwa_rozgalezienia-start
   $ cvs tag -d nazwa_rozgalezienia

Listing 14. Potwierdzanie zmian po połączeniu
   bmerge_confirm: not_in_branch ready_for_commit blist
      @read -p "name of existing branch to confirm\
         [a-z_0-9]+: " branch_name;\
      now=date +%Y-%m-%d %H:%M';\
      info="$$now $$USER: commiting branch\
         $$branch_name into HEAD";\
      echo "$$info" >> changelog.txt;\
      cvs ci -m "$$info";\

Podsumowanie

CVS jest bardzo elastycznym narzędziem, które odgrywa znaczącą rolę w ruchu Open Source i jest często wybierane przy projektach komercyjnych. Pomimo, że ma swoje wady (np. zmiana nazwy pliku powoduje zgubienie historii zmian tego pliku) a po piętach depczą już nowe systemy kontroli wersji (jak np. opisywany na łamach Software subversion) CVS nadal pozostaje niekwestionowanym liderem jeśli chodzi o systemy kontroli wersji.

m4_include(_SIDEFILE)