import { minimatch } from "../../deps.ts"; import { getCurrentGitBranch, isGitRepository } from "../utils/git.ts"; import { isFileResource } from "../utils/utils.ts"; import { SyncOptions } from "./conf.ts"; import { TRIGGER_TYPES } from "../types.ts"; export interface SpecificItemsConfig { variables?: string[]; resources?: string[]; triggers?: string[]; folders?: string[]; settings?: boolean; } // Define all branch-specific file types (computed lazily) function getBranchSpecificTypes() { return { variable: '.variable.yaml', resource: '.resource.yaml', // Generate trigger patterns from the list ...Object.fromEntries( TRIGGER_TYPES.map(t => [`${t}_trigger`, `.${t}_trigger.yaml`]) ) } as const; } /** * Check if a path ends with any trigger type */ function isTriggerFile(path: string): boolean { return TRIGGER_TYPES.some(type => path.endsWith(`.${type}_trigger.yaml`)); } /** * Extract the file type suffix from a path */ function getFileTypeSuffix(path: string): string | null { for (const [_, suffix] of Object.entries(getBranchSpecificTypes())) { if (path.endsWith(suffix)) { return suffix; } } const resourceFileMatch = path.match(/(\\.resource\\.file\\..+)$/); if (resourceFileMatch) { return resourceFileMatch[1]; } return null; } /** * Build regex pattern for all supported yaml file types */ function buildYamlTypePattern(): string { const basicTypes = ['variable', 'resource']; const triggerTypes = TRIGGER_TYPES.map(t => `${t}_trigger`); return `((${basicTypes.join('|')})|(${triggerTypes.join('|')}))`; } /** * Get the specific items configuration for the current git branch * Merges commonSpecificItems with branch-specific specificItems */ export function getSpecificItemsForCurrentBranch(config: SyncOptions, branchOverride?: string): SpecificItemsConfig | undefined { if (!config.gitBranches) { return undefined; } // Use branch override if provided, otherwise detect from git let currentBranch: string | null = null; if (branchOverride) { currentBranch = branchOverride; } else if (isGitRepository()) { currentBranch = getCurrentGitBranch(); } if (!currentBranch) { return undefined; } const commonItems = config.gitBranches.commonSpecificItems; const branchItems = config.gitBranches[currentBranch]?.specificItems; // If neither common nor branch-specific items exist, return undefined if (!commonItems && !branchItems) { return undefined; } // Merge common and branch-specific items const merged: SpecificItemsConfig = {}; // Add common items if (commonItems?.variables) { merged.variables = [...commonItems.variables]; } if (commonItems?.resources) { merged.resources = [...commonItems.resources]; } if (commonItems?.triggers) { merged.triggers = [...commonItems.triggers]; } if (commonItems?.folders) { merged.folders = [...commonItems.folders]; } if (commonItems?.settings !== undefined) { merged.settings = commonItems.settings; } // Add branch-specific items (extending common items) if (branchItems?.variables) { merged.variables = [...(merged.variables || []), ...branchItems.variables]; } if (branchItems?.resources) { merged.resources = [...(merged.resources || []), ...branchItems.resources]; } if (branchItems?.triggers) { merged.triggers = [...(merged.triggers || []), ...branchItems.triggers]; } if (branchItems?.folders) { merged.folders = [...(merged.folders || []), ...branchItems.folders]; } // For settings (boolean), branch-specific overrides common if (branchItems?.settings !== undefined) { merged.settings = branchItems.settings; } return merged; } /** * Check if a path matches any of the patterns in the given list */ function matchesPatterns(path: string, patterns: string[]): boolean { return patterns.some(pattern => minimatch(path, pattern)); } /** * Check if the item type for a given path is configured in specificItems. * This checks if the TYPE is configured, not whether it matches the pattern. * Used to determine if branch-specific files should be used for this type. */ export function isItemTypeConfigured(path: string, specificItems: SpecificItemsConfig | undefined): boolean { if (!specificItems) { return false; } if (path.endsWith('.variable.yaml')) { return specificItems.variables !== undefined; } if (path.endsWith('.resource.yaml')) { return specificItems.resources !== undefined; } if (isTriggerFile(path)) { return specificItems.triggers !== undefined; } if (path.endsWith('/folder.meta.yaml')) { return specificItems.folders !== undefined; } if (path === 'settings.yaml') { return specificItems.settings !== undefined; } if (isFileResource(path)) { return specificItems.resources !== undefined; } return false; } /** * Check if a file path should be treated as branch-specific */ export function isSpecificItem(path: string, specificItems: SpecificItemsConfig | undefined): boolean { if (!specificItems) { return false; } // Determine the item type from the file path if (path.endsWith('.variable.yaml')) { return specificItems.variables ? matchesPatterns(path, specificItems.variables) : false; } if (path.endsWith('.resource.yaml')) { return specificItems.resources ? matchesPatterns(path, specificItems.resources) : false; } // Check for any trigger type if (isTriggerFile(path)) { return specificItems.triggers ? matchesPatterns(path, specificItems.triggers) : false; } // Check for folder meta files if (path.endsWith('/folder.meta.yaml')) { if (specificItems.folders) { // Match against the folder path (without /folder.meta.yaml) const folderPath = path.slice(0, -'/folder.meta.yaml'.length); return matchesPatterns(folderPath, specificItems.folders); } return false; } // Check for settings.yaml (root-level file) if (path === 'settings.yaml') { return specificItems.settings === true; } // Check for resource files using the standard detection function if (isFileResource(path)) { // Extract the base path without the file extension to match against patterns const basePathMatch = path.match(/^(.+?)\.resource\.file\./); if (basePathMatch && specificItems.resources) { const basePath = basePathMatch[1] + '.resource.yaml'; return matchesPatterns(basePath, specificItems.resources); } } return false; } /** * Convert a base path to a branch-specific path */ export function toBranchSpecificPath(basePath: string, branchName: string): string { // Sanitize branch name to be filesystem-safe const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_'); // Warn about potential collisions if sanitization occurred if (sanitizedBranchName !== branchName) { console.warn(`Warning: Branch name "${branchName}" contains filesystem-unsafe characters (/ \\ : * ? " < > | .) and was sanitized to "${sanitizedBranchName}". This may cause collisions with other similarly named branches.`); } // Check for folder meta file pattern: folder.meta.yaml -> folder.branchName.meta.yaml if (basePath.endsWith('/folder.meta.yaml')) { const pathWithoutMeta = basePath.substring(0, basePath.length - '/folder.meta.yaml'.length); return `${pathWithoutMeta}/folder.${sanitizedBranchName}.meta.yaml`; } // Check for settings.yaml: settings.yaml -> settings.branchName.yaml if (basePath === 'settings.yaml') { return `settings.${sanitizedBranchName}.yaml`; } // Check for resource file pattern (e.g., .resource.file.ini) const resourceFileMatch = basePath.match(/^(.+?)(\.resource\.file\..+)$/); let extension: string; let pathWithoutExtension: string; if (resourceFileMatch) { // Handle resource files extension = resourceFileMatch[2]; pathWithoutExtension = resourceFileMatch[1]; } else { const suffix = getFileTypeSuffix(basePath); if (!suffix) { return basePath; } extension = suffix; pathWithoutExtension = basePath.substring(0, basePath.length - extension.length); } return `${pathWithoutExtension}.${sanitizedBranchName}${extension}`; } /** * Convert a branch-specific path back to a base path */ export function fromBranchSpecificPath(branchSpecificPath: string, branchName: string): string { // Sanitize branch name the same way as in toBranchSpecificPath const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_'); const escapedBranchName = sanitizedBranchName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Check for folder meta file pattern: /folder.branchName.meta.yaml -> /folder.meta.yaml const folderPattern = new RegExp(`/folder\\.${escapedBranchName}\\.meta\\.yaml$`); if (folderPattern.test(branchSpecificPath)) { return branchSpecificPath.replace(folderPattern, '/folder.meta.yaml'); } // Check for settings file pattern: settings.branchName.yaml -> settings.yaml const settingsPattern = new RegExp(`^settings\\.${escapedBranchName}\\.yaml$`); if (settingsPattern.test(branchSpecificPath)) { return 'settings.yaml'; } // Check for resource file pattern const resourceFilePattern = new RegExp(`\\.${escapedBranchName}(\\.resource\\.file\\..+)$`); const resourceFileMatch = branchSpecificPath.match(resourceFilePattern); if (resourceFileMatch) { const extension = resourceFileMatch[1]; const pathWithoutBranchAndExtension = branchSpecificPath.substring( 0, branchSpecificPath.length - `.${sanitizedBranchName}${extension}`.length ); return `${pathWithoutBranchAndExtension}${extension}`; } const yamlPattern = new RegExp(`\\.${escapedBranchName}(\\.${buildYamlTypePattern()}\\.yaml)$`); const yamlMatch = branchSpecificPath.match(yamlPattern); if (!yamlMatch) { return branchSpecificPath; // Return unchanged if not a branch-specific path } const extension = yamlMatch[1]; const pathWithoutBranchAndExtension = branchSpecificPath.substring( 0, branchSpecificPath.length - `.${sanitizedBranchName}${extension}`.length ); return `${pathWithoutBranchAndExtension}${extension}`; } /** * Get the branch-specific path for the current branch if the item should be branch-specific */ export function getBranchSpecificPath( basePath: string, specificItems: SpecificItemsConfig | undefined, branchOverride?: string ): string | undefined { if (!specificItems) { return undefined; } // Use branch override if provided, otherwise detect from git let currentBranch: string | null = null; if (branchOverride) { currentBranch = branchOverride; } else if (isGitRepository()) { currentBranch = getCurrentGitBranch(); } if (!currentBranch) { return undefined; } if (isSpecificItem(basePath, specificItems)) { return toBranchSpecificPath(basePath, currentBranch); } return undefined; } // Cache for compiled regex patterns to avoid recompilation const branchPatternCache = new Map(); /** * Check if a path is a branch-specific file for the current branch */ export function isCurrentBranchFile(path: string, branchOverride?: string): boolean { // Use branch override if provided, otherwise detect from git let currentBranch: string | null = null; if (branchOverride) { currentBranch = branchOverride; } else if (isGitRepository()) { currentBranch = getCurrentGitBranch(); } if (!currentBranch) { return false; } // Sanitize branch name to match what would be used in file naming const sanitizedBranchName = currentBranch.replace(/[\/\\:*?"<>|.]/g, '_'); const escapedBranchName = sanitizedBranchName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Use cached pattern or create and cache new one let pattern = branchPatternCache.get(currentBranch); if (!pattern) { pattern = new RegExp( `\\.${escapedBranchName}\\.${buildYamlTypePattern()}\\.yaml$|` + `\\.${escapedBranchName}\\.resource\\.file\\..+$|` + `/folder\\.${escapedBranchName}\\.meta\\.yaml$|` + `^settings\\.${escapedBranchName}\\.yaml$` ); branchPatternCache.set(currentBranch, pattern); } return pattern.test(path); } /** * Check if a path is a branch-specific file for ANY branch (not necessarily current) * Used to identify and skip files from other branches during sync operations */ export function isBranchSpecificFile(path: string): boolean { const yamlTypePattern = buildYamlTypePattern(); return new RegExp( `\\.[^.]+\\.${yamlTypePattern}\\.yaml$|` + `\\.[^.]+\\.resource\\.file\\..+$|` + `/folder\\.[^.]+\\.meta\\.yaml$|` + `^settings\\.[^.]+\\.yaml$` ).test(path); }