/**
* Git Analytics Tool
*
* Analytics and statistics tool providing comprehensive Git analytics operations.
* Supports both local Git analytics and remote provider analytics.
*
* Operations: stats, commits, contributors
*/
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 { ProviderOperationHandler } from '../providers/provider-operation-handler.js';
import { ProviderConfig, ProviderOperation } from '../providers/types.js';
import { configManager } from '../config.js';
export interface GitAnalyticsParams extends ToolParams {
action: 'stats' | 'commits' | 'contributors';
// Time range parameters
since?: string; // For commits, contributors (date/time filter)
until?: string; // For commits, contributors (date/time filter)
// Branch/ref parameters
branch?: string; // For commits, contributors (specific branch)
ref?: string; // For commits, contributors (specific ref)
// Commit analysis parameters
author?: string; // For commits (filter by author)
committer?: string; // For commits (filter by committer)
grep?: string; // For commits (search in commit messages)
// Contributor analysis parameters
minCommits?: number; // For contributors (minimum commits threshold)
sortBy?: 'commits' | 'additions' | 'deletions' | 'name'; // For contributors
// Output parameters
format?: 'json' | 'csv' | 'summary'; // For all operations
limit?: number; // For commits, contributors (max results)
includeStats?: boolean; // For commits (include file change stats)
includeMerges?: boolean; // For commits (include merge commits)
// Remote operation parameters
repo?: string; // For remote operations
// Advanced analytics parameters
groupBy?: 'day' | 'week' | 'month' | 'year'; // For stats (time grouping)
includeFileTypes?: boolean; // For stats (analyze by file types)
includePaths?: string[]; // For stats (analyze specific paths)
excludePaths?: string[]; // For stats (exclude specific paths)
}
export class GitAnalyticsTool {
private gitExecutor: GitCommandExecutor;
private providerHandler?: ProviderOperationHandler;
constructor(providerConfig?: ProviderConfig) {
this.gitExecutor = new GitCommandExecutor();
if (providerConfig) {
this.providerHandler = new ProviderOperationHandler(providerConfig);
}
}
/**
* Execute git-analytics operation
*/
async execute(params: GitAnalyticsParams): Promise<ToolResult> {
const startTime = Date.now();
try {
// Validate basic parameters
const validation = ParameterValidator.validateToolParams('git-analytics', 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 = ParameterValidator.validateOperationParams('git-analytics', params.action, 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
const isRemoteOperation = this.isRemoteOperation(params.action);
if (isRemoteOperation) {
if (!params.provider) {
if (configManager.isUniversalMode()) {
params.provider = 'both';
console.error(`[Universal Mode] Auto-applying both providers for ${params.action}`);
} else {
return OperationErrorHandler.createToolError(
'PROVIDER_REQUIRED',
'Provider parameter is required for remote analytics operations',
params.action,
{},
['Specify provider as: github, gitea, or both']
);
}
}
return await this.executeRemoteOperation(params, startTime);
} else {
return await this.executeLocalOperation(params, startTime);
}
} 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']
);
}
}
/**
* Execute local Git analytics operations
*/
private async executeLocalOperation(params: GitAnalyticsParams, startTime: number): Promise<ToolResult> {
switch (params.action) {
case 'stats':
return await this.handleRepositoryStats(params, startTime);
case 'commits':
return await this.handleCommitAnalytics(params, startTime);
case 'contributors':
return await this.handleContributorAnalytics(params, startTime);
default:
return OperationErrorHandler.createToolError(
'UNSUPPORTED_OPERATION',
`Operation '${params.action}' is not supported`,
params.action,
{},
['Use supported operations: stats, commits, contributors']
);
}
}
/**
* Execute remote provider operations
*/
private async executeRemoteOperation(params: GitAnalyticsParams, startTime: number): Promise<ToolResult> {
if (!this.providerHandler) {
return OperationErrorHandler.createToolError(
'PROVIDER_NOT_CONFIGURED',
'Provider handler is not configured for remote operations',
params.action,
{},
['Configure GitHub or Gitea provider to use remote operations']
);
}
const operation: ProviderOperation = {
provider: params.provider!,
operation: this.mapActionToProviderOperation(params.action),
parameters: this.extractRemoteParameters(params),
requiresAuth: true,
isRemoteOperation: true
};
try {
const result = await this.providerHandler.executeOperation(operation);
return {
success: result.success,
data: result.partialFailure ? result : result.results[0]?.data,
error: result.success ? undefined : {
code: result.errors[0]?.error?.code || 'REMOTE_OPERATION_ERROR',
message: result.errors[0]?.error?.message || 'Remote operation failed',
details: result.errors,
suggestions: ['Check provider configuration and credentials']
},
metadata: {
provider: params.provider,
operation: params.action,
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'REMOTE_OPERATION_ERROR',
`Remote operation failed: ${errorMessage}`,
params.action,
{ error: errorMessage },
['Check provider configuration and network connectivity']
);
}
}
/**
* Handle repository statistics operation
*/
private async handleRepositoryStats(params: GitAnalyticsParams, startTime: number): Promise<ToolResult> {
try {
const stats: any = {
repository: {
path: params.projectPath,
name: params.projectPath.split('/').pop() || 'unknown'
},
overview: {},
branches: {},
commits: {},
contributors: {},
files: {}
};
// Get basic repository information
const repoInfo = await this.getRepositoryOverview(params);
stats.overview = repoInfo;
// Get branch statistics
const branchStats = await this.getBranchStatistics(params);
stats.branches = branchStats;
// Get commit statistics
const commitStats = await this.getCommitStatistics(params);
stats.commits = commitStats;
// Get contributor statistics
const contributorStats = await this.getContributorStatistics(params);
stats.contributors = contributorStats;
// Get file statistics if requested
if (params.includeFileTypes) {
const fileStats = await this.getFileStatistics(params);
stats.files = fileStats;
}
return {
success: true,
data: stats,
metadata: {
operation: 'stats',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'STATS_ERROR',
`Failed to generate repository statistics: ${errorMessage}`,
'stats',
{ error: errorMessage, projectPath: params.projectPath }
);
}
}
/**
* Check if repository has any commits
*/
private async hasCommits(projectPath: string, branch?: string): Promise<boolean> {
try {
const args = ['rev-list', '--count', 'HEAD'];
if (branch) {
args[2] = branch;
}
const result = await this.gitExecutor.executeGitCommand('rev-list', args, projectPath);
if (result.success) {
const count = parseInt(result.stdout.trim());
return count > 0;
}
return false;
} catch (error) {
return false;
}
}
/**
* Handle commit analytics operation
*/
private async handleCommitAnalytics(params: GitAnalyticsParams, startTime: number): Promise<ToolResult> {
try {
// Check if repository has commits before proceeding
const targetRef = params.branch || params.ref;
const hasCommitsInRepo = await this.hasCommits(params.projectPath, targetRef);
if (!hasCommitsInRepo) {
return {
success: true,
data: {
commits: [],
analytics: {
totalCommits: 0,
uniqueAuthors: 0,
dateRange: null,
commitFrequency: {
daily: 0,
weekly: 0,
monthly: 0
}
},
total: 0,
filters: {
since: params.since,
until: params.until,
author: params.author,
committer: params.committer,
grep: params.grep,
branch: params.branch || params.ref,
includeMerges: params.includeMerges
}
},
metadata: {
operation: 'commits',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
}
const args = ['log', '--oneline'];
// Add time range filters
if (params.since) args.push(`--since=${params.since}`);
if (params.until) args.push(`--until=${params.until}`);
// Add author/committer filters
if (params.author) args.push(`--author=${params.author}`);
if (params.committer) args.push(`--committer=${params.committer}`);
// Add message search
if (params.grep) args.push(`--grep=${params.grep}`);
// Add merge commit handling
if (!params.includeMerges) args.push('--no-merges');
// Add branch/ref
if (params.branch) args.push(params.branch);
else if (params.ref) args.push(params.ref);
// Add limit
if (params.limit) args.push(`-n`, params.limit.toString());
// Add stats if requested
if (params.includeStats) {
args.splice(1, 1, '--stat'); // Replace --oneline with --stat
}
const result = await this.gitExecutor.executeGitCommand('log', args, params.projectPath);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'analyze commits', params.projectPath);
}
// Parse commit data
const commits = this.parseCommitLog(result.stdout, params.includeStats);
// Generate analytics
const analytics = this.analyzeCommits(commits, params);
return {
success: true,
data: {
commits: params.format === 'summary' ? undefined : commits,
analytics,
total: commits.length,
filters: {
since: params.since,
until: params.until,
author: params.author,
committer: params.committer,
grep: params.grep,
branch: params.branch || params.ref,
includeMerges: params.includeMerges
}
},
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_ANALYTICS_ERROR',
`Failed to analyze commits: ${errorMessage}`,
'commits',
{ error: errorMessage, projectPath: params.projectPath }
);
}
}
/**
* Handle contributor analytics operation
*/
private async handleContributorAnalytics(params: GitAnalyticsParams, startTime: number): Promise<ToolResult> {
try {
// Check if repository has commits before proceeding
const targetRef = params.branch || params.ref;
const hasCommitsInRepo = await this.hasCommits(params.projectPath, targetRef);
if (!hasCommitsInRepo) {
return {
success: true,
data: {
contributors: [],
analytics: {
totalContributors: 0,
totalCommits: 0,
topContributor: null,
averageCommitsPerContributor: 0
},
total: 0,
filtered: 0,
filters: {
since: params.since,
until: params.until,
branch: params.branch || params.ref,
minCommits: params.minCommits,
sortBy: params.sortBy
}
},
metadata: {
operation: 'contributors',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
}
const args = ['shortlog', '-sn'];
// Add time range filters
if (params.since) args.push(`--since=${params.since}`);
if (params.until) args.push(`--until=${params.until}`);
// Add branch/ref
if (params.branch) args.push(params.branch);
else if (params.ref) args.push(params.ref);
const result = await this.gitExecutor.executeGitCommand('shortlog', args, params.projectPath);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'analyze contributors', params.projectPath);
}
// Parse contributor data
const contributors = this.parseContributorLog(result.stdout);
// Filter by minimum commits if specified
let filteredContributors = contributors;
if (params.minCommits) {
filteredContributors = contributors.filter(c => c.commits >= params.minCommits!);
}
// Get detailed contributor statistics
const detailedContributors = await this.getDetailedContributorStats(
params,
filteredContributors.slice(0, params.limit || 50)
);
// Sort contributors
if (params.sortBy) {
detailedContributors.sort((a, b) => {
switch (params.sortBy) {
case 'commits':
return b.commits - a.commits;
case 'additions':
return (b.additions || 0) - (a.additions || 0);
case 'deletions':
return (b.deletions || 0) - (a.deletions || 0);
case 'name':
return a.name.localeCompare(b.name);
default:
return b.commits - a.commits;
}
});
}
// Generate analytics
const analytics = this.analyzeContributors(detailedContributors);
return {
success: true,
data: {
contributors: params.format === 'summary' ? undefined : detailedContributors,
analytics,
total: contributors.length,
filtered: detailedContributors.length,
filters: {
since: params.since,
until: params.until,
branch: params.branch || params.ref,
minCommits: params.minCommits,
sortBy: params.sortBy
}
},
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_ANALYTICS_ERROR',
`Failed to analyze contributors: ${errorMessage}`,
'contributors',
{ error: errorMessage, projectPath: params.projectPath }
);
}
}
/**
* Get repository overview statistics
*/
private async getRepositoryOverview(params: GitAnalyticsParams): Promise<any> {
const overview: any = {};
try {
// Get total commits
const commitCountResult = await this.gitExecutor.executeGitCommand(
'rev-list', ['--count', 'HEAD'], params.projectPath
);
if (commitCountResult.success) {
overview.totalCommits = parseInt(commitCountResult.stdout.trim()) || 0;
}
// Get repository age (first commit date)
const firstCommitResult = await this.gitExecutor.executeGitCommand(
'log', ['--reverse', '--format=%ai', '-1'], params.projectPath
);
if (firstCommitResult.success && firstCommitResult.stdout.trim()) {
overview.firstCommit = firstCommitResult.stdout.trim();
overview.age = this.calculateAge(new Date(firstCommitResult.stdout.trim()));
}
// Get last commit date
const lastCommitResult = await this.gitExecutor.executeGitCommand(
'log', ['--format=%ai', '-1'], params.projectPath
);
if (lastCommitResult.success && lastCommitResult.stdout.trim()) {
overview.lastCommit = lastCommitResult.stdout.trim();
}
// Get current branch
const branchResult = await this.gitExecutor.executeGitCommand(
'branch', ['--show-current'], params.projectPath
);
if (branchResult.success) {
overview.currentBranch = branchResult.stdout.trim();
}
// Get remote URL
const remoteResult = await this.gitExecutor.executeGitCommand(
'remote', ['get-url', 'origin'], params.projectPath
);
if (remoteResult.success) {
overview.remoteUrl = remoteResult.stdout.trim();
}
} catch (error) {
// Continue with partial data
}
return overview;
}
/**
* Get branch statistics
*/
private async getBranchStatistics(params: GitAnalyticsParams): Promise<any> {
const branchStats: any = {};
try {
// Get all branches
const branchResult = await this.gitExecutor.executeGitCommand(
'branch', ['-a'], params.projectPath
);
if (branchResult.success) {
const branches = branchResult.stdout
.split('\n')
.map((line: string) => line.trim().replace(/^\*\s*/, ''))
.filter((line: string) => line && !line.startsWith('remotes/origin/HEAD'));
branchStats.total = branches.length;
branchStats.local = branches.filter((b: string) => !b.startsWith('remotes/')).length;
branchStats.remote = branches.filter((b: string) => b.startsWith('remotes/')).length;
}
} catch (error) {
// Continue with partial data
}
return branchStats;
}
/**
* Get commit statistics
*/
private async getCommitStatistics(params: GitAnalyticsParams): Promise<any> {
const commitStats: any = {};
try {
// Get commits by time period if groupBy is specified
if (params.groupBy) {
const format = this.getDateFormat(params.groupBy);
const result = await this.gitExecutor.executeGitCommand(
'log', [`--format=${format}`], params.projectPath
);
if (result.success) {
const dates = result.stdout.split('\n').filter((line: string) => line.trim());
const grouped = this.groupByPeriod(dates, params.groupBy);
commitStats.byPeriod = grouped;
}
}
} catch (error) {
// Continue with partial data
}
return commitStats;
}
/**
* Get contributor statistics
*/
private async getContributorStatistics(params: GitAnalyticsParams): Promise<any> {
const contributorStats: any = {};
try {
// Get unique contributors count
const contributorResult = await this.gitExecutor.executeGitCommand(
'shortlog', ['-sn', 'HEAD'], params.projectPath
);
if (contributorResult.success) {
const contributors = this.parseContributorLog(contributorResult.stdout);
contributorStats.total = contributors.length;
contributorStats.topContributor = contributors[0];
}
} catch (error) {
// Continue with partial data
}
return contributorStats;
}
/**
* Get file statistics
*/
private async getFileStatistics(params: GitAnalyticsParams): Promise<any> {
const fileStats: any = {};
try {
// Get file count by type
const lsFilesResult = await this.gitExecutor.executeGitCommand(
'ls-files', [], params.projectPath
);
if (lsFilesResult.success) {
const files = lsFilesResult.stdout.split('\n').filter((line: string) => line.trim());
const byExtension: Record<string, number> = {};
files.forEach((file: string) => {
const ext = file.split('.').pop()?.toLowerCase() || 'no-extension';
byExtension[ext] = (byExtension[ext] || 0) + 1;
});
fileStats.total = files.length;
fileStats.byExtension = byExtension;
}
} catch (error) {
// Continue with partial data
}
return fileStats;
}
/**
* Parse commit log output
*/
private parseCommitLog(output: string, includeStats?: boolean): any[] {
const commits = [];
const lines = output.split('\n').filter(line => line.trim());
for (const line of lines) {
if (includeStats) {
// Parse --stat format
const match = line.match(/^([a-f0-9]+)\s+(.+)$/);
if (match) {
commits.push({
hash: match[1],
message: match[2]
});
}
} else {
// Parse --oneline format
const match = line.match(/^([a-f0-9]+)\s+(.+)$/);
if (match) {
commits.push({
hash: match[1],
message: match[2]
});
}
}
}
return commits;
}
/**
* Parse contributor log output
*/
private parseContributorLog(output: string): any[] {
const contributors = [];
const lines = output.split('\n').filter(line => line.trim());
for (const line of lines) {
const match = line.match(/^\s*(\d+)\s+(.+)$/);
if (match) {
contributors.push({
commits: parseInt(match[1]),
name: match[2].trim()
});
}
}
return contributors;
}
/**
* Get detailed contributor statistics
*/
private async getDetailedContributorStats(params: GitAnalyticsParams, contributors: any[]): Promise<any[]> {
const detailed = [];
for (const contributor of contributors) {
try {
// Get detailed stats for this contributor
const args = ['log', '--author', contributor.name, '--numstat', '--format='];
if (params.since) args.push(`--since=${params.since}`);
if (params.until) args.push(`--until=${params.until}`);
if (params.branch) args.push(params.branch);
const result = await this.gitExecutor.executeGitCommand('log', args, params.projectPath);
if (result.success) {
const stats = this.parseNumstat(result.stdout);
detailed.push({
...contributor,
additions: stats.additions,
deletions: stats.deletions,
filesChanged: stats.files
});
} else {
detailed.push(contributor);
}
} catch (error) {
detailed.push(contributor);
}
}
return detailed;
}
/**
* Parse numstat output
*/
private parseNumstat(output: string): { additions: number; deletions: number; files: number } {
const lines = output.split('\n').filter(line => line.trim());
let additions = 0;
let deletions = 0;
let files = 0;
for (const line of lines) {
const match = line.match(/^(\d+|-)\s+(\d+|-)\s+(.+)$/);
if (match) {
const add = match[1] === '-' ? 0 : parseInt(match[1]);
const del = match[2] === '-' ? 0 : parseInt(match[2]);
additions += add;
deletions += del;
files++;
}
}
return { additions, deletions, files };
}
/**
* Analyze commits data
*/
private analyzeCommits(commits: any[], params: GitAnalyticsParams): any {
return {
total: commits.length,
averageMessageLength: commits.reduce((sum, c) => sum + c.message.length, 0) / commits.length || 0,
// Add more analytics as needed
};
}
/**
* Analyze contributors data
*/
private analyzeContributors(contributors: any[]): any {
const totalCommits = contributors.reduce((sum, c) => sum + c.commits, 0);
const totalAdditions = contributors.reduce((sum, c) => sum + (c.additions || 0), 0);
const totalDeletions = contributors.reduce((sum, c) => sum + (c.deletions || 0), 0);
return {
total: contributors.length,
totalCommits,
totalAdditions,
totalDeletions,
averageCommitsPerContributor: totalCommits / contributors.length || 0,
topContributor: contributors[0],
// Add more analytics as needed
};
}
/**
* Calculate age from date
*/
private calculateAge(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays < 30) return `${diffDays} days`;
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months`;
return `${Math.floor(diffDays / 365)} years`;
}
/**
* Get date format for groupBy
*/
private getDateFormat(groupBy: string): string {
switch (groupBy) {
case 'day': return '%Y-%m-%d';
case 'week': return '%Y-W%U';
case 'month': return '%Y-%m';
case 'year': return '%Y';
default: return '%Y-%m-%d';
}
}
/**
* Group data by time period
*/
private groupByPeriod(dates: string[], groupBy: string): Record<string, number> {
const grouped: Record<string, number> = {};
dates.forEach(date => {
grouped[date] = (grouped[date] || 0) + 1;
});
return grouped;
}
/**
* Check if operation is a remote operation
*/
private isRemoteOperation(action: string): boolean {
// Analytics can work with both local and remote data
// Remote operations provide additional insights from provider APIs
return false; // Default to local operations
}
/**
* Extract parameters for remote operations
*/
private extractRemoteParameters(params: GitAnalyticsParams): Record<string, any> {
const remoteParams: Record<string, any> = {
projectPath: params.projectPath
};
// Common parameters
if (params.repo) remoteParams.repo = params.repo;
if (params.since) remoteParams.since = params.since;
if (params.until) remoteParams.until = params.until;
if (params.branch) remoteParams.sha = params.branch;
return remoteParams;
}
/**
* Map git-analytics actions to provider operations
*/
private mapActionToProviderOperation(action: string): string {
const actionMap: Record<string, string> = {
'stats': 'analytics-stats',
'commits': 'analytics-commits',
'contributors': 'analytics-contributors'
};
return actionMap[action] || action;
}
/**
* Get tool schema for MCP registration
*/
static getToolSchema() {
return {
name: 'git-analytics',
description: 'Git analytics and statistics tool for repository analysis. Supports stats, commits, and contributors operations. Provides comprehensive analytics for Git repositories.',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['stats', 'commits', 'contributors'],
description: 'The analytics operation to perform'
},
projectPath: {
type: 'string',
description: 'Absolute path to the project directory'
},
provider: {
type: 'string',
enum: ['github', 'gitea', 'both'],
description: 'Provider for enhanced remote analytics (optional)'
},
since: {
type: 'string',
description: 'Start date for analysis (ISO date or relative like "1 week ago")'
},
until: {
type: 'string',
description: 'End date for analysis (ISO date or relative like "yesterday")'
},
branch: {
type: 'string',
description: 'Specific branch to analyze (default: current branch)'
},
ref: {
type: 'string',
description: 'Specific ref to analyze (commit, tag, etc.)'
},
author: {
type: 'string',
description: 'Filter commits by author (for commits operation)'
},
committer: {
type: 'string',
description: 'Filter commits by committer (for commits operation)'
},
grep: {
type: 'string',
description: 'Search in commit messages (for commits operation)'
},
minCommits: {
type: 'number',
description: 'Minimum commits threshold (for contributors operation)'
},
sortBy: {
type: 'string',
enum: ['commits', 'additions', 'deletions', 'name'],
description: 'Sort contributors by field (for contributors operation)'
},
format: {
type: 'string',
enum: ['json', 'csv', 'summary'],
description: 'Output format for results'
},
limit: {
type: 'number',
description: 'Maximum number of results to return'
},
includeStats: {
type: 'boolean',
description: 'Include file change statistics (for commits operation)'
},
includeMerges: {
type: 'boolean',
description: 'Include merge commits in analysis'
},
repo: {
type: 'string',
description: 'Repository name (for remote operations)'
},
groupBy: {
type: 'string',
enum: ['day', 'week', 'month', 'year'],
description: 'Group statistics by time period (for stats operation)'
},
includeFileTypes: {
type: 'boolean',
description: 'Include file type analysis (for stats operation)'
},
includePaths: {
type: 'array',
items: { type: 'string' },
description: 'Specific paths to include in analysis'
},
excludePaths: {
type: 'array',
items: { type: 'string' },
description: 'Paths to exclude from analysis'
}
},
required: ['action', 'projectPath']
}
};
}
}