2019-07-30 09:35:42 +02:00
'use strict' ;
2019-04-22 20:02:45 +02:00
2019-04-28 16:09:07 +02:00
// Supported commit formats:
// (Desktop|Mobile|Android|iOS[CLI): (New|Improved|Fixed): Some message..... (#ISSUE)
2020-01-22 22:33:43 +02:00
const { execCommand , githubUsername } = require ( './tool-utils.js' ) ;
2019-04-22 20:02:45 +02:00
2020-10-20 18:16:09 +02:00
// From https://stackoverflow.com/a/6234804/561309
function escapeHtml ( unsafe ) {
2020-10-31 15:05:46 +02:00
// We only escape <> as this is enough for Markdown
2020-10-20 18:16:09 +02:00
return unsafe
2020-10-31 15:05:46 +02:00
// .replace(/&/g, '&')
2020-10-20 18:16:09 +02:00
. replace ( /</g , '<' )
2020-10-31 15:05:46 +02:00
. replace ( />/g , '>' ) ;
// .replace(/"/g, '"')
// .replace(/'/g, ''');
2020-10-20 18:16:09 +02:00
}
2019-05-06 23:18:17 +02:00
async function gitLog ( sinceTag ) {
2020-01-22 22:33:43 +02:00
let lines = await execCommand ( ` git log --pretty=format:"%H::::DIV::::%ae::::DIV::::%an::::DIV::::%s" ${ sinceTag } ..HEAD ` ) ;
2019-05-06 23:18:17 +02:00
lines = lines . split ( '\n' ) ;
const output = [ ] ;
for ( const line of lines ) {
2020-01-22 22:33:43 +02:00
const splitted = line . split ( '::::DIV::::' ) ;
2019-05-06 23:18:17 +02:00
const commit = splitted [ 0 ] ;
2020-01-22 22:33:43 +02:00
const authorEmail = splitted [ 1 ] ;
const authorName = splitted [ 2 ] ;
const message = splitted [ 3 ] . trim ( ) ;
2019-07-30 09:35:42 +02:00
2019-05-06 23:18:17 +02:00
output . push ( {
commit : commit ,
message : message ,
2020-01-22 22:33:43 +02:00
author : {
email : authorEmail ,
name : authorName ,
2020-01-25 00:43:55 +02:00
login : await githubUsername ( authorEmail , authorName ) ,
2020-01-22 22:33:43 +02:00
} ,
2019-05-06 23:18:17 +02:00
} ) ;
}
2019-04-22 20:02:45 +02:00
return output ;
}
2020-09-21 14:01:46 +02:00
async function gitTags ( ) {
const lines = await execCommand ( 'git tag --sort=committerdate' ) ;
return lines . split ( '\n' ) . map ( l => l . trim ( ) ) ;
}
2019-04-22 20:02:45 +02:00
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' ;
2019-09-19 23:51:18 +02:00
throw new Error ( ` Could not determine platform from tag: ${ tagName } ` ) ;
2019-04-22 20:02:45 +02:00
}
2020-09-21 14:01:46 +02:00
// 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}`);
// }
2019-04-22 20:02:45 +02:00
function filterLogs ( logs , platform ) {
const output = [ ] ;
2019-04-29 08:42:40 +02:00
const revertedLogs = [ ] ;
2019-04-22 20:02:45 +02:00
2020-05-10 18:54:18 +02:00
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
2020-08-05 00:00:11 +02:00
// let updatedTranslations = false;
2020-04-20 20:10:27 +02:00
2019-04-22 20:02:45 +02:00
for ( const log of logs ) {
2019-04-29 08:42:40 +02:00
// 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 ;
2019-05-03 01:19:42 +02:00
let prefix = log . message . trim ( ) . toLowerCase ( ) . split ( ':' ) ;
if ( prefix . length <= 1 ) continue ;
2020-05-21 10:14:33 +02:00
prefix = prefix [ 0 ] . split ( ',' ) . map ( s => s . trim ( ) ) ;
2019-04-22 20:02:45 +02:00
let addIt = false ;
2019-05-03 01:19:42 +02:00
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 ;
2020-10-18 22:52:10 +02:00
if ( platform === 'desktop' && ( prefix . indexOf ( 'desktop' ) >= 0 || prefix . indexOf ( 'api' ) >= 0 || prefix . indexOf ( 'plugins' ) >= 0 ) ) addIt = true ;
2019-05-03 01:19:42 +02:00
if ( platform === 'cli' && prefix . indexOf ( 'cli' ) >= 0 ) addIt = true ;
if ( platform === 'clipper' && prefix . indexOf ( 'clipper' ) >= 0 ) addIt = true ;
2019-04-22 20:02:45 +02:00
2020-04-20 20:10:27 +02:00
// 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/ ) ) {
2020-08-05 00:00:11 +02:00
// updatedTranslations = true;
2020-04-20 20:10:27 +02:00
addIt = false ;
}
2019-04-22 20:02:45 +02:00
if ( addIt ) output . push ( log ) ;
}
2020-05-10 18:54:18 +02:00
// Actually we don't really need this info - translations are being updated all the time
// if (updatedTranslations) output.push({ message: 'Updated translations' });
2020-04-20 20:10:27 +02:00
2019-04-22 20:02:45 +02:00
return output ;
}
2020-01-25 12:42:36 +02:00
function formatCommitMessage ( msg , author , options ) {
options = Object . assign ( { } , { publishFormat : 'full' } , options ) ;
2019-04-22 20:02:45 +02:00
let output = '' ;
2019-07-30 09:35:42 +02:00
2019-04-22 20:02:45 +02:00
const splitted = msg . split ( ':' ) ;
2019-10-07 10:12:10 +02:00
let subModule = '' ;
2020-05-21 10:14:33 +02:00
const isPlatformPrefix = prefix => {
prefix = prefix . split ( ',' ) . map ( p => p . trim ( ) . toLowerCase ( ) ) ;
2019-05-03 17:02:32 +02:00
for ( const p of prefix ) {
2020-10-18 22:52:10 +02:00
if ( [ 'android' , 'mobile' , 'ios' , 'desktop' , 'cli' , 'clipper' , 'all' , 'api' , 'plugins' ] . indexOf ( p ) >= 0 ) return true ;
2019-05-03 17:02:32 +02:00
}
return false ;
2019-07-30 09:35:42 +02:00
} ;
2019-05-03 17:02:32 +02:00
2019-04-22 20:02:45 +02:00
if ( splitted . length ) {
const platform = splitted [ 0 ] . trim ( ) . toLowerCase ( ) ;
2019-10-07 10:12:10 +02:00
if ( platform === 'api' ) subModule = 'api' ;
2020-10-18 22:52:10 +02:00
if ( platform === 'plugins' ) subModule = 'plugins' ;
2019-05-03 17:02:32 +02:00
if ( isPlatformPrefix ( platform ) ) {
2019-04-22 20:02:45 +02:00
splitted . splice ( 0 , 1 ) ;
}
output = splitted . join ( ':' ) ;
}
output = output . split ( '\n' ) [ 0 ] . trim ( ) ;
2020-05-21 10:14:33 +02:00
const detectType = msg => {
2019-04-28 16:09:07 +02:00
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' ;
2019-07-30 09:35:42 +02:00
} ;
2019-04-28 16:09:07 +02:00
2019-10-07 10:12:10 +02:00
const parseCommitMessage = ( msg , subModule ) => {
2019-04-28 16:09:07 +02:00
const parts = msg . split ( ':' ) ;
if ( parts . length === 1 ) {
return {
type : detectType ( msg ) ,
message : msg . trim ( ) ,
2020-01-21 12:40:29 +02:00
subModule : subModule ,
2019-04-28 16:09:07 +02:00
} ;
}
2020-05-10 18:54:18 +02:00
let originalType = parts [ 0 ] . trim ( ) ;
let t = originalType . toLowerCase ( ) ;
2019-04-28 16:09:07 +02:00
parts . splice ( 0 , 1 ) ;
2019-05-06 23:18:17 +02:00
let message = parts . join ( ':' ) . trim ( ) ;
2019-04-28 16:09:07 +02:00
let type = null ;
2019-05-06 23:18:17 +02:00
// eg. "All: Resolves #712: New: Support for note history (#1415)"
2019-07-30 09:35:42 +02:00
// "Resolves" doesn't tell us if it's new or improved so check the
2019-05-06 23:18:17 +02:00
// 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 ( ) ;
2020-05-10 18:54:18 +02:00
} else if ( t . indexOf ( 'resolves' ) === 0 ) { // If we didn't have the third token default to "improved"
t = 'improved' ;
message = parts . join ( ':' ) . trim ( ) ;
2019-05-06 23:18:17 +02:00
}
2019-04-28 16:09:07 +02:00
if ( t . indexOf ( 'fix' ) === 0 ) type = 'fixed' ;
if ( t . indexOf ( 'new' ) === 0 ) type = 'new' ;
if ( t . indexOf ( 'improved' ) === 0 ) type = 'improved' ;
2020-09-12 01:22:17 +02:00
if ( t . indexOf ( 'security' ) === 0 ) type = 'security' ;
2019-04-28 16:09:07 +02:00
2020-09-12 01:22:17 +02:00
// if (t.indexOf('security') === 0) {
// type = 'security';
// parts.splice(0, 1);
// message = parts.join(':').trim();
// }
2020-05-10 18:54:18 +02:00
if ( ! type ) {
type = detectType ( message ) ;
if ( originalType . toLowerCase ( ) === 'tinymce' ) originalType = 'WYSIWYG' ;
message = ` ${ originalType } : ${ message } ` ;
}
2019-07-30 09:35:42 +02:00
2019-04-28 16:09:07 +02:00
let issueNumber = output . match ( /#(\d+)/ ) ;
issueNumber = issueNumber && issueNumber . length >= 2 ? issueNumber [ 1 ] : null ;
return {
type : type ,
message : message ,
issueNumber : issueNumber ,
2019-10-07 10:12:10 +02:00
subModule : subModule ,
2019-04-28 16:09:07 +02:00
} ;
2019-07-30 09:35:42 +02:00
} ;
2019-04-28 16:09:07 +02:00
2019-10-07 10:12:10 +02:00
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 ) } ` ) ;
2019-04-28 16:09:07 +02:00
2019-10-07 10:12:10 +02:00
output = messagePieces . join ( ': ' ) ;
2019-04-22 20:02:45 +02:00
2020-01-25 12:42:36 +02:00
if ( options . publishFormat === 'full' ) {
if ( commitMessage . issueNumber ) {
const formattedIssueNum = ` (# ${ commitMessage . issueNumber } ) ` ;
if ( output . indexOf ( formattedIssueNum ) < 0 ) output += ` ${ formattedIssueNum } ` ;
}
let authorMd = null ;
2020-06-06 17:31:31 +02:00
const isLaurent = author . login === 'laurent22' || author . email === 'laurent22@users.noreply.github.com' ;
if ( author && ( author . login || author . name ) && ! isLaurent ) {
2020-01-25 12:42:36 +02:00
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 } ) ` ) ;
2020-01-22 22:33:43 +02:00
}
}
2020-01-25 12:42:36 +02:00
if ( options . publishFormat !== 'full' ) {
output = output . replace ( /\((#[0-9]+)\)$/ , '' ) ;
2020-01-22 22:33:43 +02:00
}
2020-10-20 18:16:09 +02:00
return escapeHtml ( output ) ;
2019-04-22 20:02:45 +02:00
}
2020-01-25 12:42:36 +02:00
function createChangeLog ( logs , options ) {
2019-04-22 20:02:45 +02:00
const output = [ ] ;
for ( const log of logs ) {
2020-01-25 12:42:36 +02:00
output . push ( formatCommitMessage ( log . message , log . author , options ) ) ;
2019-04-22 20:02:45 +02:00
}
return output ;
}
function capitalizeFirstLetter ( string ) {
2019-07-30 09:35:42 +02:00
return string . charAt ( 0 ) . toUpperCase ( ) + string . slice ( 1 ) ;
2019-04-22 20:02:45 +02:00
}
2020-09-21 14:01:46 +02:00
// function decreaseTagVersion(tag) {
// const s = tag.split('.');
2020-09-21 13:16:05 +02:00
2020-09-21 14:01:46 +02:00
// let updated = false;
2020-09-21 13:16:05 +02:00
2020-09-21 14:01:46 +02:00
// 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;
// }
// }
2020-09-21 13:16:05 +02:00
2020-09-21 14:01:46 +02:00
// if (!updated) throw new Error(`Cannot decrease tag version: ${tag}`);
2020-09-21 13:16:05 +02:00
2020-09-21 14:01:46 +02:00
// return s.join('.');
// }
2019-09-08 18:54:41 +02:00
// 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.
2020-09-21 14:01:46 +02:00
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 ;
2019-09-08 18:54:41 +02:00
try {
const logs = await gitLog ( tag ) ;
2020-09-21 14:01:46 +02:00
const filteredLogs = filterLogs ( logs , platform ) ;
if ( filteredLogs . length ) return tag ;
2019-09-08 18:54:41 +02:00
} 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 ;
}
}
}
2020-09-21 14:01:46 +02:00
throw new Error ( ` Could not find previous tag for: ${ baseTag } ` ) ;
2019-09-08 18:54:41 +02:00
}
2019-04-22 20:02:45 +02:00
async function main ( ) {
const argv = require ( 'yargs' ) . argv ;
2019-09-08 18:54:41 +02:00
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.' ) ;
2020-09-21 14:01:46 +02:00
const allTags = await gitTags ( ) ;
2019-09-08 18:54:41 +02:00
const fromTagName = argv . _ [ 0 ] ;
2019-09-09 19:50:04 +02:00
let toTagName = argv . _ . length >= 2 ? argv . _ [ 1 ] : '' ;
2019-09-08 18:54:41 +02:00
const platform = platformFromTag ( fromTagName ) ;
2020-09-21 14:01:46 +02:00
if ( ! toTagName ) toTagName = await findFirstRelevantTag ( fromTagName , platform , allTags ) ;
2019-04-22 20:02:45 +02:00
2019-09-08 18:54:41 +02:00
const logsSinceTags = await gitLog ( toTagName ) ;
2019-04-22 20:02:45 +02:00
const filteredLogs = filterLogs ( logsSinceTags , platform ) ;
2020-01-25 12:42:36 +02:00
let publishFormat = 'full' ;
if ( [ 'android' , 'ios' ] . indexOf ( platform ) >= 0 ) publishFormat = 'simple' ;
let changelog = createChangeLog ( filteredLogs , { publishFormat : publishFormat } ) ;
2019-04-28 16:09:07 +02:00
const changelogFixes = [ ] ;
const changelogImproves = [ ] ;
const changelogNews = [ ] ;
for ( const l of changelog ) {
2020-05-10 18:54:18 +02:00
if ( l . indexOf ( 'Fix' ) === 0 || l . indexOf ( 'Security' ) === 0 ) {
2019-04-28 16:09:07 +02:00
changelogFixes . push ( l ) ;
} else if ( l . indexOf ( 'Improve' ) === 0 ) {
changelogImproves . push ( l ) ;
} else if ( l . indexOf ( 'New' ) === 0 ) {
changelogNews . push ( l ) ;
} else {
2019-09-19 23:51:18 +02:00
throw new Error ( ` Invalid changelog line: ${ l } ` ) ;
2019-04-28 16:09:07 +02:00
}
}
2019-04-22 20:02:45 +02:00
2020-05-10 18:54:18 +02:00
changelogFixes . sort ( ) ;
changelogImproves . sort ( ) ;
changelogNews . sort ( ) ;
2019-04-28 16:09:07 +02:00
changelog = [ ] . concat ( changelogNews ) . concat ( changelogImproves ) . concat ( changelogFixes ) ;
2019-04-22 20:02:45 +02:00
2020-05-21 10:14:33 +02:00
const changelogString = changelog . map ( l => ` - ${ l } ` ) ;
2019-04-22 20:02:45 +02:00
console . info ( changelogString . join ( '\n' ) ) ;
}
main ( ) . catch ( ( error ) => {
console . error ( 'Fatal error' ) ;
console . error ( error ) ;
process . exit ( 1 ) ;
} ) ;