diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/AzureDevOps.WorkItemClone.ConsoleUI.csproj b/AzureDevOps.WorkItemClone.ConsoleUI/AzureDevOps.WorkItemClone.ConsoleUI.csproj
index 696d323..29887c9 100644
--- a/AzureDevOps.WorkItemClone.ConsoleUI/AzureDevOps.WorkItemClone.ConsoleUI.csproj
+++ b/AzureDevOps.WorkItemClone.ConsoleUI/AzureDevOps.WorkItemClone.ConsoleUI.csproj
@@ -30,6 +30,7 @@
+
@@ -40,6 +41,9 @@
Always
+
+ Never
+
Always
diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/CommandSettings.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/CommandSettings.cs
index 7a73c33..446a623 100644
--- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/CommandSettings.cs
+++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/CommandSettings.cs
@@ -6,6 +6,7 @@
using System.Text;
using Newtonsoft.Json;
using System.Threading.Tasks;
+using YamlDotNet.Serialization;
namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
@@ -14,7 +15,7 @@ internal class BaseCommandSettings : CommandSettings
[Description("Pre configure paramiters using this config file. Run `Init` to create it.")]
[CommandOption("--config|--configFile")]
[DefaultValue("configuration.json")]
- [JsonIgnore]
+ [JsonIgnore, YamlIgnore]
public string? configFile { get; set; }
}
}
diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs
index 9386840..9d29659 100644
--- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs
+++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs
@@ -11,6 +11,7 @@
using System.Threading.Tasks;
using System.Diagnostics.Eventing.Reader;
using AzureDevOps.WorkItemClone.Repositories;
+using YamlDotNet.Serialization;
namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
@@ -18,10 +19,15 @@ internal class WorkItemCloneCommand : WorkItemCommandBase ExecuteAsync(CommandContext context, WorkItemCloneCommandSettings settingsFromCmd)
{
+ if (!FileStoreCheckExtensionMatchesFormat(settingsFromCmd.configFile, settingsFromCmd.ConfigFormat))
+ {
+ AnsiConsole.MarkupLine($"[bold red]The file extension of {settingsFromCmd.configFile} does not match the format {settingsFromCmd.ConfigFormat.ToString()} selected! Please rerun with the correct format You can use --configFormat JSON or update your file to YAML[/]");
+ return -1;
+ }
WorkItemCloneCommandSettings config = null;
- if (File.Exists(settingsFromCmd.configFile))
+ if (FileStoreExist(settingsFromCmd.configFile, settingsFromCmd.ConfigFormat))
{
- config = LoadWorkItemCloneCommandSettingsFromFile(settingsFromCmd.configFile);
+ config = FileStoreLoad(settingsFromCmd.configFile, settingsFromCmd.ConfigFormat);
}
if (config == null)
{
@@ -34,7 +40,7 @@ public override async Task ExecuteAsync(CommandContext context, WorkItemClo
DirectoryInfo outputPathInfo = CreateOutputPath(runCache);
AzureDevOpsApi targetApi = CreateAzureDevOpsConnection(config.targetAccessToken, config.targetOrganization, config.targetProject);
- JArray inputWorkItems = DeserializeWorkItemList(config);
+ JArray workItemControlList = DeserializeControlFile(config);
// --------------------------------------------------------------
WriteOutSettings(config);
@@ -96,7 +102,7 @@ await AnsiConsole.Progress()
task1.Increment(result.processed);
await Task.Delay(250);
}
- task1.Description = $"[bold]Stage 1[/]: Load Template Items ({result.loadingFrom})";
+ task1.Description = $"[bold]Stage 1+2[/]: Load Template Items ({result.loadingFrom})";
task1.Increment(1);
}
task1.StopTask();
@@ -150,12 +156,12 @@ await AnsiConsole.Progress()
else
{
// Task 4: First Pass generation of Work Items to build
- task4.MaxValue = inputWorkItems.Count();
+ task4.MaxValue = workItemControlList.Count();
task4.StartTask();
await Task.Delay(250);
//AnsiConsole.WriteLine($"Stage 4: First Pass generation of Work Items to build will merge the provided json work items with the data from the template.");
buildItems = new List();
- await foreach (WorkItemToBuild witb in generateWorkItemsToBuildList(inputWorkItems, templateWor.Data.workitems, projectItem, config.targetProject))
+ await foreach (WorkItemToBuild witb in generateWorkItemsToBuildList(workItemControlList, templateWor.Data.workitems, projectItem, config.targetProject))
{
// AnsiConsole.WriteLine($"Stage 4: processing {witb.guid}");
buildItems.Add(witb);
@@ -166,7 +172,7 @@ await AnsiConsole.Progress()
//AnsiConsole.WriteLine($"Stage 4: Completed first pass.");
// --------------------------------------------------------------
// Task 5: Second Pass Add Relations
- task5.MaxValue = inputWorkItems.Count();
+ task5.MaxValue = workItemControlList.Count();
//AnsiConsole.WriteLine($"Stage 5: Second Pass generate relations.");
task5.StartTask();
await Task.Delay(250);
diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs
index 1bc5a06..4e72cfb 100644
--- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs
+++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs
@@ -1,30 +1,43 @@
using Spectre.Console.Cli;
using System.ComponentModel;
using Newtonsoft.Json;
+using YamlDotNet.Serialization;
namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
+ public enum ConfigFormats
+ {
+ JSON,
+ YAML
+ }
+
internal class WorkItemCloneCommandSettings : BaseCommandSettings
{
+ //------------------------------------------------
[Description("Execute with no user interaction required.")]
[CommandOption("--NonInteractive")]
- [JsonIgnore]
+ [JsonIgnore, YamlIgnore]
public bool NonInteractive { get; set; }
[Description("Clear any cache if there is any")]
[CommandOption("--ClearCache")]
- [JsonIgnore]
+ [JsonIgnore, YamlIgnore]
public bool ClearCache { get; set; }
[Description("Use this run name to execute. This will create a unique folder under the CachePath for storing run specific data and status. Defaults to yyyyyMMddHHmmss.")]
[CommandOption("--RunName")]
- [JsonIgnore]
+ [JsonIgnore, YamlIgnore]
public string? RunName { get; set; }
+ [Description("Use this run name to execute. This will create a unique folder under the CachePath for storing run specific data and status. Defaults to yyyyyMMddHHmmss.")]
+ [CommandOption("--configFormat")]
+ [DefaultValue(ConfigFormats.JSON)]
+ [JsonIgnore, YamlIgnore]
+ public ConfigFormats ConfigFormat { get; set; }
//------------------------------------------------
[CommandOption("--outputPath|--cachePath")]
[DefaultValue("./cache")]
public string? CachePath { get; set; }
//------------------------------------------------
- [CommandOption("--jsonFile|--inputJsonFile")]
- public string? inputJsonFile { get; set; }
+ [CommandOption("--jsonFile|--inputJsonFile|--controlFile")]
+ public string? controlFile { get; set; }
//------------------------------------------------
[Description("The access token for the target location")]
[CommandOption("--targetAccessToken")]
diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs
index 303ab8d..eac265a 100644
--- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs
+++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs
@@ -11,6 +11,9 @@
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
+using YamlDotNet.Serialization.NamingConventions;
+using YamlDotNet.Serialization;
+using Microsoft.SqlServer.Server;
namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
@@ -23,7 +26,7 @@ internal void CombineValuesFromConfigAndSettings(WorkItemCloneCommandSettings se
config.ClearCache = settings.ClearCache;
config.RunName = settings.RunName != null ? settings.RunName : DateTime.Now.ToString("yyyyyMMddHHmmss");
config.configFile = EnsureFileAskIfMissing(config.configFile = settings.configFile != null ? settings.configFile : config.configFile, "Where is the config file to load?");
- config.inputJsonFile = EnsureFileAskIfMissing(config.inputJsonFile = settings.inputJsonFile != null ? settings.inputJsonFile : config.inputJsonFile, "Where is the JSON File?");
+ config.controlFile = EnsureFileAskIfMissing(config.controlFile = settings.controlFile != null ? settings.controlFile : config.controlFile, "Where is the Control File?");
config.CachePath = EnsureFolderAskIfMissing(config.CachePath = settings.CachePath != null ? settings.CachePath : config.CachePath, "What is the cache path?");
config.templateOrganization = EnsureStringAskIfMissing(config.templateOrganization = settings.templateOrganization != null ? settings.templateOrganization : config.templateOrganization, "What is the template organisation?");
@@ -79,26 +82,26 @@ private JArray DeserializeWorkItemList(string jsonFile)
return configWorkItems;
}
- internal JArray DeserializeWorkItemList(WorkItemCloneCommandSettings config)
+ internal JArray DeserializeControlFile(WorkItemCloneCommandSettings config)
{
string CachedRunJson = System.IO.Path.Combine(config.CachePath, config.RunName, "input.json");
if (System.IO.File.Exists(CachedRunJson))
{
// Load From Run Cache
- config.inputJsonFile = CachedRunJson;
+ config.controlFile = CachedRunJson;
return DeserializeWorkItemList(CachedRunJson);
} else
{
// Load new
- config.inputJsonFile = EnsureFileAskIfMissing(config.inputJsonFile, "Where is the JSON File?");
- if (!System.IO.File.Exists(config.inputJsonFile))
+ config.controlFile = EnsureFileAskIfMissing(config.controlFile, "Where is the JSON File?");
+ if (!System.IO.File.Exists(config.controlFile))
{
AnsiConsole.MarkupLine("[red]Error:[/] No JSON file was found.");
- throw new Exception(config.inputJsonFile + " not found.");
+ throw new Exception(config.controlFile + " not found.");
}
JArray inputWorkItems;
- inputWorkItems= DeserializeWorkItemList(config.inputJsonFile);
+ inputWorkItems= DeserializeWorkItemList(config.controlFile);
System.IO.File.WriteAllText(CachedRunJson, JsonConvert.SerializeObject(inputWorkItems, Formatting.Indented));
return inputWorkItems;
}
@@ -137,16 +140,73 @@ internal string EnsureStringAskIfMissing(string value, string message)
}
- internal ConfigurationSettings LoadConfigFile(string? configFile)
+ public TypeToLoad FileStoreLoad(string configFile, ConfigFormats format)
{
- ConfigurationSettings configSettings = System.Text.Json.JsonSerializer.Deserialize(System.IO.File.ReadAllText(configFile));
- return configSettings;
+ TypeToLoad loadedFromFile;
+ configFile = FileStoreEnsureExtension(configFile, format);
+ if (!System.IO.File.Exists(configFile))
+ {
+ AnsiConsole.MarkupLine("[red]Error:[/] No file was found.");
+ throw new Exception(configFile + " not found.");
+ }
+ string content = System.IO.File.ReadAllText(configFile);
+ switch (format)
+ {
+ case ConfigFormats.JSON:
+ loadedFromFile = JsonConvert.DeserializeObject(content);
+ break;
+ case ConfigFormats.YAML:
+ var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
+ loadedFromFile = deserializer.Deserialize(content); /* compile error */
+ break;
+ default:
+ throw new Exception("Unknown format");
+ }
+ return loadedFromFile;
+ }
+
+ public bool FileStoreExist(string? configFile, ConfigFormats format)
+ {
+ return System.IO.File.Exists(FileStoreEnsureExtension(configFile, format));
+ }
+ public bool FileStoreCheckExtensionMatchesFormat(string? configFile, ConfigFormats format)
+ {
+ if (Path.GetExtension(configFile).ToLower() != $".{format.ToString().ToLower()}")
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public string FileStoreEnsureExtension(string? configFile, ConfigFormats format)
+ {
+ if (Path.GetExtension(configFile).ToLower() != $".{format.ToString().ToLower()}")
+ {
+ var original = configFile;
+ configFile = Path.ChangeExtension(configFile, format.ToString().ToLower());
+ AnsiConsole.MarkupLine($"[green]Info:[/] Changed name of {original} to {configFile} ");
+ }
+ return configFile;
}
- internal WorkItemCloneCommandSettings LoadWorkItemCloneCommandSettingsFromFile(string? configFile)
+ public string FileStoreSave(string configFile, TypeToSave content , ConfigFormats format)
{
- WorkItemCloneCommandSettings configSettings = System.Text.Json.JsonSerializer.Deserialize(System.IO.File.ReadAllText(configFile));
- return configSettings;
+ string output;
+ switch (format)
+ {
+ case ConfigFormats.JSON:
+ output = JsonConvert.SerializeObject(content, Formatting.Indented);
+ break;
+ case ConfigFormats.YAML:
+ var serializer = new SerializerBuilder().Build();
+ output = serializer.Serialize(content);
+ break;
+ default:
+ throw new Exception("Unknown format");
+ }
+ configFile = FileStoreEnsureExtension(configFile, format);
+ System.IO.File.WriteAllText(configFile, output);
+ return output;
}
internal string EnsureFileAskIfMissing(string? filename, string message = "What file should we load?")
@@ -155,17 +215,12 @@ internal string EnsureFileAskIfMissing(string? filename, string message = "What
{
filename = AnsiConsole.Prompt(
- new TextPrompt("Where is the config File?")
+ new TextPrompt(message)
.Validate(configFile
- => !string.IsNullOrWhiteSpace(configFile) && System.IO.File.Exists(configFile)
+ => !string.IsNullOrWhiteSpace(configFile)
? ValidationResult.Success()
: ValidationResult.Error("[yellow]Invalid config file[/]")));
}
- if (!System.IO.File.Exists(filename))
- {
- AnsiConsole.MarkupLine("[red]Error:[/] No file was found.");
- throw new Exception(filename + " not found.");
- }
return filename;
}
@@ -197,11 +252,6 @@ internal string EnsureConfigFileAskIfMissing(string? configFile)
? ValidationResult.Success()
: ValidationResult.Error("[yellow]Invalid config file[/]")));
}
- if (!System.IO.File.Exists(configFile))
- {
- AnsiConsole.MarkupLine("[red]Error:[/] No JSON file was found.");
- throw new Exception(configFile + " not found.");
- }
return configFile;
}
@@ -216,7 +266,7 @@ internal void WriteOutSettings(WorkItemCloneCommandSettings config)
.AddEmptyRow()
.AddRow("configFile", config.configFile != null ? config.configFile : "NOT SET")
.AddRow("CachePath", config.CachePath != null ? config.CachePath : "NOT SET")
- .AddRow("inputJsonFile", config.inputJsonFile != null ? config.inputJsonFile : "NOT SET")
+ .AddRow("controlFile", config.controlFile != null ? config.controlFile : "NOT SET")
.AddEmptyRow()
.AddRow( "templateAccessToken", "***************")
.AddRow("templateOrganization", config.templateOrganization != null ? config.templateOrganization : "NOT SET")
@@ -228,7 +278,11 @@ internal void WriteOutSettings(WorkItemCloneCommandSettings config)
.AddRow("targetProject", config.targetProject != null ? config.targetProject : "NOT SET")
.AddRow("targetParentId", config.targetParentId != null ? config.targetParentId.ToString() : "NOT SET")
.AddRow("targetFalbackWit", config.targetFalbackWit != null ? config.targetFalbackWit : "NOT SET")
-
+ .AddEmptyRow()
+ .AddRow("targetQueryTitle", config.targetQueryTitle != null ? config.targetQueryTitle : "NOT SET")
+ .AddRow("targetQueryFolder", config.targetQueryFolder != null ? config.targetQueryFolder : "NOT SET")
+ .AddRow("targetQuery", config.targetQuery != null ? config.targetQuery.Replace("]", "]]").Replace("[", "[[") : "NOT SET")
+
);
}
diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemInitCommand.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemInitCommand.cs
index 91604bc..51bf7a4 100644
--- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemInitCommand.cs
+++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemInitCommand.cs
@@ -5,6 +5,7 @@
using Spectre.Console;
using Spectre.Console.Cli;
using System.Linq;
+using Microsoft.SqlServer.Server;
namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
@@ -12,10 +13,14 @@ internal class WorkItemInitCommand : WorkItemCommandBase ExecuteAsync(CommandContext context, WorkItemCloneCommandSettings settings)
{
-
+ if (!FileStoreCheckExtensionMatchesFormat(settings.configFile, settings.ConfigFormat))
+ {
+ AnsiConsole.MarkupLine($"[bold red]The file extension of {settings.configFile} does not match the format {settings.ConfigFormat.ToString()} selected! Please rerun with the correct format You can use --configFormat JSON or update your file to YAML[/]");
+ return -1;
+ }
var configFile = EnsureConfigFileAskIfMissing(settings.configFile);
WorkItemCloneCommandSettings config = null;
- if (File.Exists(configFile))
+ if (FileStoreExist(configFile, settings.ConfigFormat))
{
var proceedWithSettings = AnsiConsole.Prompt(
new SelectionPrompt { Converter = value => value ? "Yes" : "No" }
@@ -23,7 +28,7 @@ public override async Task ExecuteAsync(CommandContext context, WorkItemClo
.AddChoices(true, false));
if (proceedWithSettings)
{
- config = LoadWorkItemCloneCommandSettingsFromFile(configFile);
+ config = FileStoreLoad(configFile, settings.ConfigFormat);
}
}
if (config == null)
@@ -34,12 +39,9 @@ public override async Task ExecuteAsync(CommandContext context, WorkItemClo
WriteOutSettings(config);
+ FileStoreSave(configFile, config, settings.ConfigFormat);
-
-
- System.IO.File.WriteAllText(configFile, JsonConvert.SerializeObject(config, Formatting.Indented));
-
- AnsiConsole.WriteLine("Settings saved to {configFile}!");
+ AnsiConsole.WriteLine($"Settings saved to {configFile}!");
return 0;
}
diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json b/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json
index b1deeaf..9c50c4b 100644
--- a/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json
+++ b/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json
@@ -6,7 +6,7 @@
},
"Clone": {
"commandName": "Project",
- "commandLineArgs": "clone --cachePath ..\\..\\..\\..\\..\\.cache\\ --configFile ..\\..\\..\\..\\..\\.cache\\configuration-test.json --jsonFile ..\\..\\..\\..\\..\\TestData\\tst_jsonj_export_v20.json --NonInteractive"
+ "commandLineArgs": "clone --cachePath ..\\..\\..\\..\\..\\.cache\\ --configFile ..\\..\\..\\..\\..\\.cache\\configuration-test.yaml --jsonFile ..\\..\\..\\..\\..\\TestData\\tst_jsonj_export_v20.json --NonInteractive "
},
"empty": {
"commandName": "Project"
@@ -18,6 +18,10 @@
"--help": {
"commandName": "Project",
"commandLineArgs": "clone --help"
+ },
+ "Clone YAML": {
+ "commandName": "Project",
+ "commandLineArgs": "clone --configFormat YAML --cachePath ..\\..\\..\\..\\..\\.cache\\ --configFile ..\\..\\..\\..\\..\\.cache\\configuration-test.yaml --jsonFile ..\\..\\..\\..\\..\\TestData\\tst_jsonj_export_v20.json --NonInteractive "
}
}
}
\ No newline at end of file
diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json b/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json
index 749472e..4eeb0ca 100644
--- a/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json
+++ b/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json
@@ -1,6 +1,6 @@
{
"CachePath": "./cache",
- "inputJsonFile": "ADO_TESTProjPipline_V03.json",
+ "controlFile": "ADO_TESTProjPipline_V03.json",
"targetAccessToken": null,
"targetOrganization": "nkdagility-preview",
"targetProject": "ABB-Demo",
@@ -12,5 +12,4 @@
"targetQuery": "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID",
"targetQueryTitle": "Project-@RunName - @projectTitle",
"targetQueryFolder": "Shared Queries"
-
}
diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/configuration.yaml b/AzureDevOps.WorkItemClone.ConsoleUI/configuration.yaml
new file mode 100644
index 0000000..745334f
--- /dev/null
+++ b/AzureDevOps.WorkItemClone.ConsoleUI/configuration.yaml
@@ -0,0 +1,40 @@
+CachePath: "./cache"
+controlFile: "ADO_TESTProjPipline_V03.json"
+targetAccessToken": null
+targetOrganization": "nkdagility-preview"
+targetProject": "ABB-Demo"
+targetParentId": 540
+templateAccessToken": null
+templateOrganization": "ABB-MO-ATE"
+templateProject": "ABB Traction Template"
+templateParentId": 212315
+targetQuery": |
+ SELECT
+ [Custom.Product],
+ [System.Title],
+ [System.Description],
+ [Custom.DeadlineDate],
+ [System.AreaPath],
+ [System.AssignedTo],
+ [System.State],
+ [Custom.Notes],
+ [System.WorkItemType],
+ [Custom.TRA_Milestone]
+ FROM workitemLinks
+ WHERE
+ (
+ [Source].[System.Id] = 000000
+ OR [Source].[System.Parent] = 000000
+ OR [Source].[System.Tags] CONTAINS 'xxxx xxxx'
+ )
+ AND (
+ [System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward'
+ )
+ AND (
+ [Target].[System.Parent] = 000000
+ OR [Target].[System.Tags] CONTAINS 'xxxx xxxx'
+ )
+ ORDER BY [Custom.DeadlineDate]
+ MODE (Recursive)
+targetQueryTitle": "Project-@RunName - @projectTitle"
+targetQueryFolder": "Shared Queries"