Skip to content

feat!: switch to ESM, fix output colourising, dedupe more code w/ branch-diff #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node: [12, 14, 16]
node: [14, 16]
# windows support not quite ready: os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest]

Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,20 @@ 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] [--sha] [--group|-g] [--reverse] [--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
* `--format`: dictates what formatting the output will have. Possible options are: `simple`, `markdown`, `plaintext`, and `sha`. The default is to print a `simple` output suitable for stdout.
- `simple`: don't print full markdown output, good for console printing without the additional fluff.
- `sha`: print only the 10-character truncated commit hashes.
- `plaintext`: a very simple form, without commit details, implies `--group`.
- `markdown`: a Markdown formatted from, with links and proper escaping.
* `--sha`: same as `--format=sha`.
* `--plaintext`: same as `--format=plaintext`.
* `--markdown`: same as `--format=markdown`.
* `--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
170 changes: 50 additions & 120 deletions changelog-maker.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,27 @@
#!/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 as _pipeline } from 'stream'
import { promisify } from 'util'
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 pipeline = promisify(_pipeline)
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 +43,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 +63,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 +99,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()
}
Loading