Skip to content

Commit eb17da9

Browse files
konardclaude
andcommitted
Add repository splitting by entities functionality
- Implement SplitRepositoryByEntitiesTrigger for splitting repositories by C# entities - Add repository creation methods to GitHubStorage class - Support automatic detection of classes, interfaces, and enums - Group entities by namespace and create separate repositories - Include admin-only protection via AdminAuthorIssueTriggerDecorator - Add comprehensive error handling and progress reporting - Create usage documentation and examples Resolves #88 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 6b71394 commit eb17da9

File tree

5 files changed

+782
-1
lines changed

5 files changed

+782
-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 LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new SplitRepositoryByEntitiesTrigger(githubStorage), 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: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
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

Comments
 (0)