CVS
CVS is the Concurrent Versions System. It is a source control/version control/revision control system like Git, Mercurial, Fossil, SVN, etc.
CVS has a pure client-server model. Unlike git’s distributed model, with CVS you work directly with the remote repository for doing things like committing, getting different revisions of files, comparing different revisions, and so on. You aren’t ever synchronising the state of your repository with a remote repository the same way you do with git.
Table of Contents
- Terminology
- Checking Out Source Code
- Committing Changes
- Adding a New File or Directory
- Removing a File or Directory
- Renaming a File or Directory
- Updating Your Working Directory
- Comparing File Revisions
- Reverting Changes
- Creating Tags
- The ~/.cvsrc File
I’ve written this document both as a reference for myself and as a quick start guide that can hopefully be useful to others wanting to learn CVS. Since git is a similar program that many likely already know, I’ve written this with comparisons to git’s functionality and with the assumption that the reader is already familiar with revision control systems.
For actual documentation, check out the cvs(1) manpage and the CVS book.
Terminology
A module is a single project (i.e. a collection of related files).
A repository is a collection of modules. Though sometimes this term is used to refer to a module/project as well. A repository can live on a remote machine or locally on your system.
A CVS server is simply a machine that has a CVS repository that allows people to check out code, commit changes, and so on.
You check out a particular module to create a copy of it on your system, called a working copy.
When you commit a change, the change gets immediately reflected in the CVS repository. There is no concept of making a local commit, then pushing that commit to a remote repository.
You update your local copy of the source code to bring down and merge changes in the repository with your local working copy. This is expected to be done regularly, as CVS will refuse to commit changes to an older version of a file (similar to how git won’t let you push your changes if the remote contains commits that you don’t have).
If you’re coming from git, a “module” (sometimes also called a repository or project) is equivalent to a git repository, a “repository” is equivalent to something like GitHub or a collection of git repositories, “checking out” is like cloning, “committing” is mostly the same, and “updating” is like pulling. It’s a bit more complicated than that, but this is a decent starting point.
Also, CVS stores its data in a CVS/ directory in each directory in
a given module whereas git stores its information in the .git/
directory at the top of a git repository.
Checking Out Source Code
Assuming you’re working with a repository that already exists, you can create a local working copy of a module in a repository like so:
cvs -d <remote>:<path_to_cvs_repository> checkout <module>
This will create the directory <module> in your current working
directory which contains a working copy of the source code.
For example, to check out a copy of the OpenBSD source:
cvs -d anoncvs@anoncvs.nl.openbsd.org:/cvs checkout -P src
The -d option specifies the CVSROOT, i.e. the root of the CVS
repository. This is necessary for commands that aren’t already working
on working copy of a project (working copies remember their
repositories).
The -P option will mean that if a directory is empty in the remote
repository, it will be pruned (removed) from your local working copy.
You may or may not want this.
The -D flag can also be used to check out a repository as it was at
a specific date and time. For example, this will check out a module as
it was at exactly 17:00 UTC on 2023-04-30:
cvs -d <CVSROOT> checkout -D "2023-04-30 17:00 UTC" src
This flag is also available for the update, diff, and other
commands.
Local Repositories
Although many use CVS with a remote repository which allows many people
to access and work on the code stored there; you can also create
a repository that simply exists on your local machine and work with
that. Creating a local repository is as simple as making a directory for
it (e.g. /var/cvs), then running cvs -d <dir> init. You can then
cvs import sources to create a module, or simply mkdir a directory
in the CVSROOT. Both must be followed by cvs checkout commands to
create working copies of the module.
# mkdir /var/cvs # chown/chmod this directory as required
$ cvs -d /var/cvs init
$ cd myproject
$ cvs -d /var/cvs import myproject vendor_tag release_tag
$ cd ..
$ cvs -d /var/cvs checkout myproject
Or, for an empty module, you can just:
$ mkdir /var/cvs/myproject
$ cvs -d /var/cvs checkout myproject
vendor_tag and release_tag are required by CVS when importing. More
information about these can be found under the book’s Tracking Third
Party
Sources
section. Since they don’t really matter in this context you can just set
them to something like your username and “start” respectively.
Committing Changes
Committing works similarly to git: you make changes to one or more files and commit those changes to have them enshrined in the version history of the repository.
In CVS, however, there is no additional “push” step to have changes appear in the remote repository. When you commit your changes, they are immediately checked in from your working copy to the repository.
Additionally, revisions are tracked per-file, not per-commit. That is to say, there is no “commit hash” representing the state of the repository at a given point in time, there is only specific revisions of individual files, represented by revision numbers that increase as changes to the file are made and committed.
Commits are done like so:
cvs commit [file(s)]
If files are specified, only the changes made to those files are committed. If no files are specified, then cvs will recursively commit all changed files in the current directory.
This command will also open your editor for you to type in a commit
message so you can explain your changes. You can use the -m flag
followed by a message as a shorthand if your message is not that long:
cvs commit -m "Made foo use bar instead" foobar.c
Adding a New File or Directory
To add a new file or directory to your repository, use the cvs
add command.
cvs add <file/dir>
Note that while adding a file will mark it to be added to the module on next commit, adding a directory happens instantly, without a commit required.
Also note that if you want to add a file in a subdirectory to the
module, that subdirectory must already be added to the module. For
example, if I try to add dir/file to my module without first adding
dir, I get:
$ cvs add dir/file
cvs add: in directory dir:
cvs [add aborted]: there is no version here; do 'cvs checkout' first
I must instead do:
$ cvs add dir
Directory /var/cvs/dir added to the repository
$ cvs add dir/file
cvs add: scheduling file `dir/file' for addition
cvs add: use 'cvs commit' to add this file permanently
If there are multiple files in a directory, you can also do:
cvs add dir/*
to add all of them.
Removing a File or Directory
Similarly, use the cvs remove command to remove a file or
directory:
cvs remove <file/dir>
CVS will refuse to remove a file from the repository if it still
exists on disk. That means you have to rm the file(s) or
directory(s) first, then run the remove command:
rm file
cvs remove file
Like with adding, removing a directory from the module does not require a subsequent commit.
Things that are removed are sent to the Attic/ directory on
the repository, which provides a way to recover from accidental
deletion.
Renaming a File or Directory
On many operating systems, renaming a file or directory is simply a copy operation plus a remove operation—a copy of a file or directory is made under a new name and the old one is removed. This is similar in CVS, where a file must be copied under a new name, the old file must be removed from CVS, and the new file must be added:
$ mv file newfile
$ cvs remove file
$ cvs add newfile
$ cvs commit -m "Rename file to newfile"
For a directory:
$ mkdir newdir
$ cvs add newdir
$ mv dir/* newdir
$ cd dir
$ cvs rm <files>
$ cd ../newdir
$ cvs add <files>
$ cvs commit -m "Moved dir to newdir"
This is quite cumbersome for directories. Luckily, it’s not something that is typically done often, and it can be mitigated by choosing a good layout for your project from the start.
Updating Your Working Directory
Update your local copy of the sources with the latest available in the remote repo:
cvs -q update -PAd
-q makes the output quieter, only printing out each file’s
status:
U <path> - New file created in your repository or updated on the remote
P <path> - Like U but a patch containing the changes was sent instead of the entire file
A <path> - File added by you that will be committed to the repository when you next commit
R <path> - Like the above but the file will be removed
M <path> - File has been modified by you. If there were any changes on the remote, they have been merged successfully.
C <path> - File has a conflict that must be fixed before running the next commit
-P tells update to prune (remove) local directories that are
now empty (i.e. the directory no longer contains
revision-controlled files, so get rid of it).
-A makes cvs grab the most up-to-date version of the file,
forgetting any sticky tags or dates you’ve set. From the
manpage: “If you get a working file using one of the -r, -D, or
-k options, cvs remembers the corresponding tag, date, or kflag
and continues using it on future updates; use the -A option to
make cvs forget these specifications, and retrieve the “head”
version of the file.”
-d tells update to create any directories that are missing in
your working copy but which exist in the repository. Without
this, CVS will not pull in any new directories that exist in the
repository but which don’t exist in your working copy.
Resolving Merge Conflicts
Conflict resolution looks very much like it does in git. For example:
<<<<<<< file
Eve is the best.
=======
Alice is the best.
>>>>>>> 1.2
Where the stuff before the === divider is your local working
copy and the stuff after is what’s in the revision stored in the
remote repository (in this case revision 1.2 of the file).
Comparing File Revisions
This is done with the diff command.
To view the changes you’ve made to your working copy of a file compared to the most recent committed revision:
cvs diff <file>
The diff command is recursive, so if a directory is specified then a diff is shown for each changed file underneath that directory recursively, and if no file is specified then diffs for all files under the current directory recursively are shown.
Two specific revisions can be compared by specifying the -r
flag:
cvs diff -r 1.1 -r 1.2 <file>
If only one -r is given, the revision of the current working
copy is used for the other.
The diff command is also how you can create a changeset that you can then send to a project’s mailing list for review and comment.
Reverting Changes
Reverting changes can be done with the -j flag to the update
command. In this case, if I have made some changes between
revisions 1.3 and 1.5 of a file that I want to revert (so the
file now goes back to the state it was at revision 1.3),
I would:
$ cvs update -j1.5 -j1.3 <file>
Which will create a new revision 1.6 that looks identical to 1.3. This has the effect of maintaining the changes that were later reverted as part of the history, which are almost always worth keeping.
If you want to restore the state of one or more locally modified
files to an unmodified state, you can do that with the -C
flag to the update command:
cvs update -C <file>
This will create a .#<file> file which contains the changes
you previously made where <file> will now be a clean copy
pulled from the repository. This file can be used to either undo
this operation (i.e. restore your locally modified changes) or
it can just be deleted.
Creating Tags
Tags work similarly in CVS as they do in git: they mark a specific point in time in the revision history of a repository.The CVS book has some great ASCII art illustrating how CVS tags work in its chapter on tags.
Tags can be made like so:
cvs tag RELEASE_1-0
Note that tag names can only consist of alphanumeric characters plus the dash ‘-’ and underscore ‘_’.
Tags can then be used to check out the source code of a module as it was when that tag was made:
cvs checkout -r RELEASE_1-0
Tags can also be used in place of dates or revision numbers with
commands such as diff and update.
The ~/.cvsrc File
Use this file to specify preferred command line options by default, so you don’t have to type them each time.
I prefer:
cvs -q
diff -uNp
update -P
checkout -P
CVS also has many shorthands for its commands. For example, up
instead of update, co instead of checkout, or ci instead
of commit.