Skip to main content
Glama
git-stash.ts20.2 kB
/** * Git Stash Tool * * Comprehensive Git stash tool providing stash management operations. * Supports stash, pop, apply, list, show, drop, clear operations for temporary changes storage. * * Operations: stash, pop, apply, list, show, drop, clear */ 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 { configManager } from '../config.js'; export interface GitStashParams extends ToolParams { action: 'stash' | 'pop' | 'apply' | 'list' | 'show' | 'drop' | 'clear'; // Stash parameters message?: string; // Stash message (for stash operation) stashRef?: string; // Stash reference (e.g., "stash@{0}", "0") for pop, apply, show, drop // Stash options includeUntracked?: boolean; // Include untracked files (for stash operation) keepIndex?: boolean; // Keep index unchanged (for stash operation) patch?: boolean; // Interactive patch mode (for stash operation) quiet?: boolean; // Suppress output // Apply/Pop options index?: boolean; // Try to reinstate index changes (for apply/pop) // List options oneline?: boolean; // Show stash list in oneline format } export class GitStashTool { private gitExecutor: GitCommandExecutor; constructor() { this.gitExecutor = new GitCommandExecutor(); } /** * Execute git-stash operation */ async execute(params: GitStashParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate basic parameters const validation = ParameterValidator.validateToolParams('git-stash', 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 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 'stash': return await this.handleStash(params, startTime); case 'pop': return await this.handlePop(params, startTime); case 'apply': return await this.handleApply(params, startTime); case 'list': return await this.handleList(params, startTime); case 'show': return await this.handleShow(params, startTime); case 'drop': return await this.handleDrop(params, startTime); case 'clear': return await this.handleClear(params, startTime); default: return OperationErrorHandler.createToolError( 'UNSUPPORTED_OPERATION', `Operation '${params.action}' is not supported`, params.action, {}, ['Use one of: stash, pop, apply, list, show, drop, clear'] ); } } 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: GitStashParams): { isValid: boolean; errors: string[]; suggestions: string[] } { const errors: string[] = []; const suggestions: string[] = []; switch (params.action) { case 'pop': case 'apply': case 'show': case 'drop': // These operations can optionally specify a stash reference if (params.stashRef && !this.isValidStashRef(params.stashRef)) { errors.push('Invalid stash reference format'); suggestions.push('Use format like "stash@{0}", "0", or omit for latest stash'); } break; case 'stash': case 'list': case 'clear': // These operations don't require specific validation break; } return { isValid: errors.length === 0, errors, suggestions }; } /** * Validate stash reference format */ private isValidStashRef(stashRef: string): boolean { // Valid formats: "stash@{0}", "stash@{1}", "0", "1", etc. return /^(stash@\{\d+\}|\d+)$/.test(stashRef); } /** * Handle git stash operation (save current changes) */ private async handleStash(params: GitStashParams, startTime: number): Promise<ToolResult> { try { const args: string[] = []; // Add message if provided if (params.message) { args.push('push', '-m', params.message); } else { args.push('push'); } // Add options if (params.includeUntracked) { args.push('--include-untracked'); } if (params.keepIndex) { args.push('--keep-index'); } if (params.patch) { args.push('--patch'); } if (params.quiet) { args.push('--quiet'); } const result = await this.gitExecutor.executeGitCommand('stash', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'stash', params.projectPath); } // Check if anything was actually stashed const output = result.stdout.trim(); const noChanges = output.includes('No local changes to save'); return { success: true, data: { message: noChanges ? 'No local changes to save' : 'Changes stashed successfully', stashed: !noChanges, stashMessage: params.message || 'WIP on current branch', options: { includeUntracked: params.includeUntracked || false, keepIndex: params.keepIndex || false, patch: params.patch || false }, output }, metadata: { operation: 'stash', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'STASH_ERROR', `Failed to stash changes: ${errorMessage}`, 'stash', { error: errorMessage, message: params.message } ); } } /** * Handle git stash pop operation (apply and remove latest stash) */ private async handlePop(params: GitStashParams, startTime: number): Promise<ToolResult> { try { const args = ['pop']; // Add stash reference if provided if (params.stashRef) { const normalizedRef = this.normalizeStashRef(params.stashRef); args.push(normalizedRef); } // Add index option if specified if (params.index) { args.push('--index'); } // Add quiet option if specified if (params.quiet) { args.push('--quiet'); } const result = await this.gitExecutor.executeGitCommand('stash', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'stash pop', params.projectPath); } const stashRef = params.stashRef || 'stash@{0}'; return { success: true, data: { message: `Stash ${stashRef} popped successfully`, stashRef, applied: true, removed: true, description: 'Stash applied to working directory and removed from stash list', output: result.stdout }, metadata: { operation: 'stash pop', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'STASH_POP_ERROR', `Failed to pop stash: ${errorMessage}`, 'pop', { error: errorMessage, stashRef: params.stashRef } ); } } /** * Handle git stash apply operation (apply stash without removing) */ private async handleApply(params: GitStashParams, startTime: number): Promise<ToolResult> { try { const args = ['apply']; // Add stash reference if provided if (params.stashRef) { const normalizedRef = this.normalizeStashRef(params.stashRef); args.push(normalizedRef); } // Add index option if specified if (params.index) { args.push('--index'); } // Add quiet option if specified if (params.quiet) { args.push('--quiet'); } const result = await this.gitExecutor.executeGitCommand('stash', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'stash apply', params.projectPath); } const stashRef = params.stashRef || 'stash@{0}'; return { success: true, data: { message: `Stash ${stashRef} applied successfully`, stashRef, applied: true, removed: false, description: 'Stash applied to working directory but kept in stash list', output: result.stdout }, metadata: { operation: 'stash apply', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'STASH_APPLY_ERROR', `Failed to apply stash: ${errorMessage}`, 'apply', { error: errorMessage, stashRef: params.stashRef } ); } } /** * Handle git stash list operation (show all stashes) */ private async handleList(params: GitStashParams, startTime: number): Promise<ToolResult> { try { const args = ['list']; // Add oneline format if specified if (params.oneline) { args.push('--oneline'); } const result = await this.gitExecutor.executeGitCommand('stash', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'stash list', params.projectPath); } const output = result.stdout.trim(); const stashes = output ? output.split('\n').map((line, index) => { const match = line.match(/^(stash@\{\d+\}):\s*(.+)$/); if (match) { return { ref: match[1], index, message: match[2], fullLine: line }; } return { ref: `stash@{${index}}`, index, message: line, fullLine: line }; }) : []; return { success: true, data: { message: stashes.length > 0 ? `Found ${stashes.length} stash(es)` : 'No stashes found', count: stashes.length, stashes, isEmpty: stashes.length === 0, output }, metadata: { operation: 'stash list', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'STASH_LIST_ERROR', `Failed to list stashes: ${errorMessage}`, 'list', { error: errorMessage } ); } } /** * Handle git stash show operation (show stash contents) */ private async handleShow(params: GitStashParams, startTime: number): Promise<ToolResult> { try { const args = ['show']; // Add stash reference if provided if (params.stashRef) { const normalizedRef = this.normalizeStashRef(params.stashRef); args.push(normalizedRef); } // Add patch format for detailed diff args.push('--patch'); const result = await this.gitExecutor.executeGitCommand('stash', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'stash show', params.projectPath); } const stashRef = params.stashRef || 'stash@{0}'; // Get stash summary (files changed) const summaryResult = await this.gitExecutor.executeGitCommand('stash', ['show', '--stat', stashRef], params.projectPath); const summary = summaryResult.success ? summaryResult.stdout.trim() : ''; return { success: true, data: { message: `Showing stash ${stashRef}`, stashRef, summary, diff: result.stdout, description: 'Detailed diff of stashed changes', output: result.stdout }, metadata: { operation: 'stash show', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'STASH_SHOW_ERROR', `Failed to show stash: ${errorMessage}`, 'show', { error: errorMessage, stashRef: params.stashRef } ); } } /** * Handle git stash drop operation (remove specific stash) */ private async handleDrop(params: GitStashParams, startTime: number): Promise<ToolResult> { try { const args = ['drop']; // Add stash reference if provided if (params.stashRef) { const normalizedRef = this.normalizeStashRef(params.stashRef); args.push(normalizedRef); } // Add quiet option if specified if (params.quiet) { args.push('--quiet'); } const result = await this.gitExecutor.executeGitCommand('stash', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'stash drop', params.projectPath); } const stashRef = params.stashRef || 'stash@{0}'; return { success: true, data: { message: `Stash ${stashRef} dropped successfully`, stashRef, removed: true, description: 'Stash permanently removed from stash list', output: result.stdout }, metadata: { operation: 'stash drop', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'STASH_DROP_ERROR', `Failed to drop stash: ${errorMessage}`, 'drop', { error: errorMessage, stashRef: params.stashRef } ); } } /** * Handle git stash clear operation (remove all stashes) */ private async handleClear(params: GitStashParams, startTime: number): Promise<ToolResult> { try { // Get current stash count before clearing const listResult = await this.gitExecutor.executeGitCommand('stash', ['list'], params.projectPath); const stashCount = listResult.success ? listResult.stdout.trim().split('\n').filter(line => line.trim()).length : 0; if (stashCount === 0) { return { success: true, data: { message: 'No stashes to clear', cleared: 0, description: 'Stash list was already empty' }, metadata: { operation: 'stash clear', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } const args = ['clear']; const result = await this.gitExecutor.executeGitCommand('stash', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'stash clear', params.projectPath); } return { success: true, data: { message: `All ${stashCount} stash(es) cleared successfully`, cleared: stashCount, description: 'All stashes permanently removed from stash list', warning: 'This operation cannot be undone', output: result.stdout }, metadata: { operation: 'stash clear', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'STASH_CLEAR_ERROR', `Failed to clear stashes: ${errorMessage}`, 'clear', { error: errorMessage } ); } } /** * Normalize stash reference to proper format */ private normalizeStashRef(stashRef: string): string { // If it's just a number, convert to stash@{n} format if (/^\d+$/.test(stashRef)) { return `stash@{${stashRef}}`; } return stashRef; } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-stash', description: 'Git stash tool for temporary changes management. Supports stash, pop, apply, list, show, drop, clear operations for storing and retrieving work-in-progress changes.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['stash', 'pop', 'apply', 'list', 'show', 'drop', 'clear'], description: 'The stash operation to perform' }, projectPath: { type: 'string', description: 'Absolute path to the project directory' }, message: { type: 'string', description: 'Stash message (for stash operation)' }, stashRef: { type: 'string', description: 'Stash reference (e.g., "stash@{0}", "0") for pop, apply, show, drop operations' }, includeUntracked: { type: 'boolean', description: 'Include untracked files when stashing' }, keepIndex: { type: 'boolean', description: 'Keep index unchanged when stashing' }, patch: { type: 'boolean', description: 'Interactive patch mode for selective stashing' }, quiet: { type: 'boolean', description: 'Suppress output during stash operations' }, index: { type: 'boolean', description: 'Try to reinstate index changes when applying/popping' }, oneline: { type: 'boolean', description: 'Show stash list in oneline format' } }, required: ['action', 'projectPath'] } }; } }

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