px_github_changelog_generator 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +9 -0
- data/README.md +357 -0
- data/Rakefile +19 -0
- data/bin/git-generate-changelog +5 -0
- data/bin/github_changelog_generator +5 -0
- data/lib/github_changelog_generator/argv_parser.rb +225 -0
- data/lib/github_changelog_generator/file_parser_chooser.rb +27 -0
- data/lib/github_changelog_generator/generator/entry.rb +218 -0
- data/lib/github_changelog_generator/generator/generator.rb +177 -0
- data/lib/github_changelog_generator/generator/generator_fetcher.rb +202 -0
- data/lib/github_changelog_generator/generator/generator_processor.rb +237 -0
- data/lib/github_changelog_generator/generator/generator_tags.rb +210 -0
- data/lib/github_changelog_generator/generator/section.rb +129 -0
- data/lib/github_changelog_generator/helper.rb +37 -0
- data/lib/github_changelog_generator/octo_fetcher.rb +535 -0
- data/lib/github_changelog_generator/options.rb +159 -0
- data/lib/github_changelog_generator/parser.rb +89 -0
- data/lib/github_changelog_generator/parser_file.rb +101 -0
- data/lib/github_changelog_generator/reader.rb +88 -0
- data/lib/github_changelog_generator/ssl_certs/cacert.pem +3138 -0
- data/lib/github_changelog_generator/task.rb +68 -0
- data/lib/github_changelog_generator/version.rb +5 -0
- data/lib/github_changelog_generator.rb +49 -0
- data/man/git-generate-changelog.1 +393 -0
- data/man/git-generate-changelog.1.html +359 -0
- data/man/git-generate-changelog.html +270 -0
- data/man/git-generate-changelog.md +274 -0
- data/spec/files/angular.js.md +9395 -0
- data/spec/files/bundler.md +1911 -0
- data/spec/files/config_example +5 -0
- data/spec/files/github-changelog-generator.md +305 -0
- data/spec/github_changelog_generator_spec.rb +32 -0
- data/spec/install_gem_in_bundler.gemfile +5 -0
- data/spec/spec_helper.rb +74 -0
- data/spec/unit/generator/entry_spec.rb +766 -0
- data/spec/unit/generator/generator_processor_spec.rb +203 -0
- data/spec/unit/generator/generator_spec.rb +47 -0
- data/spec/unit/generator/generator_tags_spec.rb +337 -0
- data/spec/unit/generator/section_spec.rb +43 -0
- data/spec/unit/octo_fetcher_spec.rb +590 -0
- data/spec/unit/options_spec.rb +67 -0
- data/spec/unit/parser_file_spec.rb +94 -0
- data/spec/unit/parser_spec.rb +54 -0
- data/spec/unit/reader_spec.rb +120 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits/when_API_is_valid/returns_commits.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits_before/when_API_is_valid/returns_commits.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_request_with_proper_key/values.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -0
- metadata +250 -0
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitHubChangelogGenerator
|
4
|
+
class Generator
|
5
|
+
# delete all issues with labels from options[:exclude_labels] array
|
6
|
+
# @param [Array] issues
|
7
|
+
# @return [Array] filtered array
|
8
|
+
def exclude_issues_by_labels(issues)
|
9
|
+
return issues if !options[:exclude_labels] || options[:exclude_labels].empty?
|
10
|
+
|
11
|
+
issues.reject do |issue|
|
12
|
+
labels = issue["labels"].map { |l| l["name"] }
|
13
|
+
(labels & options[:exclude_labels]).any?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Only include issues without labels if options[:add_issues_wo_labels]
|
18
|
+
# @param [Array] issues
|
19
|
+
# @return [Array] filtered array
|
20
|
+
def exclude_issues_without_labels(issues)
|
21
|
+
return issues if issues.empty?
|
22
|
+
return issues if issues.first.key?("pull_request") && options[:add_pr_wo_labels]
|
23
|
+
return issues if !issues.first.key?("pull_request") && options[:add_issues_wo_labels]
|
24
|
+
|
25
|
+
issues.reject do |issue|
|
26
|
+
issue["labels"].empty?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Array] filtered issues accourding milestone
|
31
|
+
def filter_by_milestone(filtered_issues, tag_name, all_issues)
|
32
|
+
remove_issues_in_milestones(filtered_issues)
|
33
|
+
unless tag_name.nil?
|
34
|
+
# add missed issues (according milestones)
|
35
|
+
issues_to_add = find_issues_to_add(all_issues, tag_name)
|
36
|
+
|
37
|
+
filtered_issues |= issues_to_add
|
38
|
+
end
|
39
|
+
filtered_issues
|
40
|
+
end
|
41
|
+
|
42
|
+
# Add all issues, that should be in that tag, according milestone
|
43
|
+
#
|
44
|
+
# @param [Array] all_issues
|
45
|
+
# @param [String] tag_name
|
46
|
+
# @return [Array] issues with milestone #tag_name
|
47
|
+
def find_issues_to_add(all_issues, tag_name)
|
48
|
+
all_issues.select do |issue|
|
49
|
+
if (milestone = issue["milestone"]).nil?
|
50
|
+
false
|
51
|
+
# check, that this milestone in tag list:
|
52
|
+
elsif (tag = find_tag_for_milestone(milestone)).nil?
|
53
|
+
false
|
54
|
+
else
|
55
|
+
tag["name"] == tag_name
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Array] array with removed issues, that contain milestones with same name as a tag
|
61
|
+
def remove_issues_in_milestones(filtered_issues)
|
62
|
+
filtered_issues.select! do |issue|
|
63
|
+
# leave issues without milestones
|
64
|
+
if (milestone = issue["milestone"]).nil?
|
65
|
+
true
|
66
|
+
# remove issues of open milestones if option is set
|
67
|
+
elsif milestone["state"] == "open"
|
68
|
+
@options[:issues_of_open_milestones]
|
69
|
+
else
|
70
|
+
# check, that this milestone in tag list:
|
71
|
+
find_tag_for_milestone(milestone).nil?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_tag_for_milestone(milestone)
|
77
|
+
@filtered_tags.find { |tag| tag["name"] == milestone["title"] }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Method filter issues, that belong only specified tag range
|
81
|
+
#
|
82
|
+
# @param [Array] issues issues to filter
|
83
|
+
# @param [Hash, Nil] newer_tag Tag to find PRs of. May be nil for unreleased section
|
84
|
+
# @return [Array] filtered issues
|
85
|
+
def filter_by_tag(issues, newer_tag = nil)
|
86
|
+
issues.select do |issue|
|
87
|
+
issue["first_occurring_tag"] == (newer_tag.nil? ? nil : newer_tag["name"])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Method filter issues, that belong only specified tag range
|
92
|
+
# @param [Array] issues issues to filter
|
93
|
+
# @param [Symbol] hash_key key of date value default is :actual_date
|
94
|
+
# @param [Hash, Nil] older_tag all issues before this tag date will be excluded. May be nil, if it's first tag
|
95
|
+
# @param [Hash, Nil] newer_tag all issue after this tag will be excluded. May be nil for unreleased section
|
96
|
+
# @return [Array] filtered issues
|
97
|
+
def delete_by_time(issues, hash_key = "actual_date", older_tag = nil, newer_tag = nil)
|
98
|
+
# in case if not tags specified - return unchanged array
|
99
|
+
return issues if older_tag.nil? && newer_tag.nil?
|
100
|
+
|
101
|
+
older_tag = ensure_older_tag(older_tag, newer_tag)
|
102
|
+
|
103
|
+
newer_tag_time = newer_tag && get_time_of_tag(newer_tag)
|
104
|
+
older_tag_time = older_tag && get_time_of_tag(older_tag)
|
105
|
+
|
106
|
+
issues.select do |issue|
|
107
|
+
if issue[hash_key]
|
108
|
+
time = Time.parse(issue[hash_key].to_s).utc
|
109
|
+
|
110
|
+
tag_in_range_old = tag_newer_old_tag?(older_tag_time, time)
|
111
|
+
|
112
|
+
tag_in_range_new = tag_older_new_tag?(newer_tag_time, time)
|
113
|
+
|
114
|
+
tag_in_range = tag_in_range_old && tag_in_range_new
|
115
|
+
|
116
|
+
tag_in_range
|
117
|
+
else
|
118
|
+
false
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def ensure_older_tag(older_tag, newer_tag)
|
124
|
+
return older_tag if older_tag
|
125
|
+
|
126
|
+
idx = sorted_tags.index { |t| t["name"] == newer_tag["name"] }
|
127
|
+
# skip if we are already at the oldest element
|
128
|
+
return if idx == sorted_tags.size - 1
|
129
|
+
|
130
|
+
sorted_tags[idx - 1]
|
131
|
+
end
|
132
|
+
|
133
|
+
def tag_older_new_tag?(newer_tag_time, time)
|
134
|
+
if newer_tag_time.nil?
|
135
|
+
true
|
136
|
+
else
|
137
|
+
time <= newer_tag_time
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def tag_newer_old_tag?(older_tag_time, time)
|
142
|
+
if older_tag_time.nil?
|
143
|
+
true
|
144
|
+
else
|
145
|
+
time > older_tag_time
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Include issues with labels, specified in :include_labels
|
150
|
+
# @param [Array] issues to filter
|
151
|
+
# @return [Array] filtered array of issues
|
152
|
+
def include_issues_by_labels(issues)
|
153
|
+
filtered_issues = filter_by_include_labels(issues)
|
154
|
+
filter_wo_labels(filtered_issues)
|
155
|
+
end
|
156
|
+
|
157
|
+
# @param [Array] items Issues & PRs to filter when without labels
|
158
|
+
# @return [Array] Issues & PRs without labels or empty array if
|
159
|
+
# add_issues_wo_labels or add_pr_wo_labels are false
|
160
|
+
def filter_wo_labels(items)
|
161
|
+
if items.any? && items.first.key?("pull_request")
|
162
|
+
return items if options[:add_pr_wo_labels]
|
163
|
+
elsif options[:add_issues_wo_labels]
|
164
|
+
return items
|
165
|
+
end
|
166
|
+
# The default is to filter items without labels
|
167
|
+
items.select { |item| item["labels"].map { |l| l["name"] }.any? }
|
168
|
+
end
|
169
|
+
|
170
|
+
# @todo Document this
|
171
|
+
# @param [Object] issues
|
172
|
+
def filter_by_include_labels(issues)
|
173
|
+
if options[:include_labels].nil?
|
174
|
+
issues
|
175
|
+
else
|
176
|
+
issues.select do |issue|
|
177
|
+
labels = issue["labels"].map { |l| l["name"] } & options[:include_labels]
|
178
|
+
labels.any? || issue["labels"].empty?
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# General filtered function
|
184
|
+
#
|
185
|
+
# @param [Array] all_issues PRs or issues
|
186
|
+
# @return [Array] filtered issues
|
187
|
+
def filter_array_by_labels(all_issues)
|
188
|
+
filtered_issues = include_issues_by_labels(all_issues)
|
189
|
+
filtered_issues = exclude_issues_by_labels(filtered_issues)
|
190
|
+
exclude_issues_without_labels(filtered_issues)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Filter issues according labels
|
194
|
+
# @return [Array] Filtered issues
|
195
|
+
def get_filtered_issues(issues)
|
196
|
+
issues = filter_array_by_labels(issues)
|
197
|
+
puts "Filtered issues: #{issues.count}" if options[:verbose]
|
198
|
+
issues
|
199
|
+
end
|
200
|
+
|
201
|
+
# This method fetches missing params for PR and filter them by specified options
|
202
|
+
# It include add all PR's with labels from options[:include_labels] array
|
203
|
+
# And exclude all from :exclude_labels array.
|
204
|
+
# @return [Array] filtered PR's
|
205
|
+
def get_filtered_pull_requests(pull_requests)
|
206
|
+
pull_requests = filter_array_by_labels(pull_requests)
|
207
|
+
pull_requests = filter_merged_pull_requests(pull_requests)
|
208
|
+
puts "Filtered pull requests: #{pull_requests.count}" if options[:verbose]
|
209
|
+
pull_requests
|
210
|
+
end
|
211
|
+
|
212
|
+
# This method filter only merged PR and
|
213
|
+
# fetch missing required attributes for pull requests
|
214
|
+
# :merged_at - is a date, when issue PR was merged.
|
215
|
+
# More correct to use merged date, rather than closed date.
|
216
|
+
def filter_merged_pull_requests(pull_requests)
|
217
|
+
print "Fetching merged dates...\r" if options[:verbose]
|
218
|
+
closed_pull_requests = @fetcher.fetch_closed_pull_requests
|
219
|
+
|
220
|
+
pull_requests.each do |pr|
|
221
|
+
fetched_pr = closed_pull_requests.find do |fpr|
|
222
|
+
fpr["number"] == pr["number"]
|
223
|
+
end
|
224
|
+
if fetched_pr
|
225
|
+
pr["merged_at"] = fetched_pr["merged_at"]
|
226
|
+
closed_pull_requests.delete(fetched_pr)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
pull_requests.reject! do |pr|
|
231
|
+
pr["merged_at"].nil?
|
232
|
+
end
|
233
|
+
|
234
|
+
pull_requests
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitHubChangelogGenerator
|
4
|
+
class Generator
|
5
|
+
# fetch, filter tags, fetch dates and sort them in time order
|
6
|
+
def fetch_and_filter_tags
|
7
|
+
since_tag
|
8
|
+
due_tag
|
9
|
+
|
10
|
+
all_tags = @fetcher.fetch_all_tags
|
11
|
+
fetch_tags_dates(all_tags) # Creates a Hash @tag_times_hash
|
12
|
+
all_sorted_tags = sort_tags_by_date(all_tags)
|
13
|
+
|
14
|
+
@sorted_tags = filter_included_tags(all_sorted_tags)
|
15
|
+
@sorted_tags = filter_excluded_tags(@sorted_tags)
|
16
|
+
@filtered_tags = get_filtered_tags(@sorted_tags)
|
17
|
+
@tag_section_mapping = build_tag_section_mapping(@filtered_tags, @filtered_tags)
|
18
|
+
|
19
|
+
@filtered_tags
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Array] section_tags are the tags that need a subsection output
|
23
|
+
# @param [Array] filtered_tags is the list of filtered tags ordered from newest -> oldest
|
24
|
+
# @return [Hash] key is the tag to output, value is an array of [Left Tag, Right Tag]
|
25
|
+
# PRs to include in this section will be >= [Left Tag Date] and <= [Right Tag Date]
|
26
|
+
# rubocop:disable Style/For - for allows us to be more concise
|
27
|
+
def build_tag_section_mapping(section_tags, filtered_tags)
|
28
|
+
tag_mapping = {}
|
29
|
+
for i in 0..(section_tags.length - 1)
|
30
|
+
tag = section_tags[i]
|
31
|
+
|
32
|
+
# Don't create section header for the "since" tag
|
33
|
+
next if since_tag && tag["name"] == since_tag
|
34
|
+
|
35
|
+
# Don't create a section header for the first tag in between_tags
|
36
|
+
next if options[:between_tags] && tag == section_tags.last
|
37
|
+
|
38
|
+
# Don't create a section header for excluded tags
|
39
|
+
next unless filtered_tags.include?(tag)
|
40
|
+
|
41
|
+
older_tag = section_tags[i + 1]
|
42
|
+
tag_mapping[tag] = [older_tag, tag]
|
43
|
+
end
|
44
|
+
tag_mapping
|
45
|
+
end
|
46
|
+
# rubocop:enable Style/For
|
47
|
+
|
48
|
+
# Sort all tags by date, newest to oldest
|
49
|
+
def sort_tags_by_date(tags)
|
50
|
+
puts "Sorting tags..." if options[:verbose]
|
51
|
+
tags.sort_by! do |x|
|
52
|
+
get_time_of_tag(x)
|
53
|
+
end.reverse!
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns date for given GitHub Tag hash
|
57
|
+
#
|
58
|
+
# Memoize the date by tag name.
|
59
|
+
#
|
60
|
+
# @param [Hash] tag_name
|
61
|
+
#
|
62
|
+
# @return [Time] time of specified tag
|
63
|
+
def get_time_of_tag(tag_name)
|
64
|
+
raise ChangelogGeneratorError, "tag_name is nil" if tag_name.nil?
|
65
|
+
|
66
|
+
name_of_tag = tag_name.fetch("name")
|
67
|
+
time_for_tag_name = @tag_times_hash[name_of_tag]
|
68
|
+
return time_for_tag_name if time_for_tag_name
|
69
|
+
|
70
|
+
@fetcher.fetch_date_of_tag(tag_name).tap do |time_string|
|
71
|
+
@tag_times_hash[name_of_tag] = time_string
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Detect link, name and time for specified tag.
|
76
|
+
#
|
77
|
+
# @param [Hash] newer_tag newer tag. Can be nil, if it's Unreleased section.
|
78
|
+
# @return [Array] link, name and time of the tag
|
79
|
+
def detect_link_tag_time(newer_tag)
|
80
|
+
# if tag is nil - set current time
|
81
|
+
newer_tag_time = newer_tag.nil? ? Time.new.getutc : get_time_of_tag(newer_tag)
|
82
|
+
|
83
|
+
# if it's future release tag - set this value
|
84
|
+
if newer_tag.nil? && options[:future_release]
|
85
|
+
newer_tag_name = options[:future_release]
|
86
|
+
newer_tag_link = options[:future_release]
|
87
|
+
else
|
88
|
+
# put unreleased label if there is no name for the tag
|
89
|
+
newer_tag_name = newer_tag.nil? ? options[:unreleased_label] : newer_tag["name"]
|
90
|
+
newer_tag_link = newer_tag.nil? ? "HEAD" : newer_tag_name
|
91
|
+
end
|
92
|
+
[newer_tag_link, newer_tag_name, newer_tag_time]
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Object] try to find newest tag using #Reader and :base option if specified otherwise returns nil
|
96
|
+
def since_tag
|
97
|
+
@since_tag ||= options.fetch(:since_tag) { version_of_first_item }
|
98
|
+
end
|
99
|
+
|
100
|
+
def due_tag
|
101
|
+
@due_tag ||= options.fetch(:due_tag, nil)
|
102
|
+
end
|
103
|
+
|
104
|
+
def version_of_first_item
|
105
|
+
return unless File.file?(options[:base].to_s)
|
106
|
+
|
107
|
+
sections = GitHubChangelogGenerator::Reader.new.read(options[:base])
|
108
|
+
sections.first["version"] if sections && sections.any?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Return tags after filtering tags in lists provided by option: --exclude-tags
|
112
|
+
#
|
113
|
+
# @return [Array]
|
114
|
+
def get_filtered_tags(all_tags)
|
115
|
+
filtered_tags = filter_since_tag(all_tags)
|
116
|
+
filter_due_tag(filtered_tags)
|
117
|
+
end
|
118
|
+
|
119
|
+
# @param [Array] all_tags all tags
|
120
|
+
# @return [Array] filtered tags according :since_tag option
|
121
|
+
def filter_since_tag(all_tags)
|
122
|
+
return all_tags unless (tag = since_tag)
|
123
|
+
|
124
|
+
raise ChangelogGeneratorError, "Error: can't find tag #{tag}, specified with --since-tag option." if all_tags.none? { |t| t["name"] == tag }
|
125
|
+
|
126
|
+
if (idx = all_tags.index { |t| t["name"] == tag })
|
127
|
+
all_tags[0..idx]
|
128
|
+
else
|
129
|
+
[]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# @param [Array] all_tags all tags
|
134
|
+
# @return [Array] filtered tags according :due_tag option
|
135
|
+
def filter_due_tag(all_tags)
|
136
|
+
return all_tags unless (tag = due_tag)
|
137
|
+
|
138
|
+
raise ChangelogGeneratorError, "Error: can't find tag #{tag}, specified with --due-tag option." if all_tags.none? { |t| t["name"] == tag }
|
139
|
+
|
140
|
+
if (idx = all_tags.index { |t| t["name"] == tag }) > 0
|
141
|
+
all_tags[(idx + 1)..-1]
|
142
|
+
else
|
143
|
+
[]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# @param [Array] all_tags all tags
|
148
|
+
# @return [Array] filtered tags according to :include_tags_regex option
|
149
|
+
def filter_included_tags(all_tags)
|
150
|
+
if options[:include_tags_regex]
|
151
|
+
regex = Regexp.new(options[:include_tags_regex])
|
152
|
+
all_tags.select { |tag| regex =~ tag["name"] }
|
153
|
+
else
|
154
|
+
all_tags
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# @param [Array] all_tags all tags
|
159
|
+
# @return [Array] filtered tags according :exclude_tags or :exclude_tags_regex option
|
160
|
+
def filter_excluded_tags(all_tags)
|
161
|
+
if options[:exclude_tags]
|
162
|
+
apply_exclude_tags(all_tags)
|
163
|
+
elsif options[:exclude_tags_regex]
|
164
|
+
apply_exclude_tags_regex(all_tags)
|
165
|
+
else
|
166
|
+
all_tags
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def apply_exclude_tags(all_tags)
|
173
|
+
if options[:exclude_tags].is_a?(Regexp)
|
174
|
+
filter_tags_with_regex(all_tags, options[:exclude_tags], "--exclude-tags")
|
175
|
+
else
|
176
|
+
filter_exact_tags(all_tags)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def apply_exclude_tags_regex(all_tags)
|
181
|
+
regex = Regexp.new(options[:exclude_tags_regex])
|
182
|
+
filter_tags_with_regex(all_tags, regex, "--exclude-tags-regex")
|
183
|
+
end
|
184
|
+
|
185
|
+
def filter_tags_with_regex(all_tags, regex, regex_option_name)
|
186
|
+
warn_if_nonmatching_regex(all_tags, regex, regex_option_name)
|
187
|
+
all_tags.reject { |tag| regex =~ tag["name"] }
|
188
|
+
end
|
189
|
+
|
190
|
+
def filter_exact_tags(all_tags)
|
191
|
+
options[:exclude_tags].each do |tag|
|
192
|
+
warn_if_tag_not_found(all_tags, tag)
|
193
|
+
end
|
194
|
+
all_tags.reject { |tag| options[:exclude_tags].include?(tag["name"]) }
|
195
|
+
end
|
196
|
+
|
197
|
+
def warn_if_nonmatching_regex(all_tags, regex, regex_option_name)
|
198
|
+
return if all_tags.any? { |t| regex.match?(t["name"]) }
|
199
|
+
|
200
|
+
Helper.log.warn "Warning: unable to reject any tag, using regex "\
|
201
|
+
"#{regex.inspect} in #{regex_option_name} option."
|
202
|
+
end
|
203
|
+
|
204
|
+
def warn_if_tag_not_found(all_tags, tag)
|
205
|
+
return if all_tags.any? { |t| t["name"] == tag }
|
206
|
+
|
207
|
+
Helper.log.warn("Warning: can't find tag #{tag}, specified with --exclude-tags option.")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitHubChangelogGenerator
|
4
|
+
# This class generates the content for a single section of a changelog entry.
|
5
|
+
# It turns the tagged issues and PRs into a well-formatted list of changes to
|
6
|
+
# be later incorporated into a changelog entry.
|
7
|
+
#
|
8
|
+
# @see GitHubChangelogGenerator::Entry
|
9
|
+
class Section
|
10
|
+
# @return [String]
|
11
|
+
attr_accessor :name
|
12
|
+
|
13
|
+
# @return [String] a merge prefix, or an issue prefix
|
14
|
+
attr_reader :prefix
|
15
|
+
|
16
|
+
# @return [Array<Hash>]
|
17
|
+
attr_reader :issues
|
18
|
+
|
19
|
+
# @return [Array<String>]
|
20
|
+
attr_reader :labels
|
21
|
+
|
22
|
+
# @return [Boolean]
|
23
|
+
attr_reader :body_only
|
24
|
+
|
25
|
+
# @return [Options]
|
26
|
+
attr_reader :options
|
27
|
+
|
28
|
+
def initialize(opts = {})
|
29
|
+
@name = opts[:name]
|
30
|
+
@prefix = opts[:prefix]
|
31
|
+
@labels = opts[:labels] || []
|
32
|
+
@issues = opts[:issues] || []
|
33
|
+
@options = opts[:options] || Options.new({})
|
34
|
+
@body_only = opts[:body_only] || false
|
35
|
+
@entry = Entry.new(options)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the content of a section.
|
39
|
+
#
|
40
|
+
# @return [String] Generated section content
|
41
|
+
def generate_content
|
42
|
+
content = +""
|
43
|
+
|
44
|
+
if @issues.any?
|
45
|
+
content += "#{@prefix}\n\n" unless @options[:simple_list] || @prefix.blank?
|
46
|
+
@issues.each do |issue|
|
47
|
+
merge_string = get_string_for_issue(issue)
|
48
|
+
content += "- " unless @body_only
|
49
|
+
content += "#{merge_string}\n"
|
50
|
+
end
|
51
|
+
content += "\n"
|
52
|
+
end
|
53
|
+
content
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Parse issue and generate single line formatted issue line.
|
59
|
+
#
|
60
|
+
# Example output:
|
61
|
+
# - Add coveralls integration [\#223](https://github.com/github-changelog-generator/github-changelog-generator/pull/223) (@github-changelog-generator)
|
62
|
+
#
|
63
|
+
# @param [Hash] issue Fetched issue from GitHub
|
64
|
+
# @return [String] Markdown-formatted single issue
|
65
|
+
def get_string_for_issue(issue)
|
66
|
+
encapsulated_title = encapsulate_string issue["title"]
|
67
|
+
|
68
|
+
title_with_number = "#{encapsulated_title} [\\##{issue['number']}](#{issue['html_url']})"
|
69
|
+
title_with_number = "#{title_with_number}#{@entry.line_labels_for(issue)}" if @options[:issue_line_labels].present?
|
70
|
+
line = issue_line_with_user(title_with_number, issue)
|
71
|
+
issue_line_with_body(line, issue)
|
72
|
+
end
|
73
|
+
|
74
|
+
def issue_line_with_body(line, issue)
|
75
|
+
return normalize_body(issue["body"]) if @body_only && issue["body"].present?
|
76
|
+
return line if !@options[:issue_line_body] || issue["body"].blank?
|
77
|
+
|
78
|
+
# get issue body till first line break
|
79
|
+
body_paragraph = body_till_first_break(normalize_body(issue["body"]))
|
80
|
+
# remove spaces from beginning of the string
|
81
|
+
body_paragraph.rstrip!
|
82
|
+
# encapsulate to md
|
83
|
+
encapsulated_body = " \n#{encapsulate_string(body_paragraph)}"
|
84
|
+
|
85
|
+
"**#{line}** #{encapsulated_body}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# Normalize line endings from CRLF to LF
|
89
|
+
def normalize_body(body)
|
90
|
+
body.gsub(/\r?\n/, "\n")
|
91
|
+
end
|
92
|
+
|
93
|
+
def body_till_first_break(body)
|
94
|
+
body.split(/\n/, 2).first
|
95
|
+
end
|
96
|
+
|
97
|
+
def issue_line_with_user(line, issue)
|
98
|
+
return line if !@options[:author] || issue["pull_request"].nil?
|
99
|
+
|
100
|
+
user = issue["user"]
|
101
|
+
return "#{line} ({Null user})" unless user
|
102
|
+
|
103
|
+
if @options[:usernames_as_github_logins]
|
104
|
+
"#{line} (@#{user['login']})"
|
105
|
+
else
|
106
|
+
"#{line} ([#{user['login']}](#{user['html_url']}))"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
ENCAPSULATED_CHARACTERS = %w(< > * _ \( \) [ ] #)
|
111
|
+
|
112
|
+
# Encapsulate characters to make Markdown look as expected.
|
113
|
+
#
|
114
|
+
# @param [String] string
|
115
|
+
# @return [String] encapsulated input string
|
116
|
+
def encapsulate_string(string)
|
117
|
+
string = string.gsub('\\', '\\\\')
|
118
|
+
|
119
|
+
ENCAPSULATED_CHARACTERS.each do |char|
|
120
|
+
# Only replace char with escaped version if it isn't inside backticks (markdown inline code).
|
121
|
+
# This relies on each opening '`' being closed (ie an even number in total).
|
122
|
+
# A char is *outside* backticks if there is an even number of backticks following it.
|
123
|
+
string = string.gsub(%r{#{Regexp.escape(char)}(?=([^`]*`[^`]*`)*[^`]*$)}, "\\#{char}")
|
124
|
+
end
|
125
|
+
|
126
|
+
string
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "rainbow"
|
5
|
+
|
6
|
+
module GitHubChangelogGenerator
|
7
|
+
module Helper
|
8
|
+
# @return true if the currently running program is a unit test
|
9
|
+
def self.test?
|
10
|
+
defined? SpecHelper
|
11
|
+
end
|
12
|
+
|
13
|
+
# :nocov:
|
14
|
+
@log ||= if test?
|
15
|
+
Logger.new(nil) # don't show any logs when running tests
|
16
|
+
else
|
17
|
+
Logger.new($stdout)
|
18
|
+
end
|
19
|
+
@log.formatter = proc do |severity, _datetime, _progname, msg|
|
20
|
+
string = "#{msg}\n"
|
21
|
+
case severity
|
22
|
+
when "DEBUG" then Rainbow(string).magenta
|
23
|
+
when "INFO" then Rainbow(string).white
|
24
|
+
when "WARN" then Rainbow(string).yellow
|
25
|
+
when "ERROR" then Rainbow(string).red
|
26
|
+
when "FATAL" then Rainbow(string).red.bright
|
27
|
+
else string
|
28
|
+
end
|
29
|
+
end
|
30
|
+
# :nocov:
|
31
|
+
|
32
|
+
# Logging happens using this method
|
33
|
+
class << self
|
34
|
+
attr_reader :log
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|