SourceSafe Directory Synchronization

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


Introduction

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.

Basic VSS programming

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

Access Folders & Files

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

Synchronization issues

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]:

  1. File in [DIR] does not exist in [VSS] - Add the file to [VSS]
  2. File in [DIR] is different from [VSS] - Checkin the new version
  3. File in [VSS] does not exist in [DIR] - Delete the file from [VSS]
  4. File in [DIR] was deleted from [VSS] - Undelete file from [VSS] and checkin new version
  5. Directory in [DIR] does not exist in [VSS] - Add project to [VSS] and call recursively
  6. Directory in [VSS] does not exist in [DIR] - Delete the project from [VSS]
  7. Directory in [DIR] was deleted from [VSS] - Undelete the project from [VSS] and call recursively

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

Synchronization Strategy

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.

Inifile Parameters

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.

What could possibly be missing?

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.