Bazaar uzupełnieniem SVN-a

Subversion – jeden model pracy
=======================

Najbardziej popularnym systemem wersjonowania jest [Subversion](http://subversion.tigris.org/). Został on zaprojektowany do pracy w sposób scentralizowany. Działa to tak, że gdzieś na serwerze znajduje się centralne repozytorium z którego programista pobiera kopię roboczą na swój lokalny komputer. Dokonuje modyfikacji plików po czym zatwierdza dokonane zmiany w repozytorium centralnym. Taki model pracy ma swoje zalety jednak zdarzają się sytuacje kiedy przydała by się lokalna wersja repozytorium. Poczciwy SVN nie daje jednak możliwości pracy w sposób rozproszony co jest jego wielką słabością w stosunku do np. [Gita](http://git-scm.com/) czy [Bazaar-a](http://bazaar.canonical.com/en/), które dzięki swojej elastycznej architekturze oferują wiele przeróżnych sposobów organizacji pracy z repozytorium.

Modele pracy są jednym z wielu argumentów, dla których warto rozważyć przesiadkę na inny system kontroli wersji. Polecam zapoznać się z artykułami [Why Switch to Bazaar? – Any Workflow](http://doc.bazaar.canonical.com/migration/en/why-switch-to-bazaar.html#any-workflow) albo [Workflows – Bazaar Version Control](http://wiki.bazaar.canonical.com/Workflows) oraz [Why Git is Better than X – Any Workflow](http://whygitisbetterthanx.com/#any-workflow).

Ja pracuję już kilka lat z Subversion i większość firm z którymi współpracuję lub też współpracowałem też używa SVN. Dlatego decyzja o zmianie systemu wersjonowania najczęściej nie leży w mojej gestii.

Co mogę zatem zrobić? Zarówno Bazaar ([bzr-svn](https://launchpad.net/bzr-svn)) jak i Git ([git-svn](http://www.kernel.org/pub/software/scm/git/docs/git-svn.html)) mają narzędzia umożliwiające współpracę z Subversion.

Po co łączyć Subversion z Bazaarem lub Gitem
==================================

Model pracy rozproszonej oferuje kilka możliwości stanowiących doskonałe uzupełnienie modelu scentralizowanego.

* Słabe połączenie internetowe albo jego brak nie jest problemem ponieważ wszelkie zmiany dokonywane są lokalnie. Połączenie z serwerem centralnym potrzebne jest jedynie raz na jakiś czas.
* Niezależne repozytorium, odseparowane i nienarażone na zmiany czynione przez innych użytkowników daje możliwość eksperymentowania. Dokonane zmiany w przypadku powodzenia można później scalić z główną gałęzią lub też usunąć nie zaśmiecając historii zmian centralnego repozytorium.
* Można ustalić procedurę zgodnie z którą zmiany dokonane w projekcie przez developera trafiają najpierw do osoby nadzorującej projekt, która je sprawdza, testuje i dopiero po akceptacji pcha da SVN-a. W ten sposób do centralnego repozytorium trafią jedynie zmiany stabilne.
* Warto też wspomnieć o takim prozaicznym powodzie jak brak upierdliwych katalogów „.svn” w katalogu roboczym projektu – małe a cieszy :D.

Praca z Bazaarem jako uzupełnieniem Subversion
======================================

Importowanie repozytorium
————————–

Ja wybrałem Bazaar z uwagi na moje większe doświadcznie z tym systemem kontroli wersji.

Dla tych, którzy nie mieli jeszcze styczności z Bazaarem proponuję na start prosty tutorial [Bazaar in five minutes](http://doc.bazaar.canonical.com/bzr.2.1/en/mini-tutorial/index.html). Jeśli chodzi o naukę to Bazaar ma tę przewagę nad Gite, że większość jego komend jest identyczna z tymi znanymi z SVN.

Jeśli nie mamy jeszcze zainstalowanego Bazaara (bzr) oraz wtyczki umożliwiającej współpracę z Subversion bzr-svn to w przypadku Ubuntu wystarczy jedynie pobrać wyżej wspomniane z repozytorium i zainstalować.

sudo apt-get install bzr bzr-svn

W przytoczonym wyżej tutorialu nowe repozytorium tworzy się poprzez komendę bzr init. W tym przypadku nie ma takiej potrzeby. Aby „zaimportować” repozytorium SVN do Bazaara należy utworzyć tzw. brancha. Zakładam że znajdujemy się w katalogu roboczym, w którym trzymamy projekty (workspace). Wywołujemy komendę bzr branch i podajemy jako parametry url repozytorium svn oraz nazwę katalogu jaki zostanie stworzony na naszym komputerze i w którym zostanie utworzone lokalne repozytorium Bazaara.

bzr branch svn+http://svn-repository.example.com/project/trunk ./project

Od tego momentu można już pracować z Bazaarem. Dostępna jest pełna historia zmian i jakby się uprzeć to można zapomnieć o SVN-ie 😉 (choć do właściwego importu SVN do Bazaar służy komenda bzr svn-import) Dostępne mamy zarówno narzędzia konsolowe, jak też całą kolekcję GUI z których niejako firmowym jest multoplatformowy [Bazaar Explorer](http://doc.bazaar.canonical.com/explorer/en/). Fanom Eclipse polecam z kolei [plugin do obsługi Bazaara](https://launchpad.net/bzr-eclipse), który niczym nie ustępuje swojemu odpowiednikowi dla Subversion.

Pracujemy w ten sposób, że wszelkie zmiany w plikach zatwierdzamy (commit) do lokalnego repozytorium. Kiedy zmiany w projekcie przyjmują już stabilną postać jesteśmy gotowi do zarejestrowania wprowadzonych poprawek w centralnym repozytorium SVN. Zanim to jednak zrobimy należy najpierw sprawdzić czy od czasu ostatniego pobrania zmian z SVN coś się nie zmieniło. W końcu najczęściej nie pracujemy sami. Wchodzimy do katalogu naszego projektu i wywołujemy:

Warto sprawdzić czy coś się zmieniło?
————————————

bzr pull svn+http://svn-repository.example.com/project/trunk

Jeśli pracujesz w zespole, a poprawka którą miałeś zrobić nie była mała to najprawdopodobniej otrzymasz komunikat, że w repozytorium dokonano zmian. Trzeba je będzie sprawdzić, scalić z lokalnymi zmianami i ponownie zatwierdzić. Bazaar podpowie Ci co masz zrobić.

bzr: ERROR: These branches have diverged. Use the missing command to see how.
Use the merge command to reconcile them.

Scalanie różnic jest proste
————————-

Jeśli inni developerzy nie edytowali tych samych plików co ty to zazwyczaj wystarczy:

Do podejrzenia różnic

bzr missing

Do scalenia zmian

bzr merge

Następnie przydałoby się sprawdzić jakie pliki różnią się względem lokalnego repozytorium po scaleniu ze zmianami dokonanymi w repozytorium centralnym

bzr status

Po ich zatwierdzeniu

bzr commit -m 'Treść komunikatu'

Zostaje już tylko wysłanie wszystkiego do svn.
——————————————–

bzr push svn+http://svn-repository.example.com/project/trunk

UWAGA! append_revisions_only error
———————————–

W tym momencie najprawdopodobniej wyskoczy Ci mały Zonk i otrzymasz taki niemiły komunikat

Using saved push location: svn+http://svn-repository.example.com/project/trunk
bzr: ERROR: Operation denied because it would change the mainline history. Set the append_revisions_only setting to False on branch "svn+http://svn-repository.example.com/project/trunk" to allow the mainline to change.

Niestety komunikat Bazaar nie jest w tym wypadku wyczerpujący gdyż nic nie mówi na temat tego gdzie ustawić append_revisions_only na false.

W Ubuntu wyedytuj plik ~/.bazaar/subversion.conf i dodaj wspomnianą dyrektywę pod adresem repozytorium SVN

[4ef181b9-d188-42c4-ae88-58g6dfg8760b]
locations = svn+http://svn-repository.example.com/project/trunk
append_revisions_only = False

To powinno załatwić sprawę.

Uwaga konflikt!
————–

To był optymistyczny scenariusz. W mniej pomyślnej sytuacji dokonasz zmian w tym samym pliku w którym ktoś inny grzebał i to najprawdopodobniej w tych samych liniach. Automat tego nie rozwiąże. Załóżmy, że chodzi o plik „foo.py”. Pojawia się konflikt a problematyczny plik doznaje cudownego rozmnożenia.

1. foo.py.BASE – plik w wersji pozbawionej zmian powodujących konflikt (w svn foo.py.rOLDREV)
2. foo.py.OTHER – plik ze zmianami dokonanymi przez innego użytkownika (w svn foo.py.rNEWREV)
3. foo.py.THIS – plik z moimi lokalnymi zmianami (w svn foo.py.mine)
4. foo.py – zawiera to samo co foo.py.THIS

Kilka słów od Kdiff3 – bez niego moje życie było by o wiele bardziej skomplikowane
——————————————————————————

Każdy ma swoje ulubione narzędzie do scalania różnic. Ja preferuję Kdiff3 – dostępny na Linuksa i Windowsa. Mimo, że zarówno Eclipse jak i Bazaar Explorer mają swoje narzędzia do porównywania zmian ja staram się użyć Kdiff3 którego interfejs jest mi dobrze znany.

Edycja konfliktu przy użyciu Kdiff3 wymaga wywołania w konsoli komendy

kdiff3 foo.py.BASE foo.py.OTHER foo.py.THIS -o foo.py

Oczywiście nie wszyscy lubią konsolę. Aby np. Bazaar Explorer korzystał z Kdiff3 należy wyedytować plik ~/.bazaar/bazaar.conf i w sekcji DEFAULT dodać.

external_merge = kdiff3 %b %o %t -o %r

Jako ciekawostkę kierując się zawodową uprzejmością podaję też sposób wywołania tortoisemerge.

external_merge = tortoisemerge /base:%b /theirs:%o /mine:%t /merged:%r

Kdiff3 można też zintegrować z Eclipse jednak opcję taką znalazłem jedynie dla Subversion. Należy otworzyć Preferencje->Team->Diff/Merge. W sekcji „Conflict Resolution Program” wybrać opcję „External” i wskazać gdzie leży Kdiff3 (u mnie /usr/bin/kdiff3). W polu parameters należy podać

${base} ${yours} ${theirs}  -o ${merged}

Rozwiązaniem konfliktu należy się pochwalić
—————————————–

Po ręcznym rozstrzygnięciu konfliktów należy Bazaar poinformować o tym – analogicznie jak w svn.

bzr resolve foo.py

Komenda „resolve” usuwa przy okazji pliki foo.py.BASE, foo.py.OTHER oraz foo.py.THIS. Teraz wystarczy już tylko zatwierdzić zmiany (commit) oraz pchnąć do centralnego repozytorium (push).

Dobra rada na koniec
================

Bazaar czy Git to systemy kontroli wersji stworzone do pracy rozproszonej oraz dopracowane pod kątem łączenia osobnych repozytoriów. Mają pod tym względem znaczną przewagę nad Subversion. Model pracy będący mixem korzystającym z rozproszonego repozytorium np. Bazaar oraz centralnego SVN ma swoje zalety jednak stanowi dodatkowy narzut pracy związany z koniecznością dublowania pewnych czynności. Nie należy na siłę wdrażać rozwiązań, które nie są efektywne lub wręcz zbędne. Dlatego jeśli w firmie używającej Subversion stosuje się dopracowane procedury zatwierdzania zmian w repozytorium, nie ma problemów z połączeniem, a modyfikacje w projekcie nie mają charakteru rewolucji będącej jednym wielkim eksperymentem wymagającym tworzenie piaskownicy, być może funkcjonalność Subversion jest w zupełności wystarczająca.

PHP 5.3 – wywołanie array_multisort za pośrednictwem call_user_func_array

Chcąc dostosować swój kod do wersji PHP 5.3, tak aby wykonywał się bezproblemowo z uwzględnieniem błędów STRICT natrafiłem na taki drobny szczegół. Swego czasu dodałem do manuala funkcji array_multisort na php.net taką drobną [notkę zawierającą implementację funkcji ułatwiającej sortowanie wyników zapytań sql-owych] (http://usphp.com/manual/en/function.array-multisort.php#87268).

Chcąc ją dzisiaj użyć dostałem na twarz warning-a „Parameter 2 to array_multisort() expected to be a reference, value given”.

Załóżmy, że miałem następujące dane.

$data = array(
	0 => array (
		'id' => '1',
		'name' => 'Rodzaje',
	),
	1 =>
	array (
		'id' => '2',
		'name' => 'Kategorie',
	),
	2 =>
	array (
		'id' => '3',
		'name' => 'Statusy',
	),
	3 =>
	array (
		'id' => '4',
		'name' => 'Kolory',
	),
);

Aby posortować tę tablicę według kolumny „name” wywołałem wspomnianą funkcję w następujący sposób

sortDbResult($data, 'name', SORT_ASC, SORT_STRING);

Przyjrzałem się problematycznej linii i zobaczyłem, że wywołuję funkcję array_multisort przy pomocy funkcji call_user_func_array, do której parametry przekazuję za pośrednictwem tablicy.

call_user_func_array('array_multisort', $_params);

W moim przypadku zmienna $_params miała następującą zawartość

array(
0 => &array (
	0 => 'Rodzaje',
	1 => 'Kategorie',
	2 => 'Statusy',
	3 => 'Kolory',
),
1 => 2,
2 => 4,
3 => array(
	0 => array (
		'id' => '1',
		'name' => 'Rodzaje',
	),
	1 =>
	array (
		'id' => '2',
		'name' => 'Kategorie',
	),
	2 =>
	array (
		'id' => '3',
		'name' => 'Statusy',
	),
	3 =>
	array (
		'id' => '4',
		'name' => 'Kolory',
	),
));

Pomyślałem sobie o co chodzi z tym błędem w końcu drugi parametr funkcji array_multisort to w moim przypadku wartość stałej SORT_ASC czyli 2. Przeprowadziłem mały eksperyment i wywołałem funkcję array_multisort bezpośrednio.

$data_1 = array (
	0 => 'Rodzaje',
	1 => 'Kategorie',
	2 => 'Statusy',
	3 => 'Kolory',
);
 
$data_2 = array(
	0 => array (
		'id' => '1',
		'name' => 'Rodzaje',
	),
	1 =>
	array (
		'id' => '2',
		'name' => 'Kategorie',
	),
	2 =>
	array (
		'id' => '3',
		'name' => 'Statusy',
	),
	3 =>
	array (
		'id' => '4',
		'name' => 'Kolory',
	),
);
array_multisort($data_1, SORT_ASC, SORT_STRING, $data_2);
 
var_dump($data_2);

Zadziałało. Tablica $_data2 została posortowana prawidłowo.

call_user_func_array('array_multisort',  array(&$data_1, SORT_ASC, SORT_STRING, &$data_2));

Powyższy kod również wykonał się nie zwracając żadnych komunikatów błędów.

$params = array(&$data_1, SORT_ASC, SORT_STRING, &$data_2);
call_user_func_array('array_multisort',  $params);

Wykonanie powyższego kodu kończy się już niestety warningiem. Aby się go pozbyć trzeba użyć małego obejścia i przypisać wartości stałych do zmiennych a następnie przekazać je do tablicy przy pomocy referencji.

$o = SORT_ASC;
$v = SORT_STRING;
$params = array(&$data_1, &$o, &$v, &$data_2);
call_user_func_array('array_multisort', $params);

W tym wypadku nie działa ukrywanie błędów. Funkcja array_multisort po prostu nie zadziała prawidłowo.

Poprawiona funkcja sortDbResult wygląda następująco:

/**
 * Sort DB result
 *
 * @param array $data Result of sql query as associative array
 *
 * Rest of parameters are optional
 * [, string $name  [, mixed $name or $order  [, mixed $name or $mode]]]
 * $name string - column name i database table
 * $order integer - sorting direction ascending (SORT_ASC) or descending (SORT_DESC)
 * $mode integer - sorting mode (SORT_REGULAR, SORT_STRING, SORT_NUMERIC)
 *
 * 
 *  $i,
 *                      'first_name' => sprintf('first_name_%s', rand(1, 9)),
 *                      'last_name' => sprintf('last_name_%s', rand(1, 9)),
 *                      'date' => date('Y-m-d', rand(0, time()))
 *                  );
 * }
 * $data = sortDbResult($data, 'date', SORT_DESC, SORT_NUMERIC, 'id');
 * var_dump($data);
 * $data = sortDbResult($data, 'last_name', SORT_ASC, SORT_STRING, 'first_name', SORT_ASC, SORT_STRING);    
 * var_dump($data);
 * ?>
 * 
 *
 * @return array $data - Sorted data
 */
function sortDbResult(array $data /*$name, $order, $mode*/) {
	$_argList = func_get_args();
	$_data = array_shift($_argList);
	if (empty($_data)) {
		return $_data;
	}
	$_max = count($_argList);
	$_params = array();
	$_cols = array();
	$_rules = array();
	for ($_i = 0; $_i < $_max; $_i += 3)
	{
		$_name = (string) $_argList[$_i];
		if (!in_array($_name, array_keys(current($_data)))) {
			continue;
		}
		if (!isset($_argList[($_i + 1)]) || is_string($_argList[($_i + 1)])) {
			$_order = SORT_ASC;
			$_mode = SORT_REGULAR;
			$_i -= 2;
		} else if (3 > $_argList[($_i + 1)]) {
			$_order = SORT_ASC;
			$_mode = $_argList[($_i + 1)];
			$_i--;
		} else {
			$_order = $_argList[($_i + 1)] == SORT_ASC ? SORT_ASC : SORT_DESC;
			if (!isset($_argList[($_i + 2)]) || is_string($_argList[($_i + 2)])) {
				$_mode = SORT_REGULAR;
				$_i--;
			} else {
				$_mode = $_argList[($_i + 2)];
			}
		}
		$_mode = $_mode != SORT_NUMERIC
					? $_argList[($_i + 2)] != SORT_STRING ? SORT_REGULAR : SORT_STRING
					: SORT_NUMERIC;
		// references below required from PHP 5.3
		$_rules[] = array('name' => $_name, 'order' => &$_order, 'mode' => &$_mode);
	}
	foreach ($_data as $_k => $_row) {
		foreach ($_rules as $_rule) {
			if (!isset($_cols[$_rule['name']])) {
				$_cols[$_rule['name']] = array();
				$_params[] = &$_cols[$_rule['name']];
				// references below required from PHP 5.3
				$_params[] = &$_rule['order'];
				$_params[] = &$_rule['mode'];
			}
			$_cols[$_rule['name']][$_k] = $_row[$_rule['name']];
		}
	}
	$_params[] = &$_data;
	call_user_func_array('array_multisort', $_params);
	return $_data;
}

Posted in PHP by Zbigniew Heintze · Tag: