Christian Ernst Rysgaard
Senior System Developer Consultant
Software Research Department, SimCorp A/S
Created June 11, 2001 (Last updated July 24, 2001)
Click to get the source files for this article
This article describes the development of a Visual Basic application VssSync, used for automatically synchronizing a directory tree with a Visual SourceSafe (VSS) project tree. The basic idea behind the application is to store all changes to directory tree in VSS - Kind of an automatic versioning file system, effectively storing all revisions of your files without your intervention. This could be easily accomplished manually by using the SourceSafe Explorer, but the trick is to automate the process so that it is done without using the VSS Explorer at all. Picture yourself a directory tree with contents that changes many times every day - Perhaps directory filled with developer tools or a web files. Instead of learning everybody to use VSS for adding, changing or removing files, wouldn't it be nice if some tool just silently saved all revisions of the files in SourceSafe on your behalf. Well - This is the article about just that tool (or at least something very close to that).
You can read the article as an introduction to the VSS object model since it touches almost every aspect of traversing and changing a VSS project tree programmatically. But to get the full benefit of the article you need a basic knowledge of SourceSafe and you should be able to use the VSS explorer without casually deleting files and projects at random or at least acknowledge that automating a revision workflow could be pretty cool.
Having said that, I will at least show the decency of directing you to three important articles: Beginners Guide, Best Practices and the inevitable article about SourceSafe Automation.
Too many words, too little code. Let us do something about that right now, by showing the basic steps for connecting to a vss database. Start out by launching VB, and set a reference to the vss component "Microsoft SourceSafe 6.0 Type Library" implemented by SSAPI.DLL located in the Win32 directory of your vss installation. To connect to your database you can do something like this:
dim oVssDb as SourceSafeTypeLib.VSSDatabase Const VSSDBA = "C:\Program Files\Microsoft Visual Studio\Common\VSS\srcsafe.ini" Set oVssDb = New SourceSafeTypeLib.VSSDatabase oVssDb.Open VSSDBA, "Admin", ""
It is that easy - with a few notes in mind:
But to put it short - It is no big deal opening a vss database. Just remember correct network rights, the full path, optional vss username and an optional password.
Once inside the database you can start working on the root project $/. Notice that projects are denoted with the prefix $/ which have the same meaning as the root directory of our harddrive. Obtain a reference to the root project like this:
Dim oRoot As VSSItem Const CROOT = "$/" Set oRoot = oVssDb.VSSItem(CROOT) Debug.Print "Got Root", oRoot.Spec
Create a new sub project in the root project:
Dim oProject As VSSItem Const CPROJECT = "My Sub Project" Set oProject = oRoot.NewSubproject(CPROJECT) Debug.Print "Created", oProject.Spec
Create a new local file to be placed under version control:
Dim hndFile As Integer Const CFILE = "c:\test.txt" hndFile = FreeFile If Dir$(CFILE) <> "" Then SetAttr CFILE, vbNormal: Kill CFILE Open CFILE For Output As #hndFile Print #hndFile, "First line in file" Close #hndFile
Now add the local file to the vss project:
Dim oItem As VSSItem Set oItem = oProject.Add(CFILE, "description of the file") Debug.Print "Added the file", oItem.Spec Debug.Print "File attribute is", GetAttr(CFILE)
Notice that after adding a file to vss the fileattribute will be set to vbReadOnly+vbArchive. This is a simple stunt to warn users from accidentally changing the contents. To actually change the file, it should be checked out to a local file first:
oItem.Checkout "comment string", CFILE, VSSFLAG_REPREPLACE Debug.Print "Checked out to", CFILE, GetAttr(CFILE)
Now the localfile is replaced with the latest version from vss and the attribute is reset to vbArchive. Do your changes to the local file and check in the new revision:
Open CFILE For Append As #hndFile Print #hndFile, "Second line in file" Close #hndFile oItem.Checkin "Added a line", CFILE debug.print "Added a line to", CFILE, GetAttr(CFILE)
Using the versions collection of the item, it is now possible to see all revisions of a file
Dim oVersion As VSSVersion
For Each oVersion In oItem.Versions
With oVersion
Debug.Print .VersionNumber, .Date, .Comment
End With
Next
Notice that the date, username and comment are stored along with each revision of the file, helping automation at a later stage.
Projects can obviously be placed within other projects to mimic a directory tree and with automation code it is easy to traverse such a vss project tree. This little piece of recursive code will dump the entire project tree of a vss database, showing every project and item within the tree and their relations:
DumpProject oVssDb.VSSItem("$/")
Sub DumpProject(oProject As VSSItem)
Dim oItem As VSSItem
Dim sDiv As String
sDiv = String$(UBound(Split(oProject.Spec, "/")), vbTab)
For Each oItem In oProject.Items
Debug.Print sDiv; oItem.Name
If oItem.Type = VSSITEM_PROJECT Then
DumpProject oItem
End If
Next
End Sub
Notice that the creation of the sDiv variable is the only kinky stuff in that code. Files and projects are both stored in an object of type VSSItem and distinguished by the contents of the Type property: VSSITEM_FILE or VSSITEM_PROJECT.
Once a file is added to the vss database it can be removed by setting the Deleted property True - and undeleted by setting the Deleted property to False. This does, however, not remove the item completely from vss, but it simply hides it. Notice that the Items collection of the VSSItem object takes an optional parameter which indicates whether deleted items should be included:
oItem.Deleted = True ' delete the item Dim oTmp As VSSItem For Each oTmp In oProject.Items(True) ' include deleted items If oTmp.Spec = oitem.Spec Then Debug.Print "Item still alive", oitem.spec Next oItem.Deleted = False ' undelete the item
To completely remove a File item you must call the Destroy method :
oitem.Destroy For Each oTmp In oProject.Items(True) If oTmp.Spec = oitem.Spec Then Debug.Print "File found" Next Set oitem = Nothing
The same is possible with Projects items:
oProject.Destroy For Each oTmp In oRoot.Items(True) If oTmp.Spec = oProject.Spec Then Debug.Print "Project found" Next Set oProject = Nothing
To synchronize a directory with a vss project you will also need to access the corresponding folders and files. This could probably be done using the built-in vb functions, but in my opinion they never do the trick. The Dir function will return all hits in a directory but cannot be used recursively and the file functions are awkward at their best. Instead I normally use FileSystemObject from the Scripting Runtime which contains all the needed functions for a file system (except naughty NTFS stuff like streams and junctions). Get access to the TEMP folder like this:
Dim oFs As Scripting.FileSystemObject Dim oFolder As Scripting.Folder_Class Dim sTemp As String Set oFs = New Scripting.FileSystemObject sTemp = oFs.GetSpecialFolder(TemporaryFolder) Set oFolder = oFs.GetFolder(sTemp) Debug.Print "Temp folder is", sTemp
With a Folder object in your hand you can access all files and subfolders - Access their information using the File/Folder objects:
Dim oFile As Scripting.File For Each oFile In oFolder.Files Debug.Print oFile.Path, oFile.Size, oFile.DateCreated Next Dim oSub As Scripting.Folder_Class For Each oSub In oFolder.SubFolders Debug.Print oSub.Name, oSub.SubFolders.Count; "files", oSub.DateCreated Next
Traversing a complete directory tree could be done like this:
TraverseFolder oFolder
Sub TraverseFolder(oFolder As Scripting.Folder_Class)
Dim sDiv As String
sDiv = String$(UBound(Split(oFolder.Path, "\")), vbTab)
Debug.Print sDiv; "<"; oFolder.Name; ">"
Dim oFile As Scripting.File
For Each oFile In oFolder.Files
Debug.Print sDiv; vbTab; oFile.Name
Next
Dim oSub As Scripting.Folder_Class
For Each oSub In oFolder.SubFolders
TraverseFolder oSub
Next
End Sub
The Scripting object can also work with TextStreams, effectively replacing the tedious open/print/close vb functions. Create a new textfile and append a textline like this:
Dim oText As Scripting.TextStream Const CFILE = "test.txt" Set oText = oFolder.CreateTextFile(CFILE, True, False) oText.WriteLine "the very first line" oText.Close
Reopen the Textstream and read the first line:
Dim sFile As String sFile = oFs.BuildPath(oFolder.Path, CFILE) Set oText = oFs.OpenTextFile(sFile, ForReading, False) Debug.Print "Opened file for reading", sFile Dim sLine As String sLine = oText.ReadLine oText.Close Debug.Print "Read line", sLine
This is actually all the automation knowledge you need to build the first version of your synchronization application. Only thing left is to handle the following issues during synchronization of a directory [DIR] and a vss project [VSS]:
Notice that it is necessary to use error handling in order safely check for existence of an item within a vss project - So instead of rendering your code unreadable it would be wise to hide that away in a separate function. The same function could even be extended to intelligently check if the item already exists but has been deleted and then undelete it if needed:
Public Function VssGetFromName(oVssProject As SourceSafeTypeLib.VSSItem, _
sVssItemName As String, oitem As SourceSafeTypeLib.VSSItem, Undelete As Boolean) As Boolean
Dim oVssTemp As SourceSafeTypeLib.VSSItem
For Each oVssTemp In oVssProject.Items(True)
If 0 = StrComp(oVssTemp.Name, sVssItemName, vbTextCompare) Then
' vssitem has the same name (with caseinsensitive compare)
If Undelete And oVssTemp.Deleted Then
' file exists but was deleted - undelete it
oVssTemp.Deleted = False
End If
' found item
Set oitem = oVssTemp
VssGetFromName = true
Exit Function
End If
Next
' item never found - return with failure
VssGetFromName = False
End Function
The function can be used like this to get vss items corresponding to files within a folder:
Dim oFs as Scripting.FileSystemObject, oVssDb as SourceSafeTypeLib.VssDatabase
Dim oFile as Scripting.File, oFolder as Scripting.Folder
Dim oVssItm As SourceSafeTypeLib.VSSItem, oVssPrj As SourceSafeTypeLib.VSSItem
:
set oVssPrj = oVssDb.VssItem(...)
set oFolder = oFs.GetFolder(...)
:
For Each oFile In oFolder.Files
' check if file item is already part of the corresponding vss project
If VssGetFromName(oVssPrj, oFile.Name, oVssItm, True) Then
debug.print "Found in vss", oVssItm.spec
Else
debug.print "Not found in vss", oVssItm.spec
End If
Next
Calling the IsDifferent method in issue#2 to check if a local file is different from the latest vss version is quite easy. But before checking the new version into vss, you'll have make sure that the file is not checked out by other users:
' has the file changed?
If oVssItm.IsDifferent(oFile.Path) Then
' check if file is already checked out
If oVssItm.IsCheckedOut <> VSSFILE_NOTCHECKEDOUT Then
For Each oCheck In oVssItm.Checkouts
sInfo = sInfo & vbCrLf & "by [" & oCheck.Username & "] " & _
"at [" & oCheck.Date & "] " & _
"on machine [" & oCheck.Machine & "]"
Next
Err.Raise 100001, , "File [" & oVssItm.Spec & "] already checked out" & sInfo
End If
:
The Checkin operation in issue 2 and 4 can actually be done easily by using the VSSFLAG_GETNO flag with the Checkout method followed by a call to CheckIn. The VSSFLAG_GETNO flag will execute the CheckOut normally, but the local file in the working folder or VSSItem.LocalSpec is not replaced. That way the local file in the new version will be available for the Checkin operation:
' Checkout File ' * VSSFLAG_GETNO = the CheckOut occurs, but the local file(s) ' in the working folder or VSSItem.LocalSpec are not replaced oVssItm.Checkout , , VSSFLAG_GETNO ' Checkin file (undo checkout if no contents have changed) ' * VSSFLAG_UPDUNCH = SourceSafe will undo checkout of unchanged files. ' * VSSFLAG_CMPFULL = Compare the full contents of the local file to the SourceSafe copy. oVssItm.Checkin , oFile.Path, VSSFLAG_UPDUNCH + VSSFLAG_CMPFULL
The strategy for synchronizing a vss project [VSS] with a folder [DIR] is shown here in pseudo code:
Use name of [DIR] to get name of corresponding [VSS]
Create [VSS] if it does not exist
For each file in [DIR]
Save name of file for later check
If file item is already part of the corresponding vss project
If file is different from the vssitem
If file is already checked out
raise error
Else
Checkin the new local File
Else
do nothing
Else
just add the file to vss
For each subfolder in [DIR]
Save foldername for later check
Call resursively with subfolder and corresponding vss project
For each item in [VSS]
If item not found among the visited ones
Delete the vss item
The rest of the program is really just error handling, comments and userinterface - Nothing fancy about it.
The application takes all needed parameters from the file ssdir.ini. To launch the program and start synchronization give the name of a valid section within the inifile. The settings for a valid section are:
[test] ; full path to the vss database including the srcsafe.ini file SourcesafeDatabase=c:\vssdb\srcsafe.ini ; Name of the VSS user used to run the synchronization VssUser=Admin ; Password for the user VssPassword=secret ; Full path to the source directory Directory=c:\MyDocuments ; Full vss path to the project VssProject=$/MyDocuments
Which just about explains itself! Add extra sections for multiple synchronizations and schedule an execution of vsssync with the given section name as commandline argument. If you leave the commandline empty a stunning dialog is shown where you can edit the sections of your inifile and follow the synchronization process.
If you synchronize with the userinterface turned on, you'll see all events displayed in the right side of the dialog. Some statistics will be shown in the same listbox when the process has finished.
Well it's pretty hard to tell, but the application is far from finished. The following bullets show a few of the missing features:
Send feedback on this article.