Skip to content

Collect all references in one list #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Git/Commit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ public class Commit
public string Message { get; private set; }
public string Summary => Message.Split('\n')[0];
public List<Commit> Commits { get; } = new List<Commit>();
public List<string> References { get; } = new List<string>();

internal Commit(string key) => (Key, Message) = (key, "");

Expand Down
76 changes: 30 additions & 46 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Open_Rails_Triage.Git;
using Open_Rails_Triage.Launchpad;

namespace Open_Rails_Triage
Expand Down Expand Up @@ -46,6 +45,7 @@ static void Main(string[] args)

static async Task AsyncMain(IConfigurationRoot config, bool verbose)
{
var references = new References();
var gitConfig = config.GetSection("git");
var gitHubConfig = config.GetSection("github");
var launchpadConfig = config.GetSection("launchpad");
Expand All @@ -54,21 +54,20 @@ static async Task AsyncMain(IConfigurationRoot config, bool verbose)
var git = new Git.Project(GetGitPath(), verbose);
git.Init(gitConfig["projectUrl"]);
git.Fetch();
var commits = git.GetLog(gitConfig["branch"], DateTimeOffset.Parse(gitConfig["startDate"]));

var gitHub = new GitHub.Project(gitHubConfig);

var launchpad = new Launchpad.Cache();
var launchpadProject = await launchpad.GetProject(launchpadConfig["projectUrl"]);

await CommitTriage(commits, gitConfig, gitHub);
await BugTriage(launchpadProject, launchpadConfig, commits);
await SpecificationTriage(launchpadProject, launchpadConfig, commits);
await CommitTriage(git, gitConfig, gitHub, references);
await BugTriage(launchpadProject, launchpadConfig, references);
await SpecificationTriage(launchpadProject, launchpadConfig, references);
await SpecificationApprovals(launchpadProject);

var trello = new Trello.Cache(trelloConfig["key"], trelloConfig["token"]);
var board = await trello.GetBoard(trelloConfig["board"]);
await TrelloTriage(board, trelloConfig, commits);
await TrelloTriage(board, trelloConfig, references);
}

static string GetGitPath()
Expand All @@ -77,47 +76,43 @@ static string GetGitPath()
return Path.Combine(Path.GetDirectoryName(appFilePath), "git");
}

static async Task CommitTriage(List<Commit> commits, IConfigurationSection gitConfig, GitHub.Project gitHub)
static async Task CommitTriage(Git.Project git, IConfigurationSection gitConfig, GitHub.Project gitHub, References references)
{
Console.WriteLine("Commit triage");
Console.WriteLine("=============");
Console.WriteLine();

var webUrlConfig = gitConfig.GetSection("webUrl");
var exceptionalLabels = gitConfig.GetSection("references:exceptionalLabels").GetChildren().Select(item => item.Value);
int.TryParse(gitConfig["references:minimumLines"], out var minimumLines);
if (!int.TryParse(gitConfig["references:minimumLines"], out var minimumLines)) minimumLines = 0;
var requiredLabels = gitConfig.GetSection("references:requiredLabels").GetChildren().Select(node => node.Value);
var referencePattern = new Regex(gitConfig["references:references"]);
foreach (var commit in commits)

foreach (var commit in git.GetLog(gitConfig["branch"], DateTimeOffset.Parse(gitConfig["startDate"])))
{
var data = await GetCommitDetails(gitHub, referencePattern, commit);
commit.References.AddRange(data.References);
foreach (var subCommit in commit.Commits)
{
var subData = await GetCommitDetails(gitHub, referencePattern, subCommit);
commit.References.AddRange(subData.References);
}
references.Add(commit, out var referenceTypes);

if (data.PR != null)
var pr = await gitHub.GetPullRequest(commit);
var labels = pr?.Labels.Nodes.Select(n => n.Name) ?? Array.Empty<string>();
if (pr != null)
{
if (data.PR.Labels.Nodes.Any(label => exceptionalLabels.Contains(label.Name))) continue;
if (data.PR.Additions <= minimumLines && data.PR.Deletions <= minimumLines) continue;
if (pr.Labels.Nodes.Any(label => exceptionalLabels.Contains(label.Name))) continue;
if (pr.Additions <= minimumLines && pr.Deletions <= minimumLines) continue;
}

var issues = new List<string>();

if (!requiredLabels.Any(label => data.Labels.Contains(label)))
if (requiredLabels.Any() && !requiredLabels.Any(label => labels.Contains(label)))
{
issues.Add("Missing required labels");
}
if (data.References.Count() == 0)
if (!IsValuePresentMissing(gitConfig.GetSection("references:types"), referenceTypes.ToArray()))
{
issues.Add("Missing required references");
}

if (issues.Count > 0)
{
Console.WriteLine($"- [{commit.Summary}]({webUrlConfig["commit"].Replace("%KEY%", commit.Key)}) {string.Join(", ", data.Labels)} **at** {commit.AuthorDate} **by** {commit.AuthorName}");
Console.WriteLine($"- [{commit.Summary}]({webUrlConfig["commit"].Replace("%KEY%", commit.Key)}) {string.Join(", ", labels)} **at** {commit.AuthorDate} **by** {commit.AuthorName}");
foreach (var issue in issues)
{
Console.WriteLine($" - **Issue:** {issue}");
Expand All @@ -127,18 +122,7 @@ static async Task CommitTriage(List<Commit> commits, IConfigurationSection gitCo
}
}

static async Task<(GitHub.GraphPullRequest PR, IEnumerable<string> Labels, IEnumerable<string> References)> GetCommitDetails(GitHub.Project gitHub, Regex referencePattern, Commit commit)
{
var pr = await gitHub.GetPullRequest(commit);
var message = pr != null ? pr.Title + "\n" + pr.Body : commit.Message;
return (
PR: pr,
Labels: pr?.Labels.Nodes.Select(n => n.Name) ?? new string[0],
References: referencePattern.Matches(message).Select(match => match.Value)
);
}

static async Task BugTriage(Launchpad.Project project, IConfigurationSection config, List<Commit> commits)
static async Task BugTriage(Launchpad.Project project, IConfigurationSection config, References references)
{
Console.WriteLine("Bug triage");
Console.WriteLine("==========");
Expand Down Expand Up @@ -167,6 +151,8 @@ static async Task BugTriage(Launchpad.Project project, IConfigurationSection con
continue;
}

references.Add(bug, out var _);

var issues = new List<string>();

var idealTitles = new List<string>();
Expand Down Expand Up @@ -263,14 +249,13 @@ static async Task BugTriage(Launchpad.Project project, IConfigurationSection con
}
}

var commitMentions = commits.Where(commit => commit.References.Contains(bugTask.Json.web_link));
if (commitMentions.Any())
if (references.TryGetValue(bugTask.Json.web_link, out var reference) && reference.GitCommits.Any())
{
if (bugTask.Status < Status.InProgress)
{
issues.Add("Code was committed but bug is not in progress or fixed");
}
var latestCommit = commitMentions.OrderBy(commit => commit.AuthorDate).Last();
var latestCommit = reference.GitCommits.OrderBy(commit => commit.AuthorDate).Last();
if ((DateTimeOffset.Now - latestCommit.AuthorDate).TotalDays > 28
&& bugTask.Status < Status.FixCommitted)
{
Expand Down Expand Up @@ -469,7 +454,7 @@ static SortedSet<string> GetBugIdealTags(IConfigurationSection config, string ti
return tags;
}

static async Task SpecificationTriage(Launchpad.Project project, IConfigurationSection config, List<Commit> commits)
static async Task SpecificationTriage(Launchpad.Project project, IConfigurationSection config, References references)
{
Console.WriteLine("Specification triage");
Console.WriteLine("====================");
Expand Down Expand Up @@ -539,8 +524,7 @@ static async Task SpecificationTriage(Launchpad.Project project, IConfigurationS
{
issues.Add("Implementation is completed but milestone is missing");
}
var commitMentions = commits.Where(commit => commit.References.Contains(specification.Json.web_link));
if (commitMentions.Any())
if (references.TryGetValue(specification.Json.web_link, out var reference) && reference.GitCommits.Any())
{
if (milestone != null
&& milestone.Id != config["currentMilestone"])
Expand All @@ -552,7 +536,7 @@ static async Task SpecificationTriage(Launchpad.Project project, IConfigurationS
{
issues.Add("Code was committed but definition is not approved");
}
var latestCommit = commitMentions.OrderBy(commit => commit.AuthorDate).Last();
var latestCommit = reference.GitCommits.OrderBy(commit => commit.AuthorDate).Last();
if ((DateTimeOffset.Now - latestCommit.AuthorDate).TotalDays > 28
&& specification.Implementation != Implementation.Implemented)
{
Expand Down Expand Up @@ -602,10 +586,10 @@ static async Task SpecificationApprovals(Launchpad.Project project)
}
}

static async Task TrelloTriage(Trello.Board board, IConfigurationSection config, List<Commit> commits)
static async Task TrelloTriage(Trello.Board board, IConfigurationSection config, References references)
{
Console.WriteLine("Roadmap triage");
Console.WriteLine("==============");
Console.WriteLine("Trello triage");
Console.WriteLine("=============");
Console.WriteLine();

var lists = Filter(await board.GetLists(), list => list.Name, config["includeLists"], config["excludeLists"]);
Expand Down Expand Up @@ -696,7 +680,7 @@ static async Task TrelloTriage(Trello.Board board, IConfigurationSection config,
var complete = checklist.Items.Find(item => item.Name == orderName)?.Complete;
// "https://trello.com/c/JGosmRnZ/159-consist-editor" --> "https://trello.com/c/JGosmRnZ"
var url = string.Join("/", card.Uri.ToString().Split('/').Take(5));
var expectedComplete = commits.Any(commit => commit.References.Contains(url));
var expectedComplete = references.TryGetValue(url, out var reference) && reference.GitCommits.Any();
if (complete != expectedComplete)
{
Console.WriteLine($" - [{card.Name}]({card.Uri}): {checklistConfig.Key} checklist item {orderName} is {complete}; expected {expectedComplete}");
Expand Down
51 changes: 51 additions & 0 deletions References.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Open_Rails_Triage
{
class References : Dictionary<string, Reference>
{
public static readonly Regex ReferencesPattern = new("(https://bugs\\.launchpad\\.net/[^/]+/\\+bug/[0-9]+|https://blueprints\\.launchpad\\.net/[^/]+/\\+spec/[0-9a-z-]+|https://trello\\.com/c/[0-9a-zA-Z]+)");

public static string GetReferenceType(string reference) => reference switch
{
string a when a.Contains("//bugs.launchpad.net/") => "launchpad-bug",
string a when a.Contains("//blueprints.launchpad.net/") => "launchpad-blueprint",
string a when a.Contains("//trello.com/c/") => "trello-card",
_ => "unknown"
};

public void Add(Git.Commit commit, out HashSet<string> types)
{
types = new();
foreach (var match in commit.Commits.Select(commit => ReferencesPattern.Matches(commit.Message)).Append(ReferencesPattern.Matches(commit.Message)).SelectMany(match => match).Select(match => match.Value))
{
types.Add(GetReferenceType(match));
GetReference(match).GitCommits.Add(commit);
}
}

public void Add(Launchpad.Bug bug, out HashSet<string> types)
{
types = new();
foreach (var match in ReferencesPattern.Matches(bug.Description).Select(match => match.Value))
{
types.Add(GetReferenceType(match));
GetReference(match).LaunchpadBugs.Add(bug);
}
}

Reference GetReference(string key)
{
if (!ContainsKey(key)) this[key] = new();
return this[key];
}
}

class Reference
{
public List<Git.Commit> GitCommits { get; } = new();
public List<Launchpad.Bug> LaunchpadBugs { get; } = new();
}
}
4 changes: 3 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"2": "enhancement",
"3": "refactoring"
},
"references": "(https://bugs\\.launchpad\\.net/or/\\+bug/[0-9]+|https://blueprints\\.launchpad\\.net/or/\\+spec/[0-9a-z-]+|https://trello\\.com/c/[0-9a-zA-Z]+)"
"types": {
"anyPresent": "launchpad-bug trello-card launchpad-blueprint"
}
}
},
"github": {
Expand Down
Loading