Buck Hodges

Visual Studio Online, Team Foundation Server, MSDN

April, 2006

Posts
  • Buck Hodges

    How to compare (diff) trees at different versions using the API

    • 9 Comments

    Recently the question of how to compare files and folders came up.  TFS version 1 doesn't have the project difference type of functionality that was available in VSS.  So if you want to write your own, this code sample will help you with the calls to get the information you'll want to display.

    The code shows two different approaches to getting the information.  The first approach is to use the GetExtendedItem() method that Source Control Explorer uses to get much of its information (SCE also uses QueryPendingSets() to be able show the other users with pending changes on an item, but I didn't put that in here).  Using GetExtendedItem(), you can determine what is different between what's in your workspace and the latest in the repository.

    The second approach uses a pair of calls to GetItems().  While it won't show you whether you or others have pending changes, it will show you the differences between two arbitrary versions of the tree.  If you want pending change information, you can use GetPendingChanges() and connect the information with what GetItems() returns.  You can connect the two sets of data using the item ID in the PendingChange and Item objects, as I have done in the code in order to tell which items are common between the two versions.

    This code also shows how to diff two items.  The code below uses the Item object to construct a DiffItemVersionedFile, but there are also DiffItemLocalFile, DiffItemPendingChangeBase, and DiffItemShelvedChange classes.  While the code produces a text diff, the comments contain details on how to launch an external diff viewer, which is actually simpler than producing a text diff.

    If you are just getting started with the version control API, you'll want to start with the basic example.  You may also want to look at the example showing how to display sizes and dates of files in the server and how to display the labels on a file.

    I've also attached a zip file containing the VS solution for this example.  You may need to adjust the project references in order to build the solution.  If you haven't already done so, you can add the TFS assemblies to the .NET tab in the VS Add Reference dialog.

    You can find the current documentation of the TFS API in the Visual Studio SDK as mentioned here (the current release is Visual Studio 2005 SDK – March 2006 CTP for v2).

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Text;
    using Microsoft.TeamFoundation;
    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.VersionControl.Client;
    using Microsoft.TeamFoundation.VersionControl.Common;

    namespace ProjectDiff
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Figure out the workspace information based on the local cache.
                WorkspaceInfo wsInfo = Workstation.Current.GetLocalWorkspaceInfo(Environment.CurrentDirectory);
                if (wsInfo == null)
                {
                    Console.Error.WriteLine("The current directory is not mapped.");
                    Environment.Exit(1);
                }

                // Now we can get to the workspace.
                TeamFoundationServer tfs = new TeamFoundationServer(wsInfo.ServerUri.AbsoluteUri);
                Workspace workspace = wsInfo.GetWorkspace(tfs);

                try
                {
                    // Display the differences for the current directory and all of its descendants
                    // (fully recursive).
                    if (args.Length == 0)
                    {
                        // Display the differences between what's in the workspace compared to latest.
                        DisplayCurrentDiff(workspace);
                    }
                    else
                    {
                        // Display the differences between the two specified versions.
                        if (args.Length != 2)
                        {
                            Console.Error.WriteLine("Usage: projectdiff <versionspec> <versionspec>");
                            Console.Error.WriteLine("Example: projectdiff D04/06/06 T");
                            Console.Error.WriteLine("         compare midnight April 6, 2006 and latest");
                            Console.Error.WriteLine("Example: projectdiff W T");
                            Console.Error.WriteLine("         compare what's in the workspace to latest");
                            Environment.Exit(1);
                        }

                        // Parse the 
                        VersionSpec version1 = ParseVersionSpec(args[0], workspace);
                        VersionSpec version2 = ParseVersionSpec(args[1], workspace);

                        DisplayVersionDiff(workspace, version1, version2);
                    }
                }
                catch (TeamFoundationServerException e)
                {
                    // If something goes wrong, such as not having access to the server, display
                    // the appropriate error message.
                    Console.Error.WriteLine(e.Message);
                    Environment.Exit(1);
                }
            }

            // This approach uses the same call that Source Control Explorer uses.
            static void DisplayCurrentDiff(Workspace workspace)
            {
                // Let's get a list of the files and folders from the server.  The ExtendedItem
                // contains information about which version is in the workspace and the latest version
                // on the server.
                // You would want to use this approach if you wanted to display the type of
                // information available in the Source Control Explorer.
                ItemSpec[] querySpec = new ItemSpec[] { new ItemSpec(Environment.CurrentDirectory,
                                                                     RecursionType.Full) };
                ExtendedItem[] items = workspace.GetExtendedItems(querySpec, DeletedState.NonDeleted,
                                                                  ItemType.Any)[0];

                // Now let's display what we know about each item.
                DisplayHeader("Files and folders that are at the latest versions:");
                foreach (ExtendedItem item in items)
                {
                    if (item.IsLatest)
                    {
                        DisplayExtendedItem(item, workspace);
                    }
                }

                DisplayHeader("Files and folders that are out of date:");
                foreach (ExtendedItem item in items)
                {
                    if (item.IsInWorkspace && !item.IsLatest)
                    {
                        DisplayExtendedItem(item, workspace);
                    }
                }

                DisplayHeader("Files and folders not in the workspace:");
                foreach (ExtendedItem item in items)
                {
                    if (!item.IsInWorkspace)
                    {
                        DisplayExtendedItem(item, workspace);
                    }
                }
                Console.WriteLine();
            }

            // Displays change and path information from the ExtendedItem.
            static void DisplayExtendedItem(ExtendedItem item, Workspace workspace)
            {
                Console.Write("  ");

                // Indicate whether someone else has a pending change on this file or folder.  The
                // ExtendedItem doesn't contain the list of users with pending changes on this file.
                // For that information, we'd need to call QueryPendingSets() in addition to 
                // GetExtendedItems() and join the two together via the item ID.
                if (item.HasOtherPendingChange)
                {
                    Console.Write("^ ");
                }

                // Show the lock information if someone has locked the file or folder.
                if (item.LockStatus != LockLevel.None)
                {
                    Console.Write("[{0},{1}] "
                                        PendingChange.GetLocalizedStringForLockLevel(item.LockStatus),
                                        item.LockOwner);
                }

                // If there is a change pending on the item in the current workspace, display it.
                if (item.ChangeType != ChangeType.None)
                {
                    Console.Write("({0}) ", PendingChange.GetLocalizedStringForChangeType(item.ChangeType));
                }

                // Display the local path.
                if (!item.IsInWorkspace)
                {
                    // Get the mapping so that we can determine its local path or that it is cloaked.
                    WorkingFolder wf = workspace.TryGetWorkingFolderForServerItem(item.TargetServerItem);

                    // Let's skip cloaked items, since the user doesn't want them.
                    if (!wf.IsCloaked)
                    {
                        Console.WriteLine(wf.LocalItem);
                    }
                }
                else
                {
                    Console.WriteLine(item.LocalItem);
                }
            }

            // This approach compares two different versions of the tree.  When version1 is W
            // (the workspace version spec) and version2 is T (tip/latest), the results will be
            // be equivalent to the other approach (we get some different properties).
            static void DisplayVersionDiff(Workspace workspace, VersionSpec version1,
                                           VersionSpec version2)
            {
                // We need the list of items at the specified versions.  This call only gets
                // information about the versions that have been checked in and does not include
                // any pending changes.  As a result, it does not return pending adds, branches,
                // and undeletes.
                ItemSet itemSet1 = workspace.VersionControlServer.GetItems(Environment.CurrentDirectory,
                                                                           version1, RecursionType.Full);
                ItemSet itemSet2 = workspace.VersionControlServer.GetItems(Environment.CurrentDirectory,
                                                                           version2, RecursionType.Full);
                Item[] items1 = itemSet1.Items;
                Item[] items2 = itemSet2.Items;

                // Build hash tables of the items so that we can quickly determine which items
                // are common.  Every item in the repository is assigned a unique item ID.
                // The item ID never changes, even though the item's path (item.ServerItem) may
                // change due to being renamed or moved.
                Dictionary<int, Item> itemHash1 = CreateHash(items1);
                Dictionary<int, Item> itemHash2 = CreateHash(items2);

                // Show items that are the same.
                DisplayHeader("Same:");
                foreach (Item item in itemHash1.Values)
                {
                    Item item2;
                    if (itemHash2.TryGetValue(item.ItemId, out item2) &&
                        item2.ChangesetId == item.ChangesetId)
                    {
                        Console.WriteLine("  " + item2.ServerItem);
                    }
                }

                // Show items that differ.
                DisplayHeader("Different:");
                foreach (Item item in itemHash1.Values)
                {
                    Item item2;
                    if (itemHash2.TryGetValue(item.ItemId, out item2) &&
                        item2.ChangesetId != item.ChangesetId)
                    {
                        // Figure out what changed.
                        bool showDiff = false;
                        if (item.ItemType == ItemType.File &&
                            !EqualFileContents(item, item2))
                        {
                            Console.Write('e');
                            showDiff = true;
                        }
                        else if (item.Encoding != item2.Encoding)
                        {
                            Console.Write('n');
                        }
                        else if (item.DeletionId != item2.DeletionId)
                        {
                            Console.Write('d');
                        }
                        else if (item.ServerItem != item2.ServerItem)
                        {
                            // Note that we used a case-sensitive comparison in order to catch
                            // renames where only the case changed.
                            Console.Write('r');
                        }

                        Console.WriteLine(" {0}", item2.ServerItem);

                        if (showDiff)
                        {
                            DiffFiles(item, item2);
                        }
                    }
                }

                // Show items only in the first version.
                DisplayHeader("Only in " + version1.DisplayString + ":");
                DisplayOnlyInFirst(itemHash1, itemHash2);

                // Show items only in the second version.
                DisplayHeader("Only in " + version2.DisplayString + ":");
                DisplayOnlyInFirst(itemHash2, itemHash1);
            }

            // Fill in the workspace name if it is null.
            static VersionSpec ParseVersionSpec(String spec, Workspace workspace)
            {
                String user = workspace.VersionControlServer.TeamFoundationServer.AuthenticatedUserName;

                VersionSpec version = VersionSpec.ParseSingleSpec(spec, user);

                // If the user happened to specify only W for the workspace spec, we'll have to
                // fill in the workspace here (the parse method doesn't know it).
                WorkspaceVersionSpec wvs = version as WorkspaceVersionSpec;
                if (wvs != null && wvs.Name == null)
                {
                    wvs.Name = workspace.Name;
                }

                return version;
            }

            // Creates a hash table of the files and folders.
            static Dictionary<int, Item> CreateHash(Item[] items)
            {
                Dictionary<int, Item> itemHash = new Dictionary<int, Item>();
                foreach (Item item in items)
                {
                    itemHash.Add(item.ItemId, item);
                }

                return itemHash;
            }

            // Displays files and folders that are only in the first hash table.
            static void DisplayOnlyInFirst(Dictionary<int, Item> itemHash1,
                                           Dictionary<int, Item> itemHash2)
            {
                foreach (Item item in itemHash1.Values)
                {
                    if (!itemHash2.ContainsKey(item.ItemId))
                    {
                        Console.WriteLine("  " + item.ServerItem);
                    }
                }
            }

            // Returns true if the contents of the two versions of the file are the same.
            static bool EqualFileContents(Item item1, Item item2)
            {
                if (item1.ContentLength != item2.ContentLength)
                {
                    return false;
                }

                // If the two hash values have different lengths or both have a length of zero,
                // the files are not the same.  The only time this would happen would be for
                // files uploaded by clients that have FIPS enforcement enabled (rare).
                // Those clients can't compute the MD5 hash, so it has a length of zero in that
                // case.  To do this right with FIPS, the code would need to compare file
                // contents (call item.DownloadFile()).
                // For information on FIPS enforcement and MD5, see the following link.
                // http://blogs.msdn.com/shawnfa/archive/2005/05/16/417975.aspx
                if (item1.HashValue.Length != item2.HashValue.Length ||
                    item1.HashValue.Length == 0)
                {
                    return false;
                }

                for (int i = 0; i < item1.HashValue.Length; i++)
                {
                    if (item1.HashValue[i] != item2.HashValue[i])
                    {
                        return false;
                    }
                }

                return true;
            }

            // Display the differences between the two file versions.
            static void DiffFiles(Item item1, Item item2)
            {
                if (item1.ItemType != ItemType.File)
                {
                    return;
                }

                Console.WriteLine();

                DiffItemVersionedFile diffItem1 = new DiffItemVersionedFile(item1, 
                                                           new ChangesetVersionSpec(item1.ChangesetId));
                DiffItemVersionedFile diffItem2 = new DiffItemVersionedFile(item2, 
                                                           new ChangesetVersionSpec(item2.ChangesetId));

                // Here we set up the options to show the diffs in the console with the unified diff
                // format.
                // If you simply want to launch the external diff viewer, rather than get a text diff,
                // you just need to set UseThirdPartyTool to true.  You don't need to set any of the
                // other properties to use the external tool.
                DiffOptions options = new DiffOptions();
                options.UseThirdPartyTool = false;

                // These settings are just for the text diff (not needed for an external tool).
                options.Flags = DiffOptionFlags.EnablePreambleHandling | DiffOptionFlags.IgnoreWhiteSpace;
                options.OutputType = DiffOutputType.Unified;
                options.TargetEncoding = Console.OutputEncoding;
                options.SourceEncoding = Console.OutputEncoding;
                options.StreamWriter = new StreamWriter(Console.OpenStandardOutput(), 
                                                        Console.OutputEncoding);
                options.StreamWriter.AutoFlush = true;

                // The last parameter indicates whether the code should block until the external diff
                // viewer exits.  Set it to false if you are calling this from a GUI app.
                Difference.DiffFiles(item1.VersionControlServer, diffItem1, diffItem2,
                                     options, item1.ServerItem, true);
            }

            static void DisplayHeader(String header)
            {
                Console.WriteLine();
                Console.WriteLine(String.Empty.PadLeft(80'='));
                Console.WriteLine(header);
            }
        }
    }
  • Buck Hodges

    TFS Alerts: From address and filtering

    • 6 Comments

    On internal mailing list, the following questions came up regarding the auto-generated check-in mails from TFS.

    1. Can the ‘from’ address be customized to be the email address of the developer who did the check-in?  It seems to default to the service account.
    2. Is it possible to customize the subscription (e.g., using bissubscribe) to send check-in mail for everyone except check-ins performed by a specific account?  I looked at the event schema in the database and I saw an example of using a filter on the subscription but it isn’t clear which field we would filter on.  The ‘committer’ field is optional in the schema, so I doubt that’s the correct one.  Is the ‘checked in by’ field one of the name-value pairs? If so, how would you filter this?  An example would be great.

    Jeff Lucovsky replied with the following information, which I've edited slightly.

    1. There is no mechanism in V1.0 to have the From address contain the address of the developer who checked in the code. There were issues, internally, when the internal smtpHost required the From address to match that of the caller (in this case, the identity assigned to the application pool for TFS).

      We do set the Reply-To address to the developer’s email address, so you can change Outlook to show the Have Replies Sent To values instead of the From values.  Right click on the column headers in the Outlook email display and choose Customize Current View and then click the Fields button.  In the combo box titled "Select available fields from," choose "Address Fields," select the "Have Replies Sent To" entry, click Add, and then move it up to the desired location.
    2. You want to filter on CheckinEvent\Owner (the Commiter is set if one account performs the checkin on behalf of another account). The filter will be something like the following. "Artifacts/Artifact[@TeamProject = 'VSTS Rocks']” <> null AND ‘Owner” <> ‘YourDomain\YourUserName

    In an earlier email, a similar question regarding work item tracking alerts came up.

    Does anyone know of a way to define rules to trigger automated email notifications when a certain field’s value in a work item is changed?

    Pete Sheill responded with the following.  He's also got a related blog post on using bissubscribe.exe.

    There is a way to do it through a command line tool called BisSubscribe, which is available in the SDK.   If the field is a string, the filter syntax is the following:

    "PortfolioProject = '<project name>' AND \”ChangedFields/StringFields/Field[ReferenceName='<field name>']/NewValue\” <> null"

    Example:

    BisSubscribe.exe /eventType WorkItemChangedEvent /deliveryType EmailHtml /server MyServer /address me@someplace.com /filter "PortfolioProject = 'My Project' AND \”ChangedFields/StringFields/Field[ReferenceName='System.State']/NewValue\” <> null"

    If it’s an integer, the syntax is

    "PortfolioProject = '<project name>' AND \”ChangedFields/IntegerFields/Field[ReferenceName='<field name>']/NewValue\” <> null"

    So while I was going through old email, I found another piece of information from Pete regarding the list of server events in TFS v1.

    To enumerate the list of event types in a given system you would have to look both at the files in that Transforms folder and in the Registration data (http://machinename:8080/Services/v1.0/Registration.asmx).  Inside the Registration data you can see that each registered ServiceInterface can register a list of EventTypes.  There is not an API for inserting the event types in the Registration data, but you may use the command-line tool TfsReg to do it.

    • AclChangedEvent
    • BranchMovedEvent
    • BuildCompletionEvent
    • BuildStatusChangeEvent
    • CheckinEvent
    • CommonStructureChangedEvent
    • DataChangedEvent
    • IdentityCreatedEvent
    • IdentityDeletedEvent
    • MembershipChangedEvent
    • NodeCreatedEvent
    • NodePropertiesChangedEvent
    • NodeRenamedEvent
    • NodesDeletedEvent
    • ProjectCreatedEvent
    • ProjectDeletedEvent
    • WorkItemChangedEvent

    You can find more information about the events in the Visual Studio SDK.  The current release is Visual Studio 2005 SDK – March 2006 CTP for v2 (free download after you register).

    If you need to remove a subscription, you'll need to do it the hard way for right now.  The BisSubscribe included in the March SDK doesn't have an option for removing a subscription, even though the RTM code has that option (/unsubscribe).  I've sent email about it, so hopefully it will get updated in a future release.  In the meantime, Ahmed Salijee has a post on the issue.

    [UPDATE 10/07/06]  The bissubscribe.exe installed on the AT in the Team Foundation server installation folder does have the /unsubscribe option.

  • Buck Hodges

    Switching source control providers in VS prior to VS 2005

    • 3 Comments

    Back in 2003, Korby Parnell listed a couple of tools to switch source control providers.  The interesting one was Sourcecode Control Switcher by Soenke Schau.  It adds it self into the task tray to make switching providers really simple.  I didn't try it, but it looks like it may be a good way to go

    Another possibility is to create Windows shortcuts.  Make a pair of batch scripts each containing one of the reg command lines that Ed Hintz posted.  Then create Windows shortcuts to each batch script, perhaps on your desktop.  Right click on each and choose Properties.  In the Shortcut tab you'll see a Shortcut key entry.  Click in it and press a key.  You could assign T (Ctrl + Alt + T) to the TFS MSSCCI provider and V (Ctrl + Alt + V) to the VSS provider.

    Of course, you could also have batch files that launch VS after using the reg command line to set the provider as desired.

     

     

  • Buck Hodges

    Installing TFS, the short version

    • 1 Comments

    Jeff Atwood at Vertigo posted his version of Cliff's Notes for a Team System Install, including links to get the trial versions of everything required.  If you aren't going to read the official guide, you might try his approach.  I know no one reading this blog would take shortcuts...but just in case.

    Meanwhile, you can still get a Virtual PC image with the TFS release candidate from MSDN subscriber downloads.  It's under Developers Tools | Visual Studio 2005 | English | Visual Studio Team Foundation Server | Visual Studio 2005 Team Foundation Server Trial Edition (English) | Visual… RC VPC Part 1 of 2 and RC VPC 2 of 2.  There's work being done for a new VPC with the final (RTM) release, but that won't be ready for a couple of weeks.

  • Buck Hodges

    Updated MSSCCI Provider

    • 1 Comments

    [UPDATE April 8]  The download page works now, and the link is now up to date.

    Brian Harry posted an announcement today that there is a new release of the MSSCCI provider for non-VS2005 environments.  This one actually has signed binaries. ;-)

    Here's the summary from the download page.

    Overview

    The Visual Studio Team Foundation Server MSSCCI Provider enables integrated use of Team Foundation Version Control with products that don't support Team Explorer integration.

    Instructions

    Download and run Visual Studio Team Foundation Server MSSCCI Provider.msi on a computer with one of the following products:
    • Visual Studio .NET 2003
    • Visual C++ 6 SP6
    • Visual Visual Basic 6 SP6
    • Visual FoxPro 9 SP1
    • Microsoft Access 2003 SP2
    • SQL Server Management Studio
  • Buck Hodges

    Problem with checking in from a Microsoft Content Management System solution

    • 1 Comments

    This has come up twice now, so it seems to be worth posting in the hopes that folks will find it when searching for the error message.

    Here's the scenario.

    • In Visual Studio 2005, add a Microsoft CMS project to TFS version control and check it in.
    • Check out a file in that CMS project and then try to check it in.
    • The following error occurs:  Unable to cast Microsoft.ContentManagement.DeveloperTools.TeamExplorer...

    Ben Ryan, a developer on version control, describes the problem and the workaround as follows.

    The root cause of the problem is that the CMS Service Pack that adds support for Visual Studio 2005 is built against a version of the Microsoft.VisualStudio.Shell.Interop.dll assembly that is incompatible with the version we ship with other products (i.e. VSIP 7.1 and VSIP 8.0).  Therefore, the IVsHierarchy that Microsoft.ContentManagement.DeveloperTools.TeamExplorer  subclasses cannot be cast to the IVsHierarchy that TFS references in its managed package.

    We are engaging the Content Management Server team to resolve this issue.

    In the meantime you can work around the issue by closing the solution and checking in from the Pending Changes toolwindow or by checking the files in from the command line.

  • Buck Hodges

    How to perform a silent install of Visual Studio 2005 Team Explorer

    • 0 Comments

    Aaron Stebner has a nice post showing the steps need to create uattended installations of Team Explorer (aka TFC, Team Foundation Client).

    How to perform a silent install of Visual Studio 2005 Team Explorer

    I have previously posted instructions for performing a silent installation for Visual Studio 2005 and the Visual Studio 2005 Express Editions.  A customer read those previous blog posts and asked a follow-up question about how to perform a silent installation of Visual Studio 2005 Team Explorer, which is the client software needed to access Team Foundation Server.

    Team Explorer setup is architected in a way that is similar to the Visual Studio 2005 Express Editions, but the silent installation instructions are a bit easier because unattended INI file creation was not specifically disabled in setup UI like it was for the Express Editions.

Page 1 of 1 (7 items)