Skip to main content
Glama
git-sync.ts20.9 kB
/** * Git Sync Tool * * Advanced synchronization tool providing intelligent sync and status operations. * Supports both local Git synchronization and remote provider synchronization. * * Operations: sync, status */ import { GitCommandExecutor, GitCommandResult } from '../utils/git-command-executor.js'; import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js'; import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js'; import { ProviderOperationHandler } from '../providers/provider-operation-handler.js'; import { ProviderConfig, ProviderOperation } from '../providers/types.js'; import { configManager } from '../config.js'; export interface GitSyncParams extends ToolParams { action: 'sync' | 'status'; // Sync parameters remote?: string; // Remote to sync with (default: origin) branch?: string; // Branch to sync (default: current branch) strategy?: 'merge' | 'rebase' | 'fast-forward'; // Sync strategy force?: boolean; // Force sync (use with caution) dryRun?: boolean; // Show what would be done without executing // Status parameters detailed?: boolean; // Show detailed sync status includeRemote?: boolean; // Include remote status information checkAhead?: boolean; // Check commits ahead/behind // Provider parameters (for remote sync) repo?: string; // Repository name (auto-detected if not provided) } export interface SyncStatus { localBranch: string; remoteBranch: string; ahead: number; behind: number; hasUncommittedChanges: boolean; hasUntrackedFiles: boolean; lastSync?: string; conflicts?: string[]; remoteStatus?: { exists: boolean; lastCommit?: string; lastCommitDate?: string; }; } export interface SyncResult { success: boolean; status: SyncStatus; operations: string[]; conflicts?: string[]; warnings?: string[]; } export class GitSyncTool { private gitExecutor: GitCommandExecutor; private providerHandler?: ProviderOperationHandler; constructor(providerConfig?: ProviderConfig) { this.gitExecutor = new GitCommandExecutor(); if (providerConfig) { this.providerHandler = new ProviderOperationHandler(providerConfig); } } /** * Execute git-sync operation */ async execute(params: GitSyncParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate basic parameters const validation = ParameterValidator.validateToolParams('git-sync', params); if (!validation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Parameter validation failed: ${validation.errors.join(', ')}`, params.action, { validationErrors: validation.errors }, validation.suggestions ); } // Validate operation-specific parameters const operationValidation = this.validateOperationParams(params); if (!operationValidation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Operation validation failed: ${operationValidation.errors.join(', ')}`, params.action, { validationErrors: operationValidation.errors }, operationValidation.suggestions ); } // Route to appropriate handler switch (params.action) { case 'sync': return await this.handleSync(params, startTime); case 'status': return await this.handleStatus(params, startTime); default: return OperationErrorHandler.createToolError( 'UNSUPPORTED_OPERATION', `Unsupported operation: ${params.action}`, params.action, { supportedOperations: ['sync', 'status'] }, ['Use one of the supported operations: sync, status'] ); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'EXECUTION_ERROR', `Failed to execute git-sync operation: ${errorMessage}`, params.action, { error: errorMessage } ); } } /** * Validate operation-specific parameters */ private validateOperationParams(params: GitSyncParams): { isValid: boolean; errors: string[]; suggestions: string[] } { const errors: string[] = []; const suggestions: string[] = []; // Validate sync parameters if (params.action === 'sync') { if (params.strategy && !['merge', 'rebase', 'fast-forward'].includes(params.strategy)) { errors.push('Invalid sync strategy. Must be one of: merge, rebase, fast-forward'); suggestions.push('Use strategy: "merge", "rebase", or "fast-forward"'); } } return { isValid: errors.length === 0, errors, suggestions }; } /** * Handle sync operation */ private async handleSync(params: GitSyncParams, startTime: number): Promise<ToolResult> { try { const remote = params.remote || 'origin'; const branch = params.branch || await this.getCurrentBranch(params.projectPath); const strategy = params.strategy || 'merge'; const operations: string[] = []; // Get current status first const statusResult = await this.getCurrentStatus(params); if (!statusResult.success) { return OperationErrorHandler.createToolError( 'STATUS_CHECK_FAILED', 'Failed to get current repository status', params.action, { statusError: statusResult } ); } const status = statusResult.status; // Check for uncommitted changes if (status.hasUncommittedChanges && !params.force) { return OperationErrorHandler.createToolError( 'UNCOMMITTED_CHANGES', 'Repository has uncommitted changes. Commit or stash changes before syncing.', params.action, { status }, [ 'Commit changes: git add . && git commit -m "message"', 'Stash changes: git stash (use git stash pop later to restore)', 'Use git-stash tool to save changes temporarily', 'Use force: true to override (not recommended - may cause data loss)' ] ); } // Dry run - show what would be done if (params.dryRun) { const plannedOperations = await this.planSyncOperations(params, status); return { success: true, data: { dryRun: true, plannedOperations, currentStatus: status }, metadata: { operation: params.action, executionTime: Date.now() - startTime, timestamp: new Date().toISOString() } }; } // Fetch latest changes const fetchArgs = [remote]; if (branch) { // Only fetch specific branch if it's different from current branch const currentBranch = await this.getCurrentBranch(params.projectPath); if (currentBranch !== branch) { fetchArgs.push(`${branch}:${branch}`); } } const fetchResult = await this.gitExecutor.executeGitCommand('fetch', fetchArgs, params.projectPath); if (!fetchResult.success) { return OperationErrorHandler.createToolError( 'FETCH_FAILED', `Failed to fetch from remote: ${fetchResult.stderr}`, params.action, { fetchResult } ); } operations.push(`Fetched from ${remote}`); // Get updated status after fetch const updatedStatus = await this.getCurrentStatus(params); if (!updatedStatus.success) { return OperationErrorHandler.createToolError( 'STATUS_UPDATE_FAILED', 'Failed to get updated status after fetch', params.action ); } const newStatus = updatedStatus.status; // Perform sync based on strategy const syncResult = await this.performSync(params, newStatus, strategy, operations); // Handle remote sync if provider is available if (this.providerHandler && params.provider) { const remoteSync = await this.performRemoteSync(params); if (remoteSync.success) { operations.push('Remote synchronization completed'); } else { syncResult.warnings = syncResult.warnings || []; syncResult.warnings.push('Remote synchronization failed but local sync succeeded'); } } return { success: syncResult.success, data: { syncResult, operations, finalStatus: await this.getCurrentStatus(params) }, metadata: { operation: params.action, executionTime: Date.now() - startTime, timestamp: new Date().toISOString() } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'SYNC_FAILED', `Sync operation failed: ${errorMessage}`, params.action, { error: errorMessage } ); } } /** * Handle status operation */ private async handleStatus(params: GitSyncParams, startTime: number): Promise<ToolResult> { try { const statusResult = await this.getCurrentStatus(params); if (!statusResult.success) { return OperationErrorHandler.createToolError( 'STATUS_FAILED', 'Failed to get repository status', params.action, { statusResult } ); } let data: any = { status: statusResult.status }; // Include remote status if requested if (params.includeRemote && this.providerHandler && params.provider) { const remoteStatus = await this.getRemoteStatus(params); data.remoteStatus = remoteStatus; } // Include detailed information if requested if (params.detailed) { const detailedInfo = await this.getDetailedStatus(params); data.detailed = detailedInfo; } return { success: true, data, metadata: { operation: params.action, executionTime: Date.now() - startTime, timestamp: new Date().toISOString() } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'STATUS_FAILED', `Status operation failed: ${errorMessage}`, params.action, { error: errorMessage } ); } } /** * Get current repository status */ private async getCurrentStatus(params: GitSyncParams): Promise<{ success: boolean; status: SyncStatus }> { try { const remote = params.remote || 'origin'; const branch = params.branch || await this.getCurrentBranch(params.projectPath); const remoteBranch = `${remote}/${branch}`; // Check if we have uncommitted changes const statusResult = await this.gitExecutor.executeGitCommand('status', ['--porcelain'], params.projectPath); const hasUncommittedChanges = statusResult.success && statusResult.stdout.trim().length > 0; // Check for untracked files const untrackedResult = await this.gitExecutor.executeGitCommand('ls-files', ['--others', '--exclude-standard'], params.projectPath); const hasUntrackedFiles = untrackedResult.success && untrackedResult.stdout.trim().length > 0; // Get ahead/behind information let ahead = 0; let behind = 0; if (params.checkAhead !== false) { const revListResult = await this.gitExecutor.executeGitCommand('rev-list', ['--left-right', '--count', `${remoteBranch}...HEAD`], params.projectPath); if (revListResult.success) { const counts = revListResult.stdout.trim().split('\t'); if (counts.length === 2) { behind = parseInt(counts[0]) || 0; ahead = parseInt(counts[1]) || 0; } } } const status: SyncStatus = { localBranch: branch || 'main', remoteBranch, ahead, behind, hasUncommittedChanges: !!hasUncommittedChanges, hasUntrackedFiles: !!hasUntrackedFiles }; return { success: true, status }; } catch (error) { return { success: false, status: {} as SyncStatus }; } } /** * Plan sync operations for dry run */ private async planSyncOperations(params: GitSyncParams, status: SyncStatus): Promise<string[]> { const operations: string[] = []; const remote = params.remote || 'origin'; const strategy = params.strategy || 'merge'; operations.push(`Fetch from ${remote}`); if (status.behind > 0) { switch (strategy) { case 'merge': operations.push(`Merge ${status.behind} commits from ${status.remoteBranch}`); break; case 'rebase': operations.push(`Rebase ${status.behind} commits from ${status.remoteBranch}`); break; case 'fast-forward': operations.push(`Fast-forward ${status.behind} commits from ${status.remoteBranch}`); break; } } if (status.ahead > 0) { operations.push(`Push ${status.ahead} local commits to ${status.remoteBranch}`); } if (status.behind === 0 && status.ahead === 0) { operations.push('Repository is already up to date'); } return operations; } /** * Perform actual sync operation */ private async performSync(params: GitSyncParams, status: SyncStatus, strategy: string, operations: string[]): Promise<SyncResult> { const result: SyncResult = { success: true, status, operations: [...operations] }; try { // Handle incoming changes (behind) if (status.behind > 0) { let syncCommand: string[]; switch (strategy) { case 'merge': syncCommand = ['merge', status.remoteBranch]; break; case 'rebase': syncCommand = ['rebase', status.remoteBranch]; break; case 'fast-forward': syncCommand = ['merge', '--ff-only', status.remoteBranch]; break; default: syncCommand = ['merge', status.remoteBranch]; } const syncResult = await this.gitExecutor.executeGitCommand(syncCommand[0], syncCommand.slice(1), params.projectPath); if (!syncResult.success) { result.success = false; result.conflicts = [syncResult.stderr || 'Sync failed']; return result; } result.operations.push(`${strategy} completed: ${status.behind} commits integrated`); } // Handle outgoing changes (ahead) if (status.ahead > 0) { const pushResult = await this.gitExecutor.executeGitCommand('push', [], params.projectPath); if (!pushResult.success) { result.warnings = result.warnings || []; result.warnings.push(`Failed to push local commits: ${pushResult.stderr}`); } else { result.operations.push(`Pushed ${status.ahead} commits to remote`); } } return result; } catch (error) { result.success = false; result.conflicts = [error instanceof Error ? error.message : 'Unknown sync error']; return result; } } /** * Perform remote synchronization using provider */ private async performRemoteSync(params: GitSyncParams): Promise<{ success: boolean; error?: string }> { if (!this.providerHandler || !params.provider) { return { success: false, error: 'Provider not configured' }; } try { const operation: ProviderOperation = { provider: params.provider, operation: 'sync', parameters: { repo: params.repo, branch: params.branch }, requiresAuth: true, isRemoteOperation: true }; const result = await this.providerHandler.executeOperation(operation); return { success: result.success }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Remote sync failed' }; } } /** * Get remote status information */ private async getRemoteStatus(params: GitSyncParams): Promise<any> { if (!this.providerHandler || !params.provider) { return { error: 'Provider not configured' }; } try { const operation: ProviderOperation = { provider: params.provider, operation: 'get', parameters: { repo: params.repo }, requiresAuth: true, isRemoteOperation: true }; const result = await this.providerHandler.executeOperation(operation); return result.success ? result.results[0]?.data : { error: 'Failed to get remote status' }; } catch (error) { return { error: error instanceof Error ? error.message : 'Remote status failed' }; } } /** * Get detailed status information */ private async getDetailedStatus(params: GitSyncParams): Promise<any> { try { // Get commit history const logResult = await this.gitExecutor.executeGitCommand('log', ['--oneline', '-10'], params.projectPath); // Get branch information const branchResult = await this.gitExecutor.executeGitCommand('branch', ['-vv'], params.projectPath); // Get remote information const remoteResult = await this.gitExecutor.executeGitCommand('remote', ['-v'], params.projectPath); return { recentCommits: logResult.success ? logResult.stdout.split('\n').filter((line: string) => line.trim()) : [], branches: branchResult.success ? branchResult.stdout.split('\n').filter((line: string) => line.trim()) : [], remotes: remoteResult.success ? remoteResult.stdout.split('\n').filter((line: string) => line.trim()) : [] }; } catch (error) { return { error: error instanceof Error ? error.message : 'Failed to get detailed status' }; } } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-sync', description: 'Advanced Git synchronization tool for intelligent sync and status operations. Supports both local Git synchronization and remote provider synchronization.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['sync', 'status'], description: 'The Git sync operation to perform' }, projectPath: { type: 'string', description: 'Absolute path to the project directory' }, provider: { type: 'string', enum: ['github', 'gitea', 'both'], description: 'Provider for remote operations (optional for local-only operations)' }, remote: { type: 'string', description: 'Remote to sync with (default: origin)' }, branch: { type: 'string', description: 'Branch to sync (default: current branch)' }, strategy: { type: 'string', enum: ['merge', 'rebase', 'fast-forward'], description: 'Sync strategy (default: merge)' }, force: { type: 'boolean', description: 'Force sync (use with caution, may override uncommitted changes)' }, dryRun: { type: 'boolean', description: 'Show what would be done without executing (for sync operation)' }, detailed: { type: 'boolean', description: 'Show detailed sync status (for status operation)' }, includeRemote: { type: 'boolean', description: 'Include remote status information (for status operation)' }, checkAhead: { type: 'boolean', description: 'Check commits ahead/behind (default: true for status operation)' }, repo: { type: 'string', description: 'Repository name (auto-detected if not provided, for remote operations)' } }, required: ['action', 'projectPath'] } }; } /** * Get current branch name */ private async getCurrentBranch(projectPath: string): Promise<string | null> { try { const result = await this.gitExecutor.executeGitCommand('branch', ['--show-current'], projectPath); if (result.success && result.stdout.trim()) { return result.stdout.trim(); } return null; } catch (error) { return null; } } }

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

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