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,535 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "set"
|
|
5
|
+
require "async"
|
|
6
|
+
require "async/barrier"
|
|
7
|
+
require "async/semaphore"
|
|
8
|
+
require "async/http/faraday"
|
|
9
|
+
|
|
10
|
+
module GitHubChangelogGenerator
|
|
11
|
+
# A Fetcher responsible for all requests to GitHub and all basic manipulation with related data
|
|
12
|
+
# (such as filtering, validating, e.t.c)
|
|
13
|
+
#
|
|
14
|
+
# Example:
|
|
15
|
+
# fetcher = GitHubChangelogGenerator::OctoFetcher.new(options)
|
|
16
|
+
class OctoFetcher
|
|
17
|
+
PER_PAGE_NUMBER = 100
|
|
18
|
+
MAXIMUM_CONNECTIONS = 50
|
|
19
|
+
MAX_FORBIDDEN_RETRIES = 100
|
|
20
|
+
CHANGELOG_GITHUB_TOKEN = "CHANGELOG_GITHUB_TOKEN"
|
|
21
|
+
GH_RATE_LIMIT_EXCEEDED_MSG = "Warning: Can't finish operation: GitHub API rate limit exceeded, changelog may be " \
|
|
22
|
+
"missing some issues. You can limit the number of issues fetched using the `--max-issues NUM` argument."
|
|
23
|
+
NO_TOKEN_PROVIDED = "Warning: No token provided (-t option) and variable $CHANGELOG_GITHUB_TOKEN was not found. " \
|
|
24
|
+
"This script can make only 50 requests to GitHub API per hour without a token!"
|
|
25
|
+
|
|
26
|
+
# @param options [Hash] Options passed in
|
|
27
|
+
# @option options [String] :user GitHub username
|
|
28
|
+
# @option options [String] :project GitHub project
|
|
29
|
+
# @option options [String] :since Only issues updated at or after this time are returned. This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ. eg. Time.parse("2016-01-01 10:00:00").iso8601
|
|
30
|
+
# @option options [Boolean] :http_cache Use ActiveSupport::Cache::FileStore to cache http requests
|
|
31
|
+
# @option options [Boolean] :cache_file If using http_cache, this is the cache file path
|
|
32
|
+
# @option options [Boolean] :cache_log If using http_cache, this is the cache log file path
|
|
33
|
+
def initialize(options = {})
|
|
34
|
+
@options = options || {}
|
|
35
|
+
@user = @options[:user]
|
|
36
|
+
@project = @options[:project]
|
|
37
|
+
@since = @options[:since]
|
|
38
|
+
@http_cache = @options[:http_cache]
|
|
39
|
+
@commits = []
|
|
40
|
+
@branches = nil
|
|
41
|
+
@graph = nil
|
|
42
|
+
@client = nil
|
|
43
|
+
@commits_in_tag_cache = {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def middleware
|
|
47
|
+
Faraday::RackBuilder.new do |builder|
|
|
48
|
+
if @http_cache
|
|
49
|
+
cache_file = @options.fetch(:cache_file) { File.join(Dir.tmpdir, "github-changelog-http-cache") }
|
|
50
|
+
cache_log = @options.fetch(:cache_log) { File.join(Dir.tmpdir, "github-changelog-logger.log") }
|
|
51
|
+
|
|
52
|
+
builder.use(
|
|
53
|
+
Faraday::HttpCache,
|
|
54
|
+
serializer: Marshal,
|
|
55
|
+
store: ActiveSupport::Cache::FileStore.new(cache_file),
|
|
56
|
+
logger: Logger.new(cache_log),
|
|
57
|
+
shared_cache: false
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
builder.use Octokit::Response::RaiseError
|
|
62
|
+
builder.adapter :async_http
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def connection_options
|
|
67
|
+
ca_file = @options[:ssl_ca_file] || ENV["SSL_CA_FILE"] || File.expand_path("ssl_certs/cacert.pem", __dir__)
|
|
68
|
+
|
|
69
|
+
Octokit.connection_options.merge({ ssl: { ca_file: ca_file } })
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def client_options
|
|
73
|
+
options = {
|
|
74
|
+
middleware: middleware,
|
|
75
|
+
connection_options: connection_options
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (github_token = fetch_github_token)
|
|
79
|
+
options[:access_token] = github_token
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if (endpoint = @options[:github_endpoint])
|
|
83
|
+
options[:api_endpoint] = endpoint
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
options
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def client
|
|
90
|
+
@client ||= Octokit::Client.new(client_options)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
DEFAULT_REQUEST_OPTIONS = { per_page: PER_PAGE_NUMBER }
|
|
94
|
+
|
|
95
|
+
# Fetch all tags from repo
|
|
96
|
+
#
|
|
97
|
+
# @return [Array <Hash>] array of tags
|
|
98
|
+
def fetch_all_tags
|
|
99
|
+
print "Fetching tags...\r" if @options[:verbose]
|
|
100
|
+
|
|
101
|
+
check_github_response { github_fetch_tags }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def get_all_tags # rubocop:disable Naming/AccessorMethodName
|
|
105
|
+
warn("[DEPRECATED] GitHubChangelogGenerator::OctoFetcher#get_all_tags is deprecated; use fetch_all_tags instead.")
|
|
106
|
+
fetch_all_tags
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns the number of pages for a API call
|
|
110
|
+
#
|
|
111
|
+
# @return [Integer] number of pages for this API call in total
|
|
112
|
+
# @param [Object] request_options
|
|
113
|
+
# @param [Object] method
|
|
114
|
+
# @param [Object] client
|
|
115
|
+
def calculate_pages(client, method, request_options)
|
|
116
|
+
# Makes the first API call so that we can call last_response
|
|
117
|
+
check_github_response do
|
|
118
|
+
client.send(method, user_project, DEFAULT_REQUEST_OPTIONS.merge(request_options))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
last_response = client.last_response
|
|
122
|
+
|
|
123
|
+
if (last_pg = last_response.rels[:last])
|
|
124
|
+
querystring_as_hash(last_pg.href)["page"].to_i
|
|
125
|
+
else
|
|
126
|
+
1
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Fill input array with tags
|
|
131
|
+
#
|
|
132
|
+
# @return [Array <Hash>] array of tags in repo
|
|
133
|
+
def github_fetch_tags
|
|
134
|
+
tags = []
|
|
135
|
+
page_i = 0
|
|
136
|
+
count_pages = calculate_pages(client, "tags", {})
|
|
137
|
+
|
|
138
|
+
iterate_pages(client, "tags") do |new_tags|
|
|
139
|
+
page_i += PER_PAGE_NUMBER
|
|
140
|
+
print_in_same_line("Fetching tags... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
|
|
141
|
+
tags.concat(new_tags)
|
|
142
|
+
end
|
|
143
|
+
print_empty_line
|
|
144
|
+
|
|
145
|
+
if tags.count == 0
|
|
146
|
+
Helper.log.warn "Warning: Can't find any tags in repo. \
|
|
147
|
+
Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
148
|
+
else
|
|
149
|
+
Helper.log.info "Found #{tags.count} tags"
|
|
150
|
+
end
|
|
151
|
+
# tags are a Sawyer::Resource. Convert to hash
|
|
152
|
+
tags.map { |resource| stringify_keys_deep(resource.to_hash) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def closed_pr_options
|
|
156
|
+
@closed_pr_options ||= {
|
|
157
|
+
filter: "all", labels: nil, state: "closed"
|
|
158
|
+
}.tap { |options| options[:since] = @since if @since }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# This method fetch all closed issues and separate them to pull requests and pure issues
|
|
162
|
+
# (pull request is kind of issue in term of GitHub)
|
|
163
|
+
#
|
|
164
|
+
# @return [Tuple] with (issues [Array <Hash>], pull-requests [Array <Hash>])
|
|
165
|
+
def fetch_closed_issues_and_pr
|
|
166
|
+
print "Fetching closed issues...\r" if @options[:verbose]
|
|
167
|
+
issues = []
|
|
168
|
+
page_i = 0
|
|
169
|
+
count_pages = calculate_pages(client, "issues", closed_pr_options)
|
|
170
|
+
|
|
171
|
+
iterate_pages(client, "issues", **closed_pr_options) do |new_issues|
|
|
172
|
+
page_i += PER_PAGE_NUMBER
|
|
173
|
+
print_in_same_line("Fetching issues... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
|
|
174
|
+
issues.concat(new_issues)
|
|
175
|
+
break if @options[:max_issues] && issues.length >= @options[:max_issues]
|
|
176
|
+
end
|
|
177
|
+
print_empty_line
|
|
178
|
+
Helper.log.info "Received issues: #{issues.count}"
|
|
179
|
+
|
|
180
|
+
# separate arrays of issues and pull requests:
|
|
181
|
+
issues.map { |issue| stringify_keys_deep(issue.to_hash) }
|
|
182
|
+
.partition { |issue_or_pr| issue_or_pr["pull_request"].nil? }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Fetch all pull requests. We need them to detect :merged_at parameter
|
|
186
|
+
#
|
|
187
|
+
# @return [Array <Hash>] all pull requests
|
|
188
|
+
def fetch_closed_pull_requests
|
|
189
|
+
pull_requests = []
|
|
190
|
+
options = { state: "closed" }
|
|
191
|
+
|
|
192
|
+
page_i = 0
|
|
193
|
+
count_pages = calculate_pages(client, "pull_requests", options)
|
|
194
|
+
|
|
195
|
+
iterate_pages(client, "pull_requests", **options) do |new_pr|
|
|
196
|
+
page_i += PER_PAGE_NUMBER
|
|
197
|
+
log_string = "Fetching merged dates... #{page_i}/#{count_pages * PER_PAGE_NUMBER}"
|
|
198
|
+
print_in_same_line(log_string)
|
|
199
|
+
pull_requests.concat(new_pr)
|
|
200
|
+
end
|
|
201
|
+
print_empty_line
|
|
202
|
+
|
|
203
|
+
Helper.log.info "Pull Request count: #{pull_requests.count}"
|
|
204
|
+
pull_requests.map { |pull_request| stringify_keys_deep(pull_request.to_hash) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Fetch event for all issues and add them to 'events'
|
|
208
|
+
#
|
|
209
|
+
# @param [Array] issues
|
|
210
|
+
# @return [Void]
|
|
211
|
+
def fetch_events_async(issues)
|
|
212
|
+
i = 0
|
|
213
|
+
# Add accept option explicitly for disabling the warning of preview API.
|
|
214
|
+
preview = { accept: Octokit::Preview::PREVIEW_TYPES[:project_card_events] }
|
|
215
|
+
|
|
216
|
+
barrier = Async::Barrier.new
|
|
217
|
+
semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
|
|
218
|
+
|
|
219
|
+
Sync do
|
|
220
|
+
client = self.client
|
|
221
|
+
|
|
222
|
+
issues.each do |issue|
|
|
223
|
+
semaphore.async do
|
|
224
|
+
issue["events"] = []
|
|
225
|
+
iterate_pages(client, "issue_events", issue["number"], **preview) do |new_event|
|
|
226
|
+
issue["events"].concat(new_event)
|
|
227
|
+
end
|
|
228
|
+
issue["events"] = issue["events"].map { |event| stringify_keys_deep(event.to_hash) }
|
|
229
|
+
print_in_same_line("Fetching events for issues and PR: #{i + 1}/#{issues.count}")
|
|
230
|
+
i += 1
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
barrier.wait
|
|
235
|
+
|
|
236
|
+
# to clear line from prev print
|
|
237
|
+
print_empty_line
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
Helper.log.info "Fetching events for issues and PR: #{i}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Fetch comments for PRs and add them to "comments"
|
|
244
|
+
#
|
|
245
|
+
# @param [Array] prs The array of PRs.
|
|
246
|
+
# @return [Void] No return; PRs are updated in-place.
|
|
247
|
+
def fetch_comments_async(prs)
|
|
248
|
+
barrier = Async::Barrier.new
|
|
249
|
+
semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
|
|
250
|
+
|
|
251
|
+
Sync do
|
|
252
|
+
client = self.client
|
|
253
|
+
|
|
254
|
+
prs.each do |pr|
|
|
255
|
+
semaphore.async do
|
|
256
|
+
pr["comments"] = []
|
|
257
|
+
iterate_pages(client, "issue_comments", pr["number"]) do |new_comment|
|
|
258
|
+
pr["comments"].concat(new_comment)
|
|
259
|
+
end
|
|
260
|
+
pr["comments"] = pr["comments"].map { |comment| stringify_keys_deep(comment.to_hash) }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
barrier.wait
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Fetch tag time from repo
|
|
271
|
+
#
|
|
272
|
+
# @param [Hash] tag GitHub data item about a Tag
|
|
273
|
+
#
|
|
274
|
+
# @return [Time] time of specified tag
|
|
275
|
+
def fetch_date_of_tag(tag)
|
|
276
|
+
commit_data = fetch_commit(tag["commit"]["sha"])
|
|
277
|
+
commit_data = stringify_keys_deep(commit_data.to_hash)
|
|
278
|
+
|
|
279
|
+
commit_data["commit"]["committer"]["date"]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Fetch commit for specified event
|
|
283
|
+
#
|
|
284
|
+
# @param [String] commit_id the SHA of a commit to fetch
|
|
285
|
+
# @return [Hash]
|
|
286
|
+
def fetch_commit(commit_id)
|
|
287
|
+
found = commits.find do |commit|
|
|
288
|
+
commit["sha"] == commit_id
|
|
289
|
+
end
|
|
290
|
+
if found
|
|
291
|
+
stringify_keys_deep(found.to_hash)
|
|
292
|
+
else
|
|
293
|
+
client = self.client
|
|
294
|
+
|
|
295
|
+
# cache miss; don't add to @commits because unsure of order.
|
|
296
|
+
check_github_response do
|
|
297
|
+
commit = client.commit(user_project, commit_id)
|
|
298
|
+
commit = stringify_keys_deep(commit.to_hash)
|
|
299
|
+
commit
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Fetch all commits
|
|
305
|
+
#
|
|
306
|
+
# @return [Array] Commits in a repo.
|
|
307
|
+
def commits
|
|
308
|
+
if @commits.empty?
|
|
309
|
+
Sync do
|
|
310
|
+
barrier = Async::Barrier.new
|
|
311
|
+
semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
|
|
312
|
+
|
|
313
|
+
branch = @options[:release_branch] || default_branch
|
|
314
|
+
if (since_commit = @options[:since_commit])
|
|
315
|
+
iterate_pages(client, "commits_since", since_commit, branch, parent: semaphore) do |new_commits|
|
|
316
|
+
@commits.concat(new_commits)
|
|
317
|
+
end
|
|
318
|
+
else
|
|
319
|
+
iterate_pages(client, "commits", branch, parent: semaphore) do |new_commits|
|
|
320
|
+
@commits.concat(new_commits)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
barrier.wait
|
|
325
|
+
|
|
326
|
+
@commits.sort! do |b, a|
|
|
327
|
+
a[:commit][:author][:date] <=> b[:commit][:author][:date]
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
@commits
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Return the oldest commit in a repo
|
|
335
|
+
#
|
|
336
|
+
# @return [Hash] Oldest commit in the github git history.
|
|
337
|
+
def oldest_commit
|
|
338
|
+
commits.last
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# @return [String] Default branch of the repo
|
|
342
|
+
def default_branch
|
|
343
|
+
@default_branch ||= client.repository(user_project)[:default_branch]
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# @param [String] name
|
|
347
|
+
# @return [Array<String>]
|
|
348
|
+
def commits_in_branch(name)
|
|
349
|
+
@branches ||= client.branches(user_project).map { |branch| [branch[:name], branch] }.to_h
|
|
350
|
+
|
|
351
|
+
if (branch = @branches[name])
|
|
352
|
+
commits_in_tag(branch[:commit][:sha])
|
|
353
|
+
else
|
|
354
|
+
[]
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Fetch all SHAs occurring in or before a given tag and add them to
|
|
359
|
+
# "shas_in_tag"
|
|
360
|
+
#
|
|
361
|
+
# @param [Array] tags The array of tags.
|
|
362
|
+
# @return void
|
|
363
|
+
def fetch_tag_shas(tags)
|
|
364
|
+
# Reverse the tags array to gain max benefit from the @commits_in_tag_cache
|
|
365
|
+
tags.reverse_each do |tag|
|
|
366
|
+
tag["shas_in_tag"] = commits_in_tag(tag["commit"]["sha"])
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
private
|
|
371
|
+
|
|
372
|
+
# @param [Set] shas
|
|
373
|
+
# @param [Object] sha
|
|
374
|
+
def commits_in_tag(sha, shas = Set.new)
|
|
375
|
+
# Reduce multiple runs for the same tag
|
|
376
|
+
return @commits_in_tag_cache[sha] if @commits_in_tag_cache.key?(sha)
|
|
377
|
+
|
|
378
|
+
@graph ||= commits.map { |commit| [commit[:sha], commit] }.to_h
|
|
379
|
+
return shas unless (current = @graph[sha])
|
|
380
|
+
|
|
381
|
+
queue = [current]
|
|
382
|
+
while queue.any?
|
|
383
|
+
commit = queue.shift
|
|
384
|
+
# If we've already processed this sha, just grab it's parents from the cache
|
|
385
|
+
if @commits_in_tag_cache.key?(commit[:sha])
|
|
386
|
+
shas.merge(@commits_in_tag_cache[commit[:sha]])
|
|
387
|
+
else
|
|
388
|
+
shas.add(commit[:sha])
|
|
389
|
+
commit[:parents].each do |p|
|
|
390
|
+
queue.push(@graph[p[:sha]]) unless shas.include?(p[:sha])
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
@commits_in_tag_cache[sha] = shas
|
|
396
|
+
shas
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# @param [Object] indata
|
|
400
|
+
def stringify_keys_deep(indata)
|
|
401
|
+
case indata
|
|
402
|
+
when Array
|
|
403
|
+
indata.map do |value|
|
|
404
|
+
stringify_keys_deep(value)
|
|
405
|
+
end
|
|
406
|
+
when Hash
|
|
407
|
+
indata.each_with_object({}) do |(key, value), output|
|
|
408
|
+
output[key.to_s] = stringify_keys_deep(value)
|
|
409
|
+
end
|
|
410
|
+
else
|
|
411
|
+
indata
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Exception raised to warn about moved repositories.
|
|
416
|
+
MovedPermanentlyError = Class.new(RuntimeError)
|
|
417
|
+
|
|
418
|
+
# Iterates through all pages until there are no more :next pages to follow
|
|
419
|
+
# yields the result per page
|
|
420
|
+
#
|
|
421
|
+
# @param [Octokit::Client] client
|
|
422
|
+
# @param [String] method (eg. 'tags')
|
|
423
|
+
# @param [Array] arguments
|
|
424
|
+
# @param [Async::Semaphore] parent
|
|
425
|
+
#
|
|
426
|
+
# @yield [Sawyer::Resource] An OctoKit-provided response (which can be empty)
|
|
427
|
+
#
|
|
428
|
+
# @return [void]
|
|
429
|
+
# @param [Hash] options
|
|
430
|
+
def iterate_pages(client, method, *arguments, parent: nil, **options)
|
|
431
|
+
options = DEFAULT_REQUEST_OPTIONS.merge(options)
|
|
432
|
+
|
|
433
|
+
check_github_response { client.send(method, user_project, *arguments, **options) }
|
|
434
|
+
last_response = client.last_response.tap do |response|
|
|
435
|
+
raise(MovedPermanentlyError, response.data[:url]) if response.status == 301
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
yield(last_response.data)
|
|
439
|
+
|
|
440
|
+
if parent.nil?
|
|
441
|
+
# The snail visits one leaf at a time:
|
|
442
|
+
until (next_one = last_response.rels[:next]).nil?
|
|
443
|
+
last_response = check_github_response { next_one.get }
|
|
444
|
+
yield(last_response.data)
|
|
445
|
+
end
|
|
446
|
+
elsif (last = last_response.rels[:last])
|
|
447
|
+
# OR we bring out the gatling gun:
|
|
448
|
+
parameters = querystring_as_hash(last.href)
|
|
449
|
+
last_page = Integer(parameters["page"])
|
|
450
|
+
|
|
451
|
+
(2..last_page).each do |page|
|
|
452
|
+
parent.async do
|
|
453
|
+
data = check_github_response { client.send(method, user_project, *arguments, page: page, **options) }
|
|
454
|
+
yield data
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# This is wrapper with rescue block
|
|
461
|
+
#
|
|
462
|
+
# @return [Object] returns exactly the same, what you put in the block, but wrap it with begin-rescue block
|
|
463
|
+
# @param [Proc] block
|
|
464
|
+
def check_github_response
|
|
465
|
+
yield
|
|
466
|
+
rescue MovedPermanentlyError => e
|
|
467
|
+
fail_with_message(e, "The repository has moved, update your configuration")
|
|
468
|
+
rescue Octokit::TooManyRequests => e
|
|
469
|
+
resets_in = client.rate_limit.resets_in
|
|
470
|
+
Helper.log.error("#{e.class} #{e.message}; sleeping for #{resets_in}s...")
|
|
471
|
+
|
|
472
|
+
if (task = Async::Task.current?)
|
|
473
|
+
task.sleep(resets_in)
|
|
474
|
+
else
|
|
475
|
+
sleep(resets_in)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
retry
|
|
479
|
+
rescue Octokit::Forbidden => e
|
|
480
|
+
fail_with_message(e, "Exceeded retry limit")
|
|
481
|
+
rescue Octokit::Unauthorized => e
|
|
482
|
+
fail_with_message(e, "Error: wrong GitHub token")
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Presents the exception, and the aborts with the message.
|
|
486
|
+
# @param [Object] message
|
|
487
|
+
# @param [Object] error
|
|
488
|
+
def fail_with_message(error, message)
|
|
489
|
+
Helper.log.error("#{error.class}: #{error.message}")
|
|
490
|
+
sys_abort(message)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# @param [Object] msg
|
|
494
|
+
def sys_abort(msg)
|
|
495
|
+
abort(msg)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Print specified line on the same string
|
|
499
|
+
#
|
|
500
|
+
# @param [String] log_string
|
|
501
|
+
def print_in_same_line(log_string)
|
|
502
|
+
print "#{log_string}\r"
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Print long line with spaces on same line to clear prev message
|
|
506
|
+
def print_empty_line
|
|
507
|
+
print_in_same_line(" ")
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Returns GitHub token. First try to use variable, provided by --token option,
|
|
511
|
+
# otherwise try to fetch it from CHANGELOG_GITHUB_TOKEN env variable.
|
|
512
|
+
#
|
|
513
|
+
# @return [String]
|
|
514
|
+
def fetch_github_token
|
|
515
|
+
env_var = @options[:token].presence || ENV["CHANGELOG_GITHUB_TOKEN"]
|
|
516
|
+
|
|
517
|
+
Helper.log.warn NO_TOKEN_PROVIDED unless env_var
|
|
518
|
+
|
|
519
|
+
env_var
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# @return [String] helper to return GitHub "user/project"
|
|
523
|
+
def user_project
|
|
524
|
+
"#{@options[:user]}/#{@options[:project]}"
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Returns Hash of all querystring variables in given URI.
|
|
528
|
+
#
|
|
529
|
+
# @param [String] uri eg. https://api.github.com/repositories/43914960/tags?page=37&foo=1
|
|
530
|
+
# @return [Hash] of all GET variables. eg. { 'page' => 37, 'foo' => 1 }
|
|
531
|
+
def querystring_as_hash(uri)
|
|
532
|
+
Hash[URI.decode_www_form(URI(uri).query || "")]
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
require "github_changelog_generator/helper"
|
|
5
|
+
|
|
6
|
+
module GitHubChangelogGenerator
|
|
7
|
+
# This class wraps Options, and knows a list of known options. Others options
|
|
8
|
+
# will raise exceptions.
|
|
9
|
+
class Options < SimpleDelegator
|
|
10
|
+
# Raised on initializing with unknown keys in the values hash,
|
|
11
|
+
# and when trying to store a value on an unknown key.
|
|
12
|
+
UnsupportedOptionError = Class.new(ArgumentError)
|
|
13
|
+
|
|
14
|
+
# List of valid option names
|
|
15
|
+
KNOWN_OPTIONS = %i[
|
|
16
|
+
add_issues_wo_labels
|
|
17
|
+
add_pr_wo_labels
|
|
18
|
+
add_sections
|
|
19
|
+
author
|
|
20
|
+
base
|
|
21
|
+
between_tags
|
|
22
|
+
breaking_labels
|
|
23
|
+
breaking_prefix
|
|
24
|
+
bug_labels
|
|
25
|
+
bug_prefix
|
|
26
|
+
cache_file
|
|
27
|
+
cache_log
|
|
28
|
+
config_file
|
|
29
|
+
compare_link
|
|
30
|
+
configure_sections
|
|
31
|
+
date_format
|
|
32
|
+
deprecated_labels
|
|
33
|
+
deprecated_prefix
|
|
34
|
+
due_tag
|
|
35
|
+
enhancement_labels
|
|
36
|
+
enhancement_prefix
|
|
37
|
+
exclude_labels
|
|
38
|
+
exclude_tags
|
|
39
|
+
exclude_tags_regex
|
|
40
|
+
filter_issues_by_milestone
|
|
41
|
+
issues_of_open_milestones
|
|
42
|
+
frontmatter
|
|
43
|
+
future_release
|
|
44
|
+
github_endpoint
|
|
45
|
+
github_site
|
|
46
|
+
header
|
|
47
|
+
http_cache
|
|
48
|
+
include_labels
|
|
49
|
+
include_tags_regex
|
|
50
|
+
issue_prefix
|
|
51
|
+
issue_line_labels
|
|
52
|
+
issue_line_body
|
|
53
|
+
issues
|
|
54
|
+
max_issues
|
|
55
|
+
merge_prefix
|
|
56
|
+
output
|
|
57
|
+
project
|
|
58
|
+
pulls
|
|
59
|
+
release_branch
|
|
60
|
+
release_url
|
|
61
|
+
removed_labels
|
|
62
|
+
removed_prefix
|
|
63
|
+
require
|
|
64
|
+
security_labels
|
|
65
|
+
security_prefix
|
|
66
|
+
simple_list
|
|
67
|
+
since_tag
|
|
68
|
+
since_commit
|
|
69
|
+
ssl_ca_file
|
|
70
|
+
summary_labels
|
|
71
|
+
summary_prefix
|
|
72
|
+
token
|
|
73
|
+
unreleased
|
|
74
|
+
unreleased_label
|
|
75
|
+
unreleased_only
|
|
76
|
+
user
|
|
77
|
+
usernames_as_github_logins
|
|
78
|
+
verbose
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# @param values [Hash]
|
|
82
|
+
#
|
|
83
|
+
# @raise [UnsupportedOptionError] if given values contain unknown options
|
|
84
|
+
def initialize(values)
|
|
85
|
+
super(values)
|
|
86
|
+
unsupported_options.any? && raise(UnsupportedOptionError, unsupported_options.inspect)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Set option key to val.
|
|
90
|
+
#
|
|
91
|
+
# @param key [Symbol]
|
|
92
|
+
# @param val [Object]
|
|
93
|
+
#
|
|
94
|
+
# @raise [UnsupportedOptionError] when trying to set an unknown option
|
|
95
|
+
def []=(key, val)
|
|
96
|
+
supported_option?(key) || raise(UnsupportedOptionError, key.inspect)
|
|
97
|
+
values[key] = val
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Hash]
|
|
101
|
+
def to_hash
|
|
102
|
+
values
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Loads the configured Ruby files from the --require option.
|
|
106
|
+
def load_custom_ruby_files
|
|
107
|
+
self[:require].each { |f| require f }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Pretty-prints a censored options hash, if :verbose.
|
|
111
|
+
def print_options
|
|
112
|
+
return unless self[:verbose]
|
|
113
|
+
|
|
114
|
+
Helper.log.info "Using these options:"
|
|
115
|
+
# For ruby 2.5.0+
|
|
116
|
+
censored_values.each do |key, value|
|
|
117
|
+
print(key.inspect, "=>", value.inspect)
|
|
118
|
+
puts ""
|
|
119
|
+
end
|
|
120
|
+
puts ""
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Boolean method for whether the user is using configure_sections
|
|
124
|
+
def configure_sections?
|
|
125
|
+
!self[:configure_sections].nil? && !self[:configure_sections].empty?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Boolean method for whether the user is using add_sections
|
|
129
|
+
def add_sections?
|
|
130
|
+
!self[:add_sections].nil? && !self[:add_sections].empty?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @return [Boolean] whether write to `:output`
|
|
134
|
+
def write_to_file?
|
|
135
|
+
self[:output].present?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def values
|
|
141
|
+
__getobj__
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns a censored options hash.
|
|
145
|
+
#
|
|
146
|
+
# @return [Hash] The GitHub `:token` key is censored in the output.
|
|
147
|
+
def censored_values
|
|
148
|
+
values.clone.tap { |opts| opts[:token] = opts[:token].nil? ? "No token used" : "hidden value" }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def unsupported_options
|
|
152
|
+
values.keys - KNOWN_OPTIONS
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def supported_option?(key)
|
|
156
|
+
KNOWN_OPTIONS.include?(key)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|