2019-04-28 16:09:07 +02:00
// Supported commit formats:
// (Desktop|Mobile|Android|iOS[CLI): (New|Improved|Fixed): Some message..... (#ISSUE)
2021-08-28 11:49:31 +02:00
import { execCommand , githubUsername } from './tool-utils' ;
interface LogEntry {
message : string ;
commit : string ;
author : Author ;
}
enum Platform {
Android = 'android' ,
Ios = 'ios' ,
Desktop = 'desktop' ,
Clipper = 'clipper' ,
Server = 'server' ,
Cli = 'cli' ,
PluginGenerator = 'plugin-generator' ,
2021-12-29 11:48:34 +02:00
PluginRepoCli = 'plugin-repo-cli' ,
2021-08-28 11:49:31 +02:00
}
enum PublishFormat {
Full = 'full' ,
Simple = 'simple' ,
}
interface Options {
publishFormat : PublishFormat ;
}
interface Author {
email : string ;
name : string ;
login : string ;
}
2019-04-22 20:02:45 +02:00
2020-10-20 18:16:09 +02:00
// From https://stackoverflow.com/a/6234804/561309
2021-08-28 11:49:31 +02:00
function escapeHtml ( unsafe : string ) {
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
. replace ( /</g , '<' )
2020-10-31 15:05:46 +02:00
. replace ( />/g , '>' ) ;
2020-10-20 18:16:09 +02:00
}
2021-08-28 11:49:31 +02:00
async function gitLog ( sinceTag : string ) {
const commandResult = await execCommand ( ` git log --pretty=format:"%H::::DIV::::%ae::::DIV::::%an::::DIV::::%s" ${ sinceTag } ..HEAD ` ) ;
const lines = commandResult . split ( '\n' ) ;
2019-05-06 23:18:17 +02:00
const output = [ ] ;
for ( const line of lines ) {
2020-11-11 14:40:43 +02:00
if ( ! line . trim ( ) ) continue ;
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() {
2021-08-28 11:49:31 +02:00
const lines : string = await execCommand ( 'git tag --sort=committerdate' ) ;
2020-11-26 14:10:56 +02:00
return lines . split ( '\n' ) . map ( l = > l . trim ( ) ) . filter ( l = > ! ! l ) ;
2020-09-21 14:01:46 +02:00
}
2021-08-28 11:49:31 +02:00
function platformFromTag ( tagName : string ) : Platform {
if ( tagName . indexOf ( 'v' ) === 0 ) return Platform . Desktop ;
if ( tagName . indexOf ( 'android' ) >= 0 ) return Platform . Android ;
if ( tagName . indexOf ( 'ios' ) >= 0 ) return Platform . Ios ;
if ( tagName . indexOf ( 'clipper' ) === 0 ) return Platform . Clipper ;
if ( tagName . indexOf ( 'cli' ) === 0 ) return Platform . Cli ;
if ( tagName . indexOf ( 'server' ) === 0 ) return Platform . Server ;
if ( tagName . indexOf ( 'plugin-generator' ) === 0 ) return Platform . PluginGenerator ;
2021-12-29 11:48:34 +02:00
if ( tagName . indexOf ( 'plugin-repo-cli' ) === 0 ) return Platform . PluginRepoCli ;
2020-11-26 14:10:56 +02:00
throw new Error ( ` Could not determine platform from tag: " ${ tagName } " ` ) ;
2019-04-22 20:02:45 +02:00
}
2021-08-28 11:49:31 +02:00
function filterLogs ( logs : LogEntry [ ] , platform : Platform ) {
2019-04-22 20:02:45 +02:00
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 ;
2021-01-24 21:25:32 +02:00
// "All" refers to desktop, CLI and mobile app. Clipper and Server are not included.
if ( prefix . indexOf ( 'all' ) >= 0 && ( platform !== 'clipper' && platform !== 'server' ) ) addIt = true ;
2019-05-03 01:19:42 +02:00
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-11-29 19:47:41 +02:00
if ( platform === 'desktop' && ( prefix . indexOf ( 'desktop' ) >= 0 || prefix . indexOf ( 'api' ) >= 0 || prefix . indexOf ( 'plugins' ) >= 0 || prefix . indexOf ( 'macos' ) >= 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 ;
2021-01-24 21:25:32 +02:00
if ( platform === 'server' && prefix . indexOf ( 'server' ) >= 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 ;
}
2021-08-28 11:49:31 +02:00
function formatCommitMessage ( commit : string , msg : string , author : Author , options : Options ) : string {
2020-01-25 12:42:36 +02:00
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 = '' ;
2021-08-28 11:49:31 +02:00
const isPlatformPrefix = ( prefixString : string ) = > {
const prefix = prefixString . split ( ',' ) . map ( p = > p . trim ( ) . toLowerCase ( ) ) ;
2019-05-03 17:02:32 +02:00
for ( const p of prefix ) {
2021-01-24 21:25:32 +02:00
if ( [ 'android' , 'mobile' , 'ios' , 'desktop' , 'cli' , 'clipper' , 'all' , 'api' , 'plugins' , 'server' ] . 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 ( ) ;
2021-08-28 11:49:31 +02:00
const detectType = ( msg : string ) = > {
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
2021-08-28 11:49:31 +02:00
const parseCommitMessage = ( msg : string , subModule : string ) = > {
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-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
2021-08-28 11:49:31 +02:00
const issueNumberMatch = output . match ( /#(\d+)/ ) ;
const issueNumber = issueNumberMatch && issueNumberMatch . length >= 2 ? issueNumberMatch [ 1 ] : null ;
2019-04-28 16:09:07 +02:00
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
2021-04-25 18:00:46 +02:00
const issueRegex = /\((#[0-9]+)\)$/ ;
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 } ` ;
}
}
2021-04-25 18:00:46 +02:00
if ( output . match ( issueRegex ) ) {
if ( authorMd ) {
output = output . replace ( issueRegex , ` ( $ 1 by ${ authorMd } ) ` ) ;
}
} else {
const commitStrings = [ commit . substr ( 0 , 7 ) ] ;
if ( authorMd ) commitStrings . push ( ` by ${ authorMd } ` ) ;
output += ` ( ${ commitStrings . join ( ' ' ) } ) ` ;
2020-01-22 22:33:43 +02:00
}
}
2020-01-25 12:42:36 +02:00
if ( options . publishFormat !== 'full' ) {
2021-04-25 18:00:46 +02:00
output = output . replace ( issueRegex , '' ) ;
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
}
2021-08-28 11:49:31 +02:00
function createChangeLog ( logs : LogEntry [ ] , options : Options ) {
2019-04-22 20:02:45 +02:00
const output = [ ] ;
for ( const log of logs ) {
2021-04-25 18:00:46 +02:00
output . push ( formatCommitMessage ( log . commit , log . message , log . author , options ) ) ;
2019-04-22 20:02:45 +02:00
}
return output ;
}
2021-08-28 11:49:31 +02:00
function capitalizeFirstLetter ( string : 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
}
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.
2021-08-28 11:49:31 +02:00
async function findFirstRelevantTag ( baseTag : string , platform : Platform , allTags : string [ ] ) {
2020-09-21 14:01:46 +02:00
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 ) ;
2021-08-28 11:49:31 +02:00
let publishFormat : PublishFormat = PublishFormat . Full ;
if ( [ 'android' , 'ios' ] . indexOf ( platform ) >= 0 ) publishFormat = PublishFormat . Simple ;
2021-06-15 22:08:55 +02:00
if ( argv . publishFormat ) publishFormat = argv . publishFormat ;
2020-01-25 12:42:36 +02:00
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 ) ;
} ) ;