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)
2019-09-19 23:51:18 +02:00
require ( 'app-module-path' ) . addPath ( ` ${ _ _dirname } /../ReactNativeClient ` ) ;
2019-04-22 20:02:45 +02:00
2020-01-22 22:33:43 +02:00
const { execCommand , githubUsername } = require ( './tool-utils.js' ) ;
2019-04-22 20:02:45 +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 ;
}
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
}
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-04-20 20:10:27 +02:00
let updatedTranslations = false ;
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 ;
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 ;
2019-10-07 10:12:10 +02:00
if ( platform === 'desktop' && ( prefix . indexOf ( 'desktop' ) >= 0 || prefix . indexOf ( 'api' ) >= 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/ ) ) {
updatedTranslations = true ;
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 = '' ;
2019-05-03 17:02:32 +02:00
const isPlatformPrefix = prefix => {
prefix = prefix . split ( ',' ) . map ( p => p . trim ( ) . toLowerCase ( ) ) ;
for ( const p of prefix ) {
2019-10-07 10:12:10 +02:00
if ( [ 'android' , 'mobile' , 'ios' , 'desktop' , 'cli' , 'clipper' , 'all' , 'api' ] . 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' ;
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 ( ) ;
2019-04-28 16:09:07 +02:00
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' ;
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-05-10 18:54:18 +02:00
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 } ` ;
}
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 ;
if ( author && ( author . login || author . name ) && author . login !== 'laurent22' ) {
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
}
2019-04-22 20:02:45 +02:00
return output ;
}
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
}
2019-09-08 18:54:41 +02:00
function decreaseTagVersion ( tag ) {
const s = tag . split ( '.' ) ;
let num = Number ( s . pop ( ) ) ;
num -- ;
2019-09-19 23:51:18 +02:00
if ( num < 0 ) throw new Error ( ` Cannot decrease tag version: ${ tag } ` ) ;
s . push ( ` ${ num } ` ) ;
2019-09-08 18:54:41 +02:00
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 ) {
let tag = decreaseTagVersion ( baseTag ) ;
while ( true ) {
try {
const logs = await gitLog ( tag ) ;
if ( logs . 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 ;
}
}
tag = decreaseTagVersion ( tag ) ;
}
}
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.' ) ;
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 ) ;
2019-09-09 19:50:04 +02:00
if ( ! toTagName ) toTagName = await findFirstRelevantTag ( fromTagName ) ;
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
2019-09-19 23:51:18 +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 ) ;
} ) ;