TFS Meltdown - How can I recover shelved changes

2020-06-24 08:37发布

问题:

I had my working folder set to a RAM drive. During the night there was an extended power outage, the UPS ran out and my machine went down. Thankfully I shelved my changes before I went home and that shelveset is visible in Team Explorer. The changeset includes the project file and some new files which have not yet been added to source control.

I'm attempting to recover the affected files but am getting errors:

Attempting to view the shelved files gives TF10187 (or a general, unnumbered) The system cannot find the file specified even though I can see them in the Pending Changes list.

Attempting to unshelve the set in its entirety gives errors relating to incompatible changes which I can't resolve.

I'm guessing TFS cached the shelveset locally on the RAM disc which has since reinitialised itself and therefore lost the cache, but I'm hoping I'm wrong.

Can anyone assist?

回答1:

I had someone come to me and ask the same question yesterday, fortunately they had a backup of the TFS Project database (tfs_) so we restored that to another database and I poked around and figured it out (so, if you have a backup then yes, you can recover all the files).

First of all a little info on the tables in the database.

A Shelveset can be identified by querying the tbl_Workspace table and looking for all records with Type=1 (Shelveset), you can of course also filter by name with the WorkspaceName column.

The other tables of interest are:

tbl_PendingChanges (which references the WorkspaceId from tbl_Workspace) - which files are part of the ShelveSet

tbl_VersionedItem (linked via ItemId column to tbl_PendingChanges) - parent path and name of files

tbl_Content (linked via FileId to PendingChanges) - this is where your file content is stored in as compressed (gzip) data

Now for the solution; the following query can show you your files:

SELECT c.[CreationDate], c.[Content], vi.[ChildItem], vi.ParentPath
FROM [dbo].[tbl_Content] c 
INNER JOIN [dbo].[tbl_PendingChange] pc ON pc.FileId = c.FileId
INNER JOIN [dbo].[tbl_Workspace] w ON w.WorkspaceId = pc.WorkspaceId
INNER JOIN [dbo].[tbl_VersionedItem] vi ON vi.ItemId = pc.ItemId
WHERE w.WorkspaceName = '<YOUR SHELVESET NAME>'

With that I wrote some code to get the data back from SQL and then decompress the content with the GZipStream class and save the files off to disk.

A week of work was back in an hour or so.

This was done with TFS 2010.

Hope this helps!



回答2:

Here is an updated response for TFS2015, which had another schema change. Below is a C# Console application for writing the txt files to Desktop. Make sure to fill in connString and shelvesetName variables.

using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.IO.Compression;

namespace RestoreTFSShelve
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            string shelvesetName = "";
            string connString = "";

            SqlConnection cn = new SqlConnection(connString);
            SqlCommand cmd = new SqlCommand(@"
SELECT c.[CreationDate], c.[Content], v.FullPath
FROM [dbo].[tbl_Content] c
INNER JOIN [dbo].tbl_FileMetadata f ON f.ResourceId = c.ResourceId
INNER JOIN [dbo].tbl_FileReference b ON f.ResourceId = b.ResourceId
INNER JOIN [dbo].[tbl_PendingChange] pc ON pc.FileId = b.FileId
INNER JOIN [dbo].[tbl_Workspace] w ON w.WorkspaceId = pc.WorkspaceId
INNER JOIN [dbo].[tbl_Version] v ON v.ItemId = pc.ItemId AND v.VersionTo = 2147483647
WHERE w.WorkspaceName = '@ShelvesetName'", cn);

            cmd.Parameters.AddWithValue("@ShelvesetName", shelvesetName);

            DataTable dt = new DataTable();
            new SqlDataAdapter(cmd).Fill(dt);

            foreach (DataRow row in dt.Rows)
            {
                string[] arrFilePath = row[2].ToString().Split('\\');
                string fileName = arrFilePath[arrFilePath.Length - 2];
                byte[] unzippedContent = Decompress((byte[])row[1]);
                File.WriteAllBytes(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), fileName), unzippedContent);
            }
        }

        private static byte[] Decompress(byte[] gzip)
        {
            using (GZipStream stream = new GZipStream(new MemoryStream(gzip), CompressionMode.Decompress))
            {
                const int size = 4096;
                byte[] buffer = new byte[size];
                using (MemoryStream memory = new MemoryStream())
                {
                    int count = 0;
                    do
                    {
                        count = stream.Read(buffer, 0, size);
                        if (count > 0)
                        {
                            memory.Write(buffer, 0, count);
                        }
                    }
                    while (count > 0);
                    return memory.ToArray();
                }
            }
        }
    }
}


回答3:

I had something similar happen to me with a TFS 2012 instance. My SQL query was a bit different since the schema changed for TFS 2012. Hope this helps someone.

SELECT c.[CreationDate], c.[Content], v.FullPath
FROM [dbo].[tbl_Content] c 
INNER JOIN [dbo].[tbl_File] f ON f.ResourceId = c.ResourceId
INNER JOIN [dbo].[tbl_PendingChange] pc ON pc.FileId = f.FileId--c.FileId
INNER JOIN [dbo].[tbl_Workspace] w ON w.WorkspaceId = pc.WorkspaceId
INNER JOIN [dbo].[tbl_Version] v ON v.ItemId = pc.ItemId AND v.VersionTo = 2147483647
WHERE w.WorkspaceName = @ShelvesetName

2147483647 seems to be 2^32 - 1 which I think may stand for "latest" in TFS 2012. I then also wrote a C# widget to decompress the Gzip-encoded stream and dump it to disk with the proper file name. I am not preserving hierarchy.

string cnstring = string.Format("Server={0};Database={1};Trusted_Connection=True;", txtDbInstance.Text, txtDbName.Text);
SqlConnection cn = new SqlConnection(cnstring);
SqlCommand cmd = new SqlCommand(@"
SELECT c.[CreationDate], c.[Content], v.FullPath
FROM [dbo].[tbl_Content] c 
INNER JOIN [dbo].[tbl_File] f ON f.ResourceId = c.ResourceId
INNER JOIN [dbo].[tbl_PendingChange] pc ON pc.FileId = f.FileId--c.FileId
INNER JOIN [dbo].[tbl_Workspace] w ON w.WorkspaceId = pc.WorkspaceId
INNER JOIN [dbo].[tbl_Version] v ON v.ItemId = pc.ItemId AND v.VersionTo = 2147483647
WHERE w.WorkspaceName = @ShelvesetName", cn);

cmd.Parameters.AddWithValue("@ShelvesetName", txtShelvesetName.Text);

DataTable dt = new DataTable();
new SqlDataAdapter(cmd).Fill(dt);
listBox1.DisplayMember = "FullPath";
listBox1.ValueMember = "FullPath";
listBox1.DataSource = dt;

if(!Directory.Exists(txtOutputLocation.Text)) { Directory.CreateDirectory(txtOutputLocation.Text); }
foreach (DataRow row in dt.Rows)
{
    string[] arrFilePath = row[2].ToString().Split('\\');
    string fileName = arrFilePath[arrFilePath.Length - 2];
    byte[] unzippedContent = Decompress((byte[])row[1]);
    File.WriteAllBytes(Path.Combine(txtOutputLocation.Text, fileName), unzippedContent);
}
}

    static byte[] Decompress(byte[] gzip)
    {
using(GZipStream stream = new GZipStream(new MemoryStream(gzip), CompressionMode.Decompress))
{
    const int size = 4096;
    byte[] buffer = new byte[size];
    using(MemoryStream memory = new MemoryStream())
    {
        int count = 0;
        do
        {
    count = stream.Read(buffer, 0, size);
    if(count > 0)
    {
        memory.Write(buffer, 0, count);
    }
        }
        while(count > 0);
        return memory.ToArray();
    }
}
}