| 1 | using AgentForge.Abstractions; |
| 2 | using AgentForge.Data; |
| 3 | using AgentForge.Data.Entities; |
| 4 | using AgentForge.Models; |
| 5 | using AgentForge.Plugin.Sdk; |
| 6 | using AgentForge.Services; |
| 7 | |
| 8 | namespace AgentForge.Plugin.RepositoryImport.GitHub.Core; |
| 9 | |
| 10 | public sealed class ForgeGitHubRepositoryImportEngine( |
| 11 | ForgeRepository repository, |
| 12 | GitRepoService git, |
| 13 | ForgeImportVisibilityService visibilityService, |
| 14 | ForgeImportCatalogMatcher catalogMatcher) |
| 15 | { |
| 16 | public ForgeRepositoryImportResponse ImportRepositories( |
| 17 | ForgeOrgEntity org, |
| 18 | string externalSourceLabel, |
| 19 | IReadOnlyList<GitHubRepositoryInfo> remoteRepos, |
| 20 | ForgeImportCatalogDocument? catalogDoc, |
| 21 | ForgeRepositoryImportRequest request, |
| 22 | string? gitHubToken, |
| 23 | Func<GitHubRepositoryInfo, ForgeImportCatalogMatch>? matchOverride = null) |
| 24 | { |
| 25 | if (!request.DryRun && catalogDoc is not null) |
| 26 | { |
| 27 | foreach (var groupPath in catalogDoc.Catalog.EnsureGroups) |
| 28 | repository.EnsureRepoGroupPath(org.Id, groupPath); |
| 29 | } |
| 30 | |
| 31 | var items = new List<ForgeRepositoryImportItemResult>(); |
| 32 | var imported = 0; |
| 33 | var skipped = 0; |
| 34 | var failed = 0; |
| 35 | |
| 36 | foreach (var remote in remoteRepos.OrderBy(r => r.Name, StringComparer.OrdinalIgnoreCase)) |
| 37 | { |
| 38 | var match = matchOverride?.Invoke(remote) |
| 39 | ?? (catalogDoc is null |
| 40 | ? new ForgeImportCatalogMatch(false, request.CatalogPath ?? "backup/unclassified", null, ForgeImportCatalogMatcher.ToForgeSlug(remote.Name), "single") |
| 41 | : catalogMatcher.Match(catalogDoc, remote)); |
| 42 | |
| 43 | if (match.Skip) |
| 44 | { |
| 45 | skipped++; |
| 46 | items.Add(Item(remote.Name, "", "", "", "skipped", match.Reason)); |
| 47 | continue; |
| 48 | } |
| 49 | |
| 50 | var forgeSlug = match.ForgeSlug; |
| 51 | var fullName = $"{org.Slug}/{ForgeSlug.Normalize(forgeSlug)}"; |
| 52 | var visibility = visibilityService.ResolveVisibility( |
| 53 | request.VisibilityPolicy, |
| 54 | match.RuleVisibility, |
| 55 | remote.IsPrivate); |
| 56 | var visibilitySlug = visibility.ToSlug(); |
| 57 | |
| 58 | if (request.DryRun) |
| 59 | { |
| 60 | items.Add(Item(remote.Name, fullName, match.CatalogPath, visibilitySlug, "planned", match.Reason)); |
| 61 | continue; |
| 62 | } |
| 63 | |
| 64 | try |
| 65 | { |
| 66 | if (repository.RepoNameExists(fullName)) |
| 67 | { |
| 68 | if (!request.UpdateExisting) |
| 69 | { |
| 70 | skipped++; |
| 71 | items.Add(Item(remote.Name, fullName, match.CatalogPath, visibilitySlug, "skipped", "already exists")); |
| 72 | continue; |
| 73 | } |
| 74 | |
| 75 | repository.EnsureRepoGroupPath(org.Id, match.CatalogPath); |
| 76 | repository.AssignRepoToCatalogGroup(fullName, org.Id, match.CatalogPath); |
| 77 | imported++; |
| 78 | items.Add(Item(remote.Name, fullName, match.CatalogPath, visibilitySlug, "updated", "catalog group refreshed")); |
| 79 | continue; |
| 80 | } |
| 81 | |
| 82 | repository.EnsureRepoGroupPath(org.Id, match.CatalogPath); |
| 83 | var groupIdForInsert = repository.ResolveRepoGroupIdByPath(org.Id, match.CatalogPath); |
| 84 | |
| 85 | if (!request.SkipMirror) |
| 86 | git.MirrorCloneFromRemote(fullName, remote.CloneUrl, gitHubToken); |
| 87 | else |
| 88 | git.CreateBareRepository(fullName); |
| 89 | |
| 90 | repository.InsertRepo( |
| 91 | fullName, |
| 92 | $"Imported from GitHub {externalSourceLabel}/{remote.Name}", |
| 93 | DriveMode.HumanDriven, |
| 94 | groupIdForInsert, |
| 95 | org.Id, |
| 96 | visibility); |
| 97 | |
| 98 | imported++; |
| 99 | items.Add(Item(remote.Name, fullName, match.CatalogPath, visibilitySlug, "imported", match.Reason)); |
| 100 | } |
| 101 | catch (Exception ex) |
| 102 | { |
| 103 | failed++; |
| 104 | items.Add(Item(remote.Name, fullName, match.CatalogPath, visibilitySlug, "failed", ex.Message)); |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | return new ForgeRepositoryImportResponse( |
| 109 | org.Slug, |
| 110 | externalSourceLabel, |
| 111 | items.Count, |
| 112 | imported, |
| 113 | skipped, |
| 114 | failed, |
| 115 | items); |
| 116 | } |
| 117 | |
| 118 | private static ForgeRepositoryImportItemResult Item( |
| 119 | string externalName, |
| 120 | string forgeName, |
| 121 | string catalogPath, |
| 122 | string visibility, |
| 123 | string action, |
| 124 | string? detail) => |
| 125 | new(externalName, forgeName, catalogPath, visibility, action, detail); |
| 126 | } |
| 127 | |