1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . IO ;
4+ using System . Linq ;
5+ using System . Text ;
6+ using System . Text . RegularExpressions ;
7+ using System . Threading . Tasks ;
8+ using Interfaces ;
9+ using Octokit ;
10+ using Storage . Remote . GitHub ;
11+
12+ namespace Platform . Bot . Triggers
13+ {
14+ using TContext = Issue ;
15+
16+ /// <summary>
17+ /// <para>
18+ /// Represents a trigger that splits a repository into smaller repositories,
19+ /// with each repository containing a single entity (class, interface, enum).
20+ /// The namespace becomes the repository name.
21+ /// </para>
22+ /// <para></para>
23+ /// </summary>
24+ /// <seealso cref="ITrigger{TContext}"/>
25+ public class SplitRepositoryByEntitiesTrigger : ITrigger < TContext >
26+ {
27+ private readonly GitHubStorage _githubStorage ;
28+
29+ // Regex patterns to identify different types of entities
30+ private static readonly Regex ClassPattern = new ( @"^\s*(public\s+|private\s+|protected\s+|internal\s+)*\s*(abstract\s+|sealed\s+|static\s+)*class\s+(\w+)" , RegexOptions . Multiline ) ;
31+ private static readonly Regex InterfacePattern = new ( @"^\s*(public\s+|private\s+|protected\s+|internal\s+)*\s*interface\s+(\w+)" , RegexOptions . Multiline ) ;
32+ private static readonly Regex EnumPattern = new ( @"^\s*(public\s+|private\s+|protected\s+|internal\s+)*\s*enum\s+(\w+)" , RegexOptions . Multiline ) ;
33+ private static readonly Regex NamespacePattern = new ( @"namespace\s+([\w\.]+)" , RegexOptions . Multiline ) ;
34+
35+ /// <summary>
36+ /// <para>
37+ /// Represents an entity found in source code.
38+ /// </para>
39+ /// <para></para>
40+ /// </summary>
41+ public class CodeEntity
42+ {
43+ public string Name { get ; set ; } = string . Empty ;
44+ public EntityType Type { get ; set ; }
45+ public string Namespace { get ; set ; } = string . Empty ;
46+ public string FilePath { get ; set ; } = string . Empty ;
47+ public string FileContent { get ; set ; } = string . Empty ;
48+ }
49+
50+ public enum EntityType
51+ {
52+ Class ,
53+ Interface ,
54+ Enum
55+ }
56+
57+ /// <summary>
58+ /// <para>
59+ /// Initializes a new <see cref="SplitRepositoryByEntitiesTrigger"/> instance.
60+ /// </para>
61+ /// <para></para>
62+ /// </summary>
63+ /// <param name="githubStorage">
64+ /// <para>The GitHub storage instance.</para>
65+ /// <para></para>
66+ /// </param>
67+ public SplitRepositoryByEntitiesTrigger ( GitHubStorage githubStorage )
68+ {
69+ _githubStorage = githubStorage ;
70+ }
71+
72+ /// <summary>
73+ /// <para>
74+ /// Determines whether this trigger should execute based on the issue title.
75+ /// Trigger phrase: "Split repository by entities"
76+ /// </para>
77+ /// <para></para>
78+ /// </summary>
79+ /// <param name="context">
80+ /// <para>The issue context.</para>
81+ /// <para></para>
82+ /// </param>
83+ /// <returns>
84+ /// <para>True if the trigger should execute; otherwise, false.</para>
85+ /// <para></para>
86+ /// </returns>
87+ public Task < bool > Condition ( TContext context )
88+ {
89+ return Task . FromResult ( context . Title . ToLower ( ) . Contains ( "split repository by entities" ) ) ;
90+ }
91+
92+ /// <summary>
93+ /// <para>
94+ /// Executes the repository splitting action.
95+ /// </para>
96+ /// <para></para>
97+ /// </summary>
98+ /// <param name="context">
99+ /// <para>The issue context.</para>
100+ /// <para></para>
101+ /// </param>
102+ public async Task Action ( TContext context )
103+ {
104+ var statusMessage = new StringBuilder ( ) ;
105+ statusMessage . AppendLine ( "🚀 Starting repository splitting by entities..." ) ;
106+
107+ try
108+ {
109+ // Get the repository to split
110+ var sourceRepository = context . Repository ;
111+ statusMessage . AppendLine ( $ "📁 Analyzing repository: { sourceRepository . FullName } ") ;
112+
113+ // Get all repository contents
114+ var allContents = await _githubStorage . GetAllRepositoryContentsRecursive ( sourceRepository ) ;
115+ var csFiles = allContents . Where ( c => c . Type == ContentType . File && c . Name . EndsWith ( ".cs" ) ) . ToList ( ) ;
116+
117+ statusMessage . AppendLine ( $ "📄 Found { csFiles . Count } C# files to analyze") ;
118+
119+ // Extract entities from all C# files
120+ var allEntities = new List < CodeEntity > ( ) ;
121+ foreach ( var file in csFiles )
122+ {
123+ var fileContent = _githubStorage . GetDecodedFileContent ( file ) ;
124+ var entities = ExtractEntitiesFromFile ( file . Path , fileContent ) ;
125+ allEntities . AddRange ( entities ) ;
126+ }
127+
128+ statusMessage . AppendLine ( $ "🔍 Extracted { allEntities . Count } entities (classes, interfaces, enums)") ;
129+
130+ // Group entities by namespace
131+ var entitiesByNamespace = GroupEntitiesByNamespace ( allEntities ) ;
132+ statusMessage . AppendLine ( $ "📂 Found { entitiesByNamespace . Count } unique namespaces") ;
133+
134+ // Create repositories for each namespace and populate them
135+ var createdRepositories = new List < string > ( ) ;
136+ foreach ( var namespaceGroup in entitiesByNamespace )
137+ {
138+ var namespaceName = namespaceGroup . Key ;
139+ var entities = namespaceGroup . Value ;
140+
141+ try
142+ {
143+ // Create repository for this namespace
144+ var repositoryName = SanitizeRepositoryName ( namespaceName ) ;
145+ var description = $ "Repository containing { entities . Count } entities from namespace { namespaceName } ";
146+
147+ var newRepository = await _githubStorage . CreateRepository ( repositoryName , description ) ;
148+ statusMessage . AppendLine ( $ "✅ Created repository: { newRepository . FullName } ") ;
149+
150+ // Add entities to the new repository
151+ foreach ( var entity in entities )
152+ {
153+ var fileName = Path . GetFileName ( entity . FilePath ) ;
154+ var commitMessage = $ "Add { entity . Type } { entity . Name } from { sourceRepository . Name } ";
155+
156+ await _githubStorage . CreateOrUpdateFile (
157+ entity . FileContent ,
158+ newRepository ,
159+ newRepository . DefaultBranch ,
160+ fileName ,
161+ commitMessage
162+ ) ;
163+
164+ statusMessage . AppendLine ( $ " 📝 Added { entity . Type } { entity . Name } to { fileName } ") ;
165+ }
166+
167+ createdRepositories . Add ( newRepository . FullName ) ;
168+ }
169+ catch ( Exception ex )
170+ {
171+ statusMessage . AppendLine ( $ "❌ Error creating repository for namespace { namespaceName } : { ex . Message } ") ;
172+ }
173+ }
174+
175+ statusMessage . AppendLine ( $ "🎉 Repository splitting completed!") ;
176+ statusMessage . AppendLine ( $ "📊 Summary:") ;
177+ statusMessage . AppendLine ( $ " - Source repository: { sourceRepository . FullName } ") ;
178+ statusMessage . AppendLine ( $ " - Entities processed: { allEntities . Count } ") ;
179+ statusMessage . AppendLine ( $ " - Namespaces found: { entitiesByNamespace . Count } ") ;
180+ statusMessage . AppendLine ( $ " - Repositories created: { createdRepositories . Count } ") ;
181+
182+ if ( createdRepositories . Any ( ) )
183+ {
184+ statusMessage . AppendLine ( "📋 Created repositories:" ) ;
185+ foreach ( var repo in createdRepositories )
186+ {
187+ statusMessage . AppendLine ( $ " - { repo } ") ;
188+ }
189+ }
190+ }
191+ catch ( Exception ex )
192+ {
193+ statusMessage . AppendLine ( $ "💥 Fatal error during repository splitting: { ex . Message } ") ;
194+ Console . WriteLine ( $ "Error in SplitRepositoryByEntitiesTrigger: { ex } ") ;
195+ }
196+
197+ // Post the status message as a comment on the issue
198+ await _githubStorage . CreateIssueComment ( context . Repository . Id , context . Number , statusMessage . ToString ( ) ) ;
199+
200+ // Close the issue since the operation is complete
201+ _githubStorage . CloseIssue ( context ) ;
202+ }
203+
204+ /// <summary>
205+ /// <para>
206+ /// Extracts all entities from a source file.
207+ /// </para>
208+ /// <para></para>
209+ /// </summary>
210+ /// <param name="filePath">
211+ /// <para>The file path.</para>
212+ /// <para></para>
213+ /// </param>
214+ /// <param name="fileContent">
215+ /// <para>The file content.</para>
216+ /// <para></para>
217+ /// </param>
218+ /// <returns>
219+ /// <para>A list of extracted entities.</para>
220+ /// <para></para>
221+ /// </returns>
222+ private List < CodeEntity > ExtractEntitiesFromFile ( string filePath , string fileContent )
223+ {
224+ var entities = new List < CodeEntity > ( ) ;
225+
226+ // Extract namespace
227+ var namespaceMatch = NamespacePattern . Match ( fileContent ) ;
228+ string namespaceName = namespaceMatch . Success ? namespaceMatch . Groups [ 1 ] . Value : "DefaultNamespace" ;
229+
230+ // Find classes
231+ foreach ( Match match in ClassPattern . Matches ( fileContent ) )
232+ {
233+ entities . Add ( new CodeEntity
234+ {
235+ Name = match . Groups [ 3 ] . Value ,
236+ Type = EntityType . Class ,
237+ Namespace = namespaceName ,
238+ FilePath = filePath ,
239+ FileContent = fileContent
240+ } ) ;
241+ }
242+
243+ // Find interfaces
244+ foreach ( Match match in InterfacePattern . Matches ( fileContent ) )
245+ {
246+ entities . Add ( new CodeEntity
247+ {
248+ Name = match . Groups [ 2 ] . Value ,
249+ Type = EntityType . Interface ,
250+ Namespace = namespaceName ,
251+ FilePath = filePath ,
252+ FileContent = fileContent
253+ } ) ;
254+ }
255+
256+ // Find enums
257+ foreach ( Match match in EnumPattern . Matches ( fileContent ) )
258+ {
259+ entities . Add ( new CodeEntity
260+ {
261+ Name = match . Groups [ 2 ] . Value ,
262+ Type = EntityType . Enum ,
263+ Namespace = namespaceName ,
264+ FilePath = filePath ,
265+ FileContent = fileContent
266+ } ) ;
267+ }
268+
269+ return entities ;
270+ }
271+
272+ /// <summary>
273+ /// <para>
274+ /// Groups entities by their namespace.
275+ /// </para>
276+ /// <para></para>
277+ /// </summary>
278+ /// <param name="entities">
279+ /// <para>The entities to group.</para>
280+ /// <para></para>
281+ /// </param>
282+ /// <returns>
283+ /// <para>A dictionary of namespace to entities.</para>
284+ /// <para></para>
285+ /// </returns>
286+ private Dictionary < string , List < CodeEntity > > GroupEntitiesByNamespace ( List < CodeEntity > entities )
287+ {
288+ return entities
289+ . GroupBy ( e => e . Namespace )
290+ . ToDictionary ( g => g . Key , g => g . ToList ( ) ) ;
291+ }
292+
293+ /// <summary>
294+ /// <para>
295+ /// Sanitizes a namespace name to be a valid GitHub repository name.
296+ /// </para>
297+ /// <para></para>
298+ /// </summary>
299+ /// <param name="namespaceName">
300+ /// <para>The namespace name to sanitize.</para>
301+ /// <para></para>
302+ /// </param>
303+ /// <returns>
304+ /// <para>A sanitized repository name.</para>
305+ /// <para></para>
306+ /// </returns>
307+ private static string SanitizeRepositoryName ( string namespaceName )
308+ {
309+ // GitHub repository names must:
310+ // - Be lowercase
311+ // - Use hyphens instead of dots/spaces/underscores
312+ // - Be between 1-100 characters
313+ return namespaceName
314+ . ToLowerInvariant ( )
315+ . Replace ( "." , "-" )
316+ . Replace ( " " , "-" )
317+ . Replace ( "_" , "-" )
318+ . Substring ( 0 , Math . Min ( namespaceName . Length , 100 ) ) ;
319+ }
320+ }
321+ }
0 commit comments