the blog
Back to -Blog

Using the Team Server changeset as a software revision number

The numbers game
by Dermot Hogan
Sunday 6 May 2012.

There’s one thing that’s bugged me almost from the time I started using Microsoft’s Team Server source control system – how to get some sort of build or revision number incorporated automatically into the software. Strangely, there isn’t a simple way of doing this but there are several techniques described that you can find if you search for the subject. However, all the ones I could find referred to getting the build number when you are building on the server as part of an automated build.

I don’t use automated builds. Instead, I check the code out from the server and then build it on my own workstation. Also, the key piece of information is, for me, the ‘changeset’ number - not the build number. That’s what I use to identify a particular revision of the software when it is released.

To do this, I needed to do three things: first, write an MSBuild task that generated a small file with the changeset number in it, secondly, to invoke this task in order to create the changeset file when building and, thirdly, to remove the changeset file from source control.

The MSBuild task looks like this:

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

namespace Sapphire.Steel {

public class BuildChangesetTask : Task {

private TaskLoggingHelper _tlh;


// the server URL
private string _server;
[Required]
public string Server {set { _server = value; }}

// the file containing the changeset number
private string _fileName;
[Required]
public string FileName {set { _fileName = value; }}

public BuildChangesetTask() {_tlh = this.Log;}

public override bool Execute() {
 TfsTeamProjectCollection tpc;
 VersionControlServer vcs;
 int changesetNumber;
 StreamWriter sw;
 StreamReader sr;
 bool result;
 StringBuilder sb;
 string oldText;
 string newText;

 try {
   sb = new StringBuilder(1000);
   _tlh.LogMessage(MessageImportance.Normal, "Connecting to '{0}'",_server);
   tpc = new TfsTeamProjectCollection(new Uri(_server));
   vcs = tpc.GetService<VersionControlServer>();
   changesetNumber = vcs.GetLatestChangesetId();
   if (File.Exists(_fileName)) {
     sr = new StreamReader(_fileName);
     oldText = sr.ReadToEnd();
     sr.Close();
     _tlh.LogMessage(MessageImportance.Normal, "Reading '{0}'", _fileName);
   } else {
     oldText = String.Empty;
   }
   sb.AppendLine("namespace Sapphire.Steel.Project {");
   sb.AppendLine("\tpublic partial class ProjectPackage {");
   sb.AppendFormat("\t\tpublic const int CHANGESET = {0};\r\n", changesetNumber);
   sb.AppendLine("\t}");
   sb.AppendLine("}");
   newText = sb.ToString();
   if (oldText != newText) {
     _tlh.LogMessage(MessageImportance.Normal, "Updating '{0}' to changeset {1}", _fileName, changesetNumber);
     sw = new StreamWriter(_fileName);
     sw.Write(newText);
     sw.Close();
   } else {
     _tlh.LogMessage(MessageImportance.Normal, "Changeset {0} has not been altered", changesetNumber);
   }
 } catch (Exception ex) {
   _tlh.LogMessage(MessageImportance.Normal, "Failed: changeset number has not been updated ('{0}')", ex.Message);
 }
 return true;
}

}
}

The key bit in the ‘Execute’ part of the task is this

tpc = new TfsTeamProjectCollection(new Uri(_server));
vcs = tpc.GetService<VersionControlServer>();
changesetNumber = vcs.GetLatestChangesetId();

which gets the changeset number from the server specified. This is then written it into the changeset file if it is different from the one that is already there.

The next part is to incorporate this MSBuild into the build process. So in my main build file I inserted a line immediately after the ‘Project’ line:

<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 <UsingTask TaskName="Sapphire.Steel.BuildChangesetTask" AssemblyFile="..\BuildChangesetTask\bin\Debug\BuildChangesetTask.dll" />

and I invoke it by using the BeforeBuild target:

<!-- targets -->
 <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.Targets" />
 <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\VSSDK\Microsoft.VsSDK.targets" />
 <Target Name="BeforeBuild">
   <BuildChangesetTask Server="http://server1:8080/tfs/steel" FileName="Changeset.cs" />
 </Target>

The final step is to exclude the changeset file - just use ’Exclude from Source Control menu.

Bookmark and Share   Keywords:  Visual Studio
© SapphireSteel Software 2014