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