You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
All: Use Lerna to manage monorepo
This commit is contained in:
372
packages/tools/git-changelog.js
Normal file
372
packages/tools/git-changelog.js
Normal file
@ -0,0 +1,372 @@
|
||||
'use strict';
|
||||
|
||||
// Supported commit formats:
|
||||
|
||||
// (Desktop|Mobile|Android|iOS[CLI): (New|Improved|Fixed): Some message..... (#ISSUE)
|
||||
|
||||
const { execCommand, githubUsername } = require('./tool-utils.js');
|
||||
|
||||
// From https://stackoverflow.com/a/6234804/561309
|
||||
function escapeHtml(unsafe) {
|
||||
// We only escape <> as this is enough for Markdown
|
||||
return unsafe
|
||||
// .replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
// .replace(/"/g, '"')
|
||||
// .replace(/'/g, ''');
|
||||
}
|
||||
|
||||
async function gitLog(sinceTag) {
|
||||
let lines = await execCommand(`git log --pretty=format:"%H::::DIV::::%ae::::DIV::::%an::::DIV::::%s" ${sinceTag}..HEAD`);
|
||||
lines = lines.split('\n');
|
||||
|
||||
const output = [];
|
||||
for (const line of lines) {
|
||||
const splitted = line.split('::::DIV::::');
|
||||
const commit = splitted[0];
|
||||
const authorEmail = splitted[1];
|
||||
const authorName = splitted[2];
|
||||
const message = splitted[3].trim();
|
||||
|
||||
output.push({
|
||||
commit: commit,
|
||||
message: message,
|
||||
author: {
|
||||
email: authorEmail,
|
||||
name: authorName,
|
||||
login: await githubUsername(authorEmail, authorName),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function gitTags() {
|
||||
const lines = await execCommand('git tag --sort=committerdate');
|
||||
return lines.split('\n').map(l => l.trim());
|
||||
}
|
||||
|
||||
function platformFromTag(tagName) {
|
||||
if (tagName.indexOf('v') === 0) return 'desktop';
|
||||
if (tagName.indexOf('android') >= 0) return 'android';
|
||||
if (tagName.indexOf('ios') >= 0) return 'ios';
|
||||
if (tagName.indexOf('clipper') === 0) return 'clipper';
|
||||
if (tagName.indexOf('cli') === 0) return 'cli';
|
||||
throw new Error(`Could not determine platform from tag: ${tagName}`);
|
||||
}
|
||||
|
||||
// function tagPrefixFromPlatform(platform) {
|
||||
// if (platform === 'desktop') return '';
|
||||
// if (platform === 'android') return 'android-';
|
||||
// if (platform === 'ios') return 'ios-';
|
||||
// if (platform === 'clipper') return 'clipper-';
|
||||
// if (platform === 'cli') return 'cli-';
|
||||
// throw new Error(`Could not determine tag prefix from platform: ${platform}`);
|
||||
// }
|
||||
|
||||
function filterLogs(logs, platform) {
|
||||
const output = [];
|
||||
const revertedLogs = [];
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
// let updatedTranslations = false;
|
||||
|
||||
for (const log of logs) {
|
||||
|
||||
// Save to an array any commit that has been reverted, and exclude
|
||||
// these from the final output array.
|
||||
const revertMatches = log.message.split('\n')[0].trim().match(/^Revert "(.*?)"$/);
|
||||
if (revertMatches && revertMatches.length >= 2) {
|
||||
revertedLogs.push(revertMatches[1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (revertedLogs.indexOf(log.message) >= 0) continue;
|
||||
|
||||
let prefix = log.message.trim().toLowerCase().split(':');
|
||||
if (prefix.length <= 1) continue;
|
||||
prefix = prefix[0].split(',').map(s => s.trim());
|
||||
|
||||
let addIt = false;
|
||||
|
||||
if (prefix.indexOf('all') >= 0 && platform !== 'clipper') addIt = true;
|
||||
if ((platform === 'android' || platform === 'ios') && prefix.indexOf('mobile') >= 0) addIt = true;
|
||||
if (platform === 'android' && prefix.indexOf('android') >= 0) addIt = true;
|
||||
if (platform === 'ios' && prefix.indexOf('ios') >= 0) addIt = true;
|
||||
if (platform === 'desktop' && prefix.indexOf('desktop') >= 0) addIt = true;
|
||||
if (platform === 'desktop' && (prefix.indexOf('desktop') >= 0 || prefix.indexOf('api') >= 0 || prefix.indexOf('plugins') >= 0)) addIt = true;
|
||||
if (platform === 'cli' && prefix.indexOf('cli') >= 0) addIt = true;
|
||||
if (platform === 'clipper' && prefix.indexOf('clipper') >= 0) addIt = true;
|
||||
|
||||
// Translation updates often comes in format "Translation: Update pt_PT.po"
|
||||
// but that's not useful in a changelog especially since most people
|
||||
// don't know country and language codes. So we catch all these and
|
||||
// bundle them all up in a single "Updated translations" at the end.
|
||||
if (log.message.match(/Translation: Update .*?\.po/)) {
|
||||
// updatedTranslations = true;
|
||||
addIt = false;
|
||||
}
|
||||
|
||||
if (addIt) output.push(log);
|
||||
}
|
||||
|
||||
// Actually we don't really need this info - translations are being updated all the time
|
||||
// if (updatedTranslations) output.push({ message: 'Updated translations' });
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function formatCommitMessage(msg, author, options) {
|
||||
options = Object.assign({}, { publishFormat: 'full' }, options);
|
||||
|
||||
let output = '';
|
||||
|
||||
const splitted = msg.split(':');
|
||||
|
||||
let subModule = '';
|
||||
|
||||
const isPlatformPrefix = prefix => {
|
||||
prefix = prefix.split(',').map(p => p.trim().toLowerCase());
|
||||
for (const p of prefix) {
|
||||
if (['android', 'mobile', 'ios', 'desktop', 'cli', 'clipper', 'all', 'api', 'plugins'].indexOf(p) >= 0) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (splitted.length) {
|
||||
const platform = splitted[0].trim().toLowerCase();
|
||||
if (platform === 'api') subModule = 'api';
|
||||
if (platform === 'plugins') subModule = 'plugins';
|
||||
if (isPlatformPrefix(platform)) {
|
||||
splitted.splice(0, 1);
|
||||
}
|
||||
|
||||
output = splitted.join(':');
|
||||
}
|
||||
|
||||
output = output.split('\n')[0].trim();
|
||||
|
||||
const detectType = msg => {
|
||||
msg = msg.trim().toLowerCase();
|
||||
|
||||
if (msg.indexOf('fix') === 0) return 'fixed';
|
||||
if (msg.indexOf('add') === 0) return 'new';
|
||||
if (msg.indexOf('change') === 0) return 'improved';
|
||||
if (msg.indexOf('update') === 0) return 'improved';
|
||||
if (msg.indexOf('improve') === 0) return 'improved';
|
||||
|
||||
return 'improved';
|
||||
};
|
||||
|
||||
const parseCommitMessage = (msg, subModule) => {
|
||||
const parts = msg.split(':');
|
||||
|
||||
if (parts.length === 1) {
|
||||
return {
|
||||
type: detectType(msg),
|
||||
message: msg.trim(),
|
||||
subModule: subModule,
|
||||
};
|
||||
}
|
||||
|
||||
let originalType = parts[0].trim();
|
||||
let t = originalType.toLowerCase();
|
||||
|
||||
parts.splice(0, 1);
|
||||
let message = parts.join(':').trim();
|
||||
|
||||
let type = null;
|
||||
|
||||
// eg. "All: Resolves #712: New: Support for note history (#1415)"
|
||||
// "Resolves" doesn't tell us if it's new or improved so check the
|
||||
// third token (which in this case is "new").
|
||||
if (t.indexOf('resolves') === 0 && ['new', 'improved', 'fixed'].indexOf(parts[0].trim().toLowerCase()) >= 0) {
|
||||
t = parts[0].trim().toLowerCase();
|
||||
parts.splice(0, 1);
|
||||
message = parts.join(':').trim();
|
||||
} else if (t.indexOf('resolves') === 0) { // If we didn't have the third token default to "improved"
|
||||
t = 'improved';
|
||||
message = parts.join(':').trim();
|
||||
}
|
||||
|
||||
if (t.indexOf('fix') === 0) type = 'fixed';
|
||||
if (t.indexOf('new') === 0) type = 'new';
|
||||
if (t.indexOf('improved') === 0) type = 'improved';
|
||||
if (t.indexOf('security') === 0) type = 'security';
|
||||
|
||||
// if (t.indexOf('security') === 0) {
|
||||
// type = 'security';
|
||||
// parts.splice(0, 1);
|
||||
// message = parts.join(':').trim();
|
||||
// }
|
||||
|
||||
if (!type) {
|
||||
type = detectType(message);
|
||||
if (originalType.toLowerCase() === 'tinymce') originalType = 'WYSIWYG';
|
||||
message = `${originalType}: ${message}`;
|
||||
}
|
||||
|
||||
let issueNumber = output.match(/#(\d+)/);
|
||||
issueNumber = issueNumber && issueNumber.length >= 2 ? issueNumber[1] : null;
|
||||
|
||||
return {
|
||||
type: type,
|
||||
message: message,
|
||||
issueNumber: issueNumber,
|
||||
subModule: subModule,
|
||||
};
|
||||
};
|
||||
|
||||
const commitMessage = parseCommitMessage(output, subModule);
|
||||
|
||||
const messagePieces = [];
|
||||
messagePieces.push(`${capitalizeFirstLetter(commitMessage.type)}`);
|
||||
if (commitMessage.subModule) messagePieces.push(`${capitalizeFirstLetter(commitMessage.subModule)}`);
|
||||
messagePieces.push(`${capitalizeFirstLetter(commitMessage.message)}`);
|
||||
|
||||
output = messagePieces.join(': ');
|
||||
|
||||
if (options.publishFormat === 'full') {
|
||||
if (commitMessage.issueNumber) {
|
||||
const formattedIssueNum = `(#${commitMessage.issueNumber})`;
|
||||
if (output.indexOf(formattedIssueNum) < 0) output += ` ${formattedIssueNum}`;
|
||||
}
|
||||
|
||||
let authorMd = null;
|
||||
const isLaurent = author.login === 'laurent22' || author.email === 'laurent22@users.noreply.github.com';
|
||||
if (author && (author.login || author.name) && !isLaurent) {
|
||||
if (author.login) {
|
||||
const escapedLogin = author.login.replace(/\]/g, '');
|
||||
authorMd = `[@${escapedLogin}](https://github.com/${encodeURI(author.login)})`;
|
||||
} else {
|
||||
authorMd = `${author.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (authorMd) {
|
||||
output = output.replace(/\((#[0-9]+)\)$/, `($1 by ${authorMd})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.publishFormat !== 'full') {
|
||||
output = output.replace(/\((#[0-9]+)\)$/, '');
|
||||
}
|
||||
|
||||
return escapeHtml(output);
|
||||
}
|
||||
|
||||
function createChangeLog(logs, options) {
|
||||
const output = [];
|
||||
|
||||
for (const log of logs) {
|
||||
output.push(formatCommitMessage(log.message, log.author, options));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
// function decreaseTagVersion(tag) {
|
||||
// const s = tag.split('.');
|
||||
|
||||
// let updated = false;
|
||||
|
||||
// for (let tokenIndex = s.length - 1; tokenIndex >= 0; tokenIndex--) {
|
||||
// const token = s[tokenIndex];
|
||||
// const s2 = token.split('-');
|
||||
// let num = Number(s2[0]);
|
||||
// num--;
|
||||
// if (num >= 0) {
|
||||
// updated = true;
|
||||
// s[tokenIndex] = num;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!updated) throw new Error(`Cannot decrease tag version: ${tag}`);
|
||||
|
||||
// return s.join('.');
|
||||
// }
|
||||
|
||||
// This function finds the first relevant tag starting from the given tag.
|
||||
// The first "relevant tag" is the one that exists, and from which there are changes.
|
||||
async function findFirstRelevantTag(baseTag, platform, allTags) {
|
||||
let baseTagIndex = allTags.indexOf(baseTag);
|
||||
if (baseTagIndex < 0) baseTagIndex = allTags.length;
|
||||
|
||||
for (let i = baseTagIndex - 1; i >= 0; i--) {
|
||||
const tag = allTags[i];
|
||||
if (platformFromTag(tag) !== platform) continue;
|
||||
|
||||
try {
|
||||
const logs = await gitLog(tag);
|
||||
const filteredLogs = filterLogs(logs, platform);
|
||||
if (filteredLogs.length) return tag;
|
||||
} catch (error) {
|
||||
if (error.message.indexOf('unknown revision') >= 0) {
|
||||
// We skip the error - it means this particular tag has never been created
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Could not find previous tag for: ${baseTag}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const argv = require('yargs').argv;
|
||||
if (!argv._.length) throw new Error('Tag name must be specified. Provide the tag of the new version and git-changelog will walk backward to find the changes to the previous relevant tag.');
|
||||
|
||||
const allTags = await gitTags();
|
||||
const fromTagName = argv._[0];
|
||||
let toTagName = argv._.length >= 2 ? argv._[1] : '';
|
||||
|
||||
const platform = platformFromTag(fromTagName);
|
||||
|
||||
if (!toTagName) toTagName = await findFirstRelevantTag(fromTagName, platform, allTags);
|
||||
|
||||
const logsSinceTags = await gitLog(toTagName);
|
||||
|
||||
const filteredLogs = filterLogs(logsSinceTags, platform);
|
||||
|
||||
let publishFormat = 'full';
|
||||
if (['android', 'ios'].indexOf(platform) >= 0) publishFormat = 'simple';
|
||||
let changelog = createChangeLog(filteredLogs, { publishFormat: publishFormat });
|
||||
|
||||
const changelogFixes = [];
|
||||
const changelogImproves = [];
|
||||
const changelogNews = [];
|
||||
|
||||
for (const l of changelog) {
|
||||
if (l.indexOf('Fix') === 0 || l.indexOf('Security') === 0) {
|
||||
changelogFixes.push(l);
|
||||
} else if (l.indexOf('Improve') === 0) {
|
||||
changelogImproves.push(l);
|
||||
} else if (l.indexOf('New') === 0) {
|
||||
changelogNews.push(l);
|
||||
} else {
|
||||
throw new Error(`Invalid changelog line: ${l}`);
|
||||
}
|
||||
}
|
||||
|
||||
changelogFixes.sort();
|
||||
changelogImproves.sort();
|
||||
changelogNews.sort();
|
||||
|
||||
changelog = [].concat(changelogNews).concat(changelogImproves).concat(changelogFixes);
|
||||
|
||||
const changelogString = changelog.map(l => `- ${l}`);
|
||||
console.info(changelogString.join('\n'));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
Reference in New Issue
Block a user