Introduction
Most of my development projects are Visual Studio .NET projects that are stored in CVS repositories running on a Linux machine. Additionally, we use a third party bug tracking software that runs on Windows 2000 to track bug fixes/customer problems, etc. For auditing reasons, I needed to develop a program that would tie together the history of files changed in the CVS repository between different version tags with problem tracking records in the bug tracking system. To do this, I needed an easy way to access the history data in the CVS repository. In doing so, I created a simple and extensible class for accessing the CVS repository directly from a C# class.
I am not going to cover the part about accessing the bug tracking software. That software stores its information in an ODBC compliant database. That portion of the problem was neither unique nor interesting.
Accessing the CVS Repository: Setup
I do not want to go into great details on how to set up CVS. Suffice it to say that we use WinCVS on the Windows Client machines with SSH server authentication. By using this combination, not only can we access the CVS repository through WinCVS, but we can also access it from the command prompt.
The code
To start, I created a central class called CVSCommand
. That class executes an arbitrary CVS command via a command line process. Additionally, it starts a secondary thread that monitors the console output of the CVS command and places it into a buffer for processing. Though you can issue CVS commands directly by using this class, I derived specific CVS command classes from CVSCommand
.
To execute a command, the CVSCommand
or derived class must know a few things. It must know the CVS Root, the RSH (remote shell) command, and the Working Directory for the command execution. There are property variables created to store this information. For example, to execute an arbitrary command using the CVSCommand
class:
CVSCommand cmd = new CVSCommand("log MainForm.cs");
cmd.CvsRoot = "username@myserver.com:/home/cvs";
cmd.CvsRsh = "c:/Program Files/GNU/SSHCVS/ssh.exe"
cmd.WorkingDirectory = @"c:\cvs repository\application 1\";
cmd.Start();
cmd.WaitDone();
Console.WriteLine(cmd.Output);
Using the CVSGetFileHistory Class
Though you can issue any CVS command using the CVSCommand
class, the parsing of the response is still required. For my application, I created a simple derived class called CVSGetFileHistory
. That command uses all the built in functionality of the base class to execute the CVS command, but adds special parsing code to parse the CVS response in a familiar and easy to use format. Additionally, the constructor allows you to specify the file you want the history of. For example, the above code can be changed to use the CVSGetFileHistory
class as follows:
CVSGetFileHistory cmd = new CVSGetFileHistory("MainForm.cs");
cmd.CvsRoot = "username@myserver.com:/home/cvs";
cmd.CvsRsh = "c:/Program Files/GNU/SSHCVS/ssh.exe"
cmd.WorkingDirectory = @"c:\cvs repository\application 1\";
cmd.Start();
cmd.WaitDone();
foreach (CVSHistoryItem hi in cmd.History)
{
Console.WriteLine("File: {0}", hi.File);
Console.WriteLine("Revision: {0}",hi.Revision);
Console.WriteLine("Date: {0}", hi.Date);
Console.WriteLine("Author: {0}", hi.Author);
Console.WriteLine("Description: {0}", hi.Description);
}
A Closer Look at CVSCommand
The CVS Command is a rather simple class. As previously stated, its main function is to execute an arbitrary CVS command via a command line process. To do so, it uses the System.Diagnostics.Process
class. However, it must also ensure that certain environment variables used by CVS are defined. After it starts the process, it also starts a background thread that monitors the console output of the process and appends it to a buffer. The method Start()
is where all of this is handled:
public void Start()
{
if (this.Running == true)
return;
ProcessStartInfo i = new ProcessStartInfo("cvs", command);
i.UseShellExecute = false;
if (this.CvsRoot.Length != 0)
i.EnvironmentVariables.Add("CVSROOT", this.CvsRoot);
if (this.CvsRsh.Length != 0)
i.EnvironmentVariables.Add("CVS_RSH", this.CvsRsh);
if (this.WorkingDirectory.Length != 0)
i.WorkingDirectory = this.WorkingDirectory;
i.RedirectStandardOutput = true;
i.CreateNoWindow = true;
p = Process.Start(i);
monitor = new Thread(new System.Threading.ThreadStart(MonitorMain));
monitor.Start();
}
Once the command is started, the Running
property can be checked to see if the command is still executing or the WaitDone()
method can be called to wait until the process is completed.
The background monitoring thread is rather simple. The code simply reads the StandardOutput
property of the process and appends the results to the buffer. Furthermore, it monitors the Running
property of the class to determine when it can stop execution.
Deriving a new Class
Deriving a new class is not difficult. All you really need to do is provide a constructor that accepts the proper information for the command and parsing functions to parse the command response. For example, the CVSGetFileHistory
constructor looks like this:
public CVSGetFileHistory(string file) : base("log "+file)
{
}
It also has several properties that act as parsing functions for the Output
of the CVSCommand
class. For example, when a cvs log
function is executed on the command line, this is output to the console:
C:\MCS\APPLIC~1\NM90>cvs log AssemblyInfo.cs
RCS file: /home/cvs/MCS/Applications/NM90/AssemblyInfo.cs,v
Working file: AssemblyInfo.cs
head: 1.61
branch:
locks: strict
access list:
symbolic names:
NM90_Version_2_3_Build_52: 1.57
NM90_Version_2_3_Build_50: 1.56
NM90_Version_2_3_Build_14: 1.41
NM90_Version_2_2_Build_44: 1.22
NM90_Version_2_2_Build_42: 1.21
NM90_Version_2_2_Build_40: 1.20
NM90_Version_2_2_Build_32: 1.15
NM90_Version_2_2_Build_28: 1.12
NM100_Version_2_2_Build_6: 1.5
keyword substitution: kv
total revisions: 61; selected revisions: 61
description:
----------------------------
revision 1.61
date: 2004/11/22 13:50:06; author: cnelson; state: Exp; lines: +1 -1
PRN:302
----------------------------
revision 1.60
date: 2004/11/16 21:04:21; author: cnelson; state: Exp; lines: +1 -1
PRN:310
----------------------------
...
To make this more useable from a programming perspective, the derived class needs to parse this information and present it in a way that can be easily used. For example, CVSGetFileHistory
parses all the revision text listed above and places it into a container class called CVSHistoryItemList
which is exposed as the History
property variable. In doing so, the following can be easily done:
cmd.Start();
cmd.WaitDone();
foreach (CVSHistoryItem hi in cmd.History)
{
Console.WriteLine("File: {0}", hi.File);
Console.WriteLine("Revision: {0}",hi.Revision);
Console.WriteLine("Date: {0}", hi.Date);
Console.WriteLine("Author: {0}", hi.Author);
Console.WriteLine("Description: {0}", hi.Description);
}
Additional Notes
In closing, this basic class does what I needed it to do, no more and no less. Additional error trapping certainly could be added with not too much difficulty. Additional commands could also be implemented, as well.
History
- November 22, 2004 - Initial posting.