1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const tslib_1 = require("tslib");
|
4 | const url_1 = require("url");
|
5 | const url_join_1 = tslib_1.__importDefault(require("url-join"));
|
6 | const make_hooks_1 = require("./utils/make-hooks");
|
7 | const semver_1 = tslib_1.__importDefault(require("./semver"));
|
8 |
|
9 | const getHeaderDepth = (line) => line.split('').reduce((count, char) => (char === '#' ? count + 1 : count), 0);
|
10 |
|
11 | const filterLabel = (commits, label) => commits.filter(commit => commit.labels.includes(label));
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | class Changelog {
|
17 |
|
18 | constructor(logger, options) {
|
19 | this.logger = logger;
|
20 | this.options = options;
|
21 | this.hooks = make_hooks_1.makeChangelogHooks();
|
22 | if (!this.options.labels.find(l => l.name === 'pushToBaseBranch'))
|
23 | this.options.labels.push({
|
24 | name: 'pushToBaseBranch',
|
25 | changelogTitle: `⚠️ Pushed to ${options.baseBranch}`,
|
26 | description: 'N/A',
|
27 | releaseType: semver_1.default.patch
|
28 | });
|
29 | }
|
30 |
|
31 | loadDefaultHooks() {
|
32 | this.hooks.renderChangelogAuthor.tap('Default', (author, commit) => this.createUserLink(author, commit));
|
33 | this.hooks.renderChangelogAuthorLine.tap('Default', (author, user) => {
|
34 | const authorString = author.name && user ? `${author.name} (${user})` : user;
|
35 | return authorString ? `- ${authorString}` : undefined;
|
36 | });
|
37 | this.hooks.renderChangelogLine.tap('Default', ([commit, line]) => [
|
38 | commit,
|
39 | line
|
40 | ]);
|
41 | this.hooks.renderChangelogTitle.tap('Default', (label, changelogTitles) => `#### ${changelogTitles[label]}\n`);
|
42 | this.hooks.omitReleaseNotes.tap('Renovate', commit => {
|
43 | const names = ['renovate-pro[bot]', 'renovate-bot'];
|
44 | if (commit.authors.find(author => Boolean((author.name && names.includes(author.name)) ||
|
45 | (author.username && names.includes(author.username))))) {
|
46 | return true;
|
47 | }
|
48 | });
|
49 | }
|
50 |
|
51 | async generateReleaseNotes(commits) {
|
52 | if (commits.length === 0) {
|
53 | return '';
|
54 | }
|
55 | this.logger.verbose.info('Generating release notes for:\n', commits);
|
56 | const split = this.splitCommits(commits);
|
57 | this.logger.verbose.info('Split commits into groups');
|
58 | this.logger.veryVerbose.info('\n', split);
|
59 | const sections = [];
|
60 | const extraNotes = (await this.hooks.addToBody.promise([], commits)) || [];
|
61 | extraNotes.filter(Boolean).forEach(note => sections.push(note));
|
62 | await this.createReleaseNotesSection(commits, sections);
|
63 | this.logger.verbose.info('Added release notes to changelog');
|
64 | this.authors = this.getAllAuthors(split);
|
65 | await this.createLabelSection(split, sections);
|
66 | this.logger.verbose.info('Added groups to changelog');
|
67 | await this.createAuthorSection(sections);
|
68 | this.logger.verbose.info('Added authors to changelog');
|
69 | const result = sections.join('\n\n');
|
70 | this.logger.verbose.info('Successfully generated release notes.');
|
71 | return result;
|
72 | }
|
73 |
|
74 | createUserLink(author, commit) {
|
75 | const githubUrl = new url_1.URL(this.options.baseUrl).origin;
|
76 | if (author.username === 'invalid-email-address') {
|
77 | return;
|
78 | }
|
79 | return author.username
|
80 | ? `[@${author.username}](${url_join_1.default(githubUrl, author.username)})`
|
81 | : author.email || commit.authorEmail;
|
82 | }
|
83 |
|
84 | splitCommits(commits) {
|
85 | let currentCommits = [...commits];
|
86 | const order = ['major', 'minor', 'patch'];
|
87 | const sections = this.options.labels
|
88 | .filter(label => label.changelogTitle)
|
89 | .sort((a, b) => {
|
90 | const bIndex = order.indexOf(b.releaseType || '') + 1 || order.length + 1;
|
91 | const aIndex = order.indexOf(a.releaseType || '') + 1 || order.length + 1;
|
92 | return aIndex - bIndex;
|
93 | })
|
94 | .reduce((acc, item) => [...acc, item], []);
|
95 | const defaultPatchLabelName = this.getFirstLabelNameFromLabelKey(this.options.labels, 'patch');
|
96 | commits
|
97 | .filter(({ labels }) =>
|
98 |
|
99 | !sections.some(section => labels.includes(section.name)) ||
|
100 |
|
101 | (labels[0] === 'released' && labels.length === 1))
|
102 | .map(({ labels }) => labels.push(defaultPatchLabelName));
|
103 | return Object.assign({}, ...sections.map(label => {
|
104 | const matchedCommits = filterLabel(currentCommits, label.name);
|
105 | currentCommits = currentCommits.filter(commit => !matchedCommits.includes(commit));
|
106 | return matchedCommits.length === 0
|
107 | ? {}
|
108 | : { [label.name]: matchedCommits };
|
109 | }));
|
110 | }
|
111 |
|
112 | getFirstLabelNameFromLabelKey(labels, labelKey) {
|
113 | var _a;
|
114 | return ((_a = labels.find(l => l.releaseType === labelKey)) === null || _a === void 0 ? void 0 : _a.name) || labelKey;
|
115 | }
|
116 |
|
117 | async createUserLinkList(commit) {
|
118 | const result = new Set();
|
119 | await Promise.all(commit.authors.map(async (rawAuthor) => {
|
120 | const data = this.authors.find(([, commitAuthor]) => (commitAuthor.name &&
|
121 | rawAuthor.name &&
|
122 | commitAuthor.name === rawAuthor.name) ||
|
123 | (commitAuthor.email &&
|
124 | rawAuthor.email &&
|
125 | commitAuthor.email === rawAuthor.email) ||
|
126 | (commitAuthor.username &&
|
127 | rawAuthor.username &&
|
128 | commitAuthor.username === rawAuthor.username)) || [{}, rawAuthor];
|
129 | const link = await this.hooks.renderChangelogAuthor.promise(data[1], commit, this.options);
|
130 | if (link) {
|
131 | result.add(link);
|
132 | }
|
133 | }));
|
134 | return [...result].join(' ');
|
135 | }
|
136 |
|
137 | async generateCommitNote(commit) {
|
138 | var _a;
|
139 | const subject = commit.subject ? commit.subject.trim() : '';
|
140 | let pr = '';
|
141 | if ((_a = commit.pullRequest) === null || _a === void 0 ? void 0 : _a.number) {
|
142 | const prLink = url_join_1.default(this.options.baseUrl, 'pull', commit.pullRequest.number.toString());
|
143 | pr = `[#${commit.pullRequest.number}](${prLink})`;
|
144 | }
|
145 | const user = await this.createUserLinkList(commit);
|
146 | return `- ${subject} ${pr}${user ? ` (${user})` : ''}`;
|
147 | }
|
148 |
|
149 | getAllAuthors(split) {
|
150 | const commits = Object.values(split).reduce((labeledCommits, sectionCommits) => [...labeledCommits, ...sectionCommits], []);
|
151 | return commits
|
152 | .map(commit => commit.authors
|
153 | .filter(author => author.username !== 'invalid-email-address' &&
|
154 | (author.name || author.email || author.username))
|
155 | .map(author => [commit, author]))
|
156 | .reduce((all, more) => [...all, ...more], [])
|
157 | .sort(a => ('id' in a[1] ? 0 : 1));
|
158 | }
|
159 |
|
160 | async createAuthorSection(sections) {
|
161 | const authors = new Set();
|
162 | const authorsWithFullData = this.authors.map(([, author]) => author).filter(author => 'id' in author);
|
163 | await Promise.all(this.authors.map(async ([commit, author]) => {
|
164 | const info = authorsWithFullData.find(u => (author.name && u.name === author.name) ||
|
165 | (author.email && u.email === author.email)) || author;
|
166 | const user = await this.hooks.renderChangelogAuthor.promise(info, commit, this.options);
|
167 | const authorEntry = await this.hooks.renderChangelogAuthorLine.promise(info, user);
|
168 | if (authorEntry && !authors.has(authorEntry)) {
|
169 | authors.add(authorEntry);
|
170 | }
|
171 | }));
|
172 | if (authors.size === 0) {
|
173 | return;
|
174 | }
|
175 | let authorSection = `#### Authors: ${authors.size}\n\n`;
|
176 | authorSection += [...authors].sort((a, b) => a.localeCompare(b)).join('\n');
|
177 | sections.push(authorSection);
|
178 | }
|
179 |
|
180 | async createLabelSection(split, sections) {
|
181 | const changelogTitles = this.options.labels.reduce((titles, label) => {
|
182 | if (label.changelogTitle) {
|
183 | titles[label.name] = label.changelogTitle;
|
184 | }
|
185 | return titles;
|
186 | }, {});
|
187 | const labelSections = await Promise.all(Object.entries(split).map(async ([label, labelCommits]) => {
|
188 | const title = await this.hooks.renderChangelogTitle.promise(label, changelogTitles);
|
189 | const lines = new Set();
|
190 | await Promise.all(labelCommits.map(async (commit) => {
|
191 | var _a;
|
192 | const base = ((_a = commit.pullRequest) === null || _a === void 0 ? void 0 : _a.base) || '';
|
193 | const branch = base.includes('/') ? base.split('/')[1] : base;
|
194 |
|
195 |
|
196 | if (branch && this.options.prereleaseBranches.includes(branch)) {
|
197 | return true;
|
198 | }
|
199 | const [, line] = await this.hooks.renderChangelogLine.promise([
|
200 | commit,
|
201 | await this.generateCommitNote(commit)
|
202 | ]);
|
203 | lines.add(line);
|
204 | }));
|
205 | return [
|
206 | title,
|
207 | [...lines].sort((a, b) => a.split('\n').length - b.split('\n').length)
|
208 | ];
|
209 | }));
|
210 | const mergedSections = labelSections.reduce((acc, [title, commits]) => (Object.assign(Object.assign({}, acc), { [title]: [...(acc[title] || []), ...commits] })), {});
|
211 | Object.entries(mergedSections)
|
212 | .map(([title, lines]) => [title, ...lines].join('\n'))
|
213 | .map(section => sections.push(section));
|
214 | }
|
215 |
|
216 | async createReleaseNotesSection(commits, sections) {
|
217 | if (!commits.length) {
|
218 | return;
|
219 | }
|
220 | let section = '';
|
221 | const visited = new Set();
|
222 | const included = await Promise.all(commits.map(async (commit) => {
|
223 | const omit = await this.hooks.omitReleaseNotes.promise(commit);
|
224 | if (!omit) {
|
225 | return commit;
|
226 | }
|
227 | }));
|
228 | included.forEach(commit => {
|
229 | if (!commit) {
|
230 | return;
|
231 | }
|
232 | const pr = commit.pullRequest;
|
233 | if (!pr || !pr.body) {
|
234 | return;
|
235 | }
|
236 | const title = /^[#]{0,5}[ ]*[R|r]elease [N|n]otes$/;
|
237 | const lines = pr.body.split('\n').map(line => line.replace(/\r$/, ''));
|
238 | const notesStart = lines.findIndex(line => Boolean(line.match(title)));
|
239 | if (notesStart === -1 || visited.has(pr.number)) {
|
240 | return;
|
241 | }
|
242 | const depth = getHeaderDepth(lines[notesStart]);
|
243 | visited.add(pr.number);
|
244 | let notes = '';
|
245 | for (let index = notesStart; index < lines.length; index++) {
|
246 | const line = lines[index];
|
247 | const isTitle = line.match(title);
|
248 | if (line.startsWith('#') && getHeaderDepth(line) <= depth && !isTitle) {
|
249 | break;
|
250 | }
|
251 | if (!isTitle) {
|
252 | notes += `${line}\n`;
|
253 | }
|
254 | }
|
255 | section += `_From #${pr.number}_\n\n${notes.trim()}\n\n`;
|
256 | });
|
257 | if (!section) {
|
258 | return;
|
259 | }
|
260 | sections.push(`### Release Notes\n\n${section}---`);
|
261 | }
|
262 | }
|
263 | exports.default = Changelog;
|
264 |
|
\ | No newline at end of file |