Skip to content

Commit 2986d02

Browse files
konardclaude
andcommitted
Implement MonthlyCommitAndReviewActivityTrigger for issue #92
- Add new trigger to collect users who made commits and/or reviews in a specified month - Parse month/year from issue body in format "month: X" and "year: Y" - Support ignoring repositories via Lino-formatted links - Use GitHub API to fetch commits and pull request reviews within date range - Format results with user activity summary and detailed activity list - Include usage documentation and test example - Integrate trigger with existing IssueTracker in Program.cs Solves: Collect users who made commits and/or reviews in the specified month using GitHub Bot or GitHub API 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 572ce21 commit 2986d02

File tree

4 files changed

+465
-1
lines changed

4 files changed

+465
-1
lines changed

csharp/Platform.Bot/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private static async Task<int> Main(string[] args)
9595
var dbContext = new FileStorage(databaseFilePath?.FullName ?? new TemporaryFile().Filename);
9696
Console.WriteLine($"Bot has been started. {Environment.NewLine}Press CTRL+C to close");
9797
var githubStorage = new GitHubStorage(githubUserName, githubApiToken, githubApplicationName);
98-
var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
98+
var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new OrganizationLastMonthActivityTrigger(githubStorage), new MonthlyCommitAndReviewActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
9999
var pullRequenstTracker = new PullRequestTracker(githubStorage, new MergeDependabotBumpsTrigger(githubStorage));
100100
var timestampTracker = new DateTimeTracker(githubStorage, new CreateAndSaveOrganizationRepositoriesMigrationTrigger(githubStorage, dbContext, Path.Combine(Directory.GetCurrentDirectory(), "/github-migrations")));
101101
var cancellation = new CancellationTokenSource();
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using System.Globalization;
6+
using Interfaces;
7+
using Octokit;
8+
using Platform.Communication.Protocol.Lino;
9+
using Storage.Remote.GitHub;
10+
11+
namespace Platform.Bot.Triggers
12+
{
13+
using TContext = Issue;
14+
/// <summary>
15+
/// <para>
16+
/// Represents the monthly commit and review activity trigger.
17+
/// </para>
18+
/// <para></para>
19+
/// </summary>
20+
/// <seealso cref="ITrigger{Issue}"/>
21+
internal class MonthlyCommitAndReviewActivityTrigger : ITrigger<TContext>
22+
{
23+
private readonly GitHubStorage _storage;
24+
private readonly Parser _parser = new();
25+
26+
/// <summary>
27+
/// <para>
28+
/// Initializes a new <see cref="MonthlyCommitAndReviewActivityTrigger"/> instance.
29+
/// </para>
30+
/// <para></para>
31+
/// </summary>
32+
/// <param name="storage">
33+
/// <para>A storage.</para>
34+
/// <para></para>
35+
/// </param>
36+
public MonthlyCommitAndReviewActivityTrigger(GitHubStorage storage) => _storage = storage;
37+
38+
/// <summary>
39+
/// <para>
40+
/// Determines whether this instance condition.
41+
/// </para>
42+
/// <para></para>
43+
/// </summary>
44+
/// <param name="context">
45+
/// <para>The context.</para>
46+
/// <para></para>
47+
/// </param>
48+
/// <returns>
49+
/// <para>The bool</para>
50+
/// <para></para>
51+
/// </returns>
52+
public async Task<bool> Condition(TContext context) => context.Title.ToLower().Contains("monthly commit and review activity") || context.Title.ToLower().Contains("collect users who made commits");
53+
54+
/// <summary>
55+
/// <para>
56+
/// Actions the context.
57+
/// </para>
58+
/// <para></para>
59+
/// </summary>
60+
/// <param name="context">
61+
/// <para>The context.</para>
62+
/// <para></para>
63+
/// </param>
64+
public async Task Action(TContext context)
65+
{
66+
var issueService = _storage.Client.Issue;
67+
var owner = context.Repository.Owner.Login;
68+
69+
try
70+
{
71+
var (year, month) = ParseDateFromIssueBody(context.Body);
72+
var ignoredRepositories = GetIgnoredRepositories(_parser.Parse(context.Body));
73+
var activeUsers = await GetActiveUsersInMonth(ignoredRepositories, owner, year, month);
74+
75+
var resultMessage = FormatResult(activeUsers, year, month);
76+
await issueService.Comment.Create(owner, context.Repository.Name, context.Number, resultMessage);
77+
_storage.CloseIssue(context);
78+
}
79+
catch (Exception ex)
80+
{
81+
var errorMessage = $"Error processing monthly activity request: {ex.Message}\n\nPlease ensure the issue body contains the month and year in format:\n- `month: 11` (for November)\n- `year: 2023` (for 2023)\n\nExample:\n```\nmonth: 11\nyear: 2023\n```";
82+
await issueService.Comment.Create(owner, context.Repository.Name, context.Number, errorMessage);
83+
}
84+
}
85+
86+
/// <summary>
87+
/// <para>
88+
/// Parses the date from issue body.
89+
/// </para>
90+
/// <para></para>
91+
/// </summary>
92+
/// <param name="issueBody">
93+
/// <para>The issue body.</para>
94+
/// <para></para>
95+
/// </param>
96+
/// <returns>
97+
/// <para>A tuple containing year and month.</para>
98+
/// <para></para>
99+
/// </returns>
100+
private (int year, int month) ParseDateFromIssueBody(string issueBody)
101+
{
102+
var lines = issueBody?.Split('\n', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
103+
104+
int? year = null;
105+
int? month = null;
106+
107+
foreach (var line in lines)
108+
{
109+
var trimmedLine = line.Trim();
110+
111+
if (trimmedLine.StartsWith("year:", StringComparison.OrdinalIgnoreCase))
112+
{
113+
var yearStr = trimmedLine.Substring(5).Trim();
114+
if (int.TryParse(yearStr, out var parsedYear))
115+
{
116+
year = parsedYear;
117+
}
118+
}
119+
else if (trimmedLine.StartsWith("month:", StringComparison.OrdinalIgnoreCase))
120+
{
121+
var monthStr = trimmedLine.Substring(6).Trim();
122+
if (int.TryParse(monthStr, out var parsedMonth) && parsedMonth >= 1 && parsedMonth <= 12)
123+
{
124+
month = parsedMonth;
125+
}
126+
}
127+
}
128+
129+
if (!year.HasValue || !month.HasValue)
130+
{
131+
// Default to previous month if not specified
132+
var lastMonth = DateTime.Now.AddMonths(-1);
133+
year ??= lastMonth.Year;
134+
month ??= lastMonth.Month;
135+
}
136+
137+
return (year.Value, month.Value);
138+
}
139+
140+
/// <summary>
141+
/// <para>
142+
/// Gets the ignored repositories using the specified links.
143+
/// </para>
144+
/// <para></para>
145+
/// </summary>
146+
/// <param name="links">
147+
/// <para>The links.</para>
148+
/// <para></para>
149+
/// </param>
150+
/// <returns>
151+
/// <para>The ignored repos.</para>
152+
/// <para></para>
153+
/// </returns>
154+
public HashSet<string> GetIgnoredRepositories(IList<Link> links)
155+
{
156+
HashSet<string> ignoredRepos = new() { };
157+
foreach (var link in links)
158+
{
159+
var values = link.Values;
160+
if (values != null && values.Count == 3 && string.Equals(values.First().Id, "ignore", StringComparison.OrdinalIgnoreCase) && string.Equals(values.Last().Id.Trim('.'), "repository", StringComparison.OrdinalIgnoreCase))
161+
{
162+
ignoredRepos.Add(values[1].Id);
163+
}
164+
}
165+
return ignoredRepos;
166+
}
167+
168+
/// <summary>
169+
/// <para>
170+
/// Gets the active users in the specified month.
171+
/// </para>
172+
/// <para></para>
173+
/// </summary>
174+
/// <param name="ignoredRepositories">
175+
/// <para>The ignored repositories.</para>
176+
/// <para></para>
177+
/// </param>
178+
/// <param name="owner">
179+
/// <para>The owner.</para>
180+
/// <para></para>
181+
/// </param>
182+
/// <param name="year">
183+
/// <para>The year.</para>
184+
/// <para></para>
185+
/// </param>
186+
/// <param name="month">
187+
/// <para>The month.</para>
188+
/// <para></para>
189+
/// </param>
190+
/// <returns>
191+
/// <para>A dictionary with user activities.</para>
192+
/// <para></para>
193+
/// </returns>
194+
public async Task<Dictionary<string, List<string>>> GetActiveUsersInMonth(HashSet<string> ignoredRepositories, string owner, int year, int month)
195+
{
196+
var usersActivity = new Dictionary<string, List<string>>();
197+
198+
var startDate = new DateTime(year, month, 1);
199+
var endDate = startDate.AddMonths(1).AddDays(-1);
200+
201+
var repositories = await _storage.GetAllRepositories(owner);
202+
203+
foreach (var repository in repositories)
204+
{
205+
if (ignoredRepositories.Contains(repository.Name))
206+
{
207+
continue;
208+
}
209+
210+
// Get commits for the specified month
211+
var commits = await _storage.GetCommits(repository.Id, new CommitRequest
212+
{
213+
Since = startDate,
214+
Until = endDate
215+
});
216+
217+
foreach (var commit in commits)
218+
{
219+
var authorLogin = commit.Author?.Login;
220+
if (!string.IsNullOrEmpty(authorLogin))
221+
{
222+
if (!usersActivity.ContainsKey(authorLogin))
223+
{
224+
usersActivity[authorLogin] = new List<string>();
225+
}
226+
227+
var activity = $"Commit in {repository.Name}: {commit.Commit.Message.Split('\n').FirstOrDefault()}";
228+
if (!usersActivity[authorLogin].Contains(activity))
229+
{
230+
usersActivity[authorLogin].Add(activity);
231+
}
232+
}
233+
}
234+
235+
// Get pull requests created/updated in the specified month
236+
var pullRequests = await _storage.GetPullRequests(repository.Id);
237+
238+
foreach (var pr in pullRequests)
239+
{
240+
// Check if PR was created or updated in the target month
241+
if ((pr.CreatedAt >= startDate && pr.CreatedAt <= endDate) ||
242+
(pr.UpdatedAt >= startDate && pr.UpdatedAt <= endDate))
243+
{
244+
// Get reviews for this pull request
245+
var reviews = await _storage.Client.PullRequest.Review.GetAll(repository.Id, pr.Number);
246+
247+
foreach (var review in reviews)
248+
{
249+
if (review.SubmittedAt >= startDate && review.SubmittedAt <= endDate)
250+
{
251+
var reviewerLogin = review.User?.Login;
252+
if (!string.IsNullOrEmpty(reviewerLogin))
253+
{
254+
if (!usersActivity.ContainsKey(reviewerLogin))
255+
{
256+
usersActivity[reviewerLogin] = new List<string>();
257+
}
258+
259+
var activity = $"Review in {repository.Name}: PR #{pr.Number} - {pr.Title}";
260+
if (!usersActivity[reviewerLogin].Contains(activity))
261+
{
262+
usersActivity[reviewerLogin].Add(activity);
263+
}
264+
}
265+
}
266+
}
267+
}
268+
}
269+
}
270+
271+
return usersActivity;
272+
}
273+
274+
/// <summary>
275+
/// <para>
276+
/// Formats the result for display.
277+
/// </para>
278+
/// <para></para>
279+
/// </summary>
280+
/// <param name="usersActivity">
281+
/// <para>The users activity.</para>
282+
/// <para></para>
283+
/// </param>
284+
/// <param name="year">
285+
/// <para>The year.</para>
286+
/// <para></para>
287+
/// </param>
288+
/// <param name="month">
289+
/// <para>The month.</para>
290+
/// <para></para>
291+
/// </param>
292+
/// <returns>
293+
/// <para>The formatted result string.</para>
294+
/// <para></para>
295+
/// </returns>
296+
private string FormatResult(Dictionary<string, List<string>> usersActivity, int year, int month)
297+
{
298+
var monthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(month);
299+
300+
if (!usersActivity.Any())
301+
{
302+
return $"No commit or review activity found for {monthName} {year}.";
303+
}
304+
305+
var result = $"# Users with commit and/or review activity in {monthName} {year}\n\n";
306+
307+
var sortedUsers = usersActivity.OrderBy(kvp => kvp.Key).ToList();
308+
309+
result += $"**Total active users: {sortedUsers.Count}**\n\n";
310+
311+
foreach (var userActivity in sortedUsers)
312+
{
313+
result += $"## @{userActivity.Key}\n";
314+
result += $"Activities ({userActivity.Value.Count}):\n";
315+
316+
foreach (var activity in userActivity.Value.Take(5)) // Limit to 5 activities per user to avoid too long messages
317+
{
318+
result += $"- {activity}\n";
319+
}
320+
321+
if (userActivity.Value.Count > 5)
322+
{
323+
result += $"- ... and {userActivity.Value.Count - 5} more activities\n";
324+
}
325+
result += "\n";
326+
}
327+
328+
return result;
329+
}
330+
}
331+
}

0 commit comments

Comments
 (0)