repoweaver
Version:
A GitHub App that skillfully weaves multiple templates together to create and update repositories with intelligent merge strategies
195 lines (157 loc) • 6.38 kB
text/typescript
import { GitHubClient, GitHubFile } from './github-client';
import { MergeStrategyRegistry } from './merge-strategy-registry';
import { FilePatternMergeStrategy, MergeStrategyConfig, TemplateProcessingResult, TemplateRepository } from './types';
export class GitHubTemplateManager {
private client: GitHubClient;
private mergeRegistry: MergeStrategyRegistry;
constructor(client: GitHubClient) {
this.client = client;
this.mergeRegistry = new MergeStrategyRegistry();
}
async processTemplate(
template: TemplateRepository,
targetOwner: string,
targetRepo: string,
excludePatterns: string[] = [],
mergeStrategy: 'overwrite' | 'merge' | 'skip' | MergeStrategyConfig = 'merge',
mergeStrategies: FilePatternMergeStrategy[] = [],
plugins: string[] = []
): Promise<TemplateProcessingResult> {
const result: TemplateProcessingResult = {
success: true,
template,
filesProcessed: 0,
errors: [],
};
try {
// Load plugins
for (const plugin of plugins) {
await this.mergeRegistry.loadPlugin(plugin);
}
// Get template files from GitHub
const templateFiles = await this.client.getTemplateFiles(template);
// Filter out excluded files
const filteredFiles = this.filterFiles(templateFiles, excludePatterns);
// Process files based on merge strategy
await this.processFiles(filteredFiles, targetOwner, targetRepo, mergeStrategy, mergeStrategies, result);
} catch (error) {
result.success = false;
result.errors.push(`Template processing failed: ${error}`);
}
return result;
}
private filterFiles(files: GitHubFile[], excludePatterns: string[]): GitHubFile[] {
return files.filter((file) => {
// Skip directories in processing
if (file.type === 'dir') {
return false;
}
// Apply exclude patterns
return !this.shouldExclude(file.path, excludePatterns);
});
}
private shouldExclude(filePath: string, excludePatterns: string[]): boolean {
return excludePatterns.some((pattern) => {
const regex = new RegExp(pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*'));
return regex.test(filePath);
});
}
private async processFiles(files: GitHubFile[], targetOwner: string, targetRepo: string, mergeStrategy: 'overwrite' | 'merge' | 'skip' | MergeStrategyConfig, mergeStrategies: FilePatternMergeStrategy[], result: TemplateProcessingResult): Promise<void> {
const branch = `boots-strapper-update-${Date.now()}`;
try {
// Create a new branch for the template updates
await this.client.createBranch(targetOwner, targetRepo, branch);
for (const file of files) {
try {
// Determine merge strategy for this file
const defaultMergeStrategy = typeof mergeStrategy === 'string' ? { type: mergeStrategy as 'overwrite' | 'merge' | 'skip' } : mergeStrategy;
const fileStrategy = await this.mergeRegistry.resolveStrategyForFile(file.path, mergeStrategies, defaultMergeStrategy);
const shouldProcess = await this.shouldProcessFile(targetOwner, targetRepo, file.path, fileStrategy.name);
if (shouldProcess) {
let content = file.content;
// Get existing content if file exists
let existingContent = '';
try {
const existingFiles = await this.client.getRepositoryContents(targetOwner, targetRepo, file.path);
const existingFile = existingFiles.find((f) => f.path === file.path && f.type === 'file');
existingContent = existingFile?.content || '';
} catch (error) {
// File doesn't exist, which is fine
}
// Apply merge strategy
if (existingContent) {
const mergeResult = await fileStrategy.merge({
filePath: file.path,
templateName: result.template.name,
existingContent,
newContent: file.content,
});
if (mergeResult.success) {
content = mergeResult.content;
// Track warnings and conflicts
if (mergeResult.warnings) {
result.errors.push(...mergeResult.warnings.map((w) => `Warning: ${w}`));
}
if (mergeResult.conflicts) {
result.errors.push(...mergeResult.conflicts.map((c) => `Conflict: ${c}`));
}
} else {
result.errors.push(`Merge failed for ${file.path}, using new content`);
}
}
await this.client.createOrUpdateFile(targetOwner, targetRepo, file.path, content, `Update ${file.path} from template ${result.template.name} using ${fileStrategy.name} strategy`, branch);
result.filesProcessed++;
}
} catch (error) {
result.errors.push(`Failed to process file ${file.path}: ${error}`);
}
}
// Create a pull request with the changes
if (result.filesProcessed > 0) {
const prNumber = await this.client.createPullRequest(targetOwner, targetRepo, `Update repository from template: ${result.template.name}`, this.generatePullRequestBody(result.template, result), branch, 'main');
result.pullRequestNumber = prNumber;
}
} catch (error) {
result.success = false;
result.errors.push(`Branch creation or PR failed: ${error}`);
}
}
private async shouldProcessFile(owner: string, repo: string, filePath: string, strategyName: string): Promise<boolean> {
if (strategyName === 'overwrite') {
return true;
}
try {
// Check if file exists
await this.client.getRepositoryContents(owner, repo, filePath);
// File exists
if (strategyName === 'skip') {
return false;
}
// All other strategies process existing files
return true;
} catch (error) {
// File doesn't exist, so we can create it
return true;
}
}
async cleanup(): Promise<void> {
await this.mergeRegistry.cleanup();
}
private generatePullRequestBody(template: TemplateRepository, result: TemplateProcessingResult): string {
return `
## Template Update
This pull request updates the repository with changes from the template: **${template.name}**
**Template Details:**
- Repository: ${template.url}
- Branch: ${template.branch || 'main'}
${template.subDirectory ? `- Subdirectory: ${template.subDirectory}` : ''}
**Changes:**
- ${result.filesProcessed} files processed
${result.errors.length > 0 ? `- ${result.errors.length} errors encountered` : ''}
**Files Modified:**
<!-- This will be populated with the actual file list -->
---
*This PR was automatically generated by [RepoWeaver](https://github.com/apps/repoweaver)*
`.trim();
}
}