using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; namespace File_Folder_Helper.Day.Q32024; internal static partial class Helper20240911 { public record Attribute([property: JsonPropertyName("isLocked")] bool IsLocked, [property: JsonPropertyName("name")] string Name); public record Relation([property: JsonPropertyName("rel")] string Type, [property: JsonPropertyName("url")] string URL, [property: JsonPropertyName("attributes")] Attribute Attributes); public record Record(WorkItem WorkItem, ReadOnlyDictionary Children); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Record))] internal partial class RecordCommonSourceGenerationContext : JsonSerializerContext { } public record WorkItem(string AreaPath, string? AssignedTo, int? BusinessValue, DateTime ChangedDate, DateTime? ClosedDate, int CommentCount, DateTime CreatedDate, string Description, float? Effort, int Id, string IterationPath, int? Parent, int? Priority, Relation[] Relations, string? Requester, DateTime? ResolvedDate, int Revision, int? RiskReductionMinusOpportunityEnablement, DateTime? StartDate, string State, string Tags, DateTime? TargetDate, float? TimeCriticality, string Title, string WorkItemType, float? WeightedShortestJobFirst); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(WorkItem))] internal partial class WorkItemSourceGenerationContext : JsonSerializerContext { } private static ReadOnlyDictionary GetWorkItems(string filterDirectory, string[] files) { Dictionary results = []; string json; WorkItem? workItem; string? directoryName; foreach (string file in files) { directoryName = Path.GetDirectoryName(file); if (string.IsNullOrEmpty(directoryName)) continue; if (!directoryName.EndsWith(filterDirectory)) continue; json = File.ReadAllText(file); workItem = JsonSerializer.Deserialize(json, WorkItemSourceGenerationContext.Default.WorkItem); if (workItem is null) continue; results.Add(workItem.Id, workItem); } return new(results); } private static int? GetIdFromUrlIfChild(Relation relation) { int? result; string[] segments = relation?.Attributes is null || relation.Attributes.Name != "Child" ? [] : 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; } private static Dictionary GetKeyValuePairs(ReadOnlyDictionary workItems, WorkItem workItem) { Dictionary results = []; int? childId; WorkItem? childWorkItem; List collection = []; Dictionary keyValuePairs; if (workItem.Relations is not null && workItem.Relations.Length > 0) { collection.Clear(); foreach (Relation relation in workItem.Relations) { childId = GetIdFromUrlIfChild(relation); if (childId is null || !workItems.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 item in collection) { keyValuePairs = GetKeyValuePairs(workItems, item); results.Add(item.Id, new(item, new(keyValuePairs))); } } return results; } private static ReadOnlyDictionary GetWorkItemAndChildren(ReadOnlyDictionary workItems) { Dictionary results = []; Dictionary keyValuePairs; foreach (KeyValuePair keyValuePair in workItems) { keyValuePairs = GetKeyValuePairs(workItems, keyValuePair.Value); results.Add(keyValuePair.Key, new(keyValuePair.Value, new(keyValuePairs))); } return new(results); } private static string GetClosed(WorkItem workItem) { string result = workItem.State != "Closed" ? "[ ]" : "[x]"; return result; } private static string GetLine(List spaces, WorkItem workItem, KeyValuePair keyValuePair, bool condensed, bool sprintOnly) => sprintOnly ? $"\t- [ ] {workItem.IterationPath}" : condensed ? $"{new string(spaces.Skip(1).ToArray())}- {GetClosed(workItem)} {keyValuePair.Key} - {workItem.Title}" : $"{new string(spaces.Skip(1).ToArray())}- {GetClosed(workItem)} {keyValuePair.Key} - {workItem.Title} ~~~ {workItem.AssignedTo} - {workItem.IterationPath.Replace('\\', '-')} - {workItem.CreatedDate} --- {workItem.ClosedDate}"; private static void AppendLines(List spaces, List lines, Record record, bool condensed, bool sprintOnly) { string line; spaces.Add('\t'); WorkItem workItem; foreach (KeyValuePair keyValuePair in record.Children) { workItem = keyValuePair.Value.WorkItem; line = GetLine(spaces, workItem, keyValuePair, condensed, sprintOnly).TrimEnd(); lines.Add(line); AppendLines(spaces, lines, keyValuePair.Value, condensed, sprintOnly); } spaces.RemoveAt(0); } private static void AppendLines(string url, List spaces, List lines, ReadOnlyDictionary workItemAndChildren, string workItemType) { List results = []; WorkItem workItem; string? maxIterationPath; List distinct = []; foreach (KeyValuePair keyValuePair in workItemAndChildren) { workItem = keyValuePair.Value.WorkItem; // if (keyValuePair.Key != 109724) // continue; if (workItem.WorkItemType != workItemType) continue; results.Add($"## {workItem.AssignedTo} - {workItem.Id} - {workItem.Title}"); results.Add(string.Empty); results.Add($"- [{workItem.Id}]({url}{workItem.Id})"); if (keyValuePair.Value.Children.Count == 0) results.Add(string.Empty); else { AppendLines(spaces, results, keyValuePair.Value, condensed: true, sprintOnly: false); results.Add(string.Empty); distinct.Clear(); AppendLines(spaces, distinct, keyValuePair.Value, condensed: false, sprintOnly: true); if (distinct.Count > 1) { results.Add($"## Distinct Iteration Path(s) - {workItem.WorkItemType} - {workItem.AssignedTo} - {workItem.Id} - {workItem.Title} - {workItem.IterationPath}"); results.Add(string.Empty); results.Add($"- [{workItem.Id}]({url}{workItem.Id})"); distinct.Sort(); distinct = (from l in distinct select l.Trim()).Distinct().ToList(); results.AddRange(distinct); results.Add(string.Empty); maxIterationPath = distinct.Max(); if (!string.IsNullOrEmpty(maxIterationPath) && maxIterationPath.Contains("] ") && maxIterationPath.Split(']')[1].Trim() != workItem.IterationPath) { results.Add($"### Sync to Distinct Max Iteration Path => {maxIterationPath} - {workItem.Id} - {workItem.Title}"); results.Add(string.Empty); } } results.Add($"## Extended - {workItem.Id} - {workItem.Title}"); results.Add(string.Empty); AppendLines(spaces, results, keyValuePair.Value, condensed: false, sprintOnly: false); results.Add(string.Empty); } lines.AddRange(results); results.Clear(); } } private static void WriteFiles(string destinationDirectory, string fileName, ReadOnlyCollection lines) { string text = string.Join(Environment.NewLine, lines); string markdownFile = Path.Combine(destinationDirectory, $"{fileName}.md"); string textOld = !File.Exists(markdownFile) ? string.Empty : File.ReadAllText(markdownFile); if (text != textOld) File.WriteAllText(markdownFile, text); string html = CommonMark.CommonMarkConverter.Convert(text); string htmlFile = Path.Combine(destinationDirectory, $"{fileName}.html"); string htmlOld = !File.Exists(htmlFile) ? string.Empty : File.ReadAllText(htmlFile); if (html != htmlOld) File.WriteAllText(htmlFile, html); } private static ReadOnlyCollection FilterChildren(Record record, ReadOnlyCollection workItemTypes) { List results = []; WorkItem workItem; foreach (KeyValuePair keyValuePair in record.Children) { workItem = keyValuePair.Value.WorkItem; if (!workItemTypes.Contains(workItem.WorkItemType)) continue; results.Add(workItem); } return new(results); } private static string? GetMaxIterationPath(ReadOnlyCollection workItems) { string? result; List results = []; foreach (WorkItem workItem in workItems) { if (results.Contains(workItem.IterationPath)) continue; results.Add(workItem.IterationPath); } result = results.Count == 0 ? null : results.Max(); return result; } private static void FeatureCheckIterationPath(string url, List lines, ReadOnlyCollection workItemTypes, ReadOnlyDictionary workItemAndChildren, string workItemType) { WorkItem workItem; string? maxIterationPath; List results = []; ReadOnlyCollection workItems; foreach (KeyValuePair keyValuePair in workItemAndChildren) { workItem = keyValuePair.Value.WorkItem; if (workItem.WorkItemType != workItemType) continue; results.Clear(); if (keyValuePair.Value.Children.Count == 0) continue; workItems = FilterChildren(keyValuePair.Value, workItemTypes); maxIterationPath = GetMaxIterationPath(workItems); if (string.IsNullOrEmpty(maxIterationPath) || workItem.IterationPath == maxIterationPath) continue; results.Add($"## {workItem.AssignedTo} - {workItem.Id} - {workItem.Title}"); results.Add(string.Empty); results.Add($"- [{workItem.Id}]({url}{workItem.Id})"); results.Add($"- [ ] {workItem.Id} => {workItem.IterationPath} != {maxIterationPath}"); results.Add(string.Empty); lines.AddRange(results); } } private static ReadOnlyCollection GetIdsNotMatching(string tags, ReadOnlyCollection workItems) { List results = []; string[] segments; string[] parentTags = tags.Split(';'); foreach (WorkItem workItem in workItems) { segments = tags.Split(';'); if (segments.Length > 0 && parentTags.Any(l => segments.Contains(l))) continue; results.Add(workItem.Id); } return new(results); } private static void FeatureCheckTag(string url, List lines, ReadOnlyCollection workItemTypes, ReadOnlyDictionary workItemAndChildren, string workItemType) { WorkItem workItem; List results = []; ReadOnlyCollection idsNotMatching; ReadOnlyCollection workItems; foreach (KeyValuePair keyValuePair in workItemAndChildren) { workItem = keyValuePair.Value.WorkItem; if (workItem.WorkItemType != workItemType) continue; results.Clear(); if (keyValuePair.Value.Children.Count == 0) continue; if (string.IsNullOrEmpty(workItem.Tags)) idsNotMatching = new([workItem.Id]); else { workItems = FilterChildren(keyValuePair.Value, workItemTypes); idsNotMatching = GetIdsNotMatching(workItem.Tags, workItems); if (!string.IsNullOrEmpty(workItem.Tags) && idsNotMatching.Count == 0) continue; } results.Add($"## {workItem.AssignedTo} - {workItem.Id} - {workItem.Title}"); results.Add(string.Empty); results.Add($"- [{workItem.Id}]({url}{workItem.Id})"); foreach (int id in idsNotMatching) results.Add($"- [ ] {id} {nameof(workItem.Tags)} != {workItem.Tags}"); results.Add(string.Empty); lines.AddRange(results); } } private static ReadOnlyCollection GetIdsNotMatching(int? priority, ReadOnlyCollection workItems) { List results = []; foreach (WorkItem workItem in workItems) { if (workItem.Priority == priority) continue; results.Add(workItem.Id); } return new(results); } private static void FeatureCheckPriority(string url, List lines, ReadOnlyCollection workItemTypes, ReadOnlyDictionary workItemAndChildren, string workItemType) { WorkItem workItem; List results = []; ReadOnlyCollection idsNotMatching; ReadOnlyCollection workItems; foreach (KeyValuePair keyValuePair in workItemAndChildren) { workItem = keyValuePair.Value.WorkItem; if (workItem.WorkItemType != workItemType) continue; results.Clear(); if (keyValuePair.Value.Children.Count == 0) continue; workItems = FilterChildren(keyValuePair.Value, workItemTypes); idsNotMatching = GetIdsNotMatching(workItem.Priority, workItems); if (idsNotMatching.Count == 0) continue; results.Add($"## {workItem.AssignedTo} - {workItem.Id} - {workItem.Title}"); results.Add(string.Empty); results.Add($"- [{workItem.Id}]({url}{workItem.Id})"); foreach (int id in idsNotMatching) results.Add($"- [ ] {id} {nameof(workItem.Priority)} != {workItem.Priority}"); results.Add(string.Empty); lines.AddRange(results); } } internal static void WriteMarkdown(ILogger logger, List args) { string url = args[6]; List spaces = []; List lines = []; string searchPattern = args[2]; string filterDirectory = args[3]; string[] workItemTypes = args[4].Split('|'); string sourceDirectory = Path.GetFullPath(args[0]); string destinationDirectory = Path.GetFullPath(args[5]); if (!Directory.Exists(destinationDirectory)) _ = Directory.CreateDirectory(destinationDirectory); ReadOnlyCollection userStoryWorkItemTypes = new(["User Story"]); ReadOnlyCollection userStoryTaskWorkItemTypes = new(["User Story", "Task"]); string[] files = Directory.GetFiles(sourceDirectory, searchPattern, SearchOption.AllDirectories); logger.LogInformation("With search pattern '{SearchPattern}' found {files} file(s)", searchPattern, files.Length); ReadOnlyDictionary workItems = GetWorkItems(filterDirectory, files); logger.LogInformation("With search pattern '{SearchPattern}' found {files} workItem(s)", searchPattern, workItems.Count); ReadOnlyDictionary workItemAndChildren = GetWorkItemAndChildren(workItems); logger.LogInformation("With search pattern '{SearchPattern}' found {files} workItemAndChildren", searchPattern, workItemAndChildren.Count); if (workItemAndChildren.Count == -1) { string json = JsonSerializer.Serialize(workItemAndChildren, RecordCommonSourceGenerationContext.Default.ReadOnlyDictionaryInt32Record); File.WriteAllText(".json", json); } foreach (string workItemType in workItemTypes) { lines.Clear(); lines.Add("# WorkItems"); lines.Add(string.Empty); AppendLines(url, spaces, lines, workItemAndChildren, workItemType); WriteFiles(destinationDirectory, workItemType, new(lines)); } { lines.Clear(); string workItemType = "Feature"; lines.Add($"# {nameof(FeatureCheckIterationPath)}"); lines.Add(string.Empty); FeatureCheckIterationPath(url, lines, userStoryTaskWorkItemTypes, workItemAndChildren, workItemType); WriteFiles(destinationDirectory, $"{nameof(FeatureCheckIterationPath)}", new(lines)); } { lines.Clear(); string workItemType = "Feature"; lines.Add($"# {nameof(FeatureCheckTag)}"); lines.Add(string.Empty); FeatureCheckTag(url, lines, userStoryWorkItemTypes, workItemAndChildren, workItemType); WriteFiles(destinationDirectory, $"{nameof(FeatureCheckTag)}", new(lines)); } { lines.Clear(); string workItemType = "Feature"; lines.Add($"# {nameof(FeatureCheckPriority)}"); lines.Add(string.Empty); FeatureCheckPriority(url, lines, userStoryWorkItemTypes, workItemAndChildren, workItemType); WriteFiles(destinationDirectory, $"{nameof(FeatureCheckPriority)}", new(lines)); } } }