Skip to content

Commit

Permalink
Mild refactor
Browse files Browse the repository at this point in the history
- Move any tag-specific logic from releases.js to tags.js
- Combine remote and latestVersion into options object to be passed around
- Use const rather than function where possible
- Hopefully fixes various bugs with single tags, including cookpete#174
  • Loading branch information
cookpete committed Jul 27, 2020
1 parent 4ac8830 commit 1fc916c
Show file tree
Hide file tree
Showing 12 changed files with 336 additions and 351 deletions.
36 changes: 18 additions & 18 deletions src/commits.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,26 @@ const MERGE_PATTERNS = [
/^Merge branch .+ into .+\n\n(.+)[\S\s]+See merge request [^!]*!(\d+)/ // GitLab merge
]

async function fetchCommits (diff, remote, options = {}) {
const fetchCommits = async (diff, options = {}) => {
const format = await getLogFormat()
const log = await cmd(`git log ${diff} --shortstat --pretty=format:${format} ${options.appendGitLog}`)
return parseCommits(log, remote, options)
return parseCommits(log, options)
}

async function getLogFormat () {
const getLogFormat = async () => {
const gitVersion = await getGitVersion()
const bodyFormat = gitVersion && semver.gte(gitVersion, '1.7.2') ? BODY_FORMAT : FALLBACK_BODY_FORMAT
return `${COMMIT_SEPARATOR}%H%n%ai%n%an%n%ae%n${bodyFormat}${MESSAGE_SEPARATOR}`
}

function parseCommits (string, remote, options = {}) {
const parseCommits = (string, options = {}) => {
return string
.split(COMMIT_SEPARATOR)
.slice(1)
.map(commit => parseCommit(commit, remote, options))
.map(commit => parseCommit(commit, options))
}

function parseCommit (commit, remote, options = {}) {
const parseCommit = (commit, options = {}) => {
const [, hash, date, author, email, tail] = commit.match(MATCH_COMMIT)
const [body, stats] = tail.split(MESSAGE_SEPARATOR)
const message = encodeHTML(body)
Expand All @@ -49,25 +49,25 @@ function parseCommit (commit, remote, options = {}) {
date: new Date(date).toISOString(),
subject: replaceText(getSubject(message), options),
message: message.trim(),
fixes: getFixes(message, author, remote, options),
href: remote.getCommitLink(hash),
fixes: getFixes(message, author, options),
href: options.getCommitLink(hash),
breaking: !!options.breakingPattern && new RegExp(options.breakingPattern).test(message),
...getStats(stats)
}
return {
...parsed,
merge: getMerge(parsed, message, remote, options)
merge: getMerge(parsed, message, options)
}
}

function getSubject (message) {
const getSubject = (message) => {
if (!message) {
return '_No commit message_'
}
return message.match(/[^\n]+/)[0]
}

function getStats (stats) {
const getStats = (stats) => {
if (!stats.trim()) return {}
const [, files, insertions, deletions] = stats.match(MATCH_STATS)
return {
Expand All @@ -77,21 +77,21 @@ function getStats (stats) {
}
}

function getFixes (message, author, remote, options = {}) {
const getFixes = (message, author, options = {}) => {
const pattern = getFixPattern(options)
const fixes = []
let match = pattern.exec(message)
if (!match) return null
while (match) {
const id = getFixID(match)
const href = isLink(match[2]) ? match[2] : remote.getIssueLink(id)
const href = isLink(match[2]) ? match[2] : options.getIssueLink(id)
fixes.push({ id, href, author })
match = pattern.exec(message)
}
return fixes
}

function getFixID (match) {
const getFixID = (match) => {
// Get the last non-falsey value in the match array
for (let i = match.length; i >= 0; i--) {
if (match[i]) {
Expand All @@ -100,21 +100,21 @@ function getFixID (match) {
}
}

function getFixPattern (options) {
const getFixPattern = (options) => {
if (options.issuePattern) {
return new RegExp(options.issuePattern, 'g')
}
return DEFAULT_FIX_PATTERN
}

function getMergePatterns (options) {
const getMergePatterns = (options) => {
if (options.mergePattern) {
return MERGE_PATTERNS.concat(new RegExp(options.mergePattern, 'g'))
}
return MERGE_PATTERNS
}

function getMerge (commit, message, remote, options = {}) {
const getMerge = (commit, message, options = {}) => {
const patterns = getMergePatterns(options)
for (const pattern of patterns) {
const match = pattern.exec(message)
Expand All @@ -124,7 +124,7 @@ function getMerge (commit, message, remote, options = {}) {
return {
id,
message: replaceText(message, options),
href: remote.getMergeLink(id),
href: options.getMergeLink(id),
author: commit.author,
commit
}
Expand Down
118 changes: 39 additions & 79 deletions src/releases.js
Original file line number Diff line number Diff line change
@@ -1,72 +1,35 @@
const semver = require('semver')
const { fetchCommits } = require('./commits')
const { niceDate } = require('./utils')

const MERGE_COMMIT_PATTERN = /^Merge (remote-tracking )?branch '.+'/
const COMMIT_MESSAGE_PATTERN = /\n+([\S\s]+)/

async function createRelease (tag, previousTag, date, diff, remote, options, onParsed) {
const commits = await fetchCommits(diff, remote, options)
const merges = commits.filter(commit => commit.merge).map(commit => commit.merge)
const fixes = commits.filter(commit => commit.fixes).map(commit => ({ fixes: commit.fixes, commit }))
const emptyRelease = merges.length === 0 && fixes.length === 0
const { message } = commits[0] || { message: null }
const breakingCount = commits.filter(c => c.breaking).length
const filteredCommits = commits
.filter(commit => filterCommit(commit, options, merges))
.sort(commitSorter(options))
.slice(0, getCommitLimit(options, emptyRelease, breakingCount))
const release = {
tag,
title: tag || 'Unreleased',
date,
isoDate: date.slice(0, 10),
niceDate: niceDate(date),
commits: filteredCommits,
merges,
fixes,
summary: getSummary(message, options),
major: Boolean(!options.tagPattern && tag && previousTag && semver.diff(tag, previousTag) === 'major'),
href: getCompareLink(previousTag, tag, remote, options)
}
if (onParsed) {
onParsed(release)
}
return release
}
const parseReleases = async (tags, options, onParsed) => {
return Promise.all(tags.map(async tag => {
const commits = await fetchCommits(tag.diff, options)
const merges = commits.filter(commit => commit.merge).map(commit => commit.merge)
const fixes = commits.filter(commit => commit.fixes).map(commit => ({ fixes: commit.fixes, commit }))
const emptyRelease = merges.length === 0 && fixes.length === 0
const { message } = commits[0] || { message: null }
const breakingCount = commits.filter(c => c.breaking).length
const filteredCommits = commits
.filter(filterCommits(options, merges))
.sort(sortCommits(options))
.slice(0, getCommitLimit(options, emptyRelease, breakingCount))

function parseReleases (tags, remote, latestVersion, options, onParsed) {
const releases = tags.map(({ tag, date }, index, tags) => {
if (tags[index - 1] && tags[index - 1].tag === options.startingVersion) {
return null
}
const previousTag = tags[index + 1] ? tags[index + 1].tag : null
const diff = previousTag ? `${previousTag}..${tag}` : tag
return createRelease(tag, previousTag, date, diff, remote, options, onParsed)
})
if (latestVersion || options.unreleased || options.unreleasedOnly) {
const tag = latestVersion || null
const previousTag = tags[0] ? tags[0].tag : null
const date = new Date().toISOString()
const diff = `${previousTag}..`
const unreleased = createRelease(tag, previousTag, date, diff, remote, options, onParsed)
if (options.unreleasedOnly) {
return Promise.all([unreleased])
}
releases.unshift(unreleased)
}
return Promise.all(releases.filter(release => release))
}
if (onParsed) onParsed(tag)

function getCommitLimit ({ commitLimit, backfillLimit }, emptyRelease, breakingCount) {
if (commitLimit === false) {
return undefined // Return all commits
}
const limit = emptyRelease ? backfillLimit : commitLimit
return Math.max(breakingCount, limit)
return {
...tag,
summary: getSummary(message, options),
commits: filteredCommits,
merges,
fixes
}
}))
}

function filterCommit (commit, { ignoreCommitPattern }, merges) {
const filterCommits = ({ ignoreCommitPattern }, merges) => commit => {
if (commit.fixes || commit.merge) {
// Filter out commits that already appear in fix or merge lists
return false
Expand All @@ -92,33 +55,30 @@ function filterCommit (commit, { ignoreCommitPattern }, merges) {
return true
}

function getSummary (message, { releaseSummary }) {
if (!message || !releaseSummary) {
return null
}
if (COMMIT_MESSAGE_PATTERN.test(message)) {
return message.match(COMMIT_MESSAGE_PATTERN)[1]
}
return null
const sortCommits = ({ sortCommits }) => (a, b) => {
if (!a.breaking && b.breaking) return 1
if (a.breaking && !b.breaking) return -1
if (sortCommits === 'date') return new Date(a.date) - new Date(b.date)
if (sortCommits === 'date-desc') return new Date(b.date) - new Date(a.date)
return (b.insertions + b.deletions) - (a.insertions + a.deletions)
}

function commitSorter ({ sortCommits }) {
return (a, b) => {
if (!a.breaking && b.breaking) return 1
if (a.breaking && !b.breaking) return -1
if (sortCommits === 'date') return new Date(a.date) - new Date(b.date)
if (sortCommits === 'date-desc') return new Date(b.date) - new Date(a.date)
return (b.insertions + b.deletions) - (a.insertions + a.deletions)
const getCommitLimit = ({ commitLimit, backfillLimit }, emptyRelease, breakingCount) => {
if (commitLimit === false) {
return undefined // Return all commits
}
const limit = emptyRelease ? backfillLimit : commitLimit
return Math.max(breakingCount, limit)
}

function getCompareLink (previousTag, tag, remote, { tagPrefix = '' }) {
if (!previousTag) {
const getSummary = (message, { releaseSummary }) => {
if (!message || !releaseSummary) {
return null
}
const from = `${tagPrefix}${previousTag}`
const to = tag ? `${tagPrefix}${tag}` : 'HEAD'
return remote.getCompareLink(from, to)
if (COMMIT_MESSAGE_PATTERN.test(message)) {
return message.match(COMMIT_MESSAGE_PATTERN)[1]
}
return null
}

module.exports = {
Expand Down
6 changes: 3 additions & 3 deletions src/remote.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const parseRepoURL = require('parse-github-url')
const { cmd } = require('./utils')

async function fetchRemote (options) {
const fetchRemote = async options => {
const remoteURL = await cmd(`git config --get remote.${options.remote}.url`)
return getRemote(remoteURL, options)
}

function getRemote (remoteURL, options = {}) {
const getRemote = (remoteURL, options = {}) => {
const overrides = getOverrides(options)
if (!remoteURL) {
// No point warning if everything is overridden
Expand Down Expand Up @@ -81,7 +81,7 @@ function getRemote (remoteURL, options = {}) {
}
}

function getOverrides ({ commitUrl, issueUrl, mergeUrl, compareUrl }) {
const getOverrides = ({ commitUrl, issueUrl, mergeUrl, compareUrl }) => {
const overrides = {}
if (commitUrl) overrides.getCommitLink = id => commitUrl.replace('{id}', id)
if (issueUrl) overrides.getIssueLink = id => issueUrl.replace('{id}', id)
Expand Down
40 changes: 21 additions & 19 deletions src/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const PACKAGE_FILE = 'package.json'
const PACKAGE_OPTIONS_KEY = 'auto-changelog'
const PREPEND_TOKEN = '<!-- auto-changelog-above -->'

async function getOptions (argv) {
const options = new Command()
const getOptions = async argv => {
const commandOptions = new Command()
.option('-o, --output <file>', `output file, default: ${DEFAULT_OPTIONS.output}`)
.option('-c, --config <file>', `config file location, default: ${DEFAULT_OPTIONS.config}`)
.option('-t, --template <template>', `specify template to use [compact, keepachangelog, json], default: ${DEFAULT_OPTIONS.template}`)
Expand Down Expand Up @@ -58,17 +58,23 @@ async function getOptions (argv) {

const pkg = await readJson(PACKAGE_FILE)
const packageOptions = pkg ? pkg[PACKAGE_OPTIONS_KEY] : null
const dotOptions = await readJson(options.config || DEFAULT_OPTIONS.config)

return {
const dotOptions = await readJson(commandOptions.config || DEFAULT_OPTIONS.config)
const options = {
...DEFAULT_OPTIONS,
...dotOptions,
...packageOptions,
...options
...commandOptions
}
const remote = await fetchRemote(options)
const latestVersion = await getLatestVersion(options)
return {
...options,
...remote,
latestVersion
}
}

async function getLatestVersion (options, tags) {
const getLatestVersion = async options => {
if (options.latestVersion) {
if (!semver.valid(options.latestVersion)) {
throw new Error('--latest-version must be a valid semver version')
Expand All @@ -81,45 +87,41 @@ async function getLatestVersion (options, tags) {
throw new Error(`File ${file} does not exist`)
}
const { version } = await readJson(file)
const prefix = tags.some(({ tag }) => /^v/.test(tag)) ? 'v' : ''
return `${prefix}${version}`
return version
}
return null
}

async function run (argv) {
const run = async argv => {
const options = await getOptions(argv)
const log = string => options.stdout ? null : updateLog(string)
log('Fetching remote…')
const remote = await fetchRemote(options)
log('Fetching tags…')
const tags = await fetchTags(options)
log(`${tags.length} version tags found…`)
const latestVersion = await getLatestVersion(options, tags)
const onParsed = ({ title }) => log(`Fetched ${title}…`)
const releases = await parseReleases(tags, remote, latestVersion, options, onParsed)
const changelog = await compileTemplate(options, { releases, options })
const releases = await parseReleases(tags, options, onParsed)
const changelog = await compileTemplate(releases, options)
await write(changelog, options, log)
}

async function write (changelog, options, log) {
const write = async (changelog, options, log) => {
if (options.stdout) {
process.stdout.write(changelog)
return
}
const bytes = Buffer.byteLength(changelog, 'utf8')
const bytes = formatBytes(Buffer.byteLength(changelog, 'utf8'))
const existing = await fileExists(options.output) && await readFile(options.output, 'utf8')
if (existing) {
const index = options.prepend ? 0 : existing.indexOf(PREPEND_TOKEN)
if (index !== -1) {
const prepended = `${changelog}\n${existing.slice(index)}`
await writeFile(options.output, prepended)
log(`${formatBytes(bytes)} prepended to ${options.output}\n`)
log(`${bytes} prepended to ${options.output}\n`)
return
}
}
await writeFile(options.output, changelog)
log(`${formatBytes(bytes)} written to ${options.output}\n`)
log(`${bytes} written to ${options.output}\n`)
}

module.exports = {
Expand Down
Loading

0 comments on commit 1fc916c

Please sign in to comment.