1/*
2 * Copyright (c) 2021, Arm Limited. All rights reserved.
3 *
4 * SPDX-License-Identifier: BSD-3-Clause
5 */
6
7/* eslint-env es6 */
8
9"use strict";
10
11const Handlebars = require("handlebars");
12const Q = require("q");
13const _ = require("lodash");
14
15const ccConventionalChangelog = require("conventional-changelog-conventionalcommits/conventional-changelog");
16const ccParserOpts = require("conventional-changelog-conventionalcommits/parser-opts");
17const ccRecommendedBumpOpts = require("conventional-changelog-conventionalcommits/conventional-recommended-bump");
18const ccWriterOpts = require("conventional-changelog-conventionalcommits/writer-opts");
19
20const execa = require("execa");
21
22const readFileSync = require("fs").readFileSync;
23const resolve = require("path").resolve;
24
25/*
26 * Register a Handlebars helper that lets us generate Markdown lists that can support multi-line
27 * strings. This is driven by inconsistent formatting of breaking changes, which may be multiple
28 * lines long and can terminate the list early unintentionally.
29 */
30Handlebars.registerHelper("tf-a-mdlist", function (indent, options) {
31    const spaces = new Array(indent + 1).join(" ");
32    const first = spaces + "- ";
33    const nth = spaces + "  ";
34
35    return first + options.fn(this).replace(/\n(?!\s*\n)/gm, `\n${nth}`).trim() + "\n";
36});
37
38/*
39 * Register a Handlebars helper that concatenates multiple variables. We use this to generate the
40 * title for the section partials.
41 */
42Handlebars.registerHelper("tf-a-concat", function () {
43    let argv = Array.prototype.slice.call(arguments, 0);
44
45    argv.pop();
46
47    return argv.join("");
48});
49
50function writerOpts(config) {
51    /*
52     * Flatten the configuration's sections list. This helps us iterate over all of the sections
53     * when we don't care about the hierarchy.
54     */
55
56    const flattenSections = function (sections) {
57        return sections.flatMap(section => {
58            const subsections = flattenSections(section.sections || []);
59
60            return [section].concat(subsections);
61        })
62    };
63
64    const flattenedSections = flattenSections(config.sections);
65
66    /*
67     * Register a helper to return a restructured version of the note groups that includes notes
68     * categorized by their section.
69     */
70    Handlebars.registerHelper("tf-a-notes", function (noteGroups, options) {
71        const generateTemplateData = function (sections, notes) {
72            return (sections || []).flatMap(section => {
73                const templateData = {
74                    title: section.title,
75                    sections: generateTemplateData(section.sections, notes),
76                    notes: notes.filter(note => section.scopes?.includes(note.commit.scope)),
77                };
78
79                /*
80                 * Don't return a section if it contains no notes and no sub-sections.
81                 */
82                if ((templateData.sections.length == 0) && (templateData.notes.length == 0)) {
83                    return [];
84                }
85
86                return [templateData];
87            });
88        };
89
90        return noteGroups.map(noteGroup => {
91            return {
92                title: noteGroup.title,
93                sections: generateTemplateData(config.sections, noteGroup.notes),
94                notes: noteGroup.notes.filter(note =>
95                    !flattenedSections.some(section => section.scopes?.includes(note.commit.scope))),
96            };
97        });
98    });
99
100    /*
101     * Register a helper to return a restructured version of the commit groups that includes commits
102     * categorized by their section.
103     */
104    Handlebars.registerHelper("tf-a-commits", function (commitGroups, options) {
105        const generateTemplateData = function (sections, commits) {
106            return (sections || []).flatMap(section => {
107                const templateData = {
108                    title: section.title,
109                    sections: generateTemplateData(section.sections, commits),
110                    commits: commits.filter(commit => section.scopes?.includes(commit.scope)),
111                };
112
113                /*
114                 * Don't return a section if it contains no notes and no sub-sections.
115                 */
116                if ((templateData.sections.length == 0) && (templateData.commits.length == 0)) {
117                    return [];
118                }
119
120                return [templateData];
121            });
122        };
123
124        return commitGroups.map(commitGroup => {
125            return {
126                title: commitGroup.title,
127                sections: generateTemplateData(config.sections, commitGroup.commits),
128                commits: commitGroup.commits.filter(commit =>
129                    !flattenedSections.some(section => section.scopes?.includes(commit.scope))),
130            };
131        });
132    });
133
134    const writerOpts = ccWriterOpts(config)
135        .then(writerOpts => {
136            const ccWriterOptsTransform = writerOpts.transform;
137
138            /*
139             * These configuration properties can't be injected directly into the template because
140             * they themselves are templates. Instead, we register them as partials, which allows
141             * them to be evaluated as part of the templates they're used in.
142             */
143            Handlebars.registerPartial("commitUrl", config.commitUrlFormat);
144            Handlebars.registerPartial("compareUrl", config.compareUrlFormat);
145            Handlebars.registerPartial("issueUrl", config.issueUrlFormat);
146
147            /*
148             * Register the partials that allow us to recursively create changelog sections.
149             */
150
151            const notePartial = readFileSync(resolve(__dirname, "./templates/note.hbs"), "utf-8");
152            const noteSectionPartial = readFileSync(resolve(__dirname, "./templates/note-section.hbs"), "utf-8");
153            const commitSectionPartial = readFileSync(resolve(__dirname, "./templates/commit-section.hbs"), "utf-8");
154
155            Handlebars.registerPartial("tf-a-note", notePartial);
156            Handlebars.registerPartial("tf-a-note-section", noteSectionPartial);
157            Handlebars.registerPartial("tf-a-commit-section", commitSectionPartial);
158
159            /*
160             * Override the base templates so that we can generate a changelog that looks at least
161             * similar to the pre-Conventional Commits TF-A changelog.
162             */
163            writerOpts.mainTemplate = readFileSync(resolve(__dirname, "./templates/template.hbs"), "utf-8");
164            writerOpts.headerPartial = readFileSync(resolve(__dirname, "./templates/header.hbs"), "utf-8");
165            writerOpts.commitPartial = readFileSync(resolve(__dirname, "./templates/commit.hbs"), "utf-8");
166            writerOpts.footerPartial = readFileSync(resolve(__dirname, "./templates/footer.hbs"), "utf-8");
167
168            writerOpts.transform = function (commit, context) {
169                /*
170                 * Fix up commit trailers, which for some reason are not correctly recognized and
171                 * end up showing up in the breaking changes.
172                 */
173
174                commit.notes.forEach(note => {
175                    const trailers = execa.sync("git", ["interpret-trailers", "--parse"], {
176                        input: note.text
177                    }).stdout;
178
179                    note.text = note.text.replace(trailers, "").trim();
180                });
181
182                return ccWriterOptsTransform(commit, context);
183            };
184
185            return writerOpts;
186        });
187
188    return writerOpts;
189}
190
191module.exports = function (parameter) {
192    const config = parameter || {};
193
194    return Q.all([
195        ccConventionalChangelog(config),
196        ccParserOpts(config),
197        ccRecommendedBumpOpts(config),
198        writerOpts(config)
199    ]).spread((
200        conventionalChangelog,
201        parserOpts,
202        recommendedBumpOpts,
203        writerOpts
204    ) => {
205        if (_.isFunction(parameter)) {
206            return parameter(null, {
207                gitRawCommitsOpts: { noMerges: null },
208                conventionalChangelog,
209                parserOpts,
210                recommendedBumpOpts,
211                writerOpts
212            });
213        } else {
214            return {
215                conventionalChangelog,
216                parserOpts,
217                recommendedBumpOpts,
218                writerOpts
219            };
220        }
221    });
222};
223