diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs index cbdffea..d484092 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs @@ -73,7 +73,7 @@ await AnsiConsole.Progress() var task5 = ctx.AddTask("[bold]Stage 5[/]: Create Output Plan Relations ", false); var task6 = ctx.AddTask("[bold]Stage 6[/]: Create Work Items", false); - string cacheTemplateWorkItemsFile = $"{config.CachePath}\\templateCache-{config.templateOrganization}-{config.templateProject}.json"; + string cacheTemplateWorkItemsFile = $"{config.CachePath}\\templateCache-{config.templateOrganization}-{config.templateProject}-{config.templateParentId}.json"; if (config.ClearCache && System.IO.File.Exists(cacheTemplateWorkItemsFile)) { @@ -81,11 +81,26 @@ await AnsiConsole.Progress() } task1.MaxValue = 1; - List templateWorkItems; + List templateWorkItems = null; + + + + task1.StartTask(); + task2.StartTask(); + if (System.IO.File.Exists(cacheTemplateWorkItemsFile)) { + + var changedDate = System.IO.File.GetLastWriteTime(cacheTemplateWorkItemsFile).AddDays(1).Date; + //Test Cache + QueryResults fakeItemsFromTemplateQuery; + fakeItemsFromTemplateQuery = await templateApi.GetWiqlQueryResults("Select [System.Id] From WorkItems Where [System.TeamProject] = '@project' AND [System.Parent] = @id AND [System.ChangedDate] > '@changeddate' order by [System.CreatedDate] desc", new Dictionary() { { "@id", config.templateParentId.ToString() }, { "@changeddate", changedDate.ToString("yyyy-MM-dd") } }); + if (fakeItemsFromTemplateQuery.workItems.Length == 0) + { + AnsiConsole.WriteLine($"Stage 1: Checked template for changes. None Detected. Loading Cache"); + // Load from Cache - task1.StartTask(); + task1.Increment(1); task1.Description = task1.Description + " (cache)"; await Task.Delay(250); @@ -95,8 +110,10 @@ await AnsiConsole.Progress() task2.Increment(templateWorkItems.Count); task2.Description = task2.Description + " (cache)"; AnsiConsole.WriteLine($"Stage 2: Loaded {templateWorkItems.Count()} work items from cache."); + } } - else + + if (templateWorkItems == null) { // Get From Server // -------------------------------------------------------------- @@ -105,7 +122,7 @@ await AnsiConsole.Progress() //AnsiConsole.WriteLine("Stage 1: Executing items from Query"); QueryResults fakeItemsFromTemplateQuery; - fakeItemsFromTemplateQuery = await templateApi.GetWiqlQueryResults(); + fakeItemsFromTemplateQuery = await templateApi.GetWiqlQueryResults("Select [System.Id] From WorkItems Where [System.TeamProject] = '@project' AND [System.Parent] = @id order by [System.CreatedDate] desc", new Dictionary() { { "@id", config.templateParentId.ToString() } }); AnsiConsole.WriteLine($"Stage 1: Query returned {fakeItemsFromTemplateQuery.workItems.Count()} items id's from the template."); task1.Increment(1); task1.StopTask(); @@ -221,7 +238,7 @@ await AnsiConsole.Progress() AnsiConsole.WriteLine($"Processing {buildItems.Count()} items"); task6.Description = $"[bold]Stage 6[/]: Create Work Items (0/{buildItems.Count()})"; task6.StartTask(); - await foreach ((WorkItemToBuild witb, string status, int skipped, int failed, int created) result in CreateWorkItemsToBuild(buildItems, projectItem, targetApi, config.targetWorkItemType)) + await foreach ((WorkItemToBuild witb, string status, int skipped, int failed, int created) result in CreateWorkItemsToBuild(buildItems, projectItem, targetApi)) { //AnsiConsole.WriteLine($"Stage 6: Processing {witb.guid} for output of {witb.relations.Count - 1} relations"); task6.Increment(1); @@ -266,6 +283,7 @@ private async IAsyncEnumerable generateWorkItemsToBuildList(Lis newItem.guid = Guid.NewGuid(); newItem.hasComplexRelation = false; newItem.templateId = item.id; + newItem.workItemType = templateWorkItem.fields.SystemWorkItemType; newItem.fields = new Dictionary() { { "System.Title", item.fields.title }, @@ -309,7 +327,7 @@ private async IAsyncEnumerable generateWorkItemsToBuildRelation } } - private async IAsyncEnumerable<(WorkItemToBuild, string status, int skipped, int failed, int created)> CreateWorkItemsToBuild(List workItemsToBuild, WorkItemFull projectItem, AzureDevOpsApi targetApi, string workItemTypeToCreate) + private async IAsyncEnumerable<(WorkItemToBuild, string status, int skipped, int failed, int created)> CreateWorkItemsToBuild(List workItemsToBuild, WorkItemFull projectItem, AzureDevOpsApi targetApi) { int skipped = 0; int failed = 0; @@ -323,7 +341,7 @@ private async IAsyncEnumerable generateWorkItemsToBuildRelation } else { WorkItemAdd itemToAdd = CreateWorkItemAddOperation(item, workItemsToBuild, projectItem); - WorkItemFull newWorkItem = await targetApi.CreateWorkItem(itemToAdd, workItemTypeToCreate); + WorkItemFull newWorkItem = await targetApi.CreateWorkItem(itemToAdd, item.workItemType); if (newWorkItem != null) { created++; diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs index 525be3c..93f042a 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs @@ -35,10 +35,6 @@ internal class WorkItemCloneCommandSettings : BaseCommandSettings [Description("The project name for the target location")] [CommandOption("--targetProject")] public string? targetProject { get; set; } - [Description("The Name of the work item type to use when creating")] - [CommandOption("--targetWorkItemType|--wit")] - [DefaultValue("Deliverable")] - public string? targetWorkItemType { get; set; } [Description("The ID of the work item in the target environment that will be the parent of all created work items.")] [CommandOption("-p|--parentId|--targetParentId")] public int? targetParentId { get; set; } @@ -52,6 +48,9 @@ internal class WorkItemCloneCommandSettings : BaseCommandSettings [Description("The project name for the template location")] [CommandOption("--templateProject")] public string? templateProject { get; set; } + [Description("The ID of the work item in the template environment under which we will read all the sub items.")] + [CommandOption("--templateParentId")] + public int? templateParentId { get; set; } //------------------------------------------------ } } \ No newline at end of file diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs index 87baaba..b55aab6 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs @@ -18,6 +18,8 @@ internal abstract class WorkItemCommandBase : AsyncCommand internal void CombineValuesFromConfigAndSettings(WorkItemCloneCommandSettings settings, WorkItemCloneCommandSettings config) { + config.NonInteractive = settings.NonInteractive; + config.ClearCache = settings.ClearCache; config.RunName = settings.RunName != null ? settings.RunName : DateTime.Now.ToString("yyyyyMMddHHmmss"); config.configFile = EnsureConfigFileAskIfMissing(config.configFile = settings.configFile != null ? settings.configFile : config.configFile); config.inputJsonFile = EnsureJsonFileAskIfMissing(config.inputJsonFile = settings.inputJsonFile != null ? settings.inputJsonFile : config.inputJsonFile); @@ -26,29 +28,17 @@ internal void CombineValuesFromConfigAndSettings(WorkItemCloneCommandSettings se config.templateOrganization = EnsureOrganizationAskIfMissing(config.templateOrganization = settings.templateOrganization != null ? settings.templateOrganization : config.templateOrganization); config.templateProject = EnsureProjectAskIfMissing(config.templateProject = settings.templateProject != null ? settings.templateProject : config.templateProject, config.templateOrganization); config.templateAccessToken = EnsureAccessTokenAskIfMissing(settings.templateAccessToken != null ? settings.templateAccessToken : config.templateAccessToken, config.templateOrganization); + config.templateParentId = EnsureParentIdAskIfMissing(config.templateParentId = settings.templateParentId != null ? settings.templateParentId : config.templateParentId); config.targetOrganization = EnsureOrganizationAskIfMissing(config.targetOrganization = settings.targetOrganization != null ? settings.targetOrganization : config.targetOrganization); config.targetProject = EnsureProjectAskIfMissing(config.targetProject = settings.targetProject != null ? settings.targetProject : config.targetProject, config.targetOrganization); config.targetAccessToken = EnsureAccessTokenAskIfMissing(settings.targetAccessToken != null ? settings.targetAccessToken : config.targetAccessToken, config.targetOrganization); config.targetParentId = EnsureParentIdAskIfMissing(config.targetParentId = settings.targetParentId != null ? settings.targetParentId : config.targetParentId); - config.targetWorkItemType = EnsureWorkItemTypeAskIfMissing(config.targetWorkItemType = settings.targetWorkItemType != null ? settings.targetWorkItemType : config.targetWorkItemType); - } - private string? EnsureWorkItemTypeAskIfMissing(string? v) - { - if (v == null) - { - v = AnsiConsole.Prompt( - new TextPrompt("What is the target Work Item Type?") - .Validate(v - => !string.IsNullOrWhiteSpace(v) - ? ValidationResult.Success() - : ValidationResult.Error("[yellow]Invalid Work Item Type[/]"))); - } - return v; - } + + internal int EnsureParentIdAskIfMissing(int? parentId) { if (parentId == null) diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json b/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json index 303fb04..ed8a14a 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json @@ -6,7 +6,7 @@ }, "Clone": { "commandName": "Project", - "commandLineArgs": "clone --RunName Allessandro2 --cachePath ..\\..\\..\\..\\..\\.cache\\ --configFile ..\\..\\..\\..\\..\\.cache\\configuration.json --jsonFile ..\\..\\..\\..\\..\\TestData\\new.json " + "commandLineArgs": "clone --cachePath ..\\..\\..\\..\\..\\.cache\\ --configFile ..\\..\\..\\..\\..\\.cache\\configuration-test.json --jsonFile ..\\..\\..\\..\\..\\TestData\\new.json --NonInteractive" }, "empty": { "commandName": "Project" diff --git a/AzureDevOps.WorkItemClone/AzureDevOpsApi.cs b/AzureDevOps.WorkItemClone/AzureDevOpsApi.cs index 2dfdcfe..1b6cd22 100644 --- a/AzureDevOps.WorkItemClone/AzureDevOpsApi.cs +++ b/AzureDevOps.WorkItemClone/AzureDevOpsApi.cs @@ -46,6 +46,31 @@ public async IAsyncEnumerable GetWorkItemsFullAsync(Workitem[] ite } } + public async Task GetWiqlQueryResults(string wiqlQuery, Dictionary parameters) + { + if (parameters == null) + { + parameters = new Dictionary(); + } + if (!parameters.ContainsKey("@project")) + { + parameters.Add("@project", _project); + } + if (string.IsNullOrEmpty(wiqlQuery)) + { + wiqlQuery = "Select [System.Id], [System.Title], [System.State] From WorkItems Where [System.TeamProject] = '@project' order by [System.CreatedDate] desc"; + } + foreach (var param in parameters) + { + wiqlQuery = wiqlQuery.Replace(param.Key, param.Value); + } + string post = JsonConvert.SerializeObject(new + { + query = wiqlQuery + }); + string apiCallUrl = $"https://dev.azure.com/{_account}/_apis/wit/wiql?api-version=7.2-preview.2"; + return await GetObjectResult(apiCallUrl, post); + } public async Task GetWiqlQueryResults() { diff --git a/AzureDevOps.WorkItemClone/DataContracts/WorkItemToBuild.cs b/AzureDevOps.WorkItemClone/DataContracts/WorkItemToBuild.cs index 7d2915f..d6fb9c5 100644 --- a/AzureDevOps.WorkItemClone/DataContracts/WorkItemToBuild.cs +++ b/AzureDevOps.WorkItemClone/DataContracts/WorkItemToBuild.cs @@ -11,5 +11,6 @@ public class WorkItemToBuild public bool hasComplexRelation { get; set; } public string targetUrl { get; set; } public int targetId { get; set; } + public string workItemType { get; set; } } } \ No newline at end of file