marsdevs-git-workreport
Version:
🚀 Advanced Git Work Report Generator with AI-Powered Summaries - Generate intelligent daily work reports from Git commit history using Claude AI or OpenRouter. Perfect for DevOps teams, development companies, and client reporting with comprehensive stati
226 lines • 9.21 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitAnalyzer = void 0;
const simple_git_1 = require("simple-git");
const moment_1 = __importDefault(require("moment"));
const ai_summarizer_1 = require("./ai-summarizer");
class GitAnalyzer {
constructor(repoPath) {
this.git = (0, simple_git_1.simpleGit)(repoPath || process.cwd());
}
/**
* Generate a work report for a specific date
*/
async generateWorkReport(date, options = {}) {
const targetDate = (0, moment_1.default)(date, ['DD/MM/YYYY', 'YYYY-MM-DD', 'MM/DD/YYYY']);
if (!targetDate.isValid()) {
throw new Error(`Invalid date format: ${date}. Please use DD/MM/YYYY format.`);
}
const startDate = targetDate.clone().startOf('day');
const endDate = targetDate.clone().endOf('day');
// Get commits for the specified date
const commits = await this.getCommitsForDate(startDate, endDate);
// Process commit statistics
const stats = this.calculateStats(commits);
// Generate AI summary only if there are commits and API key is provided
let aiSummary = '';
const aiOptions = this.getAIOptions(options);
if (commits.length === 0) {
// No commits found - don't make AI API call to save costs
aiSummary = 'No commits detected for this date.';
}
else if (aiOptions) {
// Commits found and AI options available - make API call
const summarizer = new ai_summarizer_1.AISummarizer(aiOptions);
const commitMessages = commits.map(commit => commit.message);
aiSummary = await summarizer.generateSummary(commitMessages, stats.totalInsertions + stats.totalDeletions);
}
else {
// Commits found but no AI options - use fallback
aiSummary = this.generateFallbackSummary(commits, stats);
}
return {
date: targetDate.format('DD/MM/YYYY'),
totalCommits: commits.length,
totalInsertions: stats.totalInsertions,
totalDeletions: stats.totalDeletions,
authors: stats.authors,
aiSummary,
totalLinesChanged: stats.totalInsertions + stats.totalDeletions
};
}
/**
* Get AI options based on provided API keys
*/
getAIOptions(options) {
// Priority: explicit provider > anthropic key > openrouter key
if (options.aiProvider && options.openrouterApiKey && options.aiProvider === 'openrouter') {
return {
provider: 'openrouter',
apiKey: options.openrouterApiKey
};
}
if (options.aiProvider && options.anthropicApiKey && options.aiProvider === 'anthropic') {
return {
provider: 'anthropic',
apiKey: options.anthropicApiKey
};
}
// Fallback to legacy behavior
if (options.anthropicApiKey) {
return {
provider: 'anthropic',
apiKey: options.anthropicApiKey
};
}
if (options.openrouterApiKey) {
return {
provider: 'openrouter',
apiKey: options.openrouterApiKey
};
}
return null;
}
/**
* Get commits for a specific date range
*/
async getCommitsForDate(startDate, endDate) {
try {
// Use the correct date format with time that works with simple-git
const logResult = await this.git.log({
'--since': startDate.format('YYYY-MM-DD HH:mm:ss'),
'--until': endDate.format('YYYY-MM-DD HH:mm:ss'),
'--pretty': 'format:%H|%an|%ad|%s',
'--date': 'format:%Y-%m-%d %H:%M:%S'
});
const commits = [];
for (const commit of logResult.all) {
const commitInfo = await this.processCommit(commit.hash);
if (commitInfo) {
commits.push(commitInfo);
}
}
return commits;
}
catch (error) {
throw new Error(`Failed to fetch commits: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Process individual commit to get detailed information
*/
async processCommit(hash) {
try {
// Get commit details
const commitDetails = await this.git.show([hash, '--stat', '--format=%H|%an|%ad|%s', '--date=format:%Y-%m-%d %H:%M:%S']);
const lines = commitDetails.split('\n');
const headerLine = lines[0];
const [commitHash, author, date, ...messageParts] = headerLine.split('|');
const message = messageParts.join('|');
// Parse stat information
const stats = this.parseCommitStats(lines);
// Get changed files
const files = await this.getChangedFiles(hash);
return {
hash: commitHash,
author,
date,
message: message.trim(),
files,
insertions: stats.insertions,
deletions: stats.deletions
};
}
catch (error) {
console.warn(`Failed to process commit ${hash}: ${error instanceof Error ? error.message : 'Unknown error'}`);
return null;
}
}
/**
* Parse commit statistics from git show output
*/
parseCommitStats(lines) {
let insertions = 0;
let deletions = 0;
for (const line of lines) {
// Look for the stat summary line that contains insertions and deletions
if (line.includes('insertion') || line.includes('insertions') || line.includes('deletion') || line.includes('deletions')) {
// Match patterns like "1 insertion(+), 1 deletion(-)" or "38 insertions(+), 44 deletions(-)"
// Also handle lines that start with spaces and may have "X file changed" prefix
const match = line.match(/(\d+)\s+insertions?\(\+\),\s*(\d+)\s+deletions?\(-\)/);
if (match) {
insertions = parseInt(match[1], 10);
deletions = parseInt(match[2], 10);
break;
}
// Also try the old format without parentheses
const oldMatch = line.match(/(\d+)\s+insertions?,\s*(\d+)\s+deletions?/);
if (oldMatch) {
insertions = parseInt(oldMatch[1], 10);
deletions = parseInt(oldMatch[2], 10);
break;
}
// Try a more flexible pattern that handles leading whitespace
const flexibleMatch = line.match(/\s*(\d+)\s+insertions?\(\+\),\s*(\d+)\s+deletions?\(-\)/);
if (flexibleMatch) {
insertions = parseInt(flexibleMatch[1], 10);
deletions = parseInt(flexibleMatch[2], 10);
break;
}
}
}
return { insertions, deletions };
}
/**
* Get list of changed files for a commit
*/
async getChangedFiles(hash) {
try {
const diffResult = await this.git.diff([`${hash}^`, hash, '--name-only']);
return diffResult.split('\n').filter((file) => file.trim() !== '');
}
catch (error) {
// If this is the first commit, there's no parent
try {
const diffResult = await this.git.show([hash, '--name-only']);
return diffResult.split('\n').filter((file) => file.trim() !== '');
}
catch (innerError) {
return [];
}
}
}
/**
* Calculate overall statistics from commits
*/
calculateStats(commits) {
const totalInsertions = commits.reduce((sum, commit) => sum + commit.insertions, 0);
const totalDeletions = commits.reduce((sum, commit) => sum + commit.deletions, 0);
const authors = [...new Set(commits.map(commit => commit.author))];
return {
totalInsertions,
totalDeletions,
authors
};
}
/**
* Generate a fallback summary when AI is not available
*/
generateFallbackSummary(commits, stats) {
const { totalInsertions, totalDeletions, authors } = stats;
if (commits.length === 0) {
return 'No changes were made on this date.';
}
const summaryParts = [
`Completed ${commits.length} commit${commits.length !== 1 ? 's' : ''}`,
`by ${authors.length} author${authors.length !== 1 ? 's' : ''} (${authors.join(', ')})`,
`with ${totalInsertions + totalDeletions} total lines changed`
];
return summaryParts.join(', ');
}
}
exports.GitAnalyzer = GitAnalyzer;
//# sourceMappingURL=git-analyzer.js.map
;