I'm apparently a shelveset packrat.  I'm not really sure how it happened, but it did (well, gauntlet not deleting them combined with me not wanting to reuse a name, but still).  I noticed yesterday that I had 89 shelvesets, and I knew a good chunk of them had already been committed, but I couldn't really tell you off-hand which were in that category.  Sure, I could check the creation date for the shelveset, but I knew I still had some older changes that I wanted to check out and potentially keep around for early V2 clean-up, but things that had flushed out of my mental cache months ago.

So, in an attempt to do some largely automated spring cleaning, I wrote up this tiny little app.  It happens to show usage of a few things, hence posting it here in case it helps as an example to others: 1) finding your shelvesets 2) getting the pending changes from a shelveset 3) using the ThreeWayMerge class for performing a three-way merge (in this case, not using a third-party tool) and 4) the call to delete a shelveset.

As with the others, this is intentionally simple and probably has some bugs in it.  It only really looks at file edits (it could look at adds, including adds of folders, deletes, etc. I'm just being lazy).  I don't try/finally to make sure I always delete the temp files.  I use Shelveset's DisplayName (includes ;user) even though I'm only looking at my own shelvesets so I could have just used Name.  The logic of both >= shelved isn't magical by any means - I just picked something that made sense to me (half or more of the changes are in), but you can (obviously) change the logic to something that you find more suitable (maybe you want to factor in the conflicting count).  Expect that this code will likely get updated at least a couple of times as I run across issues :)  That said, it did work for me, and now I'm down to a (still too large, but at least more reasonable) 23 shelvesets.

The needed references are to (GAC-installed) Microsoft.TeamFoundation.Client.dll, Microsoft.TeamFoundation.VersionControl.Client.dll, and Microsoft.TeamFoundation.VersionControl.Common.dll

Here's some output against a committed and non-committed shelveset on my test server:

User NORTHAMERICA\jmanning has 2 shelveset(s)

Examining shelveset: test-shelveset;jmanning [created Sunday, November 06, 2005 10:20:48 PM]
Skipping non-edit change: $/perf-testing-51104/add.txt [add]
Skipping non-edit change: $/perf-testing-51104/branch.txt [branch]
$/perf-testing-51104/branch+edit.txt [branch, edit]: 0 shelved, 0 latest, 1 both, 0 conflicting
Skipping non-edit change: $/perf-testing-51104/delete.txt [delete]
$/perf-testing-51104/edit.txt [edit]: 0 shelved, 0 latest, 1 both, 0 conflicting
Skipping non-edit change: $/perf-testing-51104/rename.txt [rename]
$/perf-testing-51104/rename+edit.txt [rename, edit]: 0 shelved, 0 latest, 1 both, 0 conflicting
Skipping non-edit change: $/perf-testing-51104/undelete.txt [undelete]
$/perf-testing-51104/undelete+edit.txt [undelete, edit]: 0 shelved, 0 latest, 1 both, 0 conflicting
$/perf-testing-51104/undelete+edit+rename.txt [rename, undelete, edit]: 0 shelved, 0 latest, 1 both, 0 conflicting
Shelveset totals: 0 shelved, 0 latest, 5 both, 0 conflicting
Shelveset test-shelveset;jmanning is likely already committed - go ahead and delete it? (Y/N)n

Examining shelveset: ss2;jmanning
Skipping non-edit change: $/perf-testing-51104/ss2/add.txt [add]
Skipping non-edit change: $/perf-testing-51104/ss2/branch.txt [branch]
Skipping non-committed item: $/perf-testing-51104/ss2/branch+edit.txt
Skipping non-edit change: $/perf-testing-51104/ss2/delete.txt [delete]
$/perf-testing-51104/ss2/edit.txt [edit]: 1 shelved, 0 latest, 0 both, 0 conflicting
Skipping non-edit change: $/perf-testing-51104/ss2/rename.txt [rename]
$/perf-testing-51104/ss2/rename-source+edit.txt [was $/perf-testing-51104/ss2/rename+edit.txt] [rename, edit]: 1 shelved, 0 latest, 0 both, 0 conflicting
Skipping non-edit change: $/perf-testing-51104/ss2/undelete.txt [undelete]
Skipping deleted item: $/perf-testing-51104/ss2/undelete+edit.txt
Skipping deleted item: $/perf-testing-51104/ss2/undelete+edit+rename-source.txt
Shelveset totals: 2 shelved, 0 latest, 0 both, 0 conflicting
Shelveset is likely NOT already committed

using System;

using System.IO;

using System.Text;

using System.Text.RegularExpressions;

using Microsoft.TeamFoundation.Client;

using Microsoft.TeamFoundation.VersionControl.Client;

using Microsoft.TeamFoundation.VersionControl.Common;


class CheckShelvesets


    private static VersionControlServer m_vcs;

    private static string m_tempBase;

    private static string m_tempLatest;

    private static string m_tempModified;

    private static string m_tempOutput;


    static void Main(string[] args)


        if (args.Length == 0)


            Console.Error.WriteLine("Usage: CheckShelvesets <server> [regex_to_match]");



        TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer(args[0]);

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

        Regex regex = args.Length == 1 ? new Regex(".*") : new Regex(args[1]);


        // Get some temp filenames for 3-way merge calculations

        m_tempBase = Path.GetTempFileName();

        m_tempLatest = Path.GetTempFileName();

        m_tempModified = Path.GetTempFileName();

        m_tempOutput = Path.GetTempFileName();


        // Fetch all shelvesets, we will filter using Regex instead

        Shelveset[] shelvesets = m_vcs.QueryShelvesets(null, m_vcs.AuthenticatedUser);

        Console.WriteLine("User {0} has {1} shelveset(s)", m_vcs.AuthenticatedUser, shelvesets.Length);


        foreach (Shelveset shelveset in shelvesets)


            if (!regex.IsMatch(shelveset.Name)) continue;





        // delete those temp files







    private static void ExamineShelveset(Shelveset shelveset)



        Console.WriteLine("Examining shelveset: {0} [created {1}]",



        PendingChange[] changes = m_vcs.QueryShelvedChanges(shelveset)[0].PendingChanges;

        int shelvedTotal = 0, latestTotal = 0, bothTotal = 0, conflictingTotal = 0;

        foreach (PendingChange change in changes)


            if (!change.IsEdit || change.IsAdd)


                Console.WriteLine("Skipping non-edit change: {0} [{1}]",



                continue; // TODO: handle others



            // do a GetItem call in case it's been renamed

            Item item = m_vcs.GetItem(change.ItemId, int.MaxValue);

            if (item == null ||

                item.ChangesetId == 0) // Beta3 bug - should have gotten null here


                Console.WriteLine("Skipping non-committed item: {0}", change.ServerItem);



            if (item.DeletionId > 0)


                Console.WriteLine("Skipping deleted item: {0}", item.ServerItem);



            if (item.Encoding == RepositoryConstants.EncodingBinary)


                Console.WriteLine("Skipping binary file: {0}", item.ServerItem);




            string path;

            if (VersionControlPath.Equals(change.ServerItem, item.ServerItem))


                // item has not changed name

                path = item.ServerItem;




                path = String.Format("{0} [was {1}]", item.ServerItem, change.ServerItem);



            // Fetch the 3 input files



            m_vcs.DownloadFile(item.ServerItem, m_tempLatest);


            // Detect the encodings - anything binary and we'll skip

            int baseCodePage = FileType.Detect(m_tempBase, null);

            int latestCodePage = FileType.Detect(m_tempLatest, null);

            int modifiedCodePage = FileType.Detect(m_tempModified, null);

            if (baseCodePage == RepositoryConstants.EncodingBinary ||

                latestCodePage == RepositoryConstants.EncodingBinary ||

                modifiedCodePage == RepositoryConstants.EncodingBinary)


                Console.WriteLine("Skipping merge with a binary input: {0}", item.ServerItem);




            ThreeWayMerge threeWayMerge = new ThreeWayMerge();

            threeWayMerge.UseExternalMergeTool = false;


            threeWayMerge.LatestFileName = m_tempLatest;

            threeWayMerge.LatestFileEncoding = Encoding.GetEncoding(latestCodePage);


            threeWayMerge.ModifiedFileName = m_tempModified;

            Encoding modifiedEncoding = Encoding.GetEncoding(modifiedCodePage);

            threeWayMerge.ModifiedFileEncoding = modifiedEncoding;


            threeWayMerge.OriginalFileName = m_tempBase;

            threeWayMerge.OriginalFileEncoding = Encoding.GetEncoding(baseCodePage);


            threeWayMerge.MergedFileName = m_tempOutput;

            threeWayMerge.MergedFileEncoding = modifiedEncoding;




            Console.WriteLine("{0} [{1}]: {2} shelved, {3} latest, {4} both, {5} conflicting",








            shelvedTotal += threeWayMerge.ContentMergeSummary.TotalModified;

            latestTotal += threeWayMerge.ContentMergeSummary.TotalLatest;

            bothTotal += threeWayMerge.ContentMergeSummary.TotalBoth;

            conflictingTotal += threeWayMerge.ContentMergeSummary.TotalConflicting;


        Console.WriteLine("Shelveset totals: {0} shelved, {1} latest, {2} both, {3} conflicting",

                          shelvedTotal, latestTotal, bothTotal, conflictingTotal);

        if (bothTotal > 0 && bothTotal >= shelvedTotal)


            Console.Write("Shelveset {0} is likely already committed - go ahead and delete it? (Y/N)",


            ConsoleKeyInfo keyInfo = Console.ReadKey();


            if (keyInfo.KeyChar == 'y' || keyInfo.KeyChar == 'Y')


                Console.WriteLine("Deleting shelveset {0}", shelveset.DisplayName);






            Console.WriteLine("Shelveset is likely NOT already committed");