forked from cookpete/auto-changelog
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcommits.js
149 lines (133 loc) · 4.41 KB
/
commits.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
const semver = require('semver')
const { cmd, isLink, encodeHTML, replaceText, getGitVersion } = require('./utils')
const COMMIT_SEPARATOR = '__AUTO_CHANGELOG_COMMIT_SEPARATOR__'
const MESSAGE_SEPARATOR = '__AUTO_CHANGELOG_MESSAGE_SEPARATOR__'
const MATCH_COMMIT = /(.*)\n(.*)\n(.*)\n(.*)\n([\S\s]+)/
const MATCH_STATS = /(\d+) files? changed(?:, (\d+) insertions?...)?(?:, (\d+) deletions?...)?/
const BODY_FORMAT = '%B'
const FALLBACK_BODY_FORMAT = '%s%n%n%b'
// https://help.github.com/articles/closing-issues-via-commit-messages
const DEFAULT_FIX_PATTERN = /(?:close[sd]?|fixe?[sd]?|resolve[sd]?)\s(?:#(\d+)|(https?:\/\/.+?\/(?:issues|pull|pull-requests|merge_requests)\/(\d+)))/gi
const MERGE_PATTERNS = [
/^Merge pull request #(\d+) from .+\n\n(.+)/, // Regular GitHub merge
/^(.+) \(#(\d+)\)(?:$|\n\n)/, // Github squash merge
/^Merged in .+ \(pull request #(\d+)\)\n\n(.+)/, // BitBucket merge
/^Merge branch .+ into .+\n\n(.+)[\S\s]+See merge request [^!]*!(\d+)/ // GitLab merge
]
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, options)
}
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}`
}
const parseCommits = (string, options = {}) => {
return string
.split(COMMIT_SEPARATOR)
.slice(1)
.map(commit => parseCommit(commit, options))
.filter(commit => filterCommit(commit, 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)
const parsed = {
hash,
shorthash: hash.slice(0, 7),
author,
email,
date: new Date(date).toISOString(),
subject: replaceText(getSubject(message), options),
message: message.trim(),
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, options)
}
}
const getSubject = (message) => {
if (!message.trim()) {
return '_No commit message_'
}
return message.match(/[^\n]+/)[0].trim()
}
const getStats = (stats) => {
if (!stats.trim()) return {}
const [, files, insertions, deletions] = stats.match(MATCH_STATS)
return {
files: parseInt(files || 0),
insertions: parseInt(insertions || 0),
deletions: parseInt(deletions || 0)
}
}
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] : options.getIssueLink(id)
fixes.push({ id, href, author })
match = pattern.exec(message)
}
return fixes
}
const getFixID = (match) => {
// Get the last non-falsey value in the match array
for (let i = match.length; i >= 0; i--) {
if (match[i]) {
return match[i]
}
}
}
const getFixPattern = (options) => {
if (options.issuePattern) {
return new RegExp(options.issuePattern, 'g')
}
return DEFAULT_FIX_PATTERN
}
const getMergePatterns = (options) => {
if (options.mergePattern) {
return MERGE_PATTERNS.concat(new RegExp(options.mergePattern, 'g'))
}
return MERGE_PATTERNS
}
const getMerge = (commit, message, options = {}) => {
const patterns = getMergePatterns(options)
for (const pattern of patterns) {
const match = pattern.exec(message)
if (match) {
const id = /^\d+$/.test(match[1]) ? match[1] : match[2]
const message = /^\d+$/.test(match[1]) ? match[2] : match[1]
return {
id,
message: replaceText(message, options),
href: options.getMergeLink(id),
author: commit.author,
commit
}
}
}
return null
}
const filterCommit = (commit, { ignoreCommitPattern }) => {
if (ignoreCommitPattern && new RegExp(ignoreCommitPattern).test(commit.subject)) {
return false
}
return true
}
module.exports = {
COMMIT_SEPARATOR,
MESSAGE_SEPARATOR,
fetchCommits,
parseCommit
}