Skip to content

Commit 960ea6f

Browse files
committed
feat: prepend the CHANGELOG file instead of rewriting it
1 parent fdef971 commit 960ea6f

File tree

4 files changed

+136
-6
lines changed

4 files changed

+136
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## Latest
2+
3+
- feat: add update version feature fdef971e
4+
15
## 0.2.0
26

37
- feat: include changelog in the releases 2da21c56

src/index.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ import semver from 'semver'
33
import {
44
commitRelease,
55
generateLine,
6+
generateReleased,
67
getCommitDetails,
78
getCommits,
9+
getLatestTag,
810
updateVersion
911
} from './utils'
1012

1113
export const changelog = async (version, options) => {
1214
const title = version || 'Latest'
13-
const commits = await getCommits()
15+
const latestTag = await getLatestTag()
16+
const commits = await getCommits(latestTag)
1417
const latestCommit = getCommitDetails(commits[0])
1518
const isReleaseLatest = latestCommit.scope === 'release'
1619
let changelog = isReleaseLatest ? '' : `## ${title}\n\n`
1720

21+
const released = latestTag && (await generateReleased(latestTag))
22+
1823
commits.forEach((commit, index) => {
1924
const commitDetails = getCommitDetails(commit)
2025
const nextCommit = getCommitDetails(commits[index + 1])
@@ -24,9 +29,11 @@ export const changelog = async (version, options) => {
2429
if (line) changelog += line
2530
})
2631

27-
return options && options.write
28-
? writeFileSync('CHANGELOG.md', changelog)
29-
: process.stdout.write(changelog)
32+
if (!options || !options.write) return process.stdout.write(changelog)
33+
34+
const newChangelog = released ? changelog + '\n' + released : changelog
35+
36+
return writeFileSync('CHANGELOG.md', newChangelog)
3037
}
3138

3239
export const release = async version => {

src/utils/index.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { exec } from 'child_process'
2+
import { readFile } from 'fs'
23

34
const execAsync = command =>
45
new Promise((resolve, reject) =>
@@ -14,12 +15,54 @@ export const commitRelease = async version => {
1415
await execAsync(`git tag ${version}`)
1516
}
1617

17-
export const getCommits = async () => {
18-
const commits = await execAsync('git log --format="%H %s"')
18+
export const getCommits = async tag => {
19+
const query = tag
20+
? `git log --format="%H %s" ${tag}..`
21+
: 'git log --format="%H %s"'
22+
const commits = await execAsync(query)
1923

2024
return commits.split('\n').filter(commit => commit)
2125
}
2226

27+
export const generateReleased = previousVersion =>
28+
new Promise((resolve, reject) =>
29+
readFile('CHANGELOG.md', 'utf8', (err, data) => {
30+
if (err) return reject(err)
31+
32+
let isLatest = false
33+
const released = data
34+
.split('\n')
35+
.filter(line => {
36+
if (line === '## Latest') {
37+
isLatest = true
38+
39+
return false
40+
}
41+
42+
if (isLatest && line === `## ${previousVersion}`) {
43+
isLatest = false
44+
45+
return true
46+
}
47+
48+
return !isLatest
49+
})
50+
.join('\n')
51+
52+
if (isLatest) {
53+
return reject(new Error('Previous release not found in CHANGELOG'))
54+
}
55+
56+
return resolve(released)
57+
})
58+
)
59+
60+
export const getLatestTag = async () => {
61+
const latestTag = await execAsync('git tag | tail -n 1')
62+
63+
return latestTag ? latestTag.replace('\n', '') : null
64+
}
65+
2366
export const getCommitDetails = commit => {
2467
if (!commit) return null
2568

src/utils/index.test.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { exec } from 'child_process'
2+
import { readFile } from 'fs'
23
import {
34
commitRelease,
45
generateLine,
6+
generateReleased,
57
getCommitDetails,
68
getCommits,
9+
getLatestTag,
710
updateVersion
811
} from './index'
912

1013
jest.mock('child_process', () => ({
1114
exec: jest.fn()
1215
}))
16+
jest.mock('fs', () => ({
17+
readFile: jest.fn()
18+
}))
1319

1420
describe('utils', () => {
1521
beforeEach(() => jest.resetAllMocks())
@@ -33,6 +39,26 @@ describe('utils', () => {
3339
})
3440
})
3541

42+
describe('getLatestTag', () => {
43+
it('should return null if there is no tag', async () => {
44+
exec.mockImplementation((_, cb) => cb(null))
45+
const latestTag = await getLatestTag()
46+
47+
expect(latestTag).toBe(null)
48+
expect(exec).toBeCalledTimes(1)
49+
expect(exec).toBeCalledWith('git tag | tail -n 1', expect.any(Function))
50+
})
51+
52+
it('should return latest tag', async () => {
53+
exec.mockImplementation((_, cb) => cb(null, '2.2.2'))
54+
const latestTag = await getLatestTag()
55+
56+
expect(latestTag).toBe('2.2.2')
57+
expect(exec).toBeCalledTimes(1)
58+
expect(exec).toBeCalledWith('git tag | tail -n 1', expect.any(Function))
59+
})
60+
})
61+
3662
describe('getCommits', () => {
3763
it('should reject if receives an error', async () => {
3864
const error = 'error'
@@ -62,6 +88,28 @@ describe('utils', () => {
6288
)
6389
expect(commits).toEqual(mockedOutput)
6490
})
91+
92+
it('should return commits since tag', async () => {
93+
const mockedInput =
94+
'bffc2f9e8da1c7ac133689bc9cd14494f3be08e3 refactor: extract line generating logic to function and promisify exec\naa805ce71ee103965ce3db46d4f6ed2658efd08d feat: add option to write to local CHANGELOG file\nf2191200bf7b6e5eec3d61fcef9eb756e0129cfb chore(release): 0.1.0\nb2f5901922505efbfb6dd684252e8df0cdffeeb2 fix: support other conventions\n4e02179cae1234d7083036024080a3f25fcb52c2 feat: add execute release feature'
95+
const mockedOutput = [
96+
'bffc2f9e8da1c7ac133689bc9cd14494f3be08e3 refactor: extract line generating logic to function and promisify exec',
97+
'aa805ce71ee103965ce3db46d4f6ed2658efd08d feat: add option to write to local CHANGELOG file',
98+
'f2191200bf7b6e5eec3d61fcef9eb756e0129cfb chore(release): 0.1.0',
99+
'b2f5901922505efbfb6dd684252e8df0cdffeeb2 fix: support other conventions',
100+
'4e02179cae1234d7083036024080a3f25fcb52c2 feat: add execute release feature'
101+
]
102+
103+
exec.mockImplementation((_, cb) => cb(null, mockedInput))
104+
const commits = await getCommits('2.2.2')
105+
106+
expect(exec).toBeCalledTimes(1)
107+
expect(exec).toBeCalledWith(
108+
'git log --format="%H %s" 2.2.2..',
109+
expect.any(Function)
110+
)
111+
expect(commits).toEqual(mockedOutput)
112+
})
65113
})
66114

67115
describe('getCommitDetails', () => {
@@ -90,6 +138,34 @@ describe('utils', () => {
90138
})
91139
})
92140

141+
describe('generateReleased', () => {
142+
it('should reject if receives an error', async () => {
143+
const error = 'error'
144+
readFile.mockImplementation((_, __, cb) => cb(error))
145+
146+
expect(generateReleased()).rejects.toMatch(error)
147+
})
148+
149+
it('should reject if there is no released but tag provided', async () => {
150+
const error = 'Previous release not found in CHANGELOG'
151+
const mockedInput =
152+
'## Latest\n- feat: include changelog in the releases 2da21c56\n- test: add utils tests 217b25d0'
153+
readFile.mockImplementation((_, __, cb) => cb(null, mockedInput))
154+
155+
expect(generateReleased('2.2.2')).rejects.toThrow(error)
156+
})
157+
158+
it('should generate released', async () => {
159+
const mockedInput =
160+
'## Latest\n- feat: include changelog in the releases 2da21c56\n- test: add utils tests 217b25d0\n## 2.2.2\n- feat: add feature 2da21c56'
161+
const mockedOutput = '## 2.2.2\n- feat: add feature 2da21c56'
162+
readFile.mockImplementation((_, __, cb) => cb(null, mockedInput))
163+
const released = await generateReleased('2.2.2')
164+
165+
expect(released).toBe(mockedOutput)
166+
})
167+
})
168+
93169
describe('generateLine', () => {
94170
it('should generate release line', () => {
95171
const mockedInput = {

0 commit comments

Comments
 (0)