Versionsverwaltung mit CVS
von Matthias Kranz (kranz@fokus.gmd.de)

CVS (Concurrent Versions System) ist ein Versionsverwaltungssystem, geeignet zum Quellcode-Management. Generell lässt sich mit diesem System jede Art von Quelltexten, wie z.B. Webseiten oder Konfigurationsdateien, verwalten. CVS ist plattformübergreifend, unterstützt Multiple-Developer-Entwicklung und ist Client/Server-basiert. Ursprünglich als Frontend für RCS entworfen, lässt es sich heute völlig unabhängig davon installieren.

Inhalt

Am Anfang war das Chaos
Erste Schritte
Jedem Entwickler seinen Sandkasten
Dateien verändert - und nun?
Veränderungen bestätigen
Log und Diff
Konflikt-Beratung
Hinzufügen und Löschen
Tagging
Weitere Möglichkeiten

Am Anfang war das Chaos

Welcher Entwickler kennt das Problem nicht. Man programmiert und programmiert, freut sich über Fortschritte, macht ein dicke Ankündigung für ProgrammXYZ-2.1.19 auf freshmeat.net, begibt sich an die nächsten Features der gegen unendlich strebenden To-Do-Liste, und dann meldet sich der erste Benutzer, dem ein Bug in der etwas älteren Version 2.0.10 aufgefallen ist. Man ahnt Schlimmes, und siehe da, tatsächlich: Der Fehler ist auch in der aktuellsten Fassung enthalten. Hm, der muss sich irgendwann eingeschlichen haben, denn in Release 2.0 tritt er definitiv nicht auf. Was also tun? Tja, man müsste ein System haben, welches einem genau Buch über jede kleine Veränderung einer Datei führt. Am besten sogar, wenn es möglich wäre Schritt für Schritt die einzelnen Versionen zu reproduzieren, um eventuell so den Fehler einzugrenzen. Genau das ist die Idee, die hinter dem amerikanischen Begriff Software-Configuration-Management steckt. RCS (Revision Control System) und SCCS (Source Code Control System) sind in diesem Zusammenhang wohl die bekannteren der unter Unix verwendeten Tools. CVS erweitert deren Fähigkeiten allerdings erheblich.

Erste Schritte

Das Prinzip, welches hinter CVS steht, ist schnell erklärt: Es gibt ein zentrales Verzeichnis, das sogenannte Repository, in dem die Sourcen abgelegt werden. Dieses Verzeichnis muss für alle Entwickler, die an dem Projekt beteiligt sind, schreibbar sein, d.h. bei der Einrichtung ist auf entsprechende Rechtevergabe, am besten durch Anlegen einer gemeinsamen Gruppe, zu achten.

Das Anlegen an sich wird auch als Initialisieren des Repositorys bezeichnet. Gehen wir also davon aus, dass unser Verzeichnis /usr/local/repository heißt. Für die folgenden Schritte müssen wir dem CVS-System natürlich noch mitteilen, wo es dieses Verzeichnis findet. Dazu gibt es zwei Möglichkeiten: über eine Environment-Variable (s.u.) oder über die explizite Angabe durch die -d-Option. Zunächst die Variante mit der Umgebungsvariable: Dazu geben Benutzer der csh (oder einer ihrer Abkömmlinge)

setenv CVSROOT /usr/local/repository

oder User der bash

export CVSROOT=/usr/local/repository

ein. CVSROOT muss korrekt gesetzt werden, ansonsten kommt es z.B. zu solchen Fehlermeldungen:

$ cvs checkout quellcode
cvs checkout: No CVSROOT specified! Please use the '-d' option
cvs [checkout aborted]: or set the CVSROOT environment variable.

Jetzt folgt die Initialisierung des Repositorys:

$ cd /usr/local/repository
$ cvs init

(Wenn man die Umgebungsvariable CVSROOT gesetzt hat oder mit -d arbeitet, muss man nicht extra in das Repository wechseln.) Im ansonsten noch leeren Repository ist nun ein Unterverzeichnis CVSROOT erzeugt worden, in dem zentrale Administrationsdateien liegen. Im nächsten Schritt sollen nun beliebige Projektdateien unter CVS-Kontrolle gebracht werden. In unserem Beispiel handelt es sich um folgende Dateien im Verzeichnis /home/mskranz/idee/.

$ ls -l
total 1
-rw-r--r--   1 mskranz  users           0 Apr  3 16:35 README
-rw-r--r--   1 mskranz  users           0 Apr  3 16:35 datei1.c
-rw-r--r--   1 mskranz  users           0 Apr  3 16:35 datei2.c
-rw-r--r--   1 mskranz  users           0 Apr  3 16:35 datei3.c
drwxr-xr-x   2 mskranz  users        1024 Apr  3 16:35 include

Im Verzeichnis include befindet sich noch die Datei datei4.h.

$ cd /home/mskranz/idee
$ cvs -d /usr/local/repository import -m 'Kommentar' project tag1 tag2
N project/README
N project/datei1.c
N project/datei2.c
N project/datei3.c
cvs import: Importing /usr/local/repository//project/include
N project/include/datei4.h

No conflicts created by this import

Durch diese Operation wurde im Repository ein Verzeichnis project erzeugt, in dem nun sämtliche Dateien und Unterverzeichnisse aus /home/mskranz/idee/ abgelegt sind. Sie haben alle die Endung ,v bekommen, und bei näherer Betrachtung mit einem Editor fällt auf, dass sie mit Verwaltungsinformationen erweitert worden sind. Verzichtet man auf obige -m-Option und den dazugehörigen Kommentar, wird automatisch ein Editor geöffnet. Dieser kann über die Environment-Variable CVSEDITOR gesetzt werden, ansonsten wird EDITOR ausgewertet. tag1 und tag2 sind sogenannte Vendor- und Release-Tags und zwingend vorgeschrieben. Für unsere Belange sind diese Tags zunächst nicht wichtig, weshalb wir sie beliebig wählen können. Ihre Bedeutung wird im CVS-Handbuch (s. Weitere Informationen) genauer beschrieben.

Jedem Entwickler seinen Sandkasten

Zum Anlegen eines lokalen Arbeitsverzeichnisses benutzt man den checkout-Befehl. Auf diese Art und Weise kann man beliebig oft den Inhalt des Repositories reproduzieren. Man legt in seinem Heimatverzeichnis beispielsweise das Verzeichnis work an und führt das Folgende aus:

$ cd ~/work
$ cvs -d /usr/local/repository checkout project 
cvs checkout: Updating project
U project/README
U project/datei1.c
U project/datei2.c
U project/datei3.c
cvs checkout: Updating project/include
U project/include/datei4.h

Neben den Projektdateien ist auch ein Verzeichnis CVS angelegt worden. In diesem liegen die Verwaltungsdateien, in denen auch die Information hinterlegt wird, aus welchem Repository die Dateien ausgecheckt worden sind. Bewegt man sich also innerhalb seines Arbeitsverzeichnisses, braucht man den Pfad zum Repository nicht explizit anzugeben.

Dateien verändert - und nun?

Jetzt arbeitet man also mit und an seinen Dateien, entwickelt vielleicht sogar innerhalb einer größeren Gruppe, und nun stellt sich natürlich die Frage, wie werden die Dateien konsistent gehalten, bzw. wie befördert man seine veränderten Files wieder in das Repository? Im schwierigsten Fall haben ja zwei oder mehr Programmierer an der gleichen Quelldatei gearbeitet. Wessen Veränderungen haben die höchste Priorität? Nun, CVS begegnet dieser Problematik zunächst einmal damit, dass man gezwungen wird, vor dem Einchecken, dem sogenannten committen, ein Update seines Arbeitsverzeichnisses durchzuführen. Versucht man eine Datei einzuchecken, die zwischenzeitlich von jemandem erneuert worden ist, erhält man eine Fehlermeldung:

$ cvs commit -m "procedure init_main changed" datei3.c
cvs commit datei3.c 
cvs commit: Up-to-date check failed for `datei3.c'
cvs [commit aborted]: correct above errors first!

Man wird also aufgefordert, zunächst ein Update vorzunehmen. Nun denn, nichts Leichteres als das.

$ cvs update
cvs update
cvs update: Updating .
RCS file: /home/developer/repository/project/datei3.c,v
retrieving revision 1.1.1.1
retrieving revision 1.2
Merging differences between 1.1.1.1 and 1.2 into datei3.c
M datei3.c

Dieses Updaten verlief ohne Probleme, d.h. die veränderte Datei datei3.c aus dem Repository konnte ohne Konflikte mit der Arbeitskopie verschmolzen werden. Dieses Verfahren hat sich in der Praxis als relativ zuverlässig erwiesen. Man sollte natürlich an dieser Stelle Vorsicht walten lassen und nach Merging-Meldungen anschließend überprüfen, ob sich die neu entstandenen Files immer noch compilieren lassen (im Falle der Verwaltung von Sourcecode). Verläuft das Verschmelzen allerdings nicht reibungslos, wird man von CVS durch Konfliktmeldungen darauf aufmerksam gemacht.

$ cvs update
cvs update: Updating .
RCS file: /home/developer/repository/project/datei3.c,v
retrieving revision 1.1.1.1
retrieving revision 1.2
Merging differences between 1.1.1.1 and 1.2 into datei3.c
rcsmerge: warning: conflicts during merge
cvs update: conflicts found in datei3,c
C datei3.c

CVS meldet in diesem Fall also, dass es nicht in der Lage war, die beiden verschiedenen Dateien zusammenzufügen und legt beide Varianten, entsprechend markiert, in der betreffenden Datei ab. Nun bleibt es dem Programmierer überlassen, den Inhalt zu überprüfen und anschließend die Datei einzuchecken. Was in diesem Fall zu tun ist, erfahren sie weiter unten im Abschnitt Konfliktberatung. Wurde eine Datei, die man nur importiert, selbst aber gar nicht editiert, inzwischen verändert, wird sie einfach aktualisiert. CVS macht uns durch verschiedene Meldungen darauf aufmerksam. Wie oben zu sehen, steht am Anfang der Zeile vor jeder Datei ein einzelner Buchstabe. Dabei steht U für Update, M für Modified und C für Conflict. Durch das M wird angezeigt, dass man selbst als Letzter die Datei bearbeitet hat und etwaige Änderungen allen anderen noch nicht bekannt sind. Denn, das sollte man sich immer wieder klar machen, diese Aktionen betreffen und manipulieren zunächst nur die Files im eigenen Arbeitsverzeichnis.

Veränderungen bestätigen

Nachdem das Updaten keine Fehler beim Verschmelzen gemeldet hat, sollte man seine bearbeiteten Sourcen in das Repository einchecken. Dazu dient der Befehl commit.

$ cvs commit -m "some bug fixes" datei3.c
Checking in datei3.c;
/home/developer/repository/src_import/datei3.c,v  <-  datei3.c
new revision: 1.2; previous revision: 1.1
done

Dabei wird automatisch der Editor geöffnet, und man wird aufgefordert eine Log-Message einzugeben, eine kurze Beschreibung des Vorgangs. Mehr dazu später. Nun sind die Veränderungen für jedermann im Repository sichtbar, und jeder andere wird beim Auschecken bzw. Updaten seiner Sourcen die neue Version erhalten. Wie man sehen kann, wird bei obigem Vorgang automatisch die Revisionsnummer erhöht.

Log und Diff

In jedem von uns steckt ein gewisser Grad an Neugier - beim einen mehr, beim anderen weniger. Jetzt beschäftigt uns allerdings die Frage, was zwischen verschiedenen Revisionen passiert ist. Was macht den Unterschied zwischen 1.57 und 1.58 aus? Dazu verwendet man den CVS-Befehl log (wie überraschend!).

$ cvs log datei1.c
RCS file: /home/developer/repository/src_import/datei1.c,v
Working file: datei1.c
head: 1.2
branch:
locks: strict
access list:
symbolic names:
        tag2: 1.1.1.1
        tag1: 1.1.1
keyword substitution: kv
total revisions: 3;     selected revisions: 3
description:
----------------------------
revision 1.2
date: 1998/07/12 13:22:03;  author: developer;  state: Exp;  lines: +3 -0
mkr
----------------------------
revision 1.1
date: 1998/07/12 12:41:47;  author: developer;  state: Exp;
branches:  1.1.1;
Initial revision
----------------------------
revision 1.1.1.1
date: 1998/07/12 12:41:47;  author: developer;  state: Exp;  lines: +0 -0
Kommentar
==============================================

Liest man die geballte Information von unten nach oben, wird einem das Prinzip schnell klar. Kommentar aus der letzten Zeile dürfte uns vom Anlegen des Repository bekannt sein. Wer sich hier für die Details interessiert, sei auf das CVS-Handbuch verwiesen. Vielleicht nur so viel: Bei Revision 1.2 folgt in der nächsten Zeile zunächst der genaue Zeitpunkt der Veränderung sowie der Autor der Datei. In der folgenden Zeile steht der Log-Kommentar, der beim commit-Vorgang im Editor eingegeben wurde. In diesem Fall nicht sonderlich aussagekräftig.

Interessiert man sich für bestimmte Aktionen im Detail, hat man nun mit Hilfe des diff-Befehls die Möglichkeit, sich genauestens zu informieren.

$ cvs diff -c -r 1.1 -r 1.2 datei1.c
Index: datei1.c
====================================================
RCS file: /home/developer/repository/src_import/datei1.c,v
retrieving revision 1.1
retrieving revision 1.2
diff -c -r1.1 -r1.2
*** datei1.c    1998/07/12 12:41:47     1.1
--- datei1.c    1998/07/12 13:22:03     1.2
***************
*** 0 ****
--- 1,3 ----
+ 
+ 
+ 
Die Option -c steht dabei für ein etwas ausführlicheres Format, die beiden folgenden -r spezifizieren die gewünschten Revisionen. Hier lassen sich auch größere Abschnitte auswählen. Schaut man sich die Ausgabe genauer an, so erkennt man folgendes System. Die Sterne *** kennzeichnen die ältere Version der Datei. In diesem Fall war es also so, dass die anfangs leere Datei beim Sprung von Version 1.1 nach 1.2 um die Zeilen 1-3
+ 
+ 
+ 
ergänzt wurden. Benutzer des patch-Programms dürften sich an dieser Stelle erinnert fühlen.

Konflikt-Beratung

Nein, hier findet man sich nicht in einer Nachmittags-Talkshow wieder. Unser Augenmerk liegt auf Merging-Konflikten, die wie oben erwähnt, beim update-Befehl auftreten können. Sie treten nur dann auf, wenn man einen Abschnitt einer Datei editiert hat, der gleichzeitig auch von einem anderen Entwickler bearbeitet wurde. Zur Erinnerung sehen wir uns die zweite Update-Meldung (siehe oben) an. Die Datei wurde durch ein C am Zeilenanfang gekennzeichnet. Sehen wir uns die Datei mit einem Editor näher an:
<<<<<<< datei3.c



=======



>>>>>>> 1.2

Von <<<<<<< datei3.c angefangen bis zu ======= sieht man den eigenen Teil, daran anschließend bis >>>>>>> 1.2 den Teil eines anderen. In diesem Fall handelt es sich nur um unterschiedliche Kommentarzeilen. Ist aber Sourcecode verändert worden, muss man sich eine Lösung des Problems, eventuell in Absprache mit dem anderen Entwickler, überlegen. Nach der Anpassung löscht man noch die Markierungen, und nun steht einem commit nichts mehr im Wege.

$ cvs commit datei3.c
Checking in datei3.c;
/home/developer/repository/src_import/datei3.c,v  <-  datei3.c
new revision: 1.3; previous revision: 1.2
done

Wie schon gewohnt wird der Editor aufgerufen, und man ist aufgefordert, einen Kommentar einzugeben.

Hinzufügen und Löschen

Für CVS bedeutet das Löschen oder Hinzufügen von Dateien nichts anderes, als eine Änderung am bestehenden Datenbestand. Das hat auch einen einleuchtenden Grund. Wie schon erwähnt, ist es ja unter CVS möglich, auf beliebige Versionen einzelner Dateien zurückzugreifen. Das heißt, man kann z.B. nach Belieben eine lauffähige Variante seines Programms auschecken, ohne auf aktuellere, aber möglicherweise fehlerbehaftete Teile Rücksicht nehmen zu müssen. Nun kann es ja durchaus vorkommen, dass in dieser Version Dateien vorhanden sind, die in der weiteren Entwicklung wieder verschwunden sind. Sie werden aber weiterhin mitverwaltet und existieren immer noch im Repository, auch wenn sie beim jetzigen Checkout nicht mehr auftauchen würden. Ebenso weiß CVS nichts von neuen Dateien, die lediglich im Arbeitsverzeichnis, aber noch nicht im Repository auftauchen. Diese müssen zunächst mit einem add hinzugefügt und dann mit dem schon bekannten commit-Befehl eingecheckt werden.

$ cvs add datei5.c
cvs add: scheduling file `datei5.c' for addition
cvs add: use 'cvs commit' to add this file permanently
$ cvs commit -m "new file with new functions" datei5.c
/home/developer/repository/src_import/datei5.c,v  <-  datei5.c
initial revision: 1.1
done

Wie man erkennen kann, beginnt jede Datei mit der initialen Revisionsnummer 1.1. Das Löschen funktioniert ganz ähnlich, und wenn man das zugrundeliegende Konzept von CVS verstanden hat, kann man sich schon vorstellen, was folgen muss. Zunächst löscht man mit Hilfe des gewohnten Unix-Befehls rm die Datei aus seinem Arbeitsverzeichnis. Anschließend markiert man sie zum Löschen. Durch Ausführen des commit-Befehls wird die Datei "endgültig" entfernt. Mit endgültig ist es diesem Fall nur gemeint, dass sie beim nächsten checkout bzw. update nicht mehr erscheint.

$ rm datei5.c
$ cvs remove datei5.c
cvs remove: scheduling `datei5.c' for removal
cvs remove: use 'cvs commit' to remove this file permanently
$ cvs commit -m "no usage any more ..." datei5.c

Removing datei5.c;

In diesem Zusammenhang kann man sich diese Eigenschaft von CVS praktisch zunutze machen. Möchte oder muss man die Veränderungen, die man an einer Datei vorgenommen hat, rückgängig machen, sozusagen ein Undo ausführen, löscht man die entsprechende Datei einfach aus seinem Verzeichnis und führt dann den update-Befehl aus. CVS erkennt, dass eine Datei fehlt, und man bekommt das File in seinem aktuellsten Zustand.

Komplizierter wird da schon ein Umbenennen von Files. Eine mögliche Lösung in diesem Fall ist mit den schon bekannten Befehlen zu erreichen. Zuerst gibt man der entsprechenden Datei einen neuen Namen. Im Anschluss muss man nun die alte Version aus dem Repository löschen. Dazu dient ja der rm-Befehl. Natürlich darf man nicht vergessen, dass neu benannte File mit Hilfe des add-Befehls dem Repository hinzuzufügen und die gesamte Aktion mit einem commit abzuschliessen. Nicht sehr elegant und auch mit einem Nachteil behaftet, denn die Log-Daten der Datei werden natürlich nicht übernommen.

Tagging

Es wurde ja schon erwähnt, dass ein wichtiger Vorteil der Versionsverwaltung von CVS darin liegt, jederzeit Zugriff auf den älteren Stand einer Entwicklung zu haben. So kann man sehr einfach, beispielsweise sobald man einen stabilen Zustand seiner Software erreicht hat, das komplette Paket mit einem Tag markieren und ist nun immer in der Lage, genau diese Version wieder auszuchecken.

$ cvs tag rel-1-0 .
cvs tag: Tagging .
T README
T datei1.c
T datei2.c
T datei3.c
cvs tag: Tagging include
T include/datei4.h

An strategisch wichtigen Punkten in der Entwicklung eines Projektes (in diesem Fall einer Release) ist es sinnvoll, nicht nur einzelne Dateien, sondern wie in diesem Fall ein gesamtes Verzeichnis zu markieren. Hier lautet es rel-1-0, und ab jetzt ist es möglich, über die Angabe dieses Tags alle obigen Dateien im jetzigen Zustand zu erhalten. Der Befehl status mit der Option -v zeigt einem ausführlich die entsprechenden Markierungen einer Datei.

$ cvs status -v datei3.c
===================================================
File: datei3.c          Status: Up-to-date

   Working revision:    1.3     Sun Jul 12 14:25:11 1998
   Repository revision: 1.3     /home/developer/repository/src_import/datei3.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)

   Existing Tags:
        release-1-0                     (revision: 1.3)
        tag2                            (revision: 1.1.1.1)
        tag1                            (branch: 1.1.1)

Möchte man beispielsweise die Version von datei2.c bearbeiten, die mit rel-1-0 markiert worden ist, kann man folgendes eingeben:

$ cvs update -r rel-1-0 datei2.c
cvs update:
U datei2.c

Ebenso ist es möglich, den gestrigen Stand eines Projektes wiederherzustellen:

$ cvs -d /usr/local/repository checkout -D yesterday project

Weitere Möglichkeiten

Die in diesem Vortrag vorgestellten Möglichkeiten erschöpfen bei weitem nicht die Fähigkeiten, die in CVS stecken. Beispielsweise kann man an beliebiger Stelle seine Projekte verzweigen lassen (Branching), um entweder neue Features zunächst zu testen, bevor sie in den Hauptteil einfließen, oder aber, um an einer Stelle, wie in der Einleitung beschrieben, einen Bugfix zu erarbeiten. Diese Zweige kann man dann wieder mit dem Hauptast verschmelzen. Nähere Informationen zum Branching findet man im CVS-Handbuch von Per Cederqvist.

Aber nicht nur Entwickler können von den Vorteilen von CVS profitieren. Zum Beispiel kann man die Konfigurationsdateien seines Linux-Systems (/etc/*) unter CVS-Kontrolle stellen. Erweisen sich Veränderungen als unvorteilhaft, oder möchte man einfach nur zwischen mehreren verschiedenen Versionen wechseln können, kann man diese Aufgabe mit CVS lösen. Auch gibt es ja viele Menschen, die sich relativ häufig die neuesten Snapshots eines Software-Pakets herunterladen, dann aber jedes Mal vor dem Übersetzen lokale Anpassungen vornehmen müssen. Auch diese Problematik lässt sich mit dem Concurrent Versions System erleichtern.

Die Stärken von CVS liegen eindeutig im Multiple-Developer-Bereich. Zahlreiche Projekte im Internet benutzen es, um viele verschiedene Entwickler in aller Welt an der Weiterentwicklung eines Stücks Software teilhaben zu lassen. Dass CVS sich dabei hinter kommerziellen Produkten nicht zu verstecken braucht, zeigen z.B. der Einsatz im OpenBSD-Projekt (mehr als 1349 MB Sourcen, ca. 75000 Dateien) oder bei Bentley (2,5 GB Sourcen, fast 100000 Dateien).


Aufbau eines CVS-Befehls
cvs [ cvs_options ] command [ command_options ] [ command_args ]
cvs_options Einige Optionen, die alle Sub-Befehle beeinflussen
command Einer der vielen Sub-Befehle, die teilweise abgekürzt werden. Nur cvs -H (Hilfe für command) und cvs -v (Version) kommen ohne command aus.
command_options Optionen, die sich auf command beziehen.
command_args Argumente für command

Bezugsquellen
http://download.cyclic.com/pub/
ftp://download.cyclic.com/pub/
Cyclic Download Seiten (liegt in Nordamerika)
ftp://gd.tuwien.ac.at/softeng/cvs/cyclic/
http://gd.tuwien.ac.at/softeng/cvs/cyclic/
ftp://ftp.ntrl.net/pub/mirror/cvs/
ftp://ftp.stacken.kth.se/pub/cvs/
ftp://ftp.pasteur.fr/pub/computing/cvs/
Europäische Mirror-Seiten

Weitere Informationen
http://www.cyclic.com
Cyclic Software - CVS-Maintainer
http://www.inf.fu-berlin.de/~kranz/scm/ Informationen über Software-Configuration-Management
http://www.loria.fr/~molli/cvs-index.html Pascal Mollis Informationssammlung
http://www.loria.fr/~molli/cvs/doc/cvs_toc.html Per Cederqvists CVS-Handbuch online
http://gille.loria.fr:7000/cgi-bin/faqomatic/faq.pl CVS - Frequently Asked Questions

Anmerkung: Dieser Vortrag ist in sehr ähnlicher Form als zweiteiliger Artikel in den Ausgaben 10/98 und 01/99 des Linux-Magazins erschienen.