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 8c3c561..9911f8b 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) { diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs index 5578d79..1325c7f 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs @@ -1,23 +1,36 @@ 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")] diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs index b3e02c1..20bd564 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.controlFile = EnsureFileAskIfMissing(config.controlFile = settings.controlFile != null ? settings.controlFile : config.controlFile, "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?"); @@ -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; } 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..4a633d6 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 --configFormat YAML --cachePath ..\\..\\..\\..\\..\\.cache\\ --configFile ..\\..\\..\\..\\..\\.cache\\configuration-test.yaml --jsonFile ..\\..\\..\\..\\..\\TestData\\tst_jsonj_export_v20.json --NonInteractive " }, "empty": { "commandName": "Project" diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json b/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json index 5ad2012..4eeb0ca 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json +++ b/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json @@ -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"