Copyright 2002,2003,2004 Alexander Taler

Draft LibCVS API

This document is a draft. It will change, perhaps a lot. Feeback of any kind is happily received.

Introduction

Purpose

The purpose of LibCVS is to provide access to CVS working directories and repositories through a library interface. This document describes the LibCVS API in a language-independent fashion, which can then be implemented natively in many languages. This API strives present the best solution to the problem of accessing repositories and working directories. As we learn from implementing and using the API, this document will be updated to reflect the new understanding. Its longer term goal is to expand beyond CVS, and present an interface for the more general problem space of version control.

Currently the document describes a read-only API. Specifically, it doesn't specify how to checkout or update to working directories, or commit or tag to the repository.

This is an object-oriented API. However, it is desirable that non-OO languages (notably C), be able to implement it cleanly and without significant effort. To this end, it avoids heavy use of polymorphism and inheritance.

Implementing the API

Changes will have to be made to the API when implementing it. Notably, class and method names should be changed to match language conventions. For example, in Java, it will be desirable to add "get" to the beginning of many of the method names. Classnames can also be expected to change, Branch would become VCS::LibCVS::Branch in Perl, and org.libcvs.Branch in Java.

If an implemention requires or would be eased by a major change to the interface, then that change should be discussed and propagated back to this document.

Although it is not listed, it can be useful for many of these objects to define a boolean method equals() which can determine if two objects of the same type are equal. Implementation of this is left to each language, although it might be useful to add it.

Summary of Objects

The objects of LibCVS are split into three broad groups: those used to access the repository, those used to access working directories (sandboxes), and support objects used for both purposes.

Shared Classes Repository Classes Working Directory Classes
Repository
Datum
DatumFileContents
DatumNumber
DatumBranchNumber
DatumRevisionNumber
DatumRoot
Branch
DirectoryBranch
FileBranch
FileRevision
Directory
FileOrDirectory
File
WorkingDirectory
WorkingFile
WorkingFileOrDirectory
WorkingUnmanagedFile
WorkingUnmanagedDirectory

Examples

Here are a couple of brief examples, just to get an idea of how to use the interface.

Listing Files in the Repository

Repository repo = new Repository(":ext:user@host.example.com/var/cvs")
Directory dir = new Directory(repo, "module")
File[] files = dir.getFiles()
for (i = 0; i < files.length; i++ ) {
    print files[i].name()
}

Listing Files in a Working Directory

WorkingDirectory wDir = new WorkingDirectory("/home/user/sandbox")
WorkingFile[] wFiles = wDir.getFiles()
for (i = 0; i < wFiles.length; i++ ) {
    print wFiles[i].name()
}

Shared Classes

These classes are used both when accessing working directories, and accessing repositories. The majority of these classes are subclasses of Datum, used primarily for encapsulating data.

Repository

The Repository object is used as the starting point for an interaction when no working directory is present. It is also used internally for connecting to the repository. It has the following methods:

Repository new(string root)
Constructor which takes a root string, such as ":ext:server.example.com/var/cvs".
DatumRoot root()
Returns a DatumRoot object which identifies this repository.
string version()
Returns the version of CVS running at the repository.

Datum

Datum (the singular of Data) is a superclass for several data-wrapping classes. Depending on language convention, its subclasses can provide their data either as public accessible members, or through methods. It also defines two methods which subclasses are expected to implement.

The user of LibCVS will generally use Datum objects just for reading information. There should be no need for them to construct these objects.

There are several more subclasses of Datum used internally by LibCVS, but are not part of the public API.

string asString()
Return this Datum as a string.
boolean equals(Datum d)
Indicate if they represent the same Datum, not if they are the same object. Generally subclasses will return true if and only if their data members are equal.

DatumFileContents

Superclass: Datum

A datum containing the contents of a file. It has the following data members:

Contents
Length

DatumNumber

Superclass: Datum

A datum containing a revision number. It can represent a regular revision number ("1.1"), a branch number ("1.1.2") or magic branch number("1.1.0.2"). In general this class should be primarily internal. Users of the library may need to print out values and create new ones from user supplied information, but branch traversal should usually use the methods of FileRevision and FileBranch.

Some revision number methods are branch or revision specific, these methods have been split off into subclasses. Optionally, all methods could be rolled into a single class, and modified to raise errors if they are called inappropriately. If this is done, an additional method boolean isBranch() would have to be added.

To describe the relationship among both revision and branch revision numbers, the terms ancestor and descendant are used. The ancestral relationship is transitive, so if a is an ancestor of b, and b is an ancestor of c, then it follows that a is an ancestor of c. A revision revision number is an ancestor of all revision numbers that follow it on its branch. A branch revision number is a descendant of its base revision, and an ancestor of all other revisions on that branch. Revisions which don't have an ancestral relationship are called incomparable. Descendant is the opposite of ancestor, so if a is an ancestor of b, then b is a descendant of a.

Thus "1.6" is an ancestor of "1.9", "1.6.2", "1.6.2.5" and "1.19.14.4"; "1.6.2" is an ancestor of "1.6.2.5" and a descendant of "1.6"; and "1.6.2.5" and "1.19" are incomparable.

Because of CVS's lazy branching scheme, branches that sprout from the same revision could be ancestors. The terms possible ancestor and possible descendant are used to describe this relationship. For example, "1.6.2" is a possible ancestor of "1.6.4".

It has the following data member:

Number

and some methods:

RevisionNumber new(string number)
Construct a new revision number from a string such as "1.32.6.12", or raise an error if it's malformed. It makes sense to construct a revision number, from user supplied input for example.
enum compare(DatumNumber other)
Compare this revision number to another to determine their ancestral relationship. Possible return values are:
  • EQUAL: They are the same revision.
  • ANCESTOR: other is an ancestor of this revision.
  • DESCENDANT: other is a descendant of this revision.
  • POSSIBLE_ANCESTOR: other is possibly an ancestor of this revision.
  • POSSIBLE_DESCENDANT: other is possibly a descendant of this revision.
  • INCOMPARABLE: none of the above is true.

DatumBranchNumber

Superclass: DatumNumber

A branch revision number, with the following additional methods:

boolean isTrunk()
Return true if this is a trunk revision number (eg "1" or "2").
boolean isImportBranch()
Return true if this is an import branch. It is assumed this is equivalent to having an odd branch revision number.
DatumRevisionNumber base()
Returns the revision number for the base of this branch revision.
DatumRevisionNumber firstRevision()
Returns the first revision number committed to this branch. This is not the same as the base. It's the branch number with a ".1" appended.

DatumRevisionNumber

Superclass: DatumNumber

A revision revision number, with the following additional methods:

DatumBranchNumber branch()
Returns the revision number of the branch of this revision.
DatumNumber predecessor()
Return the revision number that immediately precedes this one, it's youngest ancestor.
DatumNumber successor()
Return the revision number that immediately follows this one, it's eldest descendant. At a branch point a revision may have several immediate descendants. This method will return a revision on the same branch, essentially incrementing the revision number by one. eg successor of "1.2" is "1.3", not "1.2.2.1". As a consequence a.equals(a.predecessor().successor()) is not necessarily true, although a.equals(a.successor().predecessor()) is true.

DatumRoot

Superclass: Datum

A datum containing a CVSROOT specification, for contacting a repository. It has the following data members:

Protocol
Username
Hostname
Port
Directory

Repository Classes

These objects represent the contents of the repository. They do not require, nor refer to checked-out files or local administration files. They are named with filesystem names, relative to the root of the repository.

FileOrDirectory

A FileOrDirectory is a superclass which is not used directly. It's not called RevisionedObject because directories aren't Revisioned.

static FileOrDirectory find(Repository r, string Name)
Find a named (eg "module1/file1") file or directory in the repository. The returned object will be an instance of a subclass. This method is useful in parsing command-line arguments, when you're not sure if a file or directory has been specified.
string name()
Return the filename of this object, within the repository. eg "module1/file1"
Repository repository()
Return the repository this object is part of.
Directory directory()
Return the repository directory this object is in. Raise an error if this method is called on the top level directory ".".

Directory

Superclass: FileOrDirectory

The top level repository directory is denoted by ".".

Directory[] directories()
Returns the directories which are in this directory.
Directory directory(string name)
Get the named directory which is in this directory. Raise an error if there is no directory by that name.
File[] files()
Returns the files which are in this directory.
File file(string name)
Get the named file which is in this directory. Raise an error if there is no file with that name.

File

A file in the repository. It encompasses all revisions on all branches of that file. Essentially, this is everything in the ,v file.

FileBranch[] branches()
Return a list of all the branches on this file.
FileBranch branch(string name)
Return the named branch, or signal an error if there's no branch by that name.
FileBranch branch(DatumNumber rn)
Return the branch with the given revision number, or raise an error if there's no branch with that number.
FileBranch branch(Branch branch)
Return the branch on this file of the given repository-wide branch, or raise an error if that branch doesn't exist on this file.
FileRevision revision(DatumNumber rn)
Return the FileRevision with the given revision number, or raise an error if there's no revision with that number.
string[] tags()
Return a list of all the non-branch tags on this file.

DirectoryBranch

A directory as seen from a branch. From this class you can get all of the files which are on a specific branch, rather than all the files that exist in the repository. In CVS terms this is a rather artificial construction, since CVS neither revisions nor branches directories.

Directory directory()
The repository directory this is a branch of.
Branch branch()
The branch this is a part of.
FileBranch[] fileBranches()
One for for each file in this directory that is on this branch. (It's not clear if this should include files whose tip revision on this branch is dead.)

FileBranch

A single branch of a single file. It has a branch name, which is shared with FileBranches for other files, as well as a revision number which is specific to this file.

DatumBranchNumber revisionNumber()
The revision number of this file branch, specific to this File.
File file()
The file this is a branch of.
Branch branch()
The branch this is a part of.
FileRevision tipRevision()
The most recent revision of this file on this branch.
FileRevision firstRevision()
The first revision of this branch. This is usually "1.1" for the trunk, or the base revision of the branch. The FileRevision must remember which branch it's on, so that calling successor() on it will return another revision on this FileBranch.
FileRevision revision(time t)
The revision that was on the tip of the branch at the given time. Return nothing if the branch did not exist at that time.
boolean isTrunk()
True if this branch is a trunk.
FileBranch parent()
The parent branch, or raise an error if this is a trunk.
boolean precedes(FileBranch other)
True if this branch is an ancestor or possible ancestor of the given branch.

FileRevision

One revision of a particular file, identified by a revision number. It remembers which branch it's on, so that successor and predecessor can be used effectively to traverse the branch. This is necessary because the base revision of a branch is on that branch.

DatumRevisionNumber revisionNumber()
The revision number of this FileRevision, specific to this File.
File file()
The File this is a revision of.
FileBranch fileBranch()
The FileBranch this is a revision of.
string logMessage()
The log message that this revision was committed with.
string committer()
The username which committed this revision.
time time()
The time this revision was committed.
boolean isDead()
True if this is a dead revision.
DatumFileContents contents()
The contents of this revision.
FileRevision predecessor()
The FileRevision immediately before this one, or nothing if it is the first revision. The returned FileRevision should be on the same branch, if possible.
FileRevision successor()
The FileRevision immediately following this one, on the same branch. Nothing if it has no successor on this branch.
enum compare(FileRevision)
See DatumNumber for details of compare. Possible return values are EQUAL, ANCESTOR, DESCENDANT, and, INCOMPARABLE. Raise an error if they are FileRevisions of different files.

Branch

A named branch across the entire repository. It is a collection of specific numbered branches for multiple files, all of which share a name.

string name()
The name of the branch, just the tag which identifies it.
FileBranch[] fileBranches()
All of the FileBranches which are part of this branch. This is a very expensive operation, because it means searching for tags on every file in the repository.

Working Directory Classes

Objects which represent the state of a CVS working directory or sandbox. The working directory can be traversed using these objects.

WorkingFileOrDirectory

A parent class for local directories or files.

string name()
The filename of this object.
Repository repository()
The repository that this object is checked out from.
FileOrDirectory repositoryObject()
The associated repository object, of the same type: file or directory.
Branch branch()
The branch this object is checked out on.

WorkingDirectory

Superclass: WorkingFileOrDirectory

WorkingDirectory new(string name)
Constructor from a directory name in the local filesystem.
WorkingFile[] files()
The CVS managed files in this directory, as specified in the CVS administrative file CVS/Entries.
DirectoryBranch directoryBranch()
The directory branch this directory is checked-out from.
WorkingUnmanagedFile[] unmanagedFiles()
Files in this directory which are neither managed by nor ignored by CVS. Note that this may include a file with the same name as one in the repository, indicating a conflict situation.
WorkingUnmanagedDirectory[] unmanagedDirectories()
Directories in this directory which are neither managed by nor ignored by CVS. Note that this may include a directory with the same name as one in the repository, indicating a conflict situation.
WorkingDirectory[] directories()
Subdirectories of this one, which are managed by CVS.
string[] ignoredFiles
Files in this directory which CVS is ignoring.
string[] ignoredDirectories
Directories in this directory which CVS is ignoring.

WorkingFile

Superclass: WorkingFileOrDirectory

A file in the working directory managed by CVS.

In a way, a WorkingFile is just a special sort of FileRevision. Could this viewpoint be useful to the interface?

WorkingFile new(string name)
Constructor from a file name in the local filesystem.
FileBranch fileBranch()
The branch which this file was checked out on. It is determined by looking for a sticky branch tag, and if one isn't found, at a revision number. If a branch has a non-sticky branch tag, there is an ambiguity about which branch it's on. In some cases, looking for the same tag on other files may help, but is expensive, and not always effective. Instead, it is assumed that the tag is not on a base revision when choosing a branch.
FileRevision fileRevision()
The file revision from which this WorkingFile was checked-out.
WorkingDirectory directory()
The directory this file is in.
enum scheduledAction()
Returns one of ADD, REMOVE, or NONE, indicating if any action has been scheduled for this file.
enum state()
The state of the local file, compared to what was checked-out. One of UPTODATE, MODIFIED, HADCONFLICTS, MISSING.
enum repositoryState()
The state of the repository compared to what is checked-out. One of UPTODATE, MODIFIED, WILLCONFLICT, MISSING.
DatumRevisionNumber revisionNumber()
The checked-out revision number. In the case of an imported file which has been checked out on the trunk, CVS returns a revision number of "1.1.1.1", but a commit will result in a new revision number of "1.2". This method hacks this case to return "1.1".
DatumFileContents fileContents()
The contents of this file.

WorkingUnmanagedFile

Superclass: WorkingFileOrDirectory

A file in a CVS working directory, which is neither managed by nor ignored by CVS.

boolean isInTheWay()
True if there's a file in the repository with the same name as this one, but isn't checked out. If this file has the same contents as the one in the repository, CVS will happily check it out. Should LibCVS do the same?
DatumFileContents fileContents()
The contents of this file.

WorkingUnmanagedDirectory

Superclass: WorkingFileOrDirectory

A directory in a CVS working directory, which is neither managed by nor ignored by CVS.

boolean isInTheWay()
True if there's a directory in the repository with the same name as this one, but isn't checked out.