UNPKG

5.99 kBJavaScriptView Raw
1/**
2 * Copyright IBM Corp. 2019, 2019
3 *
4 * This source code is licensed under the Apache-2.0 license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8'use strict';
9
10const parse = require('@commitlint/parse');
11const execa = require('execa');
12
13// We keep a list of commits that are process-oriented that we never want to
14// show up in generated changelogs
15const headerDenyList = new Set([
16 'chore(project): sync generated files [skip ci]',
17 'chore(release): update package versions',
18]);
19
20/**
21 * @typedef PackageInfo
22 * @property {string} name
23 * @property {string} version
24 * @property {boolean} private
25 * @property {string} location
26 */
27
28/**
29 * Generate the contents of a CHANGELOG for a given list of packages between two
30 * tagged git objects. For example, if updating from v1.0.0 to v1.1.0, this
31 * method would generate a changelog for all commits in the range
32 * v1.0.0...v1.1.0 and group them based on the given packages.
33 *
34 * @param {Array<PackageInfo>} packages
35 * @param {string} lastTag
36 * @param {string} latestTag
37 * @returns {string}
38 */
39async function generate(packages, lastTag, latestTag) {
40 const packageCommitsInRange = await Promise.all(
41 packages.map(pkg => getCommitsInRange(pkg, `${lastTag}...${latestTag}`))
42 );
43 const packageCommitsToInclude = packageCommitsInRange.filter(
44 ({ commits }) => {
45 return commits.length > 0;
46 }
47 );
48
49 return [
50 getMarkdownTitle(lastTag, latestTag),
51 ...getMarkdownSections(packageCommitsToInclude),
52 ].join('\n');
53}
54
55const sectionTypes = [
56 {
57 title: 'New features :rocket:',
58 types: ['feat'],
59 },
60 {
61 title: 'Bug fixes :bug:',
62 types: ['fix'],
63 },
64 {
65 title: 'Documentation :memo:',
66 types: ['docs'],
67 },
68 {
69 title: 'Housekeeping :house:',
70 types: ['build', 'ci', 'chore', 'perf', 'refactor', 'revert', 'test'],
71 },
72];
73
74const commitUrl = 'https://github.com/carbon-design-system/carbon/commit';
75
76/**
77 * Get the sections to be rendered in our changelog for the given packages and
78 * their associated commits. Our plan is to group the commits for each package
79 * under various section types and build up the lists accordingly.
80 *
81 * @param {Array} packages
82 * @returns {Array}
83 */
84function getMarkdownSections(packages) {
85 return packages.map(({ name, version, commits }) => {
86 let section = `## \`${name}@${version}\`\n`;
87
88 for (const { title, types } of sectionTypes) {
89 const commitsForSection = commits.filter(commit => {
90 return types.includes(commit.info.type);
91 });
92
93 if (commitsForSection.length === 0) {
94 continue;
95 }
96
97 let subsection = `### ${title}\n`;
98
99 for (const commit of commitsForSection) {
100 const { hash, info } = commit;
101 const url = `${commitUrl}/${hash}`;
102 subsection += `- ${info.header} ([\`${hash}\`](${url}))\n`;
103 }
104
105 section += '\n' + subsection;
106 }
107
108 return section;
109 });
110}
111
112const compareUrl = 'https://github.com/carbon-design-system/carbon/compare';
113
114/**
115 * Get the markdown title for the given lastTag and latestTag. This title is
116 * used at the top of a given GitHub release.
117 *
118 * @param {string} lastTag
119 * @param {string} latestTag
120 * @returns {string}
121 */
122function getMarkdownTitle(lastTag, latestTag) {
123 const now = new Date();
124 const year = now.getFullYear();
125 const day = now.getDate();
126 const month = now.getMonth() + 1;
127 const url = `${compareUrl}/${lastTag}...${latestTag}`;
128
129 return `# [${latestTag}](${url}) (${year}-${month}-${day})`;
130}
131
132/**
133 * Retrieves the commits from the given range for the particular package.
134 * Specifically, we're going to find commits that are in the package folder so
135 * that we can group them later on when generating the changelog.
136 *
137 * @param {PackageInfo} pkg
138 * @param {string} range
139 * @returns {Array}
140 */
141async function getCommitsInRange(pkg, range) {
142 // Using the `rev-list` subcommand of `git` we can list out all of the commits
143 // for the given range inside of the package's location. This will allow us to
144 // find all the commits associated with this package that we'll display in the
145 // changelog
146 const { stdout } = await execa('git', [
147 'rev-list',
148 range,
149 '--oneline',
150 '--',
151 pkg.location,
152 ]);
153
154 // If the git sub-command returns nothing, then no commits have occurred for
155 // this package in the given commit rnage
156 if (stdout === '') {
157 return {
158 ...pkg,
159 commits: [],
160 };
161 }
162
163 const commitsInFolder = await Promise.all(
164 stdout.split('\n').map(async commit => {
165 // The output from `git rev-list` follows the pattern: `HASH <header>`, so
166 // we will need to trim the string to get the appropriate hash and text
167 // values for `parse` to consume.
168 const hash = commit.slice(0, 9);
169 const text = commit.slice(10);
170 // Just in case, we'll trim the header value as sometimes the commit gets
171 // an extra space that's unnecessary
172 const info = await parse(text.trim());
173
174 return {
175 info,
176 hash,
177 text,
178 };
179 })
180 );
181
182 const headers = new Set();
183 // There are certain conditions where we don't want a commit visible in the
184 // CHANGELOG, namely when we cannot parse the commit info type or when the
185 // commit header is in our deny list
186 const commits = commitsInFolder
187 .filter(commit => {
188 if (commit.info.type === null) {
189 return false;
190 }
191
192 if (headerDenyList.has(commit.info.header)) {
193 return false;
194 }
195
196 return true;
197 })
198 .filter(commit => {
199 // Running into an issue with duplicate headers when viewing "commited by"
200 // and "commited and authored by", as a result we'll keep a set of all
201 // headers and exclude the commit if we've seen it already
202 if (headers.has(commit.info.header)) {
203 return false;
204 }
205 headers.add(commit.info.header);
206 return true;
207 });
208
209 return {
210 ...pkg,
211 commits,
212 };
213}
214
215module.exports = {
216 generate,
217};