Skip to content

Commit

Permalink
feat!: switch to ESM, fix output colourising
Browse files Browse the repository at this point in the history
* upgrade all deps, including ESM-only deps
* --simple is now default output, including colourising, with
  --markdown being an opt-in output format.
* move process + print logic to separate module for exporting
  for simplifying branch-diff
* add --sha and --reverse options from branch-diff to dedupe some
  processing code

Fixes: #120
Closes: #107
Closes: #119
  • Loading branch information
rvagg committed Dec 3, 2021
1 parent caeeabd commit baaf077
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 238 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ npm i changelog-maker -g

## Usage

**`changelog-maker [--simple] [--group] [--commit-url=<url/with/{ref}>] [--start-ref=<ref>] [--end-ref=<ref>] [github-user[, github-project]]`**
**`changelog-maker [--plaintext|p] [--markdown|md] [--group|-g] [--commit-url=<url/with/{ref}>] [--start-ref=<ref>] [--end-ref=<ref>] [github-user[, github-project]]`**

`github-user` and `github-project` should point to the GitHub repository that can be used to find the `PR-URL` data if just an issue number is provided and will also impact how the PR-URL issue numbers are displayed

* `--simple`: print a simple form, without additional Markdown cruft
* `--plaintext`: print a very simple form, without commit details, implies `--group`
* `--markdown`: print a Markdown formatted from, with links and proper escaping
* `--sha`: print only the list of short-form commit hashes
* `--group`: reorder commits so that they are listed in groups where the `xyz:` prefix of the commit message defines the group. Commits are listed in original order _within_ group.
* `--reverse`: reverse the order of commits when printed, does not work with `--reverse`
* `--commit-url`: pass in a url template which will be used to generate commit URLs for a repository not hosted in Github. `{ref}` is the placeholder that will be replaced with the commit, i.e. `--commit-url=https://gitlab.com/myUser/myRepo/commit/{ref}`
* `--start-ref=<ref>`: use the given git `<ref>` as a starting point rather than the _last tag_. The `<ref>` can be anything commit-ish including a commit sha, tag, branch name. If you specify a `--start-ref` argument the commit log will not be pruned so that version commits and `working on <version>` commits are left in the list.
* `--end-ref=<ref>`: use the given git `<ref>` as a end-point rather than the _now_. The `<ref>` can be anything commit-ish including a commit sha, tag, branch name.
Expand Down
168 changes: 48 additions & 120 deletions changelog-maker.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,25 @@
#!/usr/bin/env node

'use strict'

const fs = require('fs')
const path = require('path')
const split2 = require('split2')
const list = require('list-stream')
const stripAnsi = require('strip-ansi')
const pkgtoId = require('pkg-to-id')
const commitStream = require('commit-stream')
const gitexec = require('gitexec')
const { commitToOutput, formatType } = require('./commit-to-output')
const groupCommits = require('./group-commits')
const collectCommitLabels = require('./collect-commit-labels')
const { isReleaseCommit, toGroups } = require('./groups')
const pkg = require('./package.json')
const debug = require('debug')(pkg.name)
const argv = require('minimist')(process.argv.slice(2))

// Skip on formatting on Node.js 10.
const formatMarkdown = process.versions.node.startsWith('10.') ? false : import('./format.mjs')

const quiet = argv.quiet || argv.q
import { existsSync, readFileSync } from 'fs'
import { join } from 'path'
import process from 'process'
import { pipeline } from 'stream/promises'
import split2 from 'split2'
import pkgtoId from 'pkg-to-id'
import commitStream from 'commit-stream'
import gitexec from 'gitexec'
import _debug from 'debug'
import minimist from 'minimist'
import { processCommits } from './process-commits.js'
import { isReleaseCommit } from './groups.js'

const debug = _debug('changelog-maker')
const argv = minimist(process.argv.slice(2))
const help = argv.h || argv.help
const commitUrl = argv['commit-url'] || 'https://github.com/{ghUser}/{ghRepo}/commit/{ref}'
const pkgFile = path.join(process.cwd(), 'package.json')
const pkgData = fs.existsSync(pkgFile) ? require(pkgFile) : {}
const pkgFile = join(process.cwd(), 'package.json')
const pkgData = existsSync(pkgFile) ? JSON.parse(readFileSync(pkgFile)) : {}
const pkgId = pkgtoId(pkgData)

const getFormat = () => {
if (argv.simple || argv.s) {
return formatType.SIMPLE
} else if (argv.plaintext || argv.p) {
return formatType.PLAINTEXT
}
return formatType.MARKDOWN
}

const ghId = {
user: argv._[0] || pkgId.user || 'nodejs',
repo: argv._[1] || (pkgId.name && stripScope(pkgId.name)) || 'node'
Expand All @@ -58,14 +41,11 @@ if (help) {
}

function showUsage () {
let usage = fs.readFileSync(path.join(__dirname, 'README.md'), 'utf8')
const usage = readFileSync(new URL('README.md', import.meta.url), 'utf8')
.replace(/[\s\S]+(## Usage\n[\s\S]*)\n## [\s\S]+/m, '$1')
if (process.stdout.isTTY) {
usage = usage
.replace(/## Usage\n[\s]*/m, '')
.replace(/\*\*/g, '')
.replace(/`/g, '')
}
.replace(/## Usage\n[\s]*/m, 'Usage: ')
.replace(/\*\*/g, '')
.replace(/`/g, '')

process.stdout.write(usage)
}
Expand All @@ -81,6 +61,18 @@ function replace (s, m) {
return s
}

const _startrefcmd = replace(refcmd, { ref: argv['start-ref'] || defaultRef })
const _endrefcmd = argv['end-ref'] && replace(refcmd, { ref: argv['end-ref'] })
const _sincecmd = replace(commitdatecmd, { refcmd: _startrefcmd })
const _untilcmd = argv['end-ref'] ? replace(commitdatecmd, { refcmd: _endrefcmd }) : untilcmd
const _gitcmd = replace(gitcmd, { sincecmd: _sincecmd, untilcmd: _untilcmd })

debug('%s', _startrefcmd)
debug('%s', _endrefcmd)
debug('%s', _sincecmd)
debug('%s', _untilcmd)
debug('%s', _gitcmd)

function organiseCommits (list) {
if (argv['start-ref'] || argv.a || argv.all) {
if (argv['filter-release']) {
Expand All @@ -105,86 +97,22 @@ function organiseCommits (list) {
})
}

async function printCommits (list) {
for await (let commit of list) {
if (!process.stdout.isTTY) {
commit = stripAnsi(commit)
}
process.stdout.write(commit)
}
}

function onCommitList (err, list) {
if (err) {
return fatal(err)
}

list = organiseCommits(list)

collectCommitLabels(list, (err) => {
if (err) {
return fatal(err)
}

if (argv.group) {
list = groupCommits(list)
}

const format = getFormat()
if (format === formatType.PLAINTEXT) {
const formatted = []

let currentGroup
for (const commit of list) {
const commitGroup = toGroups(commit.summary)
if (currentGroup !== commitGroup) {
formatted.push(`${commitGroup}:`)
currentGroup = commitGroup
}
formatted.push(commitToOutput(commit, formatType.PLAINTEXT, ghId, commitUrl))
async function run () {
let commitList = []
await pipeline(
gitexec.exec(process.cwd(), _gitcmd),
split2(),
commitStream(ghId.user, ghId.repo),
async function * (source) {
for await (const commit of source) {
commitList.push(commit)
}

list = formatted.map((line) => `${line}\n`)
} else {
list = list.map(async (commit) => {
let output = commitToOutput(commit, format, ghId, commitUrl)
if (format === formatType.MARKDOWN) {
if (!process.stdout.isTTY) {
output = stripAnsi(output)
}
if (process.versions.node.startsWith('10.')) {
return `${output}\n`
}
return formatMarkdown.then((module) => module.default(output))
}
return `${output}\n`
})
}

if (!quiet) {
printCommits(list)
}
})
})
commitList = organiseCommits(commitList)
await processCommits(argv, ghId, commitList)
}

function fatal (err) {
console.error(`Fatal error: ${err.message}`)
run().catch((err) => {
console.error(err)
process.exit(1)
}

const _startrefcmd = replace(refcmd, { ref: argv['start-ref'] || defaultRef })
const _endrefcmd = argv['end-ref'] && replace(refcmd, { ref: argv['end-ref'] })
const _sincecmd = replace(commitdatecmd, { refcmd: _startrefcmd })
const _untilcmd = argv['end-ref'] ? replace(commitdatecmd, { refcmd: _endrefcmd }) : untilcmd
const _gitcmd = replace(gitcmd, { sincecmd: _sincecmd, untilcmd: _untilcmd })

debug('%s', _startrefcmd)
debug('%s', _endrefcmd)
debug('%s', _sincecmd)
debug('%s', _untilcmd)
debug('%s', _gitcmd)

gitexec.exec(process.cwd(), _gitcmd)
.pipe(split2())
.pipe(commitStream(ghId.user, ghId.repo))
.pipe(list.obj(onCommitList))
})
80 changes: 38 additions & 42 deletions collect-commit-labels.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,63 @@
'use strict'

const ghauth = require('ghauth')
const ghissues = require('ghissues')
const async = require('async')
import { promisify } from 'util'
import ghauth from 'ghauth'
import ghissues from 'ghissues'
import async from 'async'

const authOptions = {
configName: 'changelog-maker',
scopes: ['repo'],
noDeviceFlow: true
}

function collectCommitLabels (list, callback) {
export async function collectCommitLabels (list) {
const sublist = list.filter((commit) => {
return typeof commit.ghIssue === 'number' && commit.ghUser && commit.ghProject
})

if (!sublist.length) {
return setImmediate(callback)
return
}

ghauth(authOptions, (err, authData) => {
if (err) {
return callback(err)
}

const cache = {}

const q = async.queue((commit, next) => {
function onFetch (err, issue) {
if (err) {
console.error('Error fetching issue #%s: %s', commit.ghIssue, err.message)
return next()
}
const authData = await promisify(ghauth)(authOptions)

if (issue.labels) {
commit.labels = issue.labels.map((label) => label.name)
}
const cache = {}

next()
const q = async.queue((commit, next) => {
function onFetch (err, issue) {
if (err) {
console.error('Error fetching issue #%s: %s', commit.ghIssue, err.message)
return next()
}

if (commit.ghUser === 'iojs') {
commit.ghUser = 'nodejs' // forcibly rewrite as the GH API doesn't do it for us
if (issue.labels) {
commit.labels = issue.labels.map((label) => label.name)
}

// To prevent multiple simultaneous requests for the same issue
// from hitting the network at the same time, immediately assign a Promise
// to the cache that all commits with the same ghIssue value will use.
const key = `${commit.ghUser}/${commit.ghProject}#${commit.ghIssue}`
cache[key] = cache[key] || new Promise((resolve, reject) => {
ghissues.get(authData, commit.ghUser, commit.ghProject, commit.ghIssue, (err, issue) => {
if (err) {
return reject(err)
}
next()
}

resolve(issue)
})
if (commit.ghUser === 'iojs') {
commit.ghUser = 'nodejs' // forcibly rewrite as the GH API doesn't do it for us
}

// To prevent multiple simultaneous requests for the same issue
// from hitting the network at the same time, immediately assign a Promise
// to the cache that all commits with the same ghIssue value will use.
const key = `${commit.ghUser}/${commit.ghProject}#${commit.ghIssue}`
cache[key] = cache[key] || new Promise((resolve, reject) => {
ghissues.get(authData, commit.ghUser, commit.ghProject, commit.ghIssue, (err, issue) => {
if (err) {
return reject(err)
}

resolve(issue)
})
cache[key].then((val) => onFetch(null, val), (err) => onFetch(err))
}, 15)
q.drain(callback)
q.push(sublist)
})
}
})
cache[key].then((val) => onFetch(null, val), (err) => onFetch(err))
}, 15)

module.exports = collectCommitLabels
q.push(sublist)
await q.drain()
}
29 changes: 12 additions & 17 deletions commit-to-output.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
'use strict'

const chalk = require('chalk')
const reverts = require('./reverts')
const groups = require('./groups')
import chalk from 'chalk'
import { isRevert, cleanSummary as cleanRevertSummary } from './reverts.js'
import { toGroups, cleanSummary as cleanGroupSummary } from './groups.js'

function cleanUnsupportedMarkdown (txt) {
// escape _~*\[]<>`
return txt.replace(/([_~*\\[\]<>`])/g, '\\$1')
}

function cleanMarkdown (txt) {
// Escape backticks for edge case scenarii (no code span support).
if (txt.includes('``') || txt.includes('\\`')) {
Expand All @@ -30,7 +29,8 @@ function cleanMarkdown (txt) {
return cleanMdString
}

const formatType = {
export const formatType = {
SHA: 'sha',
PLAINTEXT: 'plaintext',
MARKDOWN: 'markdown',
SIMPLE: 'simple'
Expand Down Expand Up @@ -65,7 +65,7 @@ function toStringSimple (data) {
s = s.trim()

return (data.semver && data.semver.length)
? chalk.green(chalk.bold(s))
? chalk.green.bold(s)
: (data.group === 'doc'
? chalk.grey(s)
: s)
Expand All @@ -84,13 +84,13 @@ function toStringMarkdown (data) {
s = s.trim()

return (data.semver && data.semver.length)
? chalk.green(chalk.bold(s))
? chalk.green.bold(s)
: (data.group === 'doc'
? chalk.grey(s)
: s)
}

function commitToOutput (commit, format, ghId, commitUrl) {
export function commitToOutput (commit, format, ghId, commitUrl) {
const data = {}
const prUrlMatch = commit.prUrl && commit.prUrl.match(/^https?:\/\/.+\/([^/]+\/[^/]+)\/\w+\/\d+$/i)
const urlHash = `#${commit.ghIssue}` || commit.prUrl
Expand All @@ -99,9 +99,9 @@ function commitToOutput (commit, format, ghId, commitUrl) {
data.sha = commit.sha
data.shaUrl = commitUrl.replace(/\{ghUser\}/g, ghId.user).replace(/\{ghRepo\}/g, ghId.repo).replace(/\{ref\}/g, ref)
data.semver = commit.labels && commit.labels.filter((l) => l.includes('semver'))
data.revert = reverts.isRevert(commit.summary)
data.group = groups.toGroups(commit.summary)
data.summary = groups.cleanSummary(reverts.cleanSummary(commit.summary))
data.revert = isRevert(commit.summary)
data.group = toGroups(commit.summary)
data.summary = cleanGroupSummary(cleanRevertSummary(commit.summary))
data.author = (commit.author && commit.author.name) || ''
data.pr = prUrlMatch && ((prUrlMatch[1] !== `${ghId.user}/${ghId.repo}` ? prUrlMatch[1] : '') + urlHash)
data.prUrl = prUrlMatch && commit.prUrl
Expand All @@ -114,8 +114,3 @@ function commitToOutput (commit, format, ghId, commitUrl) {

return toStringMarkdown(data)
}

module.exports = {
commitToOutput,
formatType
}
Loading

0 comments on commit baaf077

Please # to comment.