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