px_github_changelog_generator 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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