Saturday, February 25, 2012

Migrating from MorphX version control to TFS version control - Part 1

There are quite a few good topics on the web about how to use TFS for source control in Dynamics AX but one thing I couldn't find was anyone with a way to migrate from MorphX version control to TFS version control.



Migrating to TFS has many benefits.
  • Tracking of Change sets/change lists. MorphX does not track groups of files checked in together. This information can be gleaned directly from SQL tables, but is not very easy to get at.
  • Better tools for viewing history. MorphX history can only be viewed from within the AX Dev environment and only the first 20 changes can be viewed. This is very limiting for high touch objects because you quickly hit this limit. When using TFS you can see the last 20 changes which is much more useful, but you also have full access to Visual Studio and the tf.exe command line interface.
  • Integration with bug tracking. Check-ins can be automatically checked in against bugs. Although I haven't used it, I read that AX 2012 supports checking in against TFS work items. In my current job we are using FogBugz which has a built in way to register TFS check ins that are tagged with BugzId: <ID> in the description and associate those with the bug history.
  • More ways to share diffs. Using TFS shelve sets you can easily shelve a change that someone else can review the diff of. This is one way (better than passing around XPO projects and comparing in AX) that peer reviews can be done.
  • Integration with TFS Build server.
  • Check in policies.
These are just some of the benefits of using TFS, I'm sure I will find more once we start using TFS more widely.

My initial research on migrating led me to the TFS Integration Platform. At first this seemed promising and probably would be a good way to do this, but would still require at least some basic work to get the X++ files and history in a compatible format. I quickly came to the realization that this tool was overkill for what I wanted.

MorphX version control stores the version history in a table named SysVersionControlMorphXRevisionTable. This table contains all of the information that you would want to track in version control, as well the actual version of the source file as a blob. There is a method on the table that can be used to write the file to disk.

I realized that with this table and the command line interface to TFS a quick and dirty job could be written to "recreate" the MorphX check ins in TFS. It's even possible to check in on behalf of another user and maintain that history. The only thing you can't do is set the timestamp for the check-in, but this info can be tagged in the check-in comment and this was good enough for me.

I hope you find this job useful. In Part 2 of this article I mention some of the road bumps I had along the way and what I did to get over them.

[UPDATE: The source code for this job is now hosted on GitHub as open source. Please find the latest code there and feel free to contribute improvements.]

https://github.com/JOROCONSULTING/morphx2TFS

[download file]

static void SourceControl_MorphX_to_TFS(Args _args)
{
    SysVersionControlMorphXRevisionTable    VCSMorphXRevisonTable;
    Int MorphXChangeListNumber = 0;
    SysVersionControlItemComment previousComment;
    UTCDateTime previousDate;
    UserId  previousUser;
    str authorStr = "";
    str ExportFolder = @'c:\Enlistments\AXDEV\DAX';
    str FilePath;
    str TFSFilePath;
    str FolderPath;
    System.IO.Path path;
    System.IO.FileInfo fileInfo;
    //str tfPath = @"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\tf.exe "
    //Use the following line for testing if you need to see the output of the tf.exe, pause after each command, or redirect output
    str tfPath = @"C:\Temp\tf.cmd";

    str authorStr(userId _userId)
    {
        UserInfo userInfo;
        ;

        select id, networkAlias from userInfo where userInfo.Id == _userId;
        if (userInfo.networkAlias != "Admin" &amp;&amp; userInfo.networkAlias != "")
        {
            return ' /author:' + userInfo.networkAlias;
        }

        return "";
    }

    void submitChange(UserId _userId)
    {
        //Submit the TFS Changelist
        if (!WinAPI::shellExecute(tfPath,
        ' checkin' + authorStr(_userId) +
        ' /noprompt /comment:"' + previousComment +
        ' OriginalDate:' + DateTimeUtil::toStr(previousDate),
        "","",1,true))
        {
             info(strfmt('Failed: checkin %1 /noprompt /comment:"%2 OriginalDate: %3"',authorStr(_userId), previousComment, DateTimeUtil::toStr(previousDate)));
        }
    }
    ;


    //ITEMPATH, VERSION, COMMENT, ACTION, CREATEDDATETIME, CREATEDBY
    while select * from VCSMorphXRevisonTable order by CREATEDDATETIME
    where VCSMorphXRevisonTable.CREATEDDATETIME &gt; 2011-08-02T02:52:14
    {
        //If the comment changes or if the user is different,or if the date is changed by more than 60 seconds then create a new changelist.
        if (((previousComment != VCSMorphXRevisonTable.Comment) ||
            (previousUser != VCSMorphXRevisonTable.createdBy) ||
            (DateTimeUtil::getDifference(previousDate,VCSMorphXRevisonTable.createdDateTime) &gt; 60)))
        {
            /*
            //Return after 7 changelists for testing
            if (MorphXChangeListNumber &gt; 7)
            {
                return;
            }
            */

            //Don't submit the first time
            if (MorphXChangeListNumber != 0)
            {
                submitChange(VCSMorphXRevisonTable.createdBy);
            }

            //set the previous variable to the current
            previousComment = VCSMorphXRevisonTable.Comment;
            previousUser = VCSMorphXRevisonTable.createdBy;
            previousDate = VCSMorphXRevisonTable.createdDateTime;

            //Increment the morphX changelist number
            MorphXChangeListNumber++;
        }

        FilePath = ExportFolder + VCSMorphXRevisonTable.ItemPath + '.xpo';
        FolderPath = System.IO.Path::GetDirectoryName(FilePath);

        //Create the path if it doesn't exist
        if (!WinAPI::folderExists(FilePath))
        {
            WinAPI::createDirectoryPath(FolderPath);
        }

        switch (VCSMorphXRevisonTable.Action)
        {
            case "Add":
                //Write the file to disk first, then add it to version control
                try
                {
                    //Export the version of the object into a folder for this version
                    VCSMorphXRevisonTable.writeToFile(FilePath);
                }
                catch (Exception::Error)
                {
                    info(strfmt("Error adding %1",FilePath));
                }

                if(!WinAPI::shellExecute(tfPath,VCSMorphXRevisonTable.Action + ' "' + FilePath + '" /noprompt', "", "", 1, true))
                {
                    info(strfmt("Failed %1 %2", VCSMorphXRevisonTable.Action, FilePath));
                }
                break;
            case "Edit":
                //Check out the file first, then delete the copy on disk and replace with the one from MorphX VCS
                if(!WinAPI::shellExecute(tfPath,VCSMorphXRevisonTable.Action + ' "' + FilePath + '" /noprompt', "", "", 1, true))
                {
                    info(strfmt("Failed %1 %2", VCSMorphXRevisonTable.Action, FilePath));
                }
                try
                {
                    WinAPI::deleteFile(FilePath);
                    //Export the version of the object into a folder for this version
                    VCSMorphXRevisonTable.writeToFile(FilePath);
                }
                catch (Exception::Error)
                {
                    info(strfmt("Error editing %1",FilePath));
                }
                break;
            case "Delete":
                //Delete from TFS, then delete from file system
                if(!WinAPI::shellExecute(tfPath,VCSMorphXRevisonTable.Action + ' "' + FilePath + '" /noprompt', "", "", 1, true))
                {
                    info(strfmt("Failed %1 %2", VCSMorphXRevisonTable.Action, FilePath));
                }
                try
                {
                    WinAPI::deleteFile(FilePath);
                }
                catch (Exception::Error)
                {
                    info(strfmt("Error deleting %1",FilePath));
                }
                break;
        }
    }

    //Submit the last changelist
    submitChange(VCSMorphXRevisonTable.createdBy);

    info(strfmt("%1 Changelists from MorphX processed",MorphXChangeListNumber));
}

No comments:

Post a Comment