| 1 | 'use strict'; |
| 2 | |
| 3 | const {stablePackages} = require('../../../ReactVersions'); |
| 4 | const {parseArgs} = require('./args'); |
| 5 | const { |
| 6 | fetchNpmInfo, |
| 7 | collectCommitsSince, |
| 8 | loadCommitDetails, |
| 9 | extractPrNumber, |
| 10 | fetchPullRequestMetadata, |
| 11 | } = require('./data'); |
| 12 | const {summarizePackages} = require('./summaries'); |
| 13 | const {buildChangelogEntries, renderChangelog} = require('./formatters'); |
| 14 | const {noopLogger} = require('./utils'); |
| 15 | |
| 16 | async function main() { |
| 17 | const {packageSpecs, summarizer, debug, format} = parseArgs( |
| 18 | process.argv.slice(2) |
| 19 | ); |
| 20 | const log = debug |
| 21 | ? (...args) => console.log('[generate-changelog]', ...args) |
| 22 | : noopLogger; |
| 23 | |
| 24 | const allStablePackages = Object.keys(stablePackages); |
| 25 | const packageTargets = new Map(); |
| 26 | for (let i = 0; i < packageSpecs.length; i++) { |
| 27 | const spec = packageSpecs[i]; |
| 28 | if (!allStablePackages.includes(spec.name)) { |
| 29 | throw new Error( |
| 30 | `Package "${spec.name}" is not listed in stablePackages.` |
| 31 | ); |
| 32 | } |
| 33 | if (packageTargets.has(spec.name)) { |
| 34 | throw new Error(`Package "${spec.name}" was specified more than once.`); |
| 35 | } |
| 36 | packageTargets.set(spec.name, spec); |
| 37 | } |
| 38 | |
| 39 | const targetPackages = packageSpecs.map(spec => spec.name); |
| 40 | log( |
| 41 | `Starting changelog generation for: ${packageSpecs |
| 42 | .map(spec => `${spec.name}@${spec.displayVersion || spec.version}`) |
| 43 | .join(', ')}` |
| 44 | ); |
| 45 | |
| 46 | const packageInfoMap = new Map(); |
| 47 | const packageInfoResults = await Promise.all( |
| 48 | targetPackages.map(async pkg => { |
| 49 | const info = await fetchNpmInfo(pkg, {log}); |
| 50 | return {pkg, info}; |
| 51 | }) |
| 52 | ); |
| 53 | for (let i = 0; i < packageInfoResults.length; i++) { |
| 54 | const entry = packageInfoResults[i]; |
| 55 | packageInfoMap.set(entry.pkg, entry.info); |
| 56 | } |
| 57 | |
| 58 | const commitPackagesMap = new Map(); |
| 59 | const commitCollections = await Promise.all( |
| 60 | targetPackages.map(async pkg => { |
| 61 | const {gitHead} = packageInfoMap.get(pkg); |
| 62 | const commits = await collectCommitsSince(pkg, gitHead, {log}); |
| 63 | log(`Package ${pkg} has ${commits.length} commit(s) since ${gitHead}.`); |
| 64 | return {pkg, commits}; |
| 65 | }) |
| 66 | ); |
| 67 | for (let i = 0; i < commitCollections.length; i++) { |
| 68 | const entry = commitCollections[i]; |
| 69 | const pkg = entry.pkg; |
| 70 | const commits = entry.commits; |
| 71 | for (let j = 0; j < commits.length; j++) { |
| 72 | const sha = commits[j]; |
| 73 | if (!commitPackagesMap.has(sha)) { |
| 74 | commitPackagesMap.set(sha, new Set()); |
| 75 | } |
| 76 | commitPackagesMap.get(sha).add(pkg); |
| 77 | } |
| 78 | } |
| 79 | log(`Found ${commitPackagesMap.size} commits touching target packages.`); |
| 80 | |
| 81 | if (commitPackagesMap.size === 0) { |
| 82 | console.log('No commits found for the selected packages.'); |
| 83 | return; |
| 84 | } |
| 85 | |
| 86 | const commitDetails = await Promise.all( |
| 87 | Array.from(commitPackagesMap.entries()).map( |
| 88 | async ([sha, packagesTouched]) => { |
| 89 | const detail = await loadCommitDetails(sha, {log}); |
| 90 | detail.packages = packagesTouched; |
| 91 | detail.prNumber = extractPrNumber(detail.subject, detail.body); |
| 92 | return detail; |
| 93 | } |
| 94 | ) |
| 95 | ); |
| 96 | |
| 97 | commitDetails.sort((a, b) => a.timestamp - b.timestamp); |
| 98 | log(`Ordered ${commitDetails.length} commit(s) chronologically.`); |
| 99 | |
| 100 | const commitsByPackage = new Map(); |
| 101 | commitDetails.forEach(commit => { |
| 102 | commit.packages.forEach(pkgName => { |
| 103 | if (!commitsByPackage.has(pkgName)) { |
| 104 | commitsByPackage.set(pkgName, []); |
| 105 | } |
| 106 | commitsByPackage.get(pkgName).push(commit); |
| 107 | }); |
| 108 | }); |
| 109 | |
| 110 | const uniquePrNumbers = Array.from( |
| 111 | new Set(commitDetails.map(commit => commit.prNumber).filter(Boolean)) |
| 112 | ); |
| 113 | log(`Identified ${uniquePrNumbers.length} unique PR number(s).`); |
| 114 | |
| 115 | const prMetadata = new Map(); |
| 116 | log(`Summarizer selected: ${summarizer || 'none (using commit titles)'}`); |
| 117 | const prMetadataResults = await Promise.all( |
| 118 | uniquePrNumbers.map(async prNumber => { |
| 119 | const meta = await fetchPullRequestMetadata(prNumber, {log}); |
| 120 | return {prNumber, meta}; |
| 121 | }) |
| 122 | ); |
| 123 | for (let i = 0; i < prMetadataResults.length; i++) { |
| 124 | const entry = prMetadataResults[i]; |
| 125 | if (entry.meta) { |
| 126 | prMetadata.set(entry.prNumber, entry.meta); |
| 127 | } |
| 128 | } |
| 129 | log(`Fetched metadata for ${prMetadata.size} PR(s).`); |
| 130 | |
| 131 | const summariesByPackage = await summarizePackages({ |
| 132 | summarizer, |
| 133 | packageSpecs, |
| 134 | packageTargets, |
| 135 | commitsByPackage, |
| 136 | log, |
| 137 | }); |
| 138 | |
| 139 | const changelogEntries = buildChangelogEntries({ |
| 140 | packageSpecs, |
| 141 | commitsByPackage, |
| 142 | summariesByPackage, |
| 143 | prMetadata, |
| 144 | }); |
| 145 | |
| 146 | log('Generated changelog sections.'); |
| 147 | const output = renderChangelog(changelogEntries, format); |
| 148 | console.log(output); |
| 149 | } |
| 150 | |
| 151 | if (require.main === module) { |
| 152 | main().catch(error => { |
| 153 | process.stderr.write(`${error.message}\n`); |
| 154 | process.exit(1); |
| 155 | }); |
| 156 | } else { |
| 157 | module.exports = main; |
| 158 | } |