Displaying the labels on a file, including label comments


Unfortunately, there's not a fast, efficient way to see the list of labels in the system with the full comment without also seeing a list of all of the files included in a label.  You also can't efficiently answer the question, "What labels involve foo.cs?"  While this won't be changed for v1, you can certainly do it using code.  I mentioned on the TFS forum that I'd try to put together a piece of code to do this.  The result is the code below.


The code is really simple to do this, but I ended up adding more to it than I originally intended.  All that's really necessary here is to call QueryLabels() to get the information we need.


Let's look at the QueryLabels() call in a little detail, since it is the heart of the app.  Here is the method declaration from the VersionControlServer class.



public VersionControlLabel[] QueryLabels(String labelName,
                                         
String labelScope,
                                         String owner, 
                                         bool includeItems,
                                         String filterItem,
                                         VersionSpec versionFilterItem)


By convention, methods that begin with "Query" in the source control API allow you to pass null to mean "give me everything."  In the code below, I don't want to filter by labelName or owner, so I set those to null to include everything.


If the user specified a server path for the scope (scope is always a server path and not a local path), we'll use it, and otherwise we'll use the root ($/).  The scope of a label is, effectively, the part of the tree where it has ownership of that label name.  In other words by specifying the label scope, $/A and $/B can have separate labels named Foo, but no new label Foo can be used under $/A or $/B.  For this program, setting the scope simply narrows the part of the tree it will include in the output.  For example, running this with a scope of $/A would show only one label called Foo, but running it with $/ as the scope (or omitting the scope) would result in two Foo labels being printed.



D:\ws1>tree
Folder PATH listing for volume Dev
Volume serial number is 0006EE50 185C:793F
D:.
├───A
└───B


D:\ws1>tf label Foo@$/testproj/A A
Created label
Foo@$/testproj/A


D:\ws1>tf label Foo@$/testproj/B B
Created label
Foo@$/testproj/B


D:\ws1>d:\LabelHistory\LabelHistory\bin\Debug\LabelHistory.exe
Foo (10/25/2005 4:00 PM)
Foo (10/25/2005 4:00 PM)


The most important parameter here is actually includeItems.  By setting this parameter to false, we'll get the label metadata without getting the list of files and folders that are in the label.  This saves both a ton of bandwidth as well as load on the server for any query involving real-world labels that include many thousands of files.


The remaining parameters are filterItem and versionFilterItem.  The filterItem parameter allows you to specify a server or local path whereby the query results will only include labels involving that file or folder.  It allows you to answer the question, "What labels have been applied to file foo.cs?"  The versionFilterItem is used to specify what version of the item had the specified path.  It's an unfortunate complexity that's due to the fact that we support rename (e.g., A was called Z at changeset 12, F at changeset 45, and A at changeset 100 and beyond).  Before your eyes glaze over (they haven't already, right?), I just set that parameter to latest.


Here's an example of using the program with the tree mentioned earlier.  I modified the Foo label on A to have a comment, so it has a later modification time.



D:\ws1>tf label Foo@$/testproj/A /comment:"This is the first label I created."
Updated label Foo@$/testproj/A


D:\ws1>d:\LabelHistory\LabelHistory\bin\Debug\LabelHistory.exe
Foo (10/25/2005 4:05 PM)
   Comment: This is the first label I created.
Foo (10/25/2005 4:00 PM)


Then I added a file under A, called a.txt, and modified the label to include it.  Running the app on A\a.txt, we see that it is only involved in one of the two labels in the system.



D:\ws1>tf label Foo@$/testproj/A A\a.txt
Updated label
Foo@$/testproj/A


D:\ws1>d:\LabelHistory\LabelHistory\bin\Debug\LabelHistory.exe A\a.txt
Foo (10/25/2005 4:56 PM)
   Comment: This is the first label I created.


To build it, you can create a Windows console app in Visual Studio, drop this code into it, and add the following references to the VS project.



Microsoft.TeamFoundation.Client
Microsoft.TeamFoundation.Common
Microsoft.TeamFoundation.VersionControl.Client
Microsoft.TeamFoundation.VersionControl.Common

using System;
using System.IO;
using Microsoft.TeamFoundation;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.VersionControl.Common;

namespace LabelHistory
{
class Program
{
static void Main(string[] args)
{
// Check and get the arguments.
String path, scope;
VersionControlServer sourceControl;
GetPathAndScope(args, out path, out scope, out sourceControl);

// Retrieve and print the label history for the file.
VersionControlLabel[] labels = null;
try
{
// The first three arguments here are null because we do not
// want to filter by label name, scope, or owner.
// Since we don't need the server to send back the items in
// the label, we get much better performance by ommitting
// those through setting the fourth parameter to false.
labels = sourceControl.QueryLabels(null, scope, null, false,
path, VersionSpec.Latest);
}
catch (TeamFoundationServerException e)
{
// We couldn't contact the server, the item wasn't found,
// or there was some other problem reported by the server,
// so we stop here.
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
}

if (labels.Length == 0)
{
Console.WriteLine("There are no labels for " + path);
}
else
{
foreach (VersionControlLabel label in labels)
{
// Display the label's name and when it was last modified.
Console.WriteLine("{0} ({1})", label.Name,
label.LastModifiedDate.ToString("g"));

// For labels that actually have comments, display it.
if (label.Comment.Length > 0)
{
Console.WriteLine(" Comment: " + label.Comment);
}
}
}
}

private static void GetPathAndScope(String[] args,
out String path, out String scope,
out VersionControlServer sourceControl)
{
// This little app takes either no args or a file path and optionally a scope.
if (args.Length > 2 ||
args.Length == 1 && args[0] == "/?")
{
Console.WriteLine("Usage: labelhist");
Console.WriteLine(" labelhist path [label scope]");
Console.WriteLine();
Console.WriteLine("With no arguments, all label names and comments are displayed.");
Console.WriteLine("If a path is specified, only the labels containing that path");
Console.WriteLine("are displayed.");
Console.WriteLine("If a scope is supplied, only labels at or below that scope will");
Console.WriteLine("will be displayed.");
Console.WriteLine();
Console.WriteLine("Examples: labelhist c:\\projects\\secret\\notes.txt");
Console.WriteLine(" labelhist $/secret/notes.txt");
Console.WriteLine(" labelhist c:\\projects\\secret\\notes.txt $/secret");
Environment.Exit(1);
}

// Figure out the server based on either the argument or the
// current directory.
WorkspaceInfo wsInfo = null;
if (args.Length < 1)
{
path = null;
}
else
{
path = args[0];
try
{
if (!VersionControlPath.IsServerItem(path))
{
wsInfo = Workstation.Current.GetLocalWorkspaceInfo(path);
}
}
catch (Exception e)
{
// The user provided a bad path argument.
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
}
}

if (wsInfo == null)
{
wsInfo = Workstation.Current.GetLocalWorkspaceInfo(Environment.CurrentDirectory);
}

// Stop if we couldn't figure out the server.
if (wsInfo == null)
{
Console.Error.WriteLine("Unable to determine the server.");
Environment.Exit(1);
}

TeamFoundationServer tfs =
TeamFoundationServerFactory.GetServer(wsInfo.ServerName);
// RTM: wsInfo.ServerUri.AbsoluteUri);
sourceControl = (VersionControlServer)tfs.GetService(typeof(VersionControlServer));

// Pick up the label scope, if supplied.
scope = VersionControlPath.RootFolder;
if (args.Length == 2)
{
// The scope must be a server path, so we convert it here if
// the user specified a local path.
if (!VersionControlPath.IsServerItem(args[1]))
{
Workspace workspace = wsInfo.GetWorkspace(tfs);
scope = workspace.GetServerItemForLocalItem(args[1]);
}
else
{
scope = args[1];
}
}
}
}
}


[Update 10/26] I added Microsoft.TeamFoundation.Common to the list of assemblies to reference.


[Update 7/12/06]  Jeff Atwood posted a VS solution containing this code and a binary.  You can find it at the end of http://blogs.vertigosoftware.com/teamsystem/archive/2006/07/07/Listing_all_Labels_attached_to_a_file_or_folder.aspx.

Comments (14)

  1. Raoul says:

    Hi Buck,

    this seems handy… Can you explain me why the label is not visible in the history for a file or project? I was not able to see my label other then searching for it… Is it purposely left out?

    Regards, Raoul

  2. buckh says:

    Honestly, it’s an oversight. It fell through the cracks, so to speak. To actually interleave it in the file history properly, the QueryLabels() method on the server will have to be changed to provide a way to get the items in the label that match the item filter and not the entire set of items in the label.

  3. Buck Hodges says:

    Recently the question of how to compare files and folders came up.&amp;nbsp;&amp;nbsp;TFS version 1 doesn’t have&amp;nbsp;the…

  4. Buck Hodges says:

    I recently had to put together a list of links to code samples.&amp;nbsp; This isn’t even close to comprehensive,…

  5. In a recent email I was asked: I want to be able to report on what objects where changed since the last

  6. Got this cool list of code samples from Buck Hodges blog Version Control How to Write a Team Foundation

  7. Подсистема контроля версий файлов в TFS позволяет работать из командной строки. Находится эта утилита

  8. Buck Hodges says:

    Have you wished that the output of the dir command from tf.exe would show you the dates and sizes of

  9. Dave says:

    I’ve modified this to interleave with version history just have to find a place to post.  Unfortunately it takes like 4+ minutes to run on TFS 2005 most of the time on just the initial QueryLabels.

  10. buckh says:

    Dave, be sure to try it on 2008 SP1.  The performance improvement from 2005 to 2008 SP1 is significant.

    Buck

  11. Dave R says:

    Ok. I’ll try to post here.  This code I tested on VS2005 TFS2005 SP1.  It works thought takes 4+ minutes when doing full changeset history.  I haven’t had a chance to try on TFS2008 yet. Hopefully performance will be better.

    Thanks for the initial code Buck, got me started on this highly wanted feature. (many of our developers have been asking for this).

    This might need a little formatting in the output and cleanup work.  Enjoy.

    ———————————————-

    using System;

    using System.Collections;

    using System.Collections.Generic;

    using System.IO;

    using Microsoft.TeamFoundation;

    using Microsoft.TeamFoundation.Client;

    using Microsoft.TeamFoundation.VersionControl.Client;

    using Microsoft.TeamFoundation.VersionControl.Common;

    namespace LabelHistory

    {

    class LabelItem

       {

           private string _name;

           private string _comment;

           private int _changesetID;

           private string _path;

           private string _date;

           private string _owner;

           public string Name { get { return _name; }}

           public string Comment { get { return _comment; }}

           public int ChangesetID { get { return _changesetID;  } }

           public string Path { get { return _path;  } }

           public string Date { get { return _date; }}

           public string Owner { get { return _owner; }}

           /// <summary>

           ///

           /// </summary>

           /// <param name="name"></param>

           /// <param name="comment"></param>

           /// <param name="changesetID"></param>

           /// <param name="path"></param>

           public LabelItem(string name, string comment,int changesetID,string path,string date, string owner)

           {

               _name=name;

               _comment = comment;

               _changesetID = changesetID;

               _path = path;

               _date = date;

               _owner = owner;

           }

           /// <summary>

           ///

           /// </summary>

           /// <returns></returns>

           public override int GetHashCode()

           {

               return _changesetID;

           }

       }

       class Program

       {

           /// <summary>

           /// Main program – see Usage for arguments

           /// </summary>

           /// <param name="args"></param>

           static void Main(string[] args)

           {

               // Check and get the arguments.

               String path, scope;

               VersionControlServer sourceControl;

               Dictionary<int,LinkedList<LabelItem>> labelHash=new Dictionary<int,LinkedList<LabelItem>>();  //will be used as a hash of changeset# to a linked list of labelitem’s

               //Get the path and scope from the args passed in.

               GetPathAndScope(args, out path, out scope, out sourceControl);

               //Checks for optional arguments in the command line args and sets member variable

               //flags depending on the option arguments selected.  Will exit the program if

               //args invalid and display usage

               ArgCheck(args);

               // Retrieve and print the label history for the file.

               VersionControlLabel[] labels = null;

               try

               {

                   // The first three arguments here are null because we do not

                   // want to filter by label name, scope, or owner.

                   // Since we don’t need the server to send back the items in

                   // the label, we get much better performance by ommitting

                   // those through setting the fourth parameter to false.

                   labels = sourceControl.QueryLabels(null, scope, null, false,

                                                      path, VersionSpec.Latest);

               }

               catch (TeamFoundationServerException e)

               {

                   // We couldn’t contact the server, the item wasn’t found,

                   // or there was some other problem reported by the server,

                   // so we stop here.

                   Console.Error.WriteLine(e.Message);

                   Environment.Exit(1);

               }

               if (labels.Length == 0)

               {

                   if (!_changesetHistory)

                       Console.WriteLine("There are no labels for " + path);

               }

               else

               {

                   //main section for looping through the labels that were found that include our item

                   foreach (VersionControlLabel label in labels)

                   {

                       // Display the label’s name and when it was last modified.

                       if (!_changesetHistory)

                           Console.WriteLine("{0} ({1})", label.Name,

                                         label.LastModifiedDate.ToString("g"));

                       // For labels that actually have comments, display it.

                       if (label.Comment.Length > 0)

                       {

                           if (!_changesetHistory)

                               Console.WriteLine("   Comment: " + label.Comment);

                       }

                       //If we don’t need item changeset information we can continue in the loop.

                       if (_noFileItemChecks)

                           continue;

                       VersionControlLabel checkLabel = label;

                       //Query for the specific label details for the label we are looking at in the loop now

                       VersionControlLabel[] specificLabels = null;

                       specificLabels = sourceControl.QueryLabels(label.Name, scope, null,  true,

                                                      path, VersionSpec.Latest);

                       if (specificLabels.Length > 0)

                           checkLabel = specificLabels[0];

                       //Loop through the label details to see if we can find the item we are checking to get it’s changeset

                       //it was labeled at.

                       foreach (Item item in checkLabel.Items)

                       {

                           //compare the label item with the item path passed for argument

                           if (String.Compare(item.ServerItem, path, true)==0)

                           {

                               if (!_changesetHistory)

                                   Console.WriteLine("Item:{0}, Changeset:{1}, Date:{2}",item.ServerItem,item.ChangesetId, item.CheckinDate);

                               //if we are going to return full changeset history then see if there is a linked list associated with

                               //this changeset id yet, if so add a labelitem to it, if not create the list and add a label item to it

                               if (_changesetHistory)

                               {

                                   LinkedList<LabelItem> labelItemlist=null;

                                   if (!labelHash.TryGetValue(item.ChangesetId, out labelItemlist))

                                   {

                                       labelItemlist = new LinkedList<LabelItem>();

                                       labelHash.Add(item.ChangesetId, labelItemlist);

                                   }

                                   labelItemlist.AddLast(new LabelItem(checkLabel.Name, checkLabel.Comment, item.ChangesetId, path, checkLabel.LastModifiedDate.ToString(),checkLabel.OwnerName));

                               }

                               break; // found

                           }

                       }

                   }

               }

               //Main section for full changeset history return

               if (_changesetHistory)

               {

                   try

                   {

                       //Request the full changeset history for the item passed on the command line

                       IEnumerable changesets = sourceControl.QueryHistory(path, VersionSpec.Latest, 0, _recursive ? RecursionType.Full : RecursionType.None, null, null, null,

                                                                           Int32.MaxValue, false, false);

                       //Display header information.

                       Console.WriteLine("Changeset/Label history for:");

                       Console.WriteLine("{0}",path);

                       Console.WriteLine("");

                       Console.WriteLine("{0}{1}{2}{3}","Changeset".PadRight(COL1),"User".PadRight(COL2),"Date".PadRight(COL3),"Comment/Label".PadRight(COL4));

                       //Loop through all changesets and output labels that are associated with changeset

                       //then the changeset information itself.

                       foreach (Changeset change in changesets)

                       {

                           LinkedList<LabelItem> labelItemlist = null;

                           if (labelHash.TryGetValue(change.ChangesetId, out labelItemlist) && labelItemlist != null)

                           {

                               foreach (LabelItem item in labelItemlist)

                               {

                                   //Output the label associated with the changeset information

                                   Console.WriteLine("{0}{1}{2}{3}", item.ChangesetID.ToString().PadRight(COL1), item.Owner.PadRight(COL2), item.Date.PadRight(COL3), ("Label:"+item.Name).PadRight(COL4));

                                   //Console.WriteLine("Label Path:{0}", item.Path);    

                               }

                           }

                           //Output the changeset information

                           Console.WriteLine("{0}{1}{2}{3}", change.ChangesetId.ToString().PadRight(COL1), change.Committer.PadRight(COL2),change.CreationDate.ToString().PadRight(COL3) , change.Comment!=null ? change.Comment : "");

                       }

                   }

                   catch (Exception e)

                   {

                       // We couldn’t contact the server, the item wasn’t found,

                       // or there was some other problem reported by the server,

                       // so we stop here

                       Console.Error.WriteLine(e.Message);

                       Environment.Exit(1);

                   }

               }

               //Console.WriteLine("End");

           }

           /// <summary>

           /// GetPathAndScope

           /// </summary>

           /// <param name="args">Args from command line</param>

           /// <param name="path">output’s path</param>

           /// <param name="scope">output’s scope</param>

           /// <param name="sourceControl">returns VersionControlServer object</param>

           private static void GetPathAndScope(String[] args,

                                               out String path, out String scope,

                                               out VersionControlServer sourceControl)

           {

               // This little app takes either no args or a file path and optionally a scope.

               if (args.Length > 3 || args.Length==0 ||

                   (args.Length == 1 && args[0] == "/?"))

               {

                   Usage();

               }

               // Figure out the server based on either the argument or the

               // current directory.

               WorkspaceInfo wsInfo = null;

               if (args.Length < 1)

               {

                   path = null;

               }

               else

               {

                   path = args[0];

                   try

                   {

                       if (!VersionControlPath.IsServerItem(path))

                       {

                           wsInfo = Workstation.Current.GetLocalWorkspaceInfo(path);                        

                       }

                   }

                   catch (Exception e)

                   {

                       // The user provided a bad path argument.

                       Console.Error.WriteLine(e.Message);

                       Environment.Exit(1);

                   }

               }

               if (wsInfo == null)

               {

                   wsInfo = Workstation.Current.GetLocalWorkspaceInfo(Environment.CurrentDirectory);

               }

               // Stop if we couldn’t figure out the server.

               if (wsInfo == null)

               {

                   Console.Error.WriteLine("Unable to determine the server.");

                   Environment.Exit(1);

               }

               TeamFoundationServer tfs =

                   TeamFoundationServerFactory.GetServer(wsInfo.ServerUri.ToString());

               // RTM: wsInfo.ServerUri.AbsoluteUri);

               sourceControl = (VersionControlServer)tfs.GetService(typeof(VersionControlServer));

               if (args.Length>=1)

               {

                   //If the user did not pass a server item assume it is a local item and try to find it.

                   if (!VersionControlPath.IsServerItem(args[0]))

                   {

                       Workspace workspace = wsInfo.GetWorkspace(tfs);

                       string localItem = workspace.TryGetServerItemForLocalItem(args[0]);

                       if (localItem != null)

                           path = localItem;

                       else

                       {

                           Console.WriteLine("Unable to determine server item for:"+path);

                           Environment.Exit(1);

                       }

                   }

               }

               // Pick up the label scope, if supplied.

               scope = VersionControlPath.RootFolder;

               if (args.Length >= 2 && args[1].StartsWith("$/"))

               {

                   // The scope must be a server path, so we convert it here if

                   // the user specified a local path.

                   if (!VersionControlPath.IsServerItem(args[1]))

                   {

                       Workspace workspace = wsInfo.GetWorkspace(tfs);

                       scope = workspace.GetServerItemForLocalItem(args[1]);

                   }

                   else

                   {

                           scope = args[1];

                   }

               }

           }

           /// <summary>

           /// Check the optional arguments and set private static variables based on the results

           /// Sets _larg true if /l found.  Sets _iarg true if /i found.

           /// Sets _noFileItemChecks and _changesetHistory based on /l or /i found.

           /// Set _recursive to true if /r is found.

           /// Reports Usage and Exits the application  if /r is specified with /l or /i

           /// Reports Usage and Exits the application if both /l and /i specified

           /// </summary>

           /// <param name="args">The args from main command line</param>

           private static void ArgCheck(string[] args)

           {

               if (args.Length<2)

                   return;

               for (int x = 1; x < args.Length; x++)

               {

                   if (String.Compare(args[x], "/l", true) == 0)

                   {

                       _larg = true;

                       _noFileItemChecks = true;

                       _changesetHistory = false;

                   }

                   if (String.Compare(args[x], "/i", true) == 0)

                   {

                       _iarg = true;

                       _noFileItemChecks = false;

                       _changesetHistory = false;

                   }

                   if (String.Compare(args[x], "/r", true) == 0)

                   {

                       _recursive = true;

                   }

               }

               if (_larg && _iarg)

               {

                   Console.WriteLine("Invalid usage: /l and /i arguments can not be used together.");

                   Usage();

               }

               if ( (_larg || _iarg) && _recursive)

               {

                   Console.WriteLine("Invalid usage: /r can not be used with /l or /i");

                   Usage();

               }

           }

           /// <summary>

           /// Usage display for Label History

           /// </summary>

           private static void Usage()

           {

               Console.WriteLine("Usage: labelhistory");

                   Console.WriteLine("       labelhistory path [label scope] [/l|/i] [/r]");

                   Console.WriteLine();

                   Console.WriteLine("With no optional arguments, all label names and comments are displayed.");

                   Console.WriteLine("Only the labels containing that path");

                   Console.WriteLine("are displayed.");

                   Console.WriteLine("If a scope is supplied, only labels at or below that scope will");

                   Console.WriteLine("will be displayed.");

                   Console.WriteLine();

          &nbs

  12. buckh says:

    Dave, thanks for the contribution!  That’s the first time someone’s posted an entire app in a comment on my blog.  :-)

    Buck

  13. Dave R says:

    Yes thought it would be interesting in a comment.  Couple of points I noticed after the fact.  Usage is slightly off.  Must supply at least path.  I removed the returning of all labels with history with no path since that took so long I never actually saw it return. /l,/i, /r are all optional.  It also will convert a local path into a server path if possible.

    If you ask internally some of the TFS version control product managers (i.e. Matthew) if they have been working with a large firm/manager  with a similar name, they’ll probably recognize it :)

    Figured I’d contribute something back as I was learning some of this stuff.

  14. Dave says:

    I tested the above program with TFS2008 time dropped to about 1 minute and 30 seconds for a full return. A lot better but still not close to the time VSS used to return label history for a single file in under 10 seconds for many files even in a large repository (a lot longer on directories for VSS though).

    I’m also not sure the above program produces output appropriate to the intention, as it does seem hard to integrate labels into history in a reasonable display (which I’m figuring might be one of the reasons TFS didn’t do it originally).

    It is pretty critical to being able to answer questions quickly like what files have been labeled in this directory and for which labels.  Also questions like did someone drop any label on this file yet.  It turns out this can be critical to some workflows for people coming from the VSS world.

Skip to main content