UNPKG

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
"use strict"; 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