From ed25350a65e51bf90b9e6801efb8f5321832ae4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Mon, 18 Nov 2024 16:51:16 +0100 Subject: [PATCH 1/4] Consolidate MigrateCapacities method into one --- .../Processors/TfsTeamSettingsCore.cs | 132 ++++++++++++++++++ .../Processors/TfsTeamSettingsProcessor.cs | 115 ++------------- .../Tools/TfsTeamSettingsTool.cs | 123 ++-------------- 3 files changed, 158 insertions(+), 212 deletions(-) create mode 100644 src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs new file mode 100644 index 000000000..4f7a4b282 --- /dev/null +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.TeamFoundation.Client; +using Microsoft.TeamFoundation.Core.WebApi.Types; +using Microsoft.TeamFoundation.Framework.Client; +using Microsoft.TeamFoundation.Work.WebApi; +using Microsoft.VisualStudio.Services.WebApi; + +namespace MigrationTools.Processors +{ + internal static class TfsTeamSettingsCore + { + internal static void MigrateCapacities( + WorkHttpClient sourceHttpClient, + Guid sourceProjectId, + TeamFoundationTeam sourceTeam, + WorkHttpClient targetHttpClient, + Guid targetProjectId, + TeamFoundationTeam targetTeam, + Dictionary iterationMap, + Lazy> identityCache, + ITelemetryLogger telemetry, + ILogger log, + LogLevel exceptionLogLevel) + { + log.LogInformation("Migrating team capacities.."); + + try + { + var sourceTeamContext = new TeamContext(sourceProjectId, sourceTeam.Identity.TeamFoundationId); + var sourceIterations = sourceHttpClient.GetTeamIterationsAsync(sourceTeamContext).ConfigureAwait(false).GetAwaiter().GetResult(); + + var targetTeamContext = new TeamContext(targetProjectId, targetTeam.Identity.TeamFoundationId); + var targetIterations = targetHttpClient.GetTeamIterationsAsync(targetTeamContext).ConfigureAwait(false).GetAwaiter().GetResult(); + + foreach (var sourceIteration in sourceIterations) + { + try + { + var targetIterationPath = iterationMap[sourceIteration.Path]; + var targetIteration = targetIterations.FirstOrDefault(i => i.Path == targetIterationPath); + if (targetIteration == null) continue; + + var targetCapacities = new List(); + var sourceCapacities = sourceHttpClient.GetCapacitiesWithIdentityRefAsync(sourceTeamContext, sourceIteration.Id).ConfigureAwait(false).GetAwaiter().GetResult(); + foreach (var sourceCapacity in sourceCapacities) + { + var sourceDisplayName = sourceCapacity.TeamMember.DisplayName; + var index = sourceDisplayName.IndexOf("<"); + if (index > 0) + { + sourceDisplayName = sourceDisplayName.Substring(0, index).Trim(); + } + + // Match: + // "Doe, John" to "Doe, John" + // "John Doe" to "John Doe" + var targetTeamFoundatationIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); + if (targetTeamFoundatationIdentity == null) + { + if (sourceDisplayName.Contains(", ")) + { + // Match: + // "Doe, John" to "John Doe" + var splitName = sourceDisplayName.Split(','); + sourceDisplayName = $"{splitName[1].Trim()} {splitName[0].Trim()}"; + targetTeamFoundatationIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); + } + else + { + if (sourceDisplayName.Contains(' ')) + { + // Match: + // "John Doe" to "Doe, John" + var splitName = sourceDisplayName.Split(' '); + sourceDisplayName = $"{splitName[1].Trim()}, {splitName[0].Trim()}"; + targetTeamFoundatationIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); + } + } + + // last attempt to match on unique name + // Match: "John Michael Bolden" to Bolden, "John Michael" on "john.m.bolden@example.com" unique name + if (targetTeamFoundatationIdentity == null) + { + var sourceUniqueName = sourceCapacity.TeamMember.UniqueName; + targetTeamFoundatationIdentity = identityCache.Value.FirstOrDefault(i => i.UniqueName == sourceUniqueName); + } + } + + if (targetTeamFoundatationIdentity != null) + { + targetCapacities.Add(new TeamMemberCapacityIdentityRef + { + Activities = sourceCapacity.Activities, + DaysOff = sourceCapacity.DaysOff, + TeamMember = new IdentityRef + { + Id = targetTeamFoundatationIdentity.TeamFoundationId.ToString() + } + }); + } + else + { + log.LogWarning("[SKIP] Team Member {member} was not found on target when replacing capacities on iteration {iteration}.", sourceCapacity.TeamMember.DisplayName, targetIteration.Path); + } + } + + if (targetCapacities.Count > 0) + { + targetHttpClient.ReplaceCapacitiesWithIdentityRefAsync(targetCapacities, targetTeamContext, targetIteration.Id).ConfigureAwait(false).GetAwaiter().GetResult(); + log.LogDebug("Team {team} capacities for iteration {iteration} migrated.", targetTeam.Name, targetIteration.Path); + } + } + catch (Exception ex) + { + telemetry.TrackException(ex, null); + log.Log(exceptionLogLevel, ex, "[SKIP] Problem migrating team capacities for iteration {iteration}.", sourceIteration.Path); + } + } + } + catch (Exception ex) + { + telemetry.TrackException(ex, null); + log.Log(exceptionLogLevel, ex, "[SKIP] Problem migrating team capacities."); + } + + log.LogInformation("Team capacities migration done.."); + } + } +} diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs index b742c5f23..a7008ec46 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs @@ -257,112 +257,23 @@ internal static string SwitchProjectName(string expressionString, string sourceP return string.Empty; } - private void MigrateCapacities(WorkHttpClient sourceHttpClient, WorkHttpClient targetHttpClient, TeamFoundationTeam sourceTeam, TeamFoundationTeam targetTeam, Dictionary iterationMap) + private void MigrateCapacities( + WorkHttpClient sourceHttpClient, + WorkHttpClient targetHttpClient, + TeamFoundationTeam sourceTeam, + TeamFoundationTeam targetTeam, + Dictionary iterationMap) { - if (!Options.MigrateTeamCapacities) return; - - Log.LogInformation("Migrating team capacities.."); - try - { - var sourceTeamContext = new TeamContext(Source.TfsProject.Guid, sourceTeam.Identity.TeamFoundationId); - var sourceIterations = sourceHttpClient.GetTeamIterationsAsync(sourceTeamContext).ConfigureAwait(false).GetAwaiter().GetResult(); - - var targetTeamContext = new TeamContext(Target.TfsProject.Guid, targetTeam.Identity.TeamFoundationId); - var targetIterations = targetHttpClient.GetTeamIterationsAsync(targetTeamContext).ConfigureAwait(false).GetAwaiter().GetResult(); - - foreach (var sourceIteration in sourceIterations) - { - try - { - var targetIterationPath = iterationMap[sourceIteration.Path]; - var targetIteration = targetIterations.FirstOrDefault(i => i.Path == targetIterationPath); - if (targetIteration == null) continue; - - var targetCapacities = new List(); - var sourceCapacities = sourceHttpClient.GetCapacitiesWithIdentityRefAsync(sourceTeamContext, sourceIteration.Id).ConfigureAwait(false).GetAwaiter().GetResult(); - foreach (var sourceCapacity in sourceCapacities) - { - var sourceDisplayName = sourceCapacity.TeamMember.DisplayName; - var index = sourceDisplayName.IndexOf("<"); - if (index > 0) - { - sourceDisplayName = sourceDisplayName.Substring(0, index).Trim(); - } - - // Match: - // "Doe, John" to "Doe, John" - // "John Doe" to "John Doe" - var targetTeamFoundatationIdentity = _targetTeamFoundationIdentitiesLazyCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); - if (targetTeamFoundatationIdentity == null) - { - if (sourceDisplayName.Contains(", ")) - { - // Match: - // "Doe, John" to "John Doe" - var splitName = sourceDisplayName.Split(','); - sourceDisplayName = $"{splitName[1].Trim()} {splitName[0].Trim()}"; - targetTeamFoundatationIdentity = _targetTeamFoundationIdentitiesLazyCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); - } - else - { - if (sourceDisplayName.Contains(' ')) - { - // Match: - // "John Doe" to "Doe, John" - var splitName = sourceDisplayName.Split(' '); - sourceDisplayName = $"{splitName[1].Trim()}, {splitName[0].Trim()}"; - targetTeamFoundatationIdentity = _targetTeamFoundationIdentitiesLazyCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); - } - } - - // last attempt to match on unique name - // Match: "John Michael Bolden" to Bolden, "John Michael" on "john.m.bolden@example.com" unique name - if (targetTeamFoundatationIdentity == null) - { - var sourceUniqueName = sourceCapacity.TeamMember.UniqueName; - targetTeamFoundatationIdentity = _targetTeamFoundationIdentitiesLazyCache.Value.FirstOrDefault(i => i.UniqueName == sourceUniqueName); - } - } - - if (targetTeamFoundatationIdentity != null) - { - targetCapacities.Add(new TeamMemberCapacityIdentityRef - { - Activities = sourceCapacity.Activities, - DaysOff = sourceCapacity.DaysOff, - TeamMember = new IdentityRef - { - Id = targetTeamFoundatationIdentity.TeamFoundationId.ToString() - } - }); - } - else - { - Log.LogWarning("[SKIP] Team Member {member} was not found on target when replacing capacities on iteration {iteration}.", sourceCapacity.TeamMember.DisplayName, targetIteration.Path); - } - } - - if (targetCapacities.Count > 0) - { - targetHttpClient.ReplaceCapacitiesWithIdentityRefAsync(targetCapacities, targetTeamContext, targetIteration.Id).ConfigureAwait(false).GetAwaiter().GetResult(); - Log.LogDebug("Team {team} capacities for iteration {iteration} migrated.", targetTeam.Name, targetIteration.Path); - } - } - catch (Exception ex) - { - Telemetry.TrackException(ex, null); - Log.LogWarning(ex, "[SKIP] Problem migrating team capacities for iteration {iteration}.", sourceIteration.Path); - } - - } - } - catch (Exception ex) + if (!Options.MigrateTeamCapacities) { - Telemetry.TrackException(ex, null); - Log.LogWarning(ex, "[SKIP] Problem migrating team capacities."); + return; } - Log.LogInformation("Team capacities migration done.."); + TfsTeamSettingsCore.MigrateCapacities( + sourceHttpClient, Source.TfsProject.Guid, sourceTeam, + targetHttpClient, Target.TfsProject.Guid, targetTeam, + iterationMap, _targetTeamFoundationIdentitiesLazyCache, + Telemetry, Log, exceptionLogLevel: LogLevel.Warning); } } } diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs index 0f7c1aab8..3185966c4 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -47,7 +47,7 @@ public class TfsTeamSettingsTool : Tool public TfsTeamSettingsTool(IOptions options, IServiceProvider services, ILogger logger, ITelemetryLogger telemetryLogger) : base(options, services, logger, telemetryLogger) { - + } @@ -218,118 +218,21 @@ private void MigrateTeamSettings() Log.LogDebug("DONE in {Elapsed} ", stopwatch.Elapsed.ToString("c")); } - private void MigrateCapacities(TeamFoundationTeam sourceTeam, TeamFoundationTeam targetTeam, Dictionary iterationMap) + private void MigrateCapacities( + TeamFoundationTeam sourceTeam, + TeamFoundationTeam targetTeam, + Dictionary iterationMap) { - if (!Options.MigrateTeamCapacities) return; - - Log.LogInformation("Migrating team capacities.."); - - WorkHttpClient sourceHttpClient = _processor.Source.GetClient(); - WorkHttpClient targetHttpClient = _processor.Target.GetClient(); - - try + if (!Options.MigrateTeamCapacities) { - - var sourceTeamContext = new TeamContext(_processor.Source.WorkItems.Project.Guid, sourceTeam.Identity.TeamFoundationId); - var sourceIterations = sourceHttpClient.GetTeamIterationsAsync(sourceTeamContext).ConfigureAwait(false).GetAwaiter().GetResult(); - - var targetTeamContext = new TeamContext(_processor.Target.WorkItems.Project.Guid, targetTeam.Identity.TeamFoundationId); - var targetIterations = targetHttpClient.GetTeamIterationsAsync(targetTeamContext).ConfigureAwait(false).GetAwaiter().GetResult(); - - foreach (var sourceIteration in sourceIterations) - { - try - { - var targetIterationPath = iterationMap[sourceIteration.Path]; - var targetIteration = targetIterations.FirstOrDefault(i => i.Path == targetIterationPath); - if (targetIteration == null) continue; - - var targetCapacities = new List(); - var sourceCapacities = sourceHttpClient.GetCapacitiesWithIdentityRefAsync(sourceTeamContext, sourceIteration.Id).ConfigureAwait(false).GetAwaiter().GetResult(); - foreach (var sourceCapacity in sourceCapacities) - { - var sourceDisplayName = sourceCapacity.TeamMember.DisplayName; - var index = sourceDisplayName.IndexOf("<"); - if (index > 0) - { - sourceDisplayName = sourceDisplayName.Substring(0, index).Trim(); - } - - // Match: - // "Doe, John" to "Doe, John" - // "John Doe" to "John Doe" - var targetTeamFoundatationIdentity = _targetTeamFoundationIdentitiesLazyCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); - if (targetTeamFoundatationIdentity == null) - { - if (sourceDisplayName.Contains(", ")) - { - // Match: - // "Doe, John" to "John Doe" - var splitName = sourceDisplayName.Split(','); - sourceDisplayName = $"{splitName[1].Trim()} {splitName[0].Trim()}"; - targetTeamFoundatationIdentity = _targetTeamFoundationIdentitiesLazyCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); - } - else - { - if (sourceDisplayName.Contains(' ')) - { - // Match: - // "John Doe" to "Doe, John" - var splitName = sourceDisplayName.Split(' '); - sourceDisplayName = $"{splitName[1].Trim()}, {splitName[0].Trim()}"; - targetTeamFoundatationIdentity = _targetTeamFoundationIdentitiesLazyCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); - } - } - - // last attempt to match on unique name - // Match: "John Michael Bolden" to Bolden, "John Michael" on "john.m.bolden@example.com" unique name - if (targetTeamFoundatationIdentity == null) - { - var sourceUniqueName = sourceCapacity.TeamMember.UniqueName; - targetTeamFoundatationIdentity = _targetTeamFoundationIdentitiesLazyCache.Value.FirstOrDefault(i => i.UniqueName == sourceUniqueName); - } - } - - if (targetTeamFoundatationIdentity != null) - { - targetCapacities.Add(new TeamMemberCapacityIdentityRef - { - Activities = sourceCapacity.Activities, - DaysOff = sourceCapacity.DaysOff, - TeamMember = new IdentityRef - { - Id = targetTeamFoundatationIdentity.TeamFoundationId.ToString() - } - }); - } - else - { - Log.LogWarning("[SKIP] Team Member {member} was not found on target when replacing capacities on iteration {iteration}.", sourceCapacity.TeamMember.DisplayName, targetIteration.Path); - } - } - - if (targetCapacities.Count > 0) - { - targetHttpClient.ReplaceCapacitiesWithIdentityRefAsync(targetCapacities, targetTeamContext, targetIteration.Id).ConfigureAwait(false).GetAwaiter().GetResult(); - Log.LogDebug("Team {team} capacities for iteration {iteration} migrated.", targetTeam.Name, targetIteration.Path); - } - } - catch (Exception ex) - { - Telemetry.TrackException(ex, null); - Log.LogError(ex, "[SKIP] Problem migrating team capacities for iteration {iteration}.", sourceIteration.Path); - } - - } - } - catch (Exception ex) - { - Telemetry.TrackException(ex, null); - Log.LogError(ex, "[SKIP] Problem migrating team capacities."); + return; } - Log.LogInformation("Team capacities migration done.."); + TfsTeamSettingsCore.MigrateCapacities( + _processor.Source.GetClient(), _processor.Source.WorkItems.Project.Guid, sourceTeam, + _processor.Target.GetClient(), _processor.Target.WorkItems.Project.Guid, targetTeam, + iterationMap, _targetTeamFoundationIdentitiesLazyCache, + Telemetry, Log, exceptionLogLevel: LogLevel.Error); } - } } From 388232c7b672e68b5fbbcc7f03e0d64bc275474a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Sun, 24 Nov 2024 18:32:50 +0100 Subject: [PATCH 2/4] Split MigrateCapacities into several simpler methods --- .../Processors/TfsTeamSettingsCore.cs | 180 +++++++++++------- 1 file changed, 113 insertions(+), 67 deletions(-) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs index 4f7a4b282..bd285a496 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs @@ -30,10 +30,12 @@ internal static void MigrateCapacities( try { var sourceTeamContext = new TeamContext(sourceProjectId, sourceTeam.Identity.TeamFoundationId); - var sourceIterations = sourceHttpClient.GetTeamIterationsAsync(sourceTeamContext).ConfigureAwait(false).GetAwaiter().GetResult(); + var sourceIterations = sourceHttpClient.GetTeamIterationsAsync(sourceTeamContext) + .ConfigureAwait(false).GetAwaiter().GetResult(); var targetTeamContext = new TeamContext(targetProjectId, targetTeam.Identity.TeamFoundationId); - var targetIterations = targetHttpClient.GetTeamIterationsAsync(targetTeamContext).ConfigureAwait(false).GetAwaiter().GetResult(); + var targetIterations = targetHttpClient.GetTeamIterationsAsync(targetTeamContext) + .ConfigureAwait(false).GetAwaiter().GetResult(); foreach (var sourceIteration in sourceIterations) { @@ -43,74 +45,14 @@ internal static void MigrateCapacities( var targetIteration = targetIterations.FirstOrDefault(i => i.Path == targetIterationPath); if (targetIteration == null) continue; - var targetCapacities = new List(); - var sourceCapacities = sourceHttpClient.GetCapacitiesWithIdentityRefAsync(sourceTeamContext, sourceIteration.Id).ConfigureAwait(false).GetAwaiter().GetResult(); - foreach (var sourceCapacity in sourceCapacities) - { - var sourceDisplayName = sourceCapacity.TeamMember.DisplayName; - var index = sourceDisplayName.IndexOf("<"); - if (index > 0) - { - sourceDisplayName = sourceDisplayName.Substring(0, index).Trim(); - } - - // Match: - // "Doe, John" to "Doe, John" - // "John Doe" to "John Doe" - var targetTeamFoundatationIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); - if (targetTeamFoundatationIdentity == null) - { - if (sourceDisplayName.Contains(", ")) - { - // Match: - // "Doe, John" to "John Doe" - var splitName = sourceDisplayName.Split(','); - sourceDisplayName = $"{splitName[1].Trim()} {splitName[0].Trim()}"; - targetTeamFoundatationIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); - } - else - { - if (sourceDisplayName.Contains(' ')) - { - // Match: - // "John Doe" to "Doe, John" - var splitName = sourceDisplayName.Split(' '); - sourceDisplayName = $"{splitName[1].Trim()}, {splitName[0].Trim()}"; - targetTeamFoundatationIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); - } - } - - // last attempt to match on unique name - // Match: "John Michael Bolden" to Bolden, "John Michael" on "john.m.bolden@example.com" unique name - if (targetTeamFoundatationIdentity == null) - { - var sourceUniqueName = sourceCapacity.TeamMember.UniqueName; - targetTeamFoundatationIdentity = identityCache.Value.FirstOrDefault(i => i.UniqueName == sourceUniqueName); - } - } - - if (targetTeamFoundatationIdentity != null) - { - targetCapacities.Add(new TeamMemberCapacityIdentityRef - { - Activities = sourceCapacity.Activities, - DaysOff = sourceCapacity.DaysOff, - TeamMember = new IdentityRef - { - Id = targetTeamFoundatationIdentity.TeamFoundationId.ToString() - } - }); - } - else - { - log.LogWarning("[SKIP] Team Member {member} was not found on target when replacing capacities on iteration {iteration}.", sourceCapacity.TeamMember.DisplayName, targetIteration.Path); - } - } + List sourceCapacities = GetSourceCapacities( + sourceHttpClient, sourceTeamContext, sourceIteration); + List targetCapacities = GetTargetCapacities( + sourceCapacities, targetIteration.Path, identityCache, log); if (targetCapacities.Count > 0) { - targetHttpClient.ReplaceCapacitiesWithIdentityRefAsync(targetCapacities, targetTeamContext, targetIteration.Id).ConfigureAwait(false).GetAwaiter().GetResult(); - log.LogDebug("Team {team} capacities for iteration {iteration} migrated.", targetTeam.Name, targetIteration.Path); + ReplaceTargetCapacities(targetHttpClient, targetTeamContext, targetTeam, targetIteration, targetCapacities, log); } } catch (Exception ex) @@ -128,5 +70,109 @@ internal static void MigrateCapacities( log.LogInformation("Team capacities migration done.."); } + + private static List GetSourceCapacities( + WorkHttpClient sourceHttpClient, + TeamContext sourceTeamContext, + TeamSettingsIteration sourceIteration) + { + return sourceHttpClient.GetCapacitiesWithIdentityRefAsync(sourceTeamContext, sourceIteration.Id) + .ConfigureAwait(false).GetAwaiter().GetResult(); + } + + private static List GetTargetCapacities( + List sourceCapacities, + string targetIteration, + Lazy> identityCache, + ILogger log) + { + var targetCapacities = new List(); + foreach (var sourceCapacity in sourceCapacities) + { + if (TryMatchIdentity(sourceCapacity, identityCache, out TeamFoundationIdentity targetIdentity)) + { + targetCapacities.Add(new TeamMemberCapacityIdentityRef + { + Activities = sourceCapacity.Activities, + DaysOff = sourceCapacity.DaysOff, + TeamMember = new IdentityRef + { + Id = targetIdentity.TeamFoundationId.ToString() + } + }); + } + else + { + log.LogWarning("[SKIP] Team Member {member} was not found on target when replacing capacities " + + "on iteration {iteration}.", sourceCapacity.TeamMember.DisplayName, targetIteration); + } + } + + return targetCapacities; + } + + private static void ReplaceTargetCapacities( + WorkHttpClient targetHttpClient, + TeamContext targetTeamContext, + TeamFoundationTeam targetTeam, + TeamSettingsIteration targetIteration, + List targetCapacities, + ILogger log) + { + targetHttpClient.ReplaceCapacitiesWithIdentityRefAsync(targetCapacities, targetTeamContext, targetIteration.Id) + .ConfigureAwait(false).GetAwaiter().GetResult(); + log.LogDebug("Team {team} capacities for iteration {iteration} migrated.", targetTeam.Name, targetIteration.Path); + } + + private static bool TryMatchIdentity( + TeamMemberCapacityIdentityRef sourceCapacity, + Lazy> identityCache, + out TeamFoundationIdentity targetIdentity) + { + var sourceDisplayName = sourceCapacity.TeamMember.DisplayName; + var index = sourceDisplayName.IndexOf("<"); + if (index > 0) + { + sourceDisplayName = sourceDisplayName.Substring(0, index).Trim(); + } + + // Match: + // "Doe, John" to "Doe, John" + // "John Doe" to "John Doe" + targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); + if (targetIdentity == null) + { + if (sourceDisplayName.Contains(", ")) + { + // Match: + // "Doe, John" to "John Doe" + var splitName = sourceDisplayName.Split(','); + sourceDisplayName = $"{splitName[1].Trim()} {splitName[0].Trim()}"; + targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); + } + else + { + if (sourceDisplayName.Contains(' ')) + { + // Match: + // "John Doe" to "Doe, John" + var splitName = sourceDisplayName.Split(' '); + sourceDisplayName = $"{splitName[1].Trim()}, {splitName[0].Trim()}"; + targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); + } + } + + // last attempt to match on unique name + // Match: "John Michael Bolden" to Bolden, "John Michael" on "john.m.bolden@example.com" unique name + if (targetIdentity == null) + { + var sourceUniqueName = sourceCapacity.TeamMember.UniqueName; + targetIdentity = identityCache.Value.FirstOrDefault(i => i.UniqueName == sourceUniqueName); + } + } + + return targetIdentity is not null; + } } } + From 59cb2c76900d388d2992f63555ee879d170c8652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Sun, 24 Nov 2024 18:50:40 +0100 Subject: [PATCH 3/4] Use user mapping when migrating team capacities --- .../Processors/TfsTeamSettingsCore.cs | 80 +++++++++++++------ .../Processors/TfsTeamSettingsProcessor.cs | 4 +- .../TfsTeamSettingsProcessorOptions.cs | 12 ++- .../Tools/TfsTeamSettingsTool.cs | 6 +- .../Tools/TfsTeamSettingsToolOptions.cs | 19 +++-- 5 files changed, 78 insertions(+), 43 deletions(-) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs index bd285a496..027f7d66c 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.TeamFoundation.Client; using Microsoft.TeamFoundation.Core.WebApi.Types; using Microsoft.TeamFoundation.Framework.Client; using Microsoft.TeamFoundation.Work.WebApi; using Microsoft.VisualStudio.Services.WebApi; +using MigrationTools.Tools; namespace MigrationTools.Processors { @@ -21,9 +23,11 @@ internal static void MigrateCapacities( TeamFoundationTeam targetTeam, Dictionary iterationMap, Lazy> identityCache, + bool useUserMapping, ITelemetryLogger telemetry, ILogger log, - LogLevel exceptionLogLevel) + LogLevel exceptionLogLevel, + IServiceProvider services) { log.LogInformation("Migrating team capacities.."); @@ -48,7 +52,7 @@ internal static void MigrateCapacities( List sourceCapacities = GetSourceCapacities( sourceHttpClient, sourceTeamContext, sourceIteration); List targetCapacities = GetTargetCapacities( - sourceCapacities, targetIteration.Path, identityCache, log); + sourceCapacities, targetIteration.Path, useUserMapping, identityCache, log, services); if (targetCapacities.Count > 0) { @@ -83,13 +87,22 @@ private static List GetSourceCapacities( private static List GetTargetCapacities( List sourceCapacities, string targetIteration, + bool useUserMapping, Lazy> identityCache, - ILogger log) + ILogger log, + IServiceProvider services) { - var targetCapacities = new List(); + List targetCapacities = []; + Dictionary userMapping = null; + if (useUserMapping) + { + TfsCommonTools commonTools = services.GetRequiredService(); + userMapping = commonTools.UserMapping.UserMappings.Value; + } + foreach (var sourceCapacity in sourceCapacities) { - if (TryMatchIdentity(sourceCapacity, identityCache, out TeamFoundationIdentity targetIdentity)) + if (TryMatchIdentity(sourceCapacity, identityCache, userMapping, out TeamFoundationIdentity targetIdentity)) { targetCapacities.Add(new TeamMemberCapacityIdentityRef { @@ -127,51 +140,66 @@ private static void ReplaceTargetCapacities( private static bool TryMatchIdentity( TeamMemberCapacityIdentityRef sourceCapacity, Lazy> identityCache, + Dictionary userMapping, out TeamFoundationIdentity targetIdentity) { - var sourceDisplayName = sourceCapacity.TeamMember.DisplayName; - var index = sourceDisplayName.IndexOf("<"); + var sourceName = sourceCapacity.TeamMember.DisplayName; + var index = sourceName.IndexOf("<"); if (index > 0) { - sourceDisplayName = sourceDisplayName.Substring(0, index).Trim(); + sourceName = sourceName.Substring(0, index).Trim(); } + targetIdentity = MatchIdentity(sourceName, identityCache); + if ((targetIdentity is null) && (userMapping is not null)) + { + if (userMapping.TryGetValue(sourceName, out var mappedName) && !string.IsNullOrEmpty(mappedName)) + { + targetIdentity = MatchIdentity(mappedName, identityCache); + } + } + + // last attempt to match on unique name + // Match: "John Michael Bolden" to Bolden, "John Michael" on "john.m.bolden@example.com" unique name + if (targetIdentity is null) + { + var sourceUniqueName = sourceCapacity.TeamMember.UniqueName; + targetIdentity = identityCache.Value.FirstOrDefault(i => i.UniqueName == sourceUniqueName); + } + + return targetIdentity is not null; + } + + private static TeamFoundationIdentity MatchIdentity(string sourceName, Lazy> identityCache) + { // Match: // "Doe, John" to "Doe, John" // "John Doe" to "John Doe" - targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); + TeamFoundationIdentity targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceName); if (targetIdentity == null) { - if (sourceDisplayName.Contains(", ")) + if (sourceName.Contains(", ")) { // Match: // "Doe, John" to "John Doe" - var splitName = sourceDisplayName.Split(','); - sourceDisplayName = $"{splitName[1].Trim()} {splitName[0].Trim()}"; - targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); + var splitName = sourceName.Split(','); + sourceName = $"{splitName[1].Trim()} {splitName[0].Trim()}"; + targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceName); } else { - if (sourceDisplayName.Contains(' ')) + if (sourceName.Contains(' ')) { // Match: // "John Doe" to "Doe, John" - var splitName = sourceDisplayName.Split(' '); - sourceDisplayName = $"{splitName[1].Trim()}, {splitName[0].Trim()}"; - targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceDisplayName); + var splitName = sourceName.Split(' '); + sourceName = $"{splitName[1].Trim()}, {splitName[0].Trim()}"; + targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceName); } } - - // last attempt to match on unique name - // Match: "John Michael Bolden" to Bolden, "John Michael" on "john.m.bolden@example.com" unique name - if (targetIdentity == null) - { - var sourceUniqueName = sourceCapacity.TeamMember.UniqueName; - targetIdentity = identityCache.Value.FirstOrDefault(i => i.UniqueName == sourceUniqueName); - } } - return targetIdentity is not null; + return targetIdentity; } } } diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs index a7008ec46..c69f6a87d 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs @@ -272,8 +272,8 @@ private void MigrateCapacities( TfsTeamSettingsCore.MigrateCapacities( sourceHttpClient, Source.TfsProject.Guid, sourceTeam, targetHttpClient, Target.TfsProject.Guid, targetTeam, - iterationMap, _targetTeamFoundationIdentitiesLazyCache, - Telemetry, Log, exceptionLogLevel: LogLevel.Warning); + iterationMap, _targetTeamFoundationIdentitiesLazyCache, Options.UseUserMapping, + Telemetry, Log, exceptionLogLevel: LogLevel.Warning, Services); } } } diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessorOptions.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessorOptions.cs index eac131eca..e3bff851a 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessorOptions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessorOptions.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using MigrationTools.Processors.Infrastructure; namespace MigrationTools.Processors { public class TfsTeamSettingsProcessorOptions : ProcessorOptions { - /// /// Migrate original team settings after their creation on target team project /// @@ -36,5 +34,11 @@ public class TfsTeamSettingsProcessorOptions : ProcessorOptions /// public List Teams { get; set; } + /// + /// Use user mapping file from TfsTeamSettingsTool when matching users when migrating capacities. + /// By default, users in source are matched in target users by current display name. When this is set to `true`, + /// users are matched also by mapped name from user mapping file. + /// + public bool UseUserMapping { get; set; } } -} \ No newline at end of file +} diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs index 3185966c4..d113f46d8 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -231,8 +231,8 @@ private void MigrateCapacities( TfsTeamSettingsCore.MigrateCapacities( _processor.Source.GetClient(), _processor.Source.WorkItems.Project.Guid, sourceTeam, _processor.Target.GetClient(), _processor.Target.WorkItems.Project.Guid, targetTeam, - iterationMap, _targetTeamFoundationIdentitiesLazyCache, - Telemetry, Log, exceptionLogLevel: LogLevel.Error); + iterationMap, _targetTeamFoundationIdentitiesLazyCache, Options.UseUserMapping, + Telemetry, Log, exceptionLogLevel: LogLevel.Error, Services); } } } diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsToolOptions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsToolOptions.cs index e53ed37c1..968eeef13 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsToolOptions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsToolOptions.cs @@ -1,14 +1,10 @@ -using System; -using System.Collections.Generic; -using Microsoft.TeamFoundation.Build.Client; -using MigrationTools.Enrichers; +using System.Collections.Generic; using MigrationTools.Tools.Infrastructure; namespace MigrationTools.Tools { public class TfsTeamSettingsToolOptions : ToolOptions, ITfsTeamSettingsToolOptions { - /// /// Migrate original team settings after their creation on target team project /// @@ -27,16 +23,21 @@ public class TfsTeamSettingsToolOptions : ToolOptions, ITfsTeamSettingsToolOptio /// false public bool MigrateTeamCapacities { get; set; } + /// + /// Use user mapping file from TfsTeamSettingsTool when matching users when migrating capacities. + /// By default, users in source are matched in target users by current display name. When this is set to `true`, + /// users are matched also by mapped name from user mapping file. + /// + public bool UseUserMapping { get; set; } + /// /// List of Teams to process. If this is `null` then all teams will be processed. /// public List Teams { get; set; } - } public interface ITfsTeamSettingsToolOptions { - public bool MigrateTeamSettings { get; set; } public bool UpdateTeamSettings { get; set; } @@ -44,5 +45,7 @@ public interface ITfsTeamSettingsToolOptions public bool MigrateTeamCapacities { get; set; } public List Teams { get; set; } + + public bool UseUserMapping { get; set; } } -} \ No newline at end of file +} From 623f4e5e504cce44c171bf64ef9dd7bf01ded1b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Mon, 25 Nov 2024 09:43:35 +0100 Subject: [PATCH 4/4] Move methods from TfsTeamSettingsCore to TfsTeamSettingsTool --- .../Processors/TfsTeamSettingsCore.cs | 206 ------------------ .../Processors/TfsTeamSettingsProcessor.cs | 2 +- .../Tools/TfsTeamSettingsTool.cs | 190 +++++++++++++++- 3 files changed, 190 insertions(+), 208 deletions(-) delete mode 100644 src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs deleted file mode 100644 index 027f7d66c..000000000 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsCore.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.TeamFoundation.Client; -using Microsoft.TeamFoundation.Core.WebApi.Types; -using Microsoft.TeamFoundation.Framework.Client; -using Microsoft.TeamFoundation.Work.WebApi; -using Microsoft.VisualStudio.Services.WebApi; -using MigrationTools.Tools; - -namespace MigrationTools.Processors -{ - internal static class TfsTeamSettingsCore - { - internal static void MigrateCapacities( - WorkHttpClient sourceHttpClient, - Guid sourceProjectId, - TeamFoundationTeam sourceTeam, - WorkHttpClient targetHttpClient, - Guid targetProjectId, - TeamFoundationTeam targetTeam, - Dictionary iterationMap, - Lazy> identityCache, - bool useUserMapping, - ITelemetryLogger telemetry, - ILogger log, - LogLevel exceptionLogLevel, - IServiceProvider services) - { - log.LogInformation("Migrating team capacities.."); - - try - { - var sourceTeamContext = new TeamContext(sourceProjectId, sourceTeam.Identity.TeamFoundationId); - var sourceIterations = sourceHttpClient.GetTeamIterationsAsync(sourceTeamContext) - .ConfigureAwait(false).GetAwaiter().GetResult(); - - var targetTeamContext = new TeamContext(targetProjectId, targetTeam.Identity.TeamFoundationId); - var targetIterations = targetHttpClient.GetTeamIterationsAsync(targetTeamContext) - .ConfigureAwait(false).GetAwaiter().GetResult(); - - foreach (var sourceIteration in sourceIterations) - { - try - { - var targetIterationPath = iterationMap[sourceIteration.Path]; - var targetIteration = targetIterations.FirstOrDefault(i => i.Path == targetIterationPath); - if (targetIteration == null) continue; - - List sourceCapacities = GetSourceCapacities( - sourceHttpClient, sourceTeamContext, sourceIteration); - List targetCapacities = GetTargetCapacities( - sourceCapacities, targetIteration.Path, useUserMapping, identityCache, log, services); - - if (targetCapacities.Count > 0) - { - ReplaceTargetCapacities(targetHttpClient, targetTeamContext, targetTeam, targetIteration, targetCapacities, log); - } - } - catch (Exception ex) - { - telemetry.TrackException(ex, null); - log.Log(exceptionLogLevel, ex, "[SKIP] Problem migrating team capacities for iteration {iteration}.", sourceIteration.Path); - } - } - } - catch (Exception ex) - { - telemetry.TrackException(ex, null); - log.Log(exceptionLogLevel, ex, "[SKIP] Problem migrating team capacities."); - } - - log.LogInformation("Team capacities migration done.."); - } - - private static List GetSourceCapacities( - WorkHttpClient sourceHttpClient, - TeamContext sourceTeamContext, - TeamSettingsIteration sourceIteration) - { - return sourceHttpClient.GetCapacitiesWithIdentityRefAsync(sourceTeamContext, sourceIteration.Id) - .ConfigureAwait(false).GetAwaiter().GetResult(); - } - - private static List GetTargetCapacities( - List sourceCapacities, - string targetIteration, - bool useUserMapping, - Lazy> identityCache, - ILogger log, - IServiceProvider services) - { - List targetCapacities = []; - Dictionary userMapping = null; - if (useUserMapping) - { - TfsCommonTools commonTools = services.GetRequiredService(); - userMapping = commonTools.UserMapping.UserMappings.Value; - } - - foreach (var sourceCapacity in sourceCapacities) - { - if (TryMatchIdentity(sourceCapacity, identityCache, userMapping, out TeamFoundationIdentity targetIdentity)) - { - targetCapacities.Add(new TeamMemberCapacityIdentityRef - { - Activities = sourceCapacity.Activities, - DaysOff = sourceCapacity.DaysOff, - TeamMember = new IdentityRef - { - Id = targetIdentity.TeamFoundationId.ToString() - } - }); - } - else - { - log.LogWarning("[SKIP] Team Member {member} was not found on target when replacing capacities " - + "on iteration {iteration}.", sourceCapacity.TeamMember.DisplayName, targetIteration); - } - } - - return targetCapacities; - } - - private static void ReplaceTargetCapacities( - WorkHttpClient targetHttpClient, - TeamContext targetTeamContext, - TeamFoundationTeam targetTeam, - TeamSettingsIteration targetIteration, - List targetCapacities, - ILogger log) - { - targetHttpClient.ReplaceCapacitiesWithIdentityRefAsync(targetCapacities, targetTeamContext, targetIteration.Id) - .ConfigureAwait(false).GetAwaiter().GetResult(); - log.LogDebug("Team {team} capacities for iteration {iteration} migrated.", targetTeam.Name, targetIteration.Path); - } - - private static bool TryMatchIdentity( - TeamMemberCapacityIdentityRef sourceCapacity, - Lazy> identityCache, - Dictionary userMapping, - out TeamFoundationIdentity targetIdentity) - { - var sourceName = sourceCapacity.TeamMember.DisplayName; - var index = sourceName.IndexOf("<"); - if (index > 0) - { - sourceName = sourceName.Substring(0, index).Trim(); - } - - targetIdentity = MatchIdentity(sourceName, identityCache); - if ((targetIdentity is null) && (userMapping is not null)) - { - if (userMapping.TryGetValue(sourceName, out var mappedName) && !string.IsNullOrEmpty(mappedName)) - { - targetIdentity = MatchIdentity(mappedName, identityCache); - } - } - - // last attempt to match on unique name - // Match: "John Michael Bolden" to Bolden, "John Michael" on "john.m.bolden@example.com" unique name - if (targetIdentity is null) - { - var sourceUniqueName = sourceCapacity.TeamMember.UniqueName; - targetIdentity = identityCache.Value.FirstOrDefault(i => i.UniqueName == sourceUniqueName); - } - - return targetIdentity is not null; - } - - private static TeamFoundationIdentity MatchIdentity(string sourceName, Lazy> identityCache) - { - // Match: - // "Doe, John" to "Doe, John" - // "John Doe" to "John Doe" - TeamFoundationIdentity targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceName); - if (targetIdentity == null) - { - if (sourceName.Contains(", ")) - { - // Match: - // "Doe, John" to "John Doe" - var splitName = sourceName.Split(','); - sourceName = $"{splitName[1].Trim()} {splitName[0].Trim()}"; - targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceName); - } - else - { - if (sourceName.Contains(' ')) - { - // Match: - // "John Doe" to "Doe, John" - var splitName = sourceName.Split(' '); - sourceName = $"{splitName[1].Trim()}, {splitName[0].Trim()}"; - targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceName); - } - } - } - - return targetIdentity; - } - } -} - diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs index c69f6a87d..e4c413b25 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs @@ -269,7 +269,7 @@ private void MigrateCapacities( return; } - TfsTeamSettingsCore.MigrateCapacities( + TfsTeamSettingsTool.MigrateCapacities( sourceHttpClient, Source.TfsProject.Guid, sourceTeam, targetHttpClient, Target.TfsProject.Guid, targetTeam, iterationMap, _targetTeamFoundationIdentitiesLazyCache, Options.UseUserMapping, diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs index d113f46d8..ad90c6cbc 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs @@ -228,11 +228,199 @@ private void MigrateCapacities( return; } - TfsTeamSettingsCore.MigrateCapacities( + MigrateCapacities( _processor.Source.GetClient(), _processor.Source.WorkItems.Project.Guid, sourceTeam, _processor.Target.GetClient(), _processor.Target.WorkItems.Project.Guid, targetTeam, iterationMap, _targetTeamFoundationIdentitiesLazyCache, Options.UseUserMapping, Telemetry, Log, exceptionLogLevel: LogLevel.Error, Services); } + + internal static void MigrateCapacities( + WorkHttpClient sourceHttpClient, + Guid sourceProjectId, + TeamFoundationTeam sourceTeam, + WorkHttpClient targetHttpClient, + Guid targetProjectId, + TeamFoundationTeam targetTeam, + Dictionary iterationMap, + Lazy> identityCache, + bool useUserMapping, + ITelemetryLogger telemetry, + ILogger log, + LogLevel exceptionLogLevel, + IServiceProvider services) + { + log.LogInformation("Migrating team capacities.."); + + try + { + var sourceTeamContext = new TeamContext(sourceProjectId, sourceTeam.Identity.TeamFoundationId); + var sourceIterations = sourceHttpClient.GetTeamIterationsAsync(sourceTeamContext) + .ConfigureAwait(false).GetAwaiter().GetResult(); + + var targetTeamContext = new TeamContext(targetProjectId, targetTeam.Identity.TeamFoundationId); + var targetIterations = targetHttpClient.GetTeamIterationsAsync(targetTeamContext) + .ConfigureAwait(false).GetAwaiter().GetResult(); + + foreach (var sourceIteration in sourceIterations) + { + try + { + var targetIterationPath = iterationMap[sourceIteration.Path]; + var targetIteration = targetIterations.FirstOrDefault(i => i.Path == targetIterationPath); + if (targetIteration == null) continue; + + List sourceCapacities = GetSourceCapacities( + sourceHttpClient, sourceTeamContext, sourceIteration); + List targetCapacities = GetTargetCapacities( + sourceCapacities, targetIteration.Path, useUserMapping, identityCache, log, services); + + if (targetCapacities.Count > 0) + { + ReplaceTargetCapacities(targetHttpClient, targetTeamContext, targetTeam, targetIteration, targetCapacities, log); + } + } + catch (Exception ex) + { + telemetry.TrackException(ex, null); + log.Log(exceptionLogLevel, ex, "[SKIP] Problem migrating team capacities for iteration {iteration}.", sourceIteration.Path); + } + } + } + catch (Exception ex) + { + telemetry.TrackException(ex, null); + log.Log(exceptionLogLevel, ex, "[SKIP] Problem migrating team capacities."); + } + + log.LogInformation("Team capacities migration done.."); + } + + private static List GetSourceCapacities( + WorkHttpClient sourceHttpClient, + TeamContext sourceTeamContext, + TeamSettingsIteration sourceIteration) + { + return sourceHttpClient.GetCapacitiesWithIdentityRefAsync(sourceTeamContext, sourceIteration.Id) + .ConfigureAwait(false).GetAwaiter().GetResult(); + } + + private static List GetTargetCapacities( + List sourceCapacities, + string targetIteration, + bool useUserMapping, + Lazy> identityCache, + ILogger log, + IServiceProvider services) + { + List targetCapacities = []; + Dictionary userMapping = null; + if (useUserMapping) + { + TfsCommonTools commonTools = services.GetRequiredService(); + userMapping = commonTools.UserMapping.UserMappings.Value; + } + + foreach (var sourceCapacity in sourceCapacities) + { + if (TryMatchIdentity(sourceCapacity, identityCache, userMapping, out TeamFoundationIdentity targetIdentity)) + { + targetCapacities.Add(new TeamMemberCapacityIdentityRef + { + Activities = sourceCapacity.Activities, + DaysOff = sourceCapacity.DaysOff, + TeamMember = new IdentityRef + { + Id = targetIdentity.TeamFoundationId.ToString() + } + }); + } + else + { + log.LogWarning("[SKIP] Team Member {member} was not found on target when replacing capacities " + + "on iteration {iteration}.", sourceCapacity.TeamMember.DisplayName, targetIteration); + } + } + + return targetCapacities; + } + + private static void ReplaceTargetCapacities( + WorkHttpClient targetHttpClient, + TeamContext targetTeamContext, + TeamFoundationTeam targetTeam, + TeamSettingsIteration targetIteration, + List targetCapacities, + ILogger log) + { + targetHttpClient.ReplaceCapacitiesWithIdentityRefAsync(targetCapacities, targetTeamContext, targetIteration.Id) + .ConfigureAwait(false).GetAwaiter().GetResult(); + log.LogDebug("Team {team} capacities for iteration {iteration} migrated.", targetTeam.Name, targetIteration.Path); + } + + private static bool TryMatchIdentity( + TeamMemberCapacityIdentityRef sourceCapacity, + Lazy> identityCache, + Dictionary userMapping, + out TeamFoundationIdentity targetIdentity) + { + var sourceName = sourceCapacity.TeamMember.DisplayName; + var index = sourceName.IndexOf("<"); + if (index > 0) + { + sourceName = sourceName.Substring(0, index).Trim(); + } + + targetIdentity = MatchIdentity(sourceName, identityCache); + if ((targetIdentity is null) && (userMapping is not null)) + { + if (userMapping.TryGetValue(sourceName, out var mappedName) && !string.IsNullOrEmpty(mappedName)) + { + targetIdentity = MatchIdentity(mappedName, identityCache); + } + } + + // last attempt to match on unique name + // Match: "John Michael Bolden" to Bolden, "John Michael" on "john.m.bolden@example.com" unique name + if (targetIdentity is null) + { + var sourceUniqueName = sourceCapacity.TeamMember.UniqueName; + targetIdentity = identityCache.Value.FirstOrDefault(i => i.UniqueName == sourceUniqueName); + } + + return targetIdentity is not null; + } + + private static TeamFoundationIdentity MatchIdentity(string sourceName, Lazy> identityCache) + { + // Match: + // "Doe, John" to "Doe, John" + // "John Doe" to "John Doe" + TeamFoundationIdentity targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceName); + if (targetIdentity == null) + { + if (sourceName.Contains(", ")) + { + // Match: + // "Doe, John" to "John Doe" + var splitName = sourceName.Split(','); + sourceName = $"{splitName[1].Trim()} {splitName[0].Trim()}"; + targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceName); + } + else + { + if (sourceName.Contains(' ')) + { + // Match: + // "John Doe" to "Doe, John" + var splitName = sourceName.Split(' '); + sourceName = $"{splitName[1].Trim()}, {splitName[0].Trim()}"; + targetIdentity = identityCache.Value.FirstOrDefault(i => i.DisplayName == sourceName); + } + } + } + + return targetIdentity; + } } }