Linux

CVS: sistema per il controllo di versione del software

Vediamo il CVS, un sistema di controllo di versione, utilissimo durante la scrittura di programmi, specialmente se si lavora in gruppo

di Lorenzo Bettini e Antonio Gallo

Quando si lavora ad un progetto di dimensioni medio-grandi si ha la necessità di tenere traccia di tutte le modifiche, apportate durante la fase di sviluppo, al codice sorgente. Mantenere un archivio delle modifiche effettuate non è una soluzione accettabile in questi casi. Inoltre, è spesso necessario modificare i sorgenti di una versione antecedente a quella attualmente in sviluppo, per rilasciare, ad esempio, un hot fix o un workaround.

Il discorso si complica ulteriormente quando si lavora in un team di sviluppo dove l’esigenza primaria è quella di evitare l’insorgere di conflitti tra i vari sviluppatori durante la modifica del codice sorgente.

In questo articolo vedremo come il CVS aiuta a risolvere queste problematiche. L’articolo vuole essere una sorta di tutorial, e si consiglia quindi di provare ad effettuare le varie operazioni dell’esempio che tratteremo.

Cosa è il CVS

Il CVS (Control Version System) è un sistema per il controllo delle versioni di un progetto software. Molto utilizzato in ambiente GNU/Unix, ed ovviamente in ambiente Linux, sta acquistando molta popolarità tra gli sviluppatori di tutto il mondo in quanto utilizzato da tutti i maggiori team di sviluppo dei progetti GNU. CVS permette di creare una history di tutti i sorgenti, registrando ogni modifica, ed associando ad ogni sorgente un numero di revisione comprensivo di un breve commento sulle modifiche apportate. Con CVS, è inoltre possibile recuperare, in ogni momento, qualsiasi versione precedente del progetto per poterla modificare, ed è stato disegnato per essere utilizzato da un team di sviluppo, garantendo che i sorgenti restino sempre consistenti, impedendo che le modifiche apportate da uno sviluppatore vengano soprascritte da altri. Infine, permette di esportare le differenze fra due release diverse dello stesso progetto, creando i famosi file di patch.

L’architettura del CVS

L’architettura del CVS è basata su alcuni semplici concetti: tutte le directory ed i file sorgente vengono memorizzati all’interno di un repository. Al suo interno, per ogni file del progetto, sono memorizzati dei file con l’aggiunta dell’estensione ,v. Questi file contengono l’ultima revisione del sorgente e tutte le modifiche apportate (le varie parti del file per le quali le varie versioni differiscono). Più directory, all’interno di un repository possono essere raggruppate in modo da costituire un modulo.

Gli sviluppatori non modificano direttamente il codice sorgente contenuto all’interno del repository ma ognuno di loro lavora su una copia locale dei file. Una volta effettuare le modifiche necessarie (di solito ci si assicura che il codice venga compilato correttamente), il codice modificato viene inviato al CVS che si preoccupa di memorizzare solo le differenze dalla versione precedente a quella del repository e quindi di aggiornare la versione corrente del progetto. E’ buona regola specificare, durante quest’ultima operazione, anche un commento che spieghi le modifiche effettuate.

L’operazione tramite la quale si recupera i file contenuti nel repository viene detta check out, mentre quella che consiste nell’inviare le modifiche apportate si chiama commit.

Come se tutto questo non bastasse, CVS consente anche l’accesso ai sorgenti tramite rete; quindi mantenendo i sorgenti su un computer remoto è possibile accedervi anche tramite Internet. Attualmente tutti i maggiori progetti GNU sono disponibili attraverso CVS, è quindi possibile scaricare dalla rete, giorno dopo giorno, i file che sono stati modificati. Un esempio è il sito dello GNOME [GNU].

Iniziare col CVS

Prima di iniziare a vedere in dettaglio i vari modi di utilizzo del CVS, occorre sapere che ad ogni comando (il CVS funziona a linea di comando), deve essere specificata la posizione del repository. Questo può essere fatto:

  • esplicitamente, tramite l’opzione –d (es. –d /usr/local/cvsroot)
  • implicitamente, tramite la variabile di ambiente CVSROOT.

Il secondo metodo è migliore: conviene inizializzare tale variabile nel proprio script di login.

Creare un il repository

La prima volta è necessario creare il repository, e per far questo, supponendo che la precedente variabile di ambiente sia impostata al valore /usr/local/cvsroot, basta impartire il comando

cvs init

oppure, specificando esplicitamente il path:

cvs –d /usr/local/cvsroot init

Tale comando si limita ad inizializzare le strutture dati interne del CVS senza sovrascrivere o cancellare eventuali file memorizzati all’interno del repository. Attenzione, la scrittura nella directory /usr/local, non è necessariamente concessa a tutti gli utenti, quindi tale comando dovrà essere impartito come root; dopo, per rendere tale repository effettivamente utilizzabile da tutti gli utenti di un certo gruppo (supponiamo users) si dovrà impartire il comando

chmod –R root:users /usr/local/cvsroot

Popolare il repository

Abbiamo quindi creato un repository vuoto, ed occorre popolarlo con i file del progetto, se questo è già esistente, oppure con la struttura di un nuovo progetto. Poiché rinominare o spostare i file è un’operazione scomoda col CVS, è opportuno pianificare in anticipo quale sarà la struttura di un nuovo progetto prima di passare alla fase di creazione. Vediamo adesso un esempio pratico: supponiamo che ci siano due programmatori, Lorenzo e Antonio, che devono lavorare al programma hello (si tratta, tanto per cambiare, del classico hello world, la versione iniziale è riportata nel Listato 1).

Supponiamo che Lorenzo abbia già due file iniziali del programma (un sorgente, il classico hello world, e relativo Makefile), e che abbia la variabile CVSROOT impostata al valore /usr/local/cvsroot, dopo essersi posizionato sulla directory in cui sono memorizzati i sorgenti (supponiamo ~/progetto/hello), lancerà il comando cvs import:

lorenzo> cvs import -m "Hello Program" hello Lorenzo start
N hello/Makefile
N hello/hello.c
No conflicts created by this import

L’opzione –m serve specificare un commento (sarà utilizzata anche in fase di commit) e se si omettesse, verrebbe aperto l’editor predefinito per aggiungere tale commento (questa alternativa è utile se il commento deve occupare più righe), hello è il nome del programma, Lorenzo il nome del creatore, e start è il tag iniziale (parleremo più avanti dei tag). A questo punto i file possono essere cancellati: sono memorizzati nel repository, e si può utilizzare il comando cvs checkout per recuperare una copia, su cui lavorare, di tali sorgenti dal repository:

lorenzo> cd ..
lorenzo> rm -rf hello

lorenzo> cvs checkout hello
cvs checkout: Updating hello
U hello/Makefile
U hello/hello.c

le varie directory CVS sono create automaticamente e non devono essere cancellate; grazie a queste non è nemmeno più necessario specificare il path del repository.

Modificare i sorgenti

Adesso si modifica il file hello.c, aggiungendo l’istruzione return 0, ed il tipo int a main per eliminare il messaggio di warning in fase di compilazione (se utilizzate Emacs come editor, noterete che riconosce se si tratta di un file contenuto in un repository).

Per effettuare il commit si esegue il seguente comando:

lorenzo> cvs commit -m "aggiunto return 0 e int main per evitare warning" hello.c
Checking in hello.c;
/usr/local/cvsroot/hello/hello.c,v  <--  hello.c
new revision: 1.2; previous revision: 1.1
done

Si è specificato il file di cui si vuole fare il commit; se non viene specificato, si effettua automaticamente il commit di tutti i file modificati nella directory corrente e ricorsivamente nelle sottodirectory. Noterete che il numero di revisione del file è stato incrementato automaticamente.

Modifichiamo ulteriormente il file hello.c aggiungendo un commento all’inizio del programma.

Visualizzare lo stato dei sorgenti

Il comando cvs status permette di vedere lo stato di un particolare file o di tutti i file nella directory corrente

lorenzo> cvs status
cvs status: Examining .
File: Makefile  Status: Up-to-date

   Working revision:    1.1.1.1 Mon Mar  1 08:56:36 1999
======================
File: hello.c  Status: Locally Modified

   Working revision:    1.2     Mon Mar  1 09:05:31 1999

Come si può vedere (l’output è stato tagliato per comodità) lo status di hello.c indica che la nostra copia del file è stata modificata e non è ancora stato fatto il commit del file. Effettuiamo nuovamente il commit (specificando un commento opportuno).

Antonio effettuerà il checkout del repository sulla propria directory lavoro (per chi non avesse la possibilità di creare un ulteriore utente, si può simulare la situazione effettuando il checkout in un’altra directory).

antonio> cvs checkout hello
cvs checkout: Updating hello
U hello/Makefile
U hello/hello.c    

Aggiungere dei file sorgente

Supponiamo che Antonio faccia alcune modifiche al programma, ad esempio aggiunga un altro file al progetto saluti.c (con relativo file .h) con una funzione saluta che effettua il printf.

Il comando cvs update specifica quali file il programmatore ha aggiunto modificato o cancellato; in questo caso se Antonio digita tale comando, avrà in output:

antonio> cvs update
cvs update: Updating .
M Makefile
M hello.c
? saluti.c
? saluti.h
? hello  

La M indica che quei file sono stati modificati, ma ancora non è stato fatto il commit, mentre il ? indica che tali file non sono presenti nel repository; tralasciando hello, che è l’eseguibile (notare che il CVS ignora di default i .o e i file che terminano con ~), è necessario aggiungere i due file saluti.c e saluti.h; quindi dopo aver effettuato un make clean, si impartisce il comando cvs add:

antonio> make clean
rm -f *~
rm -f *.o

antonio> cvs add saluti.*
cvs add: scheduling file 'saluti.c' for addition
cvs add: scheduling file 'saluti.h' for addition
cvs add: use 'cvs commit' to add these files permanently 

il CVS fa notare che al momento i file non sono stati fisicamente aggiunti al repository (effettuando cvs update, i due file saranno indicati con una A), e per far questo si deve fare il commit (cvs commit -m “aggiunto il sorgente saluti”).

Log delle modifiche

A questo punto, supponiamo che Lorenzo torni a lavorare ai sorgenti; la prima cosa da fare è controllare di avere la copia più recente dei sorgenti; anche in questo caso si utilizza il comando update (se si lanciasse adesso il comando cvs status, sarebbe visualizzato per entrambi i due file “Needs patch”, per indicare appunto che è necessario aggiornare la propria copia dei sorgenti):

lorenzo> cvs update
cvs update: Updating .
U Makefile
U hello.c
U saluti.c
U saluti.h
? hello 

come si vede adesso la nostra copia dei sorgenti è stata aggiornata per riflettere le modifiche presenti nel repository.

Il comando cvs log permette di vedere i vari log (cioè i commenti aggiunti in fase di commit), per visualizzare la storia delle modifiche occorse al file sorgente specificato. In questo modo Lorenzo ha la possibilità di visualizzare i commenti sulle modifiche apportate da Antonio, in quanto ogni log riporta anche il nome dell’utente che ha apportato le modifiche.

Ad ogni modifica di cui viene fatto il commit viene assegnato un numero di versione o revisione; ogni file avrà un numero diverso, che non seguirà quello degli altri file (ad esempio nel nostro caso hello.c ha versione 1.4, mentre Makefile ha solo 1.2, in quanto quest’ultimo ha subito meno modifiche). E’ possibile recuperare in ogni momento qualsiasi versione di un file; ad esempio se si volesse recuperare la versione 1.3 di hello.c si potrebbe specificare esplicitamente tale numero con l’opzione -r (si consiglia di non eseguire adesso questo comando):

cvs update -r 1.3 hello.c

Tuttavia a volte può essere utile dare dei nomi simbolici ad una certa versione: non è facile tenere a mente dei numeri; nel caso si debba recuperare un solo file, questo può non essere un problema, basandosi sull’output del comando cvs log. Il problema però peggiora quando si vuole recuperare un intero modulo (o comunque un insieme di file): si dovrebbe specificare per ogni file il numero di versione appropriato, e questo potrebbe non essere banale.

Fortunatamente è possibile assegnare dei tag sia a singoli file che a gruppi di file. Ad esempio possiamo assegnare il tag prima_prova (non si possono utilizzare i punti nei nomi di tag) a tutti i nostri sorgenti attuali.

lorenzo> cvs tag prima_prova
cvs tag: Tagging .
T Makefile
T hello.c
T saluti.c
T saluti.h 

in questo modo, se dopo qualche modifica si volesse riavere la stessa situazione attuale, basterebbe effettuare il check out specificando tale tag

cvs checkout -r prima_prova hello

Risolvere i conflitti

Un altro problema che ci eravamo posti all’inizio era anche quello di evitare che le modifiche di un programmatore sovrascrivessero quelle di un altro. Del resto si è visto che lanciando il comando cvs update i file modificati nel repository sovrascrivono automaticamente quelli nella directory attuale. Anche in questo caso il CVS ci viene in aiuto col meccanismo del merging. Supponiamo che entrambi i due nostri programmatori modifichino, contemporaneamente e indipendentemente, il programma principale in modo che venga stampato i nomi degli autori (Listato 2).

Poiché i due programmatori non sanno che stanno agendo sullo stesso programma, può darsi che Antonio effettui subito il commit. A questo punto anche Lorenzo potrebbe effettuare il commit, tuttavia è buona norma, prima di effettuare tale operazione, effettuare prima un cvs update, infatti in questo caso Lorenzo otterrebbe questo output:

lorenzo> cvs update hello.c
RCS file: /usr/local/cvsroot/hello/hello.c,v
retrieving revision 1.4
retrieving revision 1.5
Merging differences between 1.4 and 1.5 into hello.c
rcsmerge: warning: conflicts during merge
cvs update: conflicts found in hello.c
C hello.c

Ecco la cosa davvero interessante: CVS si è accorto che entrambi hanno effettuato delle modifiche sullo stesso file “contemporaneamente”, ed ha effettuato il merging dei due file segnalando le eventuali parti in conflitto (come in questo caso) nel seguente modo (vedere il file hello.c in Listato 3 dopo l’operazione del merging): la parte fra i <<<< e i ==== è la modifica apportata dal programmatore attuale (Lorenzo), mentre quella fra ==== e >>>> è quella effettuata dall’altro programmatore. Si può notare che Lorenzo aveva anche aggiunto un commento all’inizio del file, mentre Antonio no, e quindi in questo caso il merging non ha provocato conflitti, mentre entrambi hanno agito sulla riga dopo la chiamata di saluti(), e questo provoca un conflitto.

Un conflitto deve essere risolto manualmente dal programmatore (il CVS non può decidere); è da notare che il file al momento non è nemmeno compilabile, a causa dei caratteri superflui, comunque una versione di backup è presente nel file .#hello.c.1.4, creato automaticamente dal CVS. A questo punto il programmatore può scegliere come risolvere il conflitto: ad esempio in questo caso può decidere che è meglio la soluzione di Antonio e quindi può ripulire il file dai caratteri superflui e dall’altra soluzione. E’ importante notare che il merging è un’operazione puramente lessicale, e quindi dopo un merging (anche senza conflitti) non è detto che la compilazione vada a buon fine. Una volta effettuato il merging si può effettuare il commit. E’ quindi fondamentale fare sempre un cvs update prima di un commit!

Al momento che Antonio effettuerà nuovamente un update avrà l’ultima versione del sorgente (notare che in questo caso non avviene il merging, a meno che nel frattempo Antonio non abbia fatto ulteriori modifiche).

Branches e patches

A questo punto i due programmatori potrebbero decidere (di comune accordo) che il programma è pronto per la prima release. In questo caso è fondamentale “taggare” tutti i file ad esempio col tag prima_release.

cvs tag prima_release

Supponiamo di trovarci nel seguente scenario: lo sviluppo di tale programma prosegue, tuttavia, dopo diverso tempo che si sta lavorando alla versione 2 del prodotto, ci viene segnalato un bug nella prima versione; ovviamente non si può aspettare di rilasciare la seconda versione col bug corretto (tra l’altro non è nemmeno detto che nella seconda versione il bug sia presente). Del resto se si recuperasse la prima versione e si effettuasse le modifiche ed il commit si andrebbe a sovrascrivere la versione attuale. Fortunatamente in CVS esiste la possibilità di creare dei rami (branch) indipendenti; quando si recupera i sorgenti da un certo ramo, le operazioni di commit riguardano solo quel ramo.

A questo punto, ci possiamo spostare in una directory differente (ad esempio progetto/patch1) e lanciare il seguente comando

lorenzo> cvs rtag -b -r prima_release patch_prima_release hello
cvs rtag: Tagging hello

in questo modo si crea un branch, a partire dalla versione prima_release, chiamato patch_prima_release. Si può adesso effettuare il check out specificando il nome di tale ramo:

lorenzo> cvs checkout -r patch_prima_release hello

Si può correggere il bug (in questo esempio scriveremo solo un commento), e col commit salviamo le modifiche solo su questo branch (nel caso di branch i numeri di versioni contengono più parti, ad esempio nel caso di questa modifica il file hello.c viene salvato con numero 1.6.2.1 indicando che si tratta di un ramo partito dalla versione 1.6 del file).

Se si pensa che sia giusto si può anche fare il merge (il merge può essere richiesto esplicitamente con l’opzione -j) di questa versione corretta con la versione attuale: si deve tornare nella directory dove abbiamo l’ultima versione dei file (spesso denominata main trunk) e lanciare il comando

lorenzo> cvs update -j patch_prima_release

Una volta risolti eventuali conflitti, si potrà effettuare il commit nella versione 2 in fase di sviluppo.

Inoltre possiamo crearci un file diff di patch applicabile col comando patch, classico in ambiente Unix (chi non ha mai applicato una patch al kernel di Linux?):

lorenzo> cvs diff -c -r prima_release -r patch_prima_release hello >
patch_rel1

con questo comando chiediamo di creare un file diff (patch_rel1) che contenga i cambiamenti necessari per portare i file della versione prima_release ad essere uguali a quelli della versione patch_prima_release.

Login ed utilizzo di CVS in rete

Vediamo adesso come è possibile utilizzare il CVS in rete, in particolare vedremo come accedere al repository CVS di GNOME [GNU]. Per accedere ad un server CVS remoto è possibile utilizzare tutti i comandi visti finora, con l’unica differenza che è necessario effettuare prima il login. Per far questo è necessario cambiare prima il valore della variabile CVSROOT tramite il comando:

export CVSROOT=:pserver:anonymous@anoncvs.gnome.org:/cvs/gnome”

Il primo campo pserver indica al CVS che si vuole accedere ad un repository situato su un server remoto. anonymous è il nome dell’utente con quale si vuole fare il login, in questo caso si usa un accesso pubblico. anoncvs.gnome.org è il nome della macchina che in questo caso fa da server. Infine, /cvs/gnome è il percorso del repository sulla macchina remota.

A questo punto è possibile effettuare il login con il comando:

cvs login
(Logging in to anonymous@anoncvs.gnome.org)
CVS password:

digitata la password ( in questo caso basta premere invio) sarà possibile recuperare i file con il comando check out.

Quando si opera in questa modalità il server CVS può anche restituire una copia compressa di un intero modulo, ad esempio per prendere il modulo del WindowManager Enlightenment del cvs del progetto Gnome è possibile utilizzare il comando:

cvs -z3 get enlightenment

che effettua un download del codice sorgente ma in forma compressa (opzione –z3), riducendo il tempo di trasferimento.

Il giorno successivo, se si vuole aggiornare la propria copia dei sorgenti, basta utilizzare, dopo aver effettuato il login, il comando:

cvs update -d -P

Installare un server CVS

Per creare un server CVS il metodo più veloce è quello di modificare il file /etc/inetd.conf aggiungendo la riga:

cvspserver stream  tcp  nowait  root  /usr/ bin/cvs cvs -b /usr/ bin pserver

Quindi controllare che sia presente in /etc/services la riga:

cvspserver      2401/tcp

Infine occorre riavviare il demone inetd. In tale modo il vostro server CVS risponderà solo quando ci sarà effettiva richiesta da parte di qualche client.

Per controllare gli accessi bisogna gestire il file /CVSROOT/passwd all’interno del repository che contiene delle password codificate in modo simile ad /etc/passwd ed assegnare i permessi desiderati alle directory che compongono i vari moduli del repository

Conclusioni

CVS dimostra di essere quindi un sistema molto potente e versatile. Un altro sistema di controllo di versione: l’RCS. L’RCS, sempre distribuito dalla GNU, non è però disegnato per lavorare in rete, dovrete sempre essere collegati via terminale sulla macchina dove risiede il sistema RCS. CVS slega da questi vincoli e permette l’effettiva implementazione del telelavoro via Internet.

Presso il sito ftp dell’Infomedia potrete scaricare un archivio .tgz contenente il repository; una volta scompattato nella directory /usr/local/cvsroot, potrete avere a disposizione tutte le varie versioni dei listati di cui abbiamo parlato.

Molte risorse sul CVS si possono trovare su [CVS1] e [CVS2].

Riferimenti

[GNU] http://download.cyclic.com/pub/cvs-1.10/www.gnome.org

[CVS1] http://www.loria.fr/~molli/cvs-index.html

[CVS2] http://www.cyclic.com