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