/**
* 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']
}
};
}
}