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 \n Please ensure the issue body contains the month and year in format:\n - `month: 11` (for November)\n - `year: 2023` (for 2023)\n \n Example:\n ```\n month: 11\n year: 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