const pMap = require ( "p-map" ) ; import progressBar from "./progress-bar" ; import { Configuration } from "./configuration" ; import findPullRequestId from "./find-pull-request-id" ; import * as Git from "./git" ; import GithubAPI , { GitHubUserResponse } from "./github-api" ; import { CommitInfo , Release } from "./interfaces" ; import MarkdownRenderer from "./markdown-renderer" ; const UNRELEASED_TAG = "___unreleased___" ; interface Options { tagFrom ? : string ; tagTo ? : string ; } export default class Changelog { private readonly config : Configuration ; private github : GithubAPI ; private renderer : MarkdownRenderer ; constructor ( config ) { this . config = config ; this . github = new GithubAPI ( this . config ) ; this . renderer = new MarkdownRenderer ( { categories : Object . keys ( this . config . labels ) . map ( key => this . config . labels [ key ] ) , baseIssueUrl : this . github . getBaseIssueUrl ( this . config . repo ) , unreleasedName : this . config . nextVersion || "Unreleased" , } ) ; } public async createMarkdown ( options = { } ) { const from = options . tagFrom || ( await Git . lastTag ( ) ) ; const to = options . tagTo || "HEAD" ; const releases = await this . listReleases ( from , to ) ; return this . renderer . renderMarkdown ( releases ) ; } private async getCommitInfos ( from , to ) { const commits = this . getListOfCommits ( from , to ) ; const commitInfos = this . toCommitInfos ( commits ) ; await this . downloadIssueData ( commitInfos ) ; this . fillInCategories ( commitInfos ) ; await this . fillInPackages ( commitInfos ) ; return commitInfos ; } private async listReleases ( from , to ) { const commits = await this . getCommitInfos ( from , to ) ; let releases = this . groupByRelease ( commits ) ; await this . fillInContributors ( releases ) ; return releases ; } private async getListOfUniquePackages ( sha ) { return ( await Git . changedPaths ( sha ) ) . map ( path => this . packageFromPath ( path ) ) . filter ( Boolean ) . filter ( onlyUnique ) ; } private packageFromPath ( path ) { const parts = path . split ( "/" ) ; if ( parts [ 0 ] !== "packages" || parts . length < 3 ) { return "" ; } if ( parts . length >= 4 && parts [ 1 ] [ 0 ] === "@" ) { return `${ parts [ 1 ] }/${ parts [ 2 ] }` ; } return parts [ 1 ] ; } private getListOfCommits ( from , to ) { return Git . listCommits ( from , to ) ; } private async getCommitters ( commits ) { const committers = { } ; for ( const commit of commits ) { const issue = commit . githubIssue ; const login = issue && issue . user && issue . user . login ; const shouldKeepCommiter = login && ! this . ignoreCommitter ( login ) ; if ( login && shouldKeepCommiter && ! committers [ login ] ) { committers [ login ] = await this . github . getUserData ( login ) ; } } return Object . keys ( committers ) . map ( k => committers [ k ] ) ; } private ignoreCommitter ( login ) { return this . config . ignoreCommitters . some ( ( c ) => c === login || login . indexOf ( c ) > - 1 ) ; } private toCommitInfos ( commits ) { return commits . map ( commit => { const { sha , refName , summary : message , date } = commit ; let tagsInCommit ; if ( refName . length > 1 ) { const TAG_PREFIX = "tag: " ; tagsInCommit = refName . split ( ", " ) . filter ( ref => ref . startsWith ( TAG_PREFIX ) ) . map ( ref => ref . substr ( TAG_PREFIX . length ) ) ; } const issueNumber = findPullRequestId ( message ) ; return { commitSHA : sha , message , tags : tagsInCommit , issueNumber , date , } as CommitInfo ; } ) ; } private async downloadIssueData ( commitInfos ) { progressBar . init ( "Downloading issue information…" , commitInfos . length ) ; await pMap ( commitInfos , async ( commitInfo ) => { if ( commitInfo . issueNumber ) { commitInfo . githubIssue = await this . github . getIssueData ( this . config . repo , commitInfo . issueNumber ) ; } progressBar . tick ( ) ; } , { concurrency : 5 } ) ; progressBar . terminate ( ) ; } private groupByRelease ( commits ) { let releaseMap = { } ; let currentTags = [ UNRELEASED_TAG ] ; for ( const commit of commits ) { if ( commit . tags && commit . tags . length > 0 ) { currentTags = commit . tags ; } for ( const currentTag of currentTags ) { if ( ! releaseMap [ currentTag ] ) { let date = currentTag === UNRELEASED_TAG ? this . getToday ( ) : commit . date ; releaseMap [ currentTag ] = { name : currentTag , date , commits : [ ] } ; } releaseMap [ currentTag ] . commits . push ( commit ) ; } } return Object . keys ( releaseMap ) . map ( tag => releaseMap [ tag ] ) ; } private getToday ( ) { const date = new Date ( ) . toISOString ( ) ; return date . slice ( 0 , date . indexOf ( "T" ) ) ; } private fillInCategories ( commits ) { for ( const commit of commits ) { if ( ! commit . githubIssue || ! commit . githubIssue . labels ) continue ; const labels = commit . githubIssue . labels . map ( label => label . name . toLowerCase ( ) ) ; commit . categories = Object . keys ( this . config . labels ) . filter ( label => labels . indexOf ( label . toLowerCase ( ) ) !== - 1 ) . map ( label => this . config . labels [ label ] ) ; } } private async fillInPackages ( commits ) { progressBar . init ( "Mapping commits to packages…" , commits . length ) ; try { await pMap ( commits , async ( commit ) => { commit . packages = await this . getListOfUniquePackages ( commit . commitSHA ) ; progressBar . tick ( ) ; } , { concurrency : 5 } ) ; } finally { progressBar . terminate ( ) ; } } private async fillInContributors ( releases ) { for ( const release of releases ) { release . contributors = await this . getCommitters ( release . commits ) ; } } } function onlyUnique ( value , index , self ) { return self . indexOf ( value ) === index ; }