Vanlige bruksområder

Det er mange forskjellige bruksområder for forgreninger og svn merge, og denne seksjonen beskriver de vanligste som du sannsynligvis vil komme over.

Flette en hel gren til en annen

For å fullføre eksempelet vårt vil vi nå reise fram i tiden. Tenk deg at flere dager har gått, og mange forandringer har skjedd både i trunk og på den private grenen din. Tenk deg så at du er ferdig med arbeidet på den private grenen; funksjonaliteten eller feilrettingen er endelig fullført, og nå vil du flette alle forandringene fra grenen din til trunk så andre kan få glede av dem.

Så hvordan bruker vi svn merge i dette tilfellet? Husk at denne kommandoen sammenligner to trær og legger forandringene inn i en arbeidskopi. For å motta forandringene må du derfor ha en arbeidskopi av trunk. Vi går ut i fra at du enten har den originale liggende (helt oppdatert), eller at du nylig hentet ut en fersk arbeidskopi av /calc/trunk.

Men hvilke to trær skal sammenlignes? Ved første øyekast ser det innlysende ut: Bare sammenlign seneste treet fra trunk med det seneste treet fra forgreningen. Men pass på – denne antakelsen er feil, noe som mange nye brukere har brent seg på. Siden svn merge opererer på samme måte som svn diff, vil en sammenligning mellom trærne i nyeste trunk og gren ikke bare vise forandringene som du har gjort på grenen. En slik sammenligning viser alt for mange forandringer: Den vil ikke bare vise tilleggingene av dine forandringer på grenen, men også fjerningen av forandringer i trunk som aldri skjedde på din gren.

For å bare vise forandringene som skjedde på din gren, må du sammenligne tilstanden ved starten av grenen din i forhold til dens endelige tilstand. Ved å bruke svn log på grenen kan du se at den ble opprettet i revisjon 341. Og den endelige tilstanden får du ved å bruke HEAD-revisjonen. Dette betyr at du vil sammenligne revisjonene 341 og HEAD i forgreningskatalogen og legge disse forskjellene inn i en arbeidskopi av trunk.

[Tips] Tips

En fin måte å finne revisjonen en gren ble opprettet i (basen av forgreningen) er å bruke valget --stop-on-copy til svn log. Delkommandoen svn log vil normalt vise hver eneste forandring gjort på grenen, inkludert å gå forbi kopieringen som opprettet grenen. Så vanligvis vil du også se historien fra trunk. --stop-on-copy-valget vil stoppe loggutlistingen med en gang svn log finner ut at målet ble kopiert eller skiftet navn.

Så hvis vi går videre i eksempelet,

$ svn log --verbose --stop-on-copy \
          http://svn.example.com/repos/calc/branches/my-calc-branch
…
------------------------------------------------------------------------
r341 | bruker | 2002-11-03 15:27:56 -0600 (tor, 07 nov 2002) | 2 lines
Endrede filstier:
   A /calc/branches/my-calc-branch (fra /calc/trunk:340)

$

Som forventet er den siste revisjonen skrevet ut av denne kommandoen den revisjonen der my-calc-branch ble opprettet ved kopiering.

Her er den siste fletteprosedyren, og deretter:

$ cd calc/trunk
$ svn update
På revisjon 405.

$ svn merge -r 341:405 http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile

$ svn status
M   integer.c
M   button.c
M   Makefile

# … Undersøk forskjellene, kompiler, test osv …

$ svn commit -m "Flettet forandringer mellom r341:405 fra my-calc-branch til trunk."
Sender        integer.c
Sender        button.c
Sender        Makefile
Sender fildata ...
La inn revisjon 406.

Igjen, legg merke til at loggmeldingen veldig spesifikt nevner området av forandringer som ble flettet inn på trunk. Husk alltid å gjøre dette, fordi det er vital informasjon som du vil trenge senere.

For eksempel, tenk at du bestemmer deg for å fortsette arbeidet på grenen en uke til, for å fullføre feilrettingen eller en forbedring av den originale funksjonaliteten. Depotets HEAD-revisjon er nå 480, og du er klar til å utføre en ny fletting fra den private grenen din til trunk. Men som diskutert i “Beste praksiser for fletting” vil du ikke flette forandringene du allerede har flettet før; du vil bare flette alt nytt på grenen siden forrige gang du flettet. Trikset er å finne ut hva som er nytt.

Første skritt er å kjøre svn log på trunk og se etter en loggmelding fra forrige gang du flettet fra grenen:

$ cd calc/trunk
$ svn log
…
------------------------------------------------------------------------
r406 | bruker | 2004-02-08 11:17:26 -0600 (søn, 08 feb 2004) | 1 line

Flettet forandringer mellom r341:405 fra my-calc-branch til trunk.
------------------------------------------------------------------------
…

Aha! Siden alle grenforandringene som skjedde mellom revisjonene 341 og 405 ble flettet til trunk som revisjon 406, vet du nå at du bare vil flette grenforandringene etter dette – ved å sammenligne revisjonene 406 og HEAD.

$ cd calc/trunk
$ svn update
På revisjon 480.

# Vi ser at HEAD er 480 for øyeblikket, så vi bruker den for å utføre 
# flettingen:

$ svn merge -r 406:480 http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile

$ svn commit -m "Flettet forandringer mellom r406:480 fra my-calc-branch til trunk."
Sender        integer.c
Sender        button.c
Sender        Makefile
Sender fildata ...
La inn revisjon 481.

Nå inneholder trunk alle forandringene som ble gjort i den andre omgangen på grenen. På dette punktet kan du enten slette grenen (dette vil vi komme tilbake til) eller fortsette arbeidet på grenen og repetere denne prosedyren for etterfølgende flettinger.

Omgjøre forandringer

En annen vanlig bruksmåte for svn merge er å omgjøre en forandring som allerede er blitt lagt inn. Tenk deg at du jobber glad og fornøyd på en arbeidskopi av /calc/trunk, og plutselig finner ut at forandringen du gjorde langt tilbake i revisjon 303, som forandret integer.c, er helt feil. Den skulle aldri vært lagt inn. Du kan bruke svn merge for å angre forandringen i arbeidskopien din, og deretter legge inn de lokale forandringene til depotet. Alt du trenger å gjøre er å spesifisere en omvendt forskjell:

$ svn merge -r 303:302 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
M  integer.c

$ svn diff
…
# Sjekk at forandringen er fjernet
…

$ svn commit -m "Fjernet forandringen som ble lagt inn i r303."
Sender        integer.c
Sender fildata .
La inn revisjon 350.

En måte å tenke på en depotrevisjon er som en spesifikk gruppe av forandringer (noen versjonskontrollsystemer kaller disse forandringssettchangesets). Ved å bruke -r-valget kan du be svn merge om å legge inn et forandringssett, eller et helt område av forandringssett, til arbeidskopien din. I vårt tilfelle med å omgjøre en forandring ber vi svn merge om å legge inn forandringssett nummer 303 baklengs inn i arbeidskopien vår.

Husk at det å rulle tilbake en forandring som dette er akkurat likt enhver annen svn merge-operasjon, så du bør bruke svn status og svn diff for å forsikre deg om at arbeidet ditt er i den tilstanden du vil det skal være i, og deretter bruke svn commit for å sende den endelige versjonen til depotet. Etter innleggingen er dette spesielle forandringssettet ikke lenger representert i HEAD-revisjonen.

Og så tenker du kanskje: Nåh, dette omgjorde vel egentlig ikke innleggingen? Forandringen eksisterer fortsatt i revisjon 303. Hvis noen henter ut en versjon av calc-prosjektet mellom revisjonene 303 og 349, vil de se den gale forandringen, ikke sant?

Ja, det stemmer. Når vi snakker om å fjerne en forandring, snakker vi egentlig om å fjerne den fra HEAD. Den originale forandringen eksisterer fortsatt i depotets historie. I de fleste situasjoner er dette greit nok. De fleste er bare interessert i å følge HEAD av et prosjekt uansett. Det er imidlertid spesielle tilfeller der du virkelig vil ødelegge alle spor etter innleggingen, kanskje la noen inn et konfidensielt dokument ved en ulykke. Dette er ikke så lett, viser det seg, fordi Subversion ble spesielt designet for å aldri miste informasjon. Revisjoner er uforanderlige trær som bygger på hverandre. Det å fjerne en revisjon fra historien vil forårsake en dominoeffekt som vil føre til kaos i alle etterfølgende revisjoner og muligens gjøre alle arbeidskopiene ubrukelige.[23]

Hente tilbake slettede elementer

Det som er fint med versjonskontrollsystemer er at informasjon aldri går tapt. Selv om du sletter ei fil eller en katalog, kan den være borte fra HEAD-revisjonen, men objektet eksisterer fortsatt i tidligere revisjoner. Et av de vanligste spørsmålene nye brukere spør om, er: Hvordan får jeg den gamle fila eller katalogen min tilbake?.

Første skritt er å definere nøyaktig hvilket element du skal prøve å hente tilbake. Her er en nyttig metafor: Du kan tenke på hvert objekt i depotet som om det eksisterer i et slags todimensjonalt koordinatsystem. Det første koordinatet er et spesifikt revisjonstre, og det andre koordinatet er en sti inne i dette treet. Så hver versjon av fila eller katalogen din kan bli definert som et spesifikt koordinatpar.

Subversion har ingen Attic-katalog som CVS har,[24] så du må bruke svn log for å finne det eksakte koordinatparet som du vil hente tilbake. En god strategi er å kjøre svn log --verbose i en katalog som inneholdt det slettede elementet ditt. Valget --verbose viser en liste over alle forandrede elementer i hver revisjon; alt du trenger å gjøre er å finne revisjonen der du slettet fila eller katalogen. Du kan gjøre dette visuelt, eller ved å bruke et annet verktøy for å undersøke utdataene fra loggen (ved hjelp av grep, eller kanskje ved hjelp av et inkrementelt søk i en tekstbehandler).

…
------------------------------------------------------------------------
r808 | joe | 2003-12-26 14:29:40 -0600 (fre, 26 des 2003) | 3 lines
Endrede filstier:
   D /calc/trunk/real.c
   M /calc/trunk/integer.c

La inn Fast Fourier transform-funksjoner i integer.c .
Slettet real.c fordi koden nå ligger i double.c .
…

I eksempelet går vi ut i fra at du ser etter en slettet fil kalt real.c. Ved å se gjennom loggene for en foreldrekatalog, har du funnet ut at denne fila ble slettet i revisjon 808. Derfor er den siste versjonen av fila der den fortsatt eksisterte i revisjonen like før dette. Konklusjon: Du vil hente tilbake stien /calc/trunk/real.c fra revisjon 807.

Dette var den vanskelige delen – etterforskningen. Nå som du vet hva du vil hente tilbake, har du to forskjellige valg.

En måte er å bruke svn merge for å legge inn revisjon 808 i revers. (Vi har allerede gått gjennom hvordan vi omgjør forandringer, se “Omgjøre forandringer”.) Dette vil ha samme effekten som å legge til real.c en gang til som en lokal modifisering. Fila vil bli klargjort for tillegging, og etter en innlegging med svn commit vil fila eksistere i HEAD igjen.

Men i dette spesielle eksempelet er det kanskje ikke den beste strategien. Å legge inn revisjon 808 i revers vil ikke bare klargjøre real.c for tillegging, men loggmeldingen indikerer at det vil også bli omgjort forandringer i integer.c, noe som du ikke ønsker. Du kan selvfølgelig bakoverflette revisjon 808 og deretter kjøre svn revert på de lokale forandringene i integer.c, men denne teknikken skalerer ikke alltid like bra. Hva hvis det var 90 filer som forandret seg i revisjon 808?

En annen og mer målrettet strategi er å ikke bruke svn merge i det hele tatt, men derimot svn copy. Kopier ganske enkelt den eksakte revisjonens og stiens koordinatpar fra depotet til arbeidskopien din:

$ svn copy --revision 807 \
           http://svn.example.com/repos/calc/trunk/real.c ./real.c

$ svn status
A  +   real.c

$ svn commit -m "Hentet tilbake real.c from revisjon 807, /calc/trunk/real.c ."
Legger til         real.c
Sender fildata .
La inn revisjon 1390.

Plusstegnet som vises i statusoversikten indikerer at elementet ikke bare er klargjort for tillegging, men er klargjort for tillegging med historie. Subversion husker hvor det ble kopiert fra. I framtiden vil kjøring av svn log på denne fila gå tilbake forbi gjenoppstandelsen av fila og gjennom hele historien den hadde før revisjon 807. Med andre ord, denne nye real.c er ikke egentlig ny; den er en direkte etterkommer av den originale, slettede fila.

Selv om eksempelet vårt viser at vi henter tilbake ei fil, legg merke til at disse samme teknikkene virker like godt når det gjelder å hente tilbake slettede kataloger.

Vanlige forgreningsmønstre

Versjonskontroll blir vanligvis brukt til programutvikling, så her er en rask kikk på to av de vanligste forgrenings-/flettemønstere som blir brukt av programmeringsteam. Hvis du ikke bruker Subversion til programutvikling, kan du hoppe over denne seksjonen. Hvis du er en programutvikler som bruker versjonskontroll for første gang, følg nøye med, da disse fremgangsmåtene blant erfarne brukere ofte er ansett som de beste metodene. Disse prosessene er ikke spesifikke for Subversion; de kan brukes med ethvert versjonskontrollsystem. Men det kan hjelpe å se dem beskrevet i Subversionterminologi.

Utgivelsesgrener

Programvare har vanligvis denne livssyklusen: Kode, teste, offentliggjøre, repetere. Det er to problemer med denne prosessen. For det første trenger utviklere å skrive ny funksjonalitet mens kvalitetssikringsteam tester versjoner som er ment å skulle være stabil. Nytt arbeide kan ikke stoppe opp mens programvaren blir testet ut. For det andre, teamet må nesten alltid støtte eldre, utgitte versjoner av programvaren; hvis en feil blir oppdaget i den seneste koden, eksisterer den sannsynligvis også i utgitte versjoner, og kundene vil ønske å få denne feilen rettet uten å måtte vente på en ny stor utgivelse.

Og det er her versjonskontroll kan hjelpe. Den vanlige prosedyren ser ut som dette:

  • Utviklerne legger inn alt nytt arbeid til trunk. Daglige forandringer legges inn på /trunk: Ny funksjonalitet, retting av feil og så videre.

  • trunk blir kopiert til en utgivelses-gren. Når teamet synes programvaren er klar for utgivelse (for eksempel en 1.0-versjon), kan /trunk bli kopiert til /branches/1.0.

  • Teamene fortsetter å arbeide parallelt. Et team begynner inngående testing av utgivelsesgrenen, mens et annet team fortsetter på nytt arbeid (for eksempel på det som skal bli versjon 2.0) i /trunk. Hvis det blir funnet feil på et av stedene, blir nødvendige reparasjoner flettet fram og tilbake. Men på et punkt stopper også denne prosessen. Grenen er frosset for avsluttende testing rett før en utgivelse.

  • Grenen blir merket og utgitt. Når testingen er ferdig, blir /branches/1.0 kopiert til /tags/1.0.0 som et referanseøyeblikksbilde. De merkede filene pakkes og sendes ut til kundene.

  • Grenen blir vedlikeholdt over tid. Mens arbeidet fortsetter på /trunk for versjon 2.0, blir fortsatt feil som blir funnet på /trunk flettet derfra til /branches/1.0. Når et tilstrekkelig antall feil er blitt rettet, kan vedlikeholderne bestemme seg for å lage en 1.0.1-utgivelse: /branches/1.0 blir kopiert til /tags/1.0.1, og de merkede filene blir pakket og utgitt.

Hele denne prosessen repeteres mens programvaren modnes: Når arbeidet for 2.0 er komplett, blir en ny utgivelsesgren for 2.0 opprettet, testet, merket og eventuelt utgitt. Etter noen år ender depotet opp med et antall grener i vedlikeholdsmodus, og et antall merker som representerer ferdige, utgitte versjoner.

Funksjonalitetsgrener

En funksjonalitetsgren er den typen gren som har vært det dominerende eksempelet i dette kapittelet, den typen du har arbeidet på mens Sally fortsetter å arbeide på /trunk. Det er en midlertidig gren som er opprettet for å arbeide på en kompleks forandring uten å la det gå ut over stabiliteten til /trunk. I motsetning til utgivelsesgrener (som kanskje må bli støttet for alltid) blir funksjonalitetsgrener født, brukt en stund, flettet tilbake til trunk, og deretter til sist slettet. De har en begrenset nyttighetsperiode.

Igjen, prosjekt-policy varierer veldig når det gjelder nøyaktig når det passer å opprette en funksjonalitetsgren. Noen prosjekter bruker ikke funksjonalitetsgrener i det hele tatt; innlegginger til /trunk er tilgjengelig for alle. Fordelen med dette systemet er at det er enkelt – ingen trenger å lære om forgrening eller fletting. Ulempen er at koden i trunk ofte er ustabil eller ubrukelig. Andre prosjekter bruker forgreninger til det ekstreme; ingen forandringer blir noen gang lagt direkte inn på trunk. Til og med de enkleste forandringer blir laget på en gren med kort levetid, nøye kontrollert og deretter flettet til trunk. Så blir grenen slettet. Dette systemet garanterer en eksepsjonelt stabil og brukbar trunk til enhver tid, men med en pris i form av mer arbeid i prosessen.

Noen prosjekter velger en rute midt i mellom. De insisterer på at /trunk skal kunne kompilere og passere systemsjekker til enhver tid. En funksjonalitetsgren er bare nødvendig når en forandring krever et stort antall innlegginger som kan gå ut over stabiliteten. En god tommelfingerregel er å stille dette spørsmålet: Hvis utvikleren jobbet i flere dager i isolasjon og deretter la inn hele den store forandringen på en gang (så /trunk aldri ble ustabil), ville den blitt for stor for gjennomlesing? Hvis svaret til det er ja, bør forandringen bli utviklet på en funksjonalitetsgren. Etter hvert som utvikleren legger inn økende forandringer til grenen, kan de bli sett over av kolleger.

Til sist har vi spørsmålet om hvordan man best kan holde en fremtidig gren i synk med trunk etterhvert som arbeidet går fram. Som vi nevnte tidligere, er det en stor risiko å arbeide på en gren i flere uker eller måneder; forandringer på trunk kommer strømmende inn, til punktet der de to utviklingslinjene er såpass forskjellige at det kan bli et mareritt å flette grenen tilbake til trunk.

Denne situasjonen unngås best ved å jevnlig flette trunk-forandringer inn på grenen. Lag en regel: En gang i uken fletter du forrige ukes forandringer til grenen. Vær nøye når du gjør dette; flettingen må sjekkes for å unngå problemet med gjentatte flettinger (som beskrevet i “Følge flettinger manuelt”). Du må skrive nøyaktige loggmeldinger med detaljer om hvilket revisjonsområde som allerede er blitt flettet (som demonstrert i “Flette en hel gren til en annen”). Det kan høres skremmende ut, men er faktisk ganske enkelt å gjøre.

Etterhvert er du klar til å flette den synkroniserte funksjonalitetsgrenen tilbake til trunk. For å gjøre dette, start med å gjøre en avsluttende fletting av de siste trunk-forandringene til grenen. Når det er gjort, vil de siste versjonene av grenen og trunk være absolutt like, bortsett fra forandringene på grenen din. Så i dette spesielle tilfellet, vil du flette ved å sammenligne grenen med trunk:

$ cd trunk-i-arbeidskopien

$ svn update
På revisjon 1910.

$ svn merge http://svn.example.com/repos/calc/trunk@1910 \
            http://svn.example.com/repos/calc/branches/mybranch@1910
U  real.c
U  integer.c
A  nykatalog
A  nykatalog/nyfil
…

Ved å sammenligne HEAD-revisjonen for trunk med HEAD-revisjonen på grenen, kan du definere ett delta som beskriver kun de forandringene som du gjorde til grenen; begge utviklingsgrener inneholder allerede forandringene fra trunk.

En annen måte å tenke på dette mønsteret er at den ukentlige synkroniseringen er det samme som å kjøre svn update i en arbeidskopi, mens det siste trinnet med fletting er det samme som å kjøre svn commit fra en arbeidskopi. Når alt kommer til alt, er jo en arbeidskopi en grunn, privat gren. Det er en gren som bare er i stand til å lagre én forandring om gangen.



[23] Subversionprosjektet har imidlertid planer om å legge inn en svnadmin obliterate-kommando som vil ta seg av oppgaven med å permanent slette informasjon. I mellomtiden, se “svndumpfilter” for en mulig omvei om problemet.

[24] Fordi CVS ikke versjonerer trær, lager den et Attic-område innenfor hver depotkatalog som en måte å huske slettede filer på.