这是从TFS获取最新信息时跳过随机变更集的最有效方法吗?

时间:2017-01-27 13:49:34

标签: c# tfs

这是从TFS获取最新信息时跳过随机变更集的最有效方法吗?

我已经对这个主题进行了大量的研究,但尚未遇到解决方案。欢迎提出所有意见/建议。即使该建议是使用完全不同的解决方案(有效)。

我的第一次尝试是我过滤了变更集,然后通过它们循环发出workspace.get()。这非常缓慢,并没有得到正确的结果。它最终占用了45分钟的一个文件夹,我的最终解决方案最终需要3:30分钟才能获得相同的文件夹。而同一文件夹上的正常获取过程每次大约需要50秒。

此代码现在是测试代码,仅用于实现此功能,因此缺少异常处理和其他最佳实践等基本功能。它已经通过了我迄今为止所做的所有测试,但是它比正常情况要慢一点,但我没有看到让它更快的方法。

以下是我最终的结果:

您需要参考
assemblyref://Microsoft.TeamFoundation.Client&
assemblyref://Microsoft.TeamFoundation.Common&
assemblyref://Microsoft.TeamFoundation.VersionControl.Client&
assemblyref://Microsoft.TeamFoundation.VersionControl.Common&
assemblyref://Microsoft.VisualStudio.Services.Common

代码

using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Common;
using Microsoft.TeamFoundation.VersionControl.Client;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {

        //these would be the changesets to ignore
        var ignoreChangeSets = new List<int>()
        {
            1,10,50,900 // change these to ids you want to ignore. These are just random example numbers
        };


        // Replace with your setup
        var tfsServer = @"http://server_name:8080/TFS/";
        var serverPath = @"$/TFS_PATH_TO_FOLDER/";

        // Connect to server
        var tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(tfsServer));
        tfs.Connect(ConnectOptions.None);
        var vcs = tfs.GetService<VersionControlServer>();

        //get both sets so we can do a comparison of the final changes
        var folderName = "Foo";
        var sourceDir = $@"{Path.GetTempPath()}\{folderName}\";
        var targetDir = $@"{Path.GetTempPath()}\{folderName}-ChangeSets\";

        //download the entire source
        DownloadSource(vcs, serverPath, sourceDir);



        var changeSets = GetChangeSets(vcs, serverPath);
        //technically this query could be anything. As long as it filters the changesets out.....
        //you could filter by user, date, info in the changesets, anything really. up to you.
        var filteredChangeSets = from cs in changeSets
                                 where !ignoreChangeSets.Contains(cs.ChangesetId)
                                 select cs;

        if (changeSets.Count() == filteredChangeSets.Count())
        {
            // we did not filter anything so do a normal pull as it is faster
            //download the entire source
            DownloadSource(vcs, serverPath, targetDir); 
        }
        else
        {
            GetChangeSetsLatest(vcs, serverPath, filteredChangeSets, targetDir);
        }

    }
    private static void RecreateDir(string dir)
    {
        if (Directory.Exists(dir))
        {
            Directory.Delete(dir, true);
        }
        Directory.CreateDirectory(dir);
    }

    private static GetStatus DownloadSource(VersionControlServer vcs, string serverPath, string dir)
    {
        string wsName = "TempWorkSpace";
        Workspace ws = null;
        try
        {
            ws = vcs.GetWorkspace(wsName, Environment.UserName);
        }
        catch (WorkspaceNotFoundException)
        {
            ws = vcs.CreateWorkspace(wsName, Environment.UserName);
        }

        RecreateDir(dir);
        ws.Map(serverPath, dir);

        var getResponse = ws.Get(VersionSpec.Latest, GetOptions.GetAll | GetOptions.Overwrite);

        vcs.DeleteWorkspace(wsName, Environment.UserName);
        return getResponse;
    }

    private static IEnumerable<Changeset> GetChangeSets(VersionControlServer vcs, string serverPath)
    {
        VersionSpec versionFrom = null; // VersionSpec.ParseSingleSpec("C529", null);
        VersionSpec versionTo = VersionSpec.Latest;

        // Get Changesets
        var changesets = vcs.QueryHistory(
            serverPath,
            VersionSpec.Latest,
            0,
            RecursionType.Full,
            null,
            versionFrom,
            versionTo,
            Int32.MaxValue,
            true,
            false
            ).Cast<Changeset>();

        return changesets;
    }

    private static void GetChangeSetsLatest(VersionControlServer vcs, string serverPath, IEnumerable<Changeset> changesets, string dir)
    {
        //we are going to hold the latest item (file) in this dictionary, so we can do all our downloads at the end. The key will be the TFS server file path
        var items = new Dictionary<string, Item>();
        RecreateDir(dir);

        //we need the changesets ordered by changesetid. 
        var changesetsOrdered = changesets.OrderBy(c => c.ChangesetId);

        //DO NOT PARALLEL HERE. We need these changesets in EXACT order
        foreach (var changeset in changesetsOrdered)
        {
            foreach (var change in changeset?.Changes.Where(i => i.Item.ItemType == ItemType.File))
            {
                var itemPath = change.Item.ServerItem.Replace(serverPath, dir).Replace("/", "\\");
                if (change.ChangeType.HasFlag(ChangeType.Edit) && change.ChangeType.HasFlag(ChangeType.SourceRename))
                {
                    if (change.Item.DeletionId == 0)
                    { items.AddOrUpdate(change.Item.ServerItem, change.Item); }
                    else
                    { items.TryRemove(change.Item.ServerItem); }
                }
                else if (change.ChangeType.HasFlag(ChangeType.Delete) && change.ChangeType.HasFlag(ChangeType.SourceRename))
                {
                    var previousChange = GetPreviousServerChange(vcs, change.Item);
                    if (previousChange != null) { items.TryRemove(previousChange?.Item.ServerItem); }

                    if (change.Item.DeletionId == 0)
                    { items.AddOrUpdate(change.Item.ServerItem, change.Item); }
                    else
                    { items.TryRemove(change.Item.ServerItem); }
                }
                else if (change.ChangeType.HasFlag(ChangeType.Rollback) && change.ChangeType.HasFlag(ChangeType.Delete))
                {
                    items.TryRemove(change.Item.ServerItem);
                }
                else if (change.ChangeType.HasFlag(ChangeType.Rollback))
                {
                    var item = GetPreviousServerChange(vcs, change.Item)?.Item;
                    if (item != null) { items.AddOrUpdate(item.ServerItem, item); }
                }
                else if (change.ChangeType.HasFlag(ChangeType.Add) || change.ChangeType.HasFlag(ChangeType.Edit) || change.ChangeType.HasFlag(ChangeType.Rename))
                {
                    if (change.Item.DeletionId == 0) { items.AddOrUpdate(change.Item.ServerItem, change.Item); }
                }
                else if (change.ChangeType.HasFlag(ChangeType.Delete))
                {
                    items.TryRemove(change.Item.ServerItem);
                }
                else
                {
                    Console.ForegroundColor = ConsoleColor.Yellow;
                    Console.WriteLine($"Unknown change types: {change.ChangeType.ToString()}");
                    Console.ResetColor();
                }
            }
        }

        //HUGE penalty for switching to parallel, stick to single file at a time, one test went from 3:30 to 11:05. File system does not appreciate threading. :|
        //Parallel.ForEach(items, (i) =>
        foreach (var item in items)
        {
            var itemPath = item.Key.Replace(serverPath, dir).Replace("/", "\\");
            item.Value.DownloadFile(itemPath);
            Console.WriteLine(item.Value.ChangesetId + " - " + itemPath);
        };
    }

    //really not sure this is the right way to do this. works quite well, but it begs the question that surely there must be an easier way?
    private static Change GetPreviousServerChange(VersionControlServer vcs, Item item)
    {
        //get the changesets and reverse their order, so we can take the next one after it
        var changesets = GetChangeSets(vcs, item.ServerItem).OrderByDescending(cs => cs.ChangesetId);

        //skip until we find our changeset, then take the following changeset
        var previousChangeSet = changesets.SkipWhile(c => c.ChangesetId != item.ChangesetId).Skip(1).FirstOrDefault();

        //return the Change that matches the itemid (file id)
        return previousChangeSet?.Changes.FirstOrDefault(c => c.Item.ItemId == item.ItemId);
    }

}

static class Extensions
{
    public static void AddOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
    {
        if (dictionary.ContainsKey(key))
        {
            dictionary[key] = value;
        }
        else
        {
            dictionary.Add(key, value);
        }
    }
    public static void TryRemove<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
    {
        if (dictionary.ContainsKey(key))
        {
            dictionary.Remove(key);
        }
    }
}

编辑:由于现有的业务流程,我被迫加入此行列。多个团队或开发人员可以同时在同一个数据库上工作。每个团队或开发人员的更改都在RFC#下编目。每个RFC#都可以在任何不同的时间点拥有自己的发布计划和发布。我将使用Red Gate SQL Compare将文件夹与文件夹的所有内容(作为源)进行比较,减去RFC更改集(作为目标),以生成该RFC的更改脚本。

然后有这些规则:

  • RFC可以在不确定的时间段内停放。例如,我已经看到RFC停放在Staging中超过一年。其他RFC将在通往生产的途中通过此RFC。
  • 个人RFC可以在最后一刻从生产推送中退出。

我改变这些现有程序的机会是零。所以我不得不想办法解决它们。就是这样。我更愿意遵循正常的发布时间表,将所有更改推送到每个版本,然后一直流向生产。不幸的是,情况并非如此。

1 个答案:

答案 0 :(得分:0)

好像你需要一个Release分支和一个Dev分支。然后,在完成工作后,在Dev分支中完成每个签到。每当RFC被批准发布时,您都会将该特定变更集从Dev合并到Release,然后执行您发布所需的任何步骤。

根据开发和发布的复杂程度,您可能需要更多分支,但我建议尽可能少分支,因为它往往变得非常复杂。