Dark Mode

Concurrent Versions System - CVS

The original project branching Version Control System

Note: The CVS documentation refers to repositories as “modules”. So I use those terms more or less interchangeably here, preferring the term “module” when it’s conceptually specific to CVS and “repository” if it’s conceptually similar to Git.

There is 1 step to take before initializing a repository. You need to setup a central root directory for cvs repositories.

CVS Root Directory

cvs will need the cvs root diectory passed in. Either with the -d option or with the $CVSROOT environment variable.

mkdir -p ~/demo/cvs
cvs -d ~/demo/cvs init
export CVSROOT=~/demo/cvs

Initializing a Repository

I know you wanted this to be step #1.

mkdir -p ~/demo/cvs/demo_project
cd ~/demo
cvs checkout demo_project
cd demo_project
echo "Hello Tutorial Reader!" > demo.txt
cvs add demo.txt
cvs commit
# Opens your $EDITOR to write a commit message
# You can use `-m "Commit Message Here"` like in git

The commit is automatically pushed to the cvs-root repository on the commit sub-command. You can also run cvs status like in git.

Once inside a checked out repository, you don’t need to specify the cvs root directory with -d or $CVSROOT. That path value is stored in the ./CVS/Root file of the checked out repository.

The cvs-root repository then stores the files in what are called “RCS files”. They have a weird ,v extension. Revision Control System (RCS) is the file-based VCS program that CVS is built on top of. RCS manages each file. CVS manages them all-together. These files are read-only. Makes sense to prevent users from directly editing them.

Checking Repo Status & Making a Change

Inside the repo, running cvs <sub-command> with no args generally uses the current working directory. You can generally specify files. git usually makes you do one or the other.

You also don’t stage changes before commiting. The cvs add sub-command just starts CVS to track the file.

cd ~/tmp/demo_project
echo "New line to file" >> demo.txt
cvs diff
cvs status
cvs commit -m "New demo line"

Removing a file & The “Attic”

When you remove a file from the CVS repo, the RCS file gets moved into a sub-directory named “Attic”.

cd ~/tmp/demo_project
# Need to actually delete the file first
rm demo.txt
cvs remove demo.txt
cvs commit -m "Removed demo.txt"
ls ~/tmp/cvs/demo_project/Attic

The Attic also appears to be where RCS-files of committed-files, not on the trunk-branch are kept.

Pulling Changes From CVSROOT

cvs status isn’t necessary, but there is no fetch sub-command. status works as fetch.

cvs status
cvs update

Undo Local Changes

It helps

cvs update -C demo.txt

This made the file read-only. Fix with chmod +w demo.txt or chmod -R +w ./* to get the whole repository.

Cloning a Repository

Probably the only thing we’d use cvs for.

mkdir -p ~/tmp/altdir
cd ~/tmp/altdir
cvs -d ~/demo/cvs checkout demo_project
cd demo_project
cvs status

Cloning Over SSH

cvs -d user@hostname:/full/path/to/cvsroot checkout demo_project

Full path required. ~ does not work for $HOME

The original way to set up CVS on a remote machine was configuring inetd to call the cvs server command or cvs pserver for password authentication. cvs server is the command that CVS runs to send the client data from the central repo.

I have noticed that the default /etc/ssh/sshd_config file has a commented out section for the anoncvs user.

# /etc/ssh/sshd_config

# Example of overriding settings on a per-user basis
Match User anoncvs
	X11Forwarding no
	AllowTcpForwarding no
	PermitTTY no
	ForceCommand cvs server

Branches In CVS

Branches in CVS are created using the tagging system with -b as a “branch tag”.

Tags in CVS

# in a checked out cvs repository
cvs tag demo-tag demo.txt

# See tags using verbose status
cvs status -v

# Delete a tag
cvs tag -d demo-tag

Tags are meant to apply to “revisions”. That is where the branching comes in.

Branch Tags

# Create branch
cvs tag -b demo-branch

# Switch to branch
# `git checkout <branch-name>` analogue
cvs update -r demo-branch
# "r" for revision
# Like git, you need to switch to branch after creating it to edit it

# See Current Branch
cvs status
# Current Branch will be in `Sticky Tag:`

# See All Branches/Tags
cvs status -v
# In `Existing Tags:` section
# - branch tags are indicated on the right

# Merge branch
## use `cvs update -A` to switch to main trunk version
cvs update -j demo-branch
# Merges `demo-branch` with curret checked-out version
# Still need to commit the merge
cvs commit -m "Merge Commit Message"

# Delete branch tag
cvs tag -Bd demo-branch
# `-B` enables `-d` for deleting a branch tag
# The documentation warns against deleting branch tags

Branches seem to function as an index to a revision/version number in CVS.

Revision Numbering

CVS has an internal version numbering system. Each file has it’s own revision numbering cadence. When creating a branch, it adds a .# to the end of the current revision number. This .# is specifically an even number for branches.

You can use this revision number with the -r option on most sub-commands, like update or checkout. -r will invoke either a revision number, tag, or branch tag as specified by the user. [rev] is what the documentation refers to such argument.

Tag names and branch names seem indexed to revision numbers globally for each of the file’s different revision numbers. Managing repository versions with tag and branch names rather than revision numbers is suggested by the documentation.

Intentionally Incrementing the Revision Number on the Main Trunk

cvs update -A will unsticky the tags and bring you to the trunk version. With this, You can merge your changes cvs update -j <merge-src-branch-name>, and run cvs commit -r <#.#> to bring all the files up to revision number you specify on the main trunk, as long as #.# is higher than any existing version.

Config Files

.cvsignore

You can add a .cvsignore file to your repository with similar syntax as .gitignore.

~/.cvsrc

You can add a ~/.cvsrc that can automatically pass options globally and for sub-commands.

# Global Options
cvs -d user@hostname:/var/cvs

# Sub-Command Options
status -v

Manually Re-Linking Remote Repository

The CVSROOT path is stored in the ./CVS/Root. I’ve been able to re-write that to a different cvs repository successfully.

Revision Control System - RCS

The file-VCS that CVS is built on top of.

A part of rcs, there is the ci and co commands, short for “check-in” & “check-out”. There is the -r option as mentioned earlier to specify a revision for ci & co. There is also -l & -u which “lock” or “unlock” a checked-out file to a user. If a checked-out file is locked to a user, that user can add to the revision trunk.

mkdir -p ~/tmp/rcs
cd ~/tmp/rcs
mkdir RCS
echo "RCS was made in 1982!" > demo.txt
ci demo.txt
ls
# The file is gone!!!
ls RCS
# There's that weird `,v` extension
co demo.txt
# Lock the file to us for editing
rcs -l demo.txt
# Suprise! It's read-only!
chmod +w demo.txt
echo "New Test Line" >> demo.txt
# See changes
rcsdiff demo.txt
ci -m"Commit Message" demo.txt

Working with the -l & -u flags

Using co -l adds write permissions to the checked-out file!

co -l demo.txt
echo "New Test Line 2" >> demo.txt
# -u will keep the file checked out, but in an unlocked state.
# -l Would keep the file checked out, and locked to us to continue editing.
ci -u -m"Commit Message" demo.txt

Branches

Branches are created using ci -r#.#.# <file-name> adding the .# to the head #.# revision number to create your branch. This will create revision #.#.#.1.

co -l demo.txt
echo "New Line Test 3" >> demo.txt
ci -r1.3.1 demo.txt
# creates branch 1.3.1, with reveion 1.3.1.1

To merge branches, we’ll use the rcsmerge command. I might be using this command the wrong way. But this makes the most sense to me. There are a lot if options with rcsmerge that might simplify this flow.

# checkout head version
co -l demo.txt

# Specify both revisions in this order
rcsmerge -r1.3 -1.3.1 demo.txt

# Commit Merge
ci -m"Merge Commit" demo.txt

There are -N & -n for symbolic naming of the revisions, But the names do not increment up with revision numbers. So you can do named branches, but you have to specify -N<branch-name> for each ci commit.

Importing CVS to Git

It has to be done.

Ok, we have 3 option.

cvs-fast-export utilizes git’s fast import sub-command demonstrated with git fast-export below.

# In a git repo
git fast-export --all > ../dump.dat

# Make a new repo
cd ..
git init new_repo
cd new_repo

# Import the fast-export dump
git fast-import <../dump.dat

cvs-fast-import will create it’s own dump, compatable with git fast-import, that we can save in a file and import.

cvs-fast-export

cvs-fast-export is probably the most up-to-date and supported option. I was able to run this on Alpine Linux. It reads from a list of files, which the documentation suggests using the find command for.

# Export CVS to dump
find ~/tmp/cvs/demo_project | cvs-fast-export > dump.dat

# Initialize Git repo
git init demo_project
cd demo_project

# Import CVS dump
git fast-import <../dump.dat
git checkout master

# In fewer steps, we can pipe the `cvs-fast-export` into `git fast-import` without needing to save a dump file
git init demo_project
cd demo_project
find ~/tmp/cvs/demo_project | cvs-fast-export | git fast-import
git checkout master

Supplemental Tools

cvs-fast-export also ships with 2 other interesting commands. cvsconvert and cvssync, the latter of which appears to work with rsync. Both of these are written in Python 3. The man cvs-fast-export appears to be written in C.

cvssync

cvssync integrates with rsync. It is designed to localize a remote CVS repository for cvs-fast-export to be able to convert.

mkdir -p ~/tmp/cvs
cd ~/tmp/cvs
cvssync user@hostname:/home/user/tmp/cvs CVSROOT
cvssync user@hostname:/home/user/tmp/cvs demo_project

# Checkout in CVS to test
cd ~/tmp
cvs -d ~/tmp/cvs checkout demo_project
cd demo_project
cvs status

This tool would be good for both CVS repo migrations to a different machine and localizing CVS repos for conversion into Git.

cvsconvert

I couldn’t get this to work. It appears to be a Python wrapper of cvs-fast-export that passes each RCS file path into cvs-fast-export so you don’t have to do the find ~/tmp/cvs/demo_project part and it might also do the git repo initializing and fast-import part.

The documentation wasn’t clear to me how to use it. Below is a copy of my console when I tried to do it.

zsh % cvsconvert -v ~/tmp/cvs/demo_project
/usr/bin/cvsconvert:28: DeprecationWarning: 'pipes' is deprecated and slated for removal in Python 3.13
  from pipes import quote
cvsconvert: repo  does not exist.

git cvsimport

Git ships with a cvs import script, however, it’s not very well maintained. man git-cvsimport. It uses cvsps which you need to verify you have installed (which cvsps). git cvsimport uses a depreciated version of cvsps. Also, it tries to connect with rsh for remote servers, so localizing the cvs root would be a good start.

We have to initialize a git repo, then we have to seed it with a back-dated commit. cvsimport is designed to import changes in order if commit date-timestamp, so we’ll use the --date argument with a date before the first commit in the CVS repository. Since we’re commiting an initial file, might as well make it a .gitignore file since we know that won’t be migrated over from CVS.

git init demo_project
cd demo_project
git branch -m main
touch .gitignore
echo ".*" >> .gitignore
echo "!.gitignore" >> .gitignore
git add .gitignore

# Initial git commit with back-date argument
git commit -m "Initial Git Commit" -m "Added .gitignore" --date="2000-01-01 00:00:00"

# Now for the CVS import
## -o branch-name is the git-repo's branch-name
git cvsimport -d ~/tmp/cvs -o main demo_project

# Verify commit log
git log main

This imports the CVS trunk revisions onto the specified git branch.

cvs2git

Like cvs-fast-export, this command exports CVS repo data into a file that git fast-import can read from. The git documentation suggests cvs2git which is part of the cvs2svn package. cvs2svn is written in Python 2.7. That’s much more current than CVS. I hope it’s compatable.

The man-page entries for the cvs2svn package are not added to the mandoc database on OpenBSD. They are printable to stout with the --man flag. But what you will see is the unrendered “roff” markup language. Also called “groff” for the GNU groff program that compiles/renders this markup language. Sometimes called “troff” for the older Unix program that GNU groff succeeded. GNU groff will render this markup language into printable postscript or to PDF. mandoc is designed to render is markup to the stdout for the man command to use.

You can either write the man-pages to the man directories, or just render the with mandoc & less.

# Render in one-shot to console
cvs2git --man | mandoc | less

# Write man-pages to system man directory
doas cvs2git --man > /usr/local/man/man1/cvs2git.1
doas cvs2svn --man > /usr/local/man/man1/cvs2svn.1
doas cvs2bzr --man > /usr/local/man/man1/cvs2bzr.1
# Add man-page entries to mandoc.db
doas makewhatis /usr/local/man
# Open man-page
man cvs2git

You can think of roff like html and mandoc like a web browser that renders the syntax. The man command will then target the mandoc engine at the correct man-page entry listed in the /usr/local/man/mandoc.db and then throw the rendered stdout to the env text $PAGER, generally less.

Actually Using cvs2git

I got this to work on OpenBSD. It has a Python 2 package. Alpine Linux does not. cvs2git will export a dump file. Then you initialize an empty git repository, then run git fast-import on the dump file.

cvs2git --dumpfile=dump.dat ~/tmp/cvs/demo_project
git init demo_project
cd demo_project
git fast-import <../dump.dat
git branch -a

This approach will only work on systems that can run Python 2.

Helpful Resouces

gnu.org - CVS

Open Source Development With CVS by Karl Fogel

ZSH - vcs_info config

GitLab - cvs-fast-export

Good cli-resouces

# Lists sub-commands
cvs --help-commands
cvs --help <sub-command>
info cvs

# I was actually disappointed with the man-pages.
# It does go into the options.
man cvs

Other Weird Stuff You Can Do

cvs checkout CVSROOT

You can checkout, edit, and commit, the system files in the CVSROOT module.

cvs admin

The admin sub-command gives you administrative functions over the repository.

git cvsserver

Git appearently ships with a daemon that will allow users to read/write to a git repository using the cvs client.

RCS commands on files in CVSROOT

If your cvs-root repo is on your local machine, you can run RCS command on the *,v rcs-files in the cvs-root repo by specifying the full path. CVS, then, will be able to read from those changes in the *,v RCS files.

rcs-fast-import

The developer of cvs-fast-export has a rcs-fast-import utility that will split a git fast-export stream into RCS-files for each trached files

GitLab - rcs-fast-import

He also made reposurgeon.

Cannot commit as root

For some reason, CVS will not allow the root user to make commits.

At my wit’s end and I should probably move on

Shit I couldn’t figure out.

Setting a default branch

The documentation, again, suggests not using this feature. You can set a default branch with the admin sub command. You actually don’t use a space between -b and the revision name or number.

# Set default version to branch
cvs admin -b<branch-name>

# Set default version to revision number
cvs admin -b2.1

# Unset a default branch
cvs admin -b

I haven’t been able to make it function like a git main branch. My workflow in CVS will either be to leave the trunk alone as an unlabeled HEAD branch, or have a “main” branch as one of the branches from the trunk.

$CVSUMASK and CVS Adding Files as Read-Only

One stupid thing that seems acknowledged in the documentation is that when a file is created from a cvs update or cvs checkout command, it’s created with read-only permissions. There is a $CVSUMASK environment variable, but it’s acknowledged to be buggy in the client/server interaction. There is a setting for locking certain modules in the cvs-root repository. I imagine the read-only setting is to support that feature.

Just get used to running chmod -R +w . frequently.

Z-Shell’s vcs_info Config

: ( CVS’s %b prints the repo name

I modified my zsh prompt to distinguish VCS programs. CVS’s settings were quite minimal and buggy. %b prints the repo name, not the branch name. There are some VCS’s that use version numbering as well. Z-Shell’s vcs_info will use %i for revision numbering, but I couldn’t get it to work for CVS.

Below is the vcs_info section of my ~/.zshrc file.

autoload -Uz vcs_info
zstyle ':vcs_info:*' check-for-changes true
zstyle ':vcs_info:*' unstagedstr $'\033[91m*%f'
zstyle ':vcs_info:*' stagedstr $'\033[92m*%f'
zstyle ':vcs_info:*' formats $'\033[95m%s%f-\033[4m%b\033[0m%u%c'
zstyle ':vcs_info:git:*' formats $'\033[92m%s%f-\033[4m%b\033[0m%u%c'
zstyle ':vcs_info:cvs:*' formats $'\033[94m%s%f-\033[4m%b\033[0m'

# Then concatenate `$vsc_info_msg_0_` in your `$PS1` or `$PROMPT` zsh shell environment cariable.

I think having different VCS’s indicated on the prompt will be helpful in an environment where you are working with different VCS’s.

Branches in RCS on OpenBSD

ci: rcs_head_set failed

I’m having trouble. On OpenBSD. This does work on Alpine Linux. Apline Linux’s apk package manager ships GNU RCS. OpenBSD uses it’s own implementation called OpenRCS.

You’re supposed to ci -r#.#.# demo.txt adding the .# to the head #.# revision number to create your branch. But I’m getting ci: rcs_head_set failed error.

zsh % ci -r1.4.1 rand5.hex
RCS/rand5.hex,v  <--  rand5.hex
new revision: 1.4.1; previous revision: 1.4
enter log message, terminated with a single '.' or end of file:
>> help.
>> .
ci: rcs_head_set failed

It then creates and leaves two /tmp/diff#.XXXXXXXXXX files with the contents of the file revisions.

I’ve tried ci -r1.4.1.1 rand5.hex, but that commits onto the 1.4 trunk and doesn’t create a branch.

CVS Cloning over IPV6

For the CVSROOT variable, it appears to be parsing the : colon for a tcp-port. So it does not support IPV6 addresses. This appears to be the case for hostnames that are set to an IPV6 address as well.