Next: , Up: Branches


Branching Basics

Why are branches useful?

Let's return for a moment to the scenario of the developer who, in the midst of working on a new version of the program, receives a bug report about an older released version. Assuming the developer fixes the problem, she still needs a way to deliver the fix to the customer. It won't help to just find an old copy of the program somewhere, patch it up without CVS's knowledge, and ship it off. There would be no record of what was done; CVS would be unaware of the fix; and later if something was discovered to be wrong with the patch, no one would have a starting point for reproducing the problem.

It's even more ill-advised to fix the bug in the current, unstable version of the sources and ship that to the customer. Sure, the reported bug may be solved, but the rest of the code is in a half-implemented, untested state. It may run, but it's certainly not ready for prime time.

Because the last released version is thought to be stable, aside from this one bug, the ideal solution is to go back and correct the bug in the old release – that is, to create an alternate universe in which the last public release includes this bug fix.

That's where branches come in. The developer splits off a branch, rooted in the main line of development (the trunk) not at its most recent revisions, but back at the point of the last release. Then she checks out a working copy of this branch, makes whatever changes are necessary to fix the bug, and commits them on that branch, so there's a record of the bug fix. Now she can package up an interim release based on the branch and ship it to the customer.

Her change won't have affected the code on the trunk, nor would she want it to without first finding out whether the trunk needs the same bug fix or not. If it does, she can merge the branch changes into the trunk. In a merge, CVS calculates the changes made on the branch between the point where it diverged from the trunk and the branch's tip (its most recent state), then applies those differences to the project at the tip of the trunk. The difference between the branch's root and its tip works out, of course, to be precisely the bug fix.

Another good way to think of a merge is as a special case of updating. The difference is that in a merge, the changes to be incorporated are derived by comparing the branch's root and tip, instead of by comparing the working copy against the repository.

The act of updating is itself similar to receiving patches directly from their authors and applying them by hand. In fact, to do an update, CVS calculates the difference (that's "difference" as in the diff program) between the working copy and the repository and then applies that diff to the working copy just as the patch program would. This mirrors the way in which a developer takes changes from the outside world, by manually applying patch files sent in by contributors.

Thus, merging the bug fix branch into the trunk is just like accepting some outside contributor's patch to fix the bug. The contributor would have made the patch against the last released version, just as the branch's changes are against that version. If that area of code in the current sources hasn't changed much since the last release, the merge will succeed with no problems. If the code is now substantially different, however, the merge will fail with conflict (that is, the patch will be rejected), and some manual fiddling will be necessary. Usually this is accomplished by reading the conflicting area, making the necessary changes by hand, and committing. Figure 2.3 shows a picture of what happens in a branch and merge.

     
                  (branch on which bug was fixed)
                .---------------->---------------.
               /                                 |
              /                                  |
             /                                   |
            /                                    |
           /                                     V (<------ point of merge)
      ====*===================================================================>
                     (main line of development)
     
     
     [Figure 2.3: A branch and then a merge.  Time flows left to right.]
     

We'll now walk through the steps necessary to make this picture happen. Remember that it's not really time that's flowing from left to right in the diagram, but rather the revision history. The branch will not have been made at the time of the release, but is created later, rooted back at the release's revisions.

In our case, let's assume the files in the project have gone through many revisions since they were tagged as Release-1999_05_01, and perhaps files have been added as well. When the bug report regarding the old release comes in, the first thing we'll want to do is create a branch rooted at the old release, which we conveniently tagged Release-1999_05_01.

One way to do this is to first check out a working copy based on that tag, then create the branch by re-tagging with the -b (branch) option:

     floss$ cd ..
     floss$ ls
     myproj/
     floss$ cvs -q checkout -d myproj_old_release -r Release-1999_05_01 myproj
     U myproj_old_release/README.txt
     U myproj_old_release/hello.c
     U myproj_old_release/a-subdir/whatever.c
     U myproj_old_release/a-subdir/subsubdir/fish.c
     U myproj_old_release/b-subdir/random.c
     floss$ ls
     myproj/      myproj_old_release/
     floss$ cd myproj_old_release
     floss$ ls
     CVS/      README.txt  a-subdir/   b-subdir/   hello.c
     floss$ cvs -q tag -b Release-1999_05_01-bugfixes
     T README.txt
     T hello.c
     T a-subdir/whatever.c
     T a-subdir/subsubdir/fish.c
     T b-subdir/random.c
     floss$

Take a good look at that last command. It may seem somewhat arbitrary that tag is used to create branches, but there's actually a reason for it: The tag name will serve as a label by which the branch can be retrieved later. Branch tags do not look any different from non-branch tags, and are subject to the same naming restrictions. Some people like to always include the word branch in the tag name itself (for example, Release-1999_05_01-bugfix-branch), so they can distinguish branch tags from other kinds of tags. You may want to do this if you find yourself often retrieving the wrong tag.

(And while we're at it, note the -d myproj_old_release option to checkout in the first CVS command. This tells checkout to put the working copy in a directory called myproj_old_release, so we won't confuse it with the current version in myproj. Be careful not to confuse this use of -d with the global option of the same name, or with the -d option to update.)

Of course, merely running the tag command does not switch this working copy over to the branch. Tagging never affects the working copy; it just records some extra information in the repository to allow you to retrieve that working copy's revisions later on (as a static piece of history or as a branch, as the case may be).

Retrieval can be done one of two ways (you're probably getting used to this motif by now!). You can check out a new working copy on the branch

     floss$ pwd
     /home/whatever
     floss$ cvs co -d myproj_branch -r Release-1999_05_01-bugfixes myproj

or switch an existing working copy over to it:

     floss$ pwd
     /home/whatever/myproj
     floss$ cvs update -r Release-1999_05_01-bugfixes

The end result is the same (well, the name of the new working copy's top-level directory may be different, but that's not important for CVS's purposes). If your current working copy has uncommitted changes, you'll probably want to use checkout instead of update to access the branch. Otherwise, CVS attempts to merge your changes into the working copy as it switches it over to the branch. In that case, you might get conflicts, and even if you didn't, you'd still have an impure branch. It won't truly reflect the state of the program as of the designated tag, because some files in the working copy will contain modifications made by you.

Anyway, let's assume that by one method or another you get a working copy on the desired branch:

     floss$ cvs -q status hello.c
     ===================================================================
     File: hello.c                 Status: Up-to-date
        Working revision:  1.5     Tue Apr 20 06:12:56 1999
        Repository revision:       1.5     /usr/local/cvs/myproj/hello.c,v
        Sticky Tag:                Release-1999_05_01-bugfixes
     (branch: 1.5.2)
        Sticky Date:               (none)
        Sticky Options:            (none)
     floss$ cvs -q status b-subdir/random.c
     ===================================================================
     File: random.c                Status: Up-to-date
        Working revision:  1.2     Mon Apr 19 06:35:27 1999
        Repository revision:       1.2 /usr/local/cvs/myproj/b-subdir/random.c,v
        Sticky Tag:                Release-1999_05_01-bugfixes (branch: 1.2.2)
        Sticky Date:               (none)
        Sticky Options:            (none)
     floss$

(The contents of those Sticky Tag lines will be explained shortly.) If you modify hello.c and random.c, and commit

     floss$ cvs -q update
     M hello.c
     M b-subdir/random.c
     floss$ cvs ci -m "fixed old punctuation bugs"
     cvs commit: Examining .
     cvs commit: Examining a-subdir
     cvs commit: Examining a-subdir/subsubdir
     cvs commit: Examining b-subdir
     Checking in hello.c;
     /usr/local/cvs/myproj/hello.c,v  <-  hello.c
     new revision: 1.5.2.1; previous revision: 1.5
     done
     Checking in b-subdir/random.c;
     /usr/local/cvs/myproj/b-subdir/random.c,v  <-  random.c
     new revision: 1.2.2.1; previous revision: 1.2
     done
     floss$

you'll notice that there's something funny going on with the revision numbers:

     floss$ cvs -q status hello.c b-subdir/random.c
     ===================================================================
     File: hello.c                 Status: Up-to-date
        Working revision:  1.5.2.1 Wed May  5 00:13:58 1999
        Repository revision:       1.5.2.1 /usr/local/cvs/myproj/hello.c,v
        Sticky Tag:                Release-1999_05_01-bugfixes (branch: 1.5.2)
        Sticky Date:               (none)
        Sticky Options:            (none)
     ===================================================================
     File: random.c                Status: Up-to-date
        Working revision:  1.2.2.1 Wed May  5 00:14:25 1999
        Repository revision:       1.2.2.1 /usr/local/cvs/myproj/b-subdir/random.c,v
        Sticky Tag:                Release-1999_05_01-bugfixes (branch: 1.2.2)
        Sticky Date:               (none)
        Sticky Options:            (none)
     floss$

They now have four digits instead of two!

A closer look reveals that each file's revision number is just the branch number (as shown on the Sticky Tag line) plus an extra digit on the end.

What you're seeing is a little bit of CVS's inner workings. Although you almost always use a branch to mark a project-wide divergence, CVS actually records the branch on a per-file basis. This project had five files in it at the point of the branch, so five individual branches were made, all with the same tag name: Release-1999_05_01-bugfixes.

Most people consider this per-file scheme a rather inelegant implementation on CVS's part. It's a bit of the old RCS legacy showing through-RCS didn't know how to group files into projects, and even though CVS does, it still uses code inherited from RCS to handle branches.

Ordinarily, you don't need to be too concerned with how CVS is keeping track of things internally, but in this case, it helps to understand the relationship between branch numbers and revision numbers. Let's look at the hello.c file; everything I'm about to say about hello.c applies to the other files in the branch (with revision/branch numbers adjusted accordingly).

The hello.c file was on revision 1.5 at the point where the branch was rooted. When we created the branch, a new number was tacked onto the end to make a branch number (CVS chooses the first unused even, nonzero integer). Thus, the branch number in this case became 1.5.2. The branch number by itself is not a revision number, but it is the root (that is, the prefix) of all the revision numbers for hello.c along this branch.

However, when we ran that first CVS status in a branched working copy, hello.c's revision number showed up as only 1.5, not 1.5.2.0 or something similar. This is because the initial revision on a branch is always the same as the trunk revision of the file, where the branch sprouts off. Therefore, CVS shows the trunk revision number in status output, for as long as the file is the same on both branch and trunk.

Once we had committed a new revision, hello.c was no longer the same on both trunk and branch – the branch incarnation of the file had changed, while the trunk remained the same. Accordingly, hello.c was assigned its first branch revision number. We saw this in the status output after the commit, where its revision number is clearly 1.5.2.1.

The same story applies to the random.c file. Its revision number at the time of branching was 1.2, so its first branch is 1.2.2, and the first new commit of random.c on that branch received the revision number 1.2.2.1.

There is no numeric relationship between 1.5.2.1 and 1.2.2.1 – no reason to think that they are part of the same branch event, except that both files are tagged with Release-1999_05_01-bugfixes, and the tag is attached to branch numbers 1.5.2 and 1.2.2 in the respective files. Therefore, the tag name is your only handle on the branch as a project-wide entity. Although it is perfectly possible to move a file to a branch by using the revision number directly

     floss$ cvs update -r 1.5.2.1 hello.c
     U hello.c
     floss$

it is almost always ill-advised. You would be mixing the branch revision of one file with non-branch revisions of the others. Who knows what losses may result? It is better to use the branch tag to refer to the branch and do all files at once by not specifying any particular file. That way you don't have to know or care what the actual branch revision number is for any particular file.

It is also possible to have branches that sprout off other branches, to any level of absurdity. For example, if a file has a revision number of 1.5.4.37.2.3, its revision history can be diagrammed like this:

                       1.1
                        |
                       1.2
                        |
                       1.3
                        |
                       1.4
                        |
                       1.5
                      /   \
                     /     \
                    /       \
                (1.5.2)   (1.5.4)         <--- (these are branch numbers)
                  /           \
              1.5.2.1        1.5.4.1
                 |              |
              1.5.2.2        1.5.4.2
                 |              |
               (etc)          (...)       <--- (collapsed 34 revisions for brevity)
                                |
                             1.5.4.37
                               /
                              /
                        (1.5.4.37.2)      <--- (this is also a branch number)
                            /
                           /
                    1.5.4.37.2.1
                          |
                    1.5.4.37.2.2
                          |
                    1.5.4.37.2.3
     
     [Figure 2.4: An unusually high degree of branching.  Time flows downward.]
     

Admittedly, only the rarest circumstances make such a branching depth necessary, but isn't it nice to know that CVS will go as far as you're willing to take it? Nested branches are created the same way as any other branch: Check out a working copy on branch N, run cvs tag -b branchname in it, and you'll create branch N.M in the repository (where N represents the appropriate branch revision number in each file, such as 1.5.2.1, and M represents the next available branch at the end of that number, such as 2).

Karl Fogel wrote this book. Buy a printed copy via his homepage at red-bean.com

copyright  ©  May 23 2019 sean dreilinger url: https://durak.org/sean/pubs/software/cvsbook/Branching-Basics.html