forked from rubocop/rubocop
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathchangelog.rb
168 lines (138 loc) · 4.18 KB
/
changelog.rb
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
# frozen_string_literal: true
# Changelog utility
class Changelog
ENTRIES_PATH = 'changelog/'
FIRST_HEADER = /#{Regexp.escape("## master (unreleased)\n")}/m.freeze
ENTRIES_PATH_TEMPLATE = "#{ENTRIES_PATH}%<type>s_%<name>s.md"
TYPE_REGEXP = /#{Regexp.escape(ENTRIES_PATH)}([a-z]+)_/.freeze
TYPE_TO_HEADER = { new: 'New features', fix: 'Bug fixes', change: 'Changes' }.freeze
HEADER = /### (.*)/.freeze
PATH = 'CHANGELOG.md'
REF_URL = 'https://github.com/rubocop/rubocop'
MAX_LENGTH = 40
CONTRIBUTOR = '[@%<user>s]: https://github.com/%<user>s'
SIGNATURE = Regexp.new(format(Regexp.escape('[@%<user>s][]'), user: '([\w-]+)'))
EOF = "\n"
# New entry
Entry = Struct.new(:type, :body, :ref_type, :ref_id, :user, keyword_init: true) do
def initialize(type:, body: last_commit_title, ref_type: nil, ref_id: nil, user: github_user)
id, body = extract_id(body)
ref_id ||= id || 'x'
ref_type ||= id ? :issues : :pull
super
end
def write
FileUtils.mkdir_p(ENTRIES_PATH)
File.write(path, content)
path
end
def path
format(ENTRIES_PATH_TEMPLATE, type: type, name: str_to_filename(body))
end
def content
period = '.' unless body.end_with? '.'
"* #{ref}: #{body}#{period} ([@#{user}][])\n"
end
def ref
"[##{ref_id}](#{REF_URL}/#{ref_type}/#{ref_id})"
end
def last_commit_title
`git log -1 --pretty=%B`.lines.first.chomp
end
def extract_id(body)
/^\[Fix(?:es)? #(\d+)\] (.*)/.match(body)&.captures || [nil, body]
end
def str_to_filename(str)
str
.downcase
.split
.each { |s| s.gsub!(/\W/, '') }
.reject(&:empty?)
.inject do |result, word|
s = "#{result}_#{word}"
return result if s.length > MAX_LENGTH
s
end
end
def github_user
user = `git config --global credential.username`.chomp
if user.empty?
warn 'Set your username with `git config --global credential.username "myusernamehere"`'
end
user
end
end
attr_reader :header, :rest
def initialize(content: File.read(PATH), entries: Changelog.read_entries)
require 'strscan'
parse(content)
@entries = entries
end
def and_delete!
@entries.each_key { |path| File.delete(path) }
end
def merge!
File.write(PATH, merge_content)
self
end
def unreleased_content
entry_map = parse_entries(@entries)
merged_map = merge_entries(entry_map)
merged_map.flat_map { |header, things| ["### #{header}\n", *things, ''] }.join("\n")
end
def merge_content
merged_content = [@header, unreleased_content, @rest.chomp, *new_contributor_lines].join("\n")
merged_content << EOF
end
def self.pending?
entry_paths.any?
end
def self.entry_paths
Dir["#{ENTRIES_PATH}*"]
end
def self.read_entries
entry_paths.to_h { |path| [path, File.read(path)] }
end
def new_contributor_lines
contributors
.map { |user| format(CONTRIBUTOR, user: user) }
.reject { |line| @rest.include?(line) }
end
def contributors
contributors = @entries.values.flat_map do |entry|
entry.match(/\. \((?<contributors>.+)\)\n/)[:contributors].split(',')
end
contributors.join.scan(SIGNATURE).flatten
end
private
def merge_entries(entry_map)
all = @unreleased.merge(entry_map) { |_k, v1, v2| v1.concat(v2) }
canonical = TYPE_TO_HEADER.values.to_h { |v| [v, nil] }
canonical.merge(all).compact
end
def parse(content)
ss = StringScanner.new(content)
@header = ss.scan_until(FIRST_HEADER)
@unreleased = parse_release(ss.scan_until(/\n(?=## )/m))
@rest = ss.rest
end
# @return [Hash<type, Array<String>]]
def parse_release(unreleased)
unreleased
.lines
.map(&:chomp)
.reject(&:empty?)
.slice_before(HEADER)
.to_h do |header, *entries|
[HEADER.match(header)[1], entries]
end
end
def parse_entries(path_content_map)
changes = Hash.new { |h, k| h[k] = [] }
path_content_map.each do |path, content|
header = TYPE_TO_HEADER.fetch(TYPE_REGEXP.match(path)[1].to_sym)
changes[header].concat(content.lines.map(&:chomp))
end
changes
end
end