Skip to content

Commit 09325ac

Browse files
committed
Refactor git data fetching logic
- Use `git tags` and then an individual `git log` for each release Breaking changes: - Removes `--starting-commit` option - Commits are no longer fetched in one long list, so this option no longer makes any sense - Removes `--include-branch` option - No longer necessary now that releases are fetched via `git tags`
1 parent bafe90c commit 09325ac

21 files changed

+611
-481
lines changed

.travis.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ cache:
1010
script:
1111
- npm run lint
1212
- npm run lint-markdown
13-
- npm run test
13+
- npm run test-coverage
1414
after_success:
15-
- npm run coverage
15+
- npm run report-coverage
1616
env:
1717
global:
1818
secure: F80ocIyMwaljpsWzLXRhoXTxR7kkYE0V9rTpAzO7MI/OmQ2UiAeQkyTXdthK6tZuz1oVVrpjDtYuJ0hmvlTBm7WmVWNK0S6kYhHsHGy1sRmwilBqVSwSFEcjjKmOAJckfIHwOG8tjWLY9qemrf9KqpvPCG7DiG4Pn+YHLOZT7XlXbBiec/qCnaskwBpN5KhA+VAhupcZEQAy5AWW5h+CU7NF60a4chlX4gmWjAgeHZlYr472pNRi+kDZVztaXusTEWExJog/NZ/Oi8VTZkzX0t3uNiWmLfRyYC1S8blq/jaPRuwtJrgzZZRRHZswN01cmBl/D8HAzvgrA24k/70QpNdjFeJ4s5PW9+4GiGnky5z5F3tB8Hlv0ofNQAR3IKAnI+lXy/DfSD10ZJls4bQMEdoPdG/34E5EGliG2WmP6chuTh8vPlZtOGMesUi0TkXSoYPO5njZEV0Ketye37LHTfwO4sf6OC0tV8MsTtLjqMU+081K20ACHITwaJlpfWP1uGLJn+zUx3XsGS8z7abt9+a9cPl9p917IpDttLsdtLhvGC988uWvJQ5F6UMlayf1MYavwmz0vKldDu3eHBKJSaTDqugwvivww3TxdG7Jw/mNVYsrdWz4Hr49QuugGxiOrL5ZtEIbBV50W0sPFe4XFXGFyjN2x/8/td3ZXL8glN4=

README.md

+2-8
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ Options:
4040
--ignore-commit-pattern [regex] # pattern to ignore when parsing commits
4141
--tag-pattern [regex] # override regex pattern for release tags
4242
--tag-prefix [prefix] # prefix used in version tags, default: v
43-
--starting-commit [hash] # starting commit to use for changelog generation
4443
--sort-commits [property] # sort commits by property [relevance, date, date-desc], default: relevance
45-
--include-branch [branch] # one or more branches to include commits from, comma separated
4644
--release-summary # display tagged commit message body as release summary
4745
--handlebars-setup [file] # handlebars setup file
4846
--append-git-log [string] # string to append to git log command
@@ -118,7 +116,7 @@ auto-changelog --compare-url https://example.com/repo/compare/{from}...{to}
118116

119117
#### Configuration
120118

121-
You can set any option in `package.json` under the `auto-changelog` key, using camelCase options. Note that `includeBranch` should be an array here, not a comma separated list:
119+
You can set any option in `package.json` under the `auto-changelog` key, using camelCase options.
122120

123121
```js
124122
{
@@ -131,11 +129,7 @@ You can set any option in `package.json` under the `auto-changelog` key, using c
131129
"output": "HISTORY.md",
132130
"template": "keepachangelog",
133131
"unreleased": true,
134-
"commitLimit": false,
135-
"includeBranch": [
136-
"release-v2",
137-
"release-v3"
138-
]
132+
"commitLimit": false
139133
}
140134
}
141135
```

package.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
"bin": {
77
"auto-changelog": "./lib/index.js"
88
},
9+
"engines" : {
10+
"node" : ">=7.6"
11+
},
912
"scripts": {
1013
"lint": "standard --verbose | snazzy",
1114
"lint-fix": "standard --fix",
1215
"lint-markdown": "markdownlint README.md test/data/*.md",
13-
"test": "cross-env NODE_ENV=test nyc mocha test",
14-
"coverage": "nyc report --reporter=json && codecov -f coverage/coverage-final.json",
16+
"test": "cross-env NODE_ENV=test mocha -r @babel/register test",
17+
"test-coverage": "cross-env NODE_ENV=test nyc mocha test",
18+
"report-coverage": "nyc report --reporter=json && codecov -f coverage/coverage-final.json",
1519
"clean": "rimraf lib coverage",
1620
"build": "babel src -d lib",
1721
"preversion": "npm run lint && npm run test",
@@ -111,5 +115,8 @@
111115
"text",
112116
"html"
113117
]
118+
},
119+
"auto-changelog": {
120+
"breakingPattern": "Breaking change"
114121
}
115122
}

src/commits.js

+11-43
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import semver from 'semver'
22
import { cmd, isLink, encodeHTML, replaceText, getGitVersion } from './utils'
33

4-
const COMMIT_SEPARATOR = '__AUTO_CHANGELOG_COMMIT_SEPARATOR__'
5-
const MESSAGE_SEPARATOR = '__AUTO_CHANGELOG_MESSAGE_SEPARATOR__'
6-
const MATCH_COMMIT = /(.*)\n(?:\s\((.*)\))?\n(.*)\n(.*)\n(.*)\n([\S\s]+)/
4+
export const COMMIT_SEPARATOR = '__AUTO_CHANGELOG_COMMIT_SEPARATOR__'
5+
export const MESSAGE_SEPARATOR = '__AUTO_CHANGELOG_MESSAGE_SEPARATOR__'
6+
const MATCH_COMMIT = /(.*)\n(.*)\n(.*)\n(.*)\n([\S\s]+)/
77
const MATCH_STATS = /(\d+) files? changed(?:, (\d+) insertions?...)?(?:, (\d+) deletions?...)?/
88
const BODY_FORMAT = '%B'
99
const FALLBACK_BODY_FORMAT = '%s%n%n%b'
@@ -18,38 +18,27 @@ const MERGE_PATTERNS = [
1818
/Merge branch .+ into .+\n\n(.+)[\S\s]+See merge request [^!]*!(\d+)/ // GitLab merge
1919
]
2020

21-
export async function fetchCommits (remote, options, branch = null, onProgress) {
22-
const command = branch ? `git log ${branch}` : 'git log'
21+
export async function fetchCommits (diff, remote, options = {}) {
2322
const format = await getLogFormat()
24-
const log = await cmd(`${command} --shortstat --pretty=format:${format} ${options.appendGitLog}`, onProgress)
23+
const log = await cmd(`git log ${diff} --shortstat --pretty=format:${format} ${options.appendGitLog}`)
2524
return parseCommits(log, remote, options)
2625
}
2726

2827
async function getLogFormat () {
2928
const gitVersion = await getGitVersion()
3029
const bodyFormat = gitVersion && semver.gte(gitVersion, '1.7.2') ? BODY_FORMAT : FALLBACK_BODY_FORMAT
31-
return `${COMMIT_SEPARATOR}%H%n%d%n%ai%n%an%n%ae%n${bodyFormat}${MESSAGE_SEPARATOR}`
30+
return `${COMMIT_SEPARATOR}%H%n%ai%n%an%n%ae%n${bodyFormat}${MESSAGE_SEPARATOR}`
3231
}
3332

3433
function parseCommits (string, remote, options = {}) {
35-
const commits = string
34+
return string
3635
.split(COMMIT_SEPARATOR)
3736
.slice(1)
3837
.map(commit => parseCommit(commit, remote, options))
39-
40-
if (options.startingCommit) {
41-
const index = commits.findIndex(c => c.hash.indexOf(options.startingCommit) === 0)
42-
if (index === -1) {
43-
throw new Error(`Starting commit ${options.startingCommit} was not found`)
44-
}
45-
return commits.slice(0, index + 1)
46-
}
47-
48-
return commits
4938
}
5039

51-
function parseCommit (commit, remote, options = {}) {
52-
const [, hash, refs, date, author, email, tail] = commit.match(MATCH_COMMIT)
40+
export function parseCommit (commit, remote, options = {}) {
41+
const [, hash, date, author, email, tail] = commit.match(MATCH_COMMIT)
5342
const [body, stats] = tail.split(MESSAGE_SEPARATOR)
5443
const message = encodeHTML(body)
5544
const parsed = {
@@ -58,40 +47,19 @@ function parseCommit (commit, remote, options = {}) {
5847
author,
5948
email,
6049
date: new Date(date).toISOString(),
61-
tag: getTag(refs, options),
6250
subject: replaceText(getSubject(message), options),
6351
message: message.trim(),
6452
fixes: getFixes(message, author, remote, options),
6553
href: remote.getCommitLink(hash),
6654
breaking: !!options.breakingPattern && new RegExp(options.breakingPattern).test(message),
67-
...getStats(stats.trim())
55+
...getStats(stats)
6856
}
6957
return {
7058
...parsed,
7159
merge: getMerge(parsed, message, remote, options)
7260
}
7361
}
7462

75-
function getTag (refs, options) {
76-
if (!refs) return null
77-
for (const ref of refs.split(', ')) {
78-
const prefix = `tag: ${options.tagPrefix}`
79-
if (ref.indexOf(prefix) === 0) {
80-
const tag = ref.replace(prefix, '')
81-
if (options.tagPattern) {
82-
if (new RegExp(options.tagPattern).test(tag)) {
83-
return tag
84-
}
85-
return null
86-
}
87-
if (semver.valid(tag)) {
88-
return tag
89-
}
90-
}
91-
}
92-
return null
93-
}
94-
9563
function getSubject (message) {
9664
if (!message) {
9765
return '_No commit message_'
@@ -100,7 +68,7 @@ function getSubject (message) {
10068
}
10169

10270
function getStats (stats) {
103-
if (!stats) return {}
71+
if (!stats.trim()) return {}
10472
const [, files, insertions, deletions] = stats.match(MATCH_STATS)
10573
return {
10674
files: parseInt(files || 0),

src/releases.js

+48-75
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,57 @@
11
import semver from 'semver'
2+
import { fetchCommits } from './commits'
23
import { niceDate } from './utils'
34

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

7-
function commitReducer ({ map, version }, commit) {
8-
const currentVersion = commit.tag || version
9-
const commits = map[currentVersion] || []
8+
async function createRelease (tag, previousTag, diff, remote, options) {
9+
const commits = await fetchCommits(diff, remote, options)
10+
const merges = commits.filter(commit => commit.merge).map(commit => commit.merge)
11+
const fixes = commits.filter(commit => commit.fixes).map(commit => ({ fixes: commit.fixes, commit }))
12+
const emptyRelease = merges.length === 0 && fixes.length === 0
13+
const { date, message } = commits[0] || { date: new Date().toISOString() }
14+
const breakingCount = commits.filter(c => c.breaking).length
15+
const filteredCommits = commits
16+
.filter(commit => filterCommit(commit, options, merges))
17+
.sort(commitSorter(options))
18+
.slice(0, getCommitLimit(options, emptyRelease, breakingCount))
1019
return {
11-
map: {
12-
...map,
13-
[currentVersion]: [...commits, commit]
14-
},
15-
version: currentVersion
20+
tag,
21+
title: tag || 'Unreleased',
22+
date,
23+
isoDate: date.slice(0, 10),
24+
niceDate: niceDate(date),
25+
commits: filteredCommits,
26+
merges,
27+
fixes,
28+
summary: getSummary(message, options),
29+
major: Boolean(!options.tagPattern && tag && previousTag && semver.diff(tag, previousTag) === 'major'),
30+
href: getCompareLink(previousTag, tag, remote, options)
1631
}
1732
}
1833

19-
export function parseReleases (commits, remote, latestVersion, options) {
20-
const { map } = commits.reduce(commitReducer, { map: {}, version: latestVersion })
21-
return Object.keys(map).map((key, index, versions) => {
22-
const commits = map[key]
23-
const previousVersion = versions[index + 1] || null
24-
const versionCommit = commits.find(commit => commit.tag) || {}
25-
const merges = commits.filter(commit => commit.merge).map(commit => commit.merge)
26-
const fixes = commits.filter(commit => commit.fixes).map(commit => ({ fixes: commit.fixes, commit }))
27-
const tag = versionCommit.tag || latestVersion
28-
const date = versionCommit.date || new Date().toISOString()
29-
const filteredCommits = commits
30-
.filter(commit => filterCommit(commit, options, merges))
31-
.sort(commitSorter(options))
32-
const emptyRelease = merges.length === 0 && fixes.length === 0
33-
const { tagPattern, tagPrefix } = options
34-
return {
35-
tag,
36-
title: tag || 'Unreleased',
37-
date,
38-
isoDate: date.slice(0, 10),
39-
niceDate: niceDate(date),
40-
commits: sliceCommits(filteredCommits, options, emptyRelease),
41-
merges,
42-
fixes,
43-
summary: getSummary(versionCommit.message, options),
44-
major: Boolean(!tagPattern && tag && previousVersion && semver.diff(tag, previousVersion) === 'major'),
45-
href: previousVersion ? remote.getCompareLink(`${tagPrefix}${previousVersion}`, tag ? `${tagPrefix}${tag}` : 'HEAD') : null
46-
}
47-
}).filter(release => {
48-
return options.unreleased ? true : release.tag
34+
export function parseReleases (tags, remote, latestVersion, options) {
35+
const releases = tags.map((tag, index, tags) => {
36+
const previousTag = tags[index + 1]
37+
const diff = previousTag ? `${previousTag}..${tag}` : tag
38+
return createRelease(tag, previousTag, diff, remote, options)
4939
})
50-
}
51-
52-
export function sortReleases (a, b) {
53-
const tags = {
54-
a: inferSemver(a.tag),
55-
b: inferSemver(b.tag)
56-
}
57-
if (tags.a && tags.b) {
58-
if (semver.valid(tags.a) && semver.valid(tags.b)) {
59-
return semver.rcompare(tags.a, tags.b)
60-
}
61-
if (tags.a === tags.b) {
62-
return 0
63-
}
64-
return tags.a < tags.b ? 1 : -1
65-
}
66-
if (tags.a) return 1
67-
if (tags.b) return -1
68-
return 0
69-
}
70-
71-
function inferSemver (tag) {
72-
if (/^v?\d+$/.test(tag)) {
73-
// v1 becomes v1.0.0
74-
return `${tag}.0.0`
40+
if (latestVersion || options.unreleased) {
41+
const tag = latestVersion || null
42+
const previousTag = tags[0]
43+
const diff = `${previousTag}..`
44+
releases.unshift(createRelease(tag, previousTag, diff, remote, options))
7545
}
76-
if (/^v?\d+\.\d+$/.test(tag)) {
77-
// v1.0 becomes v1.0.0
78-
return `${tag}.0`
79-
}
80-
return tag
46+
return Promise.all(releases)
8147
}
8248

83-
function sliceCommits (commits, { commitLimit, backfillLimit }, emptyRelease) {
49+
function getCommitLimit ({ commitLimit, backfillLimit }, emptyRelease, breakingCount) {
8450
if (commitLimit === false) {
85-
return commits
51+
return undefined // Return all commits
8652
}
8753
const limit = emptyRelease ? backfillLimit : commitLimit
88-
const minLimit = commits.filter(c => c.breaking).length
89-
return commits.slice(0, Math.max(minLimit, limit))
54+
return Math.max(breakingCount, limit)
9055
}
9156

9257
function filterCommit (commit, { ignoreCommitPattern }, merges) {
@@ -97,9 +62,8 @@ function filterCommit (commit, { ignoreCommitPattern }, merges) {
9762
if (commit.breaking) {
9863
return true
9964
}
100-
if (ignoreCommitPattern) {
101-
// Filter out commits that match ignoreCommitPattern
102-
return new RegExp(ignoreCommitPattern).test(commit.subject) === false
65+
if (ignoreCommitPattern && new RegExp(ignoreCommitPattern).test(commit.subject)) {
66+
return false
10367
}
10468
if (semver.valid(commit.subject)) {
10569
// Filter out version commits
@@ -135,3 +99,12 @@ function commitSorter ({ sortCommits }) {
13599
return (b.insertions + b.deletions) - (a.insertions + a.deletions)
136100
}
137101
}
102+
103+
function getCompareLink (previousTag, tag, remote, { tagPrefix = '' }) {
104+
if (!previousTag) {
105+
return null
106+
}
107+
const from = `${tagPrefix}${previousTag}`
108+
const to = tag ? `${tagPrefix}${tag}` : 'HEAD'
109+
return remote.getCompareLink(from, to)
110+
}

src/remote.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ export async function fetchRemote (options) {
66
return getRemote(remoteURL, options)
77
}
88

9-
function getRemote (remoteURL, options = {}) {
9+
export function getRemote (remoteURL, options = {}) {
1010
const overrides = getOverrides(options)
1111
if (!remoteURL) {
12-
// No point warning if everything is overriddens
12+
// No point warning if everything is overridden
1313
if (Object.keys(overrides).length !== 4) {
1414
console.warn(`Warning: Git remote ${options.remote} was not found`)
1515
}

0 commit comments

Comments
 (0)