4

Is it possible to move a work item from one project to another inside TFS? I’ve seen a copy option, but no move. Also, if it is possible, what’s the implication for any of the WI history?

I found this article from 2008 that seem to say it's not, but I wondered if there'd been any progress since then.

4

2 回答 2

1

It isn't possible to move, just copy. The way we do it, is we do the copy, link the original, then close the original as obsolete. You could also create the copy and TF Destroy the original, but you will lose all history.

If you wanted to, you could get very fancy and create your own "move" utility that copies the workitem and all of the history, then closes out (or destroys) the old one. Seems like overkill for something that you probably shouldn't need to do all that often.

于 2010-06-30T13:54:38.803 回答
1

Lars Wilhelmsen wrote a WorkItemMigrator -> http://larsw.codeplex.com/SourceControl/list/changesets

Good starting point for a utility you can customize for your needs. We used it to split off a 100 or so work items to a new project. Here's the program I ended up with. Modify the query to subset the items to migrate.

namespace WorkItemMigrator
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Common;
using Microsoft.TeamFoundation.Server;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

class Program
{

    #region Members
    private static readonly Dictionary<Uri, TfsTeamProjectCollection> Collections = new Dictionary<Uri, TfsTeamProjectCollection>();
    private static readonly Uri SourceCollectionUri = new Uri("http://your.domain.com:8080/tfs/DefaultCollection");
    private static readonly Uri TargetCollectionUri = new Uri("http://your.domain.com:8080/tfs/DefaultCollection");
    private const String Areas = "ProjectModelHierarchy";
    private const string Iterations = "ProjectLifecycle";
    private const string TargetProjectName = "TargetProject";
    private const string MicrosoftVstsCommonStackRankFieldName = "Microsoft.VSTS.Common.StackRank";
    private const string MicrosoftVstsCommonPriority = "Microsoft.VSTS.Common.Priority";
    private const string TargetWorkItemType = "User Story";
    private const string Wiql = "SELECT [System.Id], [System.State], [System.Title], [System.AssignedTo], [System.WorkItemType], [Microsoft.VSTS.Common.Priority], " +
                        "[System.IterationPath], [System.AreaPath], [System.History], [System.Description] " +
                        "FROM WorkItems WHERE [System.TeamProject] = 'SourceProject' AND " + 
                        "[System.State] = 'Active' " +
                        "ORDER BY [System.Id]";

    private static WorkItemTypeCollection WorkItemTypes;
    private static Dictionary<int, int> WorkItemIdMap = new Dictionary<int, int>();

    #endregion
    static void Main()
    {
    var createAreasAndIterations = GetRunMode();

    var sourceWorkItemStore = GetSourceWorkItemStore();

    var sourceWorkItems = sourceWorkItemStore.Query(Wiql);

    var targetWorkItemStore = GetTargetWorkItemStore();
    var targetProject = targetWorkItemStore.Projects[TargetProjectName];


    WorkItemTypes = targetProject.WorkItemTypes;

    foreach (WorkItem sourceWorkItem in sourceWorkItems)
    {
        if (createAreasAndIterations)
        {
        Console.WriteLine();
        EnsureThatStructureExists(TargetProjectName, Areas, sourceWorkItem.AreaPath.Substring(sourceWorkItem.AreaPath.IndexOf("\\") + 1));
        EnsureThatStructureExists(TargetProjectName, Iterations, sourceWorkItem.IterationPath.Substring(sourceWorkItem.IterationPath.IndexOf("\\") + 1));
        }
        else
        {
        MigrateWorkItem(sourceWorkItem);
        }
    }

    if (!createAreasAndIterations)
    {
        var query = from WorkItem wi in sourceWorkItems where wi.Links.Count > 0 select wi;
        foreach (WorkItem sourceWorkItem in query)
        {
        LinkRelatedItems(targetWorkItemStore, sourceWorkItem);
        }
    }

    TextWriter tw = File.CreateText(@"C:\temp\TFS_MigratedItems.csv");
    tw.WriteLine("SourceId,TargetId");
    foreach (var entry in WorkItemIdMap)
    {
        tw.WriteLine(entry.Key + "," + entry.Value);
    }
    tw.Close();
    Console.WriteLine();
    Console.WriteLine("Done! Have a nice day.");
    Console.ReadLine();
    }

    private static bool GetRunMode()
    {
    bool createAreasAndIterations;
    while (true)
    {
        Console.Write("Create [A]reas/Iterations or [M]igrate (Ctrl-C to quit)?: ");
        var command = Console.ReadLine().ToUpper().Trim();
        if (command == "A")
        {
        createAreasAndIterations = true;
        break;
        }
        if (command == "M")
        {
        createAreasAndIterations = false;
        break;
        }
        Console.WriteLine("Unknown command " + command + " - try again.");
    }
    return createAreasAndIterations;
    }

    private static void MigrateWorkItem(WorkItem sourceWorkItem)
    {

    var targetWIT = WorkItemTypes[sourceWorkItem.Type.Name];
    var newWorkItem = targetWIT.NewWorkItem();
    //var newWorkItem = targetWorkItemType.NewWorkItem();

    // Description (Task) / Steps to reproduce (Bug)

    if (sourceWorkItem.Type.Name != "Bug")
    {
        newWorkItem.Description = sourceWorkItem.Description;
    }
    else
    {
        newWorkItem.Fields["Microsoft.VSTS.TCM.ReproSteps"].Value = sourceWorkItem.Description;
    }

    // History
    newWorkItem.History = sourceWorkItem.History;
    // Title
    newWorkItem.Title = sourceWorkItem.Title;
    // Assigned To
    newWorkItem.Fields[CoreField.AssignedTo].Value = sourceWorkItem.Fields[CoreField.AssignedTo].Value;
    // Stack Rank - Priority
    newWorkItem.Fields[MicrosoftVstsCommonPriority].Value = sourceWorkItem.Fields[MicrosoftVstsCommonPriority].Value;
    // Area Path
    newWorkItem.AreaPath = FormatPath(TargetProjectName, sourceWorkItem.AreaPath);
    // Iteration Path
    newWorkItem.IterationPath = FormatPath(TargetProjectName, sourceWorkItem.IterationPath);
    // Activity
    if (sourceWorkItem.Type.Name == "Task")
    {
        newWorkItem.Fields["Microsoft.VSTS.Common.Activity"].Value = sourceWorkItem.Fields["Microsoft.VSTS.Common.Discipline"].Value;
    }
    // State
    //newWorkItem.State = sourceWorkItem.State;
    // Reason
    //newWorkItem.Reason = sourceWorkItem.Reason;

    // build a usable rendition of prior revision history
    RevisionCollection revisions = sourceWorkItem.Revisions;
    var query = from Revision r in revisions orderby r.Fields["Changed Date"].Value descending select r;
    StringBuilder sb = new StringBuilder(String.Format("Migrated from work item {0}<BR />\n", sourceWorkItem.Id));
    foreach (Revision revision in query)
    {
        String history = (String)revision.Fields["History"].Value;
        if (!String.IsNullOrEmpty(history))
        {
        foreach (Field f in revision.Fields)
        {
            if (!Object.Equals(f.Value, f.OriginalValue))
            {
            if (f.Name == "History")
            {
                string notation = string.Empty;
                if (revision.Fields["State"].OriginalValue != revision.Fields["State"].Value)
                {
                notation = String.Format("({0} to {1})", revision.Fields["State"].OriginalValue, revision.Fields["State"].Value);
                }
                //Console.WriteLine("<STRONG>{0} Edited {3} by {1}</STRONG><BR />\n{2}", revision.Fields["Changed Date"].Value.ToString(), revision.Fields["Changed By"].Value.ToString(), f.Value, notation);
                sb.Append(String.Format("<STRONG>{0} Edited {3} by {1}</STRONG><BR />\n{2}<BR />\n", revision.Fields["Changed Date"].Value.ToString(), revision.Fields["Changed By"].Value.ToString(), f.Value, notation));
            }
            }
        }
        //Console.WriteLine("Revision {0}: ", revision.Fields["Rev"].Value);
        //Console.WriteLine("  ChangedDate: " + revision.Fields["ChangedDate"].Value);
        //Console.WriteLine("  History: " + sb.ToString());
        }
    }
    newWorkItem.History = sb.ToString();

    // Attachments
    for (var i = 0; i < sourceWorkItem.AttachedFileCount; i++)
    {
        CopyAttachment(sourceWorkItem.Attachments[i], newWorkItem);
    }

    // Validate before save
    if (!newWorkItem.IsValid())
    {
        var reasons = newWorkItem.Validate();
        Console.WriteLine(string.Format("Could not validate new work item (old id: {0}).", sourceWorkItem.Id));
        foreach (Field reason in reasons)
        {
        Console.WriteLine("Field: " + reason.Name + ", Status: " + reason.Status + ", Value: " + reason.Value);
        }
    }
    else
    {
        Console.Write("[" + sourceWorkItem.Id + "] " + newWorkItem.Title);
        try
        {
        newWorkItem.Save(SaveFlags.None);
        Console.ForegroundColor = ConsoleColor.Cyan;
        Console.WriteLine(string.Format(" [saved: {0}]", newWorkItem.Id));
        WorkItemIdMap.Add(sourceWorkItem.Id, newWorkItem.Id);
        Console.ResetColor();
        }
        catch (Exception ex)
        {
        Console.WriteLine(ex.Message);
        throw;
        }
    }
    }

    private static void CopyAttachment(Attachment attachment, WorkItem newWorkItem)
    {
    using (var client = new WebClient())
    {
        client.UseDefaultCredentials = true;
        client.DownloadFile(attachment.Uri, attachment.Name);
        var newAttachment = new Attachment(attachment.Name, attachment.Comment);
        newWorkItem.Attachments.Add(newAttachment);
    }
    }

    private static void LinkRelatedItems(WorkItemStore targetWorkItemStore, WorkItem sourceWorkItem)
    {
    int newId = WorkItemIdMap[sourceWorkItem.Id];
    WorkItem targetItem = targetWorkItemStore.GetWorkItem(newId);
    foreach (Link l in sourceWorkItem.Links)
    {
        if (l is RelatedLink)
        {
        RelatedLink sl = l as RelatedLink;
        switch (sl.ArtifactLinkType.Name)
        {
            case "Related Workitem":
            {
                if (WorkItemIdMap.ContainsKey(sl.RelatedWorkItemId))
                {
                int RelatedWorkItemId = WorkItemIdMap[sl.RelatedWorkItemId];
                RelatedLink rl = new RelatedLink(sl.LinkTypeEnd, RelatedWorkItemId);
                // !!!!
                // this does not work - need to check the existing links to see if one exists already for the linked workitem.  
                // using contains expects the same object and that's not what I'm doing here!!!!!!
                //if (!targetItem.Links.Contains(rl))
                // !!!!
                var query = from RelatedLink qrl in targetItem.Links where qrl.RelatedWorkItemId == RelatedWorkItemId select qrl;
                if (query.Count() == 0)
                {
                    targetItem.Links.Add(rl); ;
                    // Validate before save
                    if (!targetItem.IsValid())
                    {
                    var reasons = targetItem.Validate();
                    Console.WriteLine(string.Format("Could not validate work item (old id: {0}) related link id {1}.", sourceWorkItem.Id, sl.RelatedWorkItemId));
                    foreach (Field reason in reasons)
                    {
                        Console.WriteLine("Field: " + reason.Name + ", Status: " + reason.Status + ", Value: " + reason.Value);
                    }
                    }
                    else
                    {
                    try
                    {
                        targetItem.Save(SaveFlags.None);
                        Console.ForegroundColor = ConsoleColor.Cyan;
                        Console.WriteLine(string.Format(" [Updated: {0}]", targetItem.Id));
                        Console.ResetColor();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                        throw;
                    }
                    }

                }
                }
                break;
            }
            default:
            { break; }
        }

        }
    }
    }

    private static void EnsureThatStructureExists(string projectName, string structureType, string structurePath)
    {
    var parts = structurePath.Split('\\');

    var css = GetCommonStructureService();
    var projectInfo = css.GetProjectFromName(projectName);
    var parentNodeUri = GetCssStructure(GetCommonStructureService(), projectInfo.Uri, structureType).Uri;
    var currentPath = FormatPath(projectName, structureType == Areas ? "Area" : "Iteration");
    foreach (var part in parts)
    {
        currentPath = FormatPath(currentPath, part);
        Console.Write(currentPath);

        try
        {
        var currentNode = css.GetNodeFromPath(currentPath);
        parentNodeUri = currentNode.Uri;
        Console.ForegroundColor = ConsoleColor.DarkGreen;
        Console.WriteLine(" [found]");
        }
        catch
        {
        parentNodeUri = css.CreateNode(part, parentNodeUri);
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(" [created]");
        }
        Console.ResetColor();
    }
    }

    private static string FormatPath(string currentPath, string part)
    {
    part = part.Substring(part.IndexOf("\\") + 1);
    currentPath = string.Format(@"{0}\{1}", currentPath, part);
    return currentPath;
    }

    private static TfsTeamProjectCollection GetProjectCollection(Uri uri)
    {
    TfsTeamProjectCollection collection;
    if (!Collections.TryGetValue(uri, out collection))
    {
        collection = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(uri);
        collection.Connect(ConnectOptions.IncludeServices);
        collection.Authenticate();
        Collections.Add(uri, collection);
    }
    return Collections[uri];
    }

    private static WorkItemStore GetSourceWorkItemStore()
    {
    var collection = GetProjectCollection(SourceCollectionUri);
    return collection.GetService<WorkItemStore>();
    }

    private static WorkItemStore GetTargetWorkItemStore()
    {
    var collection = GetProjectCollection(TargetCollectionUri);
    return collection.GetService<WorkItemStore>();
    }

    public static NodeInfo GetCssStructure(ICommonStructureService css, String projectUri, String structureType)
    {
    return css.ListStructures(projectUri).FirstOrDefault(node => node.StructureType == structureType);
    }

    private static ICommonStructureService GetCommonStructureService()
    {
    var collection = GetProjectCollection(TargetCollectionUri);
    return collection.GetService<ICommonStructureService>();
    }
}
}
于 2011-06-01T20:01:38.033 回答