| 1 | 'use strict'; |
| 2 | |
| 3 | const https = require('https'); |
| 4 | const path = require('path'); |
| 5 | |
| 6 | const {execFileAsync, repoRoot} = require('./utils'); |
| 7 | |
| 8 | async function fetchNpmInfo(packageName, {log}) { |
| 9 | const npmArgs = ['view', `${packageName}@latest`, '--json']; |
| 10 | const options = {cwd: repoRoot, maxBuffer: 10 * 1024 * 1024}; |
| 11 | log(`Fetching npm info for ${packageName}...`); |
| 12 | const {stdout} = await execFileAsync('npm', npmArgs, options); |
| 13 | |
| 14 | let data = stdout.trim(); |
| 15 | if (!data) { |
| 16 | throw new Error(`npm view returned empty result for ${packageName}`); |
| 17 | } |
| 18 | |
| 19 | let info = JSON.parse(data); |
| 20 | if (Array.isArray(info)) { |
| 21 | info = info[info.length - 1]; |
| 22 | } |
| 23 | |
| 24 | const version = info.version || info['dist-tags']?.latest; |
| 25 | let gitHead = info.gitHead || null; |
| 26 | |
| 27 | if (!gitHead) { |
| 28 | const gitHeadResult = await execFileAsync( |
| 29 | 'npm', |
| 30 | ['view', `${packageName}@${version}`, 'gitHead'], |
| 31 | {cwd: repoRoot, maxBuffer: 1024 * 1024} |
| 32 | ); |
| 33 | const possibleGitHead = gitHeadResult.stdout.trim(); |
| 34 | if ( |
| 35 | possibleGitHead && |
| 36 | possibleGitHead !== 'undefined' && |
| 37 | possibleGitHead !== 'null' |
| 38 | ) { |
| 39 | log(`Found gitHead for ${packageName}@${version}: ${possibleGitHead}`); |
| 40 | gitHead = possibleGitHead; |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | if (!version) { |
| 45 | throw new Error( |
| 46 | `Unable to determine latest published version for ${packageName}` |
| 47 | ); |
| 48 | } |
| 49 | if (!gitHead) { |
| 50 | throw new Error( |
| 51 | `Unable to determine git commit for ${packageName}@${version}` |
| 52 | ); |
| 53 | } |
| 54 | |
| 55 | return { |
| 56 | publishedVersion: version, |
| 57 | gitHead, |
| 58 | }; |
| 59 | } |
| 60 | |
| 61 | async function collectCommitsSince(packageName, sinceGitSha, {log}) { |
| 62 | log(`Collecting commits for ${packageName} since ${sinceGitSha}...`); |
| 63 | await execFileAsync('git', ['cat-file', '-e', `${sinceGitSha}^{commit}`], { |
| 64 | cwd: repoRoot, |
| 65 | }); |
| 66 | const {stdout} = await execFileAsync( |
| 67 | 'git', |
| 68 | [ |
| 69 | 'rev-list', |
| 70 | '--reverse', |
| 71 | `${sinceGitSha}..HEAD`, |
| 72 | '--', |
| 73 | path.posix.join('packages', packageName), |
| 74 | ], |
| 75 | {cwd: repoRoot, maxBuffer: 10 * 1024 * 1024} |
| 76 | ); |
| 77 | |
| 78 | return stdout |
| 79 | .trim() |
| 80 | .split('\n') |
| 81 | .map(line => line.trim()) |
| 82 | .filter(Boolean); |
| 83 | } |
| 84 | |
| 85 | async function loadCommitDetails(sha, {log}) { |
| 86 | log(`Loading commit details for ${sha}...`); |
| 87 | const format = ['%H', '%s', '%an', '%ae', '%ct', '%B'].join('%n'); |
| 88 | const {stdout} = await execFileAsync( |
| 89 | 'git', |
| 90 | ['show', '--quiet', `--format=${format}`, sha], |
| 91 | {cwd: repoRoot, maxBuffer: 10 * 1024 * 1024} |
| 92 | ); |
| 93 | |
| 94 | const [commitSha, subject, authorName, authorEmail, timestamp, ...rest] = |
| 95 | stdout.split('\n'); |
| 96 | const body = rest.join('\n').trim(); |
| 97 | |
| 98 | return { |
| 99 | sha: commitSha.trim(), |
| 100 | subject: subject.trim(), |
| 101 | authorName: authorName.trim(), |
| 102 | authorEmail: authorEmail.trim(), |
| 103 | timestamp: +timestamp.trim() || 0, |
| 104 | body, |
| 105 | }; |
| 106 | } |
| 107 | |
| 108 | function extractPrNumber(subject, body) { |
| 109 | const patterns = [ |
| 110 | /\(#(\d+)\)/, |
| 111 | /https:\/\/github\.com\/facebook\/react\/pull\/(\d+)/, |
| 112 | ]; |
| 113 | |
| 114 | for (let i = 0; i < patterns.length; i++) { |
| 115 | const pattern = patterns[i]; |
| 116 | const subjectMatch = subject && subject.match(pattern); |
| 117 | if (subjectMatch) { |
| 118 | return subjectMatch[1]; |
| 119 | } |
| 120 | const bodyMatch = body && body.match(pattern); |
| 121 | if (bodyMatch) { |
| 122 | return bodyMatch[1]; |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | return null; |
| 127 | } |
| 128 | |
| 129 | async function fetchPullRequestMetadata(prNumber, {log}) { |
| 130 | log(`Fetching PR metadata for #${prNumber}...`); |
| 131 | const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || null; |
| 132 | const requestOptions = { |
| 133 | hostname: 'api.github.com', |
| 134 | path: `/repos/facebook/react/pulls/${prNumber}`, |
| 135 | method: 'GET', |
| 136 | headers: { |
| 137 | 'User-Agent': 'generate-changelog-script', |
| 138 | Accept: 'application/vnd.github+json', |
| 139 | }, |
| 140 | }; |
| 141 | if (token) { |
| 142 | requestOptions.headers.Authorization = `Bearer ${token}`; |
| 143 | } |
| 144 | |
| 145 | return new Promise(resolve => { |
| 146 | const req = https.request(requestOptions, res => { |
| 147 | let raw = ''; |
| 148 | res.on('data', chunk => { |
| 149 | raw += chunk; |
| 150 | }); |
| 151 | res.on('end', () => { |
| 152 | if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { |
| 153 | try { |
| 154 | const json = JSON.parse(raw); |
| 155 | resolve({ |
| 156 | authorLogin: json.user?.login || null, |
| 157 | }); |
| 158 | } catch (error) { |
| 159 | process.stderr.write( |
| 160 | `Warning: unable to parse GitHub response for PR #${prNumber}: ${error.message}\n` |
| 161 | ); |
| 162 | resolve(null); |
| 163 | } |
| 164 | } else { |
| 165 | process.stderr.write( |
| 166 | `Warning: GitHub API request failed for PR #${prNumber} with status ${res.statusCode}\n` |
| 167 | ); |
| 168 | resolve(null); |
| 169 | } |
| 170 | }); |
| 171 | }); |
| 172 | |
| 173 | req.on('error', error => { |
| 174 | process.stderr.write( |
| 175 | `Warning: GitHub API request errored for PR #${prNumber}: ${error.message}\n` |
| 176 | ); |
| 177 | resolve(null); |
| 178 | }); |
| 179 | |
| 180 | req.end(); |
| 181 | }); |
| 182 | } |
| 183 | |
| 184 | module.exports = { |
| 185 | fetchNpmInfo, |
| 186 | collectCommitsSince, |
| 187 | loadCommitDetails, |
| 188 | extractPrNumber, |
| 189 | fetchPullRequestMetadata, |
| 190 | }; |