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