Monday, January 13, 2014

How to get all files modified for a work item in TFS

Introduction
Just like everything that requires coding with (or against?) TFS API - this was not a small task. This took a while for me to understand and figure out, especially since how you work with the API keeps changing depending on the version of TFS. Therefore be fully aware these instructions are for TFS 2010 and can definitely be retrofitted to work with other versions.

Getting your Work Item
So you have a work item ID and all you want to know are which files you have associated with this work item - should be simple huh? Not so much. First you need to get your individual work item, but why would that be so hard? Well it wasn't straight forward either - you need to write a query to get your work item because the API for some reason or another doesn't have a stock way of doing this which is just odd. My goal was to get a work item for a particular team project with a specific work item ID.

Work Item Hierarchy
There is a hierarchy with a work item and it goes something like this:
There is your Work Item and your work item contains Links. Links can be changes, but don't necessarily have to be - plus a Link is a Link - it isn't explicitly anything but that. So don't bother looking for a "Changes" collection because it doesn't exist. You have to convert your Links to Changes by trial - which again, shocked that there is no stock method for this.

The Code
I am providing an entire class I wrote that will return a list of files changed for a work item. I took the change object and converted it to my own type called "FileItem" so I can do further manipulations in the future. You must pass in your TFS URI first thing. There are some private properties at the top of the class that are used all over the class. You literally need a combination of these objects for just about everything.

There are some additional utilities in the class below - think of them as a bonus.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.TeamFoundation;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

/// <summary>
/// Wrapper class for performing basic TFS queries
/// </summary>
public class TfsUtilities
{
 public string TfsUri { get; private set; }

 TfsTeamProjectCollection Server { get; set; }
 VersionControlServer VersionCS { get; set; }
 WorkItemStore Store { get; set; }

 /// <summary>
 /// Initialize this class to work with the specified Server (URI)
 /// </summary>
 /// <param name="tfsUri">The URI for the desired TFS server</param>
 public TfsUtilities(string tfsUri)
 {
  TfsUri = tfsUri;

  Server = GetServer();

  VersionCS = Server.GetService(typeof(VersionControlServer)) as VersionControlServer;

  Store = new WorkItemStore(Server);
 }

 /// <summary>
 /// Get a reference to the specified server
 /// </summary>
 /// <returns>TFS Server at Collection Level</returns>
 private TfsTeamProjectCollection GetServer()
 {
  return TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(TfsUri));
 }

 /// <summary>
 /// Get items recursively in TFS via a direct TFS path.
 /// </summary>
 /// <param name="tfsPath">A path in the current TFS server</param>
 /// <returns>All items found</returns>
 public ItemSet GetTeamProjectItems(string tfsPath)
 {
  ItemSet items = VersionCS.GetItems(@"$\" + tfsPath, RecursionType.Full);
  //ItemSet items = version.GetItems(@"$\ProjectName\FileName.cs", RecursionType.Full);

  foreach (Item item in items.Items)
   System.Console.WriteLine(item.ServerItem);

  return items;
 }

 /// <summary>
 /// Query for all or a specific work item in a team project
 /// </summary>
 /// <param name="teamProject">team project name</param>
 /// <param name="workItemID">Optional - work item ID</param>
 /// <returns>Work Item Collection</returns>
 public WorkItemCollection GetWorkItems(string teamProject, int workItemID = 0)
 {
  string strSQL = 
  " SELECT [System.Id], [System.WorkItemType]," +
  " [System.State], [System.AssignedTo], [System.Title] " +
  " FROM WorkItems " +
  " WHERE [System.TeamProject] = '" + teamProject +"' ";

  if (workItemID > 0)
   strSQL += " AND [System.Id] = " + workItemID;

  strSQL += " ORDER BY [System.WorkItemType], [System.Id]";

  WorkItemCollection workItems = Store.Query(strSQL);

  foreach(WorkItem item in workItems)
   Console.WriteLine(item.Id);

  return workItems;
 }

 /// <summary>
 /// Get all of the Files/Items that were modified and associated with a Work Item
 /// </summary>
 /// <param name="teamProject">Name of the Team Project</param>
 /// <param name="workItemID">The work item ID</param>
 /// <returns>List of changes</returns>
 public List<FileItem> GetAllFilesModifiedForWorkItem(string teamProject, int workItemID)
 {
  WorkItemCollection workItems = GetWorkItems(teamProject, workItemID);

  if (workItems.Count == 0)
  {
   Console.WriteLine("No Items found for Work Item ID: " + workItemID);

   return null;
  }

  WorkItem item = workItems[0];

  Console.WriteLine("Work Item {0} has {1} Links", workItemID, item.Links.Count);

  if(item.Links.Count == 0)
   return null;

  List<Changeset> lstChangesets = GetChangesets(item.Links);

  Console.WriteLine("Work Item {0} has {1} Changesets", workItemID, lstChangesets.Count);

  if (lstChangesets.Count == 0)
   return null;

  List<FileItem> lstItems = GetItemsForChangeset(lstChangesets);

  Console.WriteLine("Work Item {0} has {1} Items (changes)", workItemID, lstItems.Count);

  if (lstItems.Count == 0)
   return null;

  return lstItems;
 }

 /// <summary>
 /// Get all of the items, regardless of duplicates, changed for each provided changeset
 /// </summary>
 /// <param name="changesets"></param>
 /// <returns></returns>
 public List<FileItem> GetItemsForChangeset(List<Changeset> changesets)
 {
  List<FileItem> lst = new List<FileItem>();

  foreach (Changeset obj in changesets)
  {
   Console.WriteLine("Changeset {0} has {1} Items", obj.ChangesetId, obj.Changes.Length);

   lst.AddRange(GetItemsForChangeset(obj));
  }

  return lst;
 }

 /// <summary>
 /// Get all of the changed files/items associated with this changeset
 /// </summary>
 /// <param name="changeset"></param>
 /// <returns></returns>
 public List<FileItem> GetItemsForChangeset(Changeset changeset)
 {
  FileItem t = null;

  List<FileItem> lst = new List<FileItem>();

  foreach (Change obj in changeset.Changes)
  {
   t = new FileItem() { CheckinDate = obj.Item.CheckinDate, ChangeType = obj.ChangeType, TfsPath = obj.Item.ServerItem };

   Console.WriteLine("Item {0} : {1} - {2} - {3}", obj.Item.ItemId, t.CheckinDate, t.ChangeType, t.TfsPath);

   lst.Add(t);
  }

  return lst;
 }

 /// <summary>
 /// Get all of the Changesets from a collection of links (Links in a Work Item)
 /// </summary>
 /// <param name="workItemLinks">The links of a work item</param>
 /// <returns>All of the found changesets - can be returned empty</returns>
 public List<Changeset> GetChangesets(LinkCollection workItemLinks)
 {
  Changeset cs = null;

  List<Changeset> lst = new List<Changeset>();

  foreach (Link obj in workItemLinks)
  {
   cs = ConvertToChangeSet(obj);

   if (cs != null)
    lst.Add(cs);
  }

  return lst;
 }

 /// <summary>
 /// Convert the provided Link to a Changeset.
 /// If the conversion cannot be made, null is returned.
 /// </summary>
 /// <param name="workItemLink">Work Item Link</param>
 /// <returns>Changeset or null</returns>
 public Changeset ConvertToChangeSet(Link workItemLink)
 { 
  Changeset cs = null;

  ExternalLink externalLink = workItemLink as ExternalLink;
  
  if (externalLink != null)
  {
   ArtifactId artifact = LinkingUtilities.DecodeUri(externalLink.LinkedArtifactUri);
   
   if (String.Equals(artifact.ArtifactType, "Changeset", StringComparison.Ordinal))
   {
    // Convert the artifact URI to Changeset object.
    cs = VersionCS.ArtifactProvider.GetChangeset(new Uri(externalLink.LinkedArtifactUri));
   }
  }

  return cs;
 }

 //http://stackoverflow.com/questions/3619078/what-is-the-fastest-method-of-retrieving-tfs-team-projects-using-the-tfs-sdk-api
 /// <summary>
 /// Get all of the Team Project Names for the current TFS Server's Collection
 /// </summary>
 /// <returns>List of team project names</returns>
 public List<string> GetTeamProjectNames()
 {
  return Server.GetService<VersionControlServer>()
      .GetAllTeamProjects(false)
      .Select(x => x.Name)
      .ToList();
 }
}

//Code for the FileItem class (not entirely necessary)
using System;
using Microsoft.TeamFoundation.VersionControl.Client;

public class FileItem
{
 public DateTime CheckinDate { get; set; }

 public string ChangeTypeString 
 { 
  get { return ChangeType.ToString(); } 
 }

 public ChangeType ChangeType { get; set; }

 public string TfsPath { get; set; }
}

DLLs Referenced in the Code
You need to reference 5 dlls to make this code work. Those dlls are:
  • Microsoft.TeamFoundation.Client
  • Microsoft.TeamFoundation.Common
  • Microsoft.TeamFoundation.VersionControl.Client
  • Microsoft.TeamFoundation.VersionControl.Common
  • Microsoft.TeamFoundation.WorkItemTracking.Client
You can locate the dlls by:
  1. Going to your project, right click on the References folder
  2. Select "Add Reference..." from the context menu
  3. Click on the "Assemblies" option in the left pane
  4. Click on "Extensions" under the "Assemblies" option tree
  5. Select the aforementioned libraries (above list) and make sure to select version "11.0.0.0"
TFS DLL Gotcha 1
  • I can't find the dlls you mentioned above
  • I don't have the version of the dlls you mentioned above
  • I tried deploying this code to my web server and it won't run, it says I am missing components
I have the same answer for the above bullet points. You must have the Team Foundation Server Client installed on your system. If you are installing this on a server, the client must be installed on the server.

TFS DLL Gotcha 2
Unfortunately these dlls are x86 - so you must set your project to "Any CPU" and if you are hosting in IIS you must run your application pool in 32 bit mode (enabled 32 bit applications - set to true).

Links that helped me figure this mess out


No comments:

Post a Comment