px_github_changelog_generator 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|