How to check out a CVS branch point?

First, I have to admit I screwed up a little with CVS. I had a release tag releaseX, which was done some time back (i.e., not HEAD). Then I decided I need a maintenance branch at that point. Instead of creating a branch tag (branchX) in addition to releaseX, I deleted the release tag and created a branch tag (erroneously) named releaseX. I then proceeded to work on that maintenance branch, and created releaseX1, releaseX2 etc.

My problem: when I check out releaseX, I get the branch head, i.e. the latest code from that branch. What I need now is the code at the branch point, i.e. the former releaseX code.

Is there any way to do this?

Reverting to earlier repository version from backup is not an option.

Edit: I know I can work around it by doing a date-based checkout. I would like to know if it’s possible to still do a tag-based one.

Update (Re @Philip Derbeko): I know that CVS does not correlate between files. But CVS does have the information where the branch occured. In ViewVC, I can even see it:

File X - Revision 1.y - Branch: MAIN - Branch point for: releaseX

The next file revision is:

File X - Revision 1.y.2.1 - Branch: releaseX - CVS Tags: releaseX1

The metadata is apparently there. Hence my question: Is it possible to check out the branch point, not the branch HEAD?

Since you did not set a tag when branching, the only way I see, is to use the date approach.
But you can still set that tag now. Lets assume you want to call that initial release “ReleaseX0”, because unfortunately the name “ReleaseX” is already occupied for the branch. Depending on whether you want to set the tag on the branch or on the MAIN branch you can use either of these checkout commands:

cvs co -r releaseX -D "2008-12-30" modulename
cvs co -D "2008-12-30" modulename

Then set the tag:

cvs tag releaseX0

From now on you can checkout this release in the same way as you checkout the other releases.


As you pointed out in your comment to my initial response, renaming the branch from releaseX to releaseX_branch like this does not work:

cvs rtag -r releaseX releaseX_branch modulename
cvs rtag -d releaseX modulename

To rename a branch cvs admin -N < old >:< new > needs to be used. But renaming a branch is a bad idea anyway. (I am sorry for bringing it up in the first place ;-).), Renaming will mess up the working copies of other users. So it is probably best to stick with the original name.

This comes four years later – it won’t help you out, but it may help someone like myself trying to figure this problem out via google. I recently ran across the same problem with a merge that I am doing. I need to see the parent ancestor of files between the branch head and mainline head – which is the branch point. The branch point was not tagged and was created years ago – at an indeterminate time so tagging by time is no help.

The solution I came up with is figuring out the branch point revision number for each file and applying a branch point tag via a PERL script. It is very similar to what is pointed out here: http://www.cvsnt.org/pipermail/cvsnt/2006-February/024063.html

A little CVS background: In CVS branches are kept track of by adding a .0.x to the mainline revision of the file. For example if the mainline revision is 1.18, then a branch off of that will be 1.18.0.x, X the branch number (if there were already a branch in existence from this revision number then x would be 2 -> 1.18.0.2). The “.0” is dropped when you revise files on the branch so the branch will be 1.18.0.2 in this example and the first revision will be 1.18.2.1 and the second revision will be 1.18.2.2. A lot of this is drawn from this: http://www.astro.princeton.edu/~rhl/cvs-branches.html#branchnumbers

I found a perl script here: https://github.com/effectiveprogramming/ep-cvs/wiki/List-CVS-Tags that will either list the CVS tags when given the -t switch or if given a branch name tag it will list all files and revision associated with that tag. This script uses this command: “cvs -q status -R -v filename” which will list the branches with the “.0” part hidden, so you simply remove the “.x” from the revision number of the desired branch name. So I took the script on github and added a few lines. You simply execute this script with a branch name as an argument and it will tag the branch point with the same branch name with an _BP appended to it.

#!/usr/bin/perl

# Simple perl script.  Given a branch name, determine 
# the file revision a branch was created on for all 
# files in a repository and tag those files with a user defined tag.    
# Created 2013/04/05 CPG
# $Id: lstag,v 1.1 2002/09/26 10:02:53 ec Exp $

use strict;

$::VERSION = "1.0";
$::cvs_ID = '$Id: lstag,v 1.1 2002/09/26 10:02:53 ec Exp $'; #'

undef ($::repo);
# Try #1 to get CVS repository location
if (-r "CVS/Root") {
    open (INF, "<CVS/Root") || die "Failed to read CVS/Root file!n";
    ###chop ($::repo = <INF>);
    $::repo = <INF>;
    close (INF);
} else {
    # Try #2 to get CVS repository location
    if (!$::ENV{"CVSROOT"}) {
        print "CVSROOT environment variable not found!n";
        print "CVS not detected...n";
        exit (10);
    }
}
$::repo =~ s/n$//g;
$::repo =~ s/r$//g;
($::repo) = $::repo =~ /([^:]+)$/;
$::repo =~ s//*$///;

### print "CVS repository at $::repon";


# Check commandline arguments
if ($#ARGV < 0) {
    print "Missing argument!n";
    print "Usage: $0 [ tag]nn";
    print "Where: tag is number of "; 
    print "     each file this tag was created.n";
    print "       tag   shows list of files with this tagn";
    print "n";
    exit (1);
}

# Get desired tagname
$::tag = $ARGV[0];
$::taglist = 0;
if ($::tag eq "-l") {
    $::taglist = 1;
}



# Run cvs status and catch output
open (INF, "cvs -q status -R -v |") || die "Failed to run cvs status command!n";
chop (@::STATUS = <INF>);
close (INF);

# Parse status
$::state = 0;
$::fpath = $::frpath = $::fname = $::fstatus = $::ftag = $::ftagrev = "!UNINITIALIZED VARIABLE!";
undef (%::TAGS);
$::found = 0;
for $::lc (0 .. $#::STATUS) {
    $_ = $::STATUS[$::lc];
    if ($::state == 0) {
        if (/^File:/) {
            ($::fname, $::fstatus) = /^File:s+(S+s*S+)s+Status:s+(S+)/;
            $::state = 1;
        }
        next;
    }
    if ($::state == 1) {
        if (/^s+Repository revision:/) {
            ($::frpath) = /(/.*),v/;
            ($::fpath) = $::frpath =~ /^$::repo(.*)$/;
            push @::INFOL, ( $::fpath );
            $::current = $::fpath;
            $::INFO{$::current}->{"rpath"}  = $::frpath;
            $::INFO{$::current}->{"name"}   = $::fname;
            $::INFO{$::current}->{"status"} = $::fstatus;
            $::fpath = $::frpath = $::fname = $::fstatus = "!UNINITIALIZED VARIABLE!";
            $::state = 2;
        }
        next;
    }
    if ($::state == 2) {
        if (/^s+Existing Tags:/) {
            $::state = 3;
        }
        next;
    }
    if (/^s+S+s+([^:]+:/) {
        ($::ftag, $::ftagrev) = /^s+(S+)s+([^:]+:s+([^)]+))/;
        if ($::taglist) {
            $::TAGL{$::ftag}++;
        }
        if ($::ftag eq $::tag) {
            $::found++;
            $::INFO{$::current}->{"tag"} = $::ftag;
            $::INFO{$::current}->{"tagrev"} = $::ftagrev;
            $::ftag = $::ftagrev = "!UNINITIALIZED VARIABLE!";
        }
    } else { $::state = 0; }
}

# Print results
print "$0 - CVS tag and file lister version $::VERSIONn";
print "ID: $::cvs_IDnn";
if ($::taglist) {
    print "List of all known tags:nn";
    foreach $::key (sort {uc($a) cmp uc($b)} keys %::TAGL) {
        print "$::keyn";
    }
} else {
    print "Files with tag "$::tag":";
    if ($::found > 0) {
        print "nn";
    } else {
        print "  NONEn";
    }
    for $::i (0 .. $#::INFOL) {
        if (!defined($::INFO{$::INFOL[$::i]}->{"tag"})) {
            next;
        }
        $::name = $::INFOL[$::i];
        $::status = $::INFO{$::name}->{"status"};
        $::tagrev = $::INFO{$::name}->{"tagrev"};

        #  Code added to apply a tag to a branch point:
        # regex to strip from last '.' to end of revision #.
        $::ge=".[^.]*$";

        $::branchPtRev = $::tagrev;
        $_ = $::branchPtRev;
        s/$::ge//;
        $::cmd = "cvs tag -r".$_." ".$::tag."_BP " . $::name . " xst";

        #printf "%10s %sn", ($_, $::name);

        #  printf "%10s %10sn", ($::branchPtRev, $_);

        #printf $::cmd;
        #  printf "n";

        # Run cvs command
        open (INF, $::cmd) || die "Failed to run cvs status command!n";
        close (INF);
    }
}

print "Script Completed Successfullyn";

I’m not a CVS expert. Here’s what I would do if I were in your place. CVS marks file versions in HEAD branch as 1.N, when you branch file at version X, commits to that branch get marked as 1.X.B.M . So after checking out releaseX, I would write a script that would update files that were changed in branch to a version 1.X, and then tag my working copy. Maybe there is a simpler way, but I don’t know about it.

Sorry, can’t be done.
The only option you have is doing time-based checkout.
The problem is that CVS does not correlated between different files, which is done using tags. Once the tag is gone, the information that ties different files together is gone forever. Meaning that you have to write a script finding the branch point for every file.

As a general practice and in case you don’t care for many tags in repository, I would suggest to create a tag on the trunk (or a branch from where you are branching) and on merge as well. Those tags should follow naming convention and then in CVS scripts you can prevent messing with them.

With CVS you should automate as much as possible 🙂

Leave a Comment