Skip to main content
Glama
git-reset.ts21.2 kB
/** * Git Reset Tool * * Comprehensive Git reset tool providing reset operations for repository state management. * Supports soft, mixed, hard reset capabilities and reset-to-commit and reset-branch functionality. * * Operations: soft, mixed, hard, reset-to-commit, reset-branch */ import { GitCommandExecutor } from '../utils/git-command-executor.js'; import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js'; import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js'; import { SafetyWarnings } from '../utils/safety-warnings.js'; import { ProviderConfig } from '../providers/types.js'; import { configManager } from '../config.js'; export interface GitResetParams extends ToolParams { action: 'soft' | 'mixed' | 'hard' | 'reset-to-commit' | 'reset-branch'; // Reset parameters commit?: string; // Commit hash or reference (required for reset-to-commit) branch?: string; // Branch name (required for reset-branch) // Options files?: string[]; // Specific files to reset (for mixed/soft resets) force?: boolean; // Force reset (for hard reset) quiet?: boolean; // Suppress output // Safety options skipWarning?: boolean; // Skip safety warnings (use with extreme caution) confirmDestructive?: boolean; // Explicit confirmation for destructive operations } export class GitResetTool { private gitExecutor: GitCommandExecutor; private providerConfig?: ProviderConfig; constructor(providerConfig?: ProviderConfig) { this.gitExecutor = new GitCommandExecutor(); this.providerConfig = providerConfig; } /** * Execute git-reset operation */ async execute(params: GitResetParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate basic parameters const validation = ParameterValidator.validateToolParams('git-reset', 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 ); } // Check for destructive operations and show safety warnings if (!params.skipWarning) { const warningResult = this.checkSafetyWarnings(params); if (warningResult) { return warningResult; } } // Check if it's a Git repository const isRepo = await this.gitExecutor.isGitRepository(params.projectPath); if (!isRepo) { return OperationErrorHandler.createToolError( 'NOT_A_GIT_REPOSITORY', 'The specified path is not a Git repository', params.action, { projectPath: params.projectPath }, ['Initialize a Git repository first with: git init'] ); } // Route to appropriate handler switch (params.action) { case 'soft': return await this.handleSoftReset(params, startTime); case 'mixed': return await this.handleMixedReset(params, startTime); case 'hard': return await this.handleHardReset(params, startTime); case 'reset-to-commit': return await this.handleResetToCommit(params, startTime); case 'reset-branch': return await this.handleResetBranch(params, startTime); default: return OperationErrorHandler.createToolError( 'UNSUPPORTED_OPERATION', `Operation '${params.action}' is not supported`, params.action, {}, ['Use one of: soft, mixed, hard, reset-to-commit, reset-branch'] ); } } 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'] ); } } /** * Validate operation-specific parameters */ private validateOperationParams(params: GitResetParams): { isValid: boolean; errors: string[]; suggestions: string[] } { const errors: string[] = []; const suggestions: string[] = []; switch (params.action) { case 'reset-to-commit': if (!params.commit) { errors.push('Commit hash or reference is required for reset-to-commit operation'); suggestions.push('Provide a commit hash (e.g., "abc123") or reference (e.g., "HEAD~1")'); } break; case 'reset-branch': if (!params.branch) { errors.push('Branch name is required for reset-branch operation'); suggestions.push('Provide the branch name to reset to (e.g., "main", "develop")'); } break; case 'soft': case 'mixed': case 'hard': // These operations can work with or without specific commit/files break; } return { isValid: errors.length === 0, errors, suggestions }; } /** * Handle git reset --soft operation */ private async handleSoftReset(params: GitResetParams, startTime: number): Promise<ToolResult> { try { const args = ['--soft']; // Add commit reference if provided, otherwise defaults to HEAD const target = params.commit || 'HEAD'; args.push(target); // Add quiet option if specified if (params.quiet) { args.unshift('--quiet'); } const result = await this.gitExecutor.executeGitCommand('reset', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'reset --soft', params.projectPath); } return { success: true, data: { message: `Soft reset to ${target} completed successfully`, resetType: 'soft', target, description: 'HEAD moved, index and working directory unchanged', output: result.stdout }, metadata: { operation: 'reset soft', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'RESET_SOFT_ERROR', `Failed to perform soft reset: ${errorMessage}`, 'soft', { error: errorMessage, target: params.commit } ); } } /** * Handle git reset --mixed operation (default reset) */ private async handleMixedReset(params: GitResetParams, startTime: number): Promise<ToolResult> { try { const args = ['--mixed']; // Add commit reference if provided, otherwise defaults to HEAD const target = params.commit || 'HEAD'; args.push(target); // Add specific files if provided if (params.files && params.files.length > 0) { args.push('--', ...params.files); } // Add quiet option if specified if (params.quiet) { args.unshift('--quiet'); } const result = await this.gitExecutor.executeGitCommand('reset', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'reset --mixed', params.projectPath); } const filesMessage = params.files && params.files.length > 0 ? ` for files: ${params.files.join(', ')}` : ''; return { success: true, data: { message: `Mixed reset to ${target} completed successfully${filesMessage}`, resetType: 'mixed', target, files: params.files || [], description: 'HEAD and index updated, working directory unchanged', output: result.stdout }, metadata: { operation: 'reset mixed', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'RESET_MIXED_ERROR', `Failed to perform mixed reset: ${errorMessage}`, 'mixed', { error: errorMessage, target: params.commit, files: params.files } ); } } /** * Handle git reset --hard operation */ private async handleHardReset(params: GitResetParams, startTime: number): Promise<ToolResult> { try { // Warn about destructive nature of hard reset if (!params.force) { return OperationErrorHandler.createToolError( 'HARD_RESET_REQUIRES_FORCE', 'Hard reset is destructive and requires explicit force confirmation', 'hard', { target: params.commit }, ['Add force: true to confirm you want to discard all local changes'] ); } const args = ['--hard']; // Add commit reference if provided, otherwise defaults to HEAD const target = params.commit || 'HEAD'; args.push(target); // Add quiet option if specified if (params.quiet) { args.unshift('--quiet'); } const result = await this.gitExecutor.executeGitCommand('reset', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'reset --hard', params.projectPath); } return { success: true, data: { message: `Hard reset to ${target} completed successfully`, resetType: 'hard', target, description: 'HEAD, index, and working directory all updated - local changes discarded', warning: 'All uncommitted changes have been permanently lost', output: result.stdout }, metadata: { operation: 'reset hard', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'RESET_HARD_ERROR', `Failed to perform hard reset: ${errorMessage}`, 'hard', { error: errorMessage, target: params.commit } ); } } /** * Handle reset to specific commit operation */ private async handleResetToCommit(params: GitResetParams, startTime: number): Promise<ToolResult> { try { // Validate commit exists const commitValidation = await this.validateCommitExists(params.commit!, params.projectPath); if (!commitValidation.isValid) { return OperationErrorHandler.createToolError( 'COMMIT_NOT_FOUND', `Commit '${params.commit}' not found`, 'reset-to-commit', { commit: params.commit }, ['Check the commit hash or use a valid reference like HEAD~1'] ); } // Default to mixed reset unless force is specified (then hard) const resetType = params.force ? '--hard' : '--mixed'; const args = [resetType, params.commit!]; // Add quiet option if specified if (params.quiet) { args.unshift('--quiet'); } const result = await this.gitExecutor.executeGitCommand('reset', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, `reset ${resetType}`, params.projectPath); } const resetTypeDescription = params.force ? 'hard (HEAD, index, and working directory updated)' : 'mixed (HEAD and index updated, working directory unchanged)'; return { success: true, data: { message: `Reset to commit ${params.commit} completed successfully`, resetType: params.force ? 'hard' : 'mixed', commit: params.commit, commitInfo: commitValidation.commitInfo, description: `Repository reset to specific commit using ${resetTypeDescription}`, output: result.stdout }, metadata: { operation: 'reset to commit', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'RESET_TO_COMMIT_ERROR', `Failed to reset to commit: ${errorMessage}`, 'reset-to-commit', { error: errorMessage, commit: params.commit } ); } } /** * Handle reset to branch operation */ private async handleResetBranch(params: GitResetParams, startTime: number): Promise<ToolResult> { try { // Validate branch exists const branchValidation = await this.validateBranchExists(params.branch!, params.projectPath); if (!branchValidation.isValid) { return OperationErrorHandler.createToolError( 'BRANCH_NOT_FOUND', `Branch '${params.branch}' not found`, 'reset-branch', { branch: params.branch, availableBranches: branchValidation.availableBranches }, [`Available branches: ${branchValidation.availableBranches?.join(', ')}`] ); } // Default to mixed reset unless force is specified (then hard) const resetType = params.force ? '--hard' : '--mixed'; const args = [resetType, params.branch!]; // Add quiet option if specified if (params.quiet) { args.unshift('--quiet'); } const result = await this.gitExecutor.executeGitCommand('reset', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, `reset ${resetType}`, params.projectPath); } const resetTypeDescription = params.force ? 'hard (HEAD, index, and working directory updated)' : 'mixed (HEAD and index updated, working directory unchanged)'; return { success: true, data: { message: `Reset to branch ${params.branch} completed successfully`, resetType: params.force ? 'hard' : 'mixed', branch: params.branch, description: `Repository reset to branch using ${resetTypeDescription}`, output: result.stdout }, metadata: { operation: 'reset to branch', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'RESET_TO_BRANCH_ERROR', `Failed to reset to branch: ${errorMessage}`, 'reset-branch', { error: errorMessage, branch: params.branch } ); } } /** * Validate that a commit exists */ private async validateCommitExists(commit: string, projectPath: string): Promise<{ isValid: boolean; commitInfo?: any }> { try { const result = await this.gitExecutor.executeGitCommand('rev-parse', ['--verify', `${commit}^{commit}`], projectPath); if (!result.success) { return { isValid: false }; } // Get commit information const infoResult = await this.gitExecutor.executeGitCommand('show', ['--no-patch', '--format=%H %s %an %ad', commit], projectPath); let commitInfo = {}; if (infoResult.success && infoResult.stdout.trim()) { const parts = infoResult.stdout.trim().split(' '); if (parts.length >= 4) { commitInfo = { hash: parts[0], subject: parts.slice(1, -2).join(' '), author: parts[parts.length - 2], date: parts[parts.length - 1] }; } } return { isValid: true, commitInfo }; } catch (error) { return { isValid: false }; } } /** * Validate that a branch exists */ private async validateBranchExists(branch: string, projectPath: string): Promise<{ isValid: boolean; availableBranches?: string[] }> { try { // Get all branches const branchResult = await this.gitExecutor.executeGitCommand('branch', ['-a'], projectPath); if (!branchResult.success) { return { isValid: false }; } const branches = branchResult.stdout .split('\n') .map(line => line.replace(/^\*?\s+/, '').replace(/^remotes\/[^\/]+\//, '')) .filter(line => line.trim() && !line.includes('->')) .map(line => line.trim()); const branchExists = branches.some(b => b === branch || b.endsWith(`/${branch}`)); return { isValid: branchExists, availableBranches: branches.slice(0, 10) // Limit to first 10 for readability }; } catch (error) { return { isValid: false }; } } /** * Check safety warnings for destructive operations */ private checkSafetyWarnings(params: GitResetParams): ToolResult | null { let operationType: string | null = null; // Determine operation type for safety warnings if (params.action === 'hard') { operationType = 'git-reset-hard'; } else if (params.action === 'mixed') { operationType = 'git-reset-mixed'; } // Only show warnings for destructive operations if (!operationType || !SafetyWarnings.isDestructiveOperation(operationType)) { return null; } // Check if user has explicitly confirmed destructive operation if (params.confirmDestructive) { return null; } // Generate and format warning const warning = SafetyWarnings.generateWarning(operationType, { commit: params.commit, branch: params.branch }); const formattedWarning = SafetyWarnings.formatWarning(warning); return { success: false, error: { code: 'SAFETY_WARNING', message: formattedWarning, details: { operationType, riskLevel: warning.level, requiresConfirmation: warning.confirmationRequired }, suggestions: [ 'Set confirmDestructive=true to proceed with this dangerous operation', 'Set skipWarning=true to bypass this warning (not recommended)', 'Consider using a safer alternative operation' ] }, metadata: { operation: params.action, timestamp: new Date().toISOString(), executionTime: 0 } }; } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-reset', description: 'Git reset tool for repository state management. Supports soft, mixed, hard reset capabilities and reset-to-commit and reset-branch functionality. Includes safety warnings for destructive operations like hard reset.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['soft', 'mixed', 'hard', 'reset-to-commit', 'reset-branch'], description: 'The reset operation to perform' }, projectPath: { type: 'string', description: 'Absolute path to the project directory' }, commit: { type: 'string', description: 'Commit hash or reference (required for reset-to-commit, optional for others)' }, branch: { type: 'string', description: 'Branch name (required for reset-branch operation)' }, files: { type: 'array', items: { type: 'string' }, description: 'Specific files to reset (for mixed/soft resets)' }, force: { type: 'boolean', description: 'Force reset - required for hard reset operations' }, quiet: { type: 'boolean', description: 'Suppress output during reset operation' }, skipWarning: { type: 'boolean', description: 'Skip safety warnings (use with extreme caution - not recommended)' }, confirmDestructive: { type: 'boolean', description: 'Explicit confirmation for destructive operations (required for hard reset)' } }, required: ['action', 'projectPath'] } }; } }

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