Migrate TFS "Repro Steps" field to the description

I recently decided, for a specific team project, to use the Description field instead of the Repro Steps field in the Bug work item. The main reason is to be able to create reports for both Product Backlog Items and Bugs, while being able to display the full description.

The first step was to migrate the content of the Repro Steps field into the Description field in order to retain data for existing items.
Since it is not possible to do field-to-field bulk update from the Web interface, I had to create a small console application to copy the data.
The main problem I encountered was about inline images. When you copy and paste a screenshot into an HTML field, it creates an img tag with Base64 data in the src attribute.
For an unknown reason, when the data in the src exceeds a certain limit (I don't know the actual value), the src attribute is blanked during the save. Images simply appear blank in the target field.

After some search, I came across this post from René van Osnabrugg. I tweaked the code to reach the final solution.
Basically, the code
  1. uses a Regex to find inline images in the Repro Steps field, 
  2. extracts the binary data from the src attribute
  3. writes the images data on the local disk
  4. attaches the image to the work item
  5. replaces the inline image with a img tag linked to the tfs attachment API
  6. saves everything into the Description field
I decided to keep the attachment just in cause I'd like to extract/share easily.

Here is the complete code sample.

 var tfsCollectionUri = new Uri("tfs collection url");  
 var tfsCollection = new TfsTeamProjectCollection(tfsCollectionUri);  
    
 var workItemStore = tfsCollection.GetService<WorkItemStore>();  
 var items = workItemStore.Query("SELECT [System.Id], [System.Description] FROM WorkItems WHERE [System.TeamProject] = '@@@YourProject@@@' AND [System.WorkItemType] = 'Bug' AND [System.State] <> 'Removed'").Cast<WorkItem>();  
    
 foreach (var item in items)  
 {  
  item.Open();  
  var reproSteps = Convert.ToString(item.Fields["Microsoft.VSTS.TCM.ReproSteps"].Value);  
    
  // Get images from repro steps  
  var imageMatches = Regex.Matches(reproSteps, "<img src=\"data:image/png;base64,[^\\.]*\" alt=\"\">")  
                     .Cast<Match>()  
                     .Select(m => m.Value)  
                     .Distinct();  
  foreach (var match in imageMatches)  
  {  
   var base64data = match.Replace("<img src=\"data:image/png;base64,", "")  
   
              .Replace("\" alt=\"\">", "");  
    
   // Write image to disk  
   var imageData = Convert.FromBase64String(base64data);  
   var imageName = "image-"+ item.Id + "-" + DateTime.Now.Ticks + ".png";  
    
   File.WriteAllBytes(imageName, imageData);  
             
   // Attach file to work item  
   int attachmentIndex = item.Attachments.Add(new Attachment(imageName, "Created from repro steps inline image"));  
   item.Save();  
    
   // Get the attachment ID  
   var attachmentGuid = item.Attachments[attachmentIndex].FileGuid.ToString();  
    
   // Replace inline image with attached version  
   reproSteps = reproSteps.Replace(match, String.Format("<img src=\"{0}/WorkItemTracking/v1.0/AttachFileHandler.ashx?FileNameGuid={1}&FileName={2}\"/>", tfsCollection.Uri.ToString(), attachmentGuid, imageName));  
  }  
    
  item.Description = reproSteps;  
  var errors = item.Validate();  
  if (errors.Count == 0)  
  {  
   item.Save(SaveFlags.MergeAll);  
  }  
 }   

Popular posts from this blog

Handling exceptions the right way in WCF (part 2)

Adding a delay before processing Textbox events

Change the deployment URL of a ClickOnce application