using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; namespace File_Folder_Helper.ADO2024.PI4; internal static partial class Helper20241108 { private record Attribute([property: JsonPropertyName("isLocked")] bool IsLocked, [property: JsonPropertyName("name")] string Name); private record Relation([property: JsonPropertyName("rel")] string Type, [property: JsonPropertyName("url")] string URL, [property: JsonPropertyName("attributes")] Attribute Attributes); private record WorkItem(DateTime? ActivatedDate, string AreaPath, string? AssignedTo, long? BusinessValue, DateTime ChangedDate, DateTime? ClosedDate, int CommentCount, DateTime CreatedDate, string Description, long? Effort, int Id, string IterationPath, int? Parent, int? Priority, Relation[]? Relations, string? Requester, DateTime? ResolvedDate, int Revision, long? RiskReductionMinusOpportunityEnablement, DateTime? StartDate, string State, string Tags, DateTime? TargetDate, long? TimeCriticality, string Title, string? Violation, long? WeightedShortestJobFirst, string WorkItemType) { public override string ToString() => $"{Id} - {WorkItemType} - {Title}"; public static WorkItem? GetWithOutRelations(WorkItem? workItem) { WorkItem? result = workItem is null ? null : new(workItem.ActivatedDate, workItem.AreaPath, workItem.AssignedTo, workItem.BusinessValue, workItem.ChangedDate, workItem.ClosedDate, workItem.CommentCount, workItem.CreatedDate, workItem.Description, workItem.Effort, workItem.Id, workItem.IterationPath, workItem.Parent, workItem.Priority, Array.Empty(), workItem.Requester, workItem.ResolvedDate, workItem.Revision, workItem.RiskReductionMinusOpportunityEnablement, workItem.StartDate, workItem.State, workItem.Tags, workItem.TargetDate, workItem.TimeCriticality, workItem.Title, workItem.Violation, workItem.WeightedShortestJobFirst, workItem.WorkItemType); return result; } } [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(WorkItem))] private partial class WorkItemSourceGenerationContext : JsonSerializerContext { } private record Record(WorkItem WorkItem, WorkItem? Parent, Record[] Children, Record[] Related, Record[] Successors) { internal static Record GetWithoutNesting(Record record, string? violation) { Record result; WorkItem workItem = new(record.WorkItem.ActivatedDate, record.WorkItem.AreaPath, record.WorkItem.AssignedTo, record.WorkItem.BusinessValue, record.WorkItem.ChangedDate, record.WorkItem.ClosedDate, record.WorkItem.CommentCount, record.WorkItem.CreatedDate, record.WorkItem.Description, record.WorkItem.Effort, record.WorkItem.Id, record.WorkItem.IterationPath, record.WorkItem.Parent, record.WorkItem.Priority, record.WorkItem.Relations, record.WorkItem.Requester, record.WorkItem.ResolvedDate, record.WorkItem.Revision, record.WorkItem.RiskReductionMinusOpportunityEnablement, record.WorkItem.StartDate, record.WorkItem.State, record.WorkItem.Tags, record.WorkItem.TargetDate, record.WorkItem.TimeCriticality, record.WorkItem.Title, record.WorkItem.Violation is null ? violation : record.WorkItem.Violation, record.WorkItem.WeightedShortestJobFirst, record.WorkItem.WorkItemType); result = new(workItem, record.Parent, [], [], []); return result; } private static Record Get(Record record, bool keepRelations) { Record result; WorkItem? parentWorkItem = keepRelations ? record.Parent : WorkItem.GetWithOutRelations(record.Parent); WorkItem? workItem = keepRelations ? record.WorkItem : WorkItem.GetWithOutRelations(record.WorkItem) ?? throw new Exception(); List childRecords = []; List relatedRecords = []; List successorRecords = []; foreach (Record r in record.Children) childRecords.Add(Get(r, keepRelations)); foreach (Record r in record.Related) relatedRecords.Add(Get(r, keepRelations)); foreach (Record r in record.Successors) successorRecords.Add(Get(r, keepRelations)); result = new(workItem, parentWorkItem, childRecords.ToArray(), relatedRecords.ToArray(), successorRecords.ToArray()); return result; } internal static Record Get(WorkItem workItem, WorkItem? parent, ReadOnlyCollection children, ReadOnlyCollection related, ReadOnlyCollection successors, bool keepRelations) { Record result; Record record = new(workItem, parent, children.ToArray(), related.ToArray(), successors.ToArray()); result = Get(record, keepRelations); return result; } private static int? GetIdFromUrl(string relationName, Relation relation) { int? result; string[] segments = relation?.Attributes is null || relation.Attributes.Name != relationName ? [] : relation.URL.Split('/'); if (segments.Length < 2) result = null; else { if (!int.TryParse(segments[^1], out int id)) result = null; else result = id; } return result; } internal static ReadOnlyCollection GetKeyValuePairs(ReadOnlyDictionary keyValuePairs, WorkItem workItem, string relationName, List nests, bool keepRelations) { List results = []; int? childId; Record record; nests.Add(true); WorkItem? childWorkItem; WorkItem? parentWorkItem; List collection = []; ReadOnlyCollection childRecords; ReadOnlyCollection relatedRecords; ReadOnlyCollection successorRecords; if (workItem.Relations is not null && workItem.Relations.Length > 0) { collection.Clear(); foreach (Relation relation in workItem.Relations) { childId = GetIdFromUrl(relationName, relation); if (childId is not null && workItem.Parent is not null && relation?.URL is not null && relation.URL.Contains(workItem.Parent.Value.ToString())) continue; if (childId is null || !keyValuePairs.TryGetValue(childId.Value, out childWorkItem)) continue; collection.Add(childWorkItem); } collection = (from l in collection orderby l.State != "Closed", l.Id select l).ToList(); foreach (WorkItem w in collection) { if (nests.Count > 99) break; if (w.Parent is null) parentWorkItem = null; else _ = keyValuePairs.TryGetValue(w.Parent.Value, out parentWorkItem); childRecords = GetKeyValuePairs(keyValuePairs, w, "Child", nests, keepRelations); // Forward // records = GetKeyValuePairs(keyValuePairs, w, "Predecessor", nests); // Reverse // successorRecords = GetKeyValuePairs(keyValuePairs, w, "Successor", nests); // Forward // if (successorRecords.Count > 0) // { // if (successorRecords.Count > 0) // { } // } successorRecords = new([]); relatedRecords = GetKeyValuePairs(keyValuePairs, w, "Related", nests, keepRelations); // Related record = Get(w, parentWorkItem, childRecords, relatedRecords, successorRecords, keepRelations); results.Add(record); } } return new(results); } } [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Record[]))] private partial class RecordCollectionBCommonSourceGenerationContext : JsonSerializerContext { } private static ReadOnlyDictionary GetKeyValuePairs(ReadOnlyDictionary keyValuePairs, bool keepRelations) { Dictionary results = []; Record record; List nests = []; WorkItem? parentWorkItem; ReadOnlyCollection childRecords; ReadOnlyCollection relatedRecords; ReadOnlyCollection successorRecords; foreach (KeyValuePair keyValuePair in keyValuePairs) { nests.Clear(); if (keyValuePair.Value.Parent is null) parentWorkItem = null; else _ = keyValuePairs.TryGetValue(keyValuePair.Value.Parent.Value, out parentWorkItem); try { childRecords = Record.GetKeyValuePairs(keyValuePairs, keyValuePair.Value, "Child", nests, keepRelations); // Forward // records = Record.GetKeyValuePairs(keyValuePairs, keyValuePair.Value, "Predecessor", nests, keepRelations); // Reverse relatedRecords = Record.GetKeyValuePairs(keyValuePairs, keyValuePair.Value, "Related", nests, keepRelations); // Related successorRecords = Record.GetKeyValuePairs(keyValuePairs, keyValuePair.Value, "Successor", nests, keepRelations); // Forward record = Record.Get(keyValuePair.Value, parentWorkItem, childRecords, relatedRecords, successorRecords, keepRelations); } catch (Exception) { record = new(keyValuePair.Value, parentWorkItem, [], [], []); } results.Add(keyValuePair.Key, record); } return new(results); } private static ReadOnlyDictionary GetWorkItems(ReadOnlyCollection workItems, bool keepRelations) { ReadOnlyDictionary results; Dictionary keyValuePairs = []; foreach (WorkItem workItem in workItems) keyValuePairs.Add(workItem.Id, workItem); results = GetKeyValuePairs(new(keyValuePairs), keepRelations); return results; } private static ReadOnlyDictionary GetCurrentDirectories(string destinationDirectory, ReadOnlyCollection bugUserStoryWorkItemTypes) { Dictionary results = []; int id; string idCheck; string? fileName; string[] directories; string[] split = ["-"]; foreach (string w in bugUserStoryWorkItemTypes) { directories = Directory.GetDirectories(destinationDirectory, $"*-{w.Replace(" ", "-")}", SearchOption.AllDirectories); foreach (string directory in directories) { fileName = Path.GetFileName(directory); if (string.IsNullOrEmpty(fileName)) continue; idCheck = fileName.Split(split, StringSplitOptions.None)[0]; if (!int.TryParse(idCheck, out id)) continue; if (!results.ContainsKey(id)) results.Add(id, directory); else MoveToDuplicate(destinationDirectory, directory); } } return new(results); } private static void MoveToDuplicate(string destinationDirectory, string directory) { if (string.IsNullOrEmpty(destinationDirectory)) throw new ArgumentException($"'{nameof(destinationDirectory)}' cannot be null or empty.", nameof(destinationDirectory)); if (string.IsNullOrEmpty(directory)) throw new ArgumentException($"'{nameof(directory)}' cannot be null or empty.", nameof(directory)); } private static ReadOnlyCollection GetWorkItems(string sourceDirectory, ReadOnlyCollection bugUserStoryWorkItemTypes) { List results = []; string json; string checkFile; WorkItem? workItem; string directoryName; string? checkDirectory = null; foreach (string w in bugUserStoryWorkItemTypes) { directoryName = Path.GetFileName(sourceDirectory); if (!directoryName.EndsWith(w.Replace(" ", "-"))) continue; checkDirectory = Path.Combine(sourceDirectory, directoryName.Split('-')[0]); if (!Directory.Exists(checkDirectory)) { checkDirectory = null; continue; } break; } if (!string.IsNullOrEmpty(checkDirectory)) { checkFile = Path.Combine(checkDirectory, ".json"); if (File.Exists(checkFile)) { json = File.ReadAllText(checkFile); workItem = JsonSerializer.Deserialize(json, WorkItemSourceGenerationContext.Default.WorkItem); if (workItem is not null) results.Add(workItem); } } return new(results); } private static ReadOnlyCollection GetWorkItems(ReadOnlyDictionary collection) { List results = []; string json; string checkFile; string? directory; WorkItem? workItem; string? checkDirectory; foreach (KeyValuePair keyValuePair in collection) { if (!collection.TryGetValue(keyValuePair.Key, out directory) || string.IsNullOrEmpty(directory)) continue; checkDirectory = Path.Combine(directory, $"{keyValuePair.Key}"); if (!Directory.Exists(checkDirectory)) continue; checkFile = Path.Combine(checkDirectory, ".json"); if (!File.Exists(checkFile)) continue; json = File.ReadAllText(checkFile); workItem = JsonSerializer.Deserialize(json, WorkItemSourceGenerationContext.Default.WorkItem); if (workItem is null) continue; results.Add(workItem); } return new(results); } private static ReadOnlyCollection GetWorkItems(string sourceDirectory, ReadOnlyCollection bugUserStoryWorkItemTypes, ReadOnlyDictionary collection) { ReadOnlyCollection results; if (collection.Count > 0) results = GetWorkItems(collection); else results = GetWorkItems(sourceDirectory, bugUserStoryWorkItemTypes); return results; } internal static void WriteMarkdown(ILogger logger, List args) { // string url = args[5]; bool keepRelations = true; string sourceDirectory = Path.GetFullPath(args[0]); ReadOnlyCollection bugUserStoryWorkItemTypes = args[2].Split('|').ToArray().AsReadOnly(); // ReadOnlyCollection bugUserStoryTaskWorkItemTypes = new(new string[] { "Bug", "User Story", "Task" }); ReadOnlyDictionary collection = GetCurrentDirectories(sourceDirectory, bugUserStoryWorkItemTypes); ReadOnlyCollection workItems = GetWorkItems(sourceDirectory, bugUserStoryWorkItemTypes, collection); ReadOnlyDictionary keyValuePairs = GetWorkItems(workItems, keepRelations); logger.LogInformation("Found {keyValuePairs}", keyValuePairs.Count); } }