file-folder-helper/Day/Q32024/Helper-2024-09-11.cs
2024-10-04 18:10:30 -07:00

427 lines
19 KiB
C#

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<int, Record> 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<int, WorkItem> GetWorkItems(string filterDirectory, string[] files)
{
Dictionary<int, WorkItem> 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<int, Record> GetKeyValuePairs(ReadOnlyDictionary<int, WorkItem> workItems, WorkItem workItem)
{
Dictionary<int, Record> results = [];
int? childId;
WorkItem? childWorkItem;
List<WorkItem> collection = [];
Dictionary<int, Record> 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<int, Record> GetWorkItemAndChildren(ReadOnlyDictionary<int, WorkItem> workItems)
{
Dictionary<int, Record> results = [];
Dictionary<int, Record> keyValuePairs;
foreach (KeyValuePair<int, WorkItem> 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<char> spaces, WorkItem workItem, KeyValuePair<int, Record> 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<char> spaces, List<string> lines, Record record, bool condensed, bool sprintOnly)
{
string line;
spaces.Add('\t');
WorkItem workItem;
foreach (KeyValuePair<int, Record> 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<char> spaces, List<string> lines, ReadOnlyDictionary<int, Record> workItemAndChildren, string workItemType)
{
List<string> results = [];
WorkItem workItem;
string? maxIterationPath;
List<string> distinct = [];
foreach (KeyValuePair<int, Record> 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<string> 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<WorkItem> FilterChildren(Record record, ReadOnlyCollection<string> workItemTypes)
{
List<WorkItem> results = [];
WorkItem workItem;
foreach (KeyValuePair<int, Record> 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<WorkItem> workItems)
{
string? result;
List<string> 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<string> lines, ReadOnlyCollection<string> workItemTypes, ReadOnlyDictionary<int, Record> workItemAndChildren, string workItemType)
{
WorkItem workItem;
string? maxIterationPath;
List<string> results = [];
ReadOnlyCollection<WorkItem> workItems;
foreach (KeyValuePair<int, Record> 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<int> GetIdsNotMatching(string tags, ReadOnlyCollection<WorkItem> workItems)
{
List<int> 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<string> lines, ReadOnlyCollection<string> workItemTypes, ReadOnlyDictionary<int, Record> workItemAndChildren, string workItemType)
{
WorkItem workItem;
List<string> results = [];
ReadOnlyCollection<int> idsNotMatching;
ReadOnlyCollection<WorkItem> workItems;
foreach (KeyValuePair<int, Record> 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<int> GetIdsNotMatching(int? priority, ReadOnlyCollection<WorkItem> workItems)
{
List<int> 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<string> lines, ReadOnlyCollection<string> workItemTypes, ReadOnlyDictionary<int, Record> workItemAndChildren, string workItemType)
{
WorkItem workItem;
List<string> results = [];
ReadOnlyCollection<int> idsNotMatching;
ReadOnlyCollection<WorkItem> workItems;
foreach (KeyValuePair<int, Record> 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<Worker> logger, List<string> args)
{
string url = args[6];
List<char> spaces = [];
List<string> 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<string> userStoryWorkItemTypes = new(["User Story"]);
ReadOnlyCollection<string> 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<int, WorkItem> workItems = GetWorkItems(filterDirectory, files);
logger.LogInformation("With search pattern '{SearchPattern}' found {files} workItem(s)", searchPattern, workItems.Count);
ReadOnlyDictionary<int, Record> 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));
}
}
}