Tree growing with Cvs

Cvs work with two interwoven trees at the same time : one across space and one across time.

The space one is familiar, it is just your usual directories & file arragement. For instance, in the cvs, webpages could be stored in the directory html, and password protected pages stored in html/internal.

To bring existing files into a new cvs directory

     cd <location of the files>
     cvs -d ~/cvsroot import <cvs-directory> <vendor-branch-tag> <branch-point-tag>

The last two argument are required since they are usualy good pracice. However
we don't use their functionnality, just put anything, try to be original. Or maybe
"<cvs-directory>-branch" and "<cvs-directory>-initial-import" would be a good choice.
You can save yourself typing "-d ~/cvsroot", set the environment variable CVSROOT to the
desired value. Depending on your shell, its done with one of the folowing :
 export CVSROOT=~/cvsroot                  bash
 setenv CVSROOT ~/cvsroot                  tcsh
 set CVSROOT=c:\home\cvsroot               command.com (ick!)
You may now delete the <location of the files>, as its content has been absorbed by the cvs
repository (or at least move it out of the way, if you are unsure). Do not go on working
right there on the source files. Wait until they become an official cvs working-directory, just
keep on reading for now.

The time-tree is unique to version control. In this tree, each branch keeps complete copy the files of the project such that development on one branch does not influence the other branches.

    ,---------o
x===^============o

Think of every character in this ascii as containing a complete set of al the files of the project. x marks the initial imported files, o's mark the lastest files. = mark the main trunk of the development tree and - is some other line of development. The ^ is the branch point : the last moment before work started getting done in parallel.

At each state of the project (each 'character'), each file has a revision number that allow to retrieve the said files, for the said revision. The version number are automaticly generated every time you checkin a file. They are numberous (pun intended) and grow complex as the number of branches grows. It is not unusual to see revision number like "1.10.2.1.2.2.10.2.2.1.2.1". You shall not worry about them and treat them as tokens. If you ever need to rool back a file, you will use the cvsweb interface to find the revision number of the desired version.

To bring a file out of cvs

   cvs -d ~/cvsroot checkout -r <revision-number (or tag name)> <directory/subdir/file.ext ...> 

Whole directory can be brought out as well (recursively):
   cvs -d ~/cvsroot checkout -r <revision-number (or tag name)> <directory/subdir/ ...> 

The directory names up there refer to within the cvs repository, ie they should not begin with a '/' nor a '~'. Beside retrieving by version number, it is also possible to go by date
   cvs -d ~/cvsroot checkout -D "24 Sep 1972 20 :05" <file(s) ...> 

A directory is created with the name of the cvs-directory. This "cvs working directory" contains your files and some control files that enable checkins, taggings, updating, and more (they are tucked away in small directories named "CVS"). Note that any subdirectory created is also a "cvs working directory". You can now start working.

To get a file to a existing cvs-working-directory use the update; instead. Its usage is otherwise symetric to checkout
   cd <location of the cvs-working-directory>
   cvs -d ~/cvsroot update -r <revision number (or tag name)> <file>

Update will deposit the requested version, without clobering whichever version you had. It will however try to carry any uncommited changes to the freshly retrieved version. If this is not what you want, just delete the file containing your changes before doing the update.

You should checkin regularly. At the low level cvs serves as a form of day-long undo. Checkins is are easy :
   cd <location of the cvs working directory>
   cvs -d ~/cvsroot commit
You will be asked to label your changes and the current state of affairs. Try to be short yet descriptive, you will thank yourself later. A good template is to state what you were working on, what for, how far along you are and what is to come next. There is no need to checkout again : your cvs-working-directory stays one.

The argument of the -r can either be a revision-tag name or a branch-tag name. They are different animals yet they both mark retrievable version. More to come, right after we discussed 'cvs update' better.

To never clober changes, to never impede change (Interlude)

With cvs, two developers are never forbiden to apply changes in parallel - even to the same file. Other systems tend to enforce only one modificator at a time, per file, through various locking mecanisims. However, historicaly this scheme has only generated lineups for locks, yelling and begings for locks to be released quicker, and, eventualy, bugs from hurried up programmers. Thus cvs was born lock-less. Cvs employs a semi-inteligent merging method based on the unix programs 'diff' and 'patch'. Typical situations goes as follows :
  • Alice and Bob both checkout and modify 'main.cc', leading to 'main.cc(A)' and 'main.cc(B)'
  • Alice checks-in. Using diff, cvs generates a patch main.cc -> main.cc(A)
  • and saves it in the repository
  • Bob learns of Alice neat, stable, and generaly useful changes. He types 'cvs update main.cc'. (note 2)
  • First cvs generates a patch main.cc -> main.cc(B) to represent Bob's changes. Then, it interweaves it line-wise with the main.cc -> main.cc(A) patch, giving main.cc -> main.cc(A + B) (note 1). This final patch is applied to Bob's main.cc
  • Bob keeps on working with a main.cc that now contains both his changes and Alice's. Nothing was lost clobered, and nobody had to wait on any lock.
  • At the end of the day, Bob commits as usual.
Note 1 : If both programmer modified the same lines, the interweaving will clash. When this happens, cvs will abort the checkin and ask you to merge the changes manualy. Upon opening the problem file, Bob will find cvs highlighted the regions needing care with the markers <<<<<<<, and >>>>>>>, with the two version shown side to side, separated by =======.

Note 2 : If Bob had never heard of Alice's changes and just went on, at his next commit he would have got an explicit request to go throught the change-merging process with the following fairly cryptic message :

/home/bob/cvs-working-dir > cvs commit
cvs commit : Examining .
cvs commit : Up-to-date check failed for `main.cc'
cvs [commit aborted]: correct above errors first!
The correct answer is, of course, 'cvs update main.cc'.

The standart piece of advice given here is : Automatic merges works better with small, incremental changes. Commit often for best results. Also, all this is no subtitute for team communication. If you don't talk to your fellows, automatic-merging will turn into automatic-bug-generation, and that sure ain't fun.

To revision-tag a version of the project

Revision tags allow to assign names to nodes in the time-tree. Since revision number only work for one file at a time, they are your only way to retrieve the complete project at a particular time (beside dated checkout). When done well, they can be extremely useful. Cast yourself in the future and look for statement like "It seem realy broken now, but I do remember it working well right before that big demo", or "Gosh, looks like that big restructuring wasn't such a good idea after all." Tagging right before the big demo, and right before the attempted restructuring garantee an easy, mess-free roll-back if something goes wrong.

You want to tag right after a checkout, or right after a checkin, otherwise it is unclear weither your current modification are being tagged (they aren't)

    cd <location of the cvs working directory>
    cvs tag <tag-name> 

It is also possible to tag by date :
 cvs rtag -D <date> <tag-name>
The command rtag (stands for "repository tagging"). It tags absolutly, without looking at your working directory. Thus no cd'ing is necessary.
More operation on revision-tags :
 
cvs rtag -d <tag-to-delete> 
cvs rtag -r <tag><another-tag-for-the-same-version> 
If you are getting a warning about duplicate tags when trying to move a tag, delete the old tag first. Don't use -F (Force), as it may awaken deleted files.

Now that you know about revision-tags, you can go back to the template cvs import and understand the branch-point-tag argument. The import command automaticly revision-tags the root of the tree, strategicly marked with an x on the ascii tree.

Branch tags

As opposed to revision-tags, which never moves until you delete them, branch-tags hop along to always point to the most recent revision of a branch (its "head"). Since there alway one and only one head to every branch, the name of the branch-tag is also used as the name of the branch itself. In the ascii tree, branch-tags are marked with an o. The head of the main trunk is always the same easy to remember toponym : it is called HEAD. You will have to name the other branches as you create them. No typos are allowed as neither branches nor branch-tags can be deleted (at least without generating some humongous revision numbers).

To create a branch

The command is similar to tagging. Make sure you execute right after a checkout, or a checkin
   cd <location of cvs working directory>
   cvs tag -b <branch-tag-name>
   cvs update -r <branch-tag-name>
   cvs commit

The second line is important. Right after creating a branch, there is an ambiguity about which branch further development should be commited to. Cvs keeps in your working directory the name of the destination branch - this is your "sticky tag". The second line set your sticky tag to the new branch as a side effect of retrieving the said version. If you forget it you may end up commiting to the MAIN branch, usualy not what you want.

To confirm which branch you are about to commit to
   cvs status
or
   cvs status <file>

And look for the name of your sticky tag. It must be a the name of a branch-tag if intent to commit.

Cvs provide plenny of automatic merging facility, which I never use. Just the though of machine-generated bugs sends shivers down my spine. Instead, I suggest you use the highly useful ediff interative merge program. It might not be as fast, but it give you an excellent occasion to revise the changes with your neighboor and confirm that no straddling happened.

To merge changes with emacs' ediff

Inspire yourself from this recipe
   mkdir tmp
   cd tmp
   cvs checkout -r branch-one <module>
   mv <module> <module>-one
   cvs checkout -r branch-two <module>
   mv <module> <module>-two
   diff -rq <module>-one <module>-two > differences
   emacs differences
In emacs, go through each differing pair with ediff (hit '?' for help). The most
important keys are n, p, a, b; and maybe 'ra' and 'rb', then '##' and 'qy'
   M-x ediff-files <module>-one/<file> <module>-two/<file>

Once you are done, save everything ('C-x s!'), and you should have two identical directory, containing all the functionality of both original modules. Just commit one of them.

The following emacs lisp code can go in your .emacs file. It is meant to be used on the output of a 'diff -rq oldVersionDir newVersionDir' command, it will pick up the two file names on the current line and start an ediff session. Very helpful to complete a manual merge quickly.

(defun pickup-differing-files-and-ediff ()
  (interactive)
  (save-excursion 
    (beginning-of-line)
    (if (not (looking-at "^Files \\(.*\\) and \\(.*\\) differ$"))
        (error "Not on a diff line - try 'diff -rq dir1 dir2'")))
      (forward-line)
      (ediff-files (match-string 1) (match-string 2)))

Happy cvsing!

[back]