1+ using Interfaces ;
2+ using Octokit ;
3+ using Storage . Remote . GitHub ;
4+ using System ;
5+ using System . Collections . Generic ;
6+ using System . Linq ;
7+ using System . Text ;
8+ using System . Threading . Tasks ;
9+
10+ namespace Platform . Bot . Triggers
11+ {
12+ using TContext = Issue ;
13+
14+ /// <summary>
15+ /// <para>
16+ /// Represents a trigger that collects all links to all user's commits in chronological order.
17+ /// </para>
18+ /// <para></para>
19+ /// </summary>
20+ internal class UserCommitLinksCollectorTrigger : ITrigger < TContext >
21+ {
22+ private readonly GitHubStorage _githubStorage ;
23+
24+ /// <summary>
25+ /// <para>
26+ /// Initializes a new <see cref="UserCommitLinksCollectorTrigger"/> instance.
27+ /// </para>
28+ /// <para></para>
29+ /// </summary>
30+ /// <param name="storage">
31+ /// <para>The GitHub storage instance.</para>
32+ /// <para></para>
33+ /// </param>
34+ public UserCommitLinksCollectorTrigger ( GitHubStorage storage ) => _githubStorage = storage ;
35+
36+ /// <summary>
37+ /// <para>
38+ /// Determines whether this trigger should be activated for the given issue.
39+ /// </para>
40+ /// <para></para>
41+ /// </summary>
42+ /// <param name="issue">
43+ /// <para>The issue context.</para>
44+ /// <para></para>
45+ /// </param>
46+ /// <returns>
47+ /// <para>True if the issue title matches the expected pattern.</para>
48+ /// <para></para>
49+ /// </returns>
50+ public Task < bool > Condition ( TContext issue )
51+ {
52+ var title = issue . Title . ToLower ( ) . Trim ( ) ;
53+ return Task . FromResult ( title . StartsWith ( "collect commits for user" ) ||
54+ title . StartsWith ( "user commits" ) ||
55+ title == "collect user commits" ||
56+ title == "collect all user commits" ) ;
57+ }
58+
59+ /// <summary>
60+ /// <para>
61+ /// Collects all commit links for the specified user in chronological order.
62+ /// </para>
63+ /// <para></para>
64+ /// </summary>
65+ /// <param name="issue">
66+ /// <para>The issue context.</para>
67+ /// <para></para>
68+ /// </param>
69+ public async Task Action ( TContext issue )
70+ {
71+ try
72+ {
73+ var organizationName = issue . Repository . Owner . Login ;
74+ var userName = ExtractUserNameFromIssue ( issue ) ;
75+
76+ if ( string . IsNullOrEmpty ( userName ) )
77+ {
78+ await _githubStorage . CreateIssueComment ( issue . Repository . Id , issue . Number ,
79+ "❌ Could not extract username from issue. Please specify the username in the issue title or body.\n \n " +
80+ "Example: `Collect commits for user konard` or add `@username` in the issue body." ) ;
81+ return ;
82+ }
83+
84+ await _githubStorage . CreateIssueComment ( issue . Repository . Id , issue . Number ,
85+ $ "🔍 Starting to collect commit links for user `{ userName } ` in chronological order...") ;
86+
87+ var userCommitLinks = await CollectUserCommitLinksChronologically ( organizationName , userName ) ;
88+
89+ if ( ! userCommitLinks . Any ( ) )
90+ {
91+ await _githubStorage . CreateIssueComment ( issue . Repository . Id , issue . Number ,
92+ $ "ℹ️ No commits found for user `{ userName } ` in organization `{ organizationName } `.") ;
93+ }
94+ else
95+ {
96+ var message = BuildCommitLinksMessage ( userName , userCommitLinks ) ;
97+ await _githubStorage . CreateIssueComment ( issue . Repository . Id , issue . Number , message ) ;
98+ }
99+
100+ Console . WriteLine ( $ "Issue { issue . Title } is processed: { issue . HtmlUrl } ") ;
101+ await _githubStorage . Client . Issue . Update ( issue . Repository . Owner . Login , issue . Repository . Name , issue . Number ,
102+ new IssueUpdate ( ) { State = ItemState . Closed } ) ;
103+ }
104+ catch ( Exception ex )
105+ {
106+ await _githubStorage . CreateIssueComment ( issue . Repository . Id , issue . Number ,
107+ $ "❌ Error occurred while collecting commits: { ex . Message } ") ;
108+ Console . WriteLine ( $ "Error processing issue { issue . Title } : { ex . Message } ") ;
109+ }
110+ }
111+
112+ /// <summary>
113+ /// <para>
114+ /// Extracts the username from the issue title or body.
115+ /// </para>
116+ /// <para></para>
117+ /// </summary>
118+ /// <param name="issue">
119+ /// <para>The issue to extract username from.</para>
120+ /// <para></para>
121+ /// </param>
122+ /// <returns>
123+ /// <para>The extracted username or null if not found.</para>
124+ /// <para></para>
125+ /// </returns>
126+ private string ExtractUserNameFromIssue ( Issue issue )
127+ {
128+ var title = issue . Title . ToLower ( ) ;
129+ var body = issue . Body ? . ToLower ( ) ?? string . Empty ;
130+
131+ // Try to extract from title patterns like "collect commits for user konard"
132+ var titleWords = title . Split ( ' ' ) ;
133+ for ( int i = 0 ; i < titleWords . Length - 1 ; i ++ )
134+ {
135+ if ( titleWords [ i ] == "user" && i + 1 < titleWords . Length )
136+ {
137+ return titleWords [ i + 1 ] ;
138+ }
139+ }
140+
141+ // Try to extract @username from body
142+ if ( body . Contains ( "@" ) )
143+ {
144+ var bodyWords = body . Split ( ' ' , '\n ' , '\r ' ) ;
145+ foreach ( var word in bodyWords )
146+ {
147+ if ( word . StartsWith ( "@" ) && word . Length > 1 )
148+ {
149+ return word . Substring ( 1 ) ;
150+ }
151+ }
152+ }
153+
154+ return null ;
155+ }
156+
157+ /// <summary>
158+ /// <para>
159+ /// Collects all commit links for a specific user across all organization repositories in chronological order.
160+ /// </para>
161+ /// <para></para>
162+ /// </summary>
163+ /// <param name="organizationName">
164+ /// <para>The organization name.</para>
165+ /// <para></para>
166+ /// </param>
167+ /// <param name="userName">
168+ /// <para>The username to collect commits for.</para>
169+ /// <para></para>
170+ /// </param>
171+ /// <returns>
172+ /// <para>List of commit information sorted chronologically (oldest first).</para>
173+ /// <para></para>
174+ /// </returns>
175+ private async Task < List < CommitInfo > > CollectUserCommitLinksChronologically ( string organizationName , string userName )
176+ {
177+ var allCommitInfos = new List < CommitInfo > ( ) ;
178+ var allRepositories = await _githubStorage . GetAllRepositories ( organizationName ) ;
179+
180+ if ( ! allRepositories . Any ( ) )
181+ {
182+ return allCommitInfos ;
183+ }
184+
185+ foreach ( var repository in allRepositories )
186+ {
187+ try
188+ {
189+ // Check if repository has any branches
190+ var branches = await _githubStorage . Client . Repository . Branch . GetAll ( repository . Id ) ;
191+ if ( ! branches . Any ( ) )
192+ {
193+ continue ;
194+ }
195+
196+ // Get commits from all time for this repository
197+ var commits = await _githubStorage . GetCommits ( repository . Id , new CommitRequest
198+ {
199+ Author = userName
200+ } ) ;
201+
202+ foreach ( var commit in commits )
203+ {
204+ if ( commit . Author ? . Login ? . ToLower ( ) == userName . ToLower ( ) )
205+ {
206+ allCommitInfos . Add ( new CommitInfo
207+ {
208+ Commit = commit ,
209+ Repository = repository ,
210+ CommittedDate = commit . Commit . Author . Date
211+ } ) ;
212+ }
213+ }
214+ }
215+ catch ( Exception ex )
216+ {
217+ Console . WriteLine ( $ "Warning: Could not get commits from repository { repository . Name } : { ex . Message } ") ;
218+ // Continue with other repositories
219+ }
220+ }
221+
222+ // Sort chronologically (oldest first)
223+ return allCommitInfos . OrderBy ( c => c . CommittedDate ) . ToList ( ) ;
224+ }
225+
226+ /// <summary>
227+ /// <para>
228+ /// Builds a formatted message with all commit links.
229+ /// </para>
230+ /// <para></para>
231+ /// </summary>
232+ /// <param name="userName">
233+ /// <para>The username.</para>
234+ /// <para></para>
235+ /// </param>
236+ /// <param name="commitInfos">
237+ /// <para>List of commit information.</para>
238+ /// <para></para>
239+ /// </param>
240+ /// <returns>
241+ /// <para>Formatted markdown message.</para>
242+ /// <para></para>
243+ /// </returns>
244+ private string BuildCommitLinksMessage ( string userName , List < CommitInfo > commitInfos )
245+ {
246+ var stringBuilder = new StringBuilder ( ) ;
247+ stringBuilder . AppendLine ( $ "# 📝 All Commit Links for User: `{ userName } ` (Chronological Order)") ;
248+ stringBuilder . AppendLine ( ) ;
249+ stringBuilder . AppendLine ( $ "**Total commits found:** { commitInfos . Count } ") ;
250+ stringBuilder . AppendLine ( ) ;
251+
252+ var currentRepository = string . Empty ;
253+ foreach ( var commitInfo in commitInfos )
254+ {
255+ // Add repository header when we switch to a new repository
256+ if ( currentRepository != commitInfo . Repository . Name )
257+ {
258+ currentRepository = commitInfo . Repository . Name ;
259+ stringBuilder . AppendLine ( $ "## 📁 Repository: [{ commitInfo . Repository . Name } ]({ commitInfo . Repository . HtmlUrl } )") ;
260+ stringBuilder . AppendLine ( ) ;
261+ }
262+
263+ // Format commit message (remove newlines and limit length)
264+ var commitMessage = commitInfo . Commit . Commit . Message . Replace ( '\n ' , ' ' ) . Replace ( '\r ' , ' ' ) ;
265+ if ( commitMessage . Length > 100 )
266+ {
267+ commitMessage = commitMessage . Substring ( 0 , 97 ) + "..." ;
268+ }
269+
270+ // Add commit link with date
271+ var commitDate = commitInfo . CommittedDate . ToString ( "yyyy-MM-dd HH:mm:ss" ) ;
272+ stringBuilder . AppendLine ( $ "- **{ commitDate } ** - [{ commitMessage } ]({ commitInfo . Commit . HtmlUrl } )") ;
273+ }
274+
275+ stringBuilder . AppendLine ( ) ;
276+ stringBuilder . AppendLine ( "---" ) ;
277+ stringBuilder . AppendLine ( "✅ **Collection completed successfully!**" ) ;
278+
279+ return stringBuilder . ToString ( ) ;
280+ }
281+ }
282+
283+ /// <summary>
284+ /// <para>
285+ /// Helper class to store commit information with repository context.
286+ /// </para>
287+ /// <para></para>
288+ /// </summary>
289+ internal class CommitInfo
290+ {
291+ public GitHubCommit Commit { get ; set ; } = null ! ;
292+ public Repository Repository { get ; set ; } = null ! ;
293+ public DateTimeOffset CommittedDate { get ; set ; }
294+ }
295+ }
0 commit comments