diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsTeamSettingsProcessor.cs index b742c5f2..e4c413b2 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.."); + TfsTeamSettingsTool.MigrateCapacities( + sourceHttpClient, Source.TfsProject.Guid, sourceTeam, + targetHttpClient, Target.TfsProject.Guid, targetTeam, + 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 eac131ec..e3bff851 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 0f7c1aab..ad90c6cb 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsTool.cs @@ -47,7 +47,7 @@ public class TfsTeamSettingsTool : Tool public TfsTeamSettingsTool(IOptions options, IServiceProvider services, ILogger logger, ITelemetryLogger telemetryLogger) : base(options, services, logger, telemetryLogger) { - + } @@ -218,23 +218,49 @@ 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; + if (!Options.MigrateTeamCapacities) + { + return; + } - Log.LogInformation("Migrating team capacities.."); + 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); + } - WorkHttpClient sourceHttpClient = _processor.Source.GetClient(); - WorkHttpClient targetHttpClient = _processor.Target.GetClient(); + 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 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(); + var targetTeamContext = new TeamContext(targetProjectId, targetTeam.Identity.TeamFoundationId); + var targetIterations = targetHttpClient.GetTeamIterationsAsync(targetTeamContext) + .ConfigureAwait(false).GetAwaiter().GetResult(); foreach (var sourceIteration in sourceIterations) { @@ -244,92 +270,157 @@ private void MigrateCapacities(TeamFoundationTeam sourceTeam, TeamFoundationTeam 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); - } - } + List sourceCapacities = GetSourceCapacities( + sourceHttpClient, sourceTeamContext, sourceIteration); + List targetCapacities = GetTargetCapacities( + sourceCapacities, targetIteration.Path, useUserMapping, identityCache, log, services); 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) { - Telemetry.TrackException(ex, null); - Log.LogError(ex, "[SKIP] Problem migrating team capacities for iteration {iteration}.", sourceIteration.Path); + 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.LogError(ex, "[SKIP] Problem migrating team capacities."); + 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); + } } - Log.LogInformation("Team capacities migration done.."); + 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/Tools/TfsTeamSettingsToolOptions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsTeamSettingsToolOptions.cs index e53ed37c..968eeef1 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 +}