ExtractKanban

This commit is contained in:
Mike Phares 2025-02-05 16:19:13 -07:00
parent 930963965d
commit 618fa0d55f
4 changed files with 322 additions and 0 deletions

View File

@ -29,6 +29,7 @@
"Kofax",
"Linc",
"mesfs",
"mestsa",
"NpgSql",
"NSFX",
"OBJE",

View File

@ -1,13 +1,17 @@
using Microsoft.Extensions.Logging;
#if WorkItems
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Serialization;
#endif
namespace File_Folder_Helper.ADO2024.PI4;
internal static partial class Helper20241108
{
#if WorkItems
private record Attribute([property: JsonPropertyName("isLocked")] bool IsLocked,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("parameterTitle")] string? ParameterTitle,
@ -367,4 +371,14 @@ internal static partial class Helper20241108
}
}
#else
internal static void WriteMarkdown(ILogger<Worker> logger, List<string> args)
{
logger.LogError("WriteMarkdown is not available in WorkItems {args[0]}", args[0]);
logger.LogError("WriteMarkdown is not available in WorkItems {args[1]}", args[1]);
}
#endif
}

View File

@ -0,0 +1,305 @@
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace File_Folder_Helper.ADO2025.PI4;
internal static partial class Helper20250204
{
[GeneratedRegex("([A-Z]+(.))")]
private static partial Regex UpperCase();
[GeneratedRegex("[\\s!?.,@:;|\\\\/\"'`£$%\\^&*{}[\\]()<>~#+\\-=_¬]+")]
private static partial Regex InvalidCharacter();
private record H1ParamCaseAndState(string H1, string ParamCase, string State)
{
private static string GetParamCase(string value)
{
string result;
StringBuilder stringBuilder = new(value);
Match[] matches = UpperCase().Matches(value).ToArray();
for (int i = matches.Length - 1; i > -1; i--)
_ = stringBuilder.Insert(matches[i].Index, '-');
string[] segments = InvalidCharacter().Split(stringBuilder.ToString().ToLower());
result = string.Join('-', segments).Trim('-');
return result;
}
private static string GetState(string value) =>
value switch
{
"New" => "ToDo",
"Active" => "In Progress",
"Closed" => "Done",
_ => "Backlog",
};
internal static H1ParamCaseAndState Get(WorkItem workItem)
{
H1ParamCaseAndState result;
string paramCase = GetParamCase(workItem.Title);
string state = GetState(workItem.State);
result = new(workItem.Title, paramCase, state);
return result;
}
}
private record Attribute([property: JsonPropertyName("isLocked")] bool IsLocked,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("parameterTitle")] string? ParameterTitle,
[property: JsonPropertyName("state")] string? State,
[property: JsonPropertyName("workItemType")] string? WorkItemType);
private record Relation([property: JsonPropertyName("attributes")] Attribute Attributes,
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("rel")] string Rel);
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}";
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(WorkItem))]
private partial class WorkItemSourceGenerationContext : JsonSerializerContext
{
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(WorkItem[]))]
private partial class WorkItemCollectionSourceGenerationContext : JsonSerializerContext
{
}
private static string GetTaskText(string directory, string rootDirectory) =>
string.Join(Environment.NewLine,
[
"{",
"\"version\": \"2.0.0\",",
"\"tasks\": [",
"{",
"\"label\": \"File-Folder-Helper AOT s X Day-Helper-2025-02-04\",",
"\"type\": \"shell\",",
"\"command\": \"L:/DevOps/Mesa_FI/File-Folder-Helper/bin/Release/net8.0/win-x64/publish/File-Folder-Helper.exe\",",
"\"args\": [",
"\"s\",",
"\"X\",",
$"\"{directory}\",",
"\"Day-Helper-2025-02-04\",",
$"\"{rootDirectory}\",",
"],",
"\"problemMatcher\": []",
"},",
"{",
"\"label\": \"File-Folder-Helper AOT s X Day-Helper-2024-06-23\",",
"\"type\": \"shell\",",
"\"command\": \"L:/DevOps/Mesa_FI/File-Folder-Helper/bin/Release/net8.0/win-x64/publish/File-Folder-Helper.exe\",",
"\"args\": [",
"\"s\",",
"\"X\",",
$"\"{directory}\",",
"\"Day-Helper-2024-06-23\",",
"\"*.md\",",
"\"##_Sub-tasks\",",
"\"-_[code-insiders](\",",
"\"index.md\",",
"\"-_[,](\",",
"\"##_Done\",",
"\".kan\",",
$"\"{rootDirectory}\",",
"\"316940400000\",",
"],",
"\"problemMatcher\": []",
"}",
"]",
"}",
]);
private static void WriteTaskFile(string sourceDirectory, string rootDirectory)
{
string tasksFile = Path.Combine(sourceDirectory, ".vscode", "tasks.json");
string oldText = File.ReadAllText(tasksFile);
string jsonSafeDirectory = sourceDirectory.Replace('\\', '/');
if (!oldText.Contains(jsonSafeDirectory))
{
string text = GetTaskText(jsonSafeDirectory, rootDirectory);
File.WriteAllText(tasksFile, text);
}
}
private static string GetFilter(ReadOnlyCollection<H1ParamCaseAndState> collection, string filter) =>
string.Join(Environment.NewLine, from l in collection where l.State == filter select $"- [{l.ParamCase}](tasks/{l.ParamCase}.md)");
private static string GetIndexText(WorkItem workItem, H1ParamCaseAndState h1ParamCaseAndState, ReadOnlyCollection<H1ParamCaseAndState> collection) =>
string.Join(Environment.NewLine, [
"---",
"startedColumns:",
" - 'In Progress'",
"completedColumns:",
" - Done",
"---",
string.Empty,
$"# {workItem.Id} - {h1ParamCaseAndState.H1}",
string.Empty,
"## Backlog",
string.Empty,
GetFilter(collection, "Backlog"),
string.Empty,
"## Todo",
string.Empty,
GetFilter(collection, "ToDo"),
string.Empty,
"## In Progress",
string.Empty,
GetFilter(collection, "In Progress"),
string.Empty,
"## Done",
string.Empty,
GetFilter(collection, "Done"),
string.Empty
]);
private static string GetIndexMarkdown(FileInfo fileInfo, ReadOnlyCollection<WorkItem> workItems)
{
string result;
H1ParamCaseAndState h1ParamCaseAndState;
List<H1ParamCaseAndState> collection = [];
foreach (WorkItem w in workItems)
{
h1ParamCaseAndState = H1ParamCaseAndState.Get(w);
collection.Add(h1ParamCaseAndState);
}
string line = Environment.NewLine;
string json = File.ReadAllText(fileInfo.FullName);
WorkItem? workItem = JsonSerializer.Deserialize(json, WorkItemSourceGenerationContext.Default.WorkItem) ??
throw new NullReferenceException(nameof(WorkItem));
h1ParamCaseAndState = H1ParamCaseAndState.Get(workItem);
string text = GetIndexText(workItem, h1ParamCaseAndState, collection.AsReadOnly());
result = text.Replace($"{line}{line}{line}{line}", $"{line}{line}").Replace("408m](tasks", "408M](tasks");
return result;
}
private static ReadOnlyCollection<WorkItem> GetWorkItems(string[] files)
{
List<WorkItem> results = [];
string json;
WorkItem? workItem;
foreach (string file in files)
{
json = File.ReadAllText(file);
workItem = JsonSerializer.Deserialize(json, WorkItemSourceGenerationContext.Default.WorkItem);
if (workItem is null)
continue;
results.Add(workItem);
}
return results.AsReadOnly();
}
private static void ExtractKanban(string searchPattern, string rootDirectory, DirectoryInfo kanbanDirectory, FileInfo fileInfo)
{
string checkFile;
string weekOfYear;
string workItemDirectory;
string line = Environment.NewLine;
H1ParamCaseAndState h1ParamCaseAndState;
Calendar calendar = new CultureInfo("en-US").Calendar;
string tasksDirectory = Path.Combine(kanbanDirectory.FullName, "tasks");
if (!Directory.Exists(tasksDirectory))
_ = Directory.CreateDirectory(tasksDirectory);
string[] files = Directory.GetFiles(tasksDirectory, searchPattern, SearchOption.TopDirectoryOnly);
ReadOnlyCollection<WorkItem> workItems = GetWorkItems(files);
string markdown = GetIndexMarkdown(fileInfo, workItems);
string indexFile = Path.Combine(kanbanDirectory.FullName, "index.md");
string markdownOld = File.Exists(indexFile) ? File.ReadAllText(indexFile) : string.Empty;
if (markdown != markdownOld)
File.WriteAllText(indexFile, markdown);
foreach (WorkItem workItem in workItems)
{
h1ParamCaseAndState = H1ParamCaseAndState.Get(workItem);
checkFile = Path.Combine(tasksDirectory, $"{h1ParamCaseAndState.ParamCase}.md");
markdownOld = File.Exists(checkFile) ? File.ReadAllText(checkFile) : string.Empty;
if (markdownOld.Contains("]("))
continue;
weekOfYear = calendar.GetWeekOfYear(workItem.CreatedDate, CalendarWeekRule.FirstDay, DayOfWeek.Sunday).ToString("00");
workItemDirectory = Path.GetFullPath(Path.Combine(rootDirectory, $"{workItem.CreatedDate:yyyy}", $"{workItem.CreatedDate:yyyy}_Week_{weekOfYear}", $"{workItem.Id}"));
markdown = $"# {h1ParamCaseAndState.H1}{line}{line}## Id {workItem.Id}{line}{line}## Code Insiders{line}{line}- [code-insiders]({workItemDirectory}){line}";
if (markdown != markdownOld)
File.WriteAllText(checkFile, markdown);
}
}
private static string GetSourceDirectory(string directory)
{
string? result = null;
DirectoryInfo directoryInfo;
string? checkDirectory = directory;
string? pathRoot = Path.GetPathRoot(directory);
for (int i = 0; i < int.MaxValue; i++)
{
checkDirectory = Path.GetDirectoryName(checkDirectory);
if (string.IsNullOrEmpty(checkDirectory) || checkDirectory == pathRoot)
break;
directoryInfo = new(checkDirectory);
if (string.IsNullOrEmpty(directoryInfo.LinkTarget))
continue;
result = directory.Replace(checkDirectory, directoryInfo.LinkTarget);
break;
}
result ??= directory;
return result;
}
internal static void ExtractKanban(ILogger<Worker> logger, List<string> args)
{
string searchPattern = "*.json";
string fullPath = Path.GetFullPath(args[0]);
string sourceDirectory = GetSourceDirectory(fullPath);
string rootDirectory = args.Count < 3 || args[2].Length < 16 ? "D:/5-Other-Small/Kanban-mestsa003/{}" : args[2];
WriteTaskFile(sourceDirectory, rootDirectory);
string sourceDirectoryName = Path.GetFileName(sourceDirectory);
DirectoryInfo directoryInfo = new(Path.Combine(sourceDirectory, ".kanbn"));
FileInfo? fileInfo = !directoryInfo.Exists ? null : new(Path.Combine(directoryInfo.FullName, $"{sourceDirectoryName}.json"));
if (directoryInfo.Exists && fileInfo is not null && fileInfo.Exists)
ExtractKanban(searchPattern, rootDirectory, directoryInfo, fileInfo);
else
logger.LogWarning("<{directoryInfo}> doesn't exist", directoryInfo.FullName);
}
}

View File

@ -131,6 +131,8 @@ internal static class HelperDay
ADO2025.PI4.Helper20250114.Rename(logger, args);
else if (args[1] == "Day-Helper-2025-01-26")
ADO2025.PI4.Helper20250126.Move(logger, args);
else if (args[1] == "Day-Helper-2025-02-04")
ADO2025.PI4.Helper20250204.ExtractKanban(logger, args);
else
throw new Exception(appSettings.Company);
}