CVS: Surviving Code Merges Before Git Changed Everything
CVS was the standard version control system for a decade. It was painful. Here is how we used it, what the workflows looked like, and what we learnt.
CVS: Surviving Code Merges Before Git Changed Everything
CVS — the Concurrent Versions System — was the dominant version control tool from the early 1990s until Subversion and later Git replaced it. At Motorola we used CVS throughout the late 90s for all Java development. It had serious limitations, but it was a genuine improvement over what came before, which in many teams was a shared directory and developer discipline.
What CVS Was
CVS stored the history of every file in a central repository. Developers checked out a working copy, made changes, and committed them back. Conflicts arose when two developers modified the same file concurrently.
The repository lived on a server:
# Set the repository location
export CVSROOT=:pserver:rishi@cvsserver:/var/cvs/motorola
# Authenticate
cvs login
# Check out the project
cvs checkout nms-core
This created a local working copy in nms-core/. Every file had a corresponding file in the repository at /var/cvs/motorola/nms-core/.
Basic Workflow
The daily workflow was: update, edit, commit.
# Pull latest changes from the repository
cvs update
# After editing files, see what changed
cvs diff -u NetworkDevice.java
# Commit changes with a message
cvs commit -m "Add retry logic to SnmpV2PollStrategy" NetworkDevice.java
# Add a new file
cvs add DeviceRegistry.java
cvs commit -m "Add DeviceRegistry" DeviceRegistry.java
# Check the log for a file
cvs log NetworkDevice.java
Branching and Tagging
CVS branching was the feature most developers feared. Unlike Git, CVS branches were not cheap copies of the repository — they were implemented with branch tags in the RCS files. Merging back to the trunk (mainline) required specifying the branch tag:
# Create a release tag (snapshot)
cvs tag RELEASE_1_0
# Create a branch for hotfixes
cvs tag -b RELEASE_1_0_FIXES
# Switch working copy to the branch
cvs update -r RELEASE_1_0_FIXES
# After fixing, merge branch changes back to trunk
cvs update -A # switch back to trunk
cvs update -j RELEASE_1_0 -j RELEASE_1_0_FIXES # merge branch delta
cvs commit -m "Merge RELEASE_1_0_FIXES to trunk"
This was genuinely difficult. The merge command required specifying two tags — the branch point and the current branch tip — and getting this wrong corrupted the merge. Many teams avoided branches entirely and only used trunk, accepting that releases meant code freezes.
Merge Conflicts
When two developers edited the same file, CVS tried to merge the changes automatically. When the same lines were touched, it produced a conflict:
<<<<<<< NetworkDevice.java
public void poll() throws SnmpException {
snmpClient.get(ip, MIB.SYS_UPTIME);
}
=======
public DeviceStatus poll() throws SnmpException, IOException {
return snmpClient.getStatus(ip);
}
>>>>>>> 1.14
You edited the file to resolve the conflict, removed the markers, and committed. On a large team with frequent parallel changes, conflict resolution consumed meaningful time.
What CVS Could Not Do
Rename files. CVS tracked files, not content. Renaming a file meant removing it and adding a new file — losing all its history. We kept poorly-named files for years to avoid this.
Atomic commits. A commit of multiple files in CVS was not atomic. If the server connection dropped mid-commit, the repository could end up with a partial commit. We discovered this twice in production when a developer's VPN dropped during a commit.
Offline work. Every CVS operation required a network connection to the server. No network meant no commits, no logs, no diffs against the repository. Laptops were unreliable and VPNs were slower than the CVS protocol needed.
Practices That Helped
Short-lived working changes. The longer a developer's changes sat uncommitted, the more painful the eventual merge. We enforced a "commit at least daily" norm.
Descriptive commit messages. CVS had no good UI for browsing history. Commit messages were the primary way to understand what changed and why. Short messages like "fix" made archaeology impossible.
Tag before you release. Always tag the exact codebase that was released. We had one incident where we could not reproduce a bug because we had not tagged the release and the trunk had moved on.
What Git Changed
Git solved every structural problem CVS had. Branching became instant. Commits were atomic and content-addressed. Renames were tracked. Working offline was fully supported. The distributed model meant every developer had a complete repository backup.
The mental model shift was real. CVS developers thought in terms of files and the central server. Git developers think in terms of commits and a graph. The graph model is more powerful and more correct, but it took most CVS developers time to adapt.