Skip to main content
Glama
repository-sync.ts13.4 kB
/** * Repository Synchronizer * * Handles automatic synchronization of repositories between GitHub and Gitea providers. * Ensures repositories exist on both providers and keeps settings in sync. */ import { ProviderConfig } from '../providers/types.js'; import { GitHubProvider } from '../providers/github-provider.js'; import { GiteaProvider } from '../providers/gitea-provider.js'; import { GitCommandExecutor } from './git-command-executor.js'; export interface SyncResult { success: boolean; synchronized: boolean; actions: string[]; errors: string[]; details?: any; } export class RepositorySynchronizer { private githubProvider?: GitHubProvider; private giteaProvider?: GiteaProvider; private gitExecutor: GitCommandExecutor; private config: ProviderConfig; constructor(config: ProviderConfig) { this.config = config; if (config.github) { this.githubProvider = new GitHubProvider(config.github); } if (config.gitea) { this.giteaProvider = new GiteaProvider(config.gitea); } this.gitExecutor = new GitCommandExecutor(); } private getOwnerForProvider(provider: 'github' | 'gitea'): string { if (provider === 'github' && this.config.github) { return this.config.github.username; } if (provider === 'gitea' && this.config.gitea) { return this.config.gitea.username; } throw new Error(`No configuration found for provider: ${provider}`); } /** * Ensures repository exists on both providers, creating if necessary */ async ensureRepositoryExists(repoName: string, provider: 'github' | 'gitea'): Promise<SyncResult> { const result: SyncResult = { success: true, synchronized: true, actions: [], errors: [] }; try { // Get owners for each provider const githubOwner = this.getOwnerForProvider('github'); const giteaOwner = this.getOwnerForProvider('gitea'); // Check if repositories exist on both providers const githubExists = await this.checkRepositoryExists('github', repoName, githubOwner); const giteaExists = await this.checkRepositoryExists('gitea', repoName, giteaOwner); result.actions.push(`Checked GitHub: ${githubExists ? 'exists' : 'missing'}`); result.actions.push(`Checked Gitea: ${giteaExists ? 'exists' : 'missing'}`); // Create missing repositories if (!githubExists && this.githubProvider) { const createResult = await this.createRepository('github', repoName, githubOwner); if (createResult.success) { result.actions.push('Created repository on GitHub'); } else { result.errors.push(`Failed to create GitHub repository: ${createResult.error}`); result.success = false; } } if (!giteaExists && this.giteaProvider) { const createResult = await this.createRepository('gitea', repoName, giteaOwner); if (createResult.success) { result.actions.push('Created repository on Gitea'); } else { result.errors.push(`Failed to create Gitea repository: ${createResult.error}`); result.success = false; } } // If both existed or both were created successfully result.synchronized = (githubExists || result.success) && (giteaExists || result.success); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; result.errors.push(`Repository sync failed: ${errorMessage}`); result.success = false; result.synchronized = false; } return result; } /** * Syncs repository settings (description, visibility, etc.) between providers */ async syncRepositorySettings(repoName: string, provider: 'github' | 'gitea'): Promise<SyncResult> { const result: SyncResult = { success: true, synchronized: true, actions: [], errors: [] }; try { // Get owners for each provider const githubOwner = this.getOwnerForProvider('github'); const giteaOwner = this.getOwnerForProvider('gitea'); // Get repository info from both providers const githubInfo = await this.getRepositoryInfo('github', repoName, githubOwner); const giteaInfo = await this.getRepositoryInfo('gitea', repoName, giteaOwner); if (!githubInfo.success || !giteaInfo.success) { result.errors.push('Failed to get repository information from one or both providers'); result.success = false; return result; } // Sync settings (use GitHub as source of truth for simplicity) if (githubInfo.data && giteaInfo.data) { const syncActions = await this.syncSettingsFromSource(githubInfo.data, giteaInfo.data, repoName, giteaOwner); result.actions.push(...syncActions); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; result.errors.push(`Settings sync failed: ${errorMessage}`); result.success = false; } return result; } /** * Configures local git remotes for both providers */ async syncRemotes(projectPath: string, repoName: string, provider: 'github' | 'gitea'): Promise<SyncResult> { const result: SyncResult = { success: true, synchronized: true, actions: [], errors: [] }; try { // Get owners for each provider const githubOwner = this.getOwnerForProvider('github'); const giteaOwner = this.getOwnerForProvider('gitea'); // Get repository URLs from both providers const githubUrl = await this.getRepositoryUrl('github', repoName, githubOwner); const giteaUrl = await this.getRepositoryUrl('gitea', repoName, giteaOwner); if (!githubUrl || !giteaUrl) { result.errors.push('Failed to get repository URLs from providers'); result.success = false; return result; } // Configure remotes // First check if remote exists, then add or set-url accordingly const originExists = await this.checkRemoteExists(projectPath, 'origin'); const originResult = originExists ? await this.gitExecutor.executeGitCommand('remote', ['set-url', 'origin', githubUrl], projectPath) : await this.gitExecutor.executeGitCommand('remote', ['add', 'origin', githubUrl], projectPath); if (originResult.success) { result.actions.push('Configured origin remote (GitHub)'); } else { result.errors.push('Failed to configure origin remote'); result.success = false; } const backupExists = await this.checkRemoteExists(projectPath, 'backup'); const backupResult = backupExists ? await this.gitExecutor.executeGitCommand('remote', ['set-url', 'backup', giteaUrl], projectPath) : await this.gitExecutor.executeGitCommand('remote', ['add', 'backup', giteaUrl], projectPath); if (backupResult.success) { result.actions.push('Configured backup remote (Gitea)'); } else { result.errors.push('Failed to configure backup remote'); result.success = false; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; result.errors.push(`Remote sync failed: ${errorMessage}`); result.success = false; } return result; } /** * Attempts to recover from operation failures by creating missing resources */ async attemptRecovery(operation: string, params: any, failedProvider: string): Promise<boolean> { try { // For repository operations, try to create missing repository if (operation.includes('repo-') && params.repo) { const provider = failedProvider as 'github' | 'gitea'; const owner = this.getOwnerForProvider(provider); const exists = await this.checkRepositoryExists(provider, params.repo, owner); if (!exists) { const createResult = await this.createRepository(provider, params.repo, owner); return createResult.success; } } return false; } catch (error) { return false; } } /** * Checks if repository exists on specified provider */ private async checkRepositoryExists(provider: 'github' | 'gitea', repoName: string, owner: string): Promise<boolean> { try { const providerInstance = provider === 'github' ? this.githubProvider : this.giteaProvider; if (!providerInstance) return false; const result = await providerInstance.executeOperation(`repo-get`, { owner, repo: repoName }); return result.success; } catch (error) { return false; } } /** * Creates repository on specified provider */ private async createRepository(provider: 'github' | 'gitea', repoName: string, owner: string): Promise<{ success: boolean; error?: string }> { try { const providerInstance = provider === 'github' ? this.githubProvider : this.giteaProvider; if (!providerInstance) { return { success: false, error: `${provider} provider not configured` }; } const result = await providerInstance.executeOperation(`repo-create`, { name: repoName, private: false, // Default to public for sync description: `Repository synchronized from ${provider === 'github' ? 'GitHub' : 'Gitea'}` }); return { success: result.success, error: result.success ? undefined : result.error?.message }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { success: false, error: errorMessage }; } } /** * Gets repository information from specified provider */ private async getRepositoryInfo(provider: 'github' | 'gitea', repoName: string, owner: string): Promise<{ success: boolean; data?: any }> { try { const providerInstance = provider === 'github' ? this.githubProvider : this.giteaProvider; if (!providerInstance) return { success: false }; const result = await providerInstance.executeOperation(`repo-get`, { owner, repo: repoName }); if (result.success) { return { success: true, data: result.data }; } return { success: false }; } catch (error) { return { success: false }; } } /** * Gets repository clone URL from specified provider */ private async getRepositoryUrl(provider: 'github' | 'gitea', repoName: string, owner: string): Promise<string | null> { try { const providerInstance = provider === 'github' ? this.githubProvider : this.giteaProvider; if (!providerInstance) return null; const result = await providerInstance.executeOperation(`repo-get`, { owner, repo: repoName }); if (result.success && result.data) { // Return clone URL based on provider if (provider === 'github') { return `https://github.com/${owner}/${repoName}.git`; } else if (provider === 'gitea' && this.giteaProvider) { const config = this.giteaProvider['config']; // Access private config return `${config.url}/${owner}/${repoName}.git`; } } return null; } catch (error) { return null; } } /** * Syncs repository settings from source to target */ private async syncSettingsFromSource(sourceData: any, targetData: any, repoName: string, owner: string): Promise<string[]> { const actions: string[] = []; try { // Sync description if (sourceData.description !== targetData.description) { const updateResult = await this.updateRepositorySettings('gitea', repoName, owner, { description: sourceData.description }); if (updateResult) { actions.push('Synced repository description'); } } // Sync visibility (private/public) if (sourceData.private !== targetData.private) { const updateResult = await this.updateRepositorySettings('gitea', repoName, owner, { private: sourceData.private }); if (updateResult) { actions.push(`Synced repository visibility to ${sourceData.private ? 'private' : 'public'}`); } } } catch (error) { actions.push('Failed to sync some repository settings'); } return actions; } /** * Updates repository settings on specified provider */ private async updateRepositorySettings(provider: 'github' | 'gitea', repoName: string, owner: string, settings: any): Promise<boolean> { try { const providerInstance = provider === 'github' ? this.githubProvider : this.giteaProvider; if (!providerInstance) return false; const result = await providerInstance.executeOperation(`repo-update`, { owner, repo: repoName, ...settings }); return result.success; } catch (error) { return false; } } /** * Check if a remote exists in the repository */ private async checkRemoteExists(projectPath: string, remoteName: string): Promise<boolean> { try { const result = await this.gitExecutor.executeGitCommand('remote', [], projectPath); if (result.success && result.stdout) { const remotes = result.stdout.trim().split('\n').map(r => r.trim()); return remotes.includes(remoteName); } return false; } catch (error) { return false; } } }

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/Andre-Buzeli/git-mcp'

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