Skip to main content
Glama
branch-specification-parser.ts6.19 kB
/** * BranchSpecificationParser - Parses TeamCity branch specifications */ export interface BranchSpec { pattern: string; type: 'include' | 'exclude'; isDefault: boolean; regex?: RegExp; } export interface MatchResult { configId: string; configName: string; matchedSpec: string; confidence: number; } export interface BuildConfiguration { id: string; name: string; branchSpecs: string[]; } export class BranchSpecificationParser { /** * Parse a single branch specification */ parseSpecification(spec: string): BranchSpec { if (!spec?.trim()) { throw new Error('Empty branch specification'); } let trimmedSpec = spec.trim(); let type: 'include' | 'exclude' = 'include'; let isDefault = false; // Check for default branch marker if (trimmedSpec.includes('(default)')) { isDefault = true; trimmedSpec = trimmedSpec.replace('(default)', '').trim(); } // Check for <default> placeholder if (trimmedSpec === '<default>') { return { pattern: '<default>', type: 'include', isDefault: true, regex: new RegExp('^<default>$'), }; } // Parse inclusion/exclusion prefix if (trimmedSpec.startsWith('+:')) { type = 'include'; trimmedSpec = trimmedSpec.substring(2); } else if (trimmedSpec.startsWith('-:')) { type = 'exclude'; trimmedSpec = trimmedSpec.substring(2); } const pattern = trimmedSpec; const regex = this.convertWildcardToRegex(pattern); return { pattern, type, isDefault, regex, }; } /** * Parse multiple branch specifications */ parseMultipleSpecifications(specs: string[] | string): BranchSpec[] { let specList: string[]; if (typeof specs === 'string') { specList = specs.split('\n'); } else { specList = specs; } return specList.filter((spec) => spec?.trim()).map((spec) => this.parseSpecification(spec)); } /** * Convert wildcard pattern to regular expression */ convertWildcardToRegex(pattern: string): RegExp { // First, temporarily replace ** to avoid confusion with single * let regexPattern = pattern.replace(/\*\*/g, '___DOUBLE_WILDCARD___'); // Escape special regex characters except wildcards and parentheses/pipes for groups regexPattern = regexPattern .replace(/[.+?^${}[\]\\]/g, '\\$&') // Escape special chars (but not * ( ) |) .replace(/\*/g, '[^/]*') // Single wildcard matches anything except / .replace(/___DOUBLE_WILDCARD___/g, '.*'); // Double wildcard matches everything return new RegExp(`^${regexPattern}$`); } /** * Extract default branch from specifications */ extractDefaultBranch(specs: BranchSpec[]): string | null { const defaultSpec = specs.find((spec) => spec.isDefault); return defaultSpec ? defaultSpec.pattern : null; } /** * Check if a branch matches the given specifications */ matchBranch(branchName: string, specs: BranchSpec[]): boolean { let matched = false; // Apply rules in order for (const spec of specs) { if (spec.regex?.test(branchName)) { if (spec.type === 'include') { matched = true; } else if (spec.type === 'exclude') { matched = false; } } } return matched; } } export class BranchMatcher { constructor(private parser: BranchSpecificationParser) {} /** * Check if a branch matches the given specifications */ matchBranch(branchName: string, specs: BranchSpec[]): boolean { let matched = false; // Apply rules in order for (const spec of specs) { if (spec.regex?.test(branchName)) { if (spec.type === 'include') { matched = true; } else if (spec.type === 'exclude') { matched = false; } } } return matched; } /** * Find configurations that can build a specific branch */ getMatchingConfigurations( branchName: string, configurations: BuildConfiguration[] ): MatchResult[] { const results: MatchResult[] = []; for (const config of configurations) { const specs = this.parser.parseMultipleSpecifications(config.branchSpecs); if (this.matchBranch(branchName, specs)) { // Find the spec that matched let matchedSpec = ''; let confidence = 0; for (const spec of specs) { if (spec.type === 'include' && spec.regex?.test(branchName)) { matchedSpec = spec.pattern; confidence = this.calculateConfidence(spec.pattern, branchName); break; } } results.push({ configId: config.id, configName: config.name, matchedSpec, confidence, }); } } // Sort by confidence (highest first) return results.sort((a, b) => b.confidence - a.confidence); } /** * Calculate confidence score for a match */ private calculateConfidence(pattern: string, branchName: string): number { // Exact match if (pattern === branchName) { return 1.0; } // Count wildcards to determine specificity const singleWildcards = (pattern.match(/(?<!\*)\*(?!\*)/g) ?? []).length; const doubleWildcards = (pattern.match(/\*\*/g) ?? []).length; if (doubleWildcards > 0) { return 0.6; // Least specific } else if (singleWildcards > 0) { return 0.8; // Moderately specific } return 0.9; // Pattern with regex groups or other patterns } /** * Extract potential branches from configuration specifications */ getBranchesForConfiguration(specs: string[]): string[] { const branches: string[] = []; const parsedSpecs = this.parser.parseMultipleSpecifications(specs); for (const spec of parsedSpecs) { if (spec.type === 'include') { // For non-wildcard patterns, add them directly if (!spec.pattern.includes('*')) { branches.push(spec.pattern); } else { // For wildcard patterns, add a representative example branches.push(spec.pattern); } } } return branches; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Daghis/teamcity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server