Perché CVS e Subversion non sono adatti ad un buon sviluppo del software
CVS e Subversion (SVN) sono strumenti di versionamento diffusissimi in ambito software. In quanto strumenti, dovrebbero aiutare il programmatore sollevandolo da compiti ripetitivi e proni a errori. In questo blog mostro come, utilizzando CVS e SVN al meglio, si ottenga esattamente l’effetto opposto, cioè un aggravio per il programmatore.
Best practice
Chiarisco cosa intendo per ‘utilizzare CVS s SVN al meglio’. Mi riferisco all’utilizzo dei branch, che in molte fonti (per CVS e SVN) è considerato un buon modo di lavorare.
Il principio è condivisibile. Si può fare un branch per provare feature nuove senza interferire con altri sviluppatori. Oppure si possono creare uno o più branch di rilascio, denominati con i nomi delle versioni del prodotto (p.e., v1_0, v1_1, v2_0, ecc.). Infine si può avere rami diversi per fasi diverse del ciclo di vita: un ramo per lo sviluppo, uno per il testing, uno per il rilascio.
Insomma ci sono tanti buoni motivi per utilizzare il branching. Vediamo ora i problemi con un esempio. Utilizzerò CVS e riporterò tutti i comandi eseguiti in modo che possiate, se volte, riprodurre il problema sul vostro computer.
Un menu del bar versionato: impostazione e branching
Niente Java, C++, o codice per il nostro esempio. Ciò che vogliamo versionare è un semplice menu del bar che, in prima battuta sarà il seguente testo:
Bar: - caffe - cappuccino - spremuta
Ok. Per partire dobbiamo creare una directory ‘documenti’ e il file ‘bar.txt’
$ mkdir documenti $ cd documenti/ $ echo "Bar:" > bar.txt $ echo "- caffe" >> bar.txt $ echo "- cappuccino" >> bar.txt $ echo "- spremuta" >> bar.txt
Adesso possiamo importare il modulo in CVS:
$ cvs import -m "Importazione" documenti predonzani start N documenti/bar.txt No conflicts created by this import
Bene. Dopo l’importazione, la directory creata può essere cancellata e ricreata con un checkout fresco:
$ cd .. $ rm -rf documenti/ $ cvs co documenti cvs checkout: Updating documenti U documenti/bar.txt
La directory è stata ricreata e la presenza della sotto-directory ‘CVS’ ci conferma che CVS la sta gestendo.
Il branch su cui attualmente ci troviamo è HEAD, il branch di base. Assumiamo che su questo si svolgano le modifiche considerate in bozza.
Dedichiamo invece un altro branch alle versioni ufficiali. Creiamo questo branch, chiamandolo appunto ‘ufficiale’. Il comando cvs per il branching è ‘tag’ con il parametro ‘-b’
$ cd documenti/ $ cvs tag -b ufficiale cvs tag: Tagging . T bar.txt
Bisogna ricordarsi che la directory corrente, dopo aver creato il branch, rimane comunque su HEAD. Possiamo rinominarla per evitare ogni ambiguità (CVS esegue il check out utilizzando sempre il nome del modulo, ma poi non si lamenta se il nome della directory principale viene cambiato):
$ cd .. $ mv documenti documenti-HEAD
Ora chiediamo a CVS un altro checkout, ma questa volta gli chiederemo di tirare fuori il ramo ‘ufficiale’.
$ cvs co -r ufficiale documenti cvs checkout: Updating documenti U documenti/bar.txt
Anche in questo caso cambiamo nome alla directory per ricordarci del branch a cui appartiene:
mv documenti documenti-ufficiale
Bene l’impostazione è completata. Abbiamo creato un modulo chiamato ‘documenti’ e abbiamo due rami: HEAD e ufficiale. Abbiamo inoltre due check-out contenenti i due rami: ‘documenti-HEAD’ e ‘documenti-ufficiale’.
I due branch sono perfettamente allineati, quindi i due file ‘bar.txt’ nelle due directory sono uguali. Ora siamo pronti a fare le prime modifiche.
Un menu del bar versionato: prima modifica e merge
Notiamo che il file ‘bar.txt’ contiene un errore di battitura: “caffè” e scritto con la ‘e’ non accentata. Siccome vogliamo fare le cose per bene, per prima cosa andiamo su HEAD, il branch dedicato alle bozze.
$ cd documenti-HEAD
Effettuiamo la modifica con un editor e controlliamo il contenuto:
$ cat bar.txt Bar: - caffè - cappuccino - spremuta
Ok. Il testo è corretto. Possiamo fare un commit:
$ cvs commit -m "correzione" cvs commit: Examining . Checking in bar.txt; /Users/predo/cvsroot/documenti/bar.txt,v <-- bar.txt new revision: 1.2; previous revision: 1.1 done
La nuova versione è stata registrata correttamente su HEAD.
Supponiamo che la modifica sia definitiva e che sia autorizzata a passare da HEAD a ‘ufficiale’. Dobbiamo andare in ‘ufficiale’ ed effettuare un merge.
$ cd ../documenti-ufficiale $ cvs update -j HEAD bar.txt RCS file: /Users/predo/cvsroot/documenti/bar.txt,v retrieving revision 1.1 retrieving revision 1.2 Merging differences between 1.1 and 1.2 into bar.txt
Il merge è l’operazione che riunisce branch diversi: siamo andati su ‘ufficiale’ e abbiamo tirato dentro le modifiche fatte su HEAD. L’operazione ha avuto successo e non sono stati generati conflitti. Possiamo verificare il contenuto di bar.txt:
$ cat bar.txt Bar: - caffè - cappuccino - spremuta
Ufficializziamo il merge con un commit:
$ cvs commit -m "merge da HEAD" cvs commit: Examining . Checking in bar.txt; /Users/predo/cvsroot/documenti/bar.txt,v <-- bar.txt new revision: 1.1.1.1.2.1; previous revision: 1.1.1.1 done
Un menu del bar versionato: seconda modifica e merge
Immaginiamo che dopo un po’ di tempo qualche cliente del bar chieda se il caffè è un espresso o un caffè americano. Decidiamo che sia un espresso. Dobbiamo tornare su HEAD…
$ cd ../documenti-HEAD/
Ed effettuare la modifica con un editor. Verifichiamo il nuovo contenuto:
$ cat bar.txt Bar: - caffè espresso - cappuccino - spremuta
La modifica va bene - possiamo committare:
$ cvs commit -m "precisazione" cvs commit: Examining . Checking in bar.txt; /Users/predo/cvsroot/documenti/bar.txt,v <-- bar.txt new revision: 1.3; previous revision: 1.2 done
Di nuovo riteniamo che valga la pena ufficializzare la modifica propagandola da HEAD a ‘ufficiale’. Ci spostiamo nella directory di ‘ufficiale’ e rifacciamo il merge:
$ cd ../documenti-ufficiale $ cvs update -j HEAD bar.txt M bar.txt RCS file: /Users/predo/cvsroot/documenti/bar.txt,v retrieving revision 1.1 retrieving revision 1.3 Merging differences between 1.1 and 1.3 into bar.txt rcsmerge: warning: conflicts during merge
Conflitti? Da dove escono? Vediamo il file:
$ cat bar.txt Bar: <<<<<<< bar.txt - caffè ======= - caffè espresso >>>>>>> 1.3 - cappuccino - spremuta
E’ proprio la nostra riga. Quello che però non ci torna è il modo in cui il conflitto si è verificato.
Per esperienza sappiamo che i conflitti nascono quando due sviluppatori lavorano in parallelo sullo stesso codice: righe modificate da entrambi generano conflitti e vanno risolti manualmente.
Ma il problema qui è che non ci sono due sviluppatori che hanno lavorato in parallelo. Tutte le modifiche sono state fatte su HEAD e l’unico lavoro svolto sul branch ‘ufficale’ è stato quello di fare merge da HEAD.
Uno sguardo dall’alto
Come sempre un motivo c’è. Vediamo un grafo delle versioni e dello operazioni svolte.

Il ramo HEAD è costituito dalle versioni 1.1, 1.2 e 1.3. 1.1 è la versione inizialmente importata. 1.2 è la versione dopo la prima modifica. 1.3 è la versione dopo la seconda modifica.
Il ramo ‘ufficiale’ è costituito dalle versioni 1.1.1.1, 1.1.1.1.2.1 e 1.1.1.1.2.2. 1.1.1.1 è un alias per 1.1: sono la stessa versione. 1.1.1.1.2.1 è la versione committata dopo aver fatto il primo merge da HEAD. 1.1.1.1.2.2 è la versione che stavamo preparando (e che in realtà non abbiamo committato) quando abbiamo fatto il secondo merge da HEAD e abbiamo riscontrato il conflitto.
Le operazioni di merge sono indicate con frecce rosse da versioni di HEAD verso versioni di ‘ufficiale’.
Il problema
In realtà CVS ignora le frecce rosse. Cioè le operazioni di merge corrispondenti a tali frecce sono state eseguite, ma CVS non le registra da nessuna parte. Il risultato è che, per quanto ne sa CVS, la versione 1.1.1.1.2.1 potrebbe essere stata modificata manualmente (indipendentemente dal merge) in parallelo alle modifiche di 1.2 e 1.3: modifiche eseguite in parallelo generano conflitti.
Il problema si verificherà all’infinito, anche se andiamo avanti con versioni 1.4, 1.5 ecc. e relativi merge. La riga del ‘caffè’ genererà conflitti ogni volta che la toccheremo.
Dove abbiamo sbagliato
Per evitare il problema dovevamo fare così:
- taggare la versione 1.2: cvs tag v1
- taggare la versione 1.3: cvs tag v2
- eseguire update con la doppia opzione ‘-j’: cvs update -j v1 -j v2 bar.txt
Non entro nel merito di perché questa soluzione è più corretta di quella iniziale. Quello che voglio sottolineare è che CVS ci obbliga:
- a taggare prima di fare merge
- a ricordarci l’ultimo e il penultimo tag usati per il merge. Questo anche se usiamo i tag per scopi diversi dal merge
- a usare sintassi poco intuitive.
In pratica dobbiamo segnarci e gestire a mano il grafo dei merge, cioè fare un’operazione che avremmo felicemente delegato a CVS ma che CVS non sa svolgere.
Subversion
SVN 1.4.x (la versione corrente) funziona esattamente come CVS. Il manuale è esplicito su questo punto:
Ideally, your version control system should prevent the double-application of changes to a branch. It should automatically remember which changes a branch has already received, and be able to list them for you. It should use this information to help automate merges as much as possible.
Unfortunately, Subversion is not such a system; it does not yet record any information about merge operations. When you commit local modifications, the repository has no idea whether those changes came from running svn merge, or from just hand-editing the files.
Viva la sincerità.
Per quanto riguarda la prossima versione (SVN 1.5, ancora non stabile) il manuale parla di un’opzione ‘--reintegrate‘ che bisogna ricordarsi di utilizzare quando si effettua merge ripetutamente come nell’esempio che ho mostrato.
Probabilmente funzionerà ma non sembra una soluzione pulitissima e comunque dalla documentazione sembra che nei suoi metadati SVN continuerà a non tracciare i merge.
Conclusioni
Penso che l’esempio del bar sia quello più semplice in assoluto. Qualunque situazione di sviluppo software reale è molto più complessa. Abbiamo più righe di codice, più rami, più versioni, più tag. Inoltre la propagazione delle modifiche è spesso bidirezionale (nel nostro esempio sarebbe stato da ‘ufficiale’ a HEAD, oltre che da HEAD a ‘ufficiale’).
I problemi derivanti da CVS crescono al crescere della dimensione del codice gestito e della complessità dell’approccio al versionamento. Si può evitare che i problemi crescano con una solida metodologia di branching, tagging e di tracciamento manuale, ma anche qui c’è un costo.
Il risultato? Il più comune è che si rinuncia a fare branching. Chi scrive software spesso vive con HEAD e al massimo un branch di rilascio. Chi scrive codice sperimentale, spesso si riduce
a lavorare in locale, cioè a non committare il suo lavoro per settimane, finché non è abbastanza stabile.
Soluzione? Fino a un po’ di tempo fa avrei detto che il metodo descritto nella sezione ‘dove abbiamo sbagliato’ era la soluzione. Sì, era complessa, ma era il modo di lavorare corretto con CVS. Avrei visto il bicchiere ‘mezzo pieno’. Il un precedente blog avevo utilizzato la soluzione senza nessuna critica.
Recentemente però, grazie a Carlo, ho scoperto Git, Mercurial e Bazaar. Sono tool nuovi di zecca che fanno apparire CVS/SVN come strumenti dell’età della pietra. Ma di questo parlerò in un prossimo blog.


Aprile 8th, 2008 10:16
Ottimo articolo, vai col prossimo :)
Aprile 20th, 2008 19:27
[…] discusso in un precedente blog i limiti di CVS e Subversion. In particolare ho evidenziato come i branch risultano difficili con CVS/SNV e di conseguenza poco […]