From b6b74d4f5977676047a567af2fe18ae8d6839164 Mon Sep 17 00:00:00 2001 From: Andrew Kanieski Date: Tue, 15 Dec 2020 11:34:26 -0500 Subject: [PATCH] Various bug fixes --- .../Pages/EndpointPage.xaml.cs | 7 +- .../Pages/RunMigrationPage.xaml.cs | 1 + .../Pages/WitPages/WitQueryPage.xaml | 47 ++++++++++++-- .../Pages/WitPages/WitQueryPage.xaml.cs | 19 +++++- .../Pages/WorkItemsPage.xaml | 2 +- AzureDevOpsMigrator/Models/Migration.cs | 2 +- .../Services/Endpoints/RestEndpointService.cs | 11 ++-- .../Migrators/ClassificationNodeMigrator.cs | 25 ++++--- .../Services/Migrators/OrchestratorService.cs | 65 +++++++++++++++++-- .../Services/Migrators/WorkItemMigrator.cs | 12 +++- 10 files changed, 158 insertions(+), 33 deletions(-) diff --git a/AzureDevOpsMigrator.WPF/Pages/EndpointPage.xaml.cs b/AzureDevOpsMigrator.WPF/Pages/EndpointPage.xaml.cs index a5b5bea..c999773 100644 --- a/AzureDevOpsMigrator.WPF/Pages/EndpointPage.xaml.cs +++ b/AzureDevOpsMigrator.WPF/Pages/EndpointPage.xaml.cs @@ -145,7 +145,12 @@ private void ResetTestOn_TextChanged(object sender, TextChangedEventArgs e) private void Button_GeneratePat_Click(object sender, RoutedEventArgs e) { - System.Diagnostics.Process.Start($"{Model.EndpointUri}/_usersSettings/tokens"); + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("cmd", $"/C start {Model.EndpointUri}/_usersSettings/tokens") + { + WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden, + CreateNoWindow = true, + UseShellExecute = true + }); } private void Button_LoadProjects_Click(object sender, RoutedEventArgs e) diff --git a/AzureDevOpsMigrator.WPF/Pages/RunMigrationPage.xaml.cs b/AzureDevOpsMigrator.WPF/Pages/RunMigrationPage.xaml.cs index 3a2483f..8033967 100644 --- a/AzureDevOpsMigrator.WPF/Pages/RunMigrationPage.xaml.cs +++ b/AzureDevOpsMigrator.WPF/Pages/RunMigrationPage.xaml.cs @@ -112,6 +112,7 @@ private void Run() } catch (Exception ex) { + GUILogger_Logged(this, new LogEventArgs(ex.ToString(), Microsoft.Extensions.Logging.LogLevel.Error)); IsRunning = false; RefreshBindings(); await Task.Delay(500); diff --git a/AzureDevOpsMigrator.WPF/Pages/WitPages/WitQueryPage.xaml b/AzureDevOpsMigrator.WPF/Pages/WitPages/WitQueryPage.xaml index f095f6a..ab56f0b 100644 --- a/AzureDevOpsMigrator.WPF/Pages/WitPages/WitQueryPage.xaml +++ b/AzureDevOpsMigrator.WPF/Pages/WitPages/WitQueryPage.xaml @@ -4,30 +4,63 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:AzureDevOpsMigrator.WPF.Pages.WitPages" + xmlns:fa="http://schemas.awesome.incremented/wpf/xaml/fontawesome.sharp" xmlns:clr="clr-namespace:System;assembly=mscorlib" mc:Ignorable="d" - d:DesignHeight="784" d:DesignWidth="1152" + d:DesignHeight="784" d:DesignWidth="1152" Focusable="False" Title="WitQueryPage"> Found {0} work items.. showing top 100 - + - - Query Filter - - - + + + + + + + + Query Filter + + + + + + + + + + + + + + + + + + + Available Fields + + + + + + diff --git a/AzureDevOpsMigrator.WPF/Pages/WitPages/WitQueryPage.xaml.cs b/AzureDevOpsMigrator.WPF/Pages/WitPages/WitQueryPage.xaml.cs index 961cbcb..a23dbd7 100644 --- a/AzureDevOpsMigrator.WPF/Pages/WitPages/WitQueryPage.xaml.cs +++ b/AzureDevOpsMigrator.WPF/Pages/WitPages/WitQueryPage.xaml.cs @@ -44,11 +44,22 @@ public WorkItemSummary(WorkItem wit) /// public partial class WitQueryPage : Page, INotifyPropertyChanged { + public List ProjectFields { get; set; } private IEndpointService _endpoint; public event PropertyChangedEventHandler PropertyChanged; public MigrationConfig Model => MainWindow.CurrentModel.CurrentConfig; + private bool _fieldPopupVisible { get; set; } + public bool FieldPopupVisible + { + get => _fieldPopupVisible; + set + { + _fieldPopupVisible = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FieldPopupVisible))); + } + } public ObservableCollection Results { get; set; } = new ObservableCollection(); public int Total { get; set; } public bool HasRecords { get; set; } @@ -71,8 +82,9 @@ private async Task Load() _endpoint.Initialize(Model.SourceEndpointConfig.EndpointUri, Model.SourceEndpointConfig.PersonalAccessToken); try { + ProjectFields = (await _endpoint.GetFields(Model.SourceEndpointConfig.ProjectName, CancellationToken.None)).ToList(); var result = await _endpoint.GetWorkItemsAsync( - $"select * from WorkItems {(string.IsNullOrEmpty(Model.SourceQuery) ? "" : "where " + Model.SourceQuery)}", + $"select * from WorkItems where [System.TeamProject] = '{Model.SourceEndpointConfig.ProjectName}' {(string.IsNullOrEmpty(Model.SourceQuery) ? "" : $"and ({Model.SourceQuery})")}", CancellationToken.None, top: 100); Total = result.TotalCount; @@ -81,6 +93,7 @@ private async Task Load() HasRecords = Total > 0; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Total))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasRecords))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProjectFields))); } catch (Exception ex) { @@ -88,5 +101,9 @@ private async Task Load() } } + private void Button_Help_Click(object sender, RoutedEventArgs e) + { + FieldPopupVisible = !FieldPopupVisible; + } } } diff --git a/AzureDevOpsMigrator.WPF/Pages/WorkItemsPage.xaml b/AzureDevOpsMigrator.WPF/Pages/WorkItemsPage.xaml index b8c6159..d68f3d7 100644 --- a/AzureDevOpsMigrator.WPF/Pages/WorkItemsPage.xaml +++ b/AzureDevOpsMigrator.WPF/Pages/WorkItemsPage.xaml @@ -25,7 +25,7 @@ Iterations Transformation - + diff --git a/AzureDevOpsMigrator/Models/Migration.cs b/AzureDevOpsMigrator/Models/Migration.cs index 30adebf..5d116eb 100644 --- a/AzureDevOpsMigrator/Models/Migration.cs +++ b/AzureDevOpsMigrator/Models/Migration.cs @@ -13,7 +13,7 @@ public class MigrationConfig public EndpointConfig TargetEndpointConfig { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, ItemTypeNameHandling = TypeNameHandling.Objects, TypeNameHandling = TypeNameHandling.Objects)] - public ObservableCollection Transformations { get; set; } + public ObservableCollection Transformations { get; set; } = new ObservableCollection(); public string SourceQuery { get; set; } = ""; public int MaxDegreeOfParallelism { get; set; } = 3; public bool FixHyperlinks { get; set; } = true; diff --git a/AzureDevOpsMigrator/Services/Endpoints/RestEndpointService.cs b/AzureDevOpsMigrator/Services/Endpoints/RestEndpointService.cs index f79fb32..c38701e 100644 --- a/AzureDevOpsMigrator/Services/Endpoints/RestEndpointService.cs +++ b/AzureDevOpsMigrator/Services/Endpoints/RestEndpointService.cs @@ -117,7 +117,8 @@ public async Task> GetCommitRefs(Guid repoId, Cancella public async Task UpsertClassificationNodesAsync(WorkItemClassificationNode node, string projectName, TreeStructureGroup group, CancellationToken token) { - await _witClient.CreateOrUpdateClassificationNodeAsync(node, projectName, group, cancellationToken: token); + var path = string.Join("/", node.Path.Split('\\').Skip(3).Where(x => x != node.Name)); + await _witClient.CreateOrUpdateClassificationNodeAsync(node, projectName, group, path: path, cancellationToken: token); } public async Task> GetAreaPaths(string projectName, CancellationToken token) => @@ -143,11 +144,11 @@ public async Task> GetAllProjects() return await _projectsClient.GetProjects(top: ushort.MaxValue); } - public async Task> GetIdsByWiql(string wiqlQuery, CancellationToken token) + public async Task> GetIdsByWiql(string wiqlQuery, CancellationToken token, string projectName = "") { CheckInitialized(); var wiql = new Wiql(); - wiql.Query = $"SELECT * FROM WORKITEMS WHERE {wiqlQuery}"; + wiql.Query = $"SELECT * FROM WORKITEMS where [System.TeamProject] = '{projectName}' {(string.IsNullOrEmpty(wiqlQuery) ? "" : $" and ({wiqlQuery})")}"; return (await _witClient.QueryByWiqlAsync(wiql, false, cancellationToken: token)).WorkItems.Select(i => i.Id); } @@ -281,7 +282,7 @@ public async Task GetField(string referenceName, CancellationToke public async Task GetWorkItemByMigrationState(string projectName, string migrationStateField, string url, CancellationToken token) { - var existing = await GetIdsByWiql($"[Custom.{migrationStateField}] = '{url}'", token); + var existing = await GetIdsByWiql($"[Custom.{migrationStateField}] = '{url}'", token, projectName); if (existing.Count() == 0) { @@ -326,7 +327,7 @@ public interface IEndpointService Task> GetClassificationNodes(string projectName, TreeStructureGroup type, CancellationToken token); Task> GetFields(string project, CancellationToken token); Task CreateField(WorkItemField field, CancellationToken token); - Task> GetIdsByWiql(string wiqlQuery, CancellationToken token); + Task> GetIdsByWiql(string wiqlQuery, CancellationToken token, string projectName = ""); Task> GetIterations(string projectName, CancellationToken token); Task GetProject(string projectName); Task GetWorkItem(string projectName, int workItemId, CancellationToken token, WorkItemExpand expand = WorkItemExpand.None); diff --git a/AzureDevOpsMigrator/Services/Migrators/ClassificationNodeMigrator.cs b/AzureDevOpsMigrator/Services/Migrators/ClassificationNodeMigrator.cs index a89fd7b..f80363b 100644 --- a/AzureDevOpsMigrator/Services/Migrators/ClassificationNodeMigrator.cs +++ b/AzureDevOpsMigrator/Services/Migrators/ClassificationNodeMigrator.cs @@ -62,12 +62,14 @@ public async Task ExecuteAsync(System.Threading.CancellationToken token) if (existing == null) { // Create a new area path in target + var updatedPath = @"\" + _config.TargetEndpointConfig.ProjectName + (_nodeType == TreeNodeStructureType.Area ? @"\Area\" : @"\Iteration\") + sourceNode.Path; upsertedNodes.Add((SyncState.Create, new WorkItemClassificationNode() { - Path = @"\" + _config.TargetEndpointConfig.ProjectName + (_nodeType == TreeNodeStructureType.Area ? @"\Area\" : @"\Iteration\") + sourceNode.Path, + Path = updatedPath, Name = sourceNode.Path.Split(@"\").Last(), StructureType = sourceNode.Node.StructureType, - Attributes = sourceNode.Node.Attributes + Attributes = sourceNode.Node.Attributes, + HasChildren = sourceNode.Node.HasChildren })); } else @@ -78,15 +80,18 @@ public async Task ExecuteAsync(System.Threading.CancellationToken token) { foreach (var sourcePair in sourceNode.Node.Attributes) { - if (existing.Node.Attributes.ContainsKey(sourcePair.Key) && !existing.Node.Attributes[sourcePair.Key].Equals(sourcePair.Value)) + if (existing.Node.Attributes != null) { - changes = true; - existing.Node.Attributes[sourcePair.Key] = sourcePair.Value; - } - else if (!existing.Node.Attributes.ContainsKey(sourcePair.Key)) - { - changes = true; - existing.Node.Attributes.Add(sourcePair); + if (existing.Node.Attributes.ContainsKey(sourcePair.Key) && !existing.Node.Attributes[sourcePair.Key].Equals(sourcePair.Value)) + { + changes = true; + existing.Node.Attributes[sourcePair.Key] = sourcePair.Value; + } + else if (!existing.Node.Attributes.ContainsKey(sourcePair.Key)) + { + changes = true; + existing.Node.Attributes.Add(sourcePair); + } } } } diff --git a/AzureDevOpsMigrator/Services/Migrators/OrchestratorService.cs b/AzureDevOpsMigrator/Services/Migrators/OrchestratorService.cs index 6e31b90..8c982d2 100644 --- a/AzureDevOpsMigrator/Services/Migrators/OrchestratorService.cs +++ b/AzureDevOpsMigrator/Services/Migrators/OrchestratorService.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; +using System.Collections.Generic; namespace AzureDevOpsMigrator.Migrators { @@ -74,10 +75,50 @@ public async Task BuildMigrationPlan(CancellationToken token) InitializeEndpoints(); + var exceptions = new List(); + await Task.WhenAll( - Task.Run(async () => plan.IterationsCount = await _iterationMigrator.GetPlannedCount(token)), - Task.Run(async () => plan.AreaPathsCount = await _areaPathMigrator.GetPlannedCount(token)), - Task.Run(async () => plan.WorkItemsCount = await _workItemMigrator.GetPlannedCount(token))); + Task.Run(async () => + { + try + { + return plan.IterationsCount = await _iterationMigrator.GetPlannedCount(token); + } + catch (Exception ex) + { + exceptions.Add(ex); + return -1; + } + }), + Task.Run(async () => + { + try + { + return plan.AreaPathsCount = await _areaPathMigrator.GetPlannedCount(token); + } + catch (Exception ex) + { + exceptions.Add(ex); + return -1; + } + }), + Task.Run(async () => + { + try + { + return plan.WorkItemsCount = await _workItemMigrator.GetPlannedCount(token); + } + catch (Exception ex) + { + exceptions.Add(ex); + return -1; + } + })); + + if (exceptions.Count > 0) + { + throw new MigrationException($"Failed to generate migration plan.", exceptions.First()); + } return plan; } @@ -129,9 +170,23 @@ public async Task ExecuteAsync(MigrationPlan plan, CancellationToken token) _currentPlan = plan; _log.LogInformation("Starting migrations"); - await _areaPathMigrator.ExecuteAsync(token); + if (_config.Execution.AreaPathMigratorEnabled) + { + await _areaPathMigrator.ExecuteAsync(token); + } + else + { + _log.LogInformation("Skipping area path migrations.."); + } + if (_config.Execution.IterationsMigratorEnabled) + { + await _iterationMigrator.ExecuteAsync(token); + } + else + { + _log.LogInformation("Skipping iteration migrations.."); + } - await _iterationMigrator.ExecuteAsync(token); await _workItemMigrator.ExecuteAsync(token); } diff --git a/AzureDevOpsMigrator/Services/Migrators/WorkItemMigrator.cs b/AzureDevOpsMigrator/Services/Migrators/WorkItemMigrator.cs index a2e97cf..daa3d75 100644 --- a/AzureDevOpsMigrator/Services/Migrators/WorkItemMigrator.cs +++ b/AzureDevOpsMigrator/Services/Migrators/WorkItemMigrator.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.TeamFoundation.Core.WebApi; +using Microsoft.VisualStudio.Services.WebApi; namespace AzureDevOpsMigrator.Migrators { @@ -38,7 +39,7 @@ public async Task ExecuteAsync(CancellationToken token) _sourceProject = await _sourceEndpoint.GetProject(_config.SourceEndpointConfig.ProjectName); _targetProject = await _targetEndpoint.GetProject(_config.TargetEndpointConfig.ProjectName); - var result = new Queue(await _sourceEndpoint.GetIdsByWiql(_config.SourceQuery, token)); + var result = new Queue(await _sourceEndpoint.GetIdsByWiql(_config.SourceQuery, token, _config.SourceEndpointConfig.ProjectName)); _totalCount = result.Count; _processedCount = 0; ConcurrentBag haltedExceptions = new ConcurrentBag(); @@ -162,8 +163,15 @@ await _targetEndpoint.CreateField(new WorkItemField() foreach (var field in source.Fields.Where(srcField => !_fieldsNotToCopy.Contains(srcField.Key))) { var projectField = _sourceProjectFields.FirstOrDefault(projectField => projectField.ReferenceName.Equals(field.Key)); + if (projectField != null && !projectField.ReadOnly) { + + if (field.Value is IdentityRef) + { + (field.Value as IdentityRef).Descriptor = new SubjectDescriptor(); + } + if (!target.Fields.ContainsKey(field.Key)) { target.Fields.Add(field); @@ -428,7 +436,7 @@ orderby rev.Rev ascending } public async Task GetPlannedCount(CancellationToken token) => !_config.Execution.WorkItemsMigratorEnabled ? - await Task.FromResult(null) : (await _sourceEndpoint.GetIdsByWiql(_config.SourceQuery, token)).Count(); + await Task.FromResult(null) : (await _sourceEndpoint.GetIdsByWiql(_config.SourceQuery, token, _config.SourceEndpointConfig.ProjectName)).Count(); public async Task CompareFields(CancellationToken token) {