Discovering TFS merge history using the client API - Part 1

Previously I talked about how to find the contributor to a merge (e.g. the source item of a merge operation).  This information is all on the server however (in my opinion) it is not presented to the user in the most obvious form.  In this post I’ll walk through the relevant portions of a simple tool I wrote to help find merge contributors.

 

Disclaimer … I didn’t write any of the server or client side code related to merging or branching.  I’m by no means an expert here – I’m just another user.  The experts in this area have not reviewed any of this.  It’s just something I came up with one night.  This code probably has bugs and certainly has limitations (which I’ll mention later).  But in my very informal testing it has worked as expected and it solved my immediate problem.

 

Getting started …

 

First we need to agree on a way to represent the integration history of a merged item.  We need to represent the source and target items at the changeset of the merge and the changeset the merge operation happened in.  For example we want to be able to see that:

 

$/teamproject/main;C3 -> $/teamproject/team1;C4

 

(read “->” as “was merged to”)

 

This is what I used…

 

    public struct MergeInfo

    {

        public string SourceServerPath;

        public int SourceChangesetId;

        public string TargetServerPath;

        public int TargetChangesetId;

        public override string ToString()

        {

            return string.Format("{0}{4}C{1} -> {2}{4}C{3}",

                SourceServerPath,

                SourceChangesetId,

                TargetServerPath,

                TargetChangesetId,

                VersionSpec.Separator);

        }

    }

 

And to save some space … let’s assume you have a valid Change object.  If you want more info about how to get one just let me know and I’ll post an example or two…

 

If you recall from my previous post we needed to call “tf branches” to figure out what the potential contributors in any merge are.  In any but the most trivial branching hierarchy there could be multiple contributors.  And in any changeset any number of those multiple contributors may have changed (which was why we needed to call “tf merges” for each contributor to find out who was involved in a merge the changeset).

 

The client OM exposes something like “tf branches” via a call to GetBranchHistory.  This function returns an array (because you could query multiple items) of arrays (because you could query recursively) of tree structures which describes the branch history of the item you requested.  The node you get back is the root of the tree and not the item you specifically queried for.  To get that node you need to call GetRequestedItem() on any item in the tree.

 

private static MergeInfo getContributorInfo(Change change)

{

    int changeId = (change.Item.DeletionId != 0) ? change.Item.ChangesetId - 1 : change.Item.ChangesetId;

    ChangesetVersionSpec version = new ChangesetVersionSpec(changeId);

    BranchHistoryTreeItem[][] history = tfsClient.GetBranchHistory(

    new ItemSpec[] { new ItemSpec(change.Item.ServerItem, RecursionType.None) },

    version);

    BranchHistoryTreeItem item = history[0][0].GetRequestedItem();

    List<Item> potentialContributors = getPotentialContributors(item);

    foreach (Item potentialContrib in potentialContributors)

    {

    MergeInfo mi;

    if (tryGetMergeInfo(potentialContrib, change.Item, version, out mi))

    {

    return mi;

    }

    }

    throw new Exception(string.Format("Contributor not found for item {0}",

    formatItemNameWithChange(change.Item.ServerItem, change.Item.ChangesetId)));

}

 

There are a few things to notice here:

 

1) This code assumes that the object “change” is a merge operation ((change.ChangeType & ChangeType.Merge) != 0)

2) I tweaked the change ID to be one-prior if the change is a merge of a deleted item (since you can’t call GetBranchHistory or QueryMerges on deleted items even if you specify the deletion ID in the item spec).

3) I left out error checking on the return value of GetBranchHistory (and all of it’s values).  Also I used [0][0] because I made a non-recursive call on a single item spec.

4) The call to GetRequestedItem is important – remember that we’re getting the entire branch history so we need to get the node focus on the actual item we care about (it “knows” which we care about because we specified a VersionSpec.

5) There are potentially multiple potential contributors to any change (as we’ve discussed).

6) There can only be one actual contributor (TFS does not allow multiple merges to a single item in the same changeset) so we can return the first one we find.

7) getPotentialContributors and tryGetMergeInfo are methods I wrote – I’ll get to the former today and the latter in my next post.

 

Since it’s small I’ll wrap up today’s post (did I mention this will take a few posts to get through?) with the code for getPotentialContributors:

static List<Item> getPotentialContributors(BranchHistoryTreeItem item)

{

    List<Item> pots = new List<Item>();

    if (item.Parent != null)

    {

    pots.Add(item.Parent.Relative.BranchToItem);

    }

    foreach (BranchHistoryTreeItem i in item.Children)

    {

    pots.Add(i.Relative.BranchToItem);

    }

    if (item.Relative.BranchFromItem != null)

    {

    pots.Add(item.Relative.BranchFromItem);

    }

    return pots;

}

 

The goal is to get the potential list of Item objects that potentially contributed to the merge.  The parameter “item” is the result of calling GetBranchHistory + GetRequestedItem.  In the branch history tree it is focused on the node we are merging to.  Its parent (if not null) is the parent branch item.  Its children (if not null) are the branches from it.  And its relative BranchFrom is where it was originally branched from (probably the same as the parent item but with the specific version information.  This is needed if the parent has been deleted – remember that we can’t query merges of deleted items).

 

That’s enough for today.  It should be obvious by now that the real meat occurs in tryGetMergeInfo – I’ll discuss that in more detail next time.

 

Questions?