Search code examples
ms-accessversion-control

How do you use version control with Access development?


I'm involved with updating an Access solution. It has a good amount of VBA, a number of queries, a small amount of tables, and a few forms for data entry & report generation. It's an ideal candidate for Access.

I want to make changes to the table design, the VBA, the queries, and the forms. How can I track my changes with version control? (we use Subversion, but this goes for any flavor) I can stick the entire mdb in subversion, but that will be storing a binary file, and I won't be able to tell that I just changed one line of VBA code.

I thought about copying the VBA code to separate files, and saving those, but I could see those quickly getting out of sync with what's in the database.


Solution

  • We wrote our own script in VBScript, that uses the undocumented Application.SaveAsText() in Access to export all code, form, macro and report modules. Here it is, it should give you some pointers. (Beware: some of the messages are in german, but you can easily change that.)

    EDIT: To summarize various comments below: Our Project assumes an .adp-file. In order to get this work with .mdb/.accdb, you have to change OpenAccessProject() to OpenCurrentDatabase(). (Updated to use OpenAccessProject() if it sees a .adp extension, else use OpenCurrentDatabase().)

    decompose.vbs:

    ' Usage:
    '  CScript decompose.vbs <input file> <path>
    
    ' Converts all modules, classes, forms and macros from an Access Project file (.adp) <input file> to
    ' text and saves the results in separate files to <path>.  Requires Microsoft Access.
    '
    
    Option Explicit
    
    const acForm = 2
    const acModule = 5
    const acMacro = 4
    const acReport = 3
    
    ' BEGIN CODE
    Dim fso
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    dim sADPFilename
    If (WScript.Arguments.Count = 0) then
        MsgBox "Bitte den Dateinamen angeben!", vbExclamation, "Error"
        Wscript.Quit()
    End if
    sADPFilename = fso.GetAbsolutePathName(WScript.Arguments(0))
    
    Dim sExportpath
    If (WScript.Arguments.Count = 1) then
        sExportpath = ""
    else
        sExportpath = WScript.Arguments(1)
    End If
    
    
    exportModulesTxt sADPFilename, sExportpath
    
    If (Err <> 0) and (Err.Description <> NULL) Then
        MsgBox Err.Description, vbExclamation, "Error"
        Err.Clear
    End If
    
    Function exportModulesTxt(sADPFilename, sExportpath)
        Dim myComponent
        Dim sModuleType
        Dim sTempname
        Dim sOutstring
    
        dim myType, myName, myPath, sStubADPFilename
        myType = fso.GetExtensionName(sADPFilename)
        myName = fso.GetBaseName(sADPFilename)
        myPath = fso.GetParentFolderName(sADPFilename)
    
        If (sExportpath = "") then
            sExportpath = myPath & "\Source\"
        End If
        sStubADPFilename = sExportpath & myName & "_stub." & myType
    
        WScript.Echo "copy stub to " & sStubADPFilename & "..."
        On Error Resume Next
            fso.CreateFolder(sExportpath)
        On Error Goto 0
        fso.CopyFile sADPFilename, sStubADPFilename
    
        WScript.Echo "starting Access..."
        Dim oApplication
        Set oApplication = CreateObject("Access.Application")
        WScript.Echo "opening " & sStubADPFilename & " ..."
        If (Right(sStubADPFilename,4) = ".adp") Then
            oApplication.OpenAccessProject sStubADPFilename
        Else
            oApplication.OpenCurrentDatabase sStubADPFilename
        End If
    
        oApplication.Visible = false
    
        dim dctDelete
        Set dctDelete = CreateObject("Scripting.Dictionary")
        WScript.Echo "exporting..."
        Dim myObj
        For Each myObj In oApplication.CurrentProject.AllForms
            WScript.Echo "  " & myObj.fullname
            oApplication.SaveAsText acForm, myObj.fullname, sExportpath & "\" & myObj.fullname & ".form"
            oApplication.DoCmd.Close acForm, myObj.fullname
            dctDelete.Add "FO" & myObj.fullname, acForm
        Next
        For Each myObj In oApplication.CurrentProject.AllModules
            WScript.Echo "  " & myObj.fullname
            oApplication.SaveAsText acModule, myObj.fullname, sExportpath & "\" & myObj.fullname & ".bas"
            dctDelete.Add "MO" & myObj.fullname, acModule
        Next
        For Each myObj In oApplication.CurrentProject.AllMacros
            WScript.Echo "  " & myObj.fullname
            oApplication.SaveAsText acMacro, myObj.fullname, sExportpath & "\" & myObj.fullname & ".mac"
            dctDelete.Add "MA" & myObj.fullname, acMacro
        Next
        For Each myObj In oApplication.CurrentProject.AllReports
            WScript.Echo "  " & myObj.fullname
            oApplication.SaveAsText acReport, myObj.fullname, sExportpath & "\" & myObj.fullname & ".report"
            dctDelete.Add "RE" & myObj.fullname, acReport
        Next
    
        WScript.Echo "deleting..."
        dim sObjectname
        For Each sObjectname In dctDelete
            WScript.Echo "  " & Mid(sObjectname, 3)
            oApplication.DoCmd.DeleteObject dctDelete(sObjectname), Mid(sObjectname, 3)
        Next
    
        oApplication.CloseCurrentDatabase
        oApplication.CompactRepair sStubADPFilename, sStubADPFilename & "_"
        oApplication.Quit
    
        fso.CopyFile sStubADPFilename & "_", sStubADPFilename
        fso.DeleteFile sStubADPFilename & "_"
    
    
    End Function
    
    Public Function getErr()
        Dim strError
        strError = vbCrLf & "----------------------------------------------------------------------------------------------------------------------------------------" & vbCrLf & _
                   "From " & Err.source & ":" & vbCrLf & _
                   "    Description: " & Err.Description & vbCrLf & _
                   "    Code: " & Err.Number & vbCrLf
        getErr = strError
    End Function
    

    If you need a clickable Command, instead of using the command line, create a file named "decompose.cmd" with

    cscript decompose.vbs youraccessapplication.adp
    

    By default, all exported files go into a "Scripts" subfolder of your Access-application. The .adp/mdb file is also copied to this location (with a "stub" suffix) and stripped of all the exported modules, making it really small.

    You MUST checkin this stub with the source-files, because most access settings and custom menu-bars cannot be exported any other way. Just be sure to commit changes to this file only, if you really changed some setting or menu.

    Note: If you have any Autoexec-Makros defined in your Application, you may have to hold the Shift-key when you invoke the decompose to prevent it from executing and interfering with the export!

    Of course, there is also the reverse script, to build the Application from the "Source"-Directory:

    compose.vbs:

    ' Usage:
    '  WScript compose.vbs <file> <path>
    
    ' Converts all modules, classes, forms and macros in a directory created by "decompose.vbs"
    ' and composes then into an Access Project file (.adp). This overwrites any existing Modules with the
    ' same names without warning!!!
    ' Requires Microsoft Access.
    
    Option Explicit
    
    const acForm = 2
    const acModule = 5
    const acMacro = 4
    const acReport = 3
    
    Const acCmdCompileAndSaveAllModules = &H7E
    
    ' BEGIN CODE
    Dim fso
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    dim sADPFilename
    If (WScript.Arguments.Count = 0) then
        MsgBox "Please enter the file name!", vbExclamation, "Error"
        Wscript.Quit()
    End if
    sADPFilename = fso.GetAbsolutePathName(WScript.Arguments(0))
    
    Dim sPath
    If (WScript.Arguments.Count = 1) then
        sPath = ""
    else
        sPath = WScript.Arguments(1)
    End If
    
    
    importModulesTxt sADPFilename, sPath
    
    If (Err <> 0) and (Err.Description <> NULL) Then
        MsgBox Err.Description, vbExclamation, "Error"
        Err.Clear
    End If
    
    Function importModulesTxt(sADPFilename, sImportpath)
        Dim myComponent
        Dim sModuleType
        Dim sTempname
        Dim sOutstring
    
        ' Build file and pathnames
        dim myType, myName, myPath, sStubADPFilename
        myType = fso.GetExtensionName(sADPFilename)
        myName = fso.GetBaseName(sADPFilename)
        myPath = fso.GetParentFolderName(sADPFilename)
    
        ' if no path was given as argument, use a relative directory
        If (sImportpath = "") then
            sImportpath = myPath & "\Source\"
        End If
        sStubADPFilename = sImportpath & myName & "_stub." & myType
    
        ' check for existing file and ask to overwrite with the stub
        if (fso.FileExists(sADPFilename)) Then
            WScript.StdOut.Write sADPFilename & " exists. Overwrite? (y/n) "
            dim sInput
            sInput = WScript.StdIn.Read(1)
            if (sInput <> "y") Then
                WScript.Quit
            end if
    
            fso.CopyFile sADPFilename, sADPFilename & ".bak"
        end if
    
        fso.CopyFile sStubADPFilename, sADPFilename
    
        ' launch MSAccess
        WScript.Echo "starting Access..."
        Dim oApplication
        Set oApplication = CreateObject("Access.Application")
        WScript.Echo "opening " & sADPFilename & " ..."
        If (Right(sStubADPFilename,4) = ".adp") Then
            oApplication.OpenAccessProject sADPFilename
        Else
            oApplication.OpenCurrentDatabase sADPFilename
        End If
        oApplication.Visible = false
    
        Dim folder
        Set folder = fso.GetFolder(sImportpath)
    
        ' load each file from the import path into the stub
        Dim myFile, objectname, objecttype
        for each myFile in folder.Files
            objecttype = fso.GetExtensionName(myFile.Name)
            objectname = fso.GetBaseName(myFile.Name)
            WScript.Echo "  " & objectname & " (" & objecttype & ")"
    
            if (objecttype = "form") then
                oApplication.LoadFromText acForm, objectname, myFile.Path
            elseif (objecttype = "bas") then
                oApplication.LoadFromText acModule, objectname, myFile.Path
            elseif (objecttype = "mac") then
                oApplication.LoadFromText acMacro, objectname, myFile.Path
            elseif (objecttype = "report") then
                oApplication.LoadFromText acReport, objectname, myFile.Path
            end if
    
        next
    
        oApplication.RunCommand acCmdCompileAndSaveAllModules
        oApplication.Quit
    End Function
    
    Public Function getErr()
        Dim strError
        strError = vbCrLf & "----------------------------------------------------------------------------------------------------------------------------------------" & vbCrLf & _
                   "From " & Err.source & ":" & vbCrLf & _
                   "    Description: " & Err.Description & vbCrLf & _
                   "    Code: " & Err.Number & vbCrLf
        getErr = strError
    End Function
    

    Again, this goes with a companion "compose.cmd" containing:

    cscript compose.vbs youraccessapplication.adp
    

    It asks you to confirm overwriting your current application and first creates a backup, if you do. It then collects all source-files in the Source-Directory and re-inserts them into the stub.

    Have Fun!