Forge
csharp3407750f
1using AgentForge.Abstractions;
2using AgentForge.Plugin.Sdk;
3
4namespace AgentForge.Plugin.RepositoryImport.GitHub.Core;
5
6public sealed class ForgeImportCatalogMatcher
7{
8 public ForgeImportCatalogMatch Match(ForgeImportCatalogDocument doc, GitHubRepositoryInfo repo)
9 {
10 if (doc.Catalog.SkipRepos.Contains(repo.Name, StringComparer.OrdinalIgnoreCase))
11 return ForgeImportCatalogMatch.Skipped("listed in skip_repos");
12
13 foreach (var entry in doc.Explicit)
14 {
15 if (!string.Equals(entry.GitHubName, repo.Name, StringComparison.OrdinalIgnoreCase))
16 continue;
17 return new ForgeImportCatalogMatch(
18 false,
19 entry.CatalogPath,
20 entry.Visibility,
21 entry.ForgeSlug ?? ToForgeSlug(entry.GitHubName),
22 $"explicit:{entry.GitHubName}");
23 }
24
25 foreach (var rule in doc.Rules)
26 {
27 if (!RuleMatches(rule, repo.Name))
28 continue;
29
30 var path = rule.CatalogPath;
31 if (!string.IsNullOrWhiteSpace(rule.CatalogPathTemplate))
32 {
33 path = ApplyTemplate(rule.CatalogPathTemplate, repo.Name, rule.NamePrefix);
34 if (path is null)
35 continue;
36 }
37
38 if (string.IsNullOrWhiteSpace(path))
39 continue;
40
41 return new ForgeImportCatalogMatch(
42 false,
43 path,
44 rule.Visibility,
45 ToForgeSlug(repo.Name),
46 $"rule:{rule.Id}");
47 }
48
49 foreach (var rule in doc.TopicRules)
50 {
51 if (rule.TopicsAll.Count == 0)
52 continue;
53 if (!rule.TopicsAll.All(topic => repo.Topics.Contains(topic, StringComparer.OrdinalIgnoreCase)))
54 continue;
55
56 return new ForgeImportCatalogMatch(
57 false,
58 rule.CatalogPath,
59 rule.Visibility,
60 ToForgeSlug(repo.Name),
61 "topic_rule");
62 }
63
64 return new ForgeImportCatalogMatch(
65 false,
66 doc.Catalog.UnmatchedCatalog,
67 doc.Catalog.UnmatchedVisibility,
68 ToForgeSlug(repo.Name),
69 "unmatched");
70 }
71
72 private static bool RuleMatches(ForgeImportCatalogRule rule, string repoName)
73 {
74 if (!string.IsNullOrWhiteSpace(rule.NameExact)
75 && string.Equals(rule.NameExact, repoName, StringComparison.Ordinal))
76 return true;
77
78 if (rule.NameOneOf.Count > 0
79 && rule.NameOneOf.Any(name => string.Equals(name, repoName, StringComparison.OrdinalIgnoreCase)))
80 return true;
81
82 if (!string.IsNullOrWhiteSpace(rule.NamePrefix)
83 && repoName.StartsWith(rule.NamePrefix, StringComparison.OrdinalIgnoreCase))
84 {
85 if (string.IsNullOrWhiteSpace(rule.NameSuffix))
86 return true;
87 return repoName.EndsWith(rule.NameSuffix, StringComparison.OrdinalIgnoreCase);
88 }
89
90 return false;
91 }
92
93 private static string? ApplyTemplate(string template, string repoName, string? namePrefix)
94 {
95 if (template.Contains("{tail}", StringComparison.Ordinal))
96 {
97 if (string.IsNullOrWhiteSpace(namePrefix)
98 || !repoName.StartsWith(namePrefix, StringComparison.OrdinalIgnoreCase))
99 return null;
100 var tail = repoName[namePrefix.Length..];
101 return template.Replace("{tail}", tail, StringComparison.Ordinal);
102 }
103
104 return template;
105 }
106
107 public static string ToForgeSlug(string githubName)
108 {
109 if (ForgeSlug.IsValid(githubName))
110 return ForgeSlug.Normalize(githubName);
111
112 var chars = new List<char>(githubName.Length + 8);
113 for (var i = 0; i < githubName.Length; i++)
114 {
115 var c = githubName[i];
116 if (char.IsUpper(c) && i > 0 && (char.IsLower(githubName[i - 1]) || char.IsDigit(githubName[i - 1])))
117 chars.Add('-');
118 if (c is '_' or ' ')
119 {
120 chars.Add('-');
121 continue;
122 }
123
124 chars.Add(char.ToLowerInvariant(c));
125 }
126
127 var slug = new string(chars.ToArray()).Trim('-');
128 while (slug.Contains("--", StringComparison.Ordinal))
129 slug = slug.Replace("--", "-", StringComparison.Ordinal);
130
131 return ForgeSlug.IsValid(slug) ? slug : ForgeSlug.Normalize(slug.Replace('.', '-'));
132 }
133}
134
135public sealed record ForgeImportCatalogMatch(
136 bool Skip,
137 string CatalogPath,
138 string? RuleVisibility,
139 string ForgeSlug,
140 string Reason)
141{
142 public static ForgeImportCatalogMatch Skipped(string reason) =>
143 new(true, "", null, "", reason);
144}
145
View only · write via MCP/CIDE