Skip to main content
Glama
git-monitor.ts22.5 kB
/** * Git Monitor Tool * * Git monitoring and logging tool providing comprehensive repository monitoring. * Supports log, status, commits, and contributors operations for repository analysis. * * Operations: log, status, commits, contributors */ 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 { configManager } from '../config.js'; export interface GitMonitorParams extends ToolParams { action: 'log' | 'status' | 'commits' | 'contributors'; // Log parameters limit?: number; // Number of commits to show (default: 10) branch?: string; // Branch to analyze (default: current) since?: string; // Date since when to show logs (e.g., '2024-01-01', '1 week ago') until?: string; // Date until when to show logs author?: string; // Filter by author grep?: string; // Filter by commit message // Status parameters detailed?: boolean; // Show detailed status information // Commits parameters format?: 'short' | 'full' | 'oneline' | 'raw'; // Commit format graph?: boolean; // Show commit graph // Contributors parameters sortBy?: 'commits' | 'lines' | 'name'; // Sort contributors by includeStats?: boolean; // Include detailed statistics } export interface CommitInfo { hash: string; shortHash: string; message: string; author: string; authorEmail: string; date: string; insertions?: number; deletions?: number; } export interface ContributorInfo { name: string; email: string; commits: number; insertions: number; deletions: number; firstCommit: string; lastCommit: string; } export class GitMonitorTool { private gitExecutor: GitCommandExecutor; constructor() { this.gitExecutor = new GitCommandExecutor(); } /** * Execute git-monitor operation */ async execute(params: GitMonitorParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate basic parameters const validation = ParameterValidator.validateToolParams('git-monitor', 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 ); } // Route to appropriate handler switch (params.action) { case 'log': return await this.handleLog(params, startTime); case 'status': return await this.handleStatus(params, startTime); case 'commits': return await this.handleCommits(params, startTime); case 'contributors': return await this.handleContributors(params, startTime); default: return OperationErrorHandler.createToolError( 'UNSUPPORTED_OPERATION', `Operation '${params.action}' is not supported`, params.action, {}, ['Use one of: log, status, commits, contributors'] ); } } 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: GitMonitorParams): { isValid: boolean; errors: string[]; suggestions: string[] } { const errors: string[] = []; const suggestions: string[] = []; // Validate limit parameter if (params.limit !== undefined && (params.limit < 1 || params.limit > 1000)) { errors.push('Limit must be between 1 and 1000'); suggestions.push('Use a reasonable limit (e.g., 10, 50, 100)'); } // Validate format parameter if (params.format && !['short', 'full', 'oneline', 'raw'].includes(params.format)) { errors.push('Invalid format specified'); suggestions.push('Use one of: short, full, oneline, raw'); } // Validate sortBy parameter if (params.sortBy && !['commits', 'lines', 'name'].includes(params.sortBy)) { errors.push('Invalid sortBy specified'); suggestions.push('Use one of: commits, lines, name'); } // Validate date formats if (params.since && !this.isValidDateFormat(params.since)) { errors.push('Invalid since date format'); suggestions.push('Use formats like: "2024-01-01", "1 week ago", "2024-01-01 10:00"'); } if (params.until && !this.isValidDateFormat(params.until)) { errors.push('Invalid until date format'); suggestions.push('Use formats like: "2024-01-01", "1 week ago", "2024-01-01 10:00"'); } return { isValid: errors.length === 0, errors, suggestions }; } /** * Validate date format */ private isValidDateFormat(dateStr: string): boolean { // Accept various Git date formats const patterns = [ /^\d{4}-\d{2}-\d{2}$/, // 2024-01-01 /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/, // 2024-01-01 10:00 /^\d+ (second|minute|hour|day|week|month|year)s? ago$/, // 1 week ago /^yesterday$/, /^today$/ ]; return patterns.some(pattern => pattern.test(dateStr)); } /** * Handle log operation */ private async handleLog(params: GitMonitorParams, startTime: number): Promise<ToolResult> { try { const args = ['log']; // Add limit const limit = params.limit || 10; args.push('-n', limit.toString()); // Add format args.push('--format=%H|%h|%s|%an|%ae|%ad'); args.push('--date=iso'); // Add branch if specified if (params.branch) { args.push(params.branch); } // Add date filters if (params.since) { args.push(`--since=${params.since}`); } if (params.until) { args.push(`--until=${params.until}`); } // Add author filter if (params.author) { args.push(`--author=${params.author}`); } // Add grep filter if (params.grep) { args.push(`--grep=${params.grep}`); } const result = await this.gitExecutor.executeGitCommand( 'log', args.slice(1), params.projectPath ); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'get log', params.projectPath); } const commits = this.parseLogOutput(result.stdout); return { success: true, data: { message: `Retrieved ${commits.length} commits from log`, totalCommits: commits.length, commits: commits, filters: { branch: params.branch, since: params.since, until: params.until, author: params.author, grep: params.grep, limit: limit }, raw: result.stdout }, metadata: { operation: 'log', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'LOG_ERROR', `Failed to get repository log: ${errorMessage}`, 'log', { error: errorMessage } ); } } /** * Handle status operation */ private async handleStatus(params: GitMonitorParams, startTime: number): Promise<ToolResult> { try { // Get basic status const statusResult = await this.gitExecutor.getStatus(params.projectPath); if (!statusResult.success) { return OperationErrorHandler.handleGitError(statusResult.stderr, 'get status', params.projectPath); } let additionalInfo = {}; if (params.detailed) { // Get additional detailed information const branchResult = await this.gitExecutor.getCurrentBranch(params.projectPath); const remoteResult = await this.gitExecutor.getRemoteUrl(params.projectPath); // Get last commit info const lastCommitResult = await this.gitExecutor.executeGitCommand( 'log', ['-1', '--format=%H|%h|%s|%an|%ad', '--date=iso'], params.projectPath ); // Get stash count const stashResult = await this.gitExecutor.executeGitCommand( 'stash', ['list'], params.projectPath ); const stashCount = stashResult.success ? stashResult.stdout.split('\n').filter(line => line.trim()).length : 0; additionalInfo = { currentBranch: branchResult.branch, remoteUrl: remoteResult.url, lastCommit: lastCommitResult.success ? this.parseLogOutput(lastCommitResult.stdout)[0] : null, stashCount: stashCount, isGitRepository: statusResult.isGitRepository }; } return { success: true, data: { message: 'Repository status retrieved successfully', status: statusResult.parsedStatus, detailed: params.detailed, ...additionalInfo, raw: statusResult.stdout }, metadata: { operation: 'status', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'STATUS_ERROR', `Failed to get repository status: ${errorMessage}`, 'status', { error: errorMessage } ); } } /** * Handle commits operation */ private async handleCommits(params: GitMonitorParams, startTime: number): Promise<ToolResult> { try { const args = ['log']; // Add limit const limit = params.limit || 50; args.push('-n', limit.toString()); // Add format based on requested format let formatString = '%H|%h|%s|%an|%ae|%ad'; if (params.format === 'full') { formatString = '%H|%h|%s|%an|%ae|%ad|%B'; } else if (params.format === 'oneline') { formatString = '%h %s'; } else if (params.format === 'raw') { formatString = 'raw'; } if (params.format !== 'raw') { args.push(`--format=${formatString}`); args.push('--date=iso'); } // Add graph if requested if (params.graph) { args.push('--graph'); } // Add branch if specified if (params.branch) { args.push(params.branch); } // Add date filters if (params.since) { args.push(`--since=${params.since}`); } if (params.until) { args.push(`--until=${params.until}`); } // Add author filter if (params.author) { args.push(`--author=${params.author}`); } const result = await this.gitExecutor.executeGitCommand( 'log', args.slice(1), params.projectPath ); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'get commits', params.projectPath); } let commits: CommitInfo[] = []; let formattedOutput = result.stdout; if (params.format !== 'raw' && params.format !== 'oneline') { commits = this.parseLogOutput(result.stdout); // Get commit stats if requested if (params.includeStats) { commits = await this.enrichCommitsWithStats(commits, params.projectPath); } } return { success: true, data: { message: `Retrieved ${commits.length || 'commits'} from repository`, totalCommits: commits.length, commits: commits.length > 0 ? commits : undefined, format: params.format || 'short', graph: params.graph, filters: { branch: params.branch, since: params.since, until: params.until, author: params.author, limit: limit }, raw: formattedOutput }, metadata: { operation: 'commits', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'COMMITS_ERROR', `Failed to get repository commits: ${errorMessage}`, 'commits', { error: errorMessage } ); } } /** * Handle contributors operation */ private async handleContributors(params: GitMonitorParams, startTime: number): Promise<ToolResult> { try { // Get contributors with commit counts const shortlogResult = await this.gitExecutor.executeGitCommand( 'shortlog', ['-sn', '--all'], params.projectPath ); if (!shortlogResult.success) { return OperationErrorHandler.handleGitError(shortlogResult.stderr, 'get contributors', params.projectPath); } const contributors: ContributorInfo[] = []; const lines = shortlogResult.stdout.split('\n').filter(line => line.trim()); for (const line of lines) { const match = line.match(/^\s*(\d+)\s+(.+)$/); if (match) { const commits = parseInt(match[1]); const name = match[2]; // Get email for this contributor const emailResult = await this.gitExecutor.executeGitCommand( 'log', ['--format=%ae', `--author=${name}`, '-1'], params.projectPath ); const email = emailResult.success ? emailResult.stdout.trim() : ''; let insertions = 0; let deletions = 0; let firstCommit = ''; let lastCommit = ''; if (params.includeStats) { // Get detailed stats for this contributor const statsResult = await this.gitExecutor.executeGitCommand( 'log', ['--author=' + name, '--numstat', '--format=%ad', '--date=iso'], params.projectPath ); if (statsResult.success) { const statsLines = statsResult.stdout.split('\n'); for (const statsLine of statsLines) { const statMatch = statsLine.match(/^(\d+)\s+(\d+)\s+/); if (statMatch) { insertions += parseInt(statMatch[1]) || 0; deletions += parseInt(statMatch[2]) || 0; } } } // Get first and last commit dates const firstCommitResult = await this.gitExecutor.executeGitCommand( 'log', ['--author=' + name, '--format=%ad', '--date=iso', '--reverse'], params.projectPath ); const lastCommitResult = await this.gitExecutor.executeGitCommand( 'log', ['--author=' + name, '--format=%ad', '--date=iso', '-1'], params.projectPath ); if (firstCommitResult.success) { const firstLine = firstCommitResult.stdout.split('\n')[0]; firstCommit = firstLine ? firstLine.trim() : ''; } if (lastCommitResult.success) { lastCommit = lastCommitResult.stdout.trim(); } } contributors.push({ name, email, commits, insertions, deletions, firstCommit, lastCommit }); } } // Sort contributors const sortBy = params.sortBy || 'commits'; contributors.sort((a, b) => { switch (sortBy) { case 'commits': return b.commits - a.commits; case 'lines': return (b.insertions + b.deletions) - (a.insertions + a.deletions); case 'name': return a.name.localeCompare(b.name); default: return b.commits - a.commits; } }); const totalCommits = contributors.reduce((sum, c) => sum + c.commits, 0); const totalInsertions = contributors.reduce((sum, c) => sum + c.insertions, 0); const totalDeletions = contributors.reduce((sum, c) => sum + c.deletions, 0); return { success: true, data: { message: `Found ${contributors.length} contributors`, totalContributors: contributors.length, contributors: contributors, summary: { totalCommits, totalInsertions, totalDeletions, totalLines: totalInsertions + totalDeletions }, sortBy: sortBy, includeStats: params.includeStats, raw: shortlogResult.stdout }, metadata: { operation: 'contributors', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'CONTRIBUTORS_ERROR', `Failed to get repository contributors: ${errorMessage}`, 'contributors', { error: errorMessage } ); } } /** * Parse log output into structured commit information */ private parseLogOutput(logOutput: string): CommitInfo[] { const commits: CommitInfo[] = []; const lines = logOutput.split('\n').filter(line => line.trim()); for (const line of lines) { const parts = line.split('|'); if (parts.length >= 6) { commits.push({ hash: parts[0], shortHash: parts[1], message: parts[2], author: parts[3], authorEmail: parts[4], date: parts[5] }); } } return commits; } /** * Enrich commits with statistics (insertions/deletions) */ private async enrichCommitsWithStats(commits: CommitInfo[], projectPath: string): Promise<CommitInfo[]> { const enrichedCommits: CommitInfo[] = []; for (const commit of commits) { const statsResult = await this.gitExecutor.executeGitCommand( 'show', ['--numstat', '--format=', commit.hash], projectPath ); let insertions = 0; let deletions = 0; if (statsResult.success) { const lines = statsResult.stdout.split('\n').filter(line => line.trim()); for (const line of lines) { const match = line.match(/^(\d+)\s+(\d+)\s+/); if (match) { insertions += parseInt(match[1]) || 0; deletions += parseInt(match[2]) || 0; } } } enrichedCommits.push({ ...commit, insertions, deletions }); } return enrichedCommits; } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-monitor', description: 'Git monitoring and logging tool for log, status, commits, and contributors operations. Provides comprehensive repository analysis and monitoring capabilities.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['log', 'status', 'commits', 'contributors'], description: 'The Git monitoring operation to perform' }, projectPath: { type: 'string', description: 'Absolute path to the project directory' }, limit: { type: 'number', description: 'Number of commits to show (1-1000, default: 10 for log, 50 for commits)', minimum: 1, maximum: 1000 }, branch: { type: 'string', description: 'Branch to analyze (default: current branch)' }, since: { type: 'string', description: 'Date since when to show logs (e.g., "2024-01-01", "1 week ago")' }, until: { type: 'string', description: 'Date until when to show logs (e.g., "2024-01-01", "1 week ago")' }, author: { type: 'string', description: 'Filter by author name or email' }, grep: { type: 'string', description: 'Filter by commit message pattern' }, detailed: { type: 'boolean', description: 'Show detailed status information (for status operation)' }, format: { type: 'string', enum: ['short', 'full', 'oneline', 'raw'], description: 'Commit format (for commits operation)' }, graph: { type: 'boolean', description: 'Show commit graph (for commits operation)' }, sortBy: { type: 'string', enum: ['commits', 'lines', 'name'], description: 'Sort contributors by (for contributors operation)' }, includeStats: { type: 'boolean', description: 'Include detailed statistics (insertions/deletions)' } }, 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