Getting the Modified Files for a Team Build Build

I've had several people inquire recently about how to figure out which files have changed since the previous Team Build build...  In V1 there is no straightforward way to do this, unfortunately, though we plan to remedy that in the next version of Team Build.  For now, here is one approach - the basic idea is to do a preview get prior to doing the actual get.  There are issues with this, of course - you might be interested in the list of changesets rather than the list of files, for example; additional modifications might be made to source control between the preview and the actual get; you might want to get one of these lists without having to do an incremental get; and so forth.  To a first approximation, however, this should do the trick.

The approach I took was to impement a custom task derived from the TeamBuildTask I presented in an earlier post.  I didn't add a lot of bells and whistles here - none of the fancy Get properties are exposed (FileSpec, Version, RecursionType, etc.); only the target local filename is included for each item; etc.  If you want stuff like that, you'll have to add it yourself!  As always, I make no guarantees about the awesomeness of this sample code, etc.  In fact, I would appreciate any feedback on this one - I am in the middle of a bunch of stuff with my dev box and didn't even have a chance to test this one...  All I know for sure is that it compiles!

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

namespace CustomTasks
{
    class PreviewGet : TeamBuildTask
    {
        /// <summary>
        /// Mirrors the v1 Get task property.
        /// </summary>
        [Required]
        public String Workspace
        {
            get
            {
                return m_workspace;
            }
            set
            {
                m_workspace = value;
            }
        }

        /// <summary>
        /// Output property that contains the list of all deletes that will be performed by the Get task.
        /// </summary>
        [Output]
        public ITaskItem[] Deletes
        {
            get
            {
                return m_deletes.ToArray();
            }
        }

        /// <summary>
        /// Output property that contains the list of all gets that will be performed by the Get task.
        /// </summary>
        [Output]
        public ITaskItem[] Gets
        {
            get
            {
                return m_gets.ToArray();
            }
        }

        /// <summary>
        /// Output property that contains the list of all replaces that will be performed by the Get task.
        /// </summary>
        [Output]
        public ITaskItem[] Replaces
        {
            get
            {
                return m_replaces.ToArray();
            }
        }

        /// <summary>
        /// Lazy init property that gives access to the VersionControlServer service of the TF Server.
        /// </summary>
        protected VersionControlServer VersionControlServer
        {
            get
            {
                if (m_versionControlServer == null)
                {
                    m_versionControlServer = (VersionControlServer)Tfs.GetService(typeof(VersionControlServer));
                }
                return m_versionControlServer;
            }
        }

        /// <summary>
        /// Returns the name of the build step to be added for this task.
        /// </summary>
        /// <returns>Name of the build step to be added.</returns>
        protected override string GetBuildStepName()
        {
            return "GetPreview";
        }

        /// <summary>
        /// Returns the message of the build step to be added for this task - this is the
        /// string displayed in the Team Build GUI.
        /// </summary>
        /// <returns>Message of the build step to be added.</returns>
        protected override string GetBuildStepMessage()
        {
            return "Getting the list of files to be updated by the Get task...";
        }

        /// <summary>
        /// Execute the task logic.
        /// </summary>
        /// <returns>True. Exceptions thrown on failure.</returns>
        protected override bool ExecuteInternal()
        {
            Workspace workspace = VersionControlServer.GetWorkspace(Workspace, RepositoryConstants.AuthenticatedUser);

            try
            {
                VersionControlServer.Getting += VersionControlServer_Getting;
                workspace.Get(VersionSpec.Latest, GetOptions.Preview);
            }
            finally
            {
                VersionControlServer.Getting -= VersionControlServer_Getting;
            }

            return true;
        }

        /// <summary>
        /// Getting event handler.
        /// </summary>
        void VersionControlServer_Getting(object sender, GettingEventArgs e)
        {
            switch (e.Status)
            {
                case OperationStatus.Deleting:
                    m_deletes.Add(new TaskItem(e.TargetLocalItem));
                    break;

                case OperationStatus.Getting:
                    m_gets.Add(new TaskItem(e.TargetLocalItem));
                    break;

                case OperationStatus.Replacing:
                    m_replaces.Add(new TaskItem(e.TargetLocalItem));
                    break;
            }
        }

        private String m_workspace;
        private VersionControlServer m_versionControlServer;
        private List<ITaskItem> m_gets = new List<ITaskItem>();
        private List<ITaskItem> m_replaces = new List<ITaskItem>();
        private List<ITaskItem> m_deletes = new List<ITaskItem>();
    }
}

To use this task, make sure to run it before the actual Get task.  Something like the following should typically do the trick:

 <Task Name="BeforeGet">
    <PreviewGet TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
                BuildUri="$(BuildUri)"
                Workspace="$(WorkspaceName)">
        <Output TaskParameter="Deletes" ItemName="Deletes" />
        <Output TaskParameter="Gets" ItemName="Gets" />
        <Output TaskParameter="Replaces" ItemName="Replaces" />
    </PreviewGet>
</Task>

Note that this task should typically only be used when doing incremental gets / incremental builds (See my post here to learn how to do an incremental get without also doing an incremental build), and will only function as advertised (i.e. provide a list of the changed files since the last build) when:

  1. Your build does an incremental get, and
  2. You only use one build machine!

Additionally, if you use this task when retrieving many millions of files, you can imagine it causing your build process to run out of memory...  Use it with caution!