Może się komuś przypadkiem zdarzyć, jako i mi się zdarzyło, że na początku swojej przygody z Subversion pomyśli:
“Jedno repozytorium dla wszystkich projektów – dobry pomysł!” (one repo to hold them all!)
Może się też zdarzyć, że po kilku miesiącach, gdy liczba projektów urośnie do setki, a rozmiar repozytorium do kilkuset MB, że nasunie się myśl nieco inna:
“Osobne repozytorium dla każdego projektu – dam się za to pokroić!”
I wreszcie:
“Jak cofnąć czas i zmienić tą debilną decyzję sprzed kilku miesięcy, nie tracąc jednocześnie historii zmian w poszczególnych projektach?”
Ten post jest właśnie lekiem na ową bolączkę. Korzystając z luźnej świąteczno-noworocznej atmosfery postanowiłem zrobić wreszcie porządek w swoim domowym REPO, siłując się jednocześnie z Powershell i SvnBook. We wspomnianej książce jest nawet przedstawiona dokładnie taka sytuacja (rozdział “Repository Maintenance”, punkt “Filtering Repository History”), ale mimo to trochę czasu zabrało zrobienie mi wszystkiego jak trzeba.
Input
Sytuacja przed zmianami wygląda następująco:
Jedno repozytorium, kilka projektów wewnątrz. Struktura każdego projektu jest podobna: katalog główny z nazwą projektu oraz podkatalogi “branches”, “tags” i “trunk”. Standard. (na szczęście nie przyszło mi do głowy zrobienie wspólnych katalogów branches/tags/trunk dla wszystkich projektów, ale to byłoby chyba już zbyt głupie; wówczas za karę sam dobrowolnie zapisałbym się do chóru “Słowiki”).
Output
Chcemy osiągnąć osobne repozytorium dla każdego projektu z jednym warunkiem: katalogami głównymi w nowym repo powinny być branches/tags/trunk, a nie folder z nazwą projektu. Jakoś będziemy musieli się go pozbyć. Demonstracja:
Processing…
0) “Zdumpowanie” starego repozytorium.
Na samym początku musimy utworzyć kopię starego repozytorium, na której będziemy pracować. W naszym przypadku owa kopia powinna mieć postać pliku “dump”, który stworzymy za pomocą komendy:
svnadmin dump multiprojects > multiprojects_dump.dat
1) Utworzenie nowego repozytorium
Pierwszą (i najproszą) czynnością do wykonania jest utworzenie nowego repozytorium. Robimy to jedną linijką:
svnadmin create project_1
2) Odfiltrowanie niepotrzebnych informacji z repozytorium
W tym kroku posłużymy się innym narzędziem z pakietu Subversion – svndumpfilter. Pozwala ono na utworzenie drugiego “dumpa” zawierającego tylko te katalogi, na których nam zależy, odfiltrowując całą resztę. Skorzystamy z dodatkowych parametrów narzędzia, które zapewnią nam spójność w numerach wersji nowego repo. Efektem wykonania poniższej linijki będzie nowy plik “project_1.dat” z historią tylko tego jednego projektu.
svndumpfilter include project_1 –drop-empty-revs –renumber-revs –quiet < multiprojects_dump.dat > project_1.dat
3) Usunięcie z historii katalogu głównego
Kolejna czynność to pozbycie się irytującego “nadkatalogu” “project_1” z nowego repozytorium. W tym kroku pobawimy się PowerShell. Dokładniej omówię poniższy skrypt później, teraz tylko przedstawię sposób jego wywołania:
powershell .\remove_main_dir.ps1 project_1
4) Załadowanie nowego “dumpa” do repozytorium
Po modyfikacjach plik project_1.dat zawiera to co nas interesuje. Wtłoczenie tego do utworzonego w pierwszym kroku repozytorium to kwestia jednej linijki:
svnadmin load –ignore-uuid -q project_1 < project_1.dat
Zwracam uwagę na parametr –ignore-uuid. Nakazuje on narzędziu svnadmin wykasować identyfikator repozytorium zawarty w pliku dump i nadać nowy guid właśnie tworzonemu repo. Bez tego parametru programy klienckie mogą się pogubić i wyświetlać nie te logi których oczekujemy czy historie pomieszane z różnych repozytoriów. Generalnie – bez tego parametru popadniemy w kałabanię, a czort karty będzie rozdawał.
5) Usunięcie pliku tymczasowego
Plik z historią dla nowego repozytorium nie jest już nam potrzebny, w każdej chwili możemy go wygenerować z samego repo. A więc:
del project_1.dat
Cały skrypt new_repo.bat
Cały sparametryzowany skrypt new_repo.bat (parametr 1 to nazwa pełnego “dumpa”, czyli multiprojects_dump.dat, a parametr 2 to nazwa katalogu, dla którego chcemy utworzyć nowe repozytorium – project_1) wygląda tak:
svnadmin create %2
svndumpfilter include %2 –drop-empty-revs –renumber-revs –quiet < %1 > %2.dat
powershell .\remove_main_dir.ps1 %2
svnadmin load –ignore-uuid -q %2 < %2.dat
del %2.dat
… a po dokładne omówienie zastosowanych narzędzi i ich parametrów odsyłam do SvnBook.
Zabawy z Powershell
Decydując się na Powershell miałem nadzieję, że wszystko uda się zawrzeć w jednym skrypcie. Niestety okazało się, że dużo praktyczniejszym rozwiązaniem jest skorzystanie ze starego dobrego cmd.exe i zastosowanie Powershell tylko w jednym, najbardziej skomplikowanym miejscu. Głównym tego powodem jest niemożność dostarczenia procesowi wywoływanemu przez Powershell pliku zewnętrznego jako Input. Mówiąc prosto – przykładowa linijka svndumpfilter include project_1 < project_1.dat wywali błąd, że operator “<” jest niezaimplementowany i zarezerwowany do przyszłego użycia. Ta sama sytuacja da się zaobserwować także w najnowszym CTP. Jedyne wyjście to wczytanie całej zawartości pliku do pamięci i przekazanie go przez “pipe” do kolejnej instrukcji, co w przypadku kilkuset MB wydaje się antypraktyczne: gc project_1.dat | svndumpfilter…. A może po prostu ja coś robię źle?
Pewnym problemem było też poprawne wczytanie zawartości pliku dump do obróbki oraz ponowne go zapisanie. Standardowe instrukcje Get-Content i Set-Content nie dają takiej kontroli nad tekstem jak statyczne metody z klasy System.IO.File, stąd użycie tych ostatnich. Pliki dump SVN są bardzo czułe na odpowiednie zakończenie linii (LF vs CRLF), więc zdanie się na zapis z poziomu cmdletów Powershell okazało się wybitnie nieskuteczne.
Oto cały skrypt remove_main_dir.ps1 usuwający główny katalog o nazwie podanej w parametrze z pliku “dump”:
param($dirname)
$encoding = [system.text.encoding]::getEncoding(1252)
$pattern = [string]::format(“Node-path: {0}[^/].*?PROPS-END”, $dirname)
$inputContent = [system.io.file]::readAllText(“${dirname}.dat”, $encoding)
$targetContent = [regex]::replace($inputContent, $pattern, [string]::empty, [system.text.regularexpressions.regexOptions]::Singleline)
$targetContent = $targetContent.Replace([string]::format(“: {0}/”, $dirname), “: “)
[system.io.file]::writeAllText(“${dirname}.dat”, $targetContent, $encoding)
Jusadż
Mając w jednym katalogu pliki: multiprojects_dump.dat (dump całego repozytorium), batch new_repo.bat oraz skrypt Powershell remove_main_dir.ps1 uzyskamy nowe repozytorium dla projektu project_1 wywołując komendę:
new_repo.bat multiprojects_dump.dat project_1
I git, to by było na tyle tymże razem.
To ma być skrypt? Toż to horror! Czy goście co tego powershella projektowali widzieli w ogóle jakiś skrypt? Podejrzewam że w Perlu to samo by się dało zapisac w 15 znakach ascii, a przede wszystkim dało by się zrozumieć.
Całkiem możliwe, że to nie wina “gości od powershella” tylko moja; doświadczenia w pisaniu skryptów nie mam żadnego. Dodatkowo tak jak pisałem: gdyby nie specjalnie wymogi formatu plików dump to można by wykorzystać get-content i set-content, zamiast uciekac sie do klas .netowych.
Ale tak czy siak przyznaje – efekt sliczny nie jest:).
“rg” ma racje. w dobrym skrypcie to da sie zapisac w 1-2 znakach wszystko i to tak aby bylo zrozumiale. jak ja sobie stworze jakis nowy skryptowy jezyk to ten problem zostanie rozwiazana za pomoca specjalnej komendy jednoznakowej
;-)
Bardzo fajne rozwiązanie. Let’s give a sh** on da script style ;)
Tytułowy vss, to oczywiście Visual SourceSafe – najbardziej znienawidzony na świecie system kontroli
nie wiedziałem ze Maciuś to taki mózgowiec.