11
votes

I'm looking for a way to automatically produce a changelog (actually a list of workitems) between two releases of my application. I have two versions of my application, v1 and v2, each is identified by a label in TFS 2010 (LABEL1 and LABEL2) that I manually created before building the setups of my app. I have a branching system, which means I have a trunk were most of bugs are fixed, and a branch where patches are applied mostly using merges from the trunk (but there are also some fixes on the branch only that do not concern the trunk). The two versions of my application (v1 and v2) are versions from the branch.

I would like TFS 2010 to be able to return the list of bugs that were fixed (ie. the list of work items with type = Bug that are closed and verified) between these two labels.

I tried to achieve this using the web UI of TFS 2010, or using Visual Studio, but I didn't find any way.

Then I tried to ask tf.exe for a history using the following command line:

tf history /server:http://server_url/collection_name "$/project_path" /version:LLABEL1~LLABEL2 /recursive /noprompt /format:brief

where LABEL1 is the label that has been associated with the source code of the v1 of the application, and LABEL2 the label that has been associated with the source code of the v2 of the application. It actually fails in two ways: - the command line only returns a list of changesets, not a list of associated closed work items - the list of changesets only contains the changesets that I applied on the branch itself, not the changesets that I also applied and the trunk and then merged to the branch. Setting or not the "/slotmode" parameter doesn't change anything.

There I tried to write a piece of C# code to retrieve the list of workitems (not the list of changesets):

var tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri("http://server_url/collection_name"));

VersionControlServer controlServer = tfs.GetService<VersionControlServer>();
VersionControlServer vcs = tfs.GetService<VersionControlServer>();

VersionSpec sFrom = VersionSpec.ParseSingleSpec("LLABEL1", null);
VersionSpec sTo = VersionSpec.ParseSingleSpec("LLABEL2", null);

var changesets = vcs.QueryHistory(
    "$/project_path",
    sTo,
    0,
    RecursionType.Full,
    null,
    sFrom,
    sTo,
    int.MaxValue,
    true,
    false); // Slotmode to false

Dictionary<int, WorkItem> dico = new Dictionary<int, WorkItem>();
foreach (Changeset set in changesets)
{
    foreach (WorkItem zz in set.WorkItems)
    {
        if (!dico.ContainsKey(zz.Id))
        {
            dico.Add(zz.Id, zz);
        }
    }
}

foreach (KeyValuePair<int, WorkItem> pair in dico.OrderBy(z => z.Key))
{
    Console.WriteLine(string.Format("ID: {0}, Title: {1}", pair.Key, pair.Value.Title));
}

This actually works, I get the list of workitems between my two labels which is actually what I wanted. But only workitems associated to changesets that were committed on the branch itself are taken into account: the workitems of type "Bug" that were solved on the trunk then merged to the branch don't appear. Slotmode doesn't change anything.

Then I finally tried to replace VersionSpecs that were defined by a label with VersionSpecs that are defined by changesets:

VersionSpec sFrom = VersionSpec.ParseSingleSpec("C5083", null);
VersionSpec sTo = VersionSpec.ParseSingleSpec("C5276", null);

And my code finally works.

So my question is: how could I get the same result with labels, which are the TFS objects I use to identify a version? If it's not possible, how should I identify a version in TFS 2010? Thx.

Btw I found some questions on stackoverflow, but none of them gave me answers with labels. For instance: Question example

4

4 Answers

2
votes

I think http://tfschangelog.codeplex.com/ can possibly help you here.

TFS ChangeLog applicatoin allows users to automatically generate release notes from TFS. Users will have to provide information on thier project, branch and changeset range and then TFS ChangeLog application will extract information from each changeset in a given range and all the associated workitems to such changesets. i.e. it will travel from starting changeset upto ending changeset and will extract data about each changeset along with associated workitems in an XML file.

Users can then use their own transformation logic including filter, sorting, styling, output formatting, etc. to generate Release Notes Report.

Another thing I would like to add here will be related to Labels in TFS. Labels are basically assigned / associated with changesets. Currently, TFS ChangeLog application does not support Labels to define starting and ending point but it does support changeset which can be used as a workaround solution.

Hope this is useful.

1
votes

In general, the absolute method of defining points in time in any SCM is clearly the checkin-id.
Using labels to abstract this, is in TFS not the optimum as discussed here & here. A better approach is to use builds instead, especially in a modern CI environment.

In order to retrieve the max changeset that was contained in a given build you 'd have to do something like this:

using System;
using System.Collections.Generic;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Client;

namespace GetChangesetsFromBuild
{
    class Program
    {
        static void Main()
        {
            TfsTeamProjectCollection tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri("http://TFSServer:8080/Name"));
            IBuildServer bs = (IBuildServer)tpc.GetService(typeof(IBuildServer));

            IBuildDetail build = bs.GetAllBuildDetails(new Uri("vstfs:///..."));

            List<IChangesetSummary> associatedChangesets = InformationNodeConverters.GetAssociatedChangesets(build);

            int idMax = associatedChangesets[0].ChangesetId; 
        }
    }
}


A difficulty with the above is to retrieve the BuildUri of the builds you are interested in. In order to get this information you could do something like this:

IBuildDetail[] builds = bs.QueryBuilds("TeamPorjectName", "yourBuildDefinitionName")

and then retrieve the Uri's that are important to you.

This is also a good vehicle if you eventually insist on using labels: Besides Uri, each build[] has also a LabelName.

1
votes

I have been in the same situation as you. I also want Work Items from merged changesets included. I only include Work Items that are Done. Also if the same Work Item is linked to multiple changesets, only the last changeset is reported. I use this in a CI setup; and create a changelog for each build. The List<ChangeInfo> can then be exported to a XML/HTML/TXT-file. Here is my solution:

namespace TFSChangelog
{
  public class TFSChangelogGenerator
  {
    private const string workItemDoneText = "Done";

    /// <summary>
    /// This class describes a change by:
    /// Changeset details
    /// and
    /// WorkItem details
    /// </summary>
    public class ChangeInfo
    {
      #region Changeset details

      public DateTime ChangesetCreationDate { get; set; }
      public int ChangesetId { get; set; }

      #endregion

      #region WorkItem details

      public string WorkItemTitle { get; set; }
      public int WorkItemId { get; set; }

      #endregion
    }

    public static List<ChangeInfo> GetChangeinfo(string tfsServer, string serverPath, string from, string to)
    {
      // Connect to server
      var tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(tfsServer));
      tfs.Connect(ConnectOptions.None);
      var vcs = tfs.GetService<VersionControlServer>();

      // Create versionspec's
      VersionSpec versionFrom = null;
      if (!string.IsNullOrEmpty(from))
        versionFrom = VersionSpec.ParseSingleSpec(from, null);
      VersionSpec versionTo = VersionSpec.Latest;
      if (!string.IsNullOrEmpty(to))
        versionTo = VersionSpec.ParseSingleSpec(to, null);

      // Internally used dictionary
      var changes = new Dictionary<int, ChangeInfo>();

      // Find Changesets that are checked into the branch
      var directChangesets = vcs.QueryHistory(
        serverPath,
        VersionSpec.Latest,
        0,
        RecursionType.Full,
        null,
        versionFrom,
        versionTo,
        Int32.MaxValue,
        true,
        false
        ).Cast<Changeset>();
      foreach (var changeset in directChangesets)
      {
        foreach (var workItem in changeset.WorkItems.Where(workItem => workItem.State == workItemDoneText))
        {
          if (changes.ContainsKey(workItem.Id))
          {
            if (changeset.ChangesetId < changes[workItem.Id].ChangesetId) continue;
          }
          changes[workItem.Id] = new ChangeInfo { ChangesetId = changeset.ChangesetId, ChangesetCreationDate = changeset.CreationDate, WorkItemId = workItem.Id, WorkItemTitle = workItem.Title };
        }
      }

      // Find Changesets that are merged into the branch
      var items = vcs.GetItems(serverPath, RecursionType.Full);
      foreach (var item in items.Items)
      {
        var changesetMergeDetails = vcs.QueryMergesWithDetails(
          null,
          null,
          0,
          item.ServerItem,
          VersionSpec.Latest,
          0,
          versionFrom,
          versionTo,
          RecursionType.Full
        );
        foreach (var merge in changesetMergeDetails.Changesets)
        {
          foreach (var workItem in merge.WorkItems.Where(workItem => workItem.State == workItemDoneText))
          {
            if (changes.ContainsKey(workItem.Id))
            {
              if (merge.ChangesetId < changes[workItem.Id].ChangesetId) continue;
            }
            changes[workItem.Id] = new ChangeInfo { ChangesetId = merge.ChangesetId, ChangesetCreationDate = merge.CreationDate, WorkItemId = workItem.Id, WorkItemTitle = workItem.Title };
          }
        }
      }

      // Return a list sorted by ChangesetId      
      return (from entry in changes orderby entry.Value.ChangesetId descending select entry.Value).ToList();
    }
  }
}
0
votes

This question got me closer to solving a similar problem I was having.

Use the type LabelVersionSpec instead of VersionSpec for label versions.

Replace:

VersionSpec sFrom = VersionSpec.ParseSingleSpec("LLABEL1", null);
VersionSpec sTo = VersionSpec.ParseSingleSpec("LLABEL2", null);

with:

LabelVersionSpec sFrom = new LabelVersionSpec("LLABEL1");
LabelVersionSpec sTo = new LabelVersionSpec("LLABEL2");