Over a million developers have joined DZone.

Configuring a PowerShell Adapter for the TFS Integration Platform

·

We have a customer who really, really want to ship source code from Team Foundation Server (TFS) to Perforce. Why you might ask… Well for many companies they can’t just migrate, they either need some sort of safety net, or have an external requirement that they MUST use a particular system as a matter of record.

But they really want to use TFS.


We have to have some way of getting each of the check-ins out of TFS in order and writing them to the other system on a regular basis without having to write an Adapter for each new system. This will obviously not be a perfect scenario as it will not be tailored directly for the other system, but it should suffice for 90% of cases that I will encounter.

The Theory

The TFS Integration Platform Adapter I have written is really simple, and focuses on adaptability not perfection. What is the best tool for this… well PowerShell of course Smile, and with the Team Foundation Adapters already written and provided in the box, we only have to worry about the other side.

In the integration platform if you want to do the writing, rather than the reading I recently did for Test Track Pro, then you concentrate on the IMigrationProvider interface which allows you to implement the ProcessChangeGroup method.

Public Overrides Function ProcessChangeGroup(changeGroup As Microsoft.TeamFoundation.Migration.Toolkit.ChangeGroup) As Microsoft.TeamFoundation.Migration.Toolkit.ConversionResult
    Dim conversionResult As New ConversionResult(_configurationService.MigrationPeer, _configurationService.SourceId)
    conversionResult.ChangeId = changeGroup.ChangeGroupId
    Dim OutDirectory As String = Me._configurationService.MigrationSource.SourceIdentifier
    Dim SourceRoot As String = Me._configurationService.Filters(0).Path
    TraceManager.TraceInformation(String.Format("Processing {0} actions - {1}", changeGroup.Actions.Count, changeGroup.Comment))
    If Not RunPowershell("ImportChangeGroup_Initial", changeGroup, conversionResult, OutDirectory) Then
        conversionResult.ChangeId = String.Empty
    End If
    For Each action As MigrationAction In changeGroup.Actions
        Dim path As String = ConvertPath(OutDirectory, SourceRoot, action.Path)
        If BuildLocalCache(changeGroup, conversionResult, SourceRoot, OutDirectory, path, action) Then
            conversionResult.ItemConversionHistory.Add(New ItemConversionHistory(changeGroup.Name, action.Version, path, String.Empty))
        End If
    Next
    If Not RunPowershell("ImportChangeGroup_Final", changeGroup, conversionResult, OutDirectory) Then
        conversionResult.ChangeId = String.Empty
    End If
    Return conversionResult
End Function

For each change group that is found in TFS the Integration Platform calls this method. The Platform is going to keep track of which you have already done and what types of changes need to be done in what order. All we have to do is do something with them and pass back a ConversionResult object that details a ItemConversionHistory object for each of the Actions that were part of the change group.

You can see from the code above that I am calling a PowerShell script at both the beginning and end of the process. You can’t see it here, but a PowerShell is also called for each of the actions. During this process we build up a record of what we have done and pass back the completed ConversionResult to the TFS Integration Platform so it can check all of the items that we have processed off the list.

In order to add some amount of flexibility to the system I am allowing a different PowerShell to be called for each ContentType and ChangeAction combination:

Content Type Action  
Version Controlled Folder Add  
Version Controlled Folder Edit  
Version Controlled Folder Delete  
Version Controlled Folder Rename  
Version Controlled File Add  
Version Controlled File Edit  
Version Controlled File Delete  
Version Controlled File Rename  

Figure: Supported Actions

In the configuration for the Adapter I have added a bunch of keys that translate to each of the entries above that will allow you to configure which scripts you want to use. You can use them all if you want or only use one.

<CustomSettings>
  <!--
    Powershell Arguments
    $IpChangeGroup = Microsoft.TeamFoundation.Migration.Toolkit.ChangeGroup
    $IpConversionResult = Microsoft.TeamFoundation.Migration.Toolkit.ConversionResult
    $IpMigrationAction = Microsoft.TeamFoundation.Migration.Toolkit.MigrationAction
    $IpNewPath = D:\DataPerforce\localhost\1666\ImportRun1\{localpath}
    $IpLocalRoot = D:\DataPerforce\localhost\1666\ImportRun1

    For Powershell files to import for perticular clients, please refer to:

    Perforce :
  -->
  <CustomSetting SettingKey="ImportChangeGroup_Initial" SettingValue="D:\DataPerforce\localhost\PerforceInitilise.ps1" />
  <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFolder_Add" SettingValue="" />
  <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFolder_Edit" SettingValue="" />
  <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFolder_Delete" SettingValue="" />
  <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFolder_Rename" SettingValue="" />
  <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFile_Add" SettingValue="" />
  <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFile_Edit" SettingValue="" />
  <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFile_Rename" SettingValue="" />
  <CustomSetting SettingKey="ImportChangeGroup_Final" SettingValue="D:\DataPerforce\localhost\PerforceCommit.ps1" />
</CustomSettings>

Figure: Custom settings for configuring PowerShell files

As might be eluded to by the list of arguments above I am passing a bunch of them into PowerShell that should be of use to the person configuring the options. One of the really nice things about using PowerShell is that you can pass in a complicated object graph and have it available for the PowerShell user. You can also get objects back out again, but in this case I only need in.

Public Function ExecutePowershell(powerShellKey As String, changeGroup As ChangeGroup, conversionResult As ConversionResult, localRoot As String, Optional action As MigrationAction = Nothing, Optional newPath As String = "") As List(Of PSObject)
    _pipeline = _runspace.CreatePipeline()
    Dim psFile As String = GetPowerShellFile(powerShellKey)
    Dim results As New List(Of PSObject)
    If File.Exists(psFile) Then
        Dim command As New Command(psFile)
        command.Parameters.Add(New CommandParameter("IpChangeGroup", changeGroup))
        command.Parameters.Add(New CommandParameter("IpConversionResult", conversionResult))
        If Not action Is Nothing Then
            command.Parameters.Add(New CommandParameter("IpMigrationAction", action))
            command.Parameters.Add(New CommandParameter("IpNewPath", action))
        End If
        command.Parameters.Add(New CommandParameter("IpLocalRoot", localRoot))
        command.MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output)
        _pipeline.Commands.Add(command)
        Dim dt As DateTime = Now
        results.AddRange(_pipeline.Invoke())
    Else
        TraceManager.TraceWarning(String.Format("PowerShell: DOES NOT EXIST for custom setting '{0}'", powerShellKey))
    End If
    Return results
End Function

Figure: Calling PowerShell from VB.NET with Parameters

This means that you can access things like the Comment from the original change group in TFS as well as the person that did the check-in.

p4 commit -d $IpChangeGroup.Comment

Figure: PowerShell to check-in all outstanding files with the original comment

The Practice

In order to run this adapter you are going to need somewhere to install and run the TFS Integration Platform. I would suggest the the TFS server itself, but if you have a problem with that then find another server.

The TFS Integration Platform can run with a UI so we will be using that to configure it, but it can also run off the command line. If you are going to want this tool to run in more than “One-Time” mode then you are going to want to install the service. This will run the sync even if no one is logged on.

SNAGHTMLca8aa6

Figure: Install the Service if you will be syncing

Once you have it installed you will need a configuration file as a template. This file can be pretty loose, or it can be really strict in what you can select. Depending on the Adapters that you install out of the box will depend on the templates that you get. You can have Work Item Tracking only, Version Control only or a combination.

In this case we will be using my magic config file:

<?xml version="1.0" encoding="utf-16"?>
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns:xsd="http://www.w3.org/2001/XMLSchema"
               UniqueId="76E320D8-914C-4E5D-9C3A-014A5E582462"
               FriendlyName="NWC - TFS to Powershell">
  <Providers>
    <Provider ReferenceName="566C001E-E476-4A07-8447-B2284C55A20E" FriendlyName="Powershell" />
    <Provider ReferenceName="FEBC091F-82A2-449e-AED8-133E5896C47A" FriendlyName="TFS 2010 Migration VC Provider" />
  </Providers>
  <Addins>
    <Addin ReferenceName="cdde6b6b-72fc-43b6-bbd1-b8a89a788c6f"
          FriendlyName="TFS Active Directory User Id Lookup Service Addin" />
  </Addins>
  <SessionGroup CreationTime="2011-04-14T14:59:58.72-07:00"
                FriendlyName="NWC - TFS to Powershell"
                SessionGroupGUID="B5883A26-5151-4ED3-9F3F-75EFF31E5CBE"
                Creator="MrHinsh"
                SyncIntervalInSeconds="160"
                SyncDurationInMinutes="0">
    <MigrationSources>
      <MigrationSource InternalUniqueId="A3E4BC8C-B1C3-4A8E-B18A-3066FF861E33"
                       FriendlyName="nwc (vc)"
                       ServerIdentifier="346dd524-18b0-4eda-ba05-0f4d6b14e076"
                       ServerUrl="http://tfs.mycompany.com:8080/tfs/DefaultCollection"
                       SourceIdentifier="LabDemo"
                       ProviderReferenceName="FEBC091F-82A2-449e-AED8-133E5896C47A"
                       EndpointSystemName="TFS">
        <Settings>
          <Addins />
          <UserIdentityLookup>

            <LookupAddin Precedence="1" ReferenceName="cdde6b6b-72fc-43b6-bbd1-b8a89a788c6f" />

          </UserIdentityLookup>

          <DefaultUserIdProperty UserIdPropertyName="DisplayName" />
        </Settings>
      <CustomSettings>
          <CustomSetting SettingKey="EnableBypassRuleDataSubmission" SettingValue="True" />
          <CustomSetting SettingKey="DisableAreaPathAutoCreation" SettingValue="False" />
          <CustomSetting SettingKey="DisableIterationPathAutoCreation" SettingValue="False" />
        </CustomSettings>
        <StoredCredential />
      </MigrationSource>
      <MigrationSource InternalUniqueId="5E6BCC38-68F4-4A68-AB1E-8EC9C6D64C33"
                       FriendlyName="Powershell (vc)"
                       ServerIdentifier="346dd524-18b0-4eda-ba05-0f4d6b14e076"
                       ServerUrl="fieldnotused"
                       SourceIdentifier="D:\DataPerforce\localhost\1666\ImportRun1"
                       ProviderReferenceName="566C001E-E476-4A07-8447-B2284C55A20E"
                       >
        <Settings>
          <Addins />
          <UserIdentityLookup>

            <LookupAddin Precedence="1" ReferenceName="cdde6b6b-72fc-43b6-bbd1-b8a89a788c6f" />

          </UserIdentityLookup>

          <DefaultUserIdProperty UserIdPropertyName="DisplayName" />
        </Settings>
        <CustomSettings>
          <!--
            Powershell Arguments
            $IpChangeGroup = Microsoft.TeamFoundation.Migration.Toolkit.ChangeGroup
            $IpConversionResult = Microsoft.TeamFoundation.Migration.Toolkit.ConversionResult
            $IpMigrationAction = Microsoft.TeamFoundation.Migration.Toolkit.MigrationAction
            $IpNewPath = D:\DataPerforce\localhost\1666\ImportRun1\{localpath}
            $IpLocalRoot = D:\DataPerforce\localhost\1666\ImportRun1

            For Powershell files to import for perticular clients, please refer to:

            Perforce :
          -->
          <CustomSetting SettingKey="ImportChangeGroup_Initial" SettingValue="D:\DataPerforce\localhost\PerforceInitilise.ps1" />
          <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFolder_Add" SettingValue="" />
          <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFolder_Edit" SettingValue="" />
          <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFolder_Delete" SettingValue="" />
          <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFolder_Rename" SettingValue="" />
          <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFile_Add" SettingValue="" />
          <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFile_Edit" SettingValue="" />
          <CustomSetting SettingKey="ImportChangeGroupAction_VersionControlledFile_Rename" SettingValue="" />
          <CustomSetting SettingKey="ImportChangeGroup_Final" SettingValue="D:\DataPerforce\localhost\PerforceCommit.ps1" />
        </CustomSettings>
        <StoredCredential />
      </MigrationSource>
    </MigrationSources>
    <Sessions>
      <Session CreationTime="2011-04-14T14:59:58.703-07:00"
               SessionUniqueId="1C1BF9C0-AC3E-48FA-89CE-2A759F0AA8E4"
               FriendlyName="Version Control Session"
               LeftMigrationSourceUniqueId="A3E4BC8C-B1C3-4A8E-B18A-3066FF861E33"
               RightMigrationSourceUniqueId="5E6BCC38-68F4-4A68-AB1E-8EC9C6D64C33"
               SessionType="VersionControl">
        <EventSinks />
        <CustomSettings>
          <SettingXml>

          </SettingXml>
          <SettingXmlSchema />
        </CustomSettings>
        <Filters>
          <FilterPair Neglect="false">
            <FilterItem MigrationSourceUniqueId="A3E4BC8C-B1C3-4A8E-B18A-3066FF861E33" FilterString="$/TeamProject1/Folder1" />
            <FilterItem MigrationSourceUniqueId="5E6BCC38-68F4-4A68-AB1E-8EC9C6D64C33" FilterString="$/TeamProject1/Folder1" />
          </FilterPair>
        </Filters>
      </Session>
    </Sessions>
    <Linking>
      <CustomSettings />
      <LinkTypeMappings />
    </Linking>
    <!-- See http://blogs.msdn.com/b/willy-peter_schaub/archive/2010/01/13/tfs-integration-platform-workflowtypeenum-configuration-element-is-being-deprecated.aspx -->
    <!--<WorkFlowType Frequency="ContinuousManual" DirectionOfFlow="Unidirectional" SyncContext="Disabled" />-->
     <WorkFlowType Frequency="ContinuousAutomatic" DirectionOfFlow="Unidirectional" SyncContext="Disabled" />
    <CustomSettings />
    <UserIdentityMappings EnableValidation="false">
      <UserIdentityLookupAddins />
    </UserIdentityMappings>
    <ErrorManagement>
      <ErrorRouters />
      <ReportingSettings />
    </ErrorManagement>
  </SessionGroup>
</Configuration>

Figure: Configuration for the Powershell Adapter

I have highlighted the important parts above and we have already described some of the important bits above, but there are only really three important things to configure:

  • Tfs Source folder - (e.g. $/TeamProject1/Folder1)

    The TFS Source Folder defines where in TFS you want to get the data. The Adapter does not currently support branches so it would be best to pick a folder that does not contain any.

  • Local Output Folder – (e.g. c:\Enlistment\1995\Depot\)

    This is the folder where the system will write out the files and folders before calling PowerShell. This would usually be the actual Workspace folder for the other system, or could be anywhere.

  • Power Shell File – (e.g. c:\Enlistment\1995\SyncPerforceWithDepot.ps1)

    The PowerShell files are easily configured as described. Remember that for every change “Add”, “Edit” or “Delete” a respective PowerShell can be configured to be called.

Now that you are able to configure the config file, it is time to setup the run.

SNAGHTMLe3e86f

Figure: Creating a new Configuration for the TFS Integration Platform

We need to create a new configuration and select the config file template that we built. Don’t worry, we can change the individual settings later.

SNAGHTMLe5b54e

Figure: Using the Sample PowerShell Configuration

 

SNAGHTMLe83fa3

Figure: You can reconfigure the TFS side to point at a valid TFS server and Project.

 

SNAGHTMLe8d29e

Figure: Make sure that your folder does not “contain” a branch

For a very strange reason that probably spells from my laziness, you need to set both the left and the right hand side of the Paths to be the same thing. I am sure that this is just a temporary work around, but you know how they are!

image

Figure: Paths must be the same

You can now “Save to Database” the configuration and click the “Start” button on the left to start the first run on the Adapter.

SNAGHTMLed1f28

Figure: The Adapter then starts it run

There is not much more to see, the files are populated by change set in the folder that was selected and the PowerShell scripts that were configured run.

SNAGHTMLefcfb1

Figure: Some Changesets are bigger than others

Once and a while there are some really big Changesets processed. This one is actually pretty small and only had 1846 change actions.

Conclusion

Although this process can take a while, the fact that you can configure the PowerShell any way you like on the fly makes it a very versatile adapter. Although the down side is that you have to write in PowerShell Smile

If you need a copy of your TFS Version Control data somewhere other than TFS for posterity, or you need a migration from Test Track Pro to TFS, then just ping me and see how we can help.

Topics:

Published at DZone with permission of Martin Hinshelwood, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}