Skip to main content
Glama
git-branches.ts24.5 kB
/** * Git Branches Tool * * Branch management tool providing comprehensive Git branch operations. * Supports both local Git operations and remote provider operations. * * Operations: create, list, get, delete, merge, compare */ 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 GitBranchesParams extends ToolParams { action: 'create' | 'list' | 'get' | 'delete' | 'merge' | 'compare'; // Branch operation parameters branchName?: string; // For create, get, delete, merge sourceBranch?: string; // For create (branch from) targetBranch?: string; // For merge (merge into) baseBranch?: string; // For compare compareBranch?: string; // For compare // Options force?: boolean; // For delete, merge remote?: string; // For remote operations (default: origin) checkout?: boolean; // For create (checkout after creation) // Remote operation parameters repo?: string; // For remote operations } export class GitBranchesTool { private gitExecutor: GitCommandExecutor; private providerHandler?: ProviderOperationHandler; constructor(providerConfig?: ProviderConfig) { this.gitExecutor = new GitCommandExecutor(); if (providerConfig) { this.providerHandler = new ProviderOperationHandler(providerConfig); } } /** * Execute git-branches operation */ async execute(params: GitBranchesParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate basic parameters const validation = ParameterValidator.validateToolParams('git-branches', 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 = ParameterValidator.validateOperationParams('git-branches', params.action, 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 const isRemoteOperation = this.isRemoteOperation(params.action); if (isRemoteOperation) { return await this.executeRemoteOperation(params, startTime); } else { return await this.executeLocalOperation(params, startTime); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'EXECUTION_ERROR', `Failed to execute ${params.action}: ${errorMessage}`, params.action, { error: errorMessage }, ['Check the error details and try again'] ); } } /** * Execute local Git branch operations */ private async executeLocalOperation(params: GitBranchesParams, startTime: number): Promise<ToolResult> { switch (params.action) { case 'create': return await this.handleCreateBranch(params, startTime); case 'list': return await this.handleListBranches(params, startTime); case 'get': return await this.handleGetBranch(params, startTime); case 'delete': return await this.handleDeleteBranch(params, startTime); case 'merge': return await this.handleMergeBranch(params, startTime); case 'compare': return await this.handleCompareBranches(params, startTime); default: return OperationErrorHandler.createToolError( 'UNSUPPORTED_OPERATION', `Local operation '${params.action}' is not supported`, params.action, {}, ['Use one of: create, list, get, delete, merge, compare'] ); } } /** * Execute remote provider operations */ private async executeRemoteOperation(params: GitBranchesParams, startTime: number): Promise<ToolResult> { if (!this.providerHandler) { return OperationErrorHandler.createToolError( 'PROVIDER_NOT_CONFIGURED', 'Provider handler is not configured for remote operations', params.action, {}, ['Configure GitHub or Gitea provider to use remote operations'] ); } if (!params.provider) { if (configManager.isUniversalMode()) { params.provider = 'both'; console.error(`[Universal Mode] Auto-applying both providers for ${params.action}`); } else { return OperationErrorHandler.createToolError( 'PROVIDER_REQUIRED', 'Provider parameter is required for remote operations', params.action, {}, ['Specify provider as: github, gitea, or both'] ); } } const operation: ProviderOperation = { provider: params.provider, operation: this.mapActionToProviderOperation(params.action), parameters: this.extractRemoteParameters(params), requiresAuth: true, isRemoteOperation: true }; try { const result = await this.providerHandler.executeOperation(operation); return { success: result.success, data: result.partialFailure ? result : result.results[0]?.data, error: result.success ? undefined : { code: result.errors[0]?.error?.code || 'REMOTE_OPERATION_ERROR', message: result.errors[0]?.error?.message || 'Remote operation failed', details: result.errors, suggestions: ['Check provider configuration and credentials'] }, metadata: { provider: params.provider, operation: params.action, timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_OPERATION_ERROR', `Remote operation failed: ${errorMessage}`, params.action, { error: errorMessage }, ['Check provider configuration and network connectivity'] ); } } /** * Handle create branch operation */ private async handleCreateBranch(params: GitBranchesParams, startTime: number): Promise<ToolResult> { try { if (!params.branchName) { return OperationErrorHandler.createToolError( 'MISSING_PARAMETER', 'branchName is required for branch creation', 'create', {}, ['Provide a name for the new branch'] ); } // Check if branch already exists const existingBranches = await this.gitExecutor.listBranches(params.projectPath, { all: false }); if (existingBranches.success && existingBranches.branches?.includes(params.branchName)) { return OperationErrorHandler.createToolError( 'BRANCH_EXISTS', `Branch '${params.branchName}' already exists`, 'create', { branchName: params.branchName }, ['Use a different branch name or delete the existing branch first'] ); } // Create branch const result = await this.gitExecutor.createBranch( params.projectPath, params.branchName, params.sourceBranch ); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'create branch', params.projectPath); } // Checkout branch if requested let checkoutResult: GitCommandResult | null = null; if (params.checkout) { checkoutResult = await this.gitExecutor.checkoutBranch(params.projectPath, params.branchName); if (!checkoutResult.success) { return OperationErrorHandler.handleGitError(checkoutResult.stderr, 'checkout branch', params.projectPath); } } return { success: true, data: { message: `Branch '${params.branchName}' created successfully`, branchName: params.branchName, sourceBranch: params.sourceBranch || 'current branch', checkedOut: params.checkout || false, output: result.stdout, checkoutOutput: checkoutResult?.stdout }, metadata: { operation: 'create', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'CREATE_BRANCH_ERROR', `Failed to create branch: ${errorMessage}`, 'create', { error: errorMessage, projectPath: params.projectPath } ); } } /** * Handle list branches operation */ private async handleListBranches(params: GitBranchesParams, startTime: number): Promise<ToolResult> { try { const result = await this.gitExecutor.listBranches(params.projectPath, { all: true, remote: params.remote }); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'list branches', params.projectPath); } // Get current branch const currentBranchResult = await this.gitExecutor.getCurrentBranch(params.projectPath); const currentBranch = currentBranchResult.success ? currentBranchResult.branch : null; return { success: true, data: { branches: result.branches || [], remoteBranches: result.remoteBranches || [], currentBranch, total: (result.branches?.length || 0) + (result.remoteBranches?.length || 0), raw: result.stdout }, metadata: { operation: 'list', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'LIST_BRANCHES_ERROR', `Failed to list branches: ${errorMessage}`, 'list', { error: errorMessage, projectPath: params.projectPath } ); } } /** * Handle get branch operation */ private async handleGetBranch(params: GitBranchesParams, startTime: number): Promise<ToolResult> { try { if (!params.branchName) { return OperationErrorHandler.createToolError( 'MISSING_PARAMETER', 'branchName is required for get branch operation', 'get', {}, ['Provide the name of the branch to get information about'] ); } // Get branch information const branchInfo = await this.gitExecutor.getBranchInfo(params.projectPath, params.branchName); if (!branchInfo.success) { return OperationErrorHandler.handleGitError(branchInfo.stderr, 'get branch info', params.projectPath); } // Get branch commits const commitsResult = await this.gitExecutor.getBranchCommits(params.projectPath, params.branchName, 10); return { success: true, data: { branchName: params.branchName, exists: branchInfo.exists, isRemote: branchInfo.isRemote, lastCommit: branchInfo.lastCommit, commits: commitsResult.success ? commitsResult.commits : [], upstream: branchInfo.upstream, ahead: branchInfo.ahead, behind: branchInfo.behind }, metadata: { operation: 'get', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'GET_BRANCH_ERROR', `Failed to get branch information: ${errorMessage}`, 'get', { error: errorMessage, projectPath: params.projectPath } ); } } /** * Handle delete branch operation */ private async handleDeleteBranch(params: GitBranchesParams, startTime: number): Promise<ToolResult> { try { if (!params.branchName) { return OperationErrorHandler.createToolError( 'MISSING_PARAMETER', 'branchName is required for branch deletion', 'delete', {}, ['Provide the name of the branch to delete'] ); } // Check if trying to delete current branch const currentBranchResult = await this.gitExecutor.getCurrentBranch(params.projectPath); if (currentBranchResult.success && currentBranchResult.branch === params.branchName) { return OperationErrorHandler.createToolError( 'CANNOT_DELETE_CURRENT_BRANCH', `Cannot delete current branch '${params.branchName}'`, 'delete', { branchName: params.branchName }, ['Switch to a different branch before deleting this one'] ); } const result = await this.gitExecutor.deleteBranch( params.projectPath, params.branchName, params.force || false ); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'delete branch', params.projectPath); } return { success: true, data: { message: `Branch '${params.branchName}' deleted successfully`, branchName: params.branchName, forced: params.force || false, output: result.stdout }, metadata: { operation: 'delete', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'DELETE_BRANCH_ERROR', `Failed to delete branch: ${errorMessage}`, 'delete', { error: errorMessage, projectPath: params.projectPath } ); } } /** * Handle merge branch operation */ private async handleMergeBranch(params: GitBranchesParams, startTime: number): Promise<ToolResult> { try { if (!params.branchName) { return OperationErrorHandler.createToolError( 'MISSING_PARAMETER', 'branchName is required for branch merge', 'merge', {}, ['Provide the name of the branch to merge'] ); } // Get current branch (target branch) const currentBranchResult = await this.gitExecutor.getCurrentBranch(params.projectPath); if (!currentBranchResult.success) { return OperationErrorHandler.handleGitError(currentBranchResult.stderr, 'get current branch', params.projectPath); } const targetBranch = params.targetBranch || currentBranchResult.branch; // Check for uncommitted changes const statusResult = await this.gitExecutor.getStatus(params.projectPath); if (statusResult.success && statusResult.parsedStatus && (statusResult.parsedStatus.modified.length > 0 || statusResult.parsedStatus.added.length > 0)) { return OperationErrorHandler.createToolError( 'UNCOMMITTED_CHANGES', 'Cannot merge with uncommitted changes', 'merge', { uncommittedFiles: [...statusResult.parsedStatus.modified, ...statusResult.parsedStatus.added] }, ['Commit or stash your changes before merging'] ); } const result = await this.gitExecutor.mergeBranch( params.projectPath, params.branchName, { force: params.force } ); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'merge branch', params.projectPath); } return { success: true, data: { message: `Branch '${params.branchName}' merged into '${targetBranch}' successfully`, sourceBranch: params.branchName, targetBranch, mergeCommit: result.mergeCommit, conflicts: result.conflicts || [], output: result.stdout }, metadata: { operation: 'merge', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'MERGE_BRANCH_ERROR', `Failed to merge branch: ${errorMessage}`, 'merge', { error: errorMessage, projectPath: params.projectPath } ); } } /** * Handle compare branches operation */ private async handleCompareBranches(params: GitBranchesParams, startTime: number): Promise<ToolResult> { try { if (!params.baseBranch || !params.compareBranch) { return OperationErrorHandler.createToolError( 'MISSING_PARAMETER', 'baseBranch and compareBranch are required for comparison', 'compare', {}, ['Provide both branch names to compare'] ); } const result = await this.gitExecutor.compareBranches( params.projectPath, params.baseBranch, params.compareBranch ); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'compare branches', params.projectPath); } return { success: true, data: { baseBranch: params.baseBranch, compareBranch: params.compareBranch, ahead: result.ahead || 0, behind: result.behind || 0, commits: result.commits || [], files: result.files || [], summary: { totalCommits: result.commits?.length || 0, filesChanged: result.files?.length || 0, insertions: result.insertions || 0, deletions: result.deletions || 0 } }, metadata: { operation: 'compare', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'COMPARE_BRANCHES_ERROR', `Failed to compare branches: ${errorMessage}`, 'compare', { error: errorMessage, projectPath: params.projectPath } ); } } /** * Check if operation is a remote operation */ private isRemoteOperation(action: string): boolean { // For now, all branch operations are local // Remote branch operations would be handled through git-workflow or specific remote tools return false; } /** * Extract parameters for remote operations */ private extractRemoteParameters(params: GitBranchesParams): Record<string, any> { const remoteParams: Record<string, any> = { projectPath: params.projectPath }; if (params.repo) remoteParams.repo = params.repo; if (params.branchName) remoteParams.branchName = params.branchName; return remoteParams; } /** * Map git-branches actions to provider operations */ private mapActionToProviderOperation(action: string): string { const actionMap: Record<string, string> = { 'list': 'branch-list', 'get': 'branch-get', 'create': 'branch-create', 'delete': 'branch-delete' }; return actionMap[action] || action; } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-branches', description: 'Git branch management tool for branch lifecycle operations. Supports create, list, get, delete, merge, and compare operations. Includes safety warnings for destructive operations like branch deletion. In universal mode (GIT_MCP_MODE=universal), automatically executes on both GitHub and Gitea providers.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['create', 'list', 'get', 'delete', 'merge', 'compare'], description: 'The branch operation to perform. WARNING: delete operation permanently removes branches and their commit history.' }, projectPath: { type: 'string', description: 'Absolute path to the project directory' }, provider: { type: 'string', enum: ['github', 'gitea', 'both'], description: 'Provider for remote operations (if supported)' }, branchName: { type: 'string', description: 'Name of the branch (required for create, get, delete, merge)' }, sourceBranch: { type: 'string', description: 'Source branch to create from (for create action)' }, targetBranch: { type: 'string', description: 'Target branch to merge into (for merge action)' }, baseBranch: { type: 'string', description: 'Base branch for comparison (required for compare action)' }, compareBranch: { type: 'string', description: 'Branch to compare against base (required for compare action)' }, force: { type: 'boolean', description: 'Force operation (for delete, merge actions)' }, remote: { type: 'string', description: 'Remote name (default: origin)' }, checkout: { type: 'boolean', description: 'Checkout branch after creation (for create action)' }, repo: { type: 'string', description: 'Repository name (for remote operations)' } }, required: ['action', 'projectPath'], additionalProperties: false }, errorCodes: { 'VALIDATION_ERROR': 'Parameter validation failed - check required parameters and format', 'BRANCH_EXISTS': 'Branch already exists - use different name or delete existing branch', 'BRANCH_NOT_FOUND': 'Branch not found - check branch name and remote configuration', 'MERGE_CONFLICT': 'Merge conflicts detected - resolve conflicts before completing merge', 'DIRTY_WORKING_TREE': 'Working directory has uncommitted changes - commit or stash first', 'NOT_A_GIT_REPOSITORY': 'Directory is not a Git repository - use git init first', 'PERMISSION_DENIED': 'Permission denied - check access rights and credentials' }, warnings: { 'delete': 'Branch deletion is permanent and cannot be undone. Ensure branch is merged or no longer needed.', 'force': 'Force operations can cause data loss. Use with extreme caution.' }, examples: [ { description: 'List all branches', input: { action: 'list', projectPath: '/path/to/project' } }, { description: 'Create new feature branch', input: { action: 'create', projectPath: '/path/to/project', branchName: 'feature/new-feature', sourceBranch: 'main', checkout: true } }, { description: 'Compare two branches', input: { action: 'compare', projectPath: '/path/to/project', baseBranch: 'main', compareBranch: 'feature/new-feature' } } ] }; } }

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