UNPKG

git-changelog-tool

Version:

A comprehensive collection of tools to generate, format, and visualize changelogs from git commit history

462 lines (411 loc) 12 kB
const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); /** * Git Changelog Generator Node.js API */ class GitChangelogGenerator { constructor() { this.packageDir = __dirname; } /** * Generate changelog using shell script * @param {Object} options - Options for changelog generation * @returns {Promise<string>} - Generated changelog content */ generateShell(options = {}) { return new Promise((resolve, reject) => { const scriptPath = path.join(this.packageDir, 'git_changelog.sh'); const args = this._buildShellArgs(options); let output = ''; let error = ''; const child = spawn('bash', [scriptPath, ...args], { cwd: options.cwd || process.cwd() }); child.stdout.on('data', (data) => { output += data.toString(); }); child.stderr.on('data', (data) => { error += data.toString(); }); child.on('close', (code) => { if (code === 0) { resolve(output); } else { reject(new Error(error || `Process exited with code ${code}`)); } }); child.on('error', (err) => { reject(err); }); }); } /** * Generate changelog using Python script * @param {Object} options - Options for changelog generation * @returns {Promise<string>} - Generated changelog content */ generatePython(options = {}) { return new Promise((resolve, reject) => { const scriptPath = path.join(this.packageDir, 'git_changelog.py'); const args = this._buildPythonArgs(options); let output = ''; let error = ''; const child = spawn('python3', [scriptPath, ...args], { cwd: options.cwd || process.cwd() }); child.stdout.on('data', (data) => { output += data.toString(); }); child.stderr.on('data', (data) => { error += data.toString(); }); child.on('close', (code) => { if (code === 0) { resolve(output); } else { reject(new Error(error || `Process exited with code ${code}`)); } }); child.on('error', (err) => { reject(err); }); }); } /** * Get path to the web parser * @returns {string} - Path to parser/index.html */ getParserPath() { return path.join(this.packageDir, 'parser', 'index.html'); } /** * Open the web parser in the default browser * @returns {Promise<boolean>} - Success status */ openParser() { return new Promise((resolve, reject) => { const parserPath = this.getParserPath(); // Check if parser exists if (!require('fs').existsSync(parserPath)) { reject(new Error(`Parser not found at: ${parserPath}`)); return; } // Determine the command to open the browser based on the platform let openCommand; let args = [parserPath]; switch (process.platform) { case 'darwin': // macOS openCommand = 'open'; break; case 'win32': // Windows openCommand = 'start'; args = ['', parserPath]; // start command needs empty first argument break; case 'linux': // Linux openCommand = 'xdg-open'; break; default: reject(new Error(`Unsupported platform: ${process.platform}`)); return; } // Open the parser in the default browser const child = spawn(openCommand, args, { stdio: 'ignore', detached: true }); child.unref(); child.on('error', (error) => { reject(error); }); // Consider it successful if no immediate error setTimeout(() => { resolve(true); }, 100); }); } /** * Generate changelog and parse as JSON * @param {Object} options - Options for changelog generation * @returns {Promise<Object>} - Parsed changelog JSON */ async generateJSON(options = {}) { const shellOptions = { ...options, format: 'json' }; const output = await this.generateShell(shellOptions); try { return JSON.parse(output); } catch (error) { throw new Error(`Failed to parse JSON output: ${error.message}`); } } _buildShellArgs(options) { const args = []; if (options.format) { args.push('--format', options.format); } if (options.since) { args.push('--since', options.since); } if (options.until) { args.push('--until', options.until); } if (options.branch) { args.push('--branch', options.branch); } if (options.maxCount) { args.push('--max-count', options.maxCount.toString()); } if (options.output) { args.push('--output', options.output); } if (options.title) { args.push('--title', options.title); } if (options.includeTime) { args.push('--include-time'); } return args; } _buildPythonArgs(options) { const args = []; if (options.since) { args.push('--since', options.since); } if (options.until) { args.push('--until', options.until); } if (options.branch) { args.push('--branch', options.branch); } if (options.maxCount) { args.push('--max-count', options.maxCount.toString()); } if (options.format) { args.push('--format', options.format); } if (options.title) { args.push('--title', options.title); } if (options.output) { args.push('--output', options.output); } return args; } /** * Create a git tag and generate changelog * @param {string} version - Version to tag (e.g., "1.2.3") * @param {Object} options - Configuration options * @param {string} [options.message] - Tag message (default: "Release VERSION") * @param {string} [options.prefix="v"] - Tag prefix * @param {string} [options.format="markdown"] - Changelog format (always markdown) * @param {string} [options.output] - Changelog output file * @param {boolean} [options.dryRun=false] - Show what would be done without executing * @param {boolean} [options.force=false] - Force tag creation (overwrite existing) * @param {boolean} [options.annotated=true] - Create annotated tag * @param {boolean} [options.push=false] - Push tag to origin * @param {boolean} [options.usePython=true] - Use Python script for changelog (always true) * @param {boolean} [options.sinceLastTag=true] - Generate changelog since last tag * @returns {Promise<Object>} Result object with tag and changelog info */ async createTagAndChangelog(version, options = {}) { const { message, prefix = 'v', format = 'markdown', output, dryRun = false, force = false, annotated = true, push = false, sinceLastTag = true, since, until, maxCount } = options; const tagScript = path.join(this.packageDir, 'git_tag_changelog.sh'); const args = []; // Add version if (version) { args.push(version); } // Add options if (message) { args.push('--message', message); } if (prefix !== 'v') { args.push('--prefix', prefix); } // Format is always markdown, no need to pass it if (output) { args.push('--output', output); } if (dryRun) { args.push('--dry-run'); } if (force) { args.push('--force'); } if (!annotated) { args.push('--lightweight'); } if (push) { args.push('--push'); } // Python is now the default, no --python flag needed if (sinceLastTag) { args.push('--since-last-tag'); } if (since) { args.push('--since', since); } if (until) { args.push('--until', until); } if (maxCount) { args.push('--max-count', maxCount.toString()); } try { const result = await this._executeCommand('bash', [tagScript, ...args]); const tagName = `${prefix}${version}`; const changelogFile = output || `CHANGELOG.md`; return { success: true, version, tag: tagName, changelog: changelogFile, dryRun, output: result.stdout, error: result.stderr }; } catch (error) { return { success: false, version, error: error.message, output: error.stdout || '', stderr: error.stderr || '' }; } } /** * Auto-increment version and create tag with changelog * @param {string} incrementType - Type of increment: "major", "minor", "patch" * @param {Object} options - Configuration options (same as createTagAndChangelog) * @returns {Promise<Object>} Result object with tag and changelog info */ async autoIncrementTag(incrementType, options = {}) { const tagScript = path.join(this.packageDir, 'git_tag_changelog.sh'); const args = ['--auto-increment', incrementType]; // Add all other options const { message, prefix = 'v', format = 'markdown', output, dryRun = false, force = false, annotated = true, push = false, sinceLastTag = true, since, until, maxCount } = options; if (message) { args.push('--message', message); } if (prefix !== 'v') { args.push('--prefix', prefix); } // Format is always markdown, no need to pass it if (output) { args.push('--output', output); } if (dryRun) { args.push('--dry-run'); } if (force) { args.push('--force'); } if (!annotated) { args.push('--lightweight'); } if (push) { args.push('--push'); } // Python is now the default, no --python flag needed if (sinceLastTag) { args.push('--since-last-tag'); } if (since) { args.push('--since', since); } if (until) { args.push('--until', until); } if (maxCount) { args.push('--max-count', maxCount.toString()); } try { const result = await this._executeCommand('bash', [tagScript, ...args]); // Extract version from output (it will be logged) const versionMatch = result.stderr.match(/Version: (\d+\.\d+\.\d+)/); const version = versionMatch ? versionMatch[1] : 'unknown'; const tagName = `${prefix}${version}`; const changelogFile = output || `CHANGELOG.md`; return { success: true, version, tag: tagName, changelog: changelogFile, incrementType, dryRun, output: result.stdout, error: result.stderr }; } catch (error) { return { success: false, incrementType, error: error.message, output: error.stdout || '', stderr: error.stderr || '' }; } } /** * Helper method to execute shell commands * @private */ _executeCommand(command, args, options = {}) { return new Promise((resolve, reject) => { let output = ''; let error = ''; const child = spawn(command, args, { cwd: options.cwd || process.cwd(), ...options }); child.stdout.on('data', (data) => { output += data.toString(); }); child.stderr.on('data', (data) => { error += data.toString(); }); child.on('close', (code) => { if (code === 0) { resolve({ stdout: output, stderr: error, code }); } else { const err = new Error(error || `Process exited with code ${code}`); err.stdout = output; err.stderr = error; err.code = code; reject(err); } }); child.on('error', (err) => { reject(err); }); }); } } module.exports = GitChangelogGenerator;