Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
This article contains an in-depth treatment of the Open Tools API of Borland Delphi. The techniques are used to develop the start and skeleton of a personal Version Control System that can be integrated into the Delphi IDE.
Working with several programmers on the same project at the same time requires some sort of rules or support.
We could either make a rule that every time a programmer starts working on a give unit, form or other source file, nobody else is to touch this file.
That way, only one programmer should be working on a single source file at a time.
Using a shared network fileserver, the latest versions of each source file can be distributed, for example in a directory where the entire project team can read and write.
We cannot put the entire project itself on the network and work from there, as each programmer will at least have the main .DPR project file open, and can modify it (like when some programmer adds a new form and some source lines need to be added to the .DPR file as well).
There is one other rule needed when working with several programmers on the same source files, and that's the use of version numbers.
If one programmer changes a file and puts it back on the shared network, then all other programmers must be able to see that the version number of that source file has increased, so any local copies (with lower version numbers!) must be replaced by this source file with the latest version number.
Using these two rules, we can indeed work with several programmers on the same project, but it's not easy to make sure everyone holds on to these rules.
Mistakes are easy to make, and the results often disastrous!
Version Control Systems offer features and functions to support (Delphi) Workgroup Development.
They often offer a common repository where source files can be stored, along with their version number (and some other comments, if needed).
VCSs also offer the ability to lock files in the repository, so every user of the VCS has by default only read-only access to the source files.
If someone needs to actually work on a certain version of a source file, then that user needs to tell the VCS that he wants to work with that source file.
This is done by a 'Check Out' function, which locks that version of the source file for further checkout by other users.
It also gives the user the requested version of source file with read-write permission.
So, now that user is the only one who can make changes to that particular version.
After the changes are done, the file can be 'Checked In' (with a higher version number) and is unlocked, so other users can again check it out for further changes.
The user who checked the file into the archive is left with a read-only copy again, so no nobody can make any changes (unless someone checks out the file again).
This check in/out repository scheme will assure us that at a certain time, only one programmer is working on a certain version of a source file.
This will prevent two programmers from updating the same file, while still working on the same project, which is one of the most important aspects of version control.
Certain 'smart' version control systems will not store the entire new version of a source file, but will only store the differences between one file and another, so that with the original source file and the differences, the next version can be recreated, but we'll discuss those advanced features some other time.
Basically, a Version Control System should offer programmers support for Workgroup Management, Version Management and/or Change Management.
Workgroup Management is the ability to share source files among multiple users (single repository!).
Version Management is the ability to have multiple versions of the same source files (database).
Change Management is the ability to generate 'diffs' and upgrade using diffs (visual difference engine)
All these VCS functions sure sound nice, but can we get it with Delphi? Yes, there are already several major (read: expensive!) Version Control Systems that work with Delphi.
The first one is PVCS, which comes with a DLL hook with Delphi (the hook is also part of Delphi C/S).
The second one is MKS which also ships with a hook to Delphi.
Furthermore, we all know by now that Delphi is a very open environment, and it turns out that we can write our own DLL hook from Delphi to our most popular version control system.
We can even create our own Version Control System if we'd like, and hook it up to Delphi, which is what the remainder of this article will be all about...
VCS Interface
There are some files that are very important when it comes to writing our own Version Control System for Delphi.
These files are VCSINTF.PAS (the definition of the VCS class), TOOLINTF.PAS (the definition of the ToolServices we need to use) and EXPTINTF.PAS (the unit where the ToolServices come from).
Other than that, we can consider a Version Control System for Delphi to be just another DLL-Expert.
In order to write our own Version Control System that is able to hook into Delphi's IDE, we need to derive from the abstract base class TIVCSClient.
The definition of this base class can be found in VCSINTF.PAS.
This file defines the cooperative interface between the Delphi IDE and a VCS Manager DLL.
I've modified the source for this file a little bit to ensure 16-/32-bit compatibility (single source compilable) with both 16-bits Delphi 1.0x and 32-bits Delphi 2.0x/3.0x:
unit VcsIntf; { VCS Interface declarations for both Delphi 1.x and 2.x/3.x } interface uses {$IFDEF WIN32} Windows, {$ELSE} WinTypes, {$ENDIF} VirtIntf, ToolIntf; const isVersionControl = 'Version Control'; ivVCSManager = 'VCSManager'; {$IFDEF WIN32} VCSManagerEntryPoint = 'INITVCS0013'; {$ELSE} VCSManagerEntryPoint = 'INITVCS0011'; {$ENDIF} type TIVCSClient = class(TInterface) function GetIDString: string; virtual; {$IFDEF WIN32} stdcall; {$ELSE} export; {$ENDIF} abstract; procedure ExecuteVerb(Index: Integer); virtual; {$IFDEF WIN32} stdcall; {$ELSE} export; {$ENDIF} abstract; function GetMenuName: string; virtual; {$IFDEF WIN32} stdcall; {$ELSE} export; {$ENDIF} abstract; function GetVerb(Index: Integer): string; virtual; {$IFDEF WIN32} stdcall; {$ELSE} export; {$ENDIF} abstract; function GetVerbCount: Integer; virtual; {$IFDEF WIN32} stdcall; {$ELSE} export; {$ENDIF} abstract; function GetVerbState(Index: Integer): Word; virtual; {$IFDEF WIN32} stdcall; {$ELSE} export; {$ENDIF} abstract; procedure ProjectChange; virtual; {$IFDEF WIN32} stdcall; {$ELSE} export; {$ENDIF} abstract; end; { A function matching this signature must be exported from the VCSManager DLL } TVCSManagerInitProc = function (VCSInterface: TIToolServices): TIVCSClient {$IFDEF WIN32} stdcall {$ENDIF}; { Bit flags for GetVerbState function } const vsEnabled = $01; { Verb enabled if set, otherwise disabled } vsChecked = $02; { Verb checked if set, otherwise cleared } implementation end.The following methods need to be overriden when deriving our own Version Control System: GetIDString is called at initialization and should return a unique identification string. ExecuteVerb is called when the user selects a verb from a menu. GetMenuName is called to retrieve the name of the main menu item to be added to the application's menu bar. We can return a blank string to indicate no menu. GetVerb is called to retrieve the menu text for each verb. We can return a blank string to get a seperator bar. GetVerbCount is called to determine the number of available verbs. This function will of course not be called if the GetMenuName function returns a blank string (indicating no menu). GetVerbState is called to determine the state of a particular verb. The return value is a bit field of states enabled, disabled and checked. Finally, the procedure ProjectChange is called when there is any state change of the current project, i.e. when a project is destroyed or created.
ViCiouS
For our Version Control System, I've chosen the name ViCiouS, which leads to the class name TViCiouS.
We need to override every function from TIVCSClient and provide ViCiouS with its own behaviour.
Note that in order to make the ViCiouS.DLL work, we need to remember the fact that it's a DLL Expert, and when writing a DLL Expert, the most important thing to remember is to handle all exceptions from within the DLL itself.
The construction we need for that is taken from my chapter on Experts & VCS in The Revolutionary Guide to Delphi 2, published by WROX Press.
Every routine has to be embedded in a try-except block, where the except is calling a single routine HandleException that consist of the following two lines:
if Assigned(ToolServices) then ToolServices.RaiseException(ReleaseException)This code will make sure that the raised exception is released again and handled within the DLL itself, without going to the big bad outside world (that will be unable to handle the exception, because it will be out-of-context there). The complete class definition and implementation of TViCiouS is as follows:
library ViCiouS; uses ShareMem, {$IFDEF WIN32} Windows, {$ELSE} WinTypes, WinProcs, {$ENDIF} SysUtils, Dialogs, VcsIntf, ToolIntf, ExptIntf, VirtIntf; type TViCiouS = class (TIVCSCLient) public function GetIDString: string; override; function GetMenuName: string; override; function GetVerbCount: Integer; override; function GetVerb(Index: Integer): string; override; function GetVerbState(Index: Integer): Word; override; procedure ExecuteVerb(Index: Integer); override; procedure ProjectChange; override; end; procedure HandleException; begin if Assigned(ToolServices) then ToolServices.RaiseException(ReleaseException) end {HandleException}; function TViCiouS.GetIDString: string; begin try Result := 'DrBob.ViCiouS' except HandleException end end {GetIDString}; function TViCiouS.GetMenuName: string; begin try Result := ' &ViCiouS (beta) ' except HandleException end end {GetMenuName}; function TViCiouS.GetVerbCount: Integer; begin try Result := 7 except HandleException end end {GetVerbCount}; function TViCiouS.GetVerb(Index: Integer): string; begin try case index of 0: Result := '&Options...'; 1: Result := '&Archive Info...'; 2: Result := '&Get (check out)...'; 3: Result := '&Put (check in)...'; 4: Result := 'Project &Info...'; 5: Result := ''; { menu separator } 6: Result := '&About...' end except HandleException end end {GetVerb}; function TViCiouS.GetVerbState(Index: Integer): Word; begin try Result := vsEnabled except HandleException end end {GetVerbState}; procedure TViCiouS.ExecuteVerb(Index: Integer); begin try MessageDlg(GetVerb(Index), mtInformation, [mbOk], 0) except HandleException end end {ExecuteVerb}; procedure TViCiouS.ProjectChange; begin try MessageDlg('The project just changed!', mtWarning, [mbOk], 0) except HandleException end end {ProjectChange}; function InitVCS(Delphi: TIToolServices): TIVCSClient; export; begin ExptIntf.ToolServices := Delphi; Result := TViCiouS.Create end {InitVCS}; exports InitVCS name VCSManagerEntryPoint; begin end.First of all, we need to supply a unique ID string, given as result of the GetIDString function. I've chosen to return 'DrBob.ViCiouS', a hopefully unique enough string. Second, we need to define the name as which ViCiouS will manifest itself to the outside world. This will be the menu name that comes between Tools and Help, and I've decided to use ' &ViCiouS (beta) ' for now. Next, we need to specify how many menu entries will be used when the menu ViCiouS is opened. We need seven menu entries, so GetVerbCount should return 7. Apart from the number of menu entries under the ViCiouS menu, we also need to specify them each. For this, we need to override the GetVerb function, which is given an index argument to ask for the index-th name of the menu entry. If we return an empty string here (like for entry number 5) we get a menu separator. Apart from the names of the menu entries, we can also specify if they are enabled/disabled or checked.
All what's left now is to compile the source code of ViCiouS, and install it as just like a regular DLL Expert in DELPHI.INI (for Delphi 1) or the Registry (for Delphi 2.x and 3.x).
Installation
The Version Control System DLL must be placed in the [Version Control] section of the DELPHI.INI file (for Delphi 1.0x):
[Version Control] VCSManager=C:\USR\BOB\ViCiouS\ViCiouS.DLLor in the Registry (for Delphi 2.x and 3.x) by adding VersionControl to the Registry at:
KEY_CURRENT_USER\Software\Borland\Delphi\2.0\and adding a new Key named VCSManager with as value the place of the ViCiouS.DLL:
As the Delphi IDE loads, it will load the specified DLL and attempt to obtain a proc address for the DLL's initialization function, which must be exported using the VCSManagerEntryPoint constant.
The VCS client object should be returned by the VCS Manager DLL as the result of the init call.
Delphi is responsible for freeing the client object before unloading the VCS Manager DLL.
We can fire up any menu entry from ViCiouS, but we only get a message dialog with the name of the menu entry we've just chosen (remember that this is exactly what we specified in the ExecuteVerb method, so in fact this is working just fine).
Repository of ViCiouS
Of course, the first version of ViCiouS that we've installed really doesn't do much (other than returning a message-dialog telling us what menu option we've just chosen).
In order to really make it work, we need to connect it to a repository or database to hold the actual information we want to store inside it.
To this purpose, we first need to think about which information we actually want to store.
We would like to store the following items in a multi-user database:
FieldName | Description |
FileName | Name of the file that is being stored. The path is not stored, but considered to be the same as the main project file (i.e. all files belonging to one project should remain in the same project directory). |
Version | The version of the source file. A higher version denotes a newer file. |
VersionRevision | The revision of the version. A higher revision of the same version denotes a newer file. |
VersionComments | Comments that explain what changed in this version/revision compared to the previous one. |
VersionTimeStamp | The date/time that this version was checked into the repository. |
FileDate | The date/time of the file that was checked in (so we can re-set that date/time when we check the file back out again). |
FileContents | The actual contents of the file. These contents will be stored in a Blob field, using the LoadFromFile and SaveToFile methods for interfacing. |
FormDate | The date/time of the form (belonging to the unit file above) that was checked in (so we can re-set that date/time when we check the form back out again). |
FormContents | The actual contents of the form (belonging to the unit file above). These contents will also be stored in a Blob field. |
FileReadOnly | To indicate that this is not the latest version/revision, so only read-only copies of this file can be made (ViCiouS will not support branching of versions). |
Locked | To indicate whether or not this particular version/revision is locked by someone. Note that only the latest version/revision is able to be locked, all others are read-only versions. |
LockedBy | The name of the user that has the current file locked. |
LockedComments | Comments of the user that explain why this file was locked (i.e. what will be changed). |
LockedTimeStamp | The date/time when the file was locked (so we can also see how long the file has been locked). |
The fastest way to define these fields and to generate a new, empty table for ViCiouS to use is to generate your own table using a TTable component. We'll use the Paradox file format, and use the combined FileName, Version and VersionRevision fields as unique key and index. The source code to do this is as follows:
begin with TTable.Create(Self) do begin DatabaseName := DirectoryListBox1.Directory; TableName := 'ViCiouS.DB'; Active := False; TableType := ttParadox; with FieldDefs do begin Clear; Add('FileName', ftString, 17, True); Add('Version', ftInteger, 0, True); Add('VersionRevision', ftInteger, 0, True); Add('VersionComments', ftMemo, 1, False); Add('VersionTimeStamp', ftDateTime, 0, True); Add('FileDate', ftDateTime, 0, True); Add('FileContents', ftBlob, 0, True); Add('FormDate', ftDateTime, 0, True); Add('FormContents', ftBlob, 0, False); Add('FileReadOnly', ftBoolean, 0, True); { both File & Form } Add('Locked', ftBoolean, 0, True); Add('LockedBy', ftString, 12, False); Add('LockedComments', ftMemo, 1, False); Add('LockedTimeStamp', ftDateTime, 0, False) end; with IndexDefs do begin Clear; Add('', 'FileName;Version;VersionRevision', [ixPrimary, ixUnique]) end; CreateTable; Free end end;This code is actually called from the Options dialog of ViCiouS. In this dialog, we can either select an existing VICIOUS.DB table, or create a new, empty one:
Note that ViCiouS will (try to) determine your login name automatically, and that the archive database will always need to have the name VICIOUS.DB (for now).
The code to obtain the right login-name consist of a single call to DbiGetNetUserName, which is part of DbiProcs:
procedure TOptionsFrm.FormCreate(Sender: TObject); var netUserName: DbiUserName; begin if DbiGetNetUserName(netUserName) = DBIERR_NONE then Edit1.text := StrPas(netUserName) else Edit1.text := 'USER' { default } end {FormCreate};
Archive Information
After we've selected or created a VICIOUS.DB table in the options dialog, we can get information on the files that have already been checked into the archive.
For this, we need to use the Archive Info menu option.
Although you can put any file in the archive that you want, I'd recommend putting only files from one project in an archive (i.e.
make sure you have a separate archive for each project).
By setting up the archives in a shared space on the network, and having the projects on local drives, this is a way you can work with several people on the same single project.
Note that we have created this form by using the Delphi default Database Expert with just some small modifications to the final form layout:
After selecting a filename, we can use the navigator buttons to go the the previous/next version and revisions. We can decide to check a file out of the archive by clicking on the SpeedButton on the lower-left part of the form.
Archive Actions
Now that we have the archive and access to the project and archive information, it's time to be able to put files in and get files out of the archive.
When we get something from the archive, we want the file in the archive to be locked by us (so nobody else can modify it), and the file on my disk to be read-write.
When we put something (back) in the archive, we want the version on our disk to be set to read-only, and the version in the archive to be unlocked.
This way, we can always only modify those files that we've previously done a 'Get' for out of the archive.
If we want to get a file without being able to modify it, we can always get a read-only copy, of course (so at least we'll be able to compile with the latest versions even if some units are still in use by other programmers).
Using only part of the Archive form, we can create a Put (check in) form quite easily:
We always check the current open file in the archive. If we check in a unit or form, then these are combined into one entity. This is what makes ViCiouS special when compared to PVCS or MKS (other than the fact that these last two may be more complex and feature-rich, they do not understand the coupling of Delphi forms and units).
Get looks a lot like Put, as these forms are both derived from the general Archive Information form:
Project Information
Now that we've defined the internal format of our archive, it's time to define external sources of information we would like to supply to the users of ViCiouS, like the current Project information (all files in the local opened project).
The project information is as follows without using the Project Information Expert of the DRBOB.DLL:
The source code to obtain the information regarding the number and names of the units and forms is actually very easy, and can be obtained by querying ToolServices:
var i,j: Integer; begin try if Assigned(ToolServices) then with ToolServices do begin for i:=0 to {$IFDEF WIN32}Pred(GetUnitCount){$ELSE}GetUnitCount{$ENDIF} do begin Tmp := ExtractFileName(GetUnitName(i)); StringGrid1.Cells[0,i+1] := Tmp; Tmp := ChangeFileExt(Tmp,'.DFM'); for j:=0 to Pred(GetFormCount) do if ExtractFileName(GetFormName(j)) = Tmp then StringGrid1.Cells[1,i+1] := Tmp end end; except HandleException end end;Note that in Delphi 1.0 we need to go from 0 to GetUnitCount to get all unit names. However, we need to go from 0 to GetFormCount-1 to get the form names (for both Delphi 1.0 and 2.0). For Delphi 2.x/3.x we also need to go from 0 to GetUnitCount-1, so it seems that the GetUnitCount API from ToolServices in Delphi 1.0 was actually underreporting one unit...
If the DRBOB.DLL version 1.02 or higher (1.03 is recommended) is found, then the Project Information Expert is used to replace the Project Information dialog shown above. The Project Information Expert contains the same link to the ViCiouS Check-out functions, but offers two enhanced functions to expand and reduce your project. Clicking on the Expand button opens every form and unit in your project, while the Reduce button closes down every form (saves resources, quite handy when you need to run a large program from the IDE and want to close down all forms at design-time).
Closing all project files, or opening them all, is also very easy once you know how to use the ToolServices APIs.
procedure TInformationForm.ExpandBtnClick(Sender: TObject); var i: Integer; begin try if Assigned(ToolServices) then with ToolServices do begin SaveProject; for i:=2 to GetUnitCount do OpenFile(GetUnitName(i)) end except HandleException end end {ExpandBtnClick}; procedure TInformationForm.ReduceBtnClick(Sender: TObject); var i: Integer; begin try if Assigned(ToolServices) then with ToolServices do begin SaveProject; for i:=1 to GetUnitCount do CloseFile(GetUnitName(i)) end except HandleException end end {ReduceBtnClick};