Skip to main content
Glama
svn-service.ts24.1 kB
import { SvnConfig, SvnResponse, SvnInfo, SvnStatus, SvnLogEntry, SvnCheckoutOptions, SvnUpdateOptions, SvnCommitOptions, SvnAddOptions, SvnDeleteOptions, SvnError } from '../common/types.js'; import { createSvnConfig, executeSvnCommand, parseInfoOutput, parseStatusOutput, parseLogOutput, validateSvnInstallation, isWorkingCopy, normalizePath, validatePath, validateSvnUrl, cleanOutput, formatDuration, clearSvnCredentials } from '../common/utils.js'; export class SvnService { private config: SvnConfig; constructor(config: Partial<SvnConfig> = {}) { this.config = createSvnConfig(config); } /** * Función auxiliar para manejar errores comunes de SVN */ private handleSvnError(error: any, operation: string): never { let message = `Failed to ${operation}`; if (error.message.includes('E155007') || error.message.includes('not a working copy')) { message = `El directorio '${this.config.workingDirectory}' no es un working copy de SVN. Asegúrate de estar en un directorio que contenga un repositorio SVN o hacer checkout primero.`; } else if (error.message.includes('E175002') || error.message.includes('Unable to connect')) { message = `No se puede conectar al repositorio SVN. Verifica tu conexión a internet y las credenciales.`; } else if (error.message.includes('E170001') || error.message.includes('Authentication failed')) { message = `Error de autenticación. Verifica tu nombre de usuario y contraseña SVN.`; } else if (error.message.includes('E155036') || error.message.includes('working copy locked')) { message = `El working copy está bloqueado. Ejecuta 'svn cleanup' para resolverlo.`; } else if (error.message.includes('E200030') || error.message.includes('sqlite')) { message = `Error en la base de datos del working copy. Ejecuta 'svn cleanup' para repararlo.`; } else if (error.stderr && error.stderr.length > 0) { message = `${message}: ${error.stderr}`; } else { message = `${message}: ${error.message}`; } throw new SvnError(message); } /** * Verificar que SVN está disponible y configurado correctamente */ async healthCheck(): Promise<SvnResponse<{ svnAvailable: boolean; version?: string; workingCopyValid?: boolean; repositoryAccessible?: boolean; }>> { try { // Verificar instalación de SVN const svnAvailable = await validateSvnInstallation(this.config); if (!svnAvailable) { return { success: false, error: 'SVN is not available in the system PATH', command: 'svn --version', workingDirectory: this.config.workingDirectory! }; } // Obtener versión de SVN const versionResponse = await executeSvnCommand(this.config, ['--version', '--quiet']); const version = versionResponse.data as string; // Verificar si estamos en un working copy const workingCopyValid = await isWorkingCopy(this.config.workingDirectory!); let repositoryAccessible = false; if (workingCopyValid) { try { await this.getInfo(); repositoryAccessible = true; } catch (error) { repositoryAccessible = false; } } return { success: true, data: { svnAvailable, version: version.trim(), workingCopyValid, repositoryAccessible }, command: 'health-check', workingDirectory: this.config.workingDirectory! }; } catch (error: any) { return { success: false, error: error.message, command: 'health-check', workingDirectory: this.config.workingDirectory! }; } } /** * Obtener información del working copy o directorio específico */ async getInfo(path?: string): Promise<SvnResponse<SvnInfo>> { try { const args = ['info']; if (path) { // Check if it's a URL or a local path if (validateSvnUrl(path)) { // It's a URL, add it directly without normalization args.push(path); } else if (validatePath(path)) { // It's a local path, normalize it args.push(normalizePath(path)); } else { throw new SvnError(`Invalid path or URL: ${path}`); } } const response = await executeSvnCommand(this.config, args); const info = parseInfoOutput(cleanOutput(response.data as string)); return { success: true, data: info, command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { this.handleSvnError(error, 'get SVN info'); } } /** * Obtener estado de archivos en el working copy */ async getStatus(path?: string, showAll: boolean = false): Promise<SvnResponse<SvnStatus[]>> { try { const args = ['status']; if (path) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } args.push(normalizePath(path)); } let response; // Si showAll es true, intentar primero con --show-updates if (showAll) { try { const argsWithUpdates = [...args, '--show-updates']; response = await executeSvnCommand(this.config, argsWithUpdates); } catch (error: any) { // Si falla con --show-updates, intentar sin él console.warn(`Warning: --show-updates failed, falling back to local status only: ${error.message}`); response = await executeSvnCommand(this.config, args); } } else { response = await executeSvnCommand(this.config, args); } const statusList = parseStatusOutput(cleanOutput(response.data as string)); return { success: true, data: statusList, command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { this.handleSvnError(error, 'get SVN status'); } } /** * Obtener historial de cambios (log) */ async getLog( path?: string, limit?: number, revision?: string ): Promise<SvnResponse<SvnLogEntry[]>> { try { const args = ['log']; if (limit && limit > 0) { args.push('--limit', limit.toString()); } if (revision) { args.push('--revision', revision); } if (path) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } args.push(normalizePath(path)); } let response; try { response = await executeSvnCommand(this.config, args); } catch (error: any) { // Detectar si SVN no está instalado if ((error.message.includes('spawn') && error.message.includes('ENOENT')) || error.code === 127) { const enhancedError = new SvnError( 'SVN no está instalado o no se encuentra en el PATH del sistema. Instala Subversion para usar este comando.' ); enhancedError.command = error.command; enhancedError.code = error.code; throw enhancedError; } // Detectar errores de red/conectividad y proporcionar mensajes más útiles if (error.message.includes('E175002') || error.message.includes('Unable to connect') || error.message.includes('Connection refused') || error.message.includes('Network is unreachable') || error.code === 1) { // Intentar con opciones que funcionen sin conectividad remota si es posible console.warn(`Log remoto falló, posible problema de conectividad: ${error.message}`); // Para comandos log, podemos intentar usar --offline si está disponible, // o proporcionar una respuesta vacía con información útil const enhancedError = new SvnError( `No se pudo obtener el historial de cambios. Posibles causas: - Sin conectividad al servidor SVN - Credenciales requeridas pero no proporcionadas - Servidor SVN temporalmente inaccesible - Working copy no sincronizado con el repositorio remoto` ); enhancedError.command = error.command; enhancedError.stderr = error.stderr; enhancedError.code = error.code; throw enhancedError; } // Re-lanzar otros errores sin modificar throw error; } const logEntries = parseLogOutput(cleanOutput(response.data as string)); return { success: true, data: logEntries, command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { this.handleSvnError(error, 'get SVN log'); } } /** * Obtener diferencias entre versiones */ async getDiff( path?: string, oldRevision?: string, newRevision?: string ): Promise<SvnResponse<string>> { try { const args = ['diff']; if (oldRevision && newRevision) { args.push('--old', `${path || '.'}@${oldRevision}`); args.push('--new', `${path || '.'}@${newRevision}`); } else if (oldRevision) { args.push('--revision', oldRevision); if (path) { args.push(normalizePath(path)); } } else if (path) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } args.push(normalizePath(path)); } const response = await executeSvnCommand(this.config, args); return { success: true, data: cleanOutput(response.data as string), command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { throw new SvnError(`Failed to get SVN diff: ${error.message}`); } } /** * Checkout de un repositorio */ async checkout( url: string, path?: string, options: SvnCheckoutOptions = {} ): Promise<SvnResponse<string>> { try { if (!validateSvnUrl(url)) { throw new SvnError(`Invalid SVN URL: ${url}`); } const args = ['checkout']; if (options.revision) { args.push('--revision', options.revision.toString()); } if (options.depth) { args.push('--depth', options.depth); } if (options.force) { args.push('--force'); } if (options.ignoreExternals) { args.push('--ignore-externals'); } args.push(url); if (path) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } args.push(normalizePath(path)); } const response = await executeSvnCommand(this.config, args); return { success: true, data: cleanOutput(response.data as string), command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { throw new SvnError(`Failed to checkout: ${error.message}`); } } /** * Actualizar working copy */ async update( path?: string, options: SvnUpdateOptions = {} ): Promise<SvnResponse<string>> { try { const args = ['update']; if (options.revision) { args.push('--revision', options.revision.toString()); } if (options.force) { args.push('--force'); } if (options.ignoreExternals) { args.push('--ignore-externals'); } if (options.acceptConflicts) { args.push('--accept', options.acceptConflicts); } if (path) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } args.push(normalizePath(path)); } const response = await executeSvnCommand(this.config, args); return { success: true, data: cleanOutput(response.data as string), command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { throw new SvnError(`Failed to update: ${error.message}`); } } /** * Añadir archivos al control de versiones */ async add( paths: string | string[], options: SvnAddOptions = {} ): Promise<SvnResponse<string>> { try { const pathArray = Array.isArray(paths) ? paths : [paths]; // Validar todas las rutas for (const path of pathArray) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } } const args = ['add']; if (options.force) { args.push('--force'); } if (options.noIgnore) { args.push('--no-ignore'); } if (options.autoProps) { args.push('--auto-props'); } if (options.noAutoProps) { args.push('--no-auto-props'); } if (options.parents) { args.push('--parents'); } // Añadir rutas normalizadas args.push(...pathArray.map(p => normalizePath(p))); const response = await executeSvnCommand(this.config, args); return { success: true, data: cleanOutput(response.data as string), command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { throw new SvnError(`Failed to add files: ${error.message}`); } } /** * Confirmar cambios al repositorio */ async commit( options: SvnCommitOptions, paths?: string[] ): Promise<SvnResponse<string>> { try { if (!options.message && !options.file) { throw new SvnError('Commit message is required'); } const args = ['commit']; if (options.message) { args.push('--message', options.message); } if (options.file) { args.push('--file', normalizePath(options.file)); } if (options.force) { args.push('--force'); } if (options.keepLocks) { args.push('--keep-locks'); } if (options.noUnlock) { args.push('--no-unlock'); } // Añadir rutas específicas si se proporcionan if (paths && paths.length > 0) { for (const path of paths) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } } args.push(...paths.map(p => normalizePath(p))); } else if (options.targets) { args.push(...options.targets.map(p => normalizePath(p))); } const response = await executeSvnCommand(this.config, args); return { success: true, data: cleanOutput(response.data as string), command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { throw new SvnError(`Failed to commit: ${error.message}`); } } /** * Eliminar archivos del control de versiones */ async delete( paths: string | string[], options: SvnDeleteOptions = {} ): Promise<SvnResponse<string>> { try { const pathArray = Array.isArray(paths) ? paths : [paths]; // Validar todas las rutas for (const path of pathArray) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } } const args = ['delete']; if (options.force) { args.push('--force'); } if (options.keepLocal) { args.push('--keep-local'); } if (options.message) { args.push('--message', options.message); } // Añadir rutas normalizadas args.push(...pathArray.map(p => normalizePath(p))); const response = await executeSvnCommand(this.config, args); return { success: true, data: cleanOutput(response.data as string), command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { throw new SvnError(`Failed to delete files: ${error.message}`); } } /** * Revertir cambios locales */ async revert(paths: string | string[]): Promise<SvnResponse<string>> { try { const pathArray = Array.isArray(paths) ? paths : [paths]; // Validar todas las rutas for (const path of pathArray) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } } const args = ['revert']; // Añadir rutas normalizadas args.push(...pathArray.map(p => normalizePath(p))); const response = await executeSvnCommand(this.config, args); return { success: true, data: cleanOutput(response.data as string), command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { throw new SvnError(`Failed to revert files: ${error.message}`); } } /** * Limpiar working copy */ async cleanup(path?: string): Promise<SvnResponse<string>> { try { const args = ['cleanup']; if (path) { if (!validatePath(path)) { throw new SvnError(`Invalid path: ${path}`); } args.push(normalizePath(path)); } const response = await executeSvnCommand(this.config, args); return { success: true, data: cleanOutput(response.data as string), command: response.command, workingDirectory: response.workingDirectory, executionTime: response.executionTime }; } catch (error: any) { throw new SvnError(`Failed to cleanup: ${error.message}`); } } /** * Diagnóstico específico para comandos problemáticos */ async diagnoseCommands(): Promise<SvnResponse<{ statusLocal: boolean; statusRemote: boolean; logBasic: boolean; workingCopyPath: string; errors: string[]; suggestions: string[]; }>> { const results = { statusLocal: false, statusRemote: false, logBasic: false, workingCopyPath: this.config.workingDirectory!, errors: [] as string[], suggestions: [] as string[] }; try { // Probar svn status local try { await executeSvnCommand(this.config, ['status']); results.statusLocal = true; } catch (error: any) { const errorMsg = this.categorizeError(error, 'status local'); results.errors.push(errorMsg.message); if (errorMsg.suggestion) { results.suggestions.push(errorMsg.suggestion); } } // Probar svn status con --show-updates try { await executeSvnCommand(this.config, ['status', '--show-updates']); results.statusRemote = true; } catch (error: any) { const errorMsg = this.categorizeError(error, 'status remoto'); results.errors.push(errorMsg.message); if (errorMsg.suggestion) { results.suggestions.push(errorMsg.suggestion); } } // Probar svn log básico try { await executeSvnCommand(this.config, ['log', '--limit', '1']); results.logBasic = true; } catch (error: any) { const errorMsg = this.categorizeError(error, 'log básico'); results.errors.push(errorMsg.message); if (errorMsg.suggestion) { results.suggestions.push(errorMsg.suggestion); } } // Agregar sugerencias generales basadas en los resultados if (!results.statusRemote && !results.logBasic && results.statusLocal) { results.suggestions.push('Los comandos remotos fallan pero el local funciona. Revisa la conectividad de red y credenciales SVN.'); } return { success: true, data: results, command: 'diagnostic', workingDirectory: this.config.workingDirectory! }; } catch (error: any) { results.errors.push(`Error general: ${error.message}`); return { success: false, data: results, error: error.message, command: 'diagnostic', workingDirectory: this.config.workingDirectory! }; } } /** * Categorizar errores y proporcionar sugerencias específicas */ private categorizeError(error: any, commandType: string): { message: string; suggestion?: string } { const baseMessage = `${commandType} falló`; // SVN no encontrado en el sistema if ((error.message.includes('spawn') && error.message.includes('ENOENT')) || error.code === 127) { return { message: `${baseMessage}: SVN no está instalado o no se encuentra en el PATH`, suggestion: 'Instala SVN (subversion) o verifica que esté en el PATH del sistema' }; } // Errores de conectividad if (error.message.includes('E175002') || error.message.includes('Unable to connect') || error.message.includes('Connection refused') || error.message.includes('Network is unreachable')) { return { message: `${baseMessage}: Sin conectividad al servidor SVN`, suggestion: 'Verifica tu conexión a internet y que el servidor SVN esté accesible' }; } // Errores de autenticación - demasiados intentos if (error.message.includes('E215004') || error.message.includes('No more credentials') || error.message.includes('we tried too many times')) { return { message: `${baseMessage}: Demasiados intentos de autenticación fallidos`, suggestion: 'Las credenciales pueden estar incorrectas o cachadas. Limpia el cache de credenciales SVN y verifica SVN_USERNAME y SVN_PASSWORD' }; } // Errores de autenticación generales if (error.message.includes('E170001') || error.message.includes('Authentication failed') || error.message.includes('authorization failed')) { return { message: `${baseMessage}: Error de autenticación`, suggestion: 'Verifica tus credenciales SVN (SVN_USERNAME y SVN_PASSWORD)' }; } // Working copy no válido if (error.message.includes('E155007') || error.message.includes('not a working copy')) { return { message: `${baseMessage}: No es un working copy válido`, suggestion: 'Asegúrate de estar en un directorio con checkout de SVN o ejecuta svn checkout primero' }; } // Working copy bloqueado if (error.message.includes('E155036') || error.message.includes('working copy locked')) { return { message: `${baseMessage}: Working copy bloqueado`, suggestion: 'Ejecuta "svn cleanup" para desbloquear el working copy' }; } // Error genérico con código 1 (frecuente en comandos remotos) if (error.code === 1) { return { message: `${baseMessage}: Comando falló con código 1 (posible problema de red/autenticación)`, suggestion: 'Revisa conectividad de red, credenciales SVN, y que el repositorio sea accesible' }; } // Error genérico return { message: `${baseMessage}: ${error.message}`, suggestion: undefined }; } /** * Limpiar cache de credenciales SVN para resolver errores de autenticación */ async clearCredentials(): Promise<SvnResponse> { return await clearSvnCredentials(this.config); } }

Implementation Reference

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/gcorroto/mcp-svn'

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