Subversion branch, akár Maven Release plugin-nal

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 best practice-eket 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 Explorer-be épül, az egyik legjobb kliens. Próbáltam Subversion-t 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 Subversion-ben 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. Subversion-ben 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-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 commit-olni. 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 commit-olni, 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ő poszt-ban 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 revision-ben hibás kódot commit-oltunk, 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. (A Heti Meteor #6-ban is előjött a téma, teljesen más kontextusban.) 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 revision-re 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 bug-ot is kijavítottak, amit érdemes áthozni az én ágamba is. Persze ez is eltárolásra kerül a mergeinfo-ban, így ha később az ágban lévő többi módosítást is merge-ölni akarjuk, akkor ezt a revision-t 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 conflict-tal, é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 mergeinfo-ró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 mergeinfo-nak 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 un. 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 Maven-t 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.

  1. Megvizsgálja, hogy nincs-e lokális módosítás, mely nem lett commit-olva. Ha van, hibaüzenettel leáll (Cannot prepare the release because you have local modifications).
  2. 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.)
  3. Módosítja a pom.xml-ben a scm helyét, hogy a branch-re mutasson.
  4. Commit-olja a pom.xml-t. A commit comment szövege: [maven-release-plugin] prepare branch my-branch
  5. Végrehajtja a branch-et. A commit comment szövege: [maven-release-plugin] copy for branch my-branch
  6. Megemeli a pom.xml-ben a verziószámot, és visszaállítja a scm helyét.
  7. Commit-olja 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 Subversion-t, a mergeinfo-ba 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ű script-et is írni, hiszen a release plugin mindig úgy commit-ol, 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.