Tuesday 12 April 2011

Team Foundation Server 2010 - Update content of a file being checked in

Having found the developer blogosphere increasing useful as a source of information for my daily tribulations as a developer, I've decided it's time to contribute something back to the community. Hopefully someone else can stumble upon a useful nugget here that would help them on their way to solving some problem the same way googling has led me to the blogs of countless other developers sharing their bits and pieces.

Recently a requirement came up to insert/update a version number in .sql files on check-in for some Team Projects. When a clients DBA's are looking at stored procs in dev/test/live environments they've got no easy way of correlating the installed version in SQL to a version in TFS besides comparing the actual contents of the stored proc itself. An existing version comment section required manual updates by any developer making a change, mildly error prone as you can imagine. Any sort of automatic version number in the .sql file would ensure the ability to track back to TFS changesets and work items. This should be available as a TFS Check-in Policy so that it could be enabled/disabled for individual Team Projects.

Custom check-in policies happen on the client side before a check-in occurs, so reading and writing the contents of the files being checked in is not a problem. Simply implement the IPolicyDefinition and IPolicyEvaluation interfaces in your own class, and in IPolicyEvaluation.Evaluate() access the pending files. (Or you could derive from Microsoft.TeamFoundation.VersionControl.Client.PolicyBase.) This works fine for any policy where you evaluate the validity of some rule, but does not really lend itself to updating the contents. The reason being that Evaluate() is called numerous times in response to many different events in your pending check-in list. Updating a version number this way would increment it over and over again before the final check-in.

One way to get a single pass through this is to listen to the BeforeCheckinPendingChange event raised by the VersionControlServer object and update the file in the event handler. (Other approaches such as listening to check-in events on the server would negate the requirement to have it available as a check-in policy).

Here is an example implementation, leading to a solution similar to this:

projectLayout

First create a new class library project.
Add references to Microsoft.TeamFoundation.Client and Microsoft.TeamFoundation.VersionControl.Client to the project and implement the IPolicyDefinition and IPolicyEvaluation interfaces in the SqlVersioning class and mark it as [Serializable]. Add a private IPendingCheckin variable so that we can subscribe and unsubscribe to the event we're interested in.


#region IPolicyDefinition Members
public bool CanEdit
{
 get { return false; }
}

public string Description
{
 get { return "Add or update a version description in .sql files on check-in"; }
}

public bool Edit(IPolicyEditArgs policyEditArgs)
{
 return false;
}

public string InstallationInstructions
{
 get { return "\nPlease install VS.TFS.CheckinPolicies on your local machine from: $/DevTools/VS.TFS.CheckinPolicies/VS.TFS.CheckinPolicies.installer/Installer/VS.TFS.CheckinPoliciesInstaller.vsix"; }
}

public string Type
{
 get { return GetType().FullName; }
}

public string TypeDescription
{
 get { return "This policy will add or update a custom version section in any .sql files being checked in"; }
}

#endregion

IPolicyDefinition.CanEdit & Edit should just return false as there will be nothing to edit for this policy. InstallationInstructions will contain a message with a location for the client installer, more of which later.
Type should return the FullName for the class.
TypeDescription is what the administrator see on the Add Policy dialog box.
#region IPolicyEvaluation Members
...
public PolicyFailure[] Evaluate()
{
 return new PolicyFailure[0];
}

public void Initialize(IPendingCheckin pendingCheckin)
{
 if (_pendingCheckin != null)
 {
  throw new InvalidOperationException(GetType().FullName + " policy is already active");
 }

 _pendingCheckin = pendingCheckin;

 _pendingCheckin.PendingChanges.AllPendingChanges[0].VersionControlServer.BeforeCheckinPendingChange += new ProcessingChangeEventHandler(VersionControlServer_BeforeCheckinPendingChange);
}
...
#endregion


Return an empty PolicyFailure array from IPolicyEvaluation Evaluate as we'll not evaluate and fail anything (we're just hijacking the custom policy framework).
Set up the event handler for the BeforeCheckinPendingChange in the Initialize method (remember to remove it in the Dispose method or you'll get additional updates for each subsequent changeset).


Now for the actual file update add relevant code in the BeforeCheckinPendingChange event handler:

void VersionControlServer_BeforeCheckinPendingChange(object sender, ProcessingChangeEventArgs e)
{
 VersionControlServer server = sender as VersionControlServer;
 PendingChange pendingChange = e.PendingChange;

 if (Path.GetExtension(pendingChange.FileName).ToLowerInvariant().Equals(".sql"))
 {
  PolicyEnvelope[] policies = server.GetCheckinPoliciesForServerPaths(new string[] { pendingChange.ServerItem });

  foreach (var item in policies)
  {
   if (item.Policy.Type.Equals(GetType().FullName) && item.Enabled)
   {
    if (pendingChange.ChangeType == ChangeType.Add || pendingChange.ChangeType == ChangeType.Edit || pendingChange.ChangeType == ChangeType.Rename || pendingChange.ChangeType == ChangeType.Rollback)
    {
     string contents = File.ReadAllText(pendingChange.LocalItem);
     if (!string.IsNullOrEmpty(contents))
     {
      bool hasOldString = (contents.IndexOf(_versionStringStart) != -1);

      int oldVersion = 0;
      if (hasOldString)
      {
       var elements = contents.Substring(contents.IndexOf(_versionStringStart), contents.IndexOf(_versionStringEnd) - contents.IndexOf(_versionStringStart)).Split(null);
       Int32.TryParse(elements.ElementAtOrDefault(2), out oldVersion);
      }
      oldVersion++;

      StringBuilder newString = new StringBuilder(_versionStringStart);
      newString.Append(oldVersion.ToString() + "\t");
      newString.Append("CheckedInBy " + WindowsIdentity.GetCurrent().Name + "\t");
      newString.Append("CheckedIn " + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToShortTimeString() + "\t");
      newString.Append(_versionStringEnd);

      if (hasOldString)
      {
       string oldString = contents.Substring(contents.IndexOf(_versionStringStart), contents.IndexOf(_versionStringEnd) + _versionStringEnd.Length - contents.IndexOf(_versionStringStart));
       contents = contents.Replace(oldString, newString.ToString());
      }
      else
      {
       contents = newString + Environment.NewLine + contents;
      }

      File.WriteAllText(pendingChange.LocalItem, contents);
     }
    }
    break;
   }
  }
 }
}


The VersionControlServer is the event sender and the PendingChange is retrieved from the EventArgs.
First we check if the file is one we're interested in, if so get a list of the check-in policies for the containing team project. If our policy is present and enabled we can get to work. Get the contents of the .sql file into a string (pendingChange.LocalItem is the filename), check if it has a version string already, if so update it and if not create a new one. For the purpose of this example we update a version number and add the user and time.


That's it for the custom policy itself, to use it a TFS administrator would add it to a team project as any other custom check-in policy in Team Explorer->Team Project Settings->Source Control->Check-in Policy->Add.


Deployment



As custom check-in policies are client side every developer checking in against a relevant Team Project have to install the policy on their development machine (no installation is required on the TFS server). This can be done in a multitude of ways, one elegant solution if everyone has the Team Foundation Server Power Tools installed is described here in Youhanas's WebLog. If the policy is not installed on the client machine a policy error is displayed with the installation instructions.
Here is another approach without the Power Tools, packaging it as a .vsix file.


Create a VSIX Project in the solution

createInstaller

Fill in the required details in the vsixmanifest editor, such as Author, Licence, Supported VS Editions etc.

Add a reference to the VS.TFS.CheckinPolicies project so that the dll gets included in the vsix file.

Create a package definition file VS.TFS.CheckinPolicies.dll.pkdef containing:

[$RootKey$\TeamFoundation\SourceControl\Checkin Policies]
"VS.TFS.CheckinPolicies"="$PackageFolder$\VS.TFS.CheckinPolicies.dll"


This will set up the required registry entries on the client machine for the policy.
In the vsixmanifest editor add this file as Content of type VS Package.


We can now build and install the policy. To make a built version available to developers we'll create a new folder named Installer in the installer project. Add the following post-build event to copy the release version of the vsix file:

if $(ConfigurationName) == Debug goto :Debug
:Release
xcopy /y $(TargetDir)VS.TFS.CheckinPolicies.Installer.vsix $(ProjectDir)Installer
:Debug


Now this can be checked into TFS or placed somewhere convenient for developers to simply double-click and have the policy installed. The installation instructions could contain this location as well and it would show up in the pending check-in window for developers who have not installed to policy.


The solution is available here. Feel free to pick any bits you want, as with anything you find on the internet try it out yourself and make sure it works for your purposes...

3 comments:

  1. Dear Mr. Stromsoy,

    Thank you so much for your post, it's really helpful! I have downloaded your solution and it installs fine. However, it doesn't seem to do anything! I have added loggin to the code so that I can see whether or not it is being executed and I don't see any logs being generated! It almost seems like the hooks are not in place to execute void Initialize(IPendingCheckin pendingCheckin)! Do I have to do something (besides installing the extension) in order to enable it? It shows up under extensions but I don't think it is getting executed! Any help would be great!

    Thanks!

    ReplyDelete
    Replies
    1. Hi

      Have you associated it with a TFS project? Perhaps this sample could help you out: http://jeanpaulva.com/2012/03/23/tfs-2010-how-to-set-check-in-policy/

      Regards
      Vegard

      Delete
  2. Hi

    I am using TFS 2010 , while i check in files , i get all the earlier used files in different projects and the present project which i need to check in the check in source file windows, my problem is how can i restrict only the files of present project , whenever i try to check in files.


    Thanks
    Kishore

    ReplyDelete