【问题标题】:How to find top 10 files changed most frequently in a period from TFS 2015 Version Control?如何查找 TFS 2015 版本控制期间更改最频繁的前 10 个文件?
【发布时间】:2023-03-30 02:29:01
【问题描述】:

我的团队使用 TFS 2015 作为 ALM 和版本控制系统,我想分析哪些文件更改最频繁。

我发现 TFS 没有开箱即用的这个功能,但是 TFS2015 有一个 REST API 来查询文件的变更集,如下所示:

http://{instance}/tfs/DefaultCollection/_apis/tfvc/changesets?searchCriteria.itemPath={filePath}&api-version=1.0

我的Project Repository里有上千个文件,一个一个去查询不是个好主意,有没有更好的办法解决这个问题?

【问题讨论】:

    标签: version-control tfs-2015


    【解决方案1】:

    我认为您的问题没有事实上的开箱即用解决方案,我尝试了两种不同的方法来解决您的问题,我最初专注于REST API,但后来切换到SOAP API 看看它支持哪些功能。

    在下面的所有选项中,以下 api 就足够了:

    安装客户端APIlink @NuGet

    Install-Package Microsoft.TeamFoundationServer.ExtendedClient -Version 14.89.0 or later
    

    在所有选项中都需要以下扩展方法ref

        public static class StringExtensions
       {
           public static bool ContainsAny(this string source, List<string> lookFor)
           {
               if (!string.IsNullOrEmpty(source) && lookFor.Count > 0)
               {
                   return lookFor.Any(source.Contains);
               }
               return false;
           }
       }
    

    选项 1:SOAP API

    对于 SOAP API,没有明确要求使用 maxCount 参数限制查询结果的数量,如 QueryHistory 方法的 IntelliSense 文档摘录中所述:

    maxCount:该参数允许调用者限制数量 结果返回。 QueryHistory 从服务器返回的页面结果 需求,所以限制自己消费返回的IEnumerablealmost as effective (from a performance perspective) 作为提供 这里的固定值。为此参数提供的最常见值 是Int32.MaxValue

    根据maxCount 文档,我决定为我的源代码控制系统中的每个产品提取统计信息,因为查看代码库中每个系统的代码通量可能非常重要,独立于而不是将整个代码库中可能包含数百个系统的文件限制为 10 个。

    C# REST and SOAP (ExtendedClient) api reference

    安装 SOAP API 客户端link @NuGet

    Install-Package Microsoft.TeamFoundationServer.ExtendedClient -Version 14.95.2
    

    限制条件是:仅扫描源中的特定路径 控制,因为源代码控制中的某些系统较旧,并且可能仅出于历史目的而存在。

    1. 仅包含某些文件扩展名,例如 .cs、.js
    2. 排除某些文件名,例如 AssemblyInfo.cs。
    3. 为每个路径提取的项目: 10
    4. 开始日期: 120 天前
    5. 迄今为止:今天
    6. 排除特定路径,例如包含发布分支的文件夹或 归档分支
    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.VersionControl.Client;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    
    public void GetTopChangedFilesSoapApi()
        {
            var tfsUrl = "https://<SERVERNAME>/tfs/<COLLECTION>";
            var domain = "<DOMAIN>";
            var password = "<PASSWORD>";
            var userName = "<USERNAME>";
    
            //Only interested in specific systems so will scan only these
            var directoriesToScan = new List<string> {
                "$/projectdir/subdir/subdir/subdirA/systemnameA",
                "$/projectdir/subdir/subdir/subdirB/systemnameB",
                "$/projectdir/subdir/subdir/subdirC/systemnameC",
                "$/projectdir/subdir/subdir/subdirD/systemnameD"
                };
    
            var maxResultsPerPath = 10;
            var fromDate = DateTime.Now.AddDays(-120);
            var toDate = DateTime.Now;
    
            var fileExtensionToInclude = new List<string> { ".cs", ".js" };
            var extensionExclusions = new List<string> { ".csproj", ".json", ".css" };
            var fileExclusions = new List<string> { "AssemblyInfo.cs", "jquery-1.12.3.min.js", "config.js" };
            var pathExclusions = new List<string> {
                "/subdirToForceExclude1/",
                "/subdirToForceExclude2/",
                "/subdirToForceExclude3/",
            };
    
            using (var collection = new TfsTeamProjectCollection(new Uri(tfsUrl), 
                new NetworkCredential(userName: userName, password: password, domain: domain)))
            {
                collection.EnsureAuthenticated();
    
                var tfvc = collection.GetService(typeof(VersionControlServer)) as VersionControlServer;
    
                foreach (var rootDirectory in directoriesToScan)
                {
                    //Get changesets
                    //Note: maxcount set to maxvalue since impact to server is minimized by linq query below
                    var changeSets = tfvc.QueryHistory(path: rootDirectory, version: VersionSpec.Latest,
                        deletionId: 0, recursion: RecursionType.Full, user: null,
                        versionFrom: new DateVersionSpec(fromDate), versionTo: new DateVersionSpec(toDate),
                        maxCount: int.MaxValue, includeChanges: true,
                        includeDownloadInfo: false, slotMode: true)
                        as IEnumerable<Changeset>;
    
                    //Filter changes contained in changesets
                    var changes = changeSets.SelectMany(a => a.Changes)
                    .Where(a => a.ChangeType != ChangeType.Lock || a.ChangeType != ChangeType.Delete || a.ChangeType != ChangeType.Property)
                    .Where(e => !e.Item.ServerItem.ContainsAny(pathExclusions))
                    .Where(e => !e.Item.ServerItem.Substring(e.Item.ServerItem.LastIndexOf('/') + 1).ContainsAny(fileExclusions))
                    .Where(e => !e.Item.ServerItem.Substring(e.Item.ServerItem.LastIndexOf('.')).ContainsAny(extensionExclusions))
                    .Where(e => e.Item.ServerItem.Substring(e.Item.ServerItem.LastIndexOf('.')).ContainsAny(fileExtensionToInclude))
                    .GroupBy(g => g.Item.ServerItem)
                    .Select(d => new { File=d.Key, Count=d.Count()})
                    .OrderByDescending(o => o.Count)
                    .Take(maxResultsPerPath);
    
                    //Write top items for each path to the console
                    Console.WriteLine(rootDirectory); Console.WriteLine("->");
                    foreach (var change in changes)
                    {
                        Console.WriteLine("ChangeCount: {0} : File: {1}", change.Count, change.File);
                    }
                    Console.WriteLine(Environment.NewLine);
                }
            }
        }
    

    选项 2A:REST API

    (!! OP 发现的问题导致在 api 的 v.xxx-14.95.4 中发现一个严重缺陷) - 选项 2B 是解决方法

    在 v.xxx 到 14.95.4 的 api 中发现的缺陷: TfvcChangesetSearchCriteria 类型包含一个 ItemPath 属性 这应该将搜索限制在指定的目录。这 该属性的默认值为$/,不幸的是在使用时 GetChangesetsAsync 将始终使用 tfvc 源存储库的根路径,而与设置的值无关。

    也就是说,如果要修复缺陷,这仍然是一个合理的方法。

    限制对 scm 系统的影响的一种方法是使用 TfvcHttpClient 类型中 GetChangesetsAsync 成员的 TfvcChangesetSearchCriteria 类型参数为查询指定限制条件。

    您不需要单独检查 scm 系统/项目中的每个文件,检查指定时间段的变更集可能就足够了。并非我在下面使用的所有限制值都是TfvcChangesetSearchCriteria 类型的属性,所以我写了一个简短的例子来展示我将如何做到这一点,即 您可以指定最初要考虑的最大变更集数量以及要查看的特定项目。

    注意:TheTfvcChangesetSearchCriteria 类型包含一些您可能要考虑使用的附加属性。

    在下面的示例中,我在 C# 客户端中使用了 REST API 并从 tfvc 获取结果。
    如果您打算使用不同的客户端语言并直接调用 REST 服务,例如JavaScript;下面的逻辑应该仍然给你一些指示。

    //targeted framework for example: 4.5.2
    using Microsoft.TeamFoundation.SourceControl.WebApi;
    using Microsoft.VisualStudio.Services.Client;
    using Microsoft.VisualStudio.Services.Common;
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Threading.Tasks;
    
    public async Task GetTopChangedFilesUsingRestApi()
        {
            var tfsUrl = "https://<SERVERNAME>/tfs/<COLLECTION>";
            var domain = "<DOMAIN>";
            var password = "<PASSWORD>";
            var userName = "<USERNAME>";
    
            //Criteria used to limit results
            var directoriesToScan = new List<string> {
                "$/projectdir/subdir/subdir/subdirA/systemnameA",
                "$/projectdir/subdir/subdir/subdirB/systemnameB",
                "$/projectdir/subdir/subdir/subdirC/systemnameC",
                "$/projectdir/subdir/subdir/subdirD/systemnameD"
            };
    
            var maxResultsPerPath = 10;
            var fromDate = DateTime.Now.AddDays(-120);
            var toDate = DateTime.Now;
    
            var fileExtensionToInclude = new List<string> { ".cs", ".js" };
            var folderPathsToInclude = new List<string> { "/subdirToForceInclude/" };
            var extensionExclusions = new List<string> { ".csproj", ".json", ".css" };
            var fileExclusions = new List<string> { "AssemblyInfo.cs", "jquery-1.12.3.min.js", "config.js" };
            var pathExclusions = new List<string> {
                "/subdirToForceExclude1/",
                "/subdirToForceExclude2/",
                "/subdirToForceExclude3/",
            };
    
            //Establish connection
            VssConnection connection = new VssConnection(new Uri(tfsUrl),
                new VssCredentials(new Microsoft.VisualStudio.Services.Common.WindowsCredential(new NetworkCredential(userName, password, domain))));
    
            //Get tfvc client
            var tfvcClient = await connection.GetClientAsync<TfvcHttpClient>();
    
            foreach (var rootDirectory in directoriesToScan)
            {
                //Set up date-range criteria for query
                var criteria = new TfvcChangesetSearchCriteria();
                criteria.FromDate = fromDate.ToShortDateString();
                criteria.ToDate = toDate.ToShortDateString();
                criteria.ItemPath = rootDirectory;
    
                //get change sets
                var changeSets = await tfvcClient.GetChangesetsAsync(
                    maxChangeCount: int.MaxValue,
                    includeDetails: false,
                    includeWorkItems: false,
                    searchCriteria: criteria);
    
                if (changeSets.Any())
                {
                    var sample = new List<TfvcChange>();
    
                    Parallel.ForEach(changeSets, changeSet =>
                    {
                        sample.AddRange(tfvcClient.GetChangesetChangesAsync(changeSet.ChangesetId).Result);
                    });
    
                    //Filter changes contained in changesets
                    var changes = sample.Where(a => a.ChangeType != VersionControlChangeType.Lock || a.ChangeType != VersionControlChangeType.Delete || a.ChangeType != VersionControlChangeType.Property)
                    .Where(e => e.Item.Path.ContainsAny(folderPathsToInclude))
                    .Where(e => !e.Item.Path.ContainsAny(pathExclusions))
                    .Where(e => !e.Item.Path.Substring(e.Item.Path.LastIndexOf('/') + 1).ContainsAny(fileExclusions))
                    .Where(e => !e.Item.Path.Substring(e.Item.Path.LastIndexOf('.')).ContainsAny(extensionExclusions))
                    .Where(e => e.Item.Path.Substring(e.Item.Path.LastIndexOf('.')).ContainsAny(fileExtensionToInclude))
                    .GroupBy(g => g.Item.Path)
                    .Select(d => new { File = d.Key, Count = d.Count() })
                    .OrderByDescending(o => o.Count)
                    .Take(maxResultsPerPath);
    
                    //Write top items for each path to the console
                    Console.WriteLine(rootDirectory); Console.WriteLine("->");
                    foreach (var change in changes)
                    {
                        Console.WriteLine("ChangeCount: {0} : File: {1}", change.Count, change.File);
                    }
                    Console.WriteLine(Environment.NewLine);
                }
            }
        }
    

    选项 2B

    注意:此解决方案与 OPTION 2A 非常相似,但在撰写本文时实施了一种解决方法以修复 REST 客户端 API 库中的限制。 简要总结 - 此示例没有调用客户端 api 库来获取变更集,而是使用直接向 REST API 的 Web 请求来获取变更集,因此需要定义其他类型来处理来自服务的响应。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Threading.Tasks;
    
    using Microsoft.TeamFoundation.SourceControl.WebApi;
    using Microsoft.VisualStudio.Services.Client;
    using Microsoft.VisualStudio.Services.Common;
    
    using System.Text;
    using System.IO;
    using Newtonsoft.Json;
    
    public async Task GetTopChangedFilesUsingDirectWebRestApiSO()
        {
            var tfsUrl = "https://<SERVERNAME>/tfs/<COLLECTION>";
            var domain = "<DOMAIN>";
            var password = "<PASSWORD>";
            var userName = "<USERNAME>";
    
            var changesetsUrl = "{0}/_apis/tfvc/changesets?searchCriteria.itemPath={1}&searchCriteria.fromDate={2}&searchCriteria.toDate={3}&$top={4}&api-version=1.0";
    
            //Criteria used to limit results
            var directoriesToScan = new List<string> {
                "$/projectdir/subdir/subdir/subdirA/systemnameA",
                "$/projectdir/subdir/subdir/subdirB/systemnameB",
                "$/projectdir/subdir/subdir/subdirC/systemnameC",
                "$/projectdir/subdir/subdir/subdirD/systemnameD"
            };
    
            var maxResultsPerPath = 10;
            var fromDate = DateTime.Now.AddDays(-120);
            var toDate = DateTime.Now;
    
            var fileExtensionToInclude = new List<string> { ".cs", ".js" };
            var folderPathsToInclude = new List<string> { "/subdirToForceInclude/" };
            var extensionExclusions = new List<string> { ".csproj", ".json", ".css" };
            var fileExclusions = new List<string> { "AssemblyInfo.cs", "jquery-1.12.3.min.js", "config.js" };
            var pathExclusions = new List<string> {
                "/subdirToForceExclude1/",
                "/subdirToForceExclude2/",
                "/subdirToForceExclude3/",
            };
    
            //Get tfvc client
            //Establish connection
            VssConnection connection = new VssConnection(new Uri(tfsUrl),
                new VssCredentials(new Microsoft.VisualStudio.Services.Common.WindowsCredential(new NetworkCredential(userName, password, domain))));
    
            //Get tfvc client
            var tfvcClient = await connection.GetClientAsync<TfvcHttpClient>();
    
            foreach (var rootDirectory in directoriesToScan)
            {
                var changeSets = Invoke<GetChangeSetsResponse>("GET", string.Format(changesetsUrl, tfsUrl, rootDirectory,fromDate,toDate,maxResultsPerPath), userName, password, domain).value;
    
                if (changeSets.Any())
                {
                    //Get changes
                    var sample = new List<TfvcChange>();
                    foreach (var changeSet in changeSets)
                    {
                        sample.AddRange(tfvcClient.GetChangesetChangesAsync(changeSet.changesetId).Result);
                    }
    
                    //Filter changes
                    var changes = sample.Where(a => a.ChangeType != VersionControlChangeType.Lock || a.ChangeType != VersionControlChangeType.Delete || a.ChangeType != VersionControlChangeType.Property)
                    .Where(e => e.Item.Path.ContainsAny(folderPathsToInclude))
                    .Where(e => !e.Item.Path.ContainsAny(pathExclusions))
                    .Where(e => !e.Item.Path.Substring(e.Item.Path.LastIndexOf('/') + 1).ContainsAny(fileExclusions))
                    .Where(e => !e.Item.Path.Substring(e.Item.Path.LastIndexOf('.')).ContainsAny(extensionExclusions))
                    .Where(e => e.Item.Path.Substring(e.Item.Path.LastIndexOf('.')).ContainsAny(fileExtensionToInclude))
                    .GroupBy(g => g.Item.Path)
                    .Select(d => new { File = d.Key, Count = d.Count() })
                    .OrderByDescending(o => o.Count)
                    .Take(maxResultsPerPath);
    
                    //Write top items for each path to the console
                    Console.WriteLine(rootDirectory); Console.WriteLine("->");
                    foreach (var change in changes)
                    {
                        Console.WriteLine("ChangeCount: {0} : File: {1}", change.Count, change.File);
                    }
                    Console.WriteLine(Environment.NewLine);
                }
            }
        }
    
        private T Invoke<T>(string method, string url, string userName, string password, string domain)
        {
            var request = WebRequest.Create(url);
            var httpRequest = request as HttpWebRequest;
            if (httpRequest != null) httpRequest.UserAgent = "versionhistoryApp";
            request.ContentType = "application/json";
            request.Method = method;
    
            request.Credentials = new NetworkCredential(userName, password, domain); //ntlm 401 challenge support
            request.Headers[HttpRequestHeader.Authorization] = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(domain+"\\"+userName + ":" + password)); //basic auth support if enabled on tfs instance
    
            try
            {
                using (var response = request.GetResponse())
                using (var responseStream = response.GetResponseStream())
                using (var reader = new StreamReader(responseStream))
                {
                    string s = reader.ReadToEnd();
                    return Deserialize<T>(s);
                }
            }
            catch (WebException ex)
            {
                if (ex.Response == null)
                    throw;
    
                using (var responseStream = ex.Response.GetResponseStream())
                {
                    string message;
                    try
                    {
                        message = new StreamReader(responseStream).ReadToEnd();
                    }
                    catch
                    {
                        throw ex;
                    }
    
                    throw new Exception(message, ex);
                }
            }
        }
    
        public class GetChangeSetsResponse
        {
            public IEnumerable<Changeset> value { get; set; }
            public class Changeset
            {
                public int changesetId { get; set; }
                public string url { get; set; }
                public DateTime createdDate { get; set; }
                public string comment { get; set; }
            }
        }
    
        public static T Deserialize<T>(string json)
        {
            T data = JsonConvert.DeserializeObject<T>(json);
            return data;
        }
    }
    

    其他参考:

    C# REST and SOAP (ExtendedClient) api reference

    REST API: tfvc Changesets

    TfvcChangesetSearchCriteria type @MSDN

    【讨论】:

    • 感谢您的帮助。但是我发现GetChangesetChangesAsync方法一次调用只能检索100条变更集记录,maxChangesToConsider参数不起作用。即使使用skip参数进行分页查询,也只能返回250条记录。我没有找到任何关于这个限制的设置或api。
    • @Allen:为了适应您发现的问题,我不得不彻底修改我的答案。有趣的是,您的问题导致了一个严重问题的识别,该问题迫使 GetChangesetsAsync 扫描 $/ ,然后导致您描述的症状。查看更新的答案以获取更多信息。
    猜你喜欢
    • 2018-10-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-30
    • 2017-02-12
    • 2015-12-02
    相关资源
    最近更新 更多