Posted on

Before Git conquered the world in the mid-2000s, most of the free software universe ran on a system called CVS — the Concurrent Versions System. If you have ever wondered why version control vocabulary looks the way it does ("checkout", "commit", "tag"), or why older developers flinch at the word "branching", the answer very often starts with CVS.

What CVS is

CVS began in 1990 as a set of shell scripts that Dick Grune layered on top of RCS, the Revision Control System, and was rewritten in C shortly afterwards by Brian Berliner and others. The idea was simple and, for its time, radical: let many people work on the same files simultaneously, from different machines, while a single server keeps the canonical history.

The architecture is strictly client–server. One central repository holds the project; developers connect over the network (the classic pserver protocol on port 2401, later more often tunneled over ssh), run cvs checkout to obtain a working copy, cvs update to pull other people's changes in, and cvs commit to push their own out. If two people edited the same lines, the second committer is asked to merge the conflict by hand — the file gets conflict markers, you edit it, you commit again.

Under the hood, each file is stored as an RCS archive — the famous ,v files: a forward chain of deltas with version numbers like 1.1, 1.2, 1.3. There is no object in CVS that represents the state of the whole tree. A project is just a directory of per-file histories.

The mental model

That is the crucial difference from Git, and it explains almost every quirk of CVS:

  • Files are versioned individually. Each file carries its own independent chain of versions.
  • A tag is a spread-out label. "Release 1.0" means: foo.c at 1.7, bar.c at 1.12, and so on — one label smeared across many files.
  • A branch is a magic number. Each branched file forks its numbering into 1.7.1, 1.7.2 … Files never touched on the branch simply have no branch versions at all.
  • Commits are not atomic. A commit walks the modified files one by one. If the connection drops halfway, half the files are committed and half are not, and everyone who updates in between inherits the broken intermediate state.

Git inverts the model. A commit is a snapshot of the entire tree, addressed by a checksum of its own contents. It is atomic by construction. A branch is a 41-byte pointer to a commit; making one costs nothing. History lives in your working copy, so log, diff and blame are local and instant, where CVS had to do a network round-trip for everything.

Side by side

CVSGit
Storage modelPer-file delta chains (RCS ,v)Content-addressed snapshots
CommitPer-file, non-atomicWhole tree, atomic
BranchForked version numbers per fileA pointer to a commit
TagMovable label across filesImmutable object
RenamesNot tracked (delete + add)Tracked
History accessNetwork round-tripsLocal and instant
IntegrityYou trust the serverEvery object is checksummed
TopologyStrictly centralizedDistributed; hubs optional

What survived

CVS itself is mostly a museum piece now, though more than a few legacy repositories still hum along in forgotten corporate corners. Its ideas did not die: Subversion (2000) was explicitly "CVS done right" — atomic commits, real renames — while keeping the centralized model. And Git, for all its novelty, kept the dialect: we still say "checkout" and "commit", and the GitHub-style workflow — clone a central hub, push back to it — is CVS's client–server shape with a fully replicated server on every laptop.

The next time a merge conflict slows you down, spare a thought for the 1990s: no atomic commits, no cheap branches, and every cvs log a journey across the network.