mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-27 02:27:12 +00:00
347 lines
7.6 KiB
JavaScript
347 lines
7.6 KiB
JavaScript
|
// TODO: Refactor this action
|
||
|
|
||
|
const {execSync} = require('child_process');
|
||
|
|
||
|
/**
|
||
|
* Gets the value of an input. The value is also trimmed.
|
||
|
*
|
||
|
* @param name name of the input to get
|
||
|
* @param options optional. See InputOptions.
|
||
|
* @returns string
|
||
|
*/
|
||
|
function getInput(name, options) {
|
||
|
const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || '';
|
||
|
if (options && options.required && !val) {
|
||
|
throw new Error(`Input required and not supplied: ${name}`);
|
||
|
}
|
||
|
|
||
|
return val.trim();
|
||
|
}
|
||
|
|
||
|
const START_FROM = getInput('from');
|
||
|
const END_TO = getInput('to');
|
||
|
const INCLUDE_COMMIT_BODY = getInput('include-commit-body') === 'true';
|
||
|
const INCLUDE_ABBREVIATED_COMMIT = getInput('include-abbreviated-commit') === 'true';
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} ICommit
|
||
|
* @property {string | undefined} abbreviated_commit
|
||
|
* @property {string | undefined} subject
|
||
|
* @property {string | undefined} body
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {ICommit & {type: string | undefined, scope: string | undefined}} ICommitExtended
|
||
|
*/
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Any unique string that is guaranteed not to be used in committee text.
|
||
|
* Used to split data in the commit line
|
||
|
* @type {string}
|
||
|
*/
|
||
|
const commitInnerSeparator = '~~~~';
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Any unique string that is guaranteed not to be used in committee text.
|
||
|
* Used to split each commit line
|
||
|
* @type {string}
|
||
|
*/
|
||
|
const commitOuterSeparator = '₴₴₴₴';
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Commit data to be obtained.
|
||
|
* @type {Map<string, string>}
|
||
|
*
|
||
|
* @see https://git-scm.com/docs/git-log#Documentation/git-log.txt-emnem
|
||
|
*/
|
||
|
const commitDataMap = new Map([
|
||
|
['subject', '%s'], // Required
|
||
|
]);
|
||
|
|
||
|
if (INCLUDE_COMMIT_BODY) {
|
||
|
commitDataMap.set('body', '%b');
|
||
|
}
|
||
|
|
||
|
if (INCLUDE_ABBREVIATED_COMMIT) {
|
||
|
commitDataMap.set('abbreviated_commit', '%h');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The type used to group commits that do not comply with the convention
|
||
|
* @type {string}
|
||
|
*/
|
||
|
const fallbackType = 'other';
|
||
|
|
||
|
|
||
|
/**
|
||
|
* List of all desired commit groups and in what order to display them.
|
||
|
* @type {string[]}
|
||
|
*/
|
||
|
const supportedTypes = [
|
||
|
'feat',
|
||
|
'fix',
|
||
|
'perf',
|
||
|
'refactor',
|
||
|
'style',
|
||
|
'docs',
|
||
|
'test',
|
||
|
'build',
|
||
|
'ci',
|
||
|
'chore',
|
||
|
'revert',
|
||
|
'deps',
|
||
|
fallbackType,
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* @param {string} commitString
|
||
|
* @returns {ICommit}
|
||
|
*/
|
||
|
function parseCommit(commitString) {
|
||
|
/** @type {ICommit} */
|
||
|
const commitDataObj = {};
|
||
|
const commitDataArray =
|
||
|
commitString
|
||
|
.split(commitInnerSeparator)
|
||
|
.map(s => s.trim());
|
||
|
|
||
|
for (const [key] of commitDataMap) {
|
||
|
commitDataObj[key] = commitDataArray.shift();
|
||
|
}
|
||
|
|
||
|
return commitDataObj;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns an array of commits since the last git tag
|
||
|
* @return {ICommit[]}
|
||
|
*/
|
||
|
function getCommits() {
|
||
|
|
||
|
const format = Array.from(commitDataMap.values()).join(commitInnerSeparator) + commitOuterSeparator;
|
||
|
|
||
|
const logs = String(execSync(`git --no-pager log ${START_FROM}..${END_TO} --pretty=format:"${format}" --reverse`));
|
||
|
|
||
|
return logs
|
||
|
.trim()
|
||
|
.split(commitOuterSeparator)
|
||
|
.filter(r => !!r.trim()) // Skip empty lines
|
||
|
.map(parseCommit);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param {ICommit} commit
|
||
|
* @return {ICommitExtended}
|
||
|
*/
|
||
|
function setCommitTypeAndScope(commit) {
|
||
|
|
||
|
const matchRE = new RegExp(`^(?:(${supportedTypes.join('|')})(?:\\((\\S+)\\))?:)?(.*)`, 'i');
|
||
|
|
||
|
let [, type, scope, clearSubject] = commit.subject.match(matchRE);
|
||
|
|
||
|
/**
|
||
|
* Additional rules for checking committees that do not comply with the convention, but for which it is possible to determine the type.
|
||
|
*/
|
||
|
// Commits like `revert something`
|
||
|
if (type === undefined && commit.subject.startsWith('revert')) {
|
||
|
type = 'revert';
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
...commit,
|
||
|
type: (type || fallbackType).toLowerCase().trim(),
|
||
|
scope: (scope || '').toLowerCase().trim(),
|
||
|
subject: (clearSubject || commit.subject).trim(),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
class CommitGroup {
|
||
|
constructor() {
|
||
|
this.scopes = new Map;
|
||
|
this.commits = [];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param {ICommitExtended[]} array
|
||
|
* @param {ICommitExtended} commit
|
||
|
*/
|
||
|
static _pushOrMerge(array, commit) {
|
||
|
const similarCommit = array.find(c => c.subject === commit.subject);
|
||
|
if (similarCommit) {
|
||
|
if (commit.abbreviated_commit !== undefined) {
|
||
|
similarCommit.abbreviated_commit += `, ${commit.abbreviated_commit}`;
|
||
|
}
|
||
|
} else {
|
||
|
array.push(commit);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {ICommitExtended} commit
|
||
|
*/
|
||
|
push(commit) {
|
||
|
if (!commit.scope) {
|
||
|
CommitGroup._pushOrMerge(this.commits, commit);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const scope = this.scopes.get(commit.scope) || {commits: []};
|
||
|
CommitGroup._pushOrMerge(scope.commits, commit);
|
||
|
this.scopes.set(commit.scope, scope);
|
||
|
}
|
||
|
|
||
|
get isEmpty() {
|
||
|
return this.commits.length === 0 && this.scopes.size === 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Groups all commits by type and scopes
|
||
|
* @param {ICommit[]} commits
|
||
|
* @returns {Map<string, CommitGroup>}
|
||
|
*/
|
||
|
function getGroupedCommits(commits) {
|
||
|
const parsedCommits = commits.map(setCommitTypeAndScope);
|
||
|
|
||
|
const types = new Map(
|
||
|
supportedTypes.map(id => ([id, new CommitGroup()])),
|
||
|
);
|
||
|
|
||
|
for (const parsedCommit of parsedCommits) {
|
||
|
const typeId = parsedCommit.type;
|
||
|
const type = types.get(typeId);
|
||
|
type.push(parsedCommit);
|
||
|
}
|
||
|
|
||
|
return types;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return markdown list with commits
|
||
|
* @param {ICommitExtended[]} commits
|
||
|
* @param {string} pad
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
function getCommitsList(commits, pad = '') {
|
||
|
let changelog = '';
|
||
|
for (const commit of commits) {
|
||
|
changelog += `${pad}- ${commit.subject}.`;
|
||
|
|
||
|
if (commit.abbreviated_commit !== undefined) {
|
||
|
changelog += ` (${commit.abbreviated_commit})`;
|
||
|
}
|
||
|
|
||
|
changelog += '\r\n';
|
||
|
|
||
|
if (commit.body === undefined) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const body = commit.body.replace('[skip ci]', '').trim();
|
||
|
if (body !== '') {
|
||
|
changelog += `${
|
||
|
body
|
||
|
.split(/\r*\n+/)
|
||
|
.filter(s => !!s.trim())
|
||
|
.map(s => `${pad} ${s}`)
|
||
|
.join('\r\n')
|
||
|
}${'\r\n'}`;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return changelog;
|
||
|
}
|
||
|
|
||
|
|
||
|
function replaceHeader(str) {
|
||
|
switch (str) {
|
||
|
case 'feat':
|
||
|
return 'New Features';
|
||
|
case 'fix':
|
||
|
return 'Bug Fixes';
|
||
|
case 'docs':
|
||
|
return 'Documentation Changes';
|
||
|
case 'build':
|
||
|
return 'Build System';
|
||
|
case 'chore':
|
||
|
return 'Chores';
|
||
|
case 'ci':
|
||
|
return 'Continuous Integration';
|
||
|
case 'refactor':
|
||
|
return 'Refactors';
|
||
|
case 'style':
|
||
|
return 'Code Style Changes';
|
||
|
case 'test':
|
||
|
return 'Tests';
|
||
|
case 'perf':
|
||
|
return 'Performance improvements';
|
||
|
case 'revert':
|
||
|
return 'Reverts';
|
||
|
case 'deps':
|
||
|
return 'Dependency updates';
|
||
|
case 'other':
|
||
|
return 'Other Changes';
|
||
|
default:
|
||
|
return str;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Return markdown string with changelog
|
||
|
* @param {Map<string, CommitGroup>} groups
|
||
|
*/
|
||
|
function getChangeLog(groups) {
|
||
|
|
||
|
let changelog = '';
|
||
|
|
||
|
for (const [typeId, group] of groups) {
|
||
|
if (group.isEmpty) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
changelog += `### ${replaceHeader(typeId)}${'\r\n'}`;
|
||
|
|
||
|
for (const [scopeId, scope] of group.scopes) {
|
||
|
if (scope.commits.length) {
|
||
|
changelog += `- #### ${replaceHeader(scopeId)}${'\r\n'}`;
|
||
|
changelog += getCommitsList(scope.commits, ' ');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (group.commits.length) {
|
||
|
changelog += getCommitsList(group.commits);
|
||
|
}
|
||
|
|
||
|
changelog += ('\r\n' + '\r\n');
|
||
|
}
|
||
|
|
||
|
return changelog.trim();
|
||
|
}
|
||
|
|
||
|
|
||
|
function escapeData(s) {
|
||
|
return String(s)
|
||
|
.replace(/%/g, '%25')
|
||
|
.replace(/\r/g, '%0D')
|
||
|
.replace(/\n/g, '%0A');
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
const commits = getCommits();
|
||
|
const grouped = getGroupedCommits(commits);
|
||
|
const changelog = getChangeLog(grouped);
|
||
|
process.stdout.write('::set-output name=release-note::' + escapeData(changelog) + '\r\n');
|
||
|
// require('fs').writeFileSync('../CHANGELOG.md', changelog, {encoding: 'utf-8'})
|
||
|
} catch (e) {
|
||
|
console.error(e);
|
||
|
process.exit(1);
|
||
|
}
|