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' ;
2023-09-27 01:01:52 +02:00
import { compareVersions } from 'compare-versions' ;
2021-08-28 11:49:31 +02:00
interface LogEntry {
message : string ;
commit : string ;
author : Author ;
2023-03-09 18:25:21 +02:00
files : string [ ] ;
2021-08-28 11:49:31 +02:00
}
enum Platform {
2023-05-16 10:39:28 +02:00
Unknown = 'unknown' ,
2021-08-28 11:49:31 +02:00
Android = 'android' ,
Ios = 'ios' ,
Desktop = 'desktop' ,
Clipper = 'clipper' ,
Server = 'server' ,
2022-02-23 11:02:13 +02:00
Cloud = 'cloud' ,
2021-08-28 11:49:31 +02:00
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 ( ) ;
2023-03-09 18:25:21 +02:00
let files : string [ ] = [ ] ;
const filesResult = await execCommand ( ` git diff-tree --no-commit-id --name-only ${ commit } -r ` ) ;
files = filesResult . split ( '\n' ) . map ( s = > s . trim ( ) ) . filter ( s = > ! ! s ) ;
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
} ,
2023-03-09 18:25:21 +02:00
files ,
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' ) ;
2023-06-06 17:43:46 +02:00
const collator = new Intl . Collator ( undefined , { numeric : true , sensitivity : 'base' } ) ;
return lines
. split ( '\n' )
. map ( l = > l . trim ( ) )
. filter ( l = > ! ! l )
. sort ( ( a , b ) = > collator . compare ( a , b ) ) ;
2020-09-21 14:01:46 +02:00
}
2021-08-28 11:49:31 +02:00
function platformFromTag ( tagName : string ) : P latform {
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 ;
2022-02-23 11:02:13 +02:00
if ( tagName . indexOf ( 'cloud' ) === 0 ) return Platform . Cloud ;
2021-08-28 11:49:31 +02:00
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 ;
2023-05-16 10:39:28 +02:00
return Platform . Unknown ;
// throw new Error(`Could not determine platform from tag: "${tagName}"`);
2019-04-22 20:02:45 +02:00
}
2023-03-09 18:25:21 +02:00
export const filesApplyToPlatform = ( files : string [ ] , platform : string ) : boolean = > {
const isMainApp = [ 'android' , 'ios' , 'desktop' , 'cli' , 'server' ] . includes ( platform ) ;
const isMobile = [ 'android' , 'ios' ] . includes ( platform ) ;
for ( const file of files ) {
if ( file . startsWith ( 'packages/app-cli' ) && platform === 'cli' ) return true ;
if ( file . startsWith ( 'packages/app-clipper' ) && platform === 'clipper' ) return true ;
if ( file . startsWith ( 'packages/app-mobile' ) && isMobile ) return true ;
if ( file . startsWith ( 'packages/app-desktop' ) && platform === 'desktop' ) return true ;
if ( file . startsWith ( 'packages/fork-htmlparser2' ) && isMainApp ) return true ;
if ( file . startsWith ( 'packages/fork-uslug' ) && isMainApp ) return true ;
if ( file . startsWith ( 'packages/htmlpack' ) && isMainApp ) return true ;
if ( file . startsWith ( 'packages/lib' ) && isMainApp ) return true ;
if ( file . startsWith ( 'packages/pdf-viewer' ) && platform === 'desktop' ) return true ;
if ( file . startsWith ( 'packages/react-native-' ) && isMobile ) return true ;
if ( file . startsWith ( 'packages/renderer' ) && isMainApp ) return true ;
if ( file . startsWith ( 'packages/server' ) && platform === 'server' ) return true ;
if ( file . startsWith ( 'packages/tools' ) && isMainApp ) return true ;
if ( file . startsWith ( 'packages/turndown' ) && isMainApp ) return true ;
}
return false ;
} ;
export interface RenovateMessage {
package : string ;
version : string ;
}
export const parseRenovateMessage = ( message : string ) : RenovateMessage = > {
const regexes = [
/^Update dependency ([^\s]+) to ([^\s]+)/ ,
/^Update ([^\s]+) monorepo to ([^\s]+)/ ,
2023-05-03 13:47:40 +02:00
/^Update ([^\s]+)/ ,
2023-03-09 18:25:21 +02:00
] ;
for ( const regex of regexes ) {
const m = message . match ( regex ) ;
if ( m ) {
return {
package : m [ 1 ] ,
2023-05-03 13:47:40 +02:00
version : m.length >= 3 ? m [ 2 ] : '' ,
2023-03-09 18:25:21 +02:00
} ;
}
}
throw new Error ( ` Not a Renovate message: ${ message } ` ) ;
} ;
export const summarizeRenovateMessages = ( messages : RenovateMessage [ ] ) : string = > {
// Exclude some dev dependencies
messages = messages . filter ( m = > {
if ( [
2023-12-31 02:23:27 +02:00
'@electron/notarize' ,
'eslint' ,
2023-03-09 18:25:21 +02:00
'gettext-extractor' ,
2023-12-31 02:23:27 +02:00
'gettext-parser' ,
'jest' ,
'lint-staged' ,
'madge' ,
2023-03-09 18:25:21 +02:00
'ts-jest' ,
2023-12-31 02:23:27 +02:00
'ts-loader' ,
2023-03-09 18:25:21 +02:00
'ts-node' ,
'typescript' ,
2023-12-31 02:23:27 +02:00
'yeoman-generator' ,
'nodemon' ,
2023-03-09 18:25:21 +02:00
] . includes ( m . package ) ) {
return false ;
}
if ( m . package . startsWith ( '@types/' ) ) return false ;
if ( m . package . startsWith ( 'typescript-' ) ) return false ;
2023-12-31 02:23:27 +02:00
if ( m . package . startsWith ( '@testing-' ) ) return false ;
2023-03-09 18:25:21 +02:00
return true ;
} ) ;
const temp : Record < string , string > = { } ;
for ( const message of messages ) {
2023-05-03 13:47:40 +02:00
if ( ! ( message . package in temp ) ) {
2023-03-09 18:25:21 +02:00
temp [ message . package ] = message . version ;
} else {
if ( message . version > temp [ message . package ] ) {
temp [ message . package ] = message . version ;
}
}
}
const temp2 : string [ ] = [ ] ;
for ( const [ pkg , version ] of Object . entries ( temp ) ) {
2023-05-03 13:47:40 +02:00
const versionString = version ? ` ( ${ version } ) ` : '' ;
temp2 . push ( ` ${ pkg } ${ versionString } ` ) ;
2023-03-09 18:25:21 +02:00
}
temp2 . sort ( ) ;
if ( temp2 . length ) return ` Updated packages ${ temp2 . join ( ', ' ) } ` ;
return '' ;
} ;
2023-06-06 13:04:06 +02:00
const versionFromTag = ( tag : string ) = > {
const s = tag . split ( '-' ) ;
return s [ s . length - 1 ] ;
} ;
2021-08-28 11:49:31 +02:00
function filterLogs ( logs : LogEntry [ ] , platform : Platform ) {
2022-04-03 20:27:10 +02:00
const output : LogEntry [ ] = [ ] ;
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
2023-03-09 18:25:21 +02:00
const renovateMessages : RenovateMessage [ ] = [ ] ;
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 ;
2023-03-09 18:25:21 +02:00
let isRenovate = false ;
2019-05-03 01:19:42 +02:00
let prefix = log . message . trim ( ) . toLowerCase ( ) . split ( ':' ) ;
2023-03-09 18:25:21 +02:00
if ( prefix . length <= 1 ) {
if ( log . author && log . author . name === 'renovate[bot]' ) {
prefix = [ 'renovate' ] ;
isRenovate = true ;
} else {
continue ;
}
} else {
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.
2022-02-23 11:02:13 +02:00
if ( prefix . indexOf ( 'all' ) >= 0 && ( platform !== 'clipper' && platform !== 'server' && platform !== 'cloud' ) ) 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 ;
2022-04-03 20:27:10 +02:00
if ( platform === 'cloud' && ( prefix . indexOf ( 'cloud' ) >= 0 || prefix . indexOf ( 'server' ) >= 0 ) ) addIt = true ;
2019-04-22 20:02:45 +02:00
2023-03-09 18:25:21 +02:00
if ( isRenovate && filesApplyToPlatform ( log . files , platform ) ) {
renovateMessages . push ( parseRenovateMessage ( log . message ) ) ;
addIt = false ;
}
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.
2024-07-06 12:19:17 +02:00
if ( log . message . match ( /Translation:\sUpdate\s.*?(\.po|[a-zA-Z][a-zA-Z]|[a-zA-Z][a-zA-Z]_[a-zA-Z][a-zA-Z])/ )
|| log . message . match ( /Update.+\.po/ )
) {
2020-08-05 00:00:11 +02:00
// updatedTranslations = true;
2020-04-20 20:10:27 +02:00
addIt = false ;
}
2022-04-03 20:27:10 +02:00
// Remove duplicate messages
if ( output . find ( l = > l . message === log . message ) ) {
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
2023-03-09 18:25:21 +02:00
const renovateSummary = summarizeRenovateMessages ( renovateMessages ) ;
if ( renovateSummary ) {
output . push ( {
author : { name : '' , email : '' , login : '' } ,
commit : '' ,
files : [ ] ,
message : renovateSummary ,
} ) ;
}
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 {
2023-06-01 13:02:36 +02:00
options = { publishFormat : 'full' , . . . options } ;
2020-01-25 12:42:36 +02:00
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 ) {
2022-02-23 11:02:13 +02:00
if ( [ 'android' , 'mobile' , 'ios' , 'desktop' , 'cli' , 'clipper' , 'all' , 'api' , 'plugins' , 'server' , 'cloud' ] . 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 } ` ) ;
2023-03-09 18:25:21 +02:00
if ( commitStrings . join ( '' ) . length ) {
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 ;
2023-06-06 13:04:06 +02:00
const baseVersion = versionFromTag ( baseTag ) ;
2020-09-21 14:01:46 +02:00
for ( let i = baseTagIndex - 1 ; i >= 0 ; i -- ) {
const tag = allTags [ i ] ;
if ( platformFromTag ( tag ) !== platform ) continue ;
2023-06-06 13:04:06 +02:00
const currentVersion = versionFromTag ( tag ) ;
if ( compareVersions ( baseVersion , currentVersion ) <= 0 ) continue ;
2020-09-21 14:01:46 +02:00
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' ) ) ;
}
2023-03-09 18:25:21 +02:00
if ( require . main === module ) {
// eslint-disable-next-line promise/prefer-await-to-then
main ( ) . catch ( ( error ) = > {
console . error ( 'Fatal error' ) ;
console . error ( error ) ;
process . exit ( 1 ) ;
} ) ;
}