Subversion branch, akár Maven Release pluginnal
Technológiák: Subversion 1.6, Maven 3.0.3, Maven Release Plugin 2.0
Amennyiben mégis amellett döntünk, hogy branch-elni (, és ennek következtében merge-ölni is) fogunk, rengeteg szempontot kell figyelembe venni. Hogyan működik a branch-elés, milyen szabályokat kell és milyen best practice-eket érdemes betartani, hogyan illeszkedik a fejlesztési munkafolyamatba, stb. Bár sok helyen használják, kevés helyen láttam ezek használatát kellően (teljes körűen) leszabályozva, dokumentálva, és bevetve. Nincsenek konvenciók, nem egységes a használata, nehéz beszerezni az információt, hogy honnan kell leágazni, kallódó, már senki által nem karbantartott és ismert branch-ek vannak. Ez a poszt ismerteti az alapfogalmakat, a Subversion lehetőségeit, valamint hogyan illeszthető a branch-elés a Mavenes munkafolyamatainkba, és milyen problémába ütközhetünk, és oldhatjuk meg. Ez a poszt előkészíti azt a posztot, ami abban próbál majd segíteni, hogy felállítsunk egy szabályrendszert, megpróbál rávilágítani a lehetőségekre, előnyeikre/hátrányaikra, hogy miket kell szem előtt tartani, hogy a fejlesztési munkafolyamatunkat tökéletesítsük, gyakorlatilag legjobb gyakorlatokat ad.
Mivel azt látom, hogy jelenleg a Subversion a legelterjedtebb verziókezelő rendszer, így erről fogok írni. Bizonyos dolgok mások, bizonyos dolgok egyszerűbbek elosztott verziókezelő rendszer (pl. Git, Mercurial, stb.) esetén. Abban bízom, hogy az itt (, de inkább a következő posztban) leírt dolgok egy része azért ott is felhasználható.
Bár Subversion parancsokat parancssorból kiadni menő, azért néha szóba hozom a TortoiseSVN klienst is, ami azon szinte megbocsájthatatlan hibáján kívül, hogy a Windows Explorerbe épül, az egyik legjobb kliens. Próbáltam Subversiont használni mind NetBeans, mind Eclipse IDE-kből is, de valahogy mindig csak bajom volt vele, sokszor a galibát csak TortoiseSVN-nel tudtam rendbetenni.
A branch nem más, mint egy új fejlesztési ág, mely már független attól, melyből kinőtt, mégis közös a történelmük. A Subversionben a branch (és a tag) létrehozása is egy egyszerű másolás (copy), ebben rejlik az egyszerűsége. A mantra, amit mondogathatunk magunkba, hogy a branch-elés (technikailag) nem költséges. Nem lesz tőle nagyobb a repository, nem fog belassulni, stb. Unix hasonlattal élve, valójában egy hard link létrehozásának felel meg. Azért olcsó csak technikailag, mert az erőforrásigény csak a fejlesztési munkafolyamatba illesztésnél, projekt adminisztrációnál, és a rettegett merge-nél fog megjelenni.
A branch és merge az 1.5-ös Subversion előtt nagyon problémás. Ennek
használatát mindenképp el kell kerülni, érdemes a legfrissebb, 1.6-os
verzióval dolgozni, mind szerver, mind kliens oldalon. (Annak
megakadályozására, hogy 1.5 előtti klienssel használjuk a Subversion
szervert, ezzel veszélyeztetve a merge-höz szükséges adatokat, egy
commit hook is telepíthető.) Az 1.5-ös verzióban jelent meg ugyanis a
mergeinfo, mely nagyon hasznos a merge-ök nyomon követésénél. Az
svn:mergeinfo
gyakorlatilag egy közönséges property. Bár lehetőség van
rá, lehet kézzel is szerkeszteni, de ez meglehetősen ellenjavalt,
hagyjuk a kezelését a merge parancsra.
A témában az egyik legjobb olvasmány a Version Control with Subversion könyv, mely ingyenesen elérhető. Ez a changeset elnevezést használja, és már az elején definiálja is, ugyanis sokféleképpen szokták ezt a fogalmat használni. A könyv terminológiájában a changeset az változások halmaza (ahol a változás lehet egy állományban történt változás, de akár egy állomány törlése, vagy átmozgatása is), mely egy nevet kap. Subversionben minden commit egy külön azonosítót kap (revision), ami gyakorlatilag szintén egy changeset, de implicit névvel, hiszen a verziókövető rendszer adja. A merge legkisebb egysége a changeset.
A klasszikus használati mód a következő. Elindul a fejlesztés a
fősodron, és kiderül, hogy szükség van egy branch-re. Ekkor az svn copy
használatával (másolással) gyakorlatilag létrehozzuk az új ágat. Mivel a
főágon is folyik a fejlesztés, ezt bizonyos időközönként át kell hozni a
módosításokat. Ezt már a merge
paranccsal tudjuk elvégezni. A Subversion
automatikusan karbantartja a mergeinfo
property-t is, amibe bekerül,
hogy mely revisionök kerültek már át. Tehát egy újabb merge esetén
tudja, hogy mik kerültek már át, és amelyek nem, azokat a módosításokat
gyakorlatilag lejátssza az új ágon is. Az állományokat lokálisan
módosítja, szóval itt lehet fordítani, tesztelni, és ha minden helyes,
akkor lehet commitolni. Amennyiben vissza akarjuk a branch-ben történt
módosításokat vezetni a főágra, először mindenképpen ellenőrizzük, hogy
a főág módosításai átkerültek-e, majd a merge
-öt a --reintegrate
kapcsolóval kell kiadni. Itt a működés teljesen más, ezért kell a
--reintegrate
kapcsolót használni, ugyanis a branch-en találhatók saját
fejlesztések, valamint a főágon történt merge-ök is. Ennek a kapcsolónak
a használatával valójában összehasonlítja a két ágat, és a különbséget
próbálja a főágra rájátszani. (A merge kiadható a --dry-run
kapcsolóval
is, ekkor nem módosulnak az állományok, csak egy áttekintést kapunk,
hogy mely állományok hogyan módosulnának.) Ha ez megvan, akkor ezt is
lehet commitolni, majd az új ágat törölni lehet.
Ez a klasszikus eset, azonban fejlesztés közben ez nagyon ritkán ilyen egyszerű. Először is sokszor van conflict. Ez az, mikor a két ágon olyan változás történik, ami látszólag ellentmond egymásnak. A Subversion amúgy is híresen rosszul kezeli ezeket az elosztott verziókezelőkhöz képest. És ráadásul még ott van az előző posztban említett szemantikus ütközés is, amikor fordulni fordul ugyan a módosított projekt, azonban nem fog helyesen működni.
Különösen átnevezésekkel van gond. Ugyanis a Subversion átnevezése effektív egy copy, majd egy delete, és nem őrzi meg az információt, hogy ez valójában egy átnevezés volt. Képzeljük el, hogy egy külön ágon javítunk egy állományon, míg a főágon egy mozgatást hajtunk végre. Mikor ezt a külön ágra akarjuk merge-ölni, az eredeti állományt letörli, és az újat hozzáadja. Csak közben elveszik a külön ágon történt javítás. A könyv szerint, amíg ezen nem javítanak, óvatosan merge-öljünk átnevezést.
Amúgy a könyv a merge parancsra azt mondja, hogy jobb lenne diff-and-apply-nak hívni, mert semmi bűvészkedés nincs a háttérben, a merge összehasonít két ágat, és a különbséget a working copy-ra alkalmazza.
Ezen eszközökkel haladóbb feladatokat is el tudunk végezni. Pl. ha egy
revisionben hibás kódot commitoltunk, vissza tudjuk ezt vonni,
méghozzá a reverse merge használatával (-c
kapcsolóval). Persze a
history-ban megmarad, de legalább automatikusan megtörténik az “undo”
művelet. Ez TortoiseSVN-ben is megtalálható, bár nem utal a reverse
merge-re: Revert changes from this revision menüpont. Ebben az esetben
nem keletkezik vagy módosul a mergeinfo.
Törölt elemet is vissza lehet állítani a merge-gel, azonban javasolt
inkább a copy
parancs használata, hiszen ha egy revision-ben más is
történt a törlésen kívül, akkor az is visszajátszásra kerül, míg a copy
esetén kedvünkre tudunk válogatni.
A cherrypicking szintén egy haladó fogalom, magyar fordításban mazsolázásnak, csemegézésnek, szemezgetésnek lehetne fordítani. Verziókezelés terén ez gyakorlatilag azt jelenti, hogy én egyenként kiválogatom, hogy milyen módosításokat szeretnék merge-ölni az egyik ágról a másik ágra. Ez Subversion esetén annyit tesz, hogy egy vagy több revisionre tudom megmondani, hogy annak a módosításaival történjen a merge (elég bonyolult feltételeket, intervallumokat fogalmazhatok meg). Ez különösen fontos akkor, ha a másik ágon folyik egy hosszabb fejlesztés, de közben egy bugot is kijavítottak, amit érdemes áthozni az én ágamba is. Persze ez is eltárolásra kerül a mergeinfoban, így ha később az ágban lévő többi módosítást is merge-ölni akarjuk, akkor ezt a revisiont automatikusan átugorja. Itt azonban lehet egy probléma, méghozzá egy revision intervallum közepén lévő merge két intervallumra bontja a módosításokat. Ha ez elsőben elhal a merge conflicttal, és elhalasztjuk a conflict feloldását, a teljes merge elhal. Ez lehet, hogy későbbi Subversion verziókban javítják, de addigis két részletben kell ilyenkor merge-ölnünk.
Egy kicsit bővebben a mergeinforól. Egy normál merge esetén létrejön vagy módosul ez a property, azonban vannak esetek, mikor mégsem. Ez lehet akkor, ha a forrás és cél URL nincs egymással kapcsolatban, azaz nincs közös history-juk. Ugyanez van akkor is, ha másik repository-ból merge-ölünk. A mergeinfo-t a TortoiseSVN-nel a Properties menüponttal tudjuk megnézni, nincs rá külön menüpont, hiszen standard property. Ezt amúgy az Subversion explicit mergeinfonak nevez. Az implicit mergeinfo nem más, mint a közös history. Ez annyit jelent, ha a közös history-ban lévő módosítást akarunk merge-ölni, akkor a Subversion tudja, hogy semmit nem kell tennie, hisz a közös history miatt a módosítás mindkét ágon benne van.
A merge tehát figyelembe veszi a history-t. Képzeljünk el egy esetet,
ahol az egyik ágon törlünk egy állományt, commit, majd újra hozzáadjuk,
és commit. Ebben az esetben az első és második módosításban szereplő
állományoknak nincs közös history-ja. Ekkor a merge is törölni, majd
hozzáadni fog. Abban az esetben, ha a --ignore-ancestry
kapcsolót
használjuk, a history-t nem veszi figyelembe a merge, úgy működik, mint
egy szimpla diff. Ekkor sem keletkezik vagy módosul a mergeinfo.
Ha megvizsgáljuk a merge parancsot, három paramétert lehet átadni. Initial repository tree, hasonlítás jobb oldalának is hívják. A final repository tree, a hasonlítás bal oldalának is hívják. A working copy, ahova az összehasonlítás eredményeként előállt diff rá lesz módosítva. Ezzel a paraméterezéssel nagyon vigyázzunk, hisz olyan tree-ket is megadhatunk, aminek semmi köze nincs egymáshoz, ennek az eredménye nagy kavarodás lehet. A könyv még azt is javasolja, hogy a merge-öt mindig a branch főkönyvtárán hajtsuk végre, ne alkönyvtárakon.
A TortoiseSVN ezt úgy oldja meg, hogy mikor merge-ölni akarunk, megkérdezi, hogy mit szeretnénk.
- Merge a range of revisions
- Reintegrate a branch
- Merge two different trees
Ez alapján a fenti paraméterezések helyett csak nekünk kell választanunk, hogy melyiket szeretnénk.
Mint később látni fogjuk, a --record-only
kapcsoló nagyon hasznos lehet.
Ezzel ugyanis egy változásra azt mondhatjuk, hogy nem akarjuk, hogy a
merge figyelembe vegye. Gyakorlatilag ekkor az történik, hogy a
mergeinfo módosul, mintha a kiválasztott módosítás már be lenne
merge-ölve (konkrétan behazudjuk a merge-t). Ezért a következő merge ezt
ki fogja hagyni.
Ha branch-ekkel dolgozunk, hasznos lehet a switch
parancs használata,
mellyel a working copy-t tudjuk update-elni egy másik URL-re. Azaz
megadunk egy másik branch-et, és a working copy-nk átáll arra. Pl. jól
jöhet akkor, ha fejlesztünk, és rájövünk, hogy ez akkora módosítás, hogy
érdemes lenne branch-be tenni. Akkor létrehozzuk a branch-et,
át-switch-elünk rá, majd oda történhet a commit. Mivel ez alkönyvtárra
is működik, tudunk ún. mixed working copy-t is csinálni, ahol az egyik
könyvtár az egyik branch-et, a másik könyvtár a másik branch-et
tartalmazza. Ezzel egyrészt nagyon vagány dolgokat is meg lehet
csinálni, viszont rettenetesen be tud kavarni egy merge esetén.
Javaslom, hogy kerüljük ilyenkor a mixed working copy használatát, azaz
olyan working copy-val dolgozzunk, melynek minden eleme ugyanahhoz az
időpontbeli állapothoz tartozik.
A tag létrehozása nem sokban különbözik a branch-től, hiszen itt is egy
másolás történik, valójában egy pillanatnyi állapot (snapshot), mely egy
egyedi nevet kap. A revision is egy ilyen snapshot, azonban számmal
azonosított. Konvenció szerint a projekt alá érdemes létrehozni a trunk
,
tags
és branches
könyvtárat, tartalmuk értelemszerű. A TortoiseSVN szól
is, ha a tags
-be akarunk commit-olni. Ezt ugyan lehet, hiszen a
Subversion nem különbözteti meg a tag és branch fogalmát, azonban mégis
jó, ha betartjuk a konvenciókat.
Ezen ismeretekkel már meg lehet valósítani az előző posztban említett feature és release branch-eket.
A könyv megemlíti a vendor branches fogalmát is. Ezt akkor használhatjuk, ha egy 3rd party library-t patch-elünk, de szeretnénk mindig a módosításokat rávezetni, de a mi módosításunkat nem akarjuk kiadni (persze, ha a licence engedi). Ekkor importáljuk saját repository-ba a 3rd party library-t. Módosítjuk. Amint a 3rd party library-ból kijön egy következő verzió, merge-el vezetjük rá a változásokat, annak a repositry-jából. Így egyrészt a saját módosításaink is megmaradnak, valamint a verziókat is tudjuk emelni.
Ahhoz azonban, hogy a post-nak valami köze legyen a Java-hoz is,
belekeverem a Mavent is, pontosabban annak Release
Plugin-ját,
ami ugyanis a release:branch
céllal (goal)
remekül tud branch-elni is, nem csak release-elni, mint egy előző
posztban írtam. Ez a
cél igen jól paraméterezhető, de az alapbeállításokkal is el lehet
boldogulni. A kötelező paramétere a branchName
, amivel a branch nevét
kell megadni. Amennyiben ezt nem adjuk meg, a következő hibaüzenetet
kapjuk:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-release-plugin:2.0:branch (default-cli) on project acltuto
rial: The parameters 'branchName' for goal org.apache.maven.plugins:maven-release-plugin:2.0:branch are missing or inval
id -> [Help 1]
Adjuk meg tehát a branch nevét! Tartsuk észben, hogy ezzel a paraméterezéssel létrejön egy branch (copy a branch-es könyvtárba), melyben lévő projekt az aktuális verziószámot fogja tartalmazni, és a working copy verziószáma fog emelkedni.
mvn release:branch -DbranchName=my-branch
A következő dolgok fognak megtörténni. A paraméterek alapértelmezettek,
azaz updateBranchVersions=false
, updateWorkingCopyVersions=true
.
- Megvizsgálja, hogy nincs-e lokális módosítás, mely nem lett commitolva. Ha van, hibaüzenettel leáll (Cannot prepare the release because you have local modifications).
- Megkérdezi, hogy mi legyen a working copy új verziószáma. (Persze ezt parancssorban is meg lehet adni, ha nem interaktív módot akarunk.)
- Módosítja a
pom.xml
-ben az scm helyét, hogy a branch-re mutasson. - Commitolja a
pom.xml
-t. A commit comment szövege:[maven-release-plugin] prepare branch my-branch
- Végrehajtja a branch-et. A commit comment szövege:
[maven-release-plugin] copy for branch my-branch
- Megemeli a
pom.xml
-ben a verziószámot, és visszaállítja a scm helyét. - Commitolja a
pom.xml
-t. A commit szövege:[maven-release-plugin] prepare release my-branch
Amennyiben azonban azt akarjuk, hogy a branch-ünk verziószáma ugorjon, viszont a working copy verziószáma maradjon, a következő parancsot adjuk ki:
mvn release:branch -DbranchName=my-branch -DupdateBranchVersions=true -DupdateWorkingCopyVersions=false
Ekkor a folyamat hasonló, mint az előbb, csak a working copy új
verziószáma helyett a branch verziószámát kéri be a 2. lépéseben, és a
pom.xml
-ben is átírja a verziószámot. Ebben az esetben a 6. lépésben nem
emel verziószámot, csak az scm helyét állítja vissza.
Meg lehet adni azt is, hogy mindkét verziószám változzon, mindkét property igazra állításával.
És valójában itt jön a feketeleves. Amennyiben van két fejlesztési
águnk, és mindkettőn folyamatosan fejlesztünk, és adjuk ki a verziókat,
mindkét ágon a release során változnak a pom.xml
-ek, kizárólag az scm
url-ek és a verziószámok. Amennyiben merge-ölni akarunk, a pom.xml
-ek
conflict-olni fognak, hiszen külön vezettük a verziószámokat mindkét
ágon. Ez a conflict azonban a fejlesztési munkafolyamatunkba nem illik
bele, hiszen nem akarunk pom.xml
-ekben verziószámot szerkesztgetni, az
kizárólag release során a release plugin feladata. Azaz ennek a
merge-nek úgy kell lefutnia, hogy a pom.xml
-ekben a verziószám
változásokat ne vegye figyelembe.
Itt vethetjük be a Subversion --record-only
kapcsolóját. Nem kell mást
csinálnunk, mint azokra a revision-ökre, melyekben a pom.xml
-ben csak a
verziószám változott, lefuttatni a merge-öt a --record-only
kapcsolóval.
Így gyakorlatilag becsapom a Subversiont, a mergeinfoba bekerül, hogy
ezen revisionök már merge-ölve lettek. És a következő merge-nél már nem
lesz conflict a pom.xml
-re. Ettől függetlenül az olyan pom.xml
változások, melyek lényegi részt érintenek, és nem a csak a release
plugin által szerkesztett verziószámot, pl. dependency, stb.,
merge-ölésre kerülnek. Erre a problémára könnyű scriptet is írni,
hiszen a Release plugin mindig úgy commitol, hogy a commit message-be
szerepel a [maven-release-plugin]
szó, valamint konfigurálhatunk saját
commit message-eket is, így ezekre lehet szűrni, és rájuk futtatni a
merge-öt --record-only
kapcsolóval.