CVS is an annotating time machine and data replication server. There, that clears everything up, doesn't it?
If you've used other revision control systems, you may be familiar with the lock-modify-unlock development model, wherein a developer first reserves ("locks") the file he plans to edit, makes his changes, and then releases the lock, allowing other developers access to it.
CVS is way, way mellower. It is byte-order-independent, gender-neutral, and uses the copy-modify-merge model, which works pretty much the way you think it does:
At this point you may be wondering, what happens when two developers edit the same part of the same page, and then both commit? This is called a conflict, and CVS of course notices it the moment the second developer tries to commit changes. Instead of allowing the second commit, CVS places "conflict markers" (which are very obvious) in the region in question, in the second developer's copy. The conflict markers delimit the conflicting region and show both sets of changes. It's up to the second committer to sort it all out and commit a new revision with the conflict resolved. Perhaps the two developers will need to talk to each other to settle the issue. All CVS can do is notice that there is a conflict; it must be human beings who actually resolve it.
What about the "master copy" mentioned above? In offical CVS terminology, it is known as the project's repository. It's just a file tree kept on a central server (in our case, cvs.onshore.com is the server, and /usr/local/cvs/ is the repository). Unless you want to be a CVS guru (you don't), you can ignore the details of the repository's history-saving techniques --- suffice it to say that it always has the most up-to-date revision of a file or set of files, but is also capable of retrieving past revisions.
A quick review of terms:
This confirms your password with the server and saves it in your home directory, in a file called .cvspass. After this, CVS will always try to get your password from .cvspass, so you only need to log in once from a given local host to a given CVS server. The .cvspass file is human-readable; take a look at it some time (you can paste lines from one .cvspass file into another if it's ever necessary).prompt$ cvs -d :pserver:username@cvs.onshore.com:/usr/local/cvs login CVS password: [type your CVS password here] prompt$
Next, try checking out a project (the first command is a single line without the backslash, of course; it's just wrapped in this document because it's long):
Let's decode that command line:prompt$ cvs -d :pserver:username@cvs.onshore.com:/usr/local/cvs \ co documentation cvs server: Updating documentation U documentation/cvs-intro.html U documentation/README.cvs [... etc, etc ...] prompt$
-d :pserver:username@cvs.onshore.com:/usr/local/cvs
The -d flag is used to explicitly tell CVS where the
repository for this project is. The repository string itself goes
like this
":accessmethod:username@machine:path-to-repository-root".
(There are several access methods. The password server, whose method name is pserver, authenticates directly with the CVS server, using a special per-user CVS password if it can, or defaulting to that user's account password on the server otherwise. Another method is ext, which tells the CVS client to connect using an external remote execution program, like rsh. You could use secure shell by setting the environment variable CVS_RSH to "ssh", though.)
co documentation
Check out the project documentation. CVS allows you to specify the
action in shorthand; co is a synonym for checkout,
and one could just as well type checkout documentation.
Anyway, at this point you have a directory named documentation. Go into that directory:CVSROOT=:pserver:lefty@cvs.onshore.com:/usr/local/cvs export CVSROOT
The project has two files, README.cvs and cvs-intro.html. Note also the CVS administrative subdirectory, CVS. It records revision numbers and repository information (take a look at the files in there sometime, they're human-readable). You should never need to do anything in the CVS subdir; only CVS uses it.prompt$ cd documentation prompt$ ls CVS/ README.cvs cvs-intro.html prompt$
The file README.cvs above exists solely for playing around with CVS, so you can edit it freely. Make some changes to it, and then do an update:
The "M" next to the filename reminds you that you have "modified" that file, and that the repository does not yet know about the change.prompt$ emacs README.cvs [... edit README.cvs to your heart's content ...] prompt$ prompt$ cvs update cvs server: Updating . M README.cvs prompt$
Notice how you don't need to specify the repository with -d anymore; the working copy knows where it came from (that information is saved in the CVS subdir), so there would be no point making you type out that long repository string again. Note also that we didn't have to tell CVS what to update -- it just knows to work on the current directory (and subdirectories, if any), examining all files. This is a general rule: in the absence of explicit filenames, "." is always an implied argument to CVS, and CVS will recurse into subdirectories automatically. CVS detects which files have been changed locally, which have changed in the repository, and does the appropriate thing in all cases.
Now, send your changes to the repository:
The -m flag, followed by a text string, gives CVS the log message for this commit. CVS requires log messages, even if they are empty strings. If you omit the -m flag, CVS will automatically start up an editor (such as vi or Emacs), with a text buffer where you're supposed to type in the log message. Only after you save the message and exit the editor does the commit continue.prompt$ cvs ci -m "edited text because i felt like it" cvs commit: Examining . Checking in README.cvs; /usr/local/cvs/internal/documentation/README.cvs,v <-- README.cvs new revision: 1.2; previous revision: 1.1 done prompt$
Because README.cvs was the only changed file, CVS committed it and nothing else. If there had been other modified files, but you'd wanted to check in only README.cvs, you could have specified that by putting the filename after the log message. In effect, you can always replace the implied "." with a list of specific files to act on.
Now that you've committed this change, others with working copies of their own can run cvs update, and your change will be merged into their copy:
The "P" means "patched"; that is, the server sent down just the differences for the changed region, and the client "patched" them into the file. It's done this way to avoid wiping out any changes the second developer might have made in other regions of the file.their_prompt$ cvs update cvs server: Updating . P README.cvs their_prompt$
Many CVS commands, particularly update, list filenames prefixed by a code letter as part of their output. Here is what the code letters mean:
M -- modified; you have made local changes, but the repository doesn't know about them.
P -- patched; repository changes have been patched into an unchanged local copy of the file
C -- conflict; conflicts detected between repository version and your local changes
? -- unknown; CVS knows nothing about this file and will not do anything with it. If you want it to be part of the project, you'll need to cvs add it (see below).
Sometimes you'll also see a more detailed message about a file, if the repository and the working copy have mutually unknown changes:
The "M" tells you that the file is still modified, but you should also check for conflict markers in the file. It would be nice if CVS always output a "C" when there are conflicts, but it doesn't. It does always insert conflict markers into the file if there's a conflict, however.prompt$ cvs update cvs server: Updating . RCS file: /usr/local/cvs/internal/documentation/README.cvs,v retrieving revision 1.9 retrieving revision 1.10 Merging differences between 1.9 and 1.10 into README.cvs M README.cvs prompt$
CVS and Binary Filesprompt$ cvs add foo cvs server: scheduling file `foo' for addition cvs server: use 'cvs commit' to add this file permanently prompt$ cvs ci -m "new file foo, contains useless information" foo RCS file: /usr/local/cvs/internal/documentation/foo,v Checking in foo; done /usr/local/cvs/internal/documentation/foo,v <-- foo initial revision: 1.1 done prompt$
Adding a binary file is done the same way, but it's very important to use the -kb flag to tell CVS it is a binary (it's a long story):
CVS may corrupt binary files unless it's told they're binary. Work is under way to correct this problem. In the meantime, always tell it that a file is binary.prompt$ cvs add -kb bar.gif cvs server: scheduling file `bar.gif' for addition cvs server: use 'cvs commit' to add this file permanently prompt$ cvs ci -m "added photo of lenscap" bar.gif RCS file: /usr/local/cvs/internal/documentation/bar.gif,v done Checking in bar.gif; /usr/local/cvs/internal/documentation/bar.gif,v <-- bar.gif initial revision: 1.1 done prompt$
CVS is broken in this respect; you can add new directories, but CVS may give confusing, even inaccurate output, while you're doing so. Basically, create the directory, then run cvs add on it just like a regular file, then add files inside it the usual way. If CVS seems to complain, just ignore it.
Removing a file is done in three steps -- remove the file, then tell CVS it's gone, then commit the change:
How do I Remove a Directory?prompt$ rm foo prompt$ cvs rm foo cvs server: scheduling `foo' for removal cvs server: use 'cvs commit' to remove this file permanently prompt$ cvs ci -m "realized foo was useless, removed" foo Removing foo; /usr/local/cvs/internal/documentation/foo,v <-- foo new revision: delete; previous revision: 1.1 done prompt$
Run the remove procedure (see above) on the files inside it, then run it on the directory itself. Again, if CVS complains, you can probably ignore it.
Glad you asked. Move the file to the new name, then run the three steps to remove the old name, then run the two steps to add the new name, then get a drink -- you deserve it.
Importing
Arrange your project's whole directory tree nice and clean like (you want to get this right the first time, as changing things around later would be painful). Any files that are not permanent parts of the project should be removed. If there are binary files, such as graphical images, take them out for now -- you should add them in later using the -kb flag.
Then you go to the top of your project's directory tree, and import the whole thing in one command. Here's how it was done for this project (Note: don't actually run this -- this project has already been imported once, if you try to re-import it, things will get messed up!):
Note that what we imported was not a working copy; because it had no CVS subdirectory, CVS didn't even know it exists. After we imported, it was still not a working copy -- it had not been produced by a CVS checkout, so it had no CVS subdirectory. After we moved the original to a backup location, and then checked out the new project from CVS, we finally had a CVS working copy.prompt$ cd documentation prompt$ ls README.cvs cvs-intro.html prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs \ import \ -m "initial import of Project for World Domination" \ internal/documentation onshore start N internal/documentation/README.cvs N internal/documentation/cvs-intro.html No conflicts created by this import prompt$ cd .. prompt$ mv documentation documentation.bak prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs co documentation [... blah blah blah, the usual checkout messages ... ] prompt$ cd documentation prompt$ ls CVS/ README.cvs cvs-intro.html prompt$
The import subcommand itself takes a log message (-m "whatever"), and then three arguments:
VENDOR_TAG: a name saying who started this project. CVS doesn't really use this for anything, but you have to include it nonetheless. I usually use my login name or company name or something like that.
START_TAG: a symbolic name for this first revision, start is
usually a good choice. Again, CVS won't use this for much, it's
basically bureaucracy.
Defining a Module for the Project
This requires a little knowledge about how the repository is set up, which I will deliver orally (vaccine delivery is under development). You may find it useful to look at the Repository Structure section of this document before reading about defining modules.
Defining a module name for your project is not, strictly speaking, necessary; you can always retrieve it from the repository by the path you imported it under. For example:
The problem with this is that it gives you the directory you want nested inside another directory that you didn't want. Ideally, you'd be able to check out the documentation project as a unit by itself, independent of any sibling projects in the internal section of the repository. To set this up, you would need to edit the modules file in the CVSROOT administrative subdirectory of the repository. But you wouldn't edit that file directly in the repository; instead, you'd check out a working copy of CVSROOT, edit the modules file in that working copy, and check it back in. You don't need to do this for the documentation module, of course, because it's already been done. Here's how it was done:prompt$ cvs -d :pserver:whoever@cvs.onshore.com:/usr/local/cvs \ co internal/documentation cvs server: Updating internal/documentation U internal/documentation/README.cvs U internal/documentation/cvs-intro.html prompt$ cd internal prompt$ ls CVS/ documentation/ prompt$ cd documentation prompt$ ls CVS/ README.cvs cvs-intro.html prompt$
There! Now, you can checkout the module documentation without reference to its actual path in the repository:prompt$ cvs -d :pserver:whoever@cvs.onshore.com:/usr/local/cvs co CVSROOT cvs server: Updating CVSROOT U CVSROOT/checkoutlist U CVSROOT/commitinfo U CVSROOT/cvsignore U CVSROOT/cvswrappers U CVSROOT/editinfo U CVSROOT/log.pl U CVSROOT/loginfo U CVSROOT/modules U CVSROOT/notify U CVSROOT/passwd U CVSROOT/rcsinfo U CVSROOT/taginfo cvs server: Updating CVSROOT/Emptydir prompt$ cd CVSROOT prompt$ emacs modules [... edit it to have a line like this: documentation internal/documentation That maps the module name `documentation' onto the repository path `internal/documentation' ...] prompt$ prompt$ cvs ci -m "new module: documentation" cvs commit: Examining . cvs commit: Examining Emptydir Checking in modules; /usr/local/cvs/CVSROOT/modules,v <-- modules new revision: 1.68; previous revision: 1.67 done cvs server: Rebuilding administrative file database prompt$
prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs co documentation cvs server: Updating documentation U documentation/README.cvs U documentation/cvs-intro.html prompt$ cd documentation prompt$ ls CVS/ README.cvs cvs-intro.html prompt$
The first developer commits her changes:
The second developer has changed line 7 as well; if he blindly tries to commit, CVS will refuse, on the basis that the file is no longer up-to-date (that is, there has been a new revision created in the repository since the developer last updated or checked out):herprompt$ cvs ci -m "first developer changes line 7" README.cvs Checking in README.cvs; /usr/local/cvs/internal/documentation/README.cvs,v <-- README.cvs new revision: 1.6; previous revision: 1.5 done herprompt$
Seeing that the up-to-date check has failed, the second developer updates. CVS then places the conflict markers in the file:hisprompt$ cvs ci -m "I also changed line 7" README.cvs cvs server: Up-to-date check failed for `README.cvs' cvs [server aborted]: correct above errors first! hisprompt$
The two versions of the changed region are clearly marked, as you can see. He edits the file (perhaps after conferring with the first developer), removing the conflict markers, then commits the new revision:hisprompt$ cvs update cvs server: Updating . RCS file: /usr/local/cvs/internal/documentation/README.cvs,v retrieving revision 1.5 retrieving revision 1.6 Merging differences between 1.5 and 1.6 into README.cvs M README.cvs hisprompt$ cat README.cvs [... head of file omitted ...] <<<<<<< README.cvs sndbox whoo hoo, playground, edit freely in here, la-dee-dah-dah... ======= s&ndbox whoo hoo, playground, edit freely in here, la-dee-dah-dah... >>>>>>> 1.6 mmm, cvs is such fun! cvs is my hero! hisprompt$
Voila! Conflict resolved, world peace soon to follow.hisprompt$ cvs ci -m "committing with conflicts resolved" README.cvs Checking in README.cvs; /usr/local/cvs/internal/documentation/README.cvs,v <-- README.cvs new revision: 1.7; previous revision: 1.6 done hisprompt$
You can can give a tag any name you want (although, inconveniently, it can't contain a period). Most projects have a convention for tagnames, for example "release-majornum-minornum". Also, it is normal to tag an entire project tree at once, although you could tag a subset of it by naming individual files on the command line.prompt$ cvs tag release-0-1 cvs server: Tagging . T README.cvs T cvs-intro.html prompt$
Retrieving a tagged version is equally easy: use the -r ("revision") option to checkout or update:
Rocket science, huh?prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs \ co -r release-0-1 documentation cvs server: Updating documentation U documentation/README.cvs U documentation/cvs-intro.html prompt$
You can use the log subcommand to view the log messages for a particular file, or for multiple files at once. The output of log tends to be verbose, here's an example in full (don't say I didn't warn you):
As you can see from the output, log is useful not only for reading the log messages, but also for finding out what tags have been set (see the top of the output), and what the file's revision numbers are.prompt$ cvs log README.cvs RCS file: /usr/local/cvs/internal/documentation/README.cvs,v Working file: README.cvs head: 1.8 branch: locks: strict access list: symbolic names: release-0-1: 1.8 keyword substitution: kv total revisions: 8; selected revisions: 8 description: ---------------------------- revision 1.8 date: 1998/05/08 16:17:47; author: kfogel; state: Exp; lines: +1 -5 . ---------------------------- revision 1.7 date: 1998/05/08 14:36:37; author: kfogel; state: Exp; lines: +4 -0 trying to merge with conflicts still present ---------------------------- revision 1.6 date: 1998/05/08 14:31:56; author: kfogel; state: Exp; lines: +1 -1 trivial change to line 7 ---------------------------- revision 1.5 date: 1998/05/08 14:31:21; author: kfogel; state: Exp; lines: +1 -1 *** empty log message *** ---------------------------- revision 1.4 date: 1998/05/08 14:28:45; author: kfogel; state: Exp; lines: +1 -1 trivial change to line 7 ---------------------------- revision 1.3 date: 1998/05/07 17:29:54; author: kfogel; state: Exp; lines: +0 -1 edited text because i felt like it ---------------------------- revision 1.2 date: 1998/05/07 17:29:41; author: kfogel; state: Exp; lines: +1 -1 edited text because i felt like it ---------------------------- revision 1.1 date: 1998/05/07 17:08:30; author: kfogel; state: Exp; . ============================================================================= prompt$
Viewing differences between revisions
If you need more detail than the log messages give you, you can ask CVS to show you the differences between two revisions. The differences are displayed like the output of the Unix diff program. You can choose any two revisions you want, though people usually choose to look at adjacent ones:
Yes, you can use tag names in place of actual revisions numbers. Yes, you can pass the -c argument to diff to view the output in "context diff" format:prompt$ cvs diff -r 1.3 -r 1.4 README.cvs Index: README.cvs =================================================================== RCS file: /usr/local/cvs/internal/documentation/README.cvs,v retrieving revision 1.3 retrieving revision 1.4 diff -r1.3 -r1.4 7c7 < sandbox whoo hoo, playground, edit freely in here, la-dee-dah-dah... --- > s&ndbox whoo hoo, playground, edit freely in here, la-dee-dah-dah... prompt$
And yes, Virginia, there is a Santa Claus.prompt$ cvs diff -c -r 1.3 -r 1.4 README.cvs [... context diff output omitted to save space ...] prompt$
It is common, but not required, to export a known, tagged version -- otherwise, you're getting whatever happens to be in the repository at that moment.prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs \ export -r release-0-1 documentation cvs export: Updating documentation U documentation/README.cvs U documentation/cvs-intro.html prompt$ cd documentation prompt$ ls README.cvs cvs-intro.html (note the lack of a CVS/ subdir!) prompt$
Reverting a change that you've already committed is only slightly trickier: you need to retrieve the contents of the version previous to the one you committed, passing the -p option to update, which tells to send the file contents to standard output rather than to the file itself:
prompt$ cvs update -p -r 1.7 README.cvs [... see the contents of README.cvs, revision 1.7, fly by ...] prompt$ cvs update -p -r 1.7 README.cvs > README.cvs [... same thing, only redirected back into README.cvs itself. Ignore the message sent to standard error at the end; it's just confirming the filename and revision number. ...] prompt$ cvs ci -m "reverted to 1.7" README.cvs Checking in README.cvs; /usr/local/cvs/internal/documentation/README.cvs,v <-- README.cvs new revision: 1.9; previous revision: 1.8 done prompt$
The CVSROOT subdirectory above is special -- it is where CVS stores administrative files, such as modules and passwd.prompt$ ls -CF /usr/local/cvs AA_Pediatrics/ configure/ internal/ regal/ CVSROOT/ cso/ lind/ shrdlu/ aap/ design-horizons/ motorola/ timesheet/ activision/ esi/ mpct/ user-space/ ap/ fcnbd/ npc-contemporary/ vworld/ cl-http/ hochunk-localbin/ pr-system/ wwwdist/ cme/ include/ racingdigest/ prompt$ ls -CF /usr/local/cvs/CVSROOT Emptydir/ cvsignore,v log.pl,v* passwd* checkoutlist* cvswrappers* loginfo* passwd,v* checkoutlist,v* cvswrappers,v* loginfo,v* rcsinfo* commitinfo* editinfo* modules* rcsinfo,v* commitinfo,v* editinfo,v* modules,v* taginfo* commitlog history* notify* taginfo,v* cvsignore log.pl* notify,v* val-tags prompt$ ls -CF /usr/local/cvs/internal cpio-ssh/ documentation/ local-bin/ server/ prompt$ ls -CF /usr/local/cvs/internal/documentation README.cvs,v cvs-intro.html,v prompt$
Sometime when you have a half-hour to spare, go log into cvs.onshore.com, cd to /usr/local/cvs, and poke around in the subdirectories there (be careful not to touch anything of course). You'll see a lot of files ending in ,v -- these are known for historical reasons as "RCS" files, and they are where the repository stores the current revision and past history of each file in a project. They are human-readable, take a look at their contents if you're interested.
http://www.cyclic.com/cyclic-pages/howget.html contains pointers to CVS documentation, including a GNU-Info manual, an introductory paper by Jim Blandy, and the CVS FAQ, which is old but apparently still useful.
You should also look at a document called CVS at onShore, at http://cafe.onShore.com/internal/using_cvs.html, which describes local CVS conventions.
This document itself is available on cafe, too: http://cafe.onShore.com/internal/cvs-intro.html.
(Back to Karl Fogel's home page.)