http://blogs.clariusconsulting.net/kzu

Daniel Cazzulino's Blog

Go Back to
kzu′s Latest post

PowerShell with TFS: how to perform batch-updates to WorkItems

In this post, I’ll show how you can use Powershell to perform one of the most annoying and (currently impossible) tasks in an iteration planning using TFS: batch update of all non-completed items so that they are moved to the next iteration.

The end result (which I’m using very effectively weekly :) ) will be to be able to issue the following commands:

PS C:\> $tfs = Connect-Tfs tfsPS C:\> $tfs.WorkItems.FindAll("[System.TeamProject] = 'MyProject' and [System.State] != 'Closed' and [System.IterationPath] = 'MyProject\Iteration 1'") | %{ $_.Open; $_.Fields["System.IterationPath"].Value = "MyProject\Iteration 2"; $_.Save(); write-host Updated $_.Title; } | out-null

 

and have all work items in “MyProject” from iteration 1 that haven’t been closed, moved to iteration 2.

This even works with CodePlex-hosted projects :)

So, the first thing is to get connected to TFS. For this, I’m using the Microsoft.TeamFoundation.Client.dll API. The only farily undocumented caveat is that you need to pass a UICredentialsProvider when connecting:

				using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Management.Automation;
using Microsoft.TeamFoundation.Client;

namespace Clarius.PowerShell.TeamFoundation
{
    [Cmdlet(VerbsCommunications.Connect, "Tfs")]
    publicclassConnectCmdlet : Cmdlet
    {
        privatestring server;

        [Parameter(Mandatory = true, Position=0, HelpMessageResourceId="Parameter_ServerName")]
        publicstring Server
        {
            get { return server; }
            set { server = value; }
        }

        protectedoverridevoid ProcessRecord()
        {
            TeamFoundationServer tfs = newTeamFoundationServer(server, newUICredentialsProvider());
            tfs.EnsureAuthenticated();

            WriteObject(tfs);
            base.ProcessRecord();
        }
    }
}

With that in place, and after registering your snap-in, you can issue the first command and get a full TeamFoundationServer object back. Now, that object’s API is not very script-friendly. You have to use GetService to achieve anything. In order to expose the WorkItems service in a script-friendly way, you can leverage PowerShell’s type extension mechanism:

				<?
				xml
				
				
				version
				="1.0"encoding="utf-8" ?>
<Types>
  <Type>
     <Name>Microsoft.TeamFoundation.Client.TeamFoundationServer</Name>
     <Members>
        <ScriptProperty>
          <Name>WorkItems</Name>
          <GetScriptBlock>
             [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Client") | out-null
             [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.WorkItemTracking.Client") | out-null
             [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore]$this.GetService([type]"Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore")
          </GetScriptBlock>
        </ScriptProperty>
     </Members>
  </Type>
</Types>


With that extension in place, we can now access the service just using $tfs.WorkItems. Note that I’m loading the necessary assemblies before invoking the APIs, so that the relevant types are available to PS. But of course, the really interesting stuff is getting the nice FindAll() .NET 2.0-ish member working:

PS C:\> $tfs.WorkItems.FindAll("[System.TeamProject] = 'MyProject'")

This time it’s a type extension on the WorkItemStore type:

				  <
				Type
				>
     <
				Name
				>Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore</Name>
     <Members>
        <ScriptMethod>
          <Name>FindAll</Name>
          <Script>
             $private:fields = @();
             foreach ($field in $this.FieldDefinitions)
             {
             $fields += ("[" + $field.Name + "]");
             }
             $private:all = [string]::Join(",", $fields);

             if ($args[0] -is [ScriptBlock])
             {
             $private:predicate = $args[0];
             $private:result = $this.Query("SELECT " + $all + " FROM WorkItems");
             foreach($item in $result)
             {
             if ([bool](&amp;$predicate $item))
             {
             write-output $item;
             }
             }
             }
             elseif ($args[0] -is [string])
             {
             $this.Query("SELECT " + $all + " FROM WorkItems WHERE " + $args[0]);
             }
          </Script>
        </ScriptMethod>
     </Members>
  </Type>


Note that because of a pretty annoying “bug” (will be fixed in Orcas + 1, aka Rosario), you can’t actually issue a SELECT * query using the WorkItem Query Language (WIQL) and instead you need to explicitly pass all fields you’re interested in retrieving. Because you may want to later update any of the fields, I’m just building the entire list of all fields and passing that to the service.

Note that you can either pass a predicate to be evaluated for each item, or a string with the WHERE clause, which is what I did at the beginning.

With all that in place, all that’s left is to pipe the items resulting from the query and perform any operation you like on them!

Enjoy.

Comments