{"id":4663,"date":"2005-10-25T15:41:00","date_gmt":"2005-10-25T15:41:00","guid":{"rendered":"https:\/\/blogs.msdn.microsoft.com\/buckh\/2005\/10\/25\/displaying-the-labels-on-a-file-including-label-comments\/"},"modified":"2005-10-25T15:41:00","modified_gmt":"2005-10-25T15:41:00","slug":"displaying-the-labels-on-a-file-including-label-comments","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/buckh\/displaying-the-labels-on-a-file-including-label-comments\/","title":{"rendered":"Displaying the labels on a file, including label comments"},"content":{"rendered":"<p><P>Unfortunately, there&#8217;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.&nbsp; You&nbsp;also can&#8217;t efficiently answer the&nbsp;question,&nbsp;&#8220;What labels&nbsp;involve foo.cs?&#8221;&nbsp; While this won&#8217;t be changed for v1, you can certainly do it using code.&nbsp; I <A href=\"http:\/\/forums.microsoft.com\/msdn\/ShowPost.aspx?PostID=108099\">mentioned on&nbsp;the TFS&nbsp;forum<\/A> that I&#8217;d try to put together a piece of code to do this.&nbsp; The result is the code below.<\/P>\n<P>The code is really simple to do this, but I ended up adding more to it than I originally intended.&nbsp; All that&#8217;s really necessary here is to call QueryLabels() to get the information we need.<\/P>\n<P>Let&#8217;s look at the QueryLabels() call in a little detail, since it is the heart of the app.&nbsp; Here is the method declaration from the VersionControlServer class.<\/P>\n<BLOCKQUOTE>\n<P><FONT face=\"Courier New\">public VersionControlLabel[] QueryLabels(String labelName,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<\/FONT><FONT face=\"Courier New\">String labelScope,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String owner,&nbsp;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bool includeItems,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String filterItem,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; VersionSpec versionFilterItem)<\/FONT><\/P><\/BLOCKQUOTE>\n<P>By convention, methods that begin with &#8220;Query&#8221; in the source control API allow you to pass null to mean &#8220;give me everything.&#8221;&nbsp; In the code below, I don&#8217;t want to filter by <FONT face=\"Courier New\">labelName<\/FONT> or <FONT face=\"Courier New\">owner<\/FONT>, so I set those to null to include everything.<\/P>\n<P>If the user specified a server path for the&nbsp;scope (scope is always a server path and not a local path), we&#8217;ll use it, and otherwise we&#8217;ll use the root ($\/).&nbsp; The scope of a label is, effectively, the part of the tree where it has ownership of that label name.&nbsp; 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.&nbsp; For this program, setting the scope simply narrows the part of the tree it will include in the output.&nbsp; 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.<\/P>\n<BLOCKQUOTE>\n<P><FONT face=\"Courier New\">D:\\ws1&gt;tree<BR>Folder PATH listing for volume Dev<BR>Volume serial number is 0006EE50 185C:793F<BR>D:.<BR>\u251c\u2500\u2500\u2500A<BR>\u2514\u2500\u2500\u2500B<\/FONT><\/P>\n<P><FONT face=\"Courier New\">D:\\ws1&gt;tf label <\/FONT><FONT face=\"Courier New\">Foo@$\/testproj\/A<\/FONT><FONT face=\"Courier New\"> A<BR>Created label <\/FONT><FONT face=\"Courier New\">Foo@$\/testproj\/A<\/FONT><\/P>\n<P><FONT face=\"Courier New\">D:\\ws1&gt;tf label <\/FONT><FONT face=\"Courier New\">Foo@$\/testproj\/B<\/FONT><FONT face=\"Courier New\"> B<BR>Created label <\/FONT><FONT face=\"Courier New\">Foo@$\/testproj\/B<\/FONT><\/P>\n<P><FONT face=\"Courier New\">D:\\ws1&gt;d:\\LabelHistory\\LabelHistory\\bin\\Debug\\LabelHistory.exe<BR>Foo (10\/25\/2005 4:00 PM)<BR>Foo (10\/25\/2005 4:00 PM)<\/FONT><\/P><\/BLOCKQUOTE>\n<P>The most important parameter here is actually <FONT face=\"Courier New\">includeItems<\/FONT>.&nbsp; By setting this parameter to false, we&#8217;ll get the label metadata without getting the list of files and folders that are in the label.&nbsp; 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.<\/P>\n<P>The remaining parameters are <FONT face=\"Courier New\">filterItem<\/FONT> and <FONT face=\"Courier New\">versionFilterItem<\/FONT>.&nbsp; The <FONT face=\"Courier New\">filterItem<\/FONT> parameter allows you to specify a server or local path whereby the query results will only include labels involving that file or folder.&nbsp; It allows you to answer the question, &#8220;What labels have been applied to file foo.cs?&#8221;&nbsp; The <FONT face=\"Courier New\">versionFilterItem<\/FONT> is used to specify what version of the item had the specified path.&nbsp; It&#8217;s an unfortunate complexity that&#8217;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).&nbsp; Before your eyes glaze over (they haven&#8217;t already, right?), I just set that parameter to latest.<\/P>\n<P>Here&#8217;s an example of using the program with the tree mentioned earlier.&nbsp; I modified the Foo label on A to have a comment, so it has a later modification time.<\/P>\n<BLOCKQUOTE>\n<P><FONT face=\"Courier New\">D:\\ws1&gt;tf label Foo@$\/testproj\/A \/comment:&#8221;This is the first label I created.&#8221;<BR>Updated label Foo@$\/testproj\/A<\/FONT><\/P>\n<P><FONT face=\"Courier New\">D:\\ws1&gt;d:\\LabelHistory\\LabelHistory\\bin\\Debug\\LabelHistory.exe<BR>Foo (10\/25\/2005 4:05 PM)<BR>&nbsp;&nbsp; Comment: This is the first label I created.<BR>Foo (10\/25\/2005 4:00 PM)<\/FONT><\/P><\/BLOCKQUOTE>\n<P>Then I added a file under A, called a.txt, and modified the label to include it.&nbsp; Running the app on A\\a.txt, we see that it is only involved in one of the two labels in the system.<\/P>\n<BLOCKQUOTE>\n<P><FONT face=\"Courier New\">D:\\ws1&gt;tf label <\/FONT><FONT face=\"Courier New\">Foo@$\/testproj\/A<\/FONT><FONT face=\"Courier New\"> A\\a.txt<BR>Updated label <\/FONT><FONT face=\"Courier New\">Foo@$\/testproj\/A<\/FONT><\/P>\n<P><FONT face=\"Courier New\">D:\\ws1&gt;d:\\LabelHistory\\LabelHistory\\bin\\Debug\\LabelHistory.exe A\\a.txt<BR>Foo (10\/25\/2005 4:56 PM)<BR>&nbsp;&nbsp; Comment: This is the first label I created.<\/FONT><\/P><\/BLOCKQUOTE>\n<P>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.<\/P>\n<BLOCKQUOTE>\n<P>Microsoft.TeamFoundation.Client<BR>Microsoft.TeamFoundation.Common<BR>Microsoft.TeamFoundation.VersionControl.Client<BR>Microsoft.TeamFoundation.VersionControl.Common<\/P><\/BLOCKQUOTE><PRE><SPAN>using<\/SPAN> System;\n<SPAN>using<\/SPAN> System.IO;\n<SPAN>using<\/SPAN> Microsoft.TeamFoundation;\n<SPAN>using<\/SPAN> Microsoft.TeamFoundation.Client;\n<SPAN>using<\/SPAN> Microsoft.TeamFoundation.VersionControl.Client;\n<SPAN>using<\/SPAN> Microsoft.TeamFoundation.VersionControl.Common;<\/p>\n<p><SPAN>namespace<\/SPAN> LabelHistory\n{\n    <SPAN>class<\/SPAN> Program\n    {\n        <SPAN>static<\/SPAN> <SPAN>void<\/SPAN> Main(<SPAN>string<\/SPAN>[] args)\n        {\n            <SPAN>\/\/ Check and get the arguments.<\/SPAN>\n            String path, scope;\n            VersionControlServer sourceControl;\n            GetPathAndScope(args, <SPAN>out<\/SPAN> path, <SPAN>out<\/SPAN> scope, <SPAN>out<\/SPAN> sourceControl);<\/p>\n<p>            <SPAN>\/\/ Retrieve and print the label history for the file.<\/SPAN>\n            VersionControlLabel[] labels = <SPAN>null<\/SPAN>;\n            <SPAN>try<\/SPAN>\n            {\n                <SPAN>\/\/ The first three arguments here are null because we do not<\/SPAN>\n                <SPAN>\/\/ want to filter by label name, scope, or owner.<\/SPAN>\n                <SPAN>\/\/ Since we don&#8217;t need the server to send back the items in<\/SPAN>\n                <SPAN>\/\/ the label, we get much better performance by ommitting<\/SPAN>\n                <SPAN>\/\/ those through setting the fourth parameter to false.<\/SPAN>\n                labels = sourceControl.QueryLabels(<SPAN>null<\/SPAN>, scope, <SPAN>null<\/SPAN>, <SPAN>false<\/SPAN>, \n                                                   path, VersionSpec.Latest);\n            }\n            <SPAN>catch<\/SPAN> (TeamFoundationServerException e)\n            {\n                <SPAN>\/\/ We couldn&#8217;t contact the server, the item wasn&#8217;t found,<\/SPAN>\n                <SPAN>\/\/ or there was some other problem reported by the server,<\/SPAN>\n                <SPAN>\/\/ so we stop here.<\/SPAN>\n                Console.Error.WriteLine(e.Message);\n                Environment.Exit(<SPAN>1<\/SPAN>);\n            }<\/p>\n<p>            <SPAN>if<\/SPAN> (labels.Length == <SPAN>0<\/SPAN>)\n            {\n                Console.WriteLine(<SPAN>&#8220;There are no labels for &#8220;<\/SPAN> + path);\n            }\n            <SPAN>else<\/SPAN>\n            {\n                <SPAN>foreach<\/SPAN> (VersionControlLabel label <SPAN>in<\/SPAN> labels)\n                {\n                    <SPAN>\/\/ Display the label&#8217;s name and when it was last modified.<\/SPAN>\n                    Console.WriteLine(<SPAN>&#8220;{0} ({1})&#8221;<\/SPAN>, label.Name,\n                                      label.LastModifiedDate.ToString(<SPAN>&#8220;g&#8221;<\/SPAN>));<\/p>\n<p>                    <SPAN>\/\/ For labels that actually have comments, display it.<\/SPAN>\n                    <SPAN>if<\/SPAN> (label.Comment.Length &gt; <SPAN>0<\/SPAN>)\n                    {\n                        Console.WriteLine(<SPAN>&#8221;   Comment: &#8220;<\/SPAN> + label.Comment);\n                    }\n                }\n            }\n        }<\/p>\n<p>        <SPAN>private<\/SPAN> <SPAN>static<\/SPAN> <SPAN>void<\/SPAN> GetPathAndScope(String[] args,\n                                            <SPAN>out<\/SPAN> String path, <SPAN>out<\/SPAN> String scope,\n                                            <SPAN>out<\/SPAN> VersionControlServer sourceControl)\n        {\n            <SPAN>\/\/ This little app takes either no args or a file path and optionally a scope.<\/SPAN>\n            <SPAN>if<\/SPAN> (args.Length &gt; <SPAN>2<\/SPAN> || \n                args.Length == <SPAN>1<\/SPAN> &amp;&amp; args[<SPAN>0<\/SPAN>] == <SPAN>&#8220;\/?&#8221;<\/SPAN>)\n            {\n                Console.WriteLine(<SPAN>&#8220;Usage: labelhist&#8221;<\/SPAN>);\n                Console.WriteLine(<SPAN>&#8221;       labelhist path [label scope]&#8221;<\/SPAN>);\n                Console.WriteLine();\n                Console.WriteLine(<SPAN>&#8220;With no arguments, all label names and comments are displayed.&#8221;<\/SPAN>);\n                Console.WriteLine(<SPAN>&#8220;If a path is specified, only the labels containing that path&#8221;<\/SPAN>);\n                Console.WriteLine(<SPAN>&#8220;are displayed.&#8221;<\/SPAN>);\n                Console.WriteLine(<SPAN>&#8220;If a scope is supplied, only labels at or below that scope will&#8221;<\/SPAN>);\n                Console.WriteLine(<SPAN>&#8220;will be displayed.&#8221;<\/SPAN>);\n                Console.WriteLine();\n                Console.WriteLine(<SPAN>&#8220;Examples: labelhist c:\\\\projects\\\\secret\\\\notes.txt&#8221;<\/SPAN>);\n                Console.WriteLine(<SPAN>&#8221;          labelhist $\/secret\/notes.txt&#8221;<\/SPAN>);\n                Console.WriteLine(<SPAN>&#8221;          labelhist c:\\\\projects\\\\secret\\\\notes.txt $\/secret&#8221;<\/SPAN>);\n                Environment.Exit(<SPAN>1<\/SPAN>);\n            }<\/p>\n<p>            <SPAN>\/\/ Figure out the server based on either the argument or the<\/SPAN>\n            <SPAN>\/\/ current directory.<\/SPAN>\n            WorkspaceInfo wsInfo = <SPAN>null<\/SPAN>;\n            <SPAN>if<\/SPAN> (args.Length &lt; <SPAN>1<\/SPAN>)\n            {\n                path = <SPAN>null<\/SPAN>;\n            }\n            <SPAN>else<\/SPAN>\n            {\n                path = args[<SPAN>0<\/SPAN>];\n                <SPAN>try<\/SPAN>\n                {\n                    <SPAN>if<\/SPAN> (!VersionControlPath.IsServerItem(path))\n                    {\n                        wsInfo = Workstation.Current.GetLocalWorkspaceInfo(path);\n                    }\n                }\n                <SPAN>catch<\/SPAN> (Exception e)\n                {\n                    <SPAN>\/\/ The user provided a bad path argument.<\/SPAN>\n                    Console.Error.WriteLine(e.Message);\n                    Environment.Exit(<SPAN>1<\/SPAN>);\n                }\n            }<\/p>\n<p>            <SPAN>if<\/SPAN> (wsInfo == <SPAN>null<\/SPAN>)\n            {\n                wsInfo = Workstation.Current.GetLocalWorkspaceInfo(Environment.CurrentDirectory);\n            }<\/p>\n<p>            <SPAN>\/\/ Stop if we couldn&#8217;t figure out the server.<\/SPAN>\n            <SPAN>if<\/SPAN> (wsInfo == <SPAN>null<\/SPAN>)\n            {\n                Console.Error.WriteLine(<SPAN>&#8220;Unable to determine the server.&#8221;<\/SPAN>);\n                Environment.Exit(<SPAN>1<\/SPAN>);\n            }<\/p>\n<p>            TeamFoundationServer tfs =\n                TeamFoundationServerFactory.GetServer(wsInfo.ServerName);\n                                                      <SPAN>\/\/ RTM: wsInfo.ServerUri.AbsoluteUri);<\/SPAN>\n            sourceControl = (VersionControlServer)tfs.GetService(<SPAN>typeof<\/SPAN>(VersionControlServer));<\/p>\n<p>            <SPAN>\/\/ Pick up the label scope, if supplied.<\/SPAN>\n            scope = VersionControlPath.RootFolder;\n            <SPAN>if<\/SPAN> (args.Length == <SPAN>2<\/SPAN>)\n            {\n                <SPAN>\/\/ The scope must be a server path, so we convert it here if<\/SPAN>\n                <SPAN>\/\/ the user specified a local path.<\/SPAN>\n                <SPAN>if<\/SPAN> (!VersionControlPath.IsServerItem(args[<SPAN>1<\/SPAN>]))\n                {\n                    Workspace workspace = wsInfo.GetWorkspace(tfs);\n                    scope = workspace.GetServerItemForLocalItem(args[<SPAN>1<\/SPAN>]);\n                }\n                <SPAN>else<\/SPAN>\n                {\n                    scope = args[<SPAN>1<\/SPAN>];\n                }\n            }\n        }\n    }\n}<\/PRE><PRE><FONT face=\"Times New Roman\"><\/FONT><\/PRE>\n<P>[Update 10\/26] I added Microsoft.TeamFoundation.Common to the list of assemblies to reference.<\/P>\n<P>[Update 7\/12\/06]&nbsp; Jeff Atwood posted a VS solution containing this code and a binary.&nbsp; You can find it at the end of <A href=\"http:\/\/blogs.vertigosoftware.com\/teamsystem\/archive\/2006\/07\/07\/Listing_all_Labels_attached_to_a_file_or_folder.aspx\">http:\/\/blogs.vertigosoftware.com\/teamsystem\/archive\/2006\/07\/07\/Listing_all_Labels_attached_to_a_file_or_folder.aspx<\/A>.<\/P><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Unfortunately, there&#8217;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.&nbsp; You&nbsp;also can&#8217;t efficiently answer the&nbsp;question,&nbsp;&#8220;What labels&nbsp;involve foo.cs?&#8221;&nbsp; While this won&#8217;t be changed for v1, you can certainly do it using code.&nbsp; [&hellip;]<\/p>\n","protected":false},"author":94,"featured_media":10268,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[6,8,15],"class_list":["post-4663","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-source-control","tag-team-foundation","tag-tfs-api"],"acf":[],"blog_post_summary":"<p>Unfortunately, there&#8217;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.&nbsp; You&nbsp;also can&#8217;t efficiently answer the&nbsp;question,&nbsp;&#8220;What labels&nbsp;involve foo.cs?&#8221;&nbsp; While this won&#8217;t be changed for v1, you can certainly do it using code.&nbsp; [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/posts\/4663","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/users\/94"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/comments?post=4663"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/posts\/4663\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/media\/10268"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/media?parent=4663"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/categories?post=4663"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/buckh\/wp-json\/wp\/v2\/tags?post=4663"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}