Skip to content

Commit

Permalink
feat(version): use conventional commit changelog writer for perf
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Jul 30, 2022
1 parent 911b9b5 commit e9d7c52
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 127 deletions.
4 changes: 4 additions & 0 deletions packages/core/package.json
Expand Up @@ -40,6 +40,8 @@
"config-chain": "^1.1.13",
"conventional-changelog-angular": "^5.0.13",
"conventional-changelog-core": "^4.2.4",
"conventional-changelog-writer": "^5.0.1",
"conventional-commits-parser": "^3.2.4",
"conventional-recommended-bump": "^6.1.0",
"cosmiconfig": "^7.0.1",
"dedent": "^0.7.0",
Expand Down Expand Up @@ -89,6 +91,8 @@
"@types/async": "^3.2.15",
"@types/clone-deep": "^4.0.1",
"@types/conventional-changelog-core": "^4.2.1",
"@types/conventional-changelog-writer": "^4.0.1",
"@types/conventional-commits-parser": "^3.0.2",
"@types/conventional-recommended-bump": "^6.1.0",
"@types/dedent": "^0.7.0",
"@types/execa": "^2.0.0",
Expand Down
Expand Up @@ -726,7 +726,7 @@ describe('conventional-commits', () => {

const opts = {
changelogPreset: 'conventional-changelog-angular',
changelogIncludeCommitsGitAuthor: ' by (**%a**)',
changelogIncludeCommitsGitAuthor: ' by **%a** (%e)',
};
const [changelogOne, changelogTwo] = await Promise.all([
updateChangelog(pkg1, 'independent', opts),
Expand All @@ -739,15 +739,15 @@ describe('conventional-commits', () => {
### Bug Fixes
* **stuff:** changed ([SHA](https://github.com/lerna/conventional-commits-independent/commit/GIT_HEAD)) by (**Tester McPerson**)
* **stuff:** changed ([SHA](https://github.com/lerna/conventional-commits-independent/commit/GIT_HEAD)) by **Tester McPerson** (test@example.com)
`);
expect(changelogTwo.newEntry.trimRight()).toMatchInlineSnapshot(`
# [1.1.0](/compare/package-2@1.0.0...package-2@1.1.0) (YYYY-MM-DD)
### Features
* **thing:** added ([SHA](https://github.com/lerna/conventional-commits-independent/commit/GIT_HEAD)) by (**Tester McPerson**)
* **thing:** added ([SHA](https://github.com/lerna/conventional-commits-independent/commit/GIT_HEAD)) by **Tester McPerson** (test@example.com)
`);
});

Expand Down Expand Up @@ -787,7 +787,7 @@ describe('conventional-commits', () => {
};
const opt2s = {
changelogPreset: 'conventional-changelog-angular',
changelogIncludeCommitsClientLogin: ' by (@%l, %a)',
changelogIncludeCommitsClientLogin: ' from @%l, _%a (%e)_',
commitsSinceLastRelease: [
{
authorName: 'Tester McPerson',
Expand Down Expand Up @@ -817,7 +817,7 @@ describe('conventional-commits', () => {
### Features
* **thing:** added ([SHA](https://github.com/lerna/conventional-commits-independent/commit/GIT_HEAD)) by (@tester-mcperson, Tester McPerson)
* **thing:** added ([SHA](https://github.com/lerna/conventional-commits-independent/commit/GIT_HEAD)) from @tester-mcperson, _Tester McPerson (test@example.com)_
`);
});
});
Expand Down
20 changes: 11 additions & 9 deletions packages/core/src/conventional-commits/get-changelog-config.ts
Expand Up @@ -2,16 +2,20 @@ import log from 'npmlog';
import pify from 'pify';
import npa from 'npm-package-arg';

import { ChangelogConfig, ChangelogPresetConfig } from '../models';
import { ValidationError } from '../validation-error';

export class GetChangelogConfig {
static cfgCache = new Map<string, any>();

static isFunction(config) {
static isFunction(config: ChangelogConfig) {
return Object.prototype.toString.call(config) === '[object Function]';
}

static resolveConfigPromise(presetPackageName: string, presetConfig: any) {
static resolveConfigPromise(
presetPackageName: string,
presetConfig: ChangelogPresetConfig
): Promise<ChangelogConfig> {
log.verbose('getChangelogConfig', 'Attempting to resolve preset %j', presetPackageName);

let config = require(presetPackageName);
Expand All @@ -32,19 +36,17 @@ export class GetChangelogConfig {
}

/**
* @param {import('..').ChangelogPresetConfig} [changelogPreset]
* @param {ChangelogPresetConfig} [changelogPreset]
* @param {string} [rootPath]
*/
static getChangelogConfig(
changelogPreset: string | { name: string } = 'conventional-changelog-angular',
changelogPreset: ChangelogPresetConfig = 'conventional-changelog-angular',
rootPath?: string
) {
): Promise<ChangelogConfig> {
const presetName = typeof changelogPreset === 'string' ? changelogPreset : changelogPreset.name;
const presetConfig = typeof changelogPreset === 'object' ? changelogPreset : {};

const presetConfig = typeof changelogPreset === 'object' ? changelogPreset : ({} as ChangelogPresetConfig);
const cacheKey = `${presetName}${presetConfig ? JSON.stringify(presetConfig) : ''}`;

let config = GetChangelogConfig.cfgCache.get(cacheKey);
let config = GetChangelogConfig.cfgCache.get(cacheKey) as Promise<ChangelogConfig>;

if (!config) {
let presetPackageName = presetName;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/conventional-commits/index.ts
Expand Up @@ -6,3 +6,4 @@ export * from './make-bump-only-filter';
export * from './read-existing-changelog';
export * from './recommend-version';
export * from './update-changelog';
export * from './writer-opts-transform';
115 changes: 15 additions & 100 deletions packages/core/src/conventional-commits/update-changelog.ts
@@ -1,4 +1,5 @@
import conventionalChangelogCore from 'conventional-changelog-core';
import conventionalChangelogCore, { Context } from 'conventional-changelog-core';
import { Options as WriterOptions } from 'conventional-changelog-writer';
import fs from 'fs-extra';
import getStream from 'get-stream';
import log from 'npmlog';
Expand All @@ -7,8 +8,9 @@ import { BLANK_LINE, CHANGELOG_HEADER, EOL } from './constants';
import { GetChangelogConfig } from './get-changelog-config';
import { makeBumpOnlyFilter } from './make-bump-only-filter';
import { readExistingChangelog } from './read-existing-changelog';
import { ChangelogType, RemoteCommit, UpdateChangelogOption } from '../models';
import { ChangelogConfig, ChangelogType, UpdateChangelogOption } from '../models';
import { Package } from '../package';
import { setConfigChangelogCommitClientLogin, setConfigChangelogCommitGitAuthor } from './writer-opts-transform';

/**
* Update changelog with the commits of the new release
Expand All @@ -32,28 +34,28 @@ export async function updateChangelog(pkg: Package, type: ChangelogType, updateO
} = updateOptions;

const config = await GetChangelogConfig.getChangelogConfig(changelogPreset, rootPath);
const options: any = {};
const context: any = {}; // pass as positional because cc-core's merge-config is wack
const options = {} as { config: ChangelogConfig; lernaPackage: string; tagPrefix: string; pkg: { path: string } };
const context = {} as Context; // pass as positional because cc-core's merge-config is wack
const writerOpts = {} as WriterOptions;

// cc-core mutates input :P
if (config.conventionalChangelog) {
// "new" preset API
options.config = Object.assign({}, config.conventionalChangelog);
options.config = Object.assign({}, config.conventionalChangelog) as ChangelogConfig;
} else {
// "old" preset API
options.config = Object.assign({}, config);
options.config = Object.assign({}, config) as ChangelogConfig;
}

// NOTE: must pass as positional argument due to weird bug in merge-config
const gitRawCommitsOpts = Object.assign({}, options.config.gitRawCommitsOpts);

// when including commit author's name, we need to change the conventional commit format
// available formats can be found at Git's url: https://git-scm.com/docs/git-log#_pretty_formats
// we will later extract a defined token from the string, of ">>author=%an<<",
// and reformat the string to get a commit string that would add (@authorName) to the end of the commit string, ie:
// **deps:** update all non-major dependencies ([ed1db35](https://github.com/.../ed1db35)) (@Renovate-Bot)
// are we including commit author name/email or commit client login name
if (changelogIncludeCommitsGitAuthor) {
gitRawCommitsOpts.format = '%B%n-hash-%n%H>>author=%an<<';
setConfigChangelogCommitGitAuthor(config, gitRawCommitsOpts, writerOpts, changelogIncludeCommitsGitAuthor);
} else if (changelogIncludeCommitsClientLogin && commitsSinceLastRelease) {
// prettier-ignore
setConfigChangelogCommitClientLogin(config, gitRawCommitsOpts, writerOpts, commitsSinceLastRelease, changelogIncludeCommitsClientLogin);
}

if (type === 'root') {
Expand Down Expand Up @@ -82,7 +84,7 @@ export async function updateChangelog(pkg: Package, type: ChangelogType, updateO
}

// generate the markdown for the upcoming release.
const changelogStream = conventionalChangelogCore(options, context, gitRawCommitsOpts);
const changelogStream = conventionalChangelogCore(options, context, gitRawCommitsOpts, undefined, writerOpts);

return Promise.all([
// prettier-ignore
Expand All @@ -91,17 +93,6 @@ export async function updateChangelog(pkg: Package, type: ChangelogType, updateO
]).then(([inputEntry, [changelogFileLoc, changelogContents]]) => {
let newEntry = inputEntry;

// include commit author name or commit client login name
if (changelogIncludeCommitsGitAuthor) {
newEntry = parseChangelogCommitAuthorFullName(inputEntry, changelogIncludeCommitsGitAuthor);
} else if (changelogIncludeCommitsClientLogin && commitsSinceLastRelease) {
newEntry = parseChangelogCommitClientLogin(
inputEntry,
commitsSinceLastRelease,
changelogIncludeCommitsClientLogin
);
}

log.silly(type, 'writing new entry: %j', newEntry);

const changelogVersion = type === 'root' ? changelogVersionMessage : '';
Expand All @@ -125,79 +116,3 @@ export async function updateChangelog(pkg: Package, type: ChangelogType, updateO
});
});
}

/**
* From an input entry string that most often, not always, include commit author's name within defined tokens ">>author=AUTHOR_NAME<<"
* We will want to extract the author's name from the commit url and recreate the commit url string and add its author to the end of the string.
* You might be wondering, WHY is the commit author part of the commit url?
* Mainly because it seems that adding a `format` to the `conventional-changelog-core` of `gitRawCommitsOpts`
* will always include it as part of the final commit url because of this line where it parses the template and always seems to include whatever we add into the commit url
* https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/git-raw-commits/index.js#L27
*
* We will transform a string that looks like this:
* "deps: update all non-major dependencies ([ed1db35](https://github.com/.../ed1db35>>author=Whitesource Renovate<<))"
* then extract the commit author's name and transform it into a new string that will look like below
* "deps: update all non-major dependencies ([ed1db35](https://github.com/.../ed1db35)) (Whitesource Whitesource)"
* @param {String} changelogEntry - changelog entry of a version being released which can contain multiple line entries
* @param {String | Boolean} [commitCustomFormat]
* @returns
*/
function parseChangelogCommitAuthorFullName(changelogEntry: string, commitCustomFormat?: string | boolean) {
// to transform the string into what we want, we need to move the substring outside of the url and remove extra search tokens
// from this:
// "...ed1db35>>author=Whitesource Renovate<<))"
// into this:
// "...ed1db35)) (Whitesource Renovate)"
// or as a custom message like this " by **%a**" into this:
// "...ed1db35)) by **Whitesource Renovate**"
return changelogEntry.replace(
/(.*)(>>author=)(.*)(<<)(.*)/g,
(_: string, lineStart: string, _tokenStart?: string, authorName?: string, _tokenEnd?: string, lineEnd?: string) => {
// rebuild the commit line entry string
const commitMsg = `${lineStart}${lineEnd || ''}`;
const authorMsg =
typeof commitCustomFormat === 'string'
? commitCustomFormat.replace(/%a/g, authorName || '')
: ` (${authorName})`;
return commitMsg + authorMsg;
}
);
}

/**
* For each commit line entry, we will append the remote client login username for the first commit entry found, for example:
* "commit message ([ed1db35](https://github.com/.../ed1db35)) (@renovate-bot)"
* @param {String} changelogEntry - changelog entry of a version being released which can contain multiple line entries
* @param {Array<RemoteCommit>} commitsSinceLastRelease
* @param {String | Boolean} [commitCustomFormat]
* @returns
*/
function parseChangelogCommitClientLogin(
changelogEntry: string,
commitsSinceLastRelease: RemoteCommit[],
commitCustomFormat?: string | boolean
) {
const entriesOutput: string[] = [];

for (const lineEntry of changelogEntry.split('\n')) {
let lineEntryOutput = lineEntry;
const [_, __, shortSha] = lineEntry.match(/(\[([0-9a-f]{7})\])/) || []; // pull first commit match only

if (shortSha) {
const remoteCommit = commitsSinceLastRelease.find((c) => c.shortHash === shortSha);
if (remoteCommit) {
const clientLogin =
typeof commitCustomFormat === 'string'
? commitCustomFormat.replace(/%l/g, remoteCommit.login || '').replace(/%a/g, remoteCommit.authorName || '')
: ` (@${remoteCommit.login})`;

// when we have a match, we need to remove any line breaks at the line ending only,
// then add our user info and finally add back a single line break
lineEntryOutput = lineEntry.replace(/\n*$/, '') + clientLogin;
}
}
entriesOutput.push(lineEntryOutput);
}

return entriesOutput.join('\n');
}
103 changes: 103 additions & 0 deletions packages/core/src/conventional-commits/writer-opts-transform.ts
@@ -0,0 +1,103 @@
import { Context, GitRawCommitsOptions } from 'conventional-changelog-core';
import { Options as WriterOptions } from 'conventional-changelog-writer';
import { Commit } from 'conventional-commits-parser';
import { ChangelogConfig, RemoteCommit } from '../models';

const GIT_COMMIT_WITH_AUTHOR_FORMAT =
'%B%n-hash-%n%H%n-gitTags-%n%d%n-committerDate-%n%ci%n-authorName-%n%an%n-authorEmail-%n%ae%n-gpgStatus-%n%G?%n-gpgSigner-%n%GS';

/**
* Change the changelog config, we need to update the default format to include commit author name/email,
* available formats can be found at Git's url: https://git-scm.com/docs/git-log#_pretty_formats
* Add a `format` to the `conventional-changelog-core` of `gitRawCommitsOpts` will make it available in the commit template
* https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/git-raw-commits/index.js#L27
* then no matter which changelog preset is loaded, we'll append the git author name to the commit template
* ie:: **deps:** update all non-major dependencies ([ed1db35](https://github.com/.../ed1db35)) (Renovate Bot)
* @param {ChangelogConfig} config
* @param {GitRawCommitsOptions} gitRawCommitsOpts
* @param {WriterOptions} writerOpts
* @param {string | boolean} [commitCustomFormat]
*/
export function setConfigChangelogCommitGitAuthor(
config: ChangelogConfig,
gitRawCommitsOpts: GitRawCommitsOptions,
writerOpts: WriterOptions,
commitCustomFormat?: string | boolean
) {
gitRawCommitsOpts.format = GIT_COMMIT_WITH_AUTHOR_FORMAT;
const extraCommitMsg =
typeof commitCustomFormat === 'string'
? commitCustomFormat.replace(/%a/g, '{{authorName}}' || '').replace(/%e/g, '{{authorEmail}}' || '')
: `({{authorName}})`;
writerOpts.commitPartial =
config.writerOpts.commitPartial!.replace(/\n*$/, '') + ` {{#if @root.linkReferences~}}${extraCommitMsg}{{~/if}}\n`;
}

/**
* Change the changelog config, we need to update the default format to include commit author name/email,
* available formats can be found at Git's url: https://git-scm.com/docs/git-log#_pretty_formats
* We also need to change the transform function and add remote client login (GitHub)
* Add a `format` to the `conventional-changelog-core` of `gitRawCommitsOpts` will make it available in the commit template
* https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/git-raw-commits/index.js#L27
* then no matter which changelog preset is loaded, we'll append the git author name to the commit template
* ie:: **deps:** update all non-major dependencies ([ed1db35](https://github.com/.../ed1db35)) (@renovate-bot)
* @param {ChangelogConfig} config
* @param {GitRawCommitsOptions} gitRawCommitsOpts
* @param {WriterOptions} writerOpts
* @param {RemoteCommit[]} commitsSinceLastRelease
* @param {string | boolean} [commitCustomFormat]
*/
export function setConfigChangelogCommitClientLogin(
config: ChangelogConfig,
gitRawCommitsOpts: GitRawCommitsOptions,
writerOpts: WriterOptions,
commitsSinceLastRelease: RemoteCommit[],
commitCustomFormat?: string | boolean
) {
gitRawCommitsOpts.format = GIT_COMMIT_WITH_AUTHOR_FORMAT;
const extraCommitMsg =
typeof commitCustomFormat === 'string'
? commitCustomFormat
.replace(/%a/g, '{{authorName}}' || '')
.replace(/%e/g, '{{authorEmail}}' || '')
.replace(/%l/g, '{{userLogin}}' || '')
: `(@{{userLogin}})`;
writerOpts.commitPartial =
config.writerOpts.commitPartial!.replace(/\n*$/, '') + ` {{#if @root.linkReferences~}}${extraCommitMsg}{{~/if}}\n`;

// add commits since last release inte the transform function
writerOpts.transform = writerOptsTransform.bind(
null,
config.writerOpts.transform as (cmt: Commit, ctx: Context) => Commit,
commitsSinceLastRelease
);
}

/**
* Extend the writerOpts transform function from whichever preset config is loaded
* We will execute the original writerOpts transform function, then from it we'll add extra properties to the commit object
* @param {Transform} originalTransform
* @param {RemoteCommit[]} commitsSinceLastRelease
* @param {Commit} commit
* @param {Context} context
* @returns
*/
export function writerOptsTransform(
originalTransform: (cmt: Commit, ctx: Context) => Commit,
commitsSinceLastRelease: RemoteCommit[],
commit: Commit,
context: Context
) {
// execute original writerOpts transform
const extendedCommit = originalTransform(commit, context);

// add client remote detail (login)
if (extendedCommit) {
const remoteCommit = commitsSinceLastRelease.find((c) => c.shortHash === commit.shortHash);
if (remoteCommit?.login) {
commit.userLogin = remoteCommit.login;
}
}

return extendedCommit;
}

0 comments on commit e9d7c52

Please sign in to comment.