Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom MSBuild Task to Undo Checked-out Binaries in TFS

0.00/5 (No votes)
19 Feb 2013 1  
Using the TFS API to undo checkouts for build processes

Introduction

This article provides a method for enabling a Team System build process to undo checked-out files in TFS. It requires the Build Service account to have the "unlock other users' changes" permission. If you're not cool with that, nothing to see here.

Background 

We have a very large code base with many shared binary assemblies, and we love our continuous integration. There are a number of solutions in this code base that rely on the shared binaries being built in a certain sequence. Moreover, when the solutions that produce these shared binaries are run, the build process needs to check those binaries into TFS so that the latest binaries are available to subsequent builds. This allows our nightly builds to have all the latest and greatest binaries, and produces a solid build for our QA team to start out their day. Of course, that's simple enough, until someone checks out one or more of these binary files, and the build process fails to check in those files because of it. That means the build of the system won't have the latest changes!  Therefore, the need to undo another user's checkout by the Build Service account has given rise to a custom MSBuild task.

This article will focus on the undo checkout. I'll follow with another article on the process of checking out from TFS, copying the output from the build, and checking the new files in.

Using the code

Creating a custom task is a relatively simple matter of creating a C# class, and adding a few references:

Likewise, add the appropriate using statements:

using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.IO; 

The objective of the custom task is to extend the Task object from MSBuild. Therefore, the Execute method is overridden.  

namespace Epsilon.BuildTask
{
    /// <summary>
    /// UndoCheckoutTask - a task for undoing binary files that have not been checked in.
    /// </summary>
    public class UndoCheckoutTask : Task
    {
        public override bool Execute()
        {
            try
            {

            }
            catch (Exception ex)
            {
                Log.LogError("Exception caught: " + ex.Message.ToString() + ex.StackTrace.ToString());
                return false;
            }
        }
    }
}

One of the arguments that will be passed in from the calling MSBuild .proj file is the server path location of the binaries to be checked in (for example, $/Project/Folder/MyBinaries). We've already pre-determined that there's a changeset pending for the binaries in this path, so it's safe to undo the check-out. We also need to provide the TFS server's URI. That, too is passed in from the calling project.  

/// <summary>
/// Input parameter - the server path of the binaries being checked.
/// </summary>
private string _binaryPath;
public string binaryPath
{
    set { _binaryPath = value; }
}
/// <summary>
/// Input parameter - The TFS URI
/// </summary>
/// 
private string _collectionURI;
public string collectionURI
{
    set { _collectionURI = value; }
}
/// <summary>
/// return the CheckinID to the caller
/// </summary> 

We can return a checkin ID back from the process, if desired. Notice the [Output] decoration.

/// <summary>
/// return the CheckinID to the caller
/// </summary>
private int _checkinID;
[Output]
public int CheckinID
{
    get { return _checkinID; }
}

Now to the meat of the matter. The idea here is to find any Pending Sets for the server path we passed in, and then to iterate through the Pending Changes for those Pending Sets. Yes, there might be more than one Pending Set, because more than one user could conceivably have binaries checked out. Once the Pending Changes are found, the workspace.Undo can be invoked for the itemSpecs that we've derived from the binary path. Here's the good stuff:

try
{
    _checkinID = 0;
    // Set up the scenario - get the server info, get the collection, connect to version control.
    Uri serverUri = new Uri(_collectionURI);
    TfsTeamProjectCollection collection = new TfsTeamProjectCollection(serverUri);
    VersionControlServer versionControl = (VersionControlServer)collection.GetService(typeof(VersionControlServer));
    // Construct the ItemSpec of the folder we want to check - passed in as _binaryPath.  
    // RecursionType.Full is REQUIRED for this to work!
    ItemSpec[] itemSpecs = new ItemSpec[1];
    itemSpecs[0] = new ItemSpec(@_binaryPath, RecursionType.Full);
    // Get the list of Pending SETS for the ItemSpec - just the folder we're interested in...
    // Workspacename and username arguments are set null to return all.
    PendingSet[] pendingSet = versionControl.QueryPendingSets(itemSpecs, null, null, includeDownloadInfo: true);
    Log.LogMessage("Number of pending sets: " + pendingSet.Length.ToString());
    // Iterate through the pendingSets (only one)
    foreach (PendingSet ps in pendingSet)
    {
        // Output to the log - the pending set's information
        Log.LogMessage("====PS====>" + ps.Computer + "\t" + ps.Name + 
          "\t" + ps.OwnerName + "\t" + ps.PendingChanges + "\t" + ps.Type);
        // More output to the log: The actual pending changes that will be checked in.
        PendingChange[] pendingChanges = ps.PendingChanges;
        foreach (PendingChange pc in pendingChanges)
        {
            Log.LogMessage("====PC====>" + pc.ChangeTypeName + "\t" + 
              pc.FileName + "\t" + pc.ServerItem + "\t" + pc.Version);
        }
            // Use the Pending Set info to query the workspaces desired.
            Workspace[] wsList = versionControl.QueryWorkspaces(ps.Name, ps.OwnerName, null);
            if (wsList.Length != 0)
            {
                Workspace ws = wsList[0];
                // Undo the pending changes!
                _checkinID = ws.Undo(itemSpecs);
                Log.LogMessage("Check-in ID = " + _checkinID.ToString());
            }
        }
        return true;
    }
    catch (Exception ex)
    {
        Log.LogError("Exception caught: " + ex.Message.ToString() + ex.StackTrace.ToString());
        return false;
    }
} 

The very important part of all this is to make sure that the ws.Undo(itemSpecs) is correctly specified. That's what limits the undo check-in process to the binary path specified.  

You've got options within all this to do useful things like sending email messages, or returning certain information back to the calling project file. The sky's the limit.  

How to Use the Task 

In order to consume this task from your MSBuild .proj file, you'll need to make the .dll from this project available to the build server. We use the MSBuild Extensions already, so we just placed the .dll into the same directory as the MSBuild project. Here's the UndoCheckOut.proj file:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTargets="UndoCheckOut">
  <UsingTask AssemblyFile="$(ExtensionTasksPath)Epsilon.BuildTask.dll" TaskName="Epsilon.BuildTask.UndoCheckoutTask"/>
  <PropertyGroup>
    <TPath>$(MSBuildExtensionsPath)\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks</TPath>
    <TPath Condition="Exists('$(MSBuildExtensionsPath)\ExtensionPack\4.0\
      MSBuild.ExtensionPack.tasks')">$(MSBuildExtensionsPath)\
      ExtensionPack\4.0\MSBuild.ExtensionPack.tasks</TPath>
  </PropertyGroup>
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
  <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
  <Import Project="$(TPath)"/>
  <Target Name="UndoCheckOut">
    <!-- Call a custom task to perform the checkin undo -->
    <Epsilon.BuildTask.UndoCheckoutTask collectionURI="$(MyURI)" binaryPath="$(MyBinaryPath)">
      <Output TaskParameter="CheckinID" PropertyName="ID"/>
    </Epsilon.BuildTask.UndoCheckoutTask>
    <Message Text="CheckinID = $(ID)" />
  </Target>
</Project>

The <UsingTask directive provides the path to the .dll, and the invocation of the task can be found within the <Target>. Note how the output parameter is consumed and used.  

Points of Interest 

As mentioned at the beginning of this article, the Build Service account needs to be able to undo other account's changes. Since these binaries are in folders by themselves, it's not a risk to grant that permission to the Build Service account. If you don't grant that permission, this will still run, but the checkouts obviously won't be undone.

Check the inline comments in the code above for little hints, too. There are some important tidbits in there.

History

  • Original: 4 Feb 2013.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here