/**
* 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;
}
}
}