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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +9 -0
  3. data/README.md +357 -0
  4. data/Rakefile +19 -0
  5. data/bin/git-generate-changelog +5 -0
  6. data/bin/github_changelog_generator +5 -0
  7. data/lib/github_changelog_generator/argv_parser.rb +225 -0
  8. data/lib/github_changelog_generator/file_parser_chooser.rb +27 -0
  9. data/lib/github_changelog_generator/generator/entry.rb +218 -0
  10. data/lib/github_changelog_generator/generator/generator.rb +177 -0
  11. data/lib/github_changelog_generator/generator/generator_fetcher.rb +202 -0
  12. data/lib/github_changelog_generator/generator/generator_processor.rb +237 -0
  13. data/lib/github_changelog_generator/generator/generator_tags.rb +210 -0
  14. data/lib/github_changelog_generator/generator/section.rb +129 -0
  15. data/lib/github_changelog_generator/helper.rb +37 -0
  16. data/lib/github_changelog_generator/octo_fetcher.rb +535 -0
  17. data/lib/github_changelog_generator/options.rb +159 -0
  18. data/lib/github_changelog_generator/parser.rb +89 -0
  19. data/lib/github_changelog_generator/parser_file.rb +101 -0
  20. data/lib/github_changelog_generator/reader.rb +88 -0
  21. data/lib/github_changelog_generator/ssl_certs/cacert.pem +3138 -0
  22. data/lib/github_changelog_generator/task.rb +68 -0
  23. data/lib/github_changelog_generator/version.rb +5 -0
  24. data/lib/github_changelog_generator.rb +49 -0
  25. data/man/git-generate-changelog.1 +393 -0
  26. data/man/git-generate-changelog.1.html +359 -0
  27. data/man/git-generate-changelog.html +270 -0
  28. data/man/git-generate-changelog.md +274 -0
  29. data/spec/files/angular.js.md +9395 -0
  30. data/spec/files/bundler.md +1911 -0
  31. data/spec/files/config_example +5 -0
  32. data/spec/files/github-changelog-generator.md +305 -0
  33. data/spec/github_changelog_generator_spec.rb +32 -0
  34. data/spec/install_gem_in_bundler.gemfile +5 -0
  35. data/spec/spec_helper.rb +74 -0
  36. data/spec/unit/generator/entry_spec.rb +766 -0
  37. data/spec/unit/generator/generator_processor_spec.rb +203 -0
  38. data/spec/unit/generator/generator_spec.rb +47 -0
  39. data/spec/unit/generator/generator_tags_spec.rb +337 -0
  40. data/spec/unit/generator/section_spec.rb +43 -0
  41. data/spec/unit/octo_fetcher_spec.rb +590 -0
  42. data/spec/unit/options_spec.rb +67 -0
  43. data/spec/unit/parser_file_spec.rb +94 -0
  44. data/spec/unit/parser_spec.rb +54 -0
  45. data/spec/unit/reader_spec.rb +120 -0
  46. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits/when_API_is_valid/returns_commits.json +1 -0
  47. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits_before/when_API_is_valid/returns_commits.json +1 -0
  48. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -0
  49. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -0
  50. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -0
  51. 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
  52. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -0
  53. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -0
  54. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -0
  55. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -0
  56. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -0
  57. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -0
  58. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -0
  59. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -0
  60. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -0
  61. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -0
  62. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -0
  63. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -0
  64. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -0
  65. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -0
  66. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -0
  67. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -0
  68. 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