private void ProcessWorkItemLinks(IWorkItemMigrationClient sourceStore, IWorkItemMigrationClient targetStore, WorkItemData sourceWorkItem, 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); } }
// TODO : Make this into the Work Item mapping tool private void PopulateWorkItem(WorkItemData oldWi, 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) && oldWorkItem.Fields[f.ReferenceName].Value != newWorkItem.Fields[f.ReferenceName].Value) { Log.LogDebug("PopulateWorkItem:FieldUpdate: {ReferenceName} | Old:{OldReferenceValue} New:{NewReferenceValue}", f.ReferenceName, oldWorkItem.Fields[f.ReferenceName].Value, newWorkItem.Fields[f.ReferenceName].Value); 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 void CreateExternalLink(ExternalLink sourceLink, WorkItemData target) { var exist = (from Link l in target.ToWorkItem().Links where l is ExternalLink && ((ExternalLink)l).LinkedArtifactUri == ((ExternalLink)sourceLink).LinkedArtifactUri select(ExternalLink) l).SingleOrDefault(); if (exist == null) { Log.Information("Creating new {SourceLinkType} on {TargetId}", sourceLink.GetType().Name, target.Id); ExternalLink el = new ExternalLink(sourceLink.ArtifactLinkType, sourceLink.LinkedArtifactUri); el.Comment = sourceLink.Comment; target.ToWorkItem().Links.Add(el); if (_save) { target.SaveToAzureDevOps(); } } else { Log.Information("Link {SourceLinkType} on {TargetId} already exists", sourceLink.GetType().Name, target.Id); } }
public override WorkItemData FindReflectedWorkItem(WorkItemData workItemToReflect, bool cache) { TfsReflectedWorkItemId ReflectedWorkItemId = new TfsReflectedWorkItemId(workItemToReflect); var workItemToFind = workItemToReflect.ToWorkItem(); WorkItem found = GetFromCache(ReflectedWorkItemId)?.ToWorkItem(); // SHOULD NEVER HAVE A rEFLECTEDiD ON SOURCE! IF WE DO ITS IRRELEVENT AS ITS FROM A PREVIOUS MIGRATION // If we have a Reflected WorkItem ID field on the source store, assume it is pointing to the desired work item on the target store //if (Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName != null && workItemToFind.Fields.Contains(Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName) && !string.IsNullOrEmpty(workItemToFind.Fields[Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName]?.Value?.ToString())) //{ // string rwiid = workItemToFind.Fields[Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName].Value.ToString(); // ReflectedWorkItemId idToFind = GetReflectedWorkItemId(workItemToReflect); // if (int.Parse(idToFind.WorkItemId) == 0) // { // found = null; // } // else // { // try // { // found = Store.GetWorkItem(int.Parse(idToFind.WorkItemId)); // } // catch (DeniedOrNotExistException) // { // found = null; // } // } //} if (found == null) { found = FindReflectedWorkItemByReflectedWorkItemId(ReflectedWorkItemId)?.ToWorkItem(); } if (Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName != null && !workItemToFind.Fields.Contains(Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName)) { if (found == null) { found = FindReflectedWorkItemByMigrationRef(ReflectedWorkItemId)?.ToWorkItem(); } // Too slow! //if (found == null) { found = FindReflectedWorkItemByTitle(workItemToFind.Title); } } if (found != null && cache) { AddToCache(found.AsWorkItemData());/// TODO MEMORY LEAK } return(found?.AsWorkItemData()); }
public override int GetReflectedWorkItemId(WorkItemData workItem, string reflectedWotkItemIdField) { var local = workItem.ToWorkItem(); if (!local.Fields.Contains(reflectedWotkItemIdField)) { return(0); } string rwiid = local.Fields[reflectedWotkItemIdField].Value.ToString(); if (Regex.IsMatch(rwiid, @"(http(s)?://)?([\w-]+\.)+[\w-]+(/[\w- ;,./?%&=]*)?")) { return(int.Parse(rwiid.Substring(rwiid.LastIndexOf(@"/") + 1))); } return(0); }
public override WorkItemData FindReflectedWorkItem(WorkItemData workItemToReflect, bool cache) { TfsReflectedWorkItemId ReflectedWorkItemId = new TfsReflectedWorkItemId(workItemToReflect); var workItemToFind = workItemToReflect.ToWorkItem(); WorkItem found = GetFromCache(ReflectedWorkItemId)?.ToWorkItem(); if (found == null) { found = FindReflectedWorkItemByReflectedWorkItemId(ReflectedWorkItemId)?.ToWorkItem(); } if (found != null && cache) { AddToCache(found.AsWorkItemData());// TODO MEMORY LEAK } return(found?.AsWorkItemData()); }
public void ProcessAttachemnts(WorkItemData source, WorkItemData target, bool save = true) { if (source is null) { throw new ArgumentNullException(nameof(source)); } if (target is null) { throw new ArgumentNullException(nameof(target)); } Log.Information("AttachmentMigrationEnricher: Migrating {AttachmentCount} attachemnts from {SourceWorkItemID} to {TargetWorkItemID}", source.ToWorkItem().Attachments.Count, source.Id, target.Id); _exportWiPath = Path.Combine(_exportBasePath, source.ToWorkItem().Id.ToString()); if (System.IO.Directory.Exists(_exportWiPath)) { System.IO.Directory.Delete(_exportWiPath, true); } System.IO.Directory.CreateDirectory(_exportWiPath); foreach (Attachment wia in source.ToWorkItem().Attachments) { try { string filepath = null; filepath = ExportAttachment(source.ToWorkItem(), wia, _exportWiPath); Log.Debug("AttachmentMigrationEnricher: Exported {Filename} to disk", System.IO.Path.GetFileName(filepath)); if (filepath != null) { ImportAttachemnt(target.ToWorkItem(), filepath, save); Log.Debug("AttachmentMigrationEnricher: Imported {Filename} from disk", System.IO.Path.GetFileName(filepath)); } } catch (Exception ex) { Log.Error(ex, "AttachmentMigrationEnricher:Unable to process atachment from source wi {SourceWorkItemId} called {AttachmentName}", source.ToWorkItem().Id, wia.Name); } } if (save) { target.SaveToAzureDevOps(); Log.Information("Work iTem now has {AttachmentCount} attachemnts", source.ToWorkItem().Attachments.Count); CleanUpAfterSave(); } }
private List <RevisionItem> RevisionsToMigrate(WorkItemData sourceWorkItem, 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 <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); }
public override int GetReflectedWorkItemId(WorkItemData workItem) { Log.Debug("GetReflectedWorkItemId: START"); var local = workItem.ToWorkItem(); if (!local.Fields.Contains(Config.ReflectedWorkItemIDFieldName)) { Log.Debug("GetReflectedWorkItemId: END - no reflected work item id on work item"); return(0); } string rwiid = local.Fields[Config.ReflectedWorkItemIDFieldName].Value.ToString(); if (Regex.IsMatch(rwiid, @"(http(s)?://)?([\w-]+\.)+[\w-]+(/[\w- ;,./?%&=]*)?")) { Log.Debug("GetReflectedWorkItemId: END - Has ReflectedWorkItemIdField and has value"); return(int.Parse(rwiid.Substring(rwiid.LastIndexOf(@"/") + 1))); } Log.Debug("GetReflectedWorkItemId: END - Has ReflectedWorkItemIdField but has no value"); return(0); }
public override ReflectedWorkItemId GetReflectedWorkItemId(WorkItemData workItem) { Log.Debug("GetReflectedWorkItemId: START"); var local = workItem.ToWorkItem(); if (!local.Fields.Contains(Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName)) { Log.Debug("GetReflectedWorkItemId: END - no reflected work item id on work item"); return(null); } string rwiid = local.Fields[Config.AsTeamProjectConfig().ReflectedWorkItemIDFieldName].Value.ToString(); if (!string.IsNullOrEmpty(rwiid)) { Log.Debug("GetReflectedWorkItemId: END - Has ReflectedWorkItemIdField and has value"); return(new TfsReflectedWorkItemId(rwiid)); } Log.Debug("GetReflectedWorkItemId: END - Has ReflectedWorkItemIdField but has no value"); return(null); }
public void AttachSourceRevisionHistoryJsonToTarget(WorkItemData sourceWorkItem, WorkItemData targetWorkItem) { var fileData = JsonConvert.SerializeObject(sourceWorkItem.Revisions, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.None }); var filePath = Path.Combine(Path.GetTempPath(), $"{sourceWorkItem.ProjectName}-ID{sourceWorkItem.Id}-R{sourceWorkItem.Rev}-PreMigrationHistory.json"); // todo: Delete this file after (!) WorkItem has been saved File.WriteAllText(filePath, fileData); if (targetWorkItem.internalObject != null) { targetWorkItem.ToWorkItem().Attachments.Add(new Attachment(filePath, "History has been consolidated into the attached file.")); } Log.LogInformation("Attached a consolidated set of {RevisionCount} revisions.", new Dictionary <string, object>() { { "RevisionCount", sourceWorkItem.Revisions.Count() } }); }
private List <RevisionItem> RevisionsToMigrate(WorkItemData sourceWorkItem, 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 } }); Log.LogDebug("RevisionsToMigrate:----------------------------------------------------"); foreach (RevisionItem item in sortedRevisions) { Log.LogDebug("RevisionsToMigrate: Index:{Index} - Number:{Number} - ChangedDate:{ChangedDate}", item.Index, item.Number, item.ChangedDate); } Log.LogDebug("RevisionsToMigrate:----------------------------------------------------"); return(sortedRevisions); }
internal override void InternalExecute(WorkItemData source, WorkItemData target) { if (source.Fields.ContainsKey(Config.sourceField)) { var sourceVal = source.Fields[Config.sourceField]; var targetWi = target.ToWorkItem(); var t = targetWi.Fields[Config.targetField].FieldDefinition.SystemType; if (sourceVal is null && Config.valueMapping.ContainsKey("null")) { targetWi.Fields[Config.targetField].Value = Convert.ChangeType(Config.valueMapping["null"], t); Log.LogDebug("FieldValueMap: [UPDATE] field value mapped {SourceId}:{SourceField} to {TargetId}:{TargetField}", source.Id, Config.sourceField, target.Id, Config.targetField); } else if (sourceVal != null && Config.valueMapping.ContainsKey(sourceVal.ToString())) { targetWi.Fields[Config.targetField].Value = Convert.ChangeType(Config.valueMapping[sourceVal.ToString()], t); Log.LogDebug("FieldValueMap: [UPDATE] field value mapped {SourceId}:{SourceField} to {TargetId}:{TargetField}", source.Id, Config.sourceField, target.Id, Config.targetField); } else if (sourceVal != null && !string.IsNullOrWhiteSpace(Config.defaultValue)) { targetWi.Fields[Config.targetField].Value = Convert.ChangeType(Config.defaultValue, t); Log.LogDebug("FieldValueMap: [UPDATE] field value mapped {SourceId}:{SourceField} to {TargetId}:{TargetField}", source.Id, Config.sourceField, target.Id, Config.targetField); } }
private void ProcessWorkItem(IWorkItemMigrationClient sourceStore, IWorkItemMigrationClient targetStore, ProjectData destProject, 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, sourceStore.Config.Collection.ToString()); AddParameter("SourceWorkItem", processWorkItemParamiters, sourceWorkItem.Id.ToString()); AddParameter("TargetURL", processWorkItemParamiters, targetStore.Config.Collection.ToString()); AddParameter("TargetProject", processWorkItemParamiters, destProject.Name); AddParameter("RetryLimit", processWorkItemParamiters, retryLimit.ToString()); AddParameter("RetryNumber", processWorkItemParamiters, retrys.ToString()); try { if (sourceWorkItem.Type != "Test Plan" || sourceWorkItem.Type != "Test Suite") { var targetWorkItem = targetStore.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 <RevisionItem> revisionsToMigrate = RevisionsToMigrate(sourceWorkItem, targetWorkItem); if (targetWorkItem == null) { targetWorkItem = ReplayRevisions(revisionsToMigrate, sourceWorkItem, null, destProject, sourceStore, _current, targetStore); AddMetric("Revisions", processWorkItemMetrics, revisionsToMigrate.Count); } else { if (revisionsToMigrate.Count == 0) { ProcessWorkItemAttachments(sourceWorkItem, targetWorkItem, false); ProcessWorkItemLinks(sourceStore, targetStore, 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, destProject, sourceStore, _current, targetStore); 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) { this.SaveWorkItem(targetWorkItem); } 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.Error(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(sourceStore, targetStore, destProject, sourceWorkItem, retryLimit, retrys); } else { TraceWriteLine(LogEventLevel.Error, "ERROR: Failed to create work item. Retry Limit reached "); } } catch (Exception ex) { Log.Error(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 } }); Trace.Flush(); Telemetry.TrackEvent("WorkItemMigrated", processWorkItemParamiters, processWorkItemMetrics); Telemetry.TrackRequest("ProcessWorkItem", starttime, witstopwatch.Elapsed, "200", true); _current++; _count--; }
private void ProcessWorkItem(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 <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) { try { targetWorkItem.SaveToAzureDevOps(); } catch (ValidationException ve) { TraceWriteLine(LogEventLevel.Error, "Unable to save {sourceWorkItemTypeName}/{sourceWorkItemId}." + Environment.NewLine + ve.ToString(), new Dictionary <string, object>() { { "sourceWorkItemTypeName", sourceWorkItem.Type }, { "sourceWorkItemId", sourceWorkItem.Id } }); } } 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 (Microsoft.TeamFoundation.TeamFoundationServiceUnavailableException ex) { Log.LogError(ex, "Team Foundation Service Unavailable:"); DoRetry(ref retrys, retryLimit, sourceWorkItem, true); } catch (WebException ex) { Log.LogError(ex, "Some kind of internet pipe blockage"); DoRetry(ref retrys, retryLimit, sourceWorkItem); } catch (Exception ex) { Log.LogError(ex, ex.ToString()); Telemetry.TrackRequest("ProcessWorkItem", starttime, witstopwatch.Elapsed, "502", false); throw ex; } witstopwatch.Stop(); _elapsedms += witstopwatch.ElapsedMilliseconds; Func <double, double, double> updater = (oldVal, newVal) => { return(newVal); }; processWorkItemMetrics.AddOrUpdate("ElapsedTimeMS", _elapsedms, updater); 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--; }
public override int Enrich(WorkItemData sourceWorkItem, WorkItemData targetWorkItem) { if (sourceWorkItem is null) { throw new ArgumentNullException(nameof(sourceWorkItem)); } if (targetWorkItem is null) { throw new ArgumentNullException(nameof(targetWorkItem)); } Log.LogInformation("GitRepositoryEnricher: Enriching {Id} To fix Git Repo Links", targetWorkItem.Id); List <ExternalLink> newEL = new List <ExternalLink>(); List <ExternalLink> removeEL = new List <ExternalLink>(); int count = 0; foreach (Link l in targetWorkItem.ToWorkItem().Links) { if (l is ExternalLink && gitWits.Contains(l.ArtifactLinkType.Name)) { ExternalLink el = (ExternalLink)l; TfsGitRepositoryInfo sourceRepoInfo = TfsGitRepositoryInfo.Create(el, sourceRepos, Engine, sourceWorkItem?.ProjectName); // if sourceRepo is null ignore this link and keep processing further links if (sourceRepoInfo == null) { continue; } // if repo was not found in source project, try to find it by repoId in the whole project collection if (sourceRepoInfo.GitRepo == null) { var anyProjectSourceRepoInfo = TfsGitRepositoryInfo.Create(el, allSourceRepos, Engine, sourceWorkItem?.ProjectName); // if repo is found in a different project and the repo Name is listed in repo mappings, use it if (anyProjectSourceRepoInfo.GitRepo != null && Engine.GitRepoMaps.Items.ContainsKey(anyProjectSourceRepoInfo.GitRepo.Name)) { sourceRepoInfo = anyProjectSourceRepoInfo; } else { Log.LogWarning("GitRepositoryEnricher: Could not find source git repo - repo referenced: {SourceRepoName}/{TargetRepoName}", anyProjectSourceRepoInfo?.GitRepo?.ProjectReference?.Name, anyProjectSourceRepoInfo?.GitRepo?.Name); } } if (sourceRepoInfo.GitRepo != null) { string targetRepoName = GetTargetRepoName(Engine.GitRepoMaps.Items, sourceRepoInfo); string sourceProjectName = sourceRepoInfo?.GitRepo?.ProjectReference?.Name ?? Engine.Target.Config.AsTeamProjectConfig().Project; string targetProjectName = Engine.Target.Config.AsTeamProjectConfig().Project; TfsGitRepositoryInfo targetRepoInfo = TfsGitRepositoryInfo.Create(targetRepoName, sourceRepoInfo, targetRepos); // if repo was not found in the target project, try to find it in the whole target project collection if (targetRepoInfo.GitRepo == null) { if (Engine.GitRepoMaps.Items.Values.Contains(targetRepoName)) { var anyTargetRepoInCollectionInfo = TfsGitRepositoryInfo.Create(targetRepoName, sourceRepoInfo, allTargetRepos); if (anyTargetRepoInCollectionInfo.GitRepo != null) { targetRepoInfo = anyTargetRepoInCollectionInfo; } } } // Fix commit links if target repo has been found if (targetRepoInfo.GitRepo != null) { Log.LogDebug("GitRepositoryEnricher: Fixing {SourceRepoUrl} to {TargetRepoUrl}?", sourceRepoInfo.GitRepo.RemoteUrl, targetRepoInfo.GitRepo.RemoteUrl); // Create External Link object ExternalLink newLink = null; switch (l.ArtifactLinkType.Name) { case "Branch": newLink = new ExternalLink(((TfsWorkItemMigrationClient)Engine.Target.WorkItems).Store.RegisteredLinkTypes[ArtifactLinkIds.Branch], $"vstfs:///git/ref/{targetRepoInfo.GitRepo.ProjectReference.Id}%2f{targetRepoInfo.GitRepo.Id}%2f{sourceRepoInfo.CommitID}"); break; case "Fixed in Changeset": // TFVC case "Fixed in Commit": newLink = new ExternalLink(((TfsWorkItemMigrationClient)Engine.Target.WorkItems).Store.RegisteredLinkTypes[ArtifactLinkIds.Commit], $"vstfs:///git/commit/{targetRepoInfo.GitRepo.ProjectReference.Id}%2f{targetRepoInfo.GitRepo.Id}%2f{sourceRepoInfo.CommitID}"); break; case "Pull Request": //newLink = new ExternalLink(targetStore.Store.RegisteredLinkTypes[ArtifactLinkIds.PullRequest], // $"vstfs:///Git/PullRequestId/{targetRepoInfo.GitRepo.ProjectReference.Id}%2f{targetRepoInfo.GitRepo.Id}%2f{sourceRepoInfo.CommitID}"); removeEL.Add(el); break; default: Log.LogWarning("Skipping unsupported link type {0}", l.ArtifactLinkType.Name); break; } if (newLink != null) { var elinks = from Link lq in targetWorkItem.ToWorkItem().Links where gitWits.Contains(lq.ArtifactLinkType.Name) select(ExternalLink) lq; var found = (from Link lq in elinks where (((ExternalLink)lq).LinkedArtifactUri.ToLower() == newLink.LinkedArtifactUri.ToLower()) select lq).SingleOrDefault(); if (found == null) { newEL.Add(newLink); } removeEL.Add(el); } } else { Log.LogWarning("GitRepositoryEnricher: Target Repo not found. Cannot map {SourceRepoUrl} to ???", sourceRepoInfo.GitRepo.RemoteUrl); } } } } // add and remove foreach (ExternalLink eln in newEL) { try { Log.LogDebug("GitRepositoryEnricher: Adding {LinkedArtifactUri} ", eln.LinkedArtifactUri); targetWorkItem.ToWorkItem().Links.Add(eln); } catch (Exception) { // eat exception as sometimes TFS thinks this is an attachment } } foreach (ExternalLink elr in removeEL) { if (targetWorkItem.ToWorkItem().Links.Contains(elr)) { try { Log.LogDebug("GitRepositoryEnricher: Removing {LinkedArtifactUri} ", elr.LinkedArtifactUri); targetWorkItem.ToWorkItem().Links.Remove(elr); count++; } catch (Exception) { // eat exception as sometimes TFS thinks this is an attachment } } } if (targetWorkItem.ToWorkItem().IsDirty&& _save) { Log.LogDebug("GitRepositoryEnricher: Saving {TargetWorkItemId}", targetWorkItem.Id); targetWorkItem.ToWorkItem().Fields["System.ChangedBy"].Value = "Migration"; targetWorkItem.SaveToAzureDevOps(); } return(count); }
public override string CreateReflectedWorkItemId(WorkItemData workItem) { var wi = workItem.ToWorkItem(); return(string.Format("{0}/{1}/_workitems/edit/{2}", wi.Store.TeamProjectCollection.Uri.ToString().TrimEnd('/'), wi.Project.Name, wi.Id)); }
public int FixExternalLinks(WorkItemData targetWorkItem, IWorkItemMigrationClient targetStore, WorkItemData sourceWorkItem, bool save = true) { List <ExternalLink> newEL = new List <ExternalLink>(); List <ExternalLink> removeEL = new List <ExternalLink>(); int count = 0; foreach (Link l in targetWorkItem.ToWorkItem().Links) { if (l is ExternalLink && gitWits.Contains(l.ArtifactLinkType.Name)) { ExternalLink el = (ExternalLink)l; GitRepositoryInfo sourceRepoInfo = GitRepositoryInfo.Create(el, sourceRepos, migrationEngine, sourceWorkItem?.ProjectName); // if sourceRepo is null ignore this link and keep processing further links if (sourceRepoInfo == null) { continue; } // if repo was not found in source project, try to find it by repoId in the whole project collection if (sourceRepoInfo.GitRepo == null) { var anyProjectSourceRepoInfo = GitRepositoryInfo.Create(el, allSourceRepos, migrationEngine, sourceWorkItem?.ProjectName); // if repo is found in a different project and the repo Name is listed in repo mappings, use it if (anyProjectSourceRepoInfo.GitRepo != null && migrationEngine.GitRepoMaps.Items.ContainsKey(anyProjectSourceRepoInfo.GitRepo.Name)) { sourceRepoInfo = anyProjectSourceRepoInfo; } else { Trace.WriteLine($"FAIL could not find source git repo - repo referenced: {anyProjectSourceRepoInfo?.GitRepo?.ProjectReference?.Name}/{anyProjectSourceRepoInfo?.GitRepo?.Name}"); } } if (sourceRepoInfo.GitRepo != null) { string targetRepoName = GetTargetRepoName(migrationEngine.GitRepoMaps.Items, sourceRepoInfo); string sourceProjectName = sourceRepoInfo?.GitRepo?.ProjectReference?.Name ?? migrationEngine.Target.Config.Project; string targetProjectName = migrationEngine.Target.Config.Project; GitRepositoryInfo targetRepoInfo = GitRepositoryInfo.Create(targetRepoName, sourceRepoInfo, targetRepos); // if repo was not found in the target project, try to find it in the whole target project collection if (targetRepoInfo.GitRepo == null) { if (migrationEngine.GitRepoMaps.Items.Values.Contains(targetRepoName)) { var anyTargetRepoInCollectionInfo = GitRepositoryInfo.Create(targetRepoName, sourceRepoInfo, allTargetRepos); if (anyTargetRepoInCollectionInfo.GitRepo != null) { targetRepoInfo = anyTargetRepoInCollectionInfo; } } } // Fix commit links if target repo has been found if (targetRepoInfo.GitRepo != null) { Trace.WriteLine($"Fixing {sourceRepoInfo.GitRepo.RemoteUrl} to {targetRepoInfo.GitRepo.RemoteUrl}?"); // Create External Link object ExternalLink newLink = null; switch (l.ArtifactLinkType.Name) { case "Branch": newLink = new ExternalLink(((WorkItemMigrationClient)targetStore).Store.RegisteredLinkTypes[ArtifactLinkIds.Branch], $"vstfs:///git/ref/{targetRepoInfo.GitRepo.ProjectReference.Id}%2f{targetRepoInfo.GitRepo.Id}%2f{sourceRepoInfo.CommitID}"); break; case "Fixed in Changeset": // TFVC case "Fixed in Commit": newLink = new ExternalLink(((WorkItemMigrationClient)targetStore).Store.RegisteredLinkTypes[ArtifactLinkIds.Commit], $"vstfs:///git/commit/{targetRepoInfo.GitRepo.ProjectReference.Id}%2f{targetRepoInfo.GitRepo.Id}%2f{sourceRepoInfo.CommitID}"); break; case "Pull Request": //newLink = new ExternalLink(targetStore.Store.RegisteredLinkTypes[ArtifactLinkIds.PullRequest], // $"vstfs:///Git/PullRequestId/{targetRepoInfo.GitRepo.ProjectReference.Id}%2f{targetRepoInfo.GitRepo.Id}%2f{sourceRepoInfo.CommitID}"); removeEL.Add(el); break; default: Trace.WriteLine(String.Format("Skipping unsupported link type {0}", l.ArtifactLinkType.Name)); break; } if (newLink != null) { var elinks = from Link lq in targetWorkItem.ToWorkItem().Links where gitWits.Contains(lq.ArtifactLinkType.Name) select(ExternalLink) lq; var found = (from Link lq in elinks where (((ExternalLink)lq).LinkedArtifactUri.ToLower() == newLink.LinkedArtifactUri.ToLower()) select lq).SingleOrDefault(); if (found == null) { newEL.Add(newLink); } removeEL.Add(el); } } else { Trace.WriteLine($"FAIL: cannot map {sourceRepoInfo.GitRepo.RemoteUrl} to ???"); } } } } // add and remove foreach (ExternalLink eln in newEL) { try { Trace.WriteLine("Adding " + eln.LinkedArtifactUri); targetWorkItem.ToWorkItem().Links.Add(eln); } catch (Exception) { // eat exception as sometimes TFS thinks this is an attachment } } foreach (ExternalLink elr in removeEL) { if (targetWorkItem.ToWorkItem().Links.Contains(elr)) { try { Trace.WriteLine("Removing " + elr.LinkedArtifactUri); targetWorkItem.ToWorkItem().Links.Remove(elr); count++; } catch (Exception) { // eat exception as sometimes TFS thinks this is an attachment } } } if (targetWorkItem.ToWorkItem().IsDirty&& save) { Trace.WriteLine($"Saving {targetWorkItem.Id}"); targetWorkItem.ToWorkItem().Fields["System.ChangedBy"].Value = "Migration"; targetWorkItem.SaveToAzureDevOps(); } return(count); }
/** * from https://gist.github.com/pietergheysens/792ed505f09557e77ddfc1b83531e4fb */ protected override void FixEmbededImages(WorkItemData wi, string oldTfsurl, string newTfsurl, string sourcePersonalAccessToken = "") { Log.LogInformation("EmbededImagesRepairEnricher: Fixing HTML field attachments for work item {Id} from {OldTfsurl} to {NewTfsUrl}", wi.Id, oldTfsurl, newTfsurl); var oldTfsurlOppositeSchema = GetUrlWithOppositeSchema(oldTfsurl); foreach (Field field in wi.ToWorkItem().Fields) { if (field.FieldDefinition.FieldType != FieldType.Html && field.FieldDefinition.FieldType != FieldType.History) { continue; } try { MatchCollection matches = Regex.Matches((string)field.Value, RegexPatternForImageUrl); foreach (Match match in matches) { if (!match.Value.ToLower().Contains(oldTfsurl.ToLower()) && !match.Value.ToLower().Contains(oldTfsurlOppositeSchema.ToLower())) { continue; } //save image locally and upload as attachment Match newFileNameMatch = Regex.Match(match.Value, RegexPatternForImageFileName, RegexOptions.IgnoreCase); if (!newFileNameMatch.Success) { continue; } Log.LogDebug("EmbededImagesRepairEnricher: field '{fieldName}' has match: {matchValue}", field.Name, System.Net.WebUtility.HtmlDecode(match.Value)); string fullImageFilePath = Path.GetTempPath() + newFileNameMatch.Value; try { using (var httpClient = new HttpClient(_httpClientHandler, false)) { if (!string.IsNullOrEmpty(sourcePersonalAccessToken)) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", "", sourcePersonalAccessToken)))); } var result = DownloadFile(httpClient, match.Value, fullImageFilePath); if (!result.IsSuccessStatusCode) { if (_ignore404Errors && result.StatusCode == HttpStatusCode.NotFound) { Log.LogDebug("EmbededImagesRepairEnricher: Image {MatchValue} could not be found in WorkItem {WorkItemId}, Field {FieldName}", match.Value, wi.Id, field.Name); continue; } else { result.EnsureSuccessStatusCode(); } } } if (GetImageFormat(File.ReadAllBytes(fullImageFilePath)) == ImageFormat.unknown) { throw new Exception($"Downloaded image [{fullImageFilePath}] from Work Item [{wi.ToWorkItem().Id}] Field: [{field.Name}] could not be identified as an image. Authentication issue?"); } var ar = UploadImageToTarget(wi.ToWorkItem(), fullImageFilePath); field.Value = field.Value.ToString().Replace(match.Value, ar.Url); } finally { if (File.Exists(fullImageFilePath)) { File.Delete(fullImageFilePath); } } } } catch (Exception ex) { Log.LogError(ex, "EmbededImagesRepairEnricher: Unable to fix HTML field attachments for work item {wiId} from {oldTfsurl} to {newTfsurl}", wi.Id, oldTfsurl, newTfsurl); } } }
private WorkItemData ReplayRevisions(List <RevisionItem> revisionsToMigrate, WorkItemData sourceWorkItem, 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); 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); string reflectedUri = Engine.Source.WorkItems.CreateReflectedWorkItemId(sourceWorkItem); if (targetWorkItem.ToWorkItem().Fields.Contains(Engine.Target.Config.ReflectedWorkItemIDFieldName)) { targetWorkItem.ToWorkItem().Fields[Engine.Target.Config.ReflectedWorkItemIDFieldName].Value = reflectedUri; } 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"); 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.LogError(ex.ToString(), ex); } return(targetWorkItem); }
public override int Enrich(WorkItemData sourceWorkItemLinkStart, WorkItemData targetWorkItemLinkStart) { if (sourceWorkItemLinkStart is null) { throw new ArgumentNullException(nameof(sourceWorkItemLinkStart)); } if (targetWorkItemLinkStart is null) { throw new ArgumentNullException(nameof(targetWorkItemLinkStart)); } if (targetWorkItemLinkStart.Id == "0") { throw new IndexOutOfRangeException("Target work item must be saved before you can add a link"); } if (ShouldCopyLinks(sourceWorkItemLinkStart, targetWorkItemLinkStart)) { foreach (Link item in sourceWorkItemLinkStart.ToWorkItem().Links) { try { Log.LogInformation("Migrating link for {sourceWorkItemLinkStartId} of type {ItemGetTypeName}", sourceWorkItemLinkStart.Id, item.GetType().Name); if (IsHyperlink(item)) { CreateHyperlink((Hyperlink)item, targetWorkItemLinkStart); } else if (IsRelatedLink(item)) { RelatedLink rl = (RelatedLink)item; CreateRelatedLink(sourceWorkItemLinkStart, rl, targetWorkItemLinkStart); } else if (IsExternalLink(item)) { var el = (ExternalLink)item; if (!IsBuildLink(el)) { CreateExternalLink(el, targetWorkItemLinkStart); } } else { UnknownLinkTypeException ex = new UnknownLinkTypeException(string.Format(" [UnknownLinkType] Unable to {0}", item.GetType().Name)); Log.LogError(ex, "LinkMigrationContext"); throw ex; } } catch (WorkItemLinkValidationException ex) { sourceWorkItemLinkStart.ToWorkItem().Reset(); targetWorkItemLinkStart.ToWorkItem().Reset(); Log.LogError(ex, "[WorkItemLinkValidationException] Adding link for wiSourceL={sourceWorkItemLinkStartId}", sourceWorkItemLinkStart.Id); } catch (FormatException ex) { sourceWorkItemLinkStart.ToWorkItem().Reset(); targetWorkItemLinkStart.ToWorkItem().Reset(); Log.LogError(ex, "[CREATE-FAIL] Adding Link for wiSourceL={sourceWorkItemLinkStartId}", sourceWorkItemLinkStart.Id); } catch (UnexpectedErrorException ex) { sourceWorkItemLinkStart.ToWorkItem().Reset(); targetWorkItemLinkStart.ToWorkItem().Reset(); Log.LogError(ex, "[UnexpectedErrorException] Adding Link for wiSourceL={sourceWorkItemLinkStartId}", sourceWorkItemLinkStart.Id); } } } if (sourceWorkItemLinkStart.Type == "Test Case") { MigrateSharedSteps(sourceWorkItemLinkStart, targetWorkItemLinkStart); } return(0); }
private WorkItemData ReplayRevisions(List <RevisionItem> revisionsToMigrate, WorkItemData sourceWorkItem, WorkItemData targetWorkItem, ProjectData destProject, IWorkItemMigrationClient sourceStore, int current, IWorkItemMigrationClient targetStore) { try { var skipToFinalRevisedWorkItemType = _config.SkipToFinalRevisedWorkItemType; var last = sourceStore.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) { targetWorkItem = CreateWorkItem_Shell(destProject, sourceWorkItem, skipToFinalRevisedWorkItemType ? finalDestType : sourceStore.GetRevision(sourceWorkItem, revisionsToMigrate.First().Number).Type); } if (_config.CollapseRevisions) { var data = revisionsToMigrate.Select(rev => sourceStore.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 = sourceStore.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(); } //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(); patchDoc.Add(typePatch); patchDoc.Add(datePatch); _witClient.UpdateWorkItemAsync(patchDoc, int.Parse(targetWorkItem.Id), bypassRules: true).Wait(); } 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); var fails = targetWorkItem.ToWorkItem().Validate(); foreach (Field f in fails) { TraceWriteLine(LogEventLevel.Information, "{Current} - Invalid: {CurrentRevisionWorkItemId}-{CurrentRevisionWorkItemTypeName}-{FieldReferenceName}-{SourceWorkItemTitle} Value: {FieldValue}", new Dictionary <string, object>() { { "Current", current }, { "CurrentRevisionWorkItemId", currentRevisionWorkItem.Id }, { "CurrentRevisionWorkItemTypeName", currentRevisionWorkItem.Type }, { "FieldReferenceName", f.ReferenceName }, { "SourceWorkItemTitle", sourceWorkItem.Title }, { "FieldValue", f.Value } }); } //if (fails.Count > 0) // TODO Do we want to stop or continue? //{ // contextLog.Error("Unable to save revission due to the error in validation"); // break; //} targetWorkItem.ToWorkItem().Save(); 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(sourceStore, targetStore, sourceWorkItem, targetWorkItem); string reflectedUri = sourceStore.CreateReflectedWorkItemId(sourceWorkItem); if (targetWorkItem.ToWorkItem().Fields.Contains(Engine.Target.Config.ReflectedWorkItemIDFieldName)) { targetWorkItem.ToWorkItem().Fields[Engine.Target.Config.ReflectedWorkItemIDFieldName].Value = reflectedUri; } 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(); this.SaveWorkItem(targetWorkItem); attachmentEnricher.CleanUpAfterSave(); TraceWriteLine(LogEventLevel.Information, "...Saved as {TargetWorkItemId}", new Dictionary <string, object> { { "TargetWorkItemId", targetWorkItem.Id } }); } } catch (Exception ex) { TraceWriteLine(LogEventLevel.Information, "...FAILED to Save"); 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.Error(ex.ToString(), ex); } return(targetWorkItem); }
private WorkItemData ReplayRevisions(List <RevisionItem> revisionsToMigrate, WorkItemData sourceWorkItem, 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, Rev = revWi.Rev, RevisedDate = revWi.ChangedDate, 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); if (!string.IsNullOrEmpty(targetWorkItem.Id)) { 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 CreateRelatedLink(WorkItemData wiSourceL, RelatedLink item, WorkItemData wiTargetL) { RelatedLink rl = (RelatedLink)item; WorkItemData wiSourceR = null; WorkItemData wiTargetR = null; try { wiSourceR = Engine.Source.WorkItems.GetWorkItem(rl.RelatedWorkItemId.ToString()); } catch (Exception ex) { Log.LogError(ex, " [FIND-FAIL] Adding Link of type {0} where wiSourceL={1}, wiTargetL={2} ", rl.LinkTypeEnd.ImmutableName, wiSourceL.Id, wiTargetL.Id); return; } try { wiTargetR = GetRightHandSideTargetWi(wiSourceL, wiSourceR, wiTargetL); } catch (Exception ex) { Log.LogError(ex, " [FIND-FAIL] Adding Link of type {0} where wiSourceL={1}, wiTargetL={2} ", rl.LinkTypeEnd.ImmutableName, wiSourceL.Id, wiTargetL.Id); return; } if (wiTargetR != null) { bool IsExisting = false; try { var exist = ( from Link l in wiTargetL.ToWorkItem().Links where l is RelatedLink && ((RelatedLink)l).RelatedWorkItemId.ToString() == wiTargetR.Id && ((RelatedLink)l).LinkTypeEnd.ImmutableName == item.LinkTypeEnd.ImmutableName select(RelatedLink) l).SingleOrDefault(); IsExisting = (exist != null); } catch (Exception ex) { Log.LogError(ex, " [SKIP] Unable to migrate links where wiSourceL={0}, wiSourceR={1}, wiTargetL={2}", ((wiSourceL != null) ? wiSourceL.Id.ToString() : "NotFound"), ((wiSourceR != null) ? wiSourceR.Id.ToString() : "NotFound"), ((wiTargetL != null) ? wiTargetL.Id.ToString() : "NotFound")); return; } if (!IsExisting && !wiTargetR.ToWorkItem().IsAccessDenied) { if (wiSourceR.Id != wiTargetR.Id) { Log.LogInformation(" [CREATE-START] Adding Link of type {0} where wiSourceL={1}, wiSourceR={2}, wiTargetL={3}, wiTargetR={4} ", rl.LinkTypeEnd.ImmutableName, wiSourceL.Id, wiSourceR.Id, wiTargetL.Id, wiTargetR.Id); var client = (TfsWorkItemMigrationClient)Engine.Target.WorkItems; if (!client.Store.WorkItemLinkTypes.LinkTypeEnds.Contains(rl.LinkTypeEnd.ImmutableName)) { Log.LogError($" [SKIP] Unable to migrate Link because type {rl.LinkTypeEnd.ImmutableName} does not exist in the target project."); return; } WorkItemLinkTypeEnd linkTypeEnd = client.Store.WorkItemLinkTypes.LinkTypeEnds[rl.LinkTypeEnd.ImmutableName]; RelatedLink newRl = new RelatedLink(linkTypeEnd, int.Parse(wiTargetR.Id)); if (linkTypeEnd.ImmutableName == "System.LinkTypes.Hierarchy-Forward") { var potentialParentConflictLink = ( // TF201036: You cannot add a Child link between work items xxx and xxx because a work item can have only one Parent link. from Link l in wiTargetR.ToWorkItem().Links where l is RelatedLink && ((RelatedLink)l).LinkTypeEnd.ImmutableName == "System.LinkTypes.Hierarchy-Reverse" select(RelatedLink) l).SingleOrDefault(); if (potentialParentConflictLink != null) { wiTargetR.ToWorkItem().Links.Remove(potentialParentConflictLink); } linkTypeEnd = ((TfsWorkItemMigrationClient)Engine.Target.WorkItems).Store.WorkItemLinkTypes.LinkTypeEnds["System.LinkTypes.Hierarchy-Reverse"]; RelatedLink newLl = new RelatedLink(linkTypeEnd, int.Parse(wiTargetL.Id)); wiTargetR.ToWorkItem().Links.Add(newLl); wiTargetR.ToWorkItem().Fields["System.ChangedBy"].Value = "Migration"; wiTargetR.SaveToAzureDevOps(); } else { if (linkTypeEnd.ImmutableName == "System.LinkTypes.Hierarchy-Reverse") { var potentialParentConflictLink = ( // TF201065: You can not add a Parent link to this work item because a work item can have only one link of this type. from Link l in wiTargetL.ToWorkItem().Links where l is RelatedLink && ((RelatedLink)l).LinkTypeEnd.ImmutableName == "System.LinkTypes.Hierarchy-Reverse" select(RelatedLink) l).SingleOrDefault(); if (potentialParentConflictLink != null) { wiTargetL.ToWorkItem().Links.Remove(potentialParentConflictLink); } } wiTargetL.ToWorkItem().Links.Add(newRl); if (_save) { wiTargetL.ToWorkItem().Fields["System.ChangedBy"].Value = "Migration"; wiTargetL.SaveToAzureDevOps(); } } Log.LogInformation( " [CREATE-SUCCESS] Adding Link of type {0} where wiSourceL={1}, wiSourceR={2}, wiTargetL={3}, wiTargetR={4} ", rl.LinkTypeEnd.ImmutableName, wiSourceL.Id, wiSourceR.Id, wiTargetL.Id, wiTargetR.Id); } else { Log.LogInformation( " [SKIP] Unable to migrate link where Link of type {0} where wiSourceL={1}, wiSourceR={2}, wiTargetL={3}, wiTargetR={4} as target WI has not been migrated", rl.LinkTypeEnd.ImmutableName, wiSourceL.Id, wiSourceR.Id, wiTargetL.Id, wiTargetR.Id); } } else { if (IsExisting) { Log.LogInformation(" [SKIP] Already Exists a Link of type {0} where wiSourceL={1}, wiSourceR={2}, wiTargetL={3}, wiTargetR={4} ", rl.LinkTypeEnd.ImmutableName, wiSourceL.Id, wiSourceR.Id, wiTargetL.Id, wiTargetR.Id); } if (wiTargetR.ToWorkItem().IsAccessDenied) { Log.LogInformation(" [AccessDenied] The Target work item is inaccessable to create a Link of type {0} where wiSourceL={1}, wiSourceR={2}, wiTargetL={3}, wiTargetR={4} ", rl.LinkTypeEnd.ImmutableName, wiSourceL.Id, wiSourceR.Id, wiTargetL.Id, wiTargetR.Id); } } } else { Log.LogInformation(" [SKIP] Cant find wiTargetR where wiSourceL={0}, wiSourceR={1}, wiTargetL={2}", wiSourceL.Id, wiSourceR.Id, wiTargetL.Id); } }
private WorkItemData ReplayRevisions(List <RevisionItem> revisionsToMigrate, WorkItemData sourceWorkItem, WorkItemData targetWorkItem) { try { var skipToFinalRevisedWorkItemType = _config.SkipToFinalRevisedWorkItemType; var 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) { var 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.AttachRevisionHistory) { revisionManager.AttachSourceRevisionHistoryJsonToTarget(sourceWorkItem, targetWorkItem); } 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 var destType = currentRevisionWorkItem.Type; if (Engine.TypeDefinitionMaps.Items.ContainsKey(destType)) { destType = Engine.TypeDefinitionMaps.Items[destType].Map(); } 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") var changedBy = targetWorkItem.ToWorkItem().Fields["System.ChangedBy"].Value.ToString(); targetWorkItem.ToWorkItem().Fields["System.ChangedBy"].Value = revision.Fields["System.ChangedBy"].Value; targetWorkItem.ToWorkItem().Fields["System.History"].Value = revision.Fields["System.History"].Value; var 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(); ProcessHTMLFieldAttachements(targetWorkItem); ProcessWorkItemEmbeddedLinks(sourceWorkItem, targetWorkItem); 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 } }); // Change this back to the original value as this object is mutated, and this value is needed elsewhere. targetWorkItem.ToWorkItem().Fields["System.ChangedBy"].Value = changedBy; } if (targetWorkItem != null) { ProcessWorkItemAttachments(sourceWorkItem, targetWorkItem, false); if (!string.IsNullOrEmpty(targetWorkItem.Id)) { 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); }
/** * from https://gist.github.com/pietergheysens/792ed505f09557e77ddfc1b83531e4fb */ public override void FixEmbededImages(WorkItemData wi, string oldTfsurl, string newTfsurl, string sourcePersonalAccessToken = "") { Debug.WriteLine($"Searching for urls: {oldTfsurl} and {GetUrlWithOppositeSchema(oldTfsurl)}"); bool wiUpdated = false; bool hasCandidates = false; var oldTfsurlOppositeSchema = GetUrlWithOppositeSchema(oldTfsurl); string regExSearchForImageUrl = "(?<=<img.*src=\")[^\"]*"; foreach (Field field in wi.ToWorkItem().Fields) { if (field.FieldDefinition.FieldType == FieldType.Html) { MatchCollection matches = Regex.Matches((string)field.Value, regExSearchForImageUrl); string regExSearchFileName = "(?<=FileName=)[^=]*"; foreach (Match match in matches) { if (match.Value.ToLower().Contains(oldTfsurl.ToLower()) || match.Value.ToLower().Contains(oldTfsurlOppositeSchema.ToLower())) { //save image locally and upload as attachment Match newFileNameMatch = Regex.Match(match.Value, regExSearchFileName, RegexOptions.IgnoreCase); if (newFileNameMatch.Success) { Trace.WriteLine($"field '{field.Name}' has match: {System.Net.WebUtility.HtmlDecode(match.Value)}"); string fullImageFilePath = Path.GetTempPath() + newFileNameMatch.Value; using (var httpClient = new HttpClient(_httpClientHandler, false)) { if (!string.IsNullOrEmpty(sourcePersonalAccessToken)) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", "", sourcePersonalAccessToken)))); } var result = DownloadFile(httpClient, match.Value, fullImageFilePath); if (!result.IsSuccessStatusCode) { if (_ignore404Errors && result.StatusCode == HttpStatusCode.NotFound) { Trace.WriteLine($"Image {match.Value} could not be found in WorkItem {wi.ToWorkItem().Id}, Field {field.Name}"); continue; } else { result.EnsureSuccessStatusCode(); } } } if (GetImageFormat(File.ReadAllBytes(fullImageFilePath)) == ImageFormat.unknown) { throw new Exception($"Downloaded image [{fullImageFilePath}] from Work Item [{wi.ToWorkItem().Id}] Field: [{field.Name}] could not be identified as an image. Authentication issue?"); } int attachmentIndex = wi.ToWorkItem().Attachments.Add(new Attachment(fullImageFilePath)); wi.SaveToAzureDevOps(); var newImageLink = wi.ToWorkItem().Attachments[attachmentIndex].Uri.ToString(); field.Value = field.Value.ToString().Replace(match.Value, newImageLink); wi.ToWorkItem().Attachments.RemoveAt(attachmentIndex); wi.SaveToAzureDevOps(); wiUpdated = true; File.Delete(fullImageFilePath); } } } } } }
public override int Enrich(WorkItemData sourceWorkItem, WorkItemData targetWorkItem) { string oldTfsurl = Engine.Source.Config.AsTeamProjectConfig().Collection.ToString(); string newTfsurl = Engine.Target.Config.AsTeamProjectConfig().Collection.ToString(); Log.LogInformation("{LogTypeName}: Fixing embedded mention links on target work item {targetWorkItemId} from {oldTfsurl} to {newTfsurl}", LogTypeName, targetWorkItem.Id, oldTfsurl, newTfsurl); foreach (Field field in targetWorkItem.ToWorkItem().Fields) { if (field.Value == null || string.IsNullOrWhiteSpace(field.Value.ToString()) || (field.FieldDefinition.FieldType != FieldType.Html && field.FieldDefinition.FieldType != FieldType.History)) { continue; } try { var anchorTagMatches = Regex.Matches((string)field.Value, RegexPatternLinkAnchorTag); foreach (Match anchorTagMatch in anchorTagMatches) { if (!anchorTagMatch.Success) { continue; } var href = anchorTagMatch.Groups["href"].Value; var version = anchorTagMatch.Groups["version"].Value; var value = anchorTagMatch.Groups["value"].Value; if (string.IsNullOrWhiteSpace(href) || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(value)) { continue; } var workItemLinkMatch = Regex.Match(href, RegexPatternWorkItemUrl); if (workItemLinkMatch.Success) { var workItemId = workItemLinkMatch.Groups["id"].Value; Log.LogDebug("{LogTypeName}: Source work item {workItemId} mention link traced on field {fieldName} on target work item {targetWorkItemId}.", LogTypeName, workItemId, field.Name, targetWorkItem.Id); var sourceLinkWi = Engine.Source.WorkItems.GetWorkItem(workItemId); if (sourceLinkWi != null) { var linkWI = Engine.Target.WorkItems.FindReflectedWorkItemByReflectedWorkItemId(sourceLinkWi); if (linkWI != null) { var replaceValue = anchorTagMatch.Value.Replace(workItemId, linkWI.Id); field.Value = field.Value.ToString().Replace(anchorTagMatch.Value, replaceValue); Log.LogInformation("{LogTypeName}: Source work item {workItemId} mention link was successfully replaced with target work item {linkWIId} mention link on field {fieldName} on target work item {targetWorkItemId}.", LogTypeName, workItemId, linkWI.Id, field.Name, targetWorkItem.Id); } else { var replaceValue = value; field.Value = field.Value.ToString().Replace(anchorTagMatch.Value, replaceValue); Log.LogError("{LogTypeName}: [SKIP] Matching target work item mention link for source work item {workItemId} mention link on field {fieldName} on target work item {targetWorkItemId} was not found on the target collection. So link is replaced with just simple text.", LogTypeName, workItemId, field.Name, targetWorkItem.Id); } } else { Log.LogInformation("{LogTypeName}: [SKIP] Source work item {workItemId} mention link on field {fieldName} on target work item {targetWorkItemId} was not found on the source collection.", LogTypeName, workItemId, field.Name, targetWorkItem.Id); } } else if ((href.StartsWith("mailto:") || href.StartsWith("#")) && value.StartsWith("@")) { var displayName = value.Substring(1); Log.LogDebug("{LogTypeName}: User identity {displayName} mention traced on field {fieldName} on target work item {targetWorkItemId}.", LogTypeName, displayName, field.Name, targetWorkItem.Id); var identity = _targetTeamFoundationIdentitiesLazyCache.Value.FirstOrDefault(i => i.DisplayName == displayName); if (identity != null) { var replaceValue = anchorTagMatch.Value.Replace(href, "#").Replace(version, $"data-vss-mention=\"version:2.0,{identity.TeamFoundationId}\""); field.Value = field.Value.ToString().Replace(anchorTagMatch.Value, replaceValue); Log.LogInformation("{LogTypeName}: User identity {displayName} mention was successfully matched on field {fieldName} on target work item {targetWorkItemId}.", LogTypeName, displayName, field.Name, targetWorkItem.Id); } else { Log.LogInformation("{LogTypeName}: [SKIP] Matching user identity {displayName} mention was not found on field {fieldName} on target work item {targetWorkItemId}. So left it as it is.", LogTypeName, displayName, field.Name, targetWorkItem.Id); } } } } catch (Exception ex) { Log.LogError(ex, "{LogTypeName}: Unable to fix embedded mention links on field {fieldName} on target work item {targetWorkItemId} from {oldTfsurl} to {newTfsurl}", LogTypeName, field.Name, targetWorkItem.Id, oldTfsurl, newTfsurl); } } return(0); }