import axios from './githubAxios.js'; import util from 'util'; import cp from 'child_process'; import Handlebars from 'handlebars'; import fs from 'fs/promises'; import { colorize } from './helpers/colorize.js'; const exec = util.promisify(cp.exec); const ONE_MB = 1024 * 1024; const removeExtraLineBreaks = (str) => str.replace(/(?:\r\n|\r|\n){3,}/gm, '\r\n\r\n'); const cleanTemplate = (template) => template .replace(/\n +/g, '\n') .replace(/^ +/, '') .replace(/\n\n\n+/g, '\n\n') .replace(/\n\n$/, '\n'); const getUserFromCommit = ((commitCache) => async (sha) => { try { if (commitCache[sha] !== undefined) { return commitCache[sha]; } console.log(colorize()`fetch github commit info (${sha})`); const { data } = await axios.get(`https://api.github.com/repos/axios/axios/commits/${sha}`); return (commitCache[sha] = { ...data.commit.author, ...data.author, avatar_url_sm: data.author.avatar_url ? data.author.avatar_url + '&s=18' : '', }); } catch (err) { return (commitCache[sha] = null); } })({}); const getIssueById = ((cache) => async (id) => { if (cache[id] !== undefined) { return cache[id]; } try { const { data } = await axios.get(`https://api.github.com/repos/axios/axios/issues/${id}`); return (cache[id] = data); } catch (err) { return null; } })({}); const getUserInfo = ((userCache) => async (userEntry) => { const { email, commits } = userEntry; if (userCache[email] !== undefined) { return userCache[email]; } console.log(colorize()`fetch github user info [${userEntry.name}]`); return (userCache[email] = { ...userEntry, ...(await getUserFromCommit(commits[0].hash)), }); })({}); const deduplicate = (authors) => { const loginsMap = {}; const combined = {}; const assign = (a, b) => { const { insertions, _deletions, _points, ...rest } = b; Object.assign(a, rest); a.insertions += insertions; a.deletions += insertions; a.insertions += insertions; }; for (const [email, user] of Object.entries(authors)) { const { login } = user; let entry; if (login && (entry = loginsMap[login])) { assign(entry, user); } else { login && (loginsMap[login] = user); combined[email] = user; } } return combined; }; const getReleaseInfo = ((releaseCache) => async (tag) => { if (releaseCache[tag] !== undefined) { return releaseCache[tag]; } const isUnreleasedTag = !tag; const version = 'v' + tag.replace(/^v/, ''); const command = isUnreleasedTag ? `npx auto-changelog --unreleased-only --stdout --commit-limit false --template json` : `npx auto-changelog ${ version ? '--starting-version ' + version + ' --ending-version ' + version : '' } --stdout --commit-limit false --template json`; console.log(command); const { stdout } = await exec(command, { maxBuffer: 10 * ONE_MB }); const release = JSON.parse(stdout)[0]; if (release) { const authors = {}; const commits = [ ...release.commits, ...release.fixes.map((fix) => fix.commit), ...release.merges.map((fix) => fix.commit), ].filter(Boolean); const commitMergeMap = {}; for (const merge of release.merges) { commitMergeMap[merge.commit.hash] = merge.id; } for (const { hash, author, email, insertions, deletions } of commits) { const entry = (authors[email] = authors[email] || { name: author, prs: [], email, commits: [], insertions: 0, deletions: 0, }); entry.commits.push({ hash }); let pr; if ((pr = commitMergeMap[hash])) { entry.prs.push(pr); } console.log(colorize()`Found commit [${hash}]`); entry.displayName = entry.name || author || entry.login; entry.github = entry.login ? `https://github.com/${encodeURIComponent(entry.login)}` : ''; entry.insertions += insertions; entry.deletions += deletions; entry.points = entry.insertions + entry.deletions; } for (const [email, author] of Object.entries(authors)) { const entry = (authors[email] = await getUserInfo(author)); entry.isBot = entry.type === 'Bot'; } release.authors = Object.values(deduplicate(authors)).sort((a, b) => b.points - a.points); release.allCommits = commits; } releaseCache[tag] = release; return release; })({}); const renderContributorsList = async (tag, template) => { const release = await getReleaseInfo(tag); const compile = Handlebars.compile(String(await fs.readFile(template))); const content = compile(release); return removeExtraLineBreaks(cleanTemplate(content)); }; const renderPRsList = async ( tag, template, { comments_threshold = 5, awesome_threshold = 5, label = 'add_to_changelog' } = {} ) => { const release = await getReleaseInfo(tag); const prs = {}; for (const merge of release.merges) { const pr = await getIssueById(merge.id); if (pr && pr.labels.find(({ name }) => name === label)) { const { reactions, body } = pr; prs[pr.number] = pr; pr.isHot = pr.comments > comments_threshold; const points = reactions['+1'] + reactions['hooray'] + reactions['rocket'] + reactions['heart'] + reactions['laugh'] - reactions['-1']; pr.isAwesome = points > awesome_threshold; let match; pr.messages = []; if (body) { const reg = /```+changelog\n*(.+?)?\n*```/gms; while ((match = reg.exec(body))) { match[1] && pr.messages.push(match[1]); } } } } release.prs = Object.values(prs); const compile = Handlebars.compile(String(await fs.readFile(template))); const content = compile(release); return removeExtraLineBreaks(cleanTemplate(content)); }; const getTagRef = async (tag) => { try { return (await exec(`git show-ref --tags "refs/tags/${tag}"`)).stdout.split(' ')[0]; } catch (e) { // Do nothing } }; export { renderContributorsList, getReleaseInfo, renderPRsList, getTagRef };