Skip to content

Commit b1f5939

Browse files
authored
Allow to create new root structure nodes (areas, iterations) when there are no sub-nodes (#2760)
Fixes #2759
2 parents 39a1814 + f4448ca commit b1f5939

File tree

2 files changed

+34
-11
lines changed

2 files changed

+34
-11
lines changed

src/MigrationTools.Clients.TfsObjectModel/Tools/TfsNodeStructureTool.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,22 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Text;
54
using System.Text.RegularExpressions;
65
using System.Xml;
76
using Microsoft.Extensions.DependencyInjection;
87
using Microsoft.Extensions.Logging;
98
using Microsoft.Extensions.Options;
109
using Microsoft.TeamFoundation.Common;
1110
using Microsoft.TeamFoundation.Server;
12-
using Microsoft.TeamFoundation.Work.WebApi;
1311
using MigrationTools.Clients;
1412
using MigrationTools.DataContracts;
1513
using MigrationTools.Endpoints;
16-
using MigrationTools.Enrichers;
1714
using MigrationTools.Exceptions;
1815
using MigrationTools.FieldMaps;
19-
using MigrationTools.Processors;
2016
using MigrationTools.Processors.Infrastructure;
2117
using MigrationTools.Services;
2218
using MigrationTools.Tools.Infrastructure;
2319
using Newtonsoft.Json;
24-
using Serilog.Context;
25-
using Serilog.Events;
26-
using static Microsoft.TeamFoundation.WorkItemTracking.Client.Node;
2720
using ILogger = Serilog.ILogger;
2821

2922

@@ -199,7 +192,6 @@ private NodeInfo GetOrCreateNode(string nodePath, DateTime? startDate, DateTime?
199192
Log.LogDebug(" Not Found:", currentAncestorPath);
200193
parentNode = null;
201194
}
202-
203195
}
204196
}
205197
else
@@ -286,7 +278,8 @@ public void ProcessorExecutionBegin(TfsProcessor processor)
286278
Log.LogInformation("Migrating all Nodes before the Processor run.");
287279
MigrateAllNodeStructures();
288280
RefreshForProcessorType(processor);
289-
} else
281+
}
282+
else
290283
{
291284
Log.LogInformation("SKIP: Migrating all Nodes before the Processor run.");
292285
}
@@ -397,13 +390,15 @@ private string GetSystemPath(string newUserPath, TfsNodeStructureType structureT
397390
private static string GetUserFriendlyPath(string systemNodePath)
398391
{
399392
// Shape of the path is \SourceProject\StructureType\Rest\Of\The\Path, user-friendly shape skips StructureType and initial \
400-
var match = Regex.Match(systemNodePath, @"^\\(?<sourceProject>[^\\]+)\\[^\\]+\\(?<restOfThePath>.*)$");
393+
var match = Regex.Match(systemNodePath, @"^\\(?<sourceProject>[^\\]+)\\[^\\]+(\\(?<restOfThePath>.*))?$");
401394
if (!match.Success)
402395
{
403396
throw new InvalidOperationException($"This path is not a valid area or iteration path: {systemNodePath}");
404397
}
398+
string sourceProject = match.Groups["sourceProject"].Value;
399+
string restOfThePath = match.Groups["restOfThePath"].Success ? match.Groups["restOfThePath"].Value : string.Empty;
405400

406-
return $"{match.Groups["sourceProject"].Value}\\{match.Groups["restOfThePath"].Value}";
401+
return restOfThePath == string.Empty ? sourceProject : $"{sourceProject}\\{restOfThePath}";
407402
}
408403

409404
private void MigrateAllNodeStructures()
@@ -471,6 +466,11 @@ private void ProcessCommonStructure(string treeTypeSource, string localizedTreeT
471466

472467
_pathToKnownNodeMap[structureParent.Path] = structureParent;
473468

469+
if (Options.MigrateRootNodes)
470+
{
471+
XmlElement mainNode = sourceTree.ChildNodes.OfType<XmlElement>().First();
472+
CreateNewRootNode(mainNode, nodeStructureType);
473+
}
474474
if (sourceTree.ChildNodes[0].HasChildNodes)
475475
{
476476
// The XPath would look like this: /Nodes/Node[Name=Area]/Children/...
@@ -479,6 +479,20 @@ private void ProcessCommonStructure(string treeTypeSource, string localizedTreeT
479479
}
480480
}
481481

482+
private void CreateNewRootNode(XmlElement node, TfsNodeStructureType nodeStructureType)
483+
{
484+
string userFriendlyPath = GetUserFriendlyPath(node.Attributes["Path"].Value);
485+
string newUserPath = GetNewNodeName(userFriendlyPath, nodeStructureType);
486+
string newSystemPath = GetSystemPath(newUserPath, nodeStructureType, _targetLanguageMaps);
487+
if (!Regex.IsMatch(newSystemPath, @"\\" + nodeStructureType + @"\\?$"))
488+
{
489+
// Do not do anything if there is no node name after structure type for the new (target) node.
490+
// For example, if the path is just "\Project\Area" or "\Project\Iteration".
491+
// This will happen if there are no mappings for nodes.
492+
GetOrCreateNode(newSystemPath, null, null);
493+
}
494+
}
495+
482496
private List<string> _matchedPath = new List<string>();
483497

484498
/// <summary>

src/MigrationTools.Clients.TfsObjectModel/Tools/TfsNodeStructureToolOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ public sealed class TfsNodeStructureToolOptions : ToolOptions, ITfsNodeStructure
3030
/// </summary>
3131
public bool ShouldCreateMissingRevisionPaths { get; set; }
3232
public bool ReplicateAllExistingNodes { get; set; }
33+
34+
/// <summary>
35+
/// By default, only child nodes in node structures are migrated. Turning this on migrates even the root node
36+
/// (default area/iteration). This is useful, when you do not have any child areas ion source, but want to migrate
37+
/// default area in source to some sub-area in target project. You have to set proper mapping for this to work.
38+
/// If after mapping the target node is still just root node (so it is not migrated under some child node),
39+
/// nothing happens – so migrating root nodes works only if the are remapped to some child node in target project.
40+
/// </summary>
41+
public bool MigrateRootNodes { get; set; } = false;
3342
}
3443

3544
public class NodeOptions

0 commit comments

Comments
 (0)