TFS2010: Retrieve all changesets associated with a branch (full recursion)
Asked Answered
S

3

17

This follows my previous question about TFS 2010 and the possibility to create a changelog.

I was previously using labels to identify a version of the program, but as labels are not fixed points in time, now I'm using branches.

Here's how the branch hierarchy looks like:

branch hierarchy

As you can see, there are two different applications that are branches of the trunk: APP_A (application A) and APP_B (application B). Both are almost identical, but there are some functional differences.

Here is the process to create a new version of the application (say version 1.3):

  1. The Main trunk is modified (new functionalities are added, bug fixes...)
  2. From the modified Main trunk, a new branch is created: Main trunk 1.3
  3. APP_A branch might be modified, so unique functionalities of APP_A will work with modification of v1.3
  4. APP_B branch might be modified, so unique functionalities of APP_B will work with modification of v1.3
  5. Main trunk 1.3 is merged to APP_A and APP_B, so both APP_A and APP_B applications receive the modifications of the Main trunk
  6. From the modified APP_A branch, a new branch is created: APP_A_1.3
  7. From the modified APP_B branch, a new branch is created: APP_B_1.3

My goal is to be able to produce a changelog between APP_A_1.3 and APP_A_1.2.

By changelog I mean a list of WorkItems. Each changeset that is checked-in is associated with one or more WorkItem (for instance a Bug item). I would like to be able to get the list of all workitems that were linked to a changeset that has impacted APP_A_1.3: those changesets might come from the Main trunk (step 1 above), the APP_A branch (step 3 above) or even the APP_A_1.3 branch itself (if hotfixes are checked-in after the branch has been created).

To get this list of workitems, I tried to get the list of all changesets that are "linked" to APP_A_1.2 ("linked" = the code that was checked-in in the changeset is now on the branch APP_A_1.2) and the list of all changesets that are "linked" to APP_A_1.3.

Then, I'll be able to know which changesets are "linked" to APP_A_1.3 and not "linked" to APP_A_1.2. From this subset of changesets, I'll get all associated WorkItems and thus my changelog.

Here's my problem: how could I get the list of ALL changesets that are "linked" with a specified branch? I'm using the TFS 2010 API for C# code.

The input of my program (that would retrieve all changesets for a specified branch) would be the name of the branch (say APP_A_1.2), and the output would be the list of following changesets:

  • changesets applied on APP_A_1.2 branch itself
  • changesets applied on APP_A branch before APP_A_1.2 was created
  • changesets applied on Main trunk 1.2 branch before it has been merged to APP_A
  • changesets applied on Main trunk branch before Main trunk 1.2 was created

I've wrote the following pieces of code to get all those changesets:

// Gets the list of all changesets ID from APP_A_1.2 branch
var branch1ChangeSets = myVersionControlServer.QueryHistory(
    "$/PATH/APP_A_1.2/",
    VersionSpec.Latest,
    0,
    RecursionType.Full,
    null,
    null,
    null,
    int.MaxValue,
    false,
    false).OfType<Changeset>().Select(z => z.ChangesetId).ToList();

Even if RecursionType.Full is specified, the above code only returns changesets that were checked-in on the APP_A_1.2 branch itself. This is identical to the "History" command on Source Code Explorer view in Visual Studio.

Then I tried the following piece of code:

// Gets the list of all changesets ID from APP_A_1.2 branch
var branch1MergedChangeSets = myVersionControlServer.QueryMerges(
    null,
    null,
    "$/PATH/APP_A_1.2/",
    VersionSpec.Latest,
    null,
    null,
    RecursionType.Full).Select(z => z.SourceVersion).ToList();

This returns changesets that were checked-in on APP_A_1.2 branch + those that were cheked-in on APP_A branch before APP_A_1.2 was created. Much better, but not sufficient. I can't find a way to make the recursion work with branches that are "above" APP_A (Main trunk in my case)...

Anyone has an idea?

Also, any better ideas to get the changelog between two branches are welcome... Thx.

Suspense answered 14/2, 2012 at 11:4 Comment(1)
Great question! ATM you are stuck with how to accurately retrieve the Changesets. Once you 'll get them, consider this fantastic article by B.Hodges, that 'll help you correlate a list of Changesets with a list of Work Items: blogs.msdn.com/b/buckh/archive/2012/02/01/…Signify
S
2

I finally came up with a simple solution. I'm not totally happy with it as it actually looks like a brute-force algorithm, but at least it works.

What I do is:

1) Get the list of every changeset that is applied on the very root of my TFS branches (i.e. the "parent path" of Main Trunk):

var allChangesets = vcs.QueryHistory(
    "MySourcePath",
    VersionSpec.Latest,
    0,
    RecursionType.Full,
    null,
    firstPossibleChangeset,
    VersionSpec.Latest,
    int.MaxValue,
    true,
    false).OfType<Changeset>().ToList();

2) For each retrieved changeset, I call TrackMerges to see if the changeset impacts in some way my branches. TrackMerges is able to tell me if a specified changeset is applied on the branches I specify as parameter of the function (it'll return the target changeset ID on these branches). If a changeset is applied on the destination branch (in my case APP_A_1.3) and not in the source branch (APP_A_1.2), then it means it's definitely something new on my APP_A_1.3 branch.

List<int> newChangesets = new List<int>();
foreach (var z in allChangesets.Where(y => y.ChangesetId > firstPossibleChangesetId))
{
    var zz = vcs.TrackMerges(
        new int[] { z.ChangesetId },
        new ItemIdentifier("THE TRUNK PATH"),   // The root of all branches
        new ItemIdentifier[] { new ItemIdentifier(fromBranchPath), new ItemIdentifier(toBranchPath) },
        null);

    var targetInFromBranch = zz.Where(t => t.TargetItem.Item == fromBranchPath).FirstOrDefault();
    var targetInToBranch = zz.Where(t => t.TargetItem.Item == toBranchPath).FirstOrDefault();

    if (targetInToBranch != null && targetInFromBranch == null)
    {
        // Then the changeset is only applied on the ToBranch
        newChangesets.Add(z.ChangesetId);
    }
}

3) Now it's very simple to get my changelog (the list of workitems) from the list of "new changesets":

// Now, gets associated work items!
Dictionary<int, WorkItem> dico = new Dictionary<int, WorkItem>();
foreach (int changesetId in newChangesets)
{
    foreach (WorkItem zz in vcs.GetChangeset(changesetId).WorkItems)
    {
        this.AddWorkItemToDicRecursive(wis, dico, zz);
    }
}

private void AddWorkItemToDicRecursive(WorkItemStore wis, Dictionary<int, WorkItem> dico, WorkItem workItem)
{
    if (!dico.ContainsKey(workItem.Id))
    {
        dico.Add(workItem.Id, workItem);

        foreach (WorkItemLink associatedItem in workItem.WorkItemLinks)
        {
            this.AddWorkItemToDicRecursive(wis, dico, wis.GetWorkItem(associatedItem.TargetId));
        }
    }
}

I don't think it's the best possible approach, but it works fine and remains simple. Also, I didn't have to hardcode anything (branch names/hierarchy) so it's not too bad IMO. Hope it'll help someone.

Suspense answered 2/3, 2012 at 16:50 Comment(0)
L
3

first let me first ask one question. At the top of the post you write: "My goal is to be able to produce a changelog between APP_A_1.3 and APP_A_1.2."

but then when you write what changes specifically you are looking for you list: changesets applied on APP_A_1.2 branch itself changesets applied on APP_A branch before APP_A_1.2 was created changesets applied on Main trunk 1.2 branch before it has been merged to APP_A changesets applied on Main trunk branch before Main trunk 1.2 was created

This is not a valid list because it will give you all changes that contributed to APP_A_1.3, APP_A_1.2, 1.1 and so on to the beginning of the repository.

I'm not able to test my approach right now, but this is what I would do: - QueryHistory to get all changes checked in directly into branch 1.3 - use QueryMergesExtended to follow merges into this branch. QueryMergesExtended (http://msdn.microsoft.com/en-us/library/ff736485.aspx) was added in TFS 2010 specifically to be much more performant and robust than QueryMerges and QueryMergesWithDetails, in order to support branch visualization tools - afaik you don't need to specify option FollowRenames in QueryMergesExtended because you query merges on the root of the branch - when you get list of source changes (from APP_A) you need to check each changeset to see of it contains merge changes. If so, you need to query merges on app_a for those changesets. Do so recursively until you walk whole branch hierarchy.

On the side topic, you can look later at the QueryMergeRelationships (http://msdn.microsoft.com/en-us/library/microsoft.teamfoundation.versioncontrol.client.versioncontrolserver.querymergerelationships.aspx) which gives you branch object list introduced in tfs 2010 (this is what happens when in Source Control Explorer you select folder and click Convert to branch). However if you can discover your branch in different way (hardcode them) than it's not needed.

Hope this helps!

Lamee answered 27/2, 2012 at 10:26 Comment(2)
Hi, I don't understand why the list I mentioned would give me the changesets applied on APP_A_1.3. I'm OK with APP_A_1.2 and APP_A_1.1 (this is what I expect), but I don't see how a changeset that is only applied on APP_A_1.3 would appear in one of the lists. Also, I've tested QueryMergesExtended but it doesn't recursively return all changesets (for instance, applied on APP_A_1.3, it returns changesets from APP_A but not from Main Trunk).Suspense
I can indeed use QueryMergeRelationships to discover my branches then call multiple times QueryMergesExtended, this should work. I've also found a way with TrackMerges, I'll post it as another answer soon. I upvote your answer cause it's useful, thanks.Suspense
S
2

I finally came up with a simple solution. I'm not totally happy with it as it actually looks like a brute-force algorithm, but at least it works.

What I do is:

1) Get the list of every changeset that is applied on the very root of my TFS branches (i.e. the "parent path" of Main Trunk):

var allChangesets = vcs.QueryHistory(
    "MySourcePath",
    VersionSpec.Latest,
    0,
    RecursionType.Full,
    null,
    firstPossibleChangeset,
    VersionSpec.Latest,
    int.MaxValue,
    true,
    false).OfType<Changeset>().ToList();

2) For each retrieved changeset, I call TrackMerges to see if the changeset impacts in some way my branches. TrackMerges is able to tell me if a specified changeset is applied on the branches I specify as parameter of the function (it'll return the target changeset ID on these branches). If a changeset is applied on the destination branch (in my case APP_A_1.3) and not in the source branch (APP_A_1.2), then it means it's definitely something new on my APP_A_1.3 branch.

List<int> newChangesets = new List<int>();
foreach (var z in allChangesets.Where(y => y.ChangesetId > firstPossibleChangesetId))
{
    var zz = vcs.TrackMerges(
        new int[] { z.ChangesetId },
        new ItemIdentifier("THE TRUNK PATH"),   // The root of all branches
        new ItemIdentifier[] { new ItemIdentifier(fromBranchPath), new ItemIdentifier(toBranchPath) },
        null);

    var targetInFromBranch = zz.Where(t => t.TargetItem.Item == fromBranchPath).FirstOrDefault();
    var targetInToBranch = zz.Where(t => t.TargetItem.Item == toBranchPath).FirstOrDefault();

    if (targetInToBranch != null && targetInFromBranch == null)
    {
        // Then the changeset is only applied on the ToBranch
        newChangesets.Add(z.ChangesetId);
    }
}

3) Now it's very simple to get my changelog (the list of workitems) from the list of "new changesets":

// Now, gets associated work items!
Dictionary<int, WorkItem> dico = new Dictionary<int, WorkItem>();
foreach (int changesetId in newChangesets)
{
    foreach (WorkItem zz in vcs.GetChangeset(changesetId).WorkItems)
    {
        this.AddWorkItemToDicRecursive(wis, dico, zz);
    }
}

private void AddWorkItemToDicRecursive(WorkItemStore wis, Dictionary<int, WorkItem> dico, WorkItem workItem)
{
    if (!dico.ContainsKey(workItem.Id))
    {
        dico.Add(workItem.Id, workItem);

        foreach (WorkItemLink associatedItem in workItem.WorkItemLinks)
        {
            this.AddWorkItemToDicRecursive(wis, dico, wis.GetWorkItem(associatedItem.TargetId));
        }
    }
}

I don't think it's the best possible approach, but it works fine and remains simple. Also, I didn't have to hardcode anything (branch names/hierarchy) so it's not too bad IMO. Hope it'll help someone.

Suspense answered 2/3, 2012 at 16:50 Comment(0)
F
1

Yeah, working on this problem myself too. I found a codeplex project that solves it, when you're diff-ing labels, anyhow.

See if this helps: http://tfslabeldiff.codeplex.com/SourceControl/changeset/view/7075#158224

I was pretty surprised how difficult this was to find, but the documentation for TFS is lacking at best. It seemed like it ought to be obvious!

Frederick answered 29/2, 2012 at 7:20 Comment(1)
Hi Sharon, I think what you are proposing is not traversing the branch history, in other words it will not retrieve recursively the changesets between different branches.Signify

© 2022 - 2024 — McMap. All rights reserved.