Retrieving by date is useful when the mere passage of time is your main concern. But more often what you really want to do is retrieve the project as it was at the time of a specific event – perhaps a public release, a known stable point in the software's development, or the addition or removal of some major feature.
Trying to remember the date when that event took place or deducing the date from log messages would be a tedious process. Presumably, the event, because it was important, was marked as such in the formal revision history. The method CVS offers for making such marks is known as tagging.
Tags differ from commits in that they don't record any particular textual change to files, but rather a change in the developers' attitude about the files. A tag gives a label to the collection of revisions represented by one developer's working copy (usually, that working copy is completely up to date so the tag name is attached to the "latest and greatest" revisions in the repository).
Setting a tag is as simple as this:
floss$ cvs -q tag Release-1999_05_01 T README.txt T hello.c T a-subdir/whatever.c T a-subdir/subsubdir/fish.c T b-subdir/random.c floss$
That command associates the symbolic name "Release-1999_05_01" with the snapshot represented by this working copy. Defined formally, snapshot means a set of files and associated revision numbers from the project. Those revision numbers do not have to be the same from file to file and, in fact, usually aren't. For example, assuming that tag was done on the same myproj directory that we've been using throughout this chapter and that the working copy was completely up to date, the symbolic name "Release-1999_05_01" will be attached to hello.c at revision 1.5, to fish.c at revision 1.2, to random.c at revision 1.2, and to everything else at revision 1.1.
It may help to visualize a tag as a path or string linking various revisions of files in the project. In Figure 2.1, an imaginary string passes through the tagged revision number of each file in a project.
File A File B File C File D File E ------ ------ ------ ------ ------ 1.1 1.1 1.1 1.1 1.1 ----1.2-. 1.2 1.2 1.2 1.2 1.3 | 1.3 1.3 1.3 1.3 \ 1.4 .-1.4-. 1.4 1.4 \ 1.5 / 1.5 \ 1.5 1.5 \ 1.6 / 1.6 | 1.6 1.6 \ 1.7 / | 1.7 1.7 \ 1.8 / | 1.8 .-1.8-------> \ 1.9 / | 1.9 / 1.9 `1.10' | 1.10 / 1.10 1.11 | 1.11 | | 1.12 | | 1.13 | \ 1.14 | \ 1.15 / \ 1.16 / `-1.17-' [Figure 2.1: How a tag might stand in relation to files's revisions.]
But if you pull the string taut and sight directly along it, you'll see a particular moment in the project's history – namely, the moment that the tag was set (Figure 2.2).
File A File B File C File D File E ------ ------ ------ ------ ------ 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.1 1.8 1.2 1.9 1.3 1.10 1.1 1.4 1.11 1.2 1.5 1.12 1.3 1.6 1.13 1.4 1.7 1.1 1.14 1.5 1.8 1.2 1.15 1.6 1.1 1.9 1.3 1.16 1.7 ----1.2---------1.10--------1.4---------1.17--------1.8-------> 1.3 1.11 1.5 1.17 1.9 1.6 1.17 1.10 [Figure 2.2: The same tag as a "straight sight" through the revision history.]
As you continue to edit files and commit changes, the tag will not move along with the increasing revision numbers. It stays fixed, "stickily", at the revision number of each file at the time the tag was made.
Given their importance as descriptors, it's a bit unfortunate that log messages can't be included with tags or that the tags themselves can't be full paragraphs of prose. In the preceding example, the tag is fairly obviously stating that the project was in a releasable state as of a certain date. However, sometimes you may want to make snapshots of a more complex state, which can result in ungainly tag names such as:
floss$ cvs tag testing-release-3_pre-19990525-public-release
As a general rule, you should try to keep tags as terse as possible while still including all necessary information about the event that you're trying to record. When in doubt, err on the side of being overly descriptive – you'll be glad later when you're able to tell from some verbose tag name exactly what circumstance was recorded.
You've probably noticed that no periods or spaces were used in the tag names. CVS is rather strict about what constitutes a valid tag name. The rules are that it must start with a letter and contain letters, digits, hyphens ("-"), and underscores ("_"). No spaces, periods, colons, commas, or any other symbols may be used.
To retrieve a snapshot by tag name, the tag name is used just like a revision number. There are two ways to retrieve snapshots: You can check out a new working copy with a certain tag, or you can switch an existing working copy over to a tag. Both result in a working copy whose files are at the revisions specified by the tag.
Most of the time, what you're trying to do is take a look at the project as it was at the time of the snapshot. You may not necessarily want to do this in your main working copy, where you presumably have uncommitted changes and other useful states built up, so let's assume you just want to check out a separate working copy with the tag. Here's how (but make sure to invoke this somewhere other than in your existing working copy or its parent directory!):
floss$ cvs checkout -r Release-1999_05_01 myproj cvs checkout: Updating myproj U myproj/README.txt U myproj/hello.c cvs checkout: Updating myproj/a-subdir U myproj/a-subdir/whatever.c cvs checkout: Updating myproj/a-subdir/subsubdir U myproj/a-subdir/subsubdir/fish.c cvs checkout: Updating myproj/b-subdir U myproj/b-subdir/random.c cvs checkout: Updating myproj/c-subdir
We've seen the -r option before in the update command, where it preceded a revision number. In many ways a tag is just like a revision number because, for any file, a given tag corresponds to exactly one revision number (it's illegal, and generally impossible, to have two tags of the same name in the same project). In fact, anywhere you can use a revision number as part of a CVS command, you can use a tag name instead (as long as the tag has been set previously). If you want to diff a file's current state against its state at the time of the last release, you can do this:
floss$ cvs diff -c -r Release-1999_05_01 hello.c
And if you want to revert it temporarily to that revision, you can do this:
floss$ cvs update -r Release-1999_05_01 hello.c
The interchangeability of tags and revision numbers explains some of the strict rules about valid tag names. Imagine if periods were legal in tag names; you could have a tag named "1.3" attached to an actual revision number of "1.47". If you then issued the command
floss$ cvs update -r 1.3 hello.c
how would CVS know whether you were referring to the tag named "1.3", or the much earlier revision 1.3 of hello.c? Thus, restrictions are placed on tag names so that they can always be easily distinguished from revision numbers. A revision number has a period; a tag name doesn't. (There are reasons for the other restrictions, too, mostly having to do with making tag names easy for CVS to parse.)
As you've probably guessed by this point, the second method of retrieving a snapshot – that is, switching an existing working directory over to the tagged revisions-is also done by updating:
floss$ cvs update -r Release-1999_05_01 cvs update: Updating . cvs update: Updating a-subdir cvs update: Updating a-subdir/subsubdir cvs update: Updating b-subdir cvs update: Updating c-subdir floss$
The preceding command is just like the one we used to revert hello.c to
Release-1999_05_01, except that the filename is omitted because
we want to revert the entire project over. (You can, if you want,
revert just one subtree of the project to the tag by invoking the
preceding command in that subtree instead of from the top level,
although you hardly ever would want to do that.)
Note that no files appear to have changed when we updated. The working copy was completely up to date when we tagged, and no changes had been committed since the tagging.
However, this does not mean that nothing changed at all. The working copy now knows that it's at a tagged revision. When you make a change and try to commit it (let's assume we modified hello.c):
floss$ cvs -q update M hello.c floss$ cvs -q ci -m "trying to commit from a working copy on a tag" cvs commit: sticky tag 'Release-1999_05_01' for file 'hello.c' is not a branch cvs [commit aborted]: correct above errors first! floss$
CVS does not permit the commit to happen. (Don't worry about the exact meaning of that error message yet – we'll cover branches next in this chapter.) It doesn't matter whether the working copy got to be on a tag via a checkout or an update. Once it is on a tag, CVS views the working copy as a static snapshot of a moment in history, and CVS won't let you change history, at least not easily. If you run cvs status or look at the CVS/Entries files, you'll see that there is a sticky tag set on each file. Here's the top level Entries file, for example:
floss$ cat CVS/Entries D/a-subdir//// D/b-subdir//// D/c-subdir//// /README.txt/18.104.22.168/Sun Apr 18 18:18:22 1999//TRelease-1999_05_01 /hello.c/1.5/Tue Apr 20 07:24:10 1999//TRelease-1999_05_01 floss$
Tags, like other sticky properties, are removed with the -A flag to update:
floss$ cvs -q update -A M hello.c floss$
The modification to hello.c did not go away, however; CVS is still aware that the file changed with respect to the repository:
floss$ cvs -q diff -c hello.c Index: hello.c =================================================================== RCS file: /usr/local/cvs/myproj/hello.c,v retrieving revision 1.5 diff -c -r1.5 hello.c *** hello.c 1999/04/20 06:12:56 1.5 --- hello.c 1999/05/04 20:09:17 *************** *** 6,9 **** --- 6,10 -- printf ("Hello, world!\n"); printf ("between hello and goodbye\n"); printf ("Goodbye, world!\n"); + /* a comment on the last line */ } floss$
Now that you've reset with update, CVS will accept a commit:
floss$ cvs ci -m "added comment to end of main function" cvs commit: Examining . cvs commit: Examining a-subdir cvs commit: Examining a-subdir/subsubdir cvs commit: Examining b-subdir cvs commit: Examining c-subdir Checking in hello.c; /usr/local/cvs/myproj/hello.c,v <- hello.c new revision: 1.6; previous revision: 1.5 done floss$
Release-1999_05_01 is still attached to revision 1.5, of
course. Compare the file's status before and after a reversion to the
floss$ cvs -q status hello.c =================================================================== File: hello.c Status: Up-to-date Working revision: 1.6 Tue May 4 20:09:17 1999 Repository revision: 1.6 /usr/local/cvs/myproj/hello.c,v Sticky Tag: (none) Sticky Date: (none) Sticky Options: (none) floss$ cvs -q update -r Release-1999_05_01 U hello.c floss$ cvs -q status hello.c =================================================================== File: hello.c Status: Up-to-date Working revision: 1.5 Tue May 4 20:21:12 1999 Repository revision: 1.5 /usr/local/cvs/myproj/hello.c,v Sticky Tag: Release-1999_05_01 (revision: 1.5) Sticky Date: (none) Sticky Options: (none) floss$
Now, having just told you that CVS doesn't let you change history, I'll show you how to change history.