Jeden z moich kolegów “po fachu” miał ostatnio problem ze swoim repozytorium SVN i zwrócił się do mnie z prośbą o pomoc w znalezieniu rozwiązania. Sprawa wyglądała następująco: na wewnętrznym dysku komputera zlokalizowana była kopia robocza [Working Copy], na której pracował i repozytorium kodu, do którego zamierzał commitować zmiany. Na dysku zewnętrznym “wymarzył sobie” za to backup tego repozytorium. Problem polegał na tym, że trzeba było w jakiś sposób ten backup zrobić oraz usprawnić jego wykonywanie na tyle, żeby kolega nie musiał “męczyć się” bardziej niż do tej pory. Sprawa była teoretycznie prosta, jednak rozwiązanie polegające na zwykłym kopiowaniu danych pomiędzy dyskami nie wchodziło w grę, ponieważ rozmiar repozytorium był rzędu gigabajtów [bardzo dużo danych binarnych]. Trzeba było zatem wymyślić lepszy, bardziej inteligentny sposób na rozprawienie się z używanym przez nas systemem kontroli wersji.

Ze względu na to, że w momencie poznawania nowego systemu / programu lubię nieco szerzej zapoznać się z jego możliwościami, w przypadku SVNa miałem świadomość istnienia tzw. “repository hooks”, czyli, w najprostszym tłumaczeniu, skryptów [angielskie słowo “hook” oznacza w wolnym, informatycznym, tłumaczeniu uchwyt na coś] które zostaną wykonane w odpowiednich momentach przetwarzania konkretnych akcji / żądań. Skrypty te uruchamiane są z podkatalogu “hooks” danego repozytorium, a domyślnie umieszczone tam pliki z rozszerzeniem “tmpl” [“template”] to “szablony” uchwytów wraz z opisami ich działania. W wersji 1.5.0 [ dokumentacja ] zostały zdefiniowane następujące rodzaje tychże:

  • start-commit
  • pre-commit
  • post-commit
  • pre-revprop-change
  • post-revprop-change
  • pre-lock
  • post-lock
  • pre-unlock
  • post-unlock
Nazwy pozwalają na łatwe określenie warunków, przy których spełnieniu zostaną wykonane, tak więc pozwolę sobie pominąć opisy, zresztą nie wszystkie dotyczą tematu tego artykułu. Ich działanie polega na tym, że wywoływany jest skrypt o odpowiedniej nazwie z parametrami, których liczba zależy od typu uruchamianego uchwytu. Aby stworzyć własny skrypt, należy, w zależności od systemu operacyjnego utworzyć jego plik, w przypadku Windowsa, będzie to plik batch [%HOOK%.bat], gdzie przez %HOOK% rozumiemy nazwę uchwytu. Aby rozwiązać problem kolegi należało użyć uchwytu “post-commit”, tak, aby dane były przekazywane do backupu zaraz po tym, jak będą już pomyślnie dodane do głównego repozytorium. W tym wypadku do skryptu zostaną przekazane następujące parametry [zgodnie z kolejnością]: ścieżka do repozytorium i numer [nazwa] dodanej właśnie rewizji.

Moim pierwszym pomysłem było wykonanie zrzutu tej rewizji do pliku i załadowanie go do backupowego repozytorium. Oczywiście, jako osoba lubiąca kontrolować każdy bit w pamięci chciałem mieć jakiś log błędów, ew. komunikatów, które pojawiłyby się podczas testowania kodu, dlatego w skrypcie “post-commit.bat” umieściłem następującą linijkę:

call %~dp0post-commit-run.bat %* > %1/hooks/post-commit.log 2>&1

Ze względu na to, że nie każdy fragment tej instrukcji może być jasny i zrozumiały, już śpieszę z wyjaśnieniami:

  • call - polecenie uruchamiające inny skrypt batch w aktualnie przetwarzanym.
  • %~dp0post-commit-run.bat - sam otworzyłem usta, jak znalazłem ten konstrukt na jednej ze stron internetowych dawno, dawno temu… ;] Znak procenta oznacza zmienną, tylda to modyfikator umożliwiający korzystanie z rozszerzonej składni zmiennych w skrypcie, litery “d” i “p” to kolejno litera dysku i ścieżka do wykonywanego pliku “wycinana” z parametru %0 uruchomionego skryptu. To wszystko sklejone z nazwą skryptu “post-commit.run.bat”.
  • %* to konstrukcja umożliwiająca doklejenie do wywoływanego skryptu wszystkich parametrów jakie zostały przekazane do skryptu aktualnie wykonywanego.
  • > przekazuje wszystkie dane, jakie zostaną wypisane na stdout [strumień wyjścia] do “czegoś” co zostanie podane jako kolejny parametr. Napisałem “czegoś”, bo nie musi być to plik. Należy zwrócić uwagę na to, że pojedynczy znak większości nadpisze w całości plik logu, jeśli chcielibyśmy do niego tylko dopisać kolejne komunikaty, to należy użyć znanego z języka C++ operatora przesunięcia bitowego “>>”.
  • %1/hooks/post-commit.log - tutaj zapisujemy wynik działania skryptu z przyrostkiem “-run” do pliku “post-commit.log” w katalogu, w którym umieściliśmy nasze skrypty.
  • 2>&1 - wszystkie dane “wypisane” na źródło stderr [strumień błędu] przekazujemy na stdout, tak, aby żaden komunikat nie pozostał niezauważony.
W powyższym skrypcie wykonywany jest inny, który obsługuje faktycznie uchwyt “post-commit”. Jego kod [plik post-commit-run.bat]:

@echo off
setlocal
set REPO %1
set BACKUP [ścieżka_do_backupowego_repozytorium]
set REV %2
set PATH=%PATH%;%DIR%
svnadmin dump -r %REV% %REPO% > %REPO%\tmp.dump
svnadmin load %BACKUP% < %REPO%\tmp.dump
del %REPO%\tmp.dump
endlocal

Wytłumaczenia wymaga tutaj jedynie linijka, w której ustawiam zmienną PATH. Dlaczego ponownie definiuję tą zmienną? Wynika to z faktu, że skrypty uchwytów SVN są uruchamiane ze względów bezpieczeństwa w “czystym” środowisku, w którym nie ma nawet zdefiniowanych podstawowych zmiennych środowiskowych [ dokumentacja ], ale dostęp do nich, oczywiście, jest. Ja, ze względu na to, że używam swojego skryptu wyłącznie na komputerze lokalnym, mogę sobie pozwolić na taki zapis. Jeśli jednak będziesz chciał zastosować zawarty tutaj kod na serwerze produkcyjnym, lub innym “ważnym” komputerze: pamiętaj, aby zdefiniować tutaj jedynie te ścieżki, które są wymagane [np. ścieżki do narzędzi svn], albo używać tylko własnych, wpisanych bezpośrednio do pliku! Inaczej dopuszczasz uruchamianie programów zawartych we wszystkich ścieżkach zmiennej PATH, co, w przypadku posiadania jakichś “nieproszonych gości” może skutkować w uruchomieniu niechcianych usług i dania im praw dostępu na poziomie standardowego shellowego skryptu! Dobrze, wystarczy tego pouczenia, możemy kontynuować nasze rozważania. ;]

Nie jest to rozwiązanie problemu, ponieważ polecenia “svnadmin load” nie można wykonać na istniejącym repozytorium, powoduje to błąd, który w spolszczonej wersji SVNa brzmi: “Plik już istnieje: system plików ‘[nazwa]‘, transakcja ‘[nazwa]‘, ścieżka ‘[ścieżka]‘“, gdzie wartości w nawiasach będą się zmieniać w zależności od konfiguracji repozytorium. Jest on wywoływany dlatego, że polecenie load próbuje dodać jeszcze raz do repozytorium plik, który już w danej ścieżce istnieje. Wydawałoby się, że w takim wypadku nie ma już innego rozwiązania niż kopiowanie, bądź każdorazowy zrzut [“dump”] całego repozytorium. I od tego miejsca rozpocznę już opisywanie rozwiązania docelowego, którego wymyślenie nie byłoby możliwe właśnie bez dogłębnej lektury listy narzędzi zawartych w konsolowym pakiecie SVN [korzystam tutaj z pakietu SlikSVN, więc jeśli nie posiadasz takiego [lub podobnego] na komputerze, szybko napraw to klikając tu: [ SlikSVN ], lub korzystając z linka na końcu artykułu].

Wykorzystamy narzędzie “svnsync” [ dokumentacja ], które, jak sama nazwa wskazuje, służy do synchronizacji repozytoriów. Svnsync posiada dwa podpolecenia, których użycie jest niezbędne do synchronizowania repozytoriów. Mam na myśli “svnsync init” i “svnsync sync”. Mają one następujące formaty wywołania i przeznaczenie:

  • svnsync init [targetUrl] [sourceUrl] - przygotowuje docelowe repozytorium pod adresem [targetUrl] na synchronizację z repozytorium źródłowym pod adresem [sourceUrl]. Ważne jest to, że MUSZĄ być to adresy URL, ścieżki lokalne nie są poprawnymi danymi dla takiego wywołania. Jeśli pracujemy z repozytorium lokalnym, należy użyć konstrukcji file:///[adres] [zauważ, że są to 3 ukośniki [forward slash]]. W przypadku parametru [sourceUrl] nie możemy użyć samego wyrażenia %1, ponieważ jest to zwykły adres bezwzględny katalogu na dysku.
  • svnsync sync [target] - kopiuje “brakujące” rewizje do docelowego repozytorium [synchronizuje je z tym] pod adresem [targetUrl] . Adres ten także musi być zgodny z wytycznymi zawartymi w opisie polecenia “init”.
W takim wypadku plik post-commit-run.bat będzie wyglądał następująco:

@echo off
setlocal
set SOURCE=file:///%1
set TARGET=[ścieżka_do_backupowego_repozytorium]
set PATH=%PATH%;%DIR%
svnsync init %TARGET% %SOURCE%
svnsync sync %TARGET%
endlocal

Tego kodu nie da się jednak wykonać, ponieważ dostaniemy w logu [post-commit.log] błąd informujący o tym, że nie możemy modyfikować atrybutów rewizji docelowego repozytorium SVN. Na szczęście rozwiązanie jest bardzo proste: należy tylko utworzyć pusty plik skryptu dla akcji pre-revprop-change, czyli “pre-revprop-change.bat”, oczywiście w podkatalogu “hooks” docelowego repozytorium. Ja w tym pliku umieściłem instrukcję która po prostu kończyła działanie skryptu, tak dla “świętego spokoju”:

@exit 0

I voila! Cieszymy się z naszych zsynchronizowanych repozytoriów. ;] Teraz przy każdym commicie odpali się skrypt, który zgodnie z założeniami zsynchronizuje oba nasze repozytoria i wypełni “backupowe” danymi, których mu brakuje, a które znajdują się w tym głównym.

Tytuł artykułu zawiera w sobie zwrot “do wielu”, więc pomimo tego, że większość czytelników na pewno już się domyśliła, w jaki sposób zmusić SVN do synchronizacji do wielu repozytoriów, napiszę te dodatkowe dwa zdania. ;] Należy dodać w pliku post-commit-run.bat kolejne linijki “svnsync init” / “svnsync sync” zmieniając oczywiście adresy docelowych [backupowych] repozytoriów. Wtedy przygotowane już do synchronizacji repozytorium główne po prostu przekopiuje dane także pod te kolejne adresy.

Moje repozytoria zawierają głównie kod, więc do tej pory nie myślałem nawet o jakimś “ambitniejszym” sposobie backupowania go poza ustawionym w cronie na serwerze skrypcie odpalającym co jakiś czas “svnadmin dump”, ale myślę, że ten tekst ułatwi kiedyś pracę także i mi. Dziękuję z tego miejsca mojemu koledze [spokojnie, zarówno ja jak i on wiemy o kogo chodzi] za ciekawy temat na wpis. ;]

Linki: [1] http://www.sliksvn.com/en/download - strona, z której można ściągnąć najnowszą wersję pakietu SlikSVN. [2] http://svnbook.red-bean.com/en/1.5/svn.ref.reposhooks.html - dokumentacja SVN opisująca wszystkie “repository hooks”. [3] http://tortoisesvn.tigris.org/ - program TortoiseSVN, moim zdaniem najlepszy pakiet do obsługi SVNa pod systemem Windows.