Skip to content

Commit da61b01

Browse files
authored
Merge branch 'main' into copilot/fix-2747
2 parents 2521e51 + c9587bc commit da61b01

File tree

3 files changed

+70
-78
lines changed

3 files changed

+70
-78
lines changed

src/MigrationTools.Clients.TfsObjectModel.Tests/Tools/TfsNodeStructureTests.cs

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
using System.Collections.Generic;
2-
using Microsoft.Extensions.Configuration;
1+
using System;
2+
using System.Collections.Generic;
33
using Microsoft.Extensions.DependencyInjection;
44
using Microsoft.TeamFoundation.TestManagement.WebApi;
55
using Microsoft.VisualStudio.TestTools.UnitTesting;
6-
using MigrationTools.Tests;
7-
using System.Threading.Tasks;
86
using MigrationTools.Shadows;
9-
using System;
107
using MigrationTools.Tools;
118

129
namespace MigrationTools.Tests.Tools
@@ -22,10 +19,9 @@ public void GetTfsNodeStructureTool_WithDifferentAreaPath()
2219
options.Enabled = true;
2320
options.Areas = new NodeOptions()
2421
{
25-
Mappings = new Dictionary<string, string>()
26-
{
27-
{ @"^SourceProject\\PUL", "TargetProject\\test\\PUL" }
28-
}
22+
Mappings = [
23+
new() { Match = @"^SourceProject\\PUL", Replacement = "TargetProject\\test\\PUL" }
24+
]
2925
};
3026
var nodeStructure = GetTfsNodeStructureTool(options);
3127

@@ -82,17 +78,15 @@ public void TestFixAreaPath_WhenAreaPathInQuery_WithPrefixProjectToNodesEnabled_
8278
options.Enabled = true;
8379
options.Areas = new NodeOptions()
8480
{
85-
Mappings = new Dictionary<string, string>()
86-
{
87-
{ "^SourceServer\\\\(.*)" , "TargetServer\\SourceServer\\$1" }
88-
}
81+
Mappings = [
82+
new() { Match = "^SourceServer\\\\(.*)", Replacement = "TargetServer\\SourceServer\\$1" }
83+
]
8984
};
9085
options.Iterations = new NodeOptions()
9186
{
92-
Mappings = new Dictionary<string, string>()
93-
{
94-
{ "^SourceServer\\\\(.*)" , "TargetServer\\SourceServer\\$1" }
95-
}
87+
Mappings = [
88+
new() { Match = "^SourceServer\\\\(.*)", Replacement = "TargetServer\\SourceServer\\$1" }
89+
]
9690
};
9791
var nodeStructure = GetTfsNodeStructureTool(options);
9892

@@ -124,18 +118,16 @@ public void TestFixAreaPath_WhenAreaPathInQuery_WithPrefixProjectToNodesEnabled_
124118
options.Enabled = true;
125119
options.Areas = new NodeOptions()
126120
{
127-
Mappings = new Dictionary<string, string>()
128-
{
129-
{ "^Source Project\\\\(.*)" , "Target Project\\Source Project\\$1" }
130-
}
121+
Mappings = [
122+
new() { Match = "^Source Project\\\\(.*)", Replacement = "Target Project\\Source Project\\$1" }
123+
]
131124
};
132125

133126
options.Iterations = new NodeOptions()
134127
{
135-
Mappings = new Dictionary<string, string>()
136-
{
137-
{ "^Source Project\\\\(.*)" , "Target Project\\Source Project\\$1" }
138-
}
128+
Mappings = [
129+
new() { Match = "^Source Project\\\\(.*)", Replacement = "Target Project\\Source Project\\$1" }
130+
]
139131
};
140132
var settings = new TfsNodeStructureToolSettings() { SourceProjectName = "Source Project", TargetProjectName = "Target Project", FoundNodes = new Dictionary<string, bool>() };
141133
var nodeStructure = GetTfsNodeStructureTool(options, settings);
@@ -225,7 +217,7 @@ private static TfsNodeStructureTool GetTfsNodeStructureTool(TfsNodeStructureTool
225217

226218
private static TfsNodeStructureTool GetTfsNodeStructureTool()
227219
{
228-
var options = new TfsNodeStructureToolOptions() { Enabled = true, Areas = new NodeOptions { Mappings = new Dictionary<string, string>() }, Iterations = new NodeOptions { Mappings = new Dictionary<string, string>() } };
220+
var options = new TfsNodeStructureToolOptions() { Enabled = true, Areas = new NodeOptions(), Iterations = new NodeOptions() };
229221
var settings = new TfsNodeStructureToolSettings() { SourceProjectName = "SourceServer", TargetProjectName = "TargetServer", FoundNodes = new Dictionary<string, bool>() };
230222
return GetTfsNodeStructureTool(options, settings);
231223
}
@@ -255,4 +247,4 @@ private static TfsNodeStructureTool GetTfsNodeStructureTool(TfsNodeStructureTool
255247
}
256248

257249
}
258-
}
250+
}

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,17 @@ public string GetNewNodeName(string sourceNodePath, TfsNodeStructureType nodeStr
117117
{
118118
foreach (var mapper in mappers)
119119
{
120-
Log.LogDebug("NodeStructureEnricher.GetNewNodeName::Mappers::{key}", mapper.Key);
121-
if (Regex.IsMatch(sourceNodePath, mapper.Key, RegexOptions.IgnoreCase))
120+
Log.LogDebug("NodeStructureEnricher.GetNewNodeName::Mappers::{key}", mapper.Match);
121+
if (Regex.IsMatch(sourceNodePath, mapper.Match, RegexOptions.IgnoreCase))
122122
{
123-
Log.LogDebug("NodeStructureEnricher.GetNewNodeName::Mappers::{key}::Match", mapper.Key);
124-
string replacement = Regex.Replace(sourceNodePath, mapper.Key, mapper.Value);
125-
Log.LogDebug("NodeStructureEnricher.GetNewNodeName::Mappers::{key}::replaceWith({replace})", mapper.Key, replacement);
123+
Log.LogDebug("NodeStructureEnricher.GetNewNodeName::Mappers::{key}::Match", mapper.Match);
124+
string replacement = Regex.Replace(sourceNodePath, mapper.Match, mapper.Replacement);
125+
Log.LogDebug("NodeStructureEnricher.GetNewNodeName::Mappers::{key}::replaceWith({replace})", mapper.Match, replacement);
126126
return replacement;
127127
}
128128
else
129129
{
130-
Log.LogDebug("NodeStructureEnricher.GetNewNodeName::Mappers::{key}::NoMatch", mapper.Key);
130+
Log.LogDebug("NodeStructureEnricher.GetNewNodeName::Mappers::{key}::NoMatch", mapper.Match);
131131
}
132132
}
133133
}
@@ -261,14 +261,14 @@ private NodeInfo GetOrCreateNode(string nodePath, DateTime? startDate, DateTime?
261261
return parentNode;
262262
}
263263

264-
private Dictionary<string, string> GetMaps(TfsNodeStructureType nodeStructureType)
264+
private List<NodeMapping> GetMaps(TfsNodeStructureType nodeStructureType)
265265
{
266266
switch (nodeStructureType)
267267
{
268268
case TfsNodeStructureType.Area:
269-
return Options.Areas != null ? Options.Areas.Mappings : new Dictionary<string, string>();
269+
return Options.Areas != null ? Options.Areas.Mappings : [];
270270
case TfsNodeStructureType.Iteration:
271-
return Options.Iterations != null ? Options.Iterations.Mappings : new Dictionary<string, string>();
271+
return Options.Iterations != null ? Options.Iterations.Mappings : [];
272272
default:
273273
throw new ArgumentOutOfRangeException(nameof(nodeStructureType), nodeStructureType, null);
274274
}
@@ -289,7 +289,7 @@ public void ProcessorExecutionBegin(TfsProcessor processor)
289289
} else
290290
{
291291
Log.LogInformation("SKIP: Migrating all Nodes before the Processor run.");
292-
}
292+
}
293293
}
294294
else
295295
{
@@ -694,9 +694,9 @@ public string GetMappingForMissingItem(NodeStructureItem missingItem)
694694
var mappers = GetMaps((TfsNodeStructureType)Enum.Parse(typeof(TfsNodeStructureType), missingItem.nodeType, true));
695695
foreach (var mapper in mappers)
696696
{
697-
if (Regex.IsMatch(missingItem.sourcePath, mapper.Key, RegexOptions.IgnoreCase))
697+
if (Regex.IsMatch(missingItem.sourcePath, mapper.Match, RegexOptions.IgnoreCase))
698698
{
699-
return mapper.Key;
699+
return mapper.Match;
700700
}
701701
}
702702
return null;

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

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Text.Json.Serialization;
4-
using Microsoft.Extensions.Options;
53
using System.Text.RegularExpressions;
6-
using Microsoft.TeamFoundation.Build.Client;
7-
using MigrationTools.Enrichers;
8-
using MigrationTools.Tools.Infrastructure;
9-
using Newtonsoft.Json.Schema;
104
using DotNet.Globbing;
5+
using Microsoft.Extensions.Options;
6+
using MigrationTools.Tools.Infrastructure;
117
using Serilog;
128

139
namespace MigrationTools.Tools
@@ -20,22 +16,14 @@ public sealed class TfsNodeStructureToolOptions : ToolOptions, ITfsNodeStructure
2016
/// <summary>
2117
/// Rules to apply to the Area Path. Is an object of NodeOptions e.g. { "Filters": ["*/**"], "Mappings": { "^oldProjectName([\\\\]?.*)$": "targetProjectA$1", } }
2218
/// </summary>
23-
/// <default>{"Filters": [], "Mappings": { "^migrationSource1([\\\\]?.*)$": "MigrationTest5$1" })</default>
24-
public NodeOptions Areas { get; set; } = new NodeOptions
25-
{
26-
Filters = new List<string>(),
27-
Mappings = new Dictionary<string, string>()
28-
};
19+
/// <default>{"Filters": [], "Mappings": []}</default>
20+
public NodeOptions Areas { get; set; } = new NodeOptions();
2921

3022
/// <summary>
3123
/// Rules to apply to the Area Path. Is an object of NodeOptions e.g. { "Filters": ["*/**"], "Mappings": { "^oldProjectName([\\\\]?.*)$": "targetProjectA$1", } }
3224
/// </summary>
33-
/// <default>{"Filters": [], "Mappings": { "^migrationSource1([\\\\]?.*)$": "MigrationTest5$1" })</default>
34-
public NodeOptions Iterations { get; set; } = new NodeOptions
35-
{
36-
Filters = new List<string>(),
37-
Mappings = new Dictionary<string, string>()
38-
};
25+
/// <default>{"Filters": [], "Mappings": []}</default>
26+
public NodeOptions Iterations { get; set; } = new NodeOptions();
3927

4028
/// <summary>
4129
/// When set to True the susyem will try to create any missing missing area or iteration paths from the revisions.
@@ -47,16 +35,29 @@ public sealed class TfsNodeStructureToolOptions : ToolOptions, ITfsNodeStructure
4735
public class NodeOptions
4836
{
4937
/// <summary>
50-
/// Using the Glob format you can specify a list of nodes that you want to match. This can be used to filter the main migration of current nodes. note: This does not negate the nees for all nodes in the history of a work item in scope for the migration MUST exist for the system to run, and this will be validated before the migration. e.g. add "migrationSource1\\Team 1,migrationSource1\\Team 1\\**" to match both the Team 1 node and all child nodes.
38+
/// Using the Glob format you can specify a list of nodes that you want to match. This can be used to filter the main migration of current nodes. note: This does not negate the nees for all nodes in the history of a work item in scope for the migration MUST exist for the system to run, and this will be validated before the migration. e.g. add "migrationSource1\\Team 1,migrationSource1\\Team 1\\**" to match both the Team 1 node and all child nodes.
5139
/// </summary>
52-
/// <default>["/"]</default>
53-
public List<string> Filters { get; set; }
40+
/// <default>[]</default>
41+
public List<string> Filters { get; set; } = [];
5442
/// <summary>
5543
/// Remapping rules for nodes, implemented with regular expressions. The rules apply with a higher priority than the `PrefixProjectToNodes`,
5644
/// that is, if no rule matches the path and the `PrefixProjectToNodes` option is enabled, then the old `PrefixProjectToNodes` behavior is applied.
5745
/// </summary>
58-
/// <default>{}</default>
59-
public Dictionary<string, string> Mappings { get; set; }
46+
/// <default>[]</default>
47+
public List<NodeMapping> Mappings { get; set; } = [];
48+
}
49+
50+
public class NodeMapping
51+
{
52+
/// <summary>
53+
/// The regular expression to match the node path.
54+
/// </summary>
55+
public string Match { get; set; } = string.Empty;
56+
57+
/// <summary>
58+
/// The replacement format for the matched node path.
59+
/// </summary>
60+
public string Replacement { get; set; } = string.Empty;
6061
}
6162

6263
public interface ITfsNodeStructureToolOptions
@@ -99,20 +100,20 @@ private ValidateOptionsResult ValidateNodeOptions(NodeOptions nodeOptions, strin
99100
return ValidateOptionsResult.Fail($"{propertyName}.Filters contains an invalid glob pattern: {filter}");
100101
}
101102
}
102-
}
103+
}
103104
// Validate Mappings (Regex for keys, Format for values)
104105
if (nodeOptions.Mappings != null)
105106
{
106107
foreach (var mapping in nodeOptions.Mappings)
107108
{
108-
if (!IsValidRegex(mapping.Key))
109+
if (!IsValidRegex(mapping.Match))
109110
{
110-
return ValidateOptionsResult.Fail($"{propertyName}.Mappings contains an invalid regex pattern: {mapping.Key}");
111+
return ValidateOptionsResult.Fail($"{propertyName}.Mappings contains an invalid regex pattern: {mapping.Match}");
111112
}
112113

113-
if (!IsValidRegexReplacementFormat(mapping.Value, mapping.Key))
114+
if (!IsValidRegexReplacementFormat(mapping.Replacement, mapping.Match))
114115
{
115-
return ValidateOptionsResult.Fail($"{propertyName}.Mappings contains an invalid format string: {mapping.Value}");
116+
return ValidateOptionsResult.Fail($"{propertyName}.Mappings contains an invalid format string: {mapping.Replacement}");
116117
}
117118
}
118119
}
@@ -122,16 +123,15 @@ private ValidateOptionsResult ValidateNodeOptions(NodeOptions nodeOptions, strin
122123
// Example glob validation (modify according to your glob syntax requirements)
123124
private bool IsValidGlobPattern(string pattern)
124125
{
125-
try
126-
{
127-
// This will parse the pattern, and if invalid, will throw an exception
128-
Glob.Parse(pattern);
129-
}
130-
catch (Exception)
131-
{
132-
return false; // If any pattern is invalid, return false
133-
}
134-
126+
try
127+
{
128+
// This will parse the pattern, and if invalid, will throw an exception
129+
Glob.Parse(pattern);
130+
}
131+
catch (Exception)
132+
{
133+
return false; // If any pattern is invalid, return false
134+
}
135135

136136
return true; // All patterns are valid
137137
}
@@ -169,4 +169,4 @@ private bool IsValidRegexReplacementFormat(string format, string regexPattern)
169169
}
170170
}
171171

172-
}
172+
}

0 commit comments

Comments
 (0)