private MigrationTools._EngineV1.DataContracts.WorkItemData CreateWorkItem_Shell(ProjectData destProject, MigrationTools._EngineV1.DataContracts.WorkItemData currentRevisionWorkItem, string destType) { WorkItem newwit; var newWorkItemstartTime = DateTime.UtcNow; var newWorkItemTimer = Stopwatch.StartNew(); if (destProject.ToProject().WorkItemTypes.Contains(destType)) { newwit = destProject.ToProject().WorkItemTypes[destType].NewWorkItem(); } else { throw new Exception(string.Format("WARNING: Unable to find '{0}' in the target project. Most likley this is due to a typo in the .json configuration under WorkItemTypeDefinition! ", destType)); } newWorkItemTimer.Stop(); Telemetry.TrackDependency(new DependencyTelemetry("TfsObjectModel", Engine.Target.Config.AsTeamProjectConfig().Collection.ToString(), "NewWorkItem", null, newWorkItemstartTime, newWorkItemTimer.Elapsed, "200", true)); if (_config.UpdateCreatedBy) { newwit.Fields["System.CreatedBy"].Value = currentRevisionWorkItem.ToWorkItem().Revisions[0].Fields["System.CreatedBy"].Value; } if (_config.UpdateCreatedDate) { newwit.Fields["System.CreatedDate"].Value = currentRevisionWorkItem.ToWorkItem().Revisions[0].Fields["System.CreatedDate"].Value; } return(newwit.AsWorkItemData()); }
private void ProcessWorkItemAttachments(MigrationTools._EngineV1.DataContracts.WorkItemData sourceWorkItem, MigrationTools._EngineV1.DataContracts.WorkItemData targetWorkItem, bool save = true) { if (targetWorkItem != null && _config.AttachmentMigration && sourceWorkItem.ToWorkItem().Attachments.Count > 0) { TraceWriteLine(LogEventLevel.Information, "Attachemnts {SourceWorkItemAttachmentCount} | LinkMigrator:{AttachmentMigration}", new Dictionary <string, object>() { { "SourceWorkItemAttachmentCount", sourceWorkItem.ToWorkItem().Attachments.Count }, { "AttachmentMigration", _config.AttachmentMigration } }); attachmentEnricher.ProcessAttachemnts(sourceWorkItem, targetWorkItem, save); AddMetric("Attachments", processWorkItemMetrics, targetWorkItem.ToWorkItem().AttachedFileCount); } }
private void ProcessWorkItemLinks(IWorkItemMigrationClient sourceStore, IWorkItemMigrationClient targetStore, MigrationTools._EngineV1.DataContracts.WorkItemData sourceWorkItem, MigrationTools._EngineV1.DataContracts.WorkItemData targetWorkItem) { if (targetWorkItem != null && _config.LinkMigration && sourceWorkItem.ToWorkItem().Links.Count > 0) { TraceWriteLine(LogEventLevel.Information, "Links {SourceWorkItemLinkCount} | LinkMigrator:{LinkMigration}", new Dictionary <string, object>() { { "SourceWorkItemLinkCount", sourceWorkItem.ToWorkItem().Links.Count }, { "LinkMigration", _config.LinkMigration } }); workItemLinkEnricher.Enrich(sourceWorkItem, targetWorkItem); AddMetric("RelatedLinkCount", processWorkItemMetrics, targetWorkItem.ToWorkItem().Links.Count); int fixedLinkCount = gitRepositoryEnricher.Enrich(sourceWorkItem, targetWorkItem); AddMetric("FixedGitLinkCount", processWorkItemMetrics, fixedLinkCount); } }
private void WorkItemTypeChange(MigrationTools._EngineV1.DataContracts.WorkItemData targetWorkItem, bool skipToFinalRevisedWorkItemType, string finalDestType, MigrationTools._EngineV1.DataContracts.RevisionItem revision, MigrationTools._EngineV1.DataContracts.WorkItemData currentRevisionWorkItem, string destType) { //If the work item already exists and its type has changed, update its type. Done this way because there doesn't appear to be a way to do this through the store. if (!skipToFinalRevisedWorkItemType && targetWorkItem.Type != finalDestType) { Debug.WriteLine($"Work Item type change! '{targetWorkItem.Title}': From {targetWorkItem.Type} to {destType}"); var typePatch = new JsonPatchOperation() { Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add, Path = "/fields/System.WorkItemType", Value = destType }; var datePatch = new JsonPatchOperation() { Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add, Path = "/fields/System.ChangedDate", Value = currentRevisionWorkItem.ToWorkItem().Revisions[revision.Index].Fields["System.ChangedDate"].Value }; var patchDoc = new JsonPatchDocument { typePatch, datePatch }; _witClient.UpdateWorkItemAsync(patchDoc, int.Parse(targetWorkItem.Id), bypassRules: true).Wait(); } }
private List <MigrationTools._EngineV1.DataContracts.RevisionItem> RevisionsToMigrate(MigrationTools._EngineV1.DataContracts.WorkItemData sourceWorkItem, MigrationTools._EngineV1.DataContracts.WorkItemData targetWorkItem) { // Revisions have been sorted already on object creation. Values of the Dictionary are sorted by RevisionItem.Number var sortedRevisions = sourceWorkItem.Revisions.Values.ToList(); if (targetWorkItem != null) { // Target exists so remove any Changed Date matches between them var targetChangedDates = (from Revision x in targetWorkItem.ToWorkItem().Revisions select Convert.ToDateTime(x.Fields["System.ChangedDate"].Value)).ToList(); if (_config.ReplayRevisions) { sortedRevisions = sortedRevisions.Where(x => !targetChangedDates.Contains(x.ChangedDate)).ToList(); } // Find Max target date and remove all source revisions that are newer var targetLatestDate = targetChangedDates.Max(); sortedRevisions = sortedRevisions.Where(x => x.ChangedDate > targetLatestDate).ToList(); } if (!_config.ReplayRevisions && sortedRevisions.Count > 0) { // Remove all but the latest revision if we are not replaying revisions sortedRevisions.RemoveRange(0, sortedRevisions.Count - 1); } TraceWriteLine(LogEventLevel.Information, "Found {RevisionsCount} revisions to migrate on Work item:{sourceWorkItemId}", new Dictionary <string, object>() { { "RevisionsCount", sortedRevisions.Count }, { "sourceWorkItemId", sourceWorkItem.Id } }); return(sortedRevisions); }
// TODO : Make this into the Work Item mapping tool private void PopulateWorkItem(MigrationTools._EngineV1.DataContracts.WorkItemData oldWi, MigrationTools._EngineV1.DataContracts.WorkItemData newwit, string destType) { var oldWorkItem = oldWi.ToWorkItem(); var newWorkItem = newwit.ToWorkItem(); var newWorkItemstartTime = DateTime.UtcNow; var fieldMappingTimer = Stopwatch.StartNew(); if (newWorkItem.IsPartialOpen || !newWorkItem.IsOpen) { newWorkItem.Open(); } newWorkItem.Title = oldWorkItem.Title; newWorkItem.State = oldWorkItem.State; newWorkItem.Reason = oldWorkItem.Reason; foreach (Field f in oldWorkItem.Fields) { if (newWorkItem.Fields.Contains(f.ReferenceName) && !_ignore.Contains(f.ReferenceName) && (!newWorkItem.Fields[f.ReferenceName].IsChangedInRevision || newWorkItem.Fields[f.ReferenceName].IsEditable)) { newWorkItem.Fields[f.ReferenceName].Value = oldWorkItem.Fields[f.ReferenceName].Value; } } newWorkItem.AreaPath = nodeStructureEnricher.GetNewNodeName(oldWorkItem.AreaPath, TfsNodeStructureType.Area); newWorkItem.IterationPath = nodeStructureEnricher.GetNewNodeName(oldWorkItem.IterationPath, TfsNodeStructureType.Iteration); switch (destType) { case "Test Case": newWorkItem.Fields["Microsoft.VSTS.TCM.Steps"].Value = oldWorkItem.Fields["Microsoft.VSTS.TCM.Steps"].Value; newWorkItem.Fields["Microsoft.VSTS.Common.Priority"].Value = oldWorkItem.Fields["Microsoft.VSTS.Common.Priority"].Value; break; } if (newWorkItem.Fields.Contains("Microsoft.VSTS.Common.BacklogPriority") && newWorkItem.Fields["Microsoft.VSTS.Common.BacklogPriority"].Value != null && !IsNumeric(newWorkItem.Fields["Microsoft.VSTS.Common.BacklogPriority"].Value.ToString(), NumberStyles.Any)) { newWorkItem.Fields["Microsoft.VSTS.Common.BacklogPriority"].Value = 10; } var description = new StringBuilder(); description.Append(oldWorkItem.Description); newWorkItem.Description = description.ToString(); fieldMappingTimer.Stop(); }
private List <MigrationTools._EngineV1.DataContracts.RevisionItem> RevisionsToMigrate(MigrationTools._EngineV1.DataContracts.WorkItemData sourceWorkItem, MigrationTools._EngineV1.DataContracts.WorkItemData targetWorkItem) { // just to make sure, we replay the events in the same order as they appeared // maybe, the Revisions collection is not sorted according to the actual Revision number List <MigrationTools._EngineV1.DataContracts.RevisionItem> sortedRevisions = null; sortedRevisions = sourceWorkItem.ToWorkItem().Revisions.Cast <Revision>() .Select(x => new RevisionItem { Index = x.Index, Number = Convert.ToInt32(x.Fields["System.Rev"].Value), ChangedDate = Convert.ToDateTime(x.Fields["System.ChangedDate"].Value) }) .ToList(); if (targetWorkItem != null) { // Target exists so remove any Changed Date matches bwtween them var targetChangedDates = (from Revision x in targetWorkItem.ToWorkItem().Revisions select Convert.ToDateTime(x.Fields["System.ChangedDate"].Value)).ToList(); if (_config.ReplayRevisions) { sortedRevisions = sortedRevisions.Where(x => !targetChangedDates.Contains(x.ChangedDate)).ToList(); } // Find Max target date and remove all source revisions that are newer var targetLatestDate = targetChangedDates.Max(); sortedRevisions = sortedRevisions.Where(x => x.ChangedDate > targetLatestDate).ToList(); } sortedRevisions = sortedRevisions.OrderBy(x => x.Number).ToList(); if (!_config.ReplayRevisions && sortedRevisions.Count > 0) { // Remove all but the latest revision if we are not replaying reviss=ions sortedRevisions.RemoveRange(0, sortedRevisions.Count - 1); } TraceWriteLine(LogEventLevel.Information, "Found {RevisionsCount} revisions to migrate on Work item:{sourceWorkItemId}", new Dictionary <string, object>() { { "RevisionsCount", sortedRevisions.Count }, { "sourceWorkItemId", sourceWorkItem.Id } }); return(sortedRevisions); }
private MigrationTools._EngineV1.DataContracts.WorkItemData ReplayRevisions(List <MigrationTools._EngineV1.DataContracts.RevisionItem> revisionsToMigrate, MigrationTools._EngineV1.DataContracts.WorkItemData sourceWorkItem, MigrationTools._EngineV1.DataContracts.WorkItemData targetWorkItem, int current) { try { var skipToFinalRevisedWorkItemType = _config.SkipToFinalRevisedWorkItemType; string finalDestType = revisionsToMigrate.Last().Type; if (skipToFinalRevisedWorkItemType && Engine.TypeDefinitionMaps.Items.ContainsKey(finalDestType)) { finalDestType = Engine.TypeDefinitionMaps.Items[finalDestType].Map(); } //If work item hasn't been created yet, create a shell if (targetWorkItem == null) { string targetType = revisionsToMigrate.First().Type; if (Engine.TypeDefinitionMaps.Items.ContainsKey(targetType)) { targetType = Engine.TypeDefinitionMaps.Items[targetType].Map(); } targetWorkItem = CreateWorkItem_Shell(Engine.Target.WorkItems.Project, sourceWorkItem, skipToFinalRevisedWorkItemType ? finalDestType : targetType); } if (_config.CollapseRevisions) { var data = revisionsToMigrate.Select(rev => { var revWi = sourceWorkItem.GetRevision(rev.Number); return(new { revWi.Id, revWi.Rev, revWi.ChangedDate, // According to https://docs.microsoft.com/en-us/azure/devops/reference/xml/reportable-fields-reference?view=azure-devops-2020 Revised Date was misused here revWi.Fields }); }); var fileData = JsonConvert.SerializeObject(data, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.None }); var filePath = Path.Combine(Path.GetTempPath(), $"{sourceWorkItem.Id}_PreMigrationHistory.json"); // todo: Delete this file after (!) WorkItem has been saved File.WriteAllText(filePath, fileData); targetWorkItem.ToWorkItem().Attachments.Add(new Attachment(filePath, "History has been consolidated into the attached file.")); revisionsToMigrate = revisionsToMigrate.GetRange(revisionsToMigrate.Count - 1, 1); TraceWriteLine(LogEventLevel.Information, " Attached a consolidated set of {RevisionCount} revisions.", new Dictionary <string, object>() { { "RevisionCount", data.Count() } }); } foreach (var revision in revisionsToMigrate) { var currentRevisionWorkItem = sourceWorkItem.GetRevision(revision.Number); TraceWriteLine(LogEventLevel.Information, " Processing Revision [{RevisionNumber}]", new Dictionary <string, object>() { { "RevisionNumber", revision.Number } }); // Decide on WIT string destType = currentRevisionWorkItem.Type; if (Engine.TypeDefinitionMaps.Items.ContainsKey(destType)) { destType = Engine.TypeDefinitionMaps.Items[destType].Map(); } WorkItemTypeChange(targetWorkItem, skipToFinalRevisedWorkItemType, finalDestType, revision, currentRevisionWorkItem, destType); PopulateWorkItem(currentRevisionWorkItem, targetWorkItem, destType); // Todo: Ensure all field maps use WorkItemData.Fields to apply a correct mapping Engine.FieldMaps.ApplyFieldMappings(currentRevisionWorkItem, targetWorkItem); // Todo: Think about an "UpdateChangedBy" flag as this is expensive! (2s/WI instead of 1,5s when writing "Migration") targetWorkItem.ToWorkItem().Fields["System.ChangedBy"].Value = currentRevisionWorkItem.Fields["System.ChangedBy"]; targetWorkItem.ToWorkItem().Fields["System.History"].Value = currentRevisionWorkItem.Fields["System.History"]; //Debug.WriteLine("Discussion:" + currentRevisionWorkItem.Revisions[revision.Index].Fields["System.History"].Value); TfsReflectedWorkItemId reflectedUri = (TfsReflectedWorkItemId)Engine.Source.WorkItems.CreateReflectedWorkItemId(sourceWorkItem); if (!targetWorkItem.ToWorkItem().Fields.Contains(Engine.Target.Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName)) { var ex = new InvalidOperationException("ReflectedWorkItemIDField Field Missing"); Log.LogError(ex, " The WorkItemType {WorkItemType} does not have a Field called {ReflectedWorkItemID}", targetWorkItem.Type, Engine.Target.Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName); throw ex; } targetWorkItem.ToWorkItem().Fields[Engine.Target.Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName].Value = reflectedUri.ToString(); targetWorkItem.SaveToAzureDevOps(); TraceWriteLine(LogEventLevel.Information, " Saved TargetWorkItem {TargetWorkItemId}. Replayed revision {RevisionNumber} of {RevisionsToMigrateCount}", new Dictionary <string, object>() { { "TargetWorkItemId", targetWorkItem.Id }, { "RevisionNumber", revision.Number }, { "RevisionsToMigrateCount", revisionsToMigrate.Count } }); } if (targetWorkItem != null) { ProcessWorkItemAttachments(sourceWorkItem, targetWorkItem, false); ProcessWorkItemLinks(Engine.Source.WorkItems, Engine.Target.WorkItems, sourceWorkItem, targetWorkItem); if (_config.GenerateMigrationComment) { var reflectedUri = targetWorkItem.ToWorkItem().Fields[Engine.Target.Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName].Value; var history = new StringBuilder(); history.Append( $"This work item was migrated from a different project or organization. You can find the old version at <a href=\"{reflectedUri}\">{reflectedUri}</a>."); targetWorkItem.ToWorkItem().History = history.ToString(); } targetWorkItem.SaveToAzureDevOps(); attachmentEnricher.CleanUpAfterSave(); TraceWriteLine(LogEventLevel.Information, "...Saved as {TargetWorkItemId}", new Dictionary <string, object> { { "TargetWorkItemId", targetWorkItem.Id } }); } } catch (Exception ex) { TraceWriteLine(LogEventLevel.Information, "...FAILED to Save"); Log.LogInformation("==============================================================="); if (targetWorkItem != null) { foreach (Field f in targetWorkItem.ToWorkItem().Fields) { TraceWriteLine(LogEventLevel.Information, "{FieldReferenceName} ({FieldName}) | {FieldValue}", new Dictionary <string, object>() { { "FieldReferenceName", f.ReferenceName }, { "FieldName", f.Name }, { "FieldValue", f.Value } }); } } Log.LogInformation("==============================================================="); Log.LogError(ex.ToString(), ex); Log.LogInformation("==============================================================="); } return(targetWorkItem); }
private void ProcessWorkItem(MigrationTools._EngineV1.DataContracts.WorkItemData sourceWorkItem, int retryLimit = 5, int retrys = 0) { var witstopwatch = Stopwatch.StartNew(); var starttime = DateTime.Now; processWorkItemMetrics = new Dictionary <string, double>(); processWorkItemParamiters = new Dictionary <string, string>(); AddParameter("SourceURL", processWorkItemParamiters, Engine.Source.WorkItems.Config.AsTeamProjectConfig().Collection.ToString()); AddParameter("SourceWorkItem", processWorkItemParamiters, sourceWorkItem.Id.ToString()); AddParameter("TargetURL", processWorkItemParamiters, Engine.Target.WorkItems.Config.AsTeamProjectConfig().Collection.ToString()); AddParameter("TargetProject", processWorkItemParamiters, Engine.Target.WorkItems.Project.Name); AddParameter("RetryLimit", processWorkItemParamiters, retryLimit.ToString()); AddParameter("RetryNumber", processWorkItemParamiters, retrys.ToString()); Log.LogDebug("######################################################################################"); Log.LogDebug("ProcessWorkItem: {sourceWorkItemId}", sourceWorkItem.Id); Log.LogDebug("######################################################################################"); try { if (sourceWorkItem.Type != "Test Plan" || sourceWorkItem.Type != "Test Suite") { var targetWorkItem = Engine.Target.WorkItems.FindReflectedWorkItem(sourceWorkItem, false); /////////////////////////////////////////////// TraceWriteLine(LogEventLevel.Information, "Work Item has {sourceWorkItemRev} revisions and revision migration is set to {ReplayRevisions}", new Dictionary <string, object>() { { "sourceWorkItemRev", sourceWorkItem.Rev }, { "ReplayRevisions", _config.ReplayRevisions } } ); List <MigrationTools._EngineV1.DataContracts.RevisionItem> revisionsToMigrate = RevisionsToMigrate(sourceWorkItem, targetWorkItem); if (targetWorkItem == null) { targetWorkItem = ReplayRevisions(revisionsToMigrate, sourceWorkItem, null, _current); AddMetric("Revisions", processWorkItemMetrics, revisionsToMigrate.Count); } else { if (revisionsToMigrate.Count == 0) { ProcessWorkItemAttachments(sourceWorkItem, targetWorkItem, false); ProcessWorkItemLinks(Engine.Source.WorkItems, Engine.Target.WorkItems, sourceWorkItem, targetWorkItem); TraceWriteLine(LogEventLevel.Information, "Skipping as work item exists and no revisions to sync detected"); processWorkItemMetrics.Add("Revisions", 0); } else { TraceWriteLine(LogEventLevel.Information, "Syncing as there are {revisionsToMigrateCount} revisons detected", new Dictionary <string, object>() { { "revisionsToMigrateCount", revisionsToMigrate.Count } }); targetWorkItem = ReplayRevisions(revisionsToMigrate, sourceWorkItem, targetWorkItem, _current); AddMetric("Revisions", processWorkItemMetrics, revisionsToMigrate.Count); AddMetric("SyncRev", processWorkItemMetrics, revisionsToMigrate.Count); } } AddParameter("TargetWorkItem", processWorkItemParamiters, targetWorkItem.ToWorkItem().Revisions.Count.ToString()); /////////////////////////////////////////////// ProcessHTMLFieldAttachements(targetWorkItem); /////////////////////////////////////////////// /////////////////////////////////////////////////////// if (targetWorkItem != null && targetWorkItem.ToWorkItem().IsDirty) { targetWorkItem.SaveToAzureDevOps(); } if (targetWorkItem != null) { targetWorkItem.ToWorkItem().Close(); } if (sourceWorkItem != null) { sourceWorkItem.ToWorkItem().Close(); } } else { TraceWriteLine(LogEventLevel.Warning, "SKIP: Unable to migrate {sourceWorkItemTypeName}/{sourceWorkItemId}. Use the TestPlansAndSuitesMigrationContext after you have migrated all Test Cases. ", new Dictionary <string, object>() { { "sourceWorkItemTypeName", sourceWorkItem.Type }, { "sourceWorkItemId", sourceWorkItem.Id } }); } } catch (WebException ex) { Log.LogError(ex, "Some kind of internet pipe blockage"); if (retrys < retryLimit) { TraceWriteLine(LogEventLevel.Warning, "WebException: Will retry in {retrys}s ", new Dictionary <string, object>() { { "retrys", retrys } }); System.Threading.Thread.Sleep(new TimeSpan(0, 0, retrys)); retrys++; TraceWriteLine(LogEventLevel.Warning, "RETRY {Retrys}/{RetryLimit} ", new Dictionary <string, object>() { { "Retrys", retrys }, { "RetryLimit", retryLimit } }); ProcessWorkItem(sourceWorkItem, retryLimit, retrys); } else { TraceWriteLine(LogEventLevel.Error, "ERROR: Failed to create work item. Retry Limit reached "); } } catch (Exception ex) { Log.LogError(ex, ex.ToString()); Telemetry.TrackRequest("ProcessWorkItem", starttime, witstopwatch.Elapsed, "502", false); throw ex; } witstopwatch.Stop(); _elapsedms += witstopwatch.ElapsedMilliseconds; processWorkItemMetrics.Add("ElapsedTimeMS", _elapsedms); var average = new TimeSpan(0, 0, 0, 0, (int)(_elapsedms / _current)); var remaining = new TimeSpan(0, 0, 0, 0, (int)(average.TotalMilliseconds * _count)); TraceWriteLine(LogEventLevel.Information, "Average time of {average:%s}.{average:%fff} per work item and {remaining:%h} hours {remaining:%m} minutes {remaining:%s}.{remaining:%fff} seconds estimated to completion", new Dictionary <string, object>() { { "average", average }, { "remaining", remaining } }); Telemetry.TrackEvent("WorkItemMigrated", processWorkItemParamiters, processWorkItemMetrics); Telemetry.TrackRequest("ProcessWorkItem", starttime, witstopwatch.Elapsed, "200", true); _current++; _count--; }
private MigrationTools._EngineV1.DataContracts.WorkItemData ReplayRevisions(List <MigrationTools._EngineV1.DataContracts.RevisionItem> revisionsToMigrate, MigrationTools._EngineV1.DataContracts.WorkItemData sourceWorkItem, MigrationTools._EngineV1.DataContracts.WorkItemData targetWorkItem, int current) { try { var skipToFinalRevisedWorkItemType = _config.SkipToFinalRevisedWorkItemType; var last = Engine.Source.WorkItems.GetRevision(sourceWorkItem, revisionsToMigrate.Last().Number); string finalDestType = last.Type; if (skipToFinalRevisedWorkItemType && Engine.TypeDefinitionMaps.Items.ContainsKey(finalDestType)) { finalDestType = Engine.TypeDefinitionMaps.Items[finalDestType].Map(); } //If work item hasn't been created yet, create a shell if (targetWorkItem == null) { string targetType = Engine.Source.WorkItems.GetRevision(sourceWorkItem, revisionsToMigrate.First().Number).Type; if (Engine.TypeDefinitionMaps.Items.ContainsKey(targetType)) { targetType = Engine.TypeDefinitionMaps.Items[targetType].Map(); } targetWorkItem = CreateWorkItem_Shell(Engine.Target.WorkItems.Project, sourceWorkItem, skipToFinalRevisedWorkItemType ? finalDestType : targetType); } if (_config.CollapseRevisions) { var data = revisionsToMigrate.Select(rev => Engine.Source.WorkItems.GetRevision(sourceWorkItem, rev.Number)).Select(rev => new { rev.Id, rev.Rev, rev.RevisedDate, Fields = rev.ToWorkItem().Fields.AsDictionary() }); var fileData = JsonConvert.SerializeObject(data, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.None }); var filePath = Path.Combine(Path.GetTempPath(), $"{sourceWorkItem.Id}_PreMigrationHistory.json"); File.WriteAllText(filePath, fileData); targetWorkItem.ToWorkItem().Attachments.Add(new Attachment(filePath, "History has been consolidated into the attached file.")); revisionsToMigrate = revisionsToMigrate.GetRange(revisionsToMigrate.Count - 1, 1); TraceWriteLine(LogEventLevel.Information, " Attached a consolidated set of {RevisionCount} revisions.", new Dictionary <string, object>() { { "RevisionCount", data.Count() } }); } foreach (var revision in revisionsToMigrate) { var currentRevisionWorkItem = Engine.Source.WorkItems.GetRevision(sourceWorkItem, revision.Number); TraceWriteLine(LogEventLevel.Information, " Processing Revision [{RevisionNumber}]", new Dictionary <string, object>() { { "RevisionNumber", revision.Number } }); // Decide on WIT string destType = currentRevisionWorkItem.Type; if (Engine.TypeDefinitionMaps.Items.ContainsKey(destType)) { destType = Engine.TypeDefinitionMaps.Items[destType].Map(); } WorkItemTypeChange(targetWorkItem, skipToFinalRevisedWorkItemType, finalDestType, revision, currentRevisionWorkItem, destType); PopulateWorkItem(currentRevisionWorkItem, targetWorkItem, destType); Engine.FieldMaps.ApplyFieldMappings(currentRevisionWorkItem, targetWorkItem); targetWorkItem.ToWorkItem().Fields["System.ChangedBy"].Value = currentRevisionWorkItem.ToWorkItem().Revisions[revision.Index].Fields["System.ChangedBy"].Value; targetWorkItem.ToWorkItem().Fields["System.History"].Value = currentRevisionWorkItem.ToWorkItem().Revisions[revision.Index].Fields["System.History"].Value; //Debug.WriteLine("Discussion:" + currentRevisionWorkItem.Revisions[revision.Index].Fields["System.History"].Value); TfsReflectedWorkItemId reflectedUri = (TfsReflectedWorkItemId)Engine.Source.WorkItems.CreateReflectedWorkItemId(sourceWorkItem); if (targetWorkItem.ToWorkItem().Fields.Contains(Engine.Target.Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName)) { targetWorkItem.ToWorkItem().Fields[Engine.Target.Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName].Value = reflectedUri.ToString(); } targetWorkItem.SaveToAzureDevOps(); TraceWriteLine(LogEventLevel.Information, " Saved TargetWorkItem {TargetWorkItemId}. Replayed revision {RevisionNumber} of {RevisionsToMigrateCount}", new Dictionary <string, object>() { { "TargetWorkItemId", targetWorkItem.Id }, { "RevisionNumber", revision.Number }, { "RevisionsToMigrateCount", revisionsToMigrate.Count } }); } if (targetWorkItem != null) { ProcessWorkItemAttachments(sourceWorkItem, targetWorkItem, false); ProcessWorkItemLinks(Engine.Source.WorkItems, Engine.Target.WorkItems, sourceWorkItem, targetWorkItem); if (_config.GenerateMigrationComment) { var reflectedUri = targetWorkItem.ToWorkItem().Fields[Engine.Target.Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName].Value; var history = new StringBuilder(); history.Append( $"This work item was migrated from a different project or organization. You can find the old version at <a href=\"{reflectedUri}\">{reflectedUri}</a>."); targetWorkItem.ToWorkItem().History = history.ToString(); } targetWorkItem.SaveToAzureDevOps(); attachmentEnricher.CleanUpAfterSave(); TraceWriteLine(LogEventLevel.Information, "...Saved as {TargetWorkItemId}", new Dictionary <string, object> { { "TargetWorkItemId", targetWorkItem.Id } }); } } catch (Exception ex) { TraceWriteLine(LogEventLevel.Information, "...FAILED to Save"); Log.LogInformation("==============================================================="); if (targetWorkItem != null) { foreach (Field f in targetWorkItem.ToWorkItem().Fields) { TraceWriteLine(LogEventLevel.Information, "{FieldReferenceName} ({FieldName}) | {FieldValue}", new Dictionary <string, object>() { { "FieldReferenceName", f.ReferenceName }, { "FieldName", f.Name }, { "FieldValue", f.Value } }); } } Log.LogInformation("==============================================================="); Log.LogError(ex.ToString(), ex); Log.LogInformation("==============================================================="); } return(targetWorkItem); }