-
-
Notifications
You must be signed in to change notification settings - Fork 10.8k
/
generate_changelog
executable file
·371 lines (331 loc) · 8.43 KB
/
generate_changelog
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
#!/usr/bin/env ruby
#
# generate_changelog
#
###
### dependencies
###
require 'open3'
require 'set'
###
### configurable constants
###
PROJECT_URL = 'https://github.com/caskroom/homebrew-cask'
MAINTAINERS = %w[
phinze
fanquake
leoj3n
NanoXD
rolandwalker
vitorgalvao
alebcay
ndr-qef
goxberry
jimbojsb
radeksimko
federicobond
claui
jawshooah
Ngrd
caskroom
]
CODE_PATHS = %w[
bin
developer
lib
spec
test
brew-cask.rb
Rakefile
Gemfile
Gemfile.lock
.travis.yml
.gitignore
]
SHA_PAT = '[\da-f]{40}'
###
### monkeypatching
###
class Array
def to_h
Hash[*self.flatten]
end
end
###
### git methods
###
def end_object
'HEAD'
end
def matches_sha(sha)
%r{\A#{SHA_PAT}\Z}.match(sha)
end
def cd_to_project_root
Dir.chdir File.dirname(File.expand_path(__FILE__))
@git_root ||= Open3.popen3(*%w[
git rev-parse --show-toplevel
]) do |stdin, stdout, stderr|
begin
stdout.gets.chomp
rescue
end
end
Dir.chdir @git_root
@git_root
end
def warn_if_off_branch(wanted_branch='master')
current_branch = Open3.popen3(*%w[
git rev-parse --abbrev-ref HEAD
]) do |stdin, stdout, stderr|
begin
stdout.gets.chomp
rescue
end
end
unless current_branch == wanted_branch
$stderr.puts "\nWARNING: you are running from branch '#{current_branch}', not '#{wanted_branch}'\n\n"
end
end
def last_release
@last_release ||= Open3.popen3(
'./developer/bin/get_release_tag'
) do |stdin, stdout, stderr|
begin
stdout.gets.chomp
rescue
end
end
end
def next_release
if @next_release.nil?
if ENV.key?('NEW_RELEASE_TAG')
@next_release = ENV['NEW_RELEASE_TAG']
else
@next_release = Open3.popen3(
'./developer/bin/get_release_tag', '-next'
) do |stdin, stdout, stderr|
begin
stdout.gets.chomp
rescue
end
end
end
else
@next_release
end
end
def verify_git_object(object)
sha = Open3.popen3(*%w[
git rev-parse -q --verify
],
object, '--'
) do |stdin, stdout, stderr|
begin
stdout.gets.chomp
rescue
end
end
raise "'#{object}' is not a git object" unless matches_sha sha
sha
end
# constrained to last_release..HEAD
def all_shas
@all_shas ||= Open3.popen3(*%w[
git rev-list --topo-order
],
"#{last_release}..#{end_object}"
) do |stdin, stdout, stderr|
stdout.each_line.map(&:chomp)
end
end
# not constrained
def tag_commits
@tag_commits ||= Open3.popen3(*%w[
git show-ref -s --tags --dereference
]) do |stdin, stdout, stderr|
stdout.each_line.collect do |line|
line.chomp!
if ! %r{\^\{\}\Z}.match(line)
nil
elsif %r{\A(#{SHA_PAT}) }.match(line)
[$1, true]
else
raise "'#{line}' does not contain an SHA"
end
end.compact.to_h
end
end
# Collect merge commits separately because "git log" does not always
# return related merge commits when using a path constraint. There
# might be a clever way to do this in a single step already built into
# git. In any case (in the opinion of the author) git's default
# behavior is a bug.
#
# constrained to last_release..HEAD
def merge_commits
@merge_commits ||= Open3.popen3(*%w[
git rev-list --pretty=oneline --topo-order --min-parents=2 --max-parents=2 --parents
],
"#{last_release}..#{end_object}"
) do |stdin, stdout, stderr|
stdout.each_line.collect do |line|
line.chomp!
# intentionally limited to the simple case of two parents
if %r{\A(#{SHA_PAT}) (#{SHA_PAT}) (#{SHA_PAT}) (.*)}.match(line)
[$1, {
:trunk_parent => $2,
:branch_parent => $3,
:log => $4,
}]
else
raise "could not parse '#{line}'"
end
end.compact.to_h
end
end
# constrained to last_release..HEAD
# also constrained to CODE_PATHS
def ordinary_code_commits
@ordinary_code_commits ||= Open3.popen3(*%w[
git rev-list --pretty=oneline --topo-order --max-parents=1 --parents
],
"#{last_release}..#{end_object}",
'--', *CODE_PATHS
) do |stdin, stdout, stderr|
stdout.each_line.collect do |line|
line.chomp!
if %r{\A(#{SHA_PAT}) (#{SHA_PAT}) (.*)}.match(line)
[$1, {
:trunk_parent => $2,
:log => $3,
}]
else
raise "could not parse '#{line}'"
end
end.compact.to_h
end
end
###
### report/analysis methods
###
# todo: read the release date from the tag
def header
<<EOT
## #{next_release.sub(/^v/,'')}
* __Casks__
- N Casks added ...
- N total Casks
* __Features__
- none
* __Breaking Changes__
- none
* __Fixes__
- none
* __Internal Changes__
- none
* __Documentation__
- N doc commits since ...
* __Contributors__
- N new contributors since ...
- N total contributors
* __Release Date__
- YYYY-MM-DD HH:MM:SS UTC
EOT
end
def footer
@footer ||= Set.new
@footer.to_a.sort
end
def add_to_footer(line)
@footer ||= Set.new
@footer.add line
end
def seen(sha)
@seen ||= {}
if @seen[sha]
return true
else
@seen[sha] = true
return nil
end
end
def log_ordinary_commit(sha)
# indent ordinary commits
" - #{ordinary_code_commits[sha][:log]}"
end
def read_pull_request(sha)
branch_parent = merge_commits[sha][:branch_parent]
if ordinary_code_commits[branch_parent] and
%r{\AMerge pull request \#(\d+) from ([^\s/]+)}.match(merge_commits[sha][:log]) then
{
:num => $1,
:gh_user => MAINTAINERS.include?($2) ? '' : $2
}
end
end
def log_merge_commit(sha)
branch_parent = merge_commits[sha][:branch_parent]
pr = read_pull_request sha
if pr then
# munge a GitHub PR commit log entry into Markdown links
# plus log content from the first parent commit
log = "[##{pr[:num]}][]"
log.concat " #{ordinary_code_commits[branch_parent][:log]}"
add_to_footer "[##{pr[:num]}]: #{PROJECT_URL}/issues/#{pr[:num]}"
seen branch_parent
if pr[:gh_user].length > 0
log.concat " <3 [@#{pr[:gh_user]}][]"
add_to_footer "[@#{pr[:gh_user]}]: https://github.com/#{pr[:gh_user]}"
end
" - #{log}"
elsif ordinary_code_commits[branch_parent]
# non-PR merge, just pass the log msg unmodified
" - #{merge_commits[sha][:log]}"
else
# drop this merge, it does not relate to CODE_PATHS
end
end
def changelog
# follows topological order
all_shas.collect do |sha|
if tag_commits[sha]
nil
elsif seen sha
nil
elsif ordinary_code_commits[sha]
log_ordinary_commit sha
elsif merge_commits[sha]
log_merge_commit sha
end
end.compact
end
###
### main
###
# process args
if %r{\A-+h(?:elp)?}i.match(ARGV.first)
puts <<EOT
generate_changelog [ <release-tag> ]
Generate a rough-draft changelog in Markdown format for changes since
<release-tag>, which defaults to the most recent release.
The output is only a draft. Changelog items still need to be edited,
removed, and/or added.
All changelog items must also be moved to within one of the given
category sections.
EOT
exit
end
if ARGV.length
@last_release = ARGV.shift
end
# initialize
cd_to_project_root
verify_git_object last_release
warn_if_off_branch 'master'
# report
puts header
puts "\n"
puts changelog
puts "\n"
puts footer
puts "\n"