github_changelog_generator 1.15.0.pre.beta → 1.16.1

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 (62) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +1 -1
  3. data/README.md +332 -285
  4. data/Rakefile +1 -1
  5. data/bin/git-generate-changelog +1 -1
  6. data/lib/github_changelog_generator.rb +10 -6
  7. data/lib/github_changelog_generator/generator/entry.rb +218 -0
  8. data/lib/github_changelog_generator/generator/generator.rb +124 -125
  9. data/lib/github_changelog_generator/generator/generator_fetcher.rb +139 -23
  10. data/lib/github_changelog_generator/generator/generator_processor.rb +59 -27
  11. data/lib/github_changelog_generator/generator/generator_tags.rb +25 -21
  12. data/lib/github_changelog_generator/generator/section.rb +124 -0
  13. data/lib/github_changelog_generator/helper.rb +1 -1
  14. data/lib/github_changelog_generator/octo_fetcher.rb +233 -96
  15. data/lib/github_changelog_generator/options.rb +74 -2
  16. data/lib/github_changelog_generator/parser.rb +118 -74
  17. data/lib/github_changelog_generator/parser_file.rb +7 -3
  18. data/lib/github_changelog_generator/reader.rb +2 -2
  19. data/lib/github_changelog_generator/task.rb +4 -3
  20. data/lib/github_changelog_generator/version.rb +1 -1
  21. data/man/git-generate-changelog.1 +144 -45
  22. data/man/git-generate-changelog.1.html +157 -84
  23. data/man/git-generate-changelog.html +19 -7
  24. data/man/git-generate-changelog.md +151 -84
  25. data/spec/files/github-changelog-generator.md +114 -114
  26. data/spec/{install-gem-in-bundler.gemfile → install_gem_in_bundler.gemfile} +2 -0
  27. data/spec/spec_helper.rb +2 -6
  28. data/spec/unit/generator/entry_spec.rb +766 -0
  29. data/spec/unit/generator/generator_processor_spec.rb +103 -41
  30. data/spec/unit/generator/generator_spec.rb +47 -0
  31. data/spec/unit/generator/generator_tags_spec.rb +51 -24
  32. data/spec/unit/generator/section_spec.rb +34 -0
  33. data/spec/unit/octo_fetcher_spec.rb +247 -197
  34. data/spec/unit/options_spec.rb +24 -0
  35. data/spec/unit/parse_file_spec.rb +2 -2
  36. data/spec/unit/reader_spec.rb +4 -4
  37. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits/when_API_is_valid/returns_commits.json +1 -0
  38. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits_before/when_API_is_valid/returns_commits.json +1 -1
  39. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -1
  40. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -1
  41. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -1
  42. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -1
  43. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_request_with_proper_key/values.json +1 -1
  44. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -1
  45. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -1
  46. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -1
  47. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -1
  48. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -1
  49. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -1
  50. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -1
  51. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -1
  52. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -1
  53. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -1
  54. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -1
  55. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -1
  56. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -1
  57. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -1
  58. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -1
  59. metadata +71 -38
  60. data/bin/ghclgen +0 -5
  61. data/lib/github_changelog_generator/generator/generator_generation.rb +0 -180
  62. data/spec/unit/generator/generator_generation_spec.rb +0 -73
@@ -0,0 +1,124 @@
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 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(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
+ def body_till_first_break(body)
89
+ body.split(/\n/, 2).first
90
+ end
91
+
92
+ def issue_line_with_user(line, issue)
93
+ return line if !@options[:author] || issue["pull_request"].nil?
94
+
95
+ user = issue["user"]
96
+ return "#{line} ({Null user})" unless user
97
+
98
+ if @options[:usernames_as_github_logins]
99
+ "#{line} (@#{user['login']})"
100
+ else
101
+ "#{line} ([#{user['login']}](#{user['html_url']}))"
102
+ end
103
+ end
104
+
105
+ ENCAPSULATED_CHARACTERS = %w(< > * _ \( \) [ ] #)
106
+
107
+ # Encapsulate characters to make Markdown look as expected.
108
+ #
109
+ # @param [String] string
110
+ # @return [String] encapsulated input string
111
+ def encapsulate_string(string)
112
+ string = string.gsub('\\', '\\\\')
113
+
114
+ ENCAPSULATED_CHARACTERS.each do |char|
115
+ # Only replace char with escaped version if it isn't inside backticks (markdown inline code).
116
+ # This relies on each opening '`' being closed (ie an even number in total).
117
+ # A char is *outside* backticks if there is an even number of backticks following it.
118
+ string = string.gsub(%r{#{Regexp.escape(char)}(?=([^`]*`[^`]*`)*[^`]*$)}, "\\#{char}")
119
+ end
120
+
121
+ string
122
+ end
123
+ end
124
+ end
@@ -14,7 +14,7 @@ module GitHubChangelogGenerator
14
14
  @log ||= if test?
15
15
  Logger.new(nil) # don't show any logs when running tests
16
16
  else
17
- Logger.new(STDOUT)
17
+ Logger.new($stdout)
18
18
  end
19
19
  @log.formatter = proc do |severity, _datetime, _progname, msg|
20
20
  string = "#{msg}\n"
@@ -2,6 +2,12 @@
2
2
 
3
3
  require "tmpdir"
4
4
  require "retriable"
5
+ require "set"
6
+ require "async"
7
+ require "async/barrier"
8
+ require "async/semaphore"
9
+ require "async/http/faraday"
10
+
5
11
  module GitHubChangelogGenerator
6
12
  # A Fetcher responsible for all requests to GitHub and all basic manipulation with related data
7
13
  # (such as filtering, validating, e.t.c)
@@ -9,14 +15,14 @@ module GitHubChangelogGenerator
9
15
  # Example:
10
16
  # fetcher = GitHubChangelogGenerator::OctoFetcher.new(options)
11
17
  class OctoFetcher
12
- PER_PAGE_NUMBER = 100
13
- MAX_THREAD_NUMBER = 25
18
+ PER_PAGE_NUMBER = 100
19
+ MAXIMUM_CONNECTIONS = 50
14
20
  MAX_FORBIDDEN_RETRIES = 100
15
21
  CHANGELOG_GITHUB_TOKEN = "CHANGELOG_GITHUB_TOKEN"
16
- GH_RATE_LIMIT_EXCEEDED_MSG = "Warning: Can't finish operation: GitHub API rate limit exceeded, change log may be " \
22
+ GH_RATE_LIMIT_EXCEEDED_MSG = "Warning: Can't finish operation: GitHub API rate limit exceeded, changelog may be " \
17
23
  "missing some issues. You can limit the number of issues fetched using the `--max-issues NUM` argument."
18
24
  NO_TOKEN_PROVIDED = "Warning: No token provided (-t option) and variable $CHANGELOG_GITHUB_TOKEN was not found. " \
19
- "This script can make only 50 requests to GitHub API per hour without token!"
25
+ "This script can make only 50 requests to GitHub API per hour without a token!"
20
26
 
21
27
  # @param options [Hash] Options passed in
22
28
  # @option options [String] :user GitHub username
@@ -31,44 +37,58 @@ module GitHubChangelogGenerator
31
37
  @project = @options[:project]
32
38
  @since = @options[:since]
33
39
  @http_cache = @options[:http_cache]
34
- @cache_file = nil
35
- @cache_log = nil
36
- prepare_cache
37
- configure_octokit_ssl
38
- @client = Octokit::Client.new(github_options)
40
+ @commits = []
41
+ @branches = nil
42
+ @graph = nil
43
+ @client = nil
44
+ @commits_in_tag_cache = {}
39
45
  end
40
46
 
41
- def prepare_cache
42
- return unless @http_cache
43
- @cache_file = @options.fetch(:cache_file) { File.join(Dir.tmpdir, "github-changelog-http-cache") }
44
- @cache_log = @options.fetch(:cache_log) { File.join(Dir.tmpdir, "github-changelog-logger.log") }
45
- init_cache
46
- end
47
+ def middleware
48
+ Faraday::RackBuilder.new do |builder|
49
+ if @http_cache
50
+ cache_file = @options.fetch(:cache_file) { File.join(Dir.tmpdir, "github-changelog-http-cache") }
51
+ cache_log = @options.fetch(:cache_log) { File.join(Dir.tmpdir, "github-changelog-logger.log") }
52
+
53
+ builder.use(
54
+ Faraday::HttpCache,
55
+ serializer: Marshal,
56
+ store: ActiveSupport::Cache::FileStore.new(cache_file),
57
+ logger: Logger.new(cache_log),
58
+ shared_cache: false
59
+ )
60
+ end
47
61
 
48
- def github_options
49
- result = {}
50
- github_token = fetch_github_token
51
- result[:access_token] = github_token if github_token
52
- endpoint = @options[:github_endpoint]
53
- result[:api_endpoint] = endpoint if endpoint
54
- result
62
+ builder.use Octokit::Response::RaiseError
63
+ builder.adapter :async_http
64
+ end
55
65
  end
56
66
 
57
- def configure_octokit_ssl
58
- ca_file = @options[:ssl_ca_file] || ENV["SSL_CA_FILE"] || File.expand_path("../ssl_certs/cacert.pem", __FILE__)
59
- Octokit.connection_options = { ssl: { ca_file: ca_file } }
67
+ def connection_options
68
+ ca_file = @options[:ssl_ca_file] || ENV["SSL_CA_FILE"] || File.expand_path("ssl_certs/cacert.pem", __dir__)
69
+
70
+ Octokit.connection_options.merge({ ssl: { ca_file: ca_file } })
60
71
  end
61
72
 
62
- def init_cache
63
- Octokit.middleware = Faraday::RackBuilder.new do |builder|
64
- builder.use(Faraday::HttpCache, serializer: Marshal,
65
- store: ActiveSupport::Cache::FileStore.new(@cache_file),
66
- logger: Logger.new(@cache_log),
67
- shared_cache: false)
68
- builder.use Octokit::Response::RaiseError
69
- builder.adapter Faraday.default_adapter
70
- # builder.response :logger
73
+ def client_options
74
+ options = {
75
+ middleware: middleware,
76
+ connection_options: connection_options
77
+ }
78
+
79
+ if (github_token = fetch_github_token)
80
+ options[:access_token] = github_token
71
81
  end
82
+
83
+ if (endpoint = @options[:github_endpoint])
84
+ options[:api_endpoint] = endpoint
85
+ end
86
+
87
+ options
88
+ end
89
+
90
+ def client
91
+ @client ||= Octokit::Client.new(client_options)
72
92
  end
73
93
 
74
94
  DEFAULT_REQUEST_OPTIONS = { per_page: PER_PAGE_NUMBER }
@@ -104,11 +124,11 @@ module GitHubChangelogGenerator
104
124
  #
105
125
  # @return [Array <Hash>] array of tags in repo
106
126
  def github_fetch_tags
107
- tags = []
108
- page_i = 0
109
- count_pages = calculate_pages(@client, "tags", {})
127
+ tags = []
128
+ page_i = 0
129
+ count_pages = calculate_pages(client, "tags", {})
110
130
 
111
- iterate_pages(@client, "tags") do |new_tags|
131
+ iterate_pages(client, "tags") do |new_tags|
112
132
  page_i += PER_PAGE_NUMBER
113
133
  print_in_same_line("Fetching tags... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
114
134
  tags.concat(new_tags)
@@ -138,10 +158,10 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
138
158
  def fetch_closed_issues_and_pr
139
159
  print "Fetching closed issues...\r" if @options[:verbose]
140
160
  issues = []
141
- page_i = 0
142
- count_pages = calculate_pages(@client, "issues", closed_pr_options)
161
+ page_i = 0
162
+ count_pages = calculate_pages(client, "issues", closed_pr_options)
143
163
 
144
- iterate_pages(@client, "issues", closed_pr_options) do |new_issues|
164
+ iterate_pages(client, "issues", closed_pr_options) do |new_issues|
145
165
  page_i += PER_PAGE_NUMBER
146
166
  print_in_same_line("Fetching issues... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
147
167
  issues.concat(new_issues)
@@ -162,14 +182,10 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
162
182
  pull_requests = []
163
183
  options = { state: "closed" }
164
184
 
165
- unless @options[:release_branch].nil?
166
- options[:base] = @options[:release_branch]
167
- end
168
-
169
- page_i = 0
170
- count_pages = calculate_pages(@client, "pull_requests", options)
185
+ page_i = 0
186
+ count_pages = calculate_pages(client, "pull_requests", options)
171
187
 
172
- iterate_pages(@client, "pull_requests", options) do |new_pr|
188
+ iterate_pages(client, "pull_requests", options) do |new_pr|
173
189
  page_i += PER_PAGE_NUMBER
174
190
  log_string = "Fetching merged dates... #{page_i}/#{count_pages * PER_PAGE_NUMBER}"
175
191
  print_in_same_line(log_string)
@@ -186,14 +202,20 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
186
202
  # @param [Array] issues
187
203
  # @return [Void]
188
204
  def fetch_events_async(issues)
189
- i = 0
190
- threads = []
205
+ i = 0
206
+ # Add accept option explicitly for disabling the warning of preview API.
207
+ preview = { accept: Octokit::Preview::PREVIEW_TYPES[:project_card_events] }
208
+
209
+ barrier = Async::Barrier.new
210
+ semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
211
+
212
+ Sync do
213
+ client = self.client
191
214
 
192
- issues.each_slice(MAX_THREAD_NUMBER) do |issues_slice|
193
- issues_slice.each do |issue|
194
- threads << Thread.new do
215
+ issues.each do |issue|
216
+ semaphore.async do
195
217
  issue["events"] = []
196
- iterate_pages(@client, "issue_events", issue["number"]) do |new_event|
218
+ iterate_pages(client, "issue_events", issue["number"], preview) do |new_event|
197
219
  issue["events"].concat(new_event)
198
220
  end
199
221
  issue["events"] = issue["events"].map { |event| stringify_keys_deep(event.to_hash) }
@@ -201,23 +223,50 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
201
223
  i += 1
202
224
  end
203
225
  end
204
- threads.each(&:join)
205
- threads = []
206
- end
207
226
 
208
- # to clear line from prev print
209
- print_empty_line
227
+ barrier.wait
228
+
229
+ # to clear line from prev print
230
+ print_empty_line
231
+ end
210
232
 
211
233
  Helper.log.info "Fetching events for issues and PR: #{i}"
212
234
  end
213
235
 
236
+ # Fetch comments for PRs and add them to "comments"
237
+ #
238
+ # @param [Array] prs The array of PRs.
239
+ # @return [Void] No return; PRs are updated in-place.
240
+ def fetch_comments_async(prs)
241
+ barrier = Async::Barrier.new
242
+ semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
243
+
244
+ Sync do
245
+ client = self.client
246
+
247
+ prs.each do |pr|
248
+ semaphore.async do
249
+ pr["comments"] = []
250
+ iterate_pages(client, "issue_comments", pr["number"]) do |new_comment|
251
+ pr["comments"].concat(new_comment)
252
+ end
253
+ pr["comments"] = pr["comments"].map { |comment| stringify_keys_deep(comment.to_hash) }
254
+ end
255
+ end
256
+
257
+ barrier.wait
258
+ end
259
+
260
+ nil
261
+ end
262
+
214
263
  # Fetch tag time from repo
215
264
  #
216
265
  # @param [Hash] tag GitHub data item about a Tag
217
266
  #
218
267
  # @return [Time] time of specified tag
219
268
  def fetch_date_of_tag(tag)
220
- commit_data = check_github_response { @client.commit(user_project, tag["commit"]["sha"]) }
269
+ commit_data = fetch_commit(tag["commit"]["sha"])
221
270
  commit_data = stringify_keys_deep(commit_data.to_hash)
222
271
 
223
272
  commit_data["commit"]["committer"]["date"]
@@ -225,28 +274,114 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
225
274
 
226
275
  # Fetch commit for specified event
227
276
  #
277
+ # @param [String] commit_id the SHA of a commit to fetch
228
278
  # @return [Hash]
229
- def fetch_commit(event)
230
- check_github_response do
231
- commit = @client.commit(user_project, event["commit_id"])
232
- commit = stringify_keys_deep(commit.to_hash)
233
- commit
279
+ def fetch_commit(commit_id)
280
+ found = commits.find do |commit|
281
+ commit["sha"] == commit_id
282
+ end
283
+ if found
284
+ stringify_keys_deep(found.to_hash)
285
+ else
286
+ client = self.client
287
+
288
+ # cache miss; don't add to @commits because unsure of order.
289
+ check_github_response do
290
+ commit = client.commit(user_project, commit_id)
291
+ commit = stringify_keys_deep(commit.to_hash)
292
+ commit
293
+ end
234
294
  end
235
295
  end
236
296
 
237
- # Fetch all commits before certain point
297
+ # Fetch all commits
238
298
  #
239
- # @return [String]
240
- def commits_before(start_time)
241
- commits = []
242
- iterate_pages(@client, "commits_before", start_time.to_datetime.to_s) do |new_commits|
243
- commits.concat(new_commits)
299
+ # @return [Array] Commits in a repo.
300
+ def commits
301
+ if @commits.empty?
302
+ Sync do
303
+ barrier = Async::Barrier.new
304
+ semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
305
+
306
+ if (since_commit = @options[:since_commit])
307
+ iterate_pages(client, "commits_since", since_commit, parent: semaphore) do |new_commits|
308
+ @commits.concat(new_commits)
309
+ end
310
+ else
311
+ iterate_pages(client, "commits", parent: semaphore) do |new_commits|
312
+ @commits.concat(new_commits)
313
+ end
314
+ end
315
+
316
+ barrier.wait
317
+
318
+ @commits.sort! do |b, a|
319
+ a[:commit][:author][:date] <=> b[:commit][:author][:date]
320
+ end
321
+ end
322
+ end
323
+ @commits
324
+ end
325
+
326
+ # Return the oldest commit in a repo
327
+ #
328
+ # @return [Hash] Oldest commit in the github git history.
329
+ def oldest_commit
330
+ commits.last
331
+ end
332
+
333
+ # @return [String] Default branch of the repo
334
+ def default_branch
335
+ @default_branch ||= client.repository(user_project)[:default_branch]
336
+ end
337
+
338
+ def commits_in_branch(name)
339
+ @branches ||= client.branches(user_project).map { |branch| [branch[:name], branch] }.to_h
340
+
341
+ if (branch = @branches[name])
342
+ commits_in_tag(branch[:commit][:sha])
343
+ end
344
+ end
345
+
346
+ # Fetch all SHAs occurring in or before a given tag and add them to
347
+ # "shas_in_tag"
348
+ #
349
+ # @param [Array] tags The array of tags.
350
+ # @return [Nil] No return; tags are updated in-place.
351
+ def fetch_tag_shas(tags)
352
+ # Reverse the tags array to gain max benefit from the @commits_in_tag_cache
353
+ tags.reverse_each do |tag|
354
+ tag["shas_in_tag"] = commits_in_tag(tag["commit"]["sha"])
244
355
  end
245
- commits
246
356
  end
247
357
 
248
358
  private
249
359
 
360
+ def commits_in_tag(sha, shas = Set.new)
361
+ # Reduce multiple runs for the same tag
362
+ return @commits_in_tag_cache[sha] if @commits_in_tag_cache.key?(sha)
363
+
364
+ @graph ||= commits.map { |commit| [commit[:sha], commit] }.to_h
365
+ return shas unless (current = @graph[sha])
366
+
367
+ queue = [current]
368
+ while queue.any?
369
+ commit = queue.shift
370
+ # If we've already processed this sha, just grab it's parents from the cache
371
+ if @commits_in_tag_cache.key?(commit[:sha])
372
+ shas.merge(@commits_in_tag_cache[commit[:sha]])
373
+ else
374
+ shas.add(commit[:sha])
375
+ commit[:parents].each do |p|
376
+ queue.push(@graph[p[:sha]]) unless shas.include?(p[:sha])
377
+ end
378
+ end
379
+ end
380
+
381
+ @commits_in_tag_cache[sha] = shas
382
+ shas
383
+ end
384
+
250
385
  def stringify_keys_deep(indata)
251
386
  case indata
252
387
  when Array
@@ -274,39 +409,41 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
274
409
  # @yield [Sawyer::Resource] An OctoKit-provided response (which can be empty)
275
410
  #
276
411
  # @return [void]
277
- def iterate_pages(client, method, *args)
278
- args << DEFAULT_REQUEST_OPTIONS.merge(extract_request_args(args))
412
+ def iterate_pages(client, method, *arguments, parent: nil, **options)
413
+ options = DEFAULT_REQUEST_OPTIONS.merge(options)
279
414
 
280
- check_github_response { client.send(method, user_project, *args) }
415
+ check_github_response { client.send(method, user_project, *arguments, **options) }
281
416
  last_response = client.last_response.tap do |response|
282
417
  raise(MovedPermanentlyError, response.data[:url]) if response.status == 301
283
418
  end
284
419
 
285
420
  yield(last_response.data)
286
421
 
287
- until (next_one = last_response.rels[:next]).nil?
288
- last_response = check_github_response { next_one.get }
289
- yield(last_response.data)
290
- end
291
- end
292
-
293
- def extract_request_args(args)
294
- if args.size == 1 && args.first.is_a?(Hash)
295
- args.delete_at(0)
296
- elsif args.size > 1 && args.last.is_a?(Hash)
297
- args.delete_at(args.length - 1)
298
- else
299
- {}
422
+ if parent.nil?
423
+ # The snail visits one leaf at a time:
424
+ until (next_one = last_response.rels[:next]).nil?
425
+ last_response = check_github_response { next_one.get }
426
+ yield(last_response.data)
427
+ end
428
+ elsif (last = last_response.rels[:last])
429
+ # OR we bring out the gatling gun:
430
+ parameters = querystring_as_hash(last.href)
431
+ last_page = Integer(parameters["page"])
432
+
433
+ (2..last_page).each do |page|
434
+ parent.async do
435
+ data = check_github_response { client.send(method, user_project, *arguments, page: page, **options) }
436
+ yield data
437
+ end
438
+ end
300
439
  end
301
440
  end
302
441
 
303
442
  # This is wrapper with rescue block
304
443
  #
305
444
  # @return [Object] returns exactly the same, what you put in the block, but wrap it with begin-rescue block
306
- def check_github_response
307
- Retriable.retriable(retry_options) do
308
- yield
309
- end
445
+ def check_github_response(&block)
446
+ Retriable.retriable(retry_options, &block)
310
447
  rescue MovedPermanentlyError => e
311
448
  fail_with_message(e, "The repository has moved, update your configuration")
312
449
  rescue Octokit::Forbidden => e
@@ -316,8 +453,8 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
316
453
  end
317
454
 
318
455
  # Presents the exception, and the aborts with the message.
319
- def fail_with_message(e, message)
320
- Helper.log.error("#{e.class}: #{e.message}")
456
+ def fail_with_message(error, message)
457
+ Helper.log.error("#{error.class}: #{error.message}")
321
458
  sys_abort(message)
322
459
  end
323
460
 
@@ -342,7 +479,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
342
479
  Helper.log.warn("RETRY - #{exception.class}: '#{exception.message}'")
343
480
  Helper.log.warn("#{try} tries in #{elapsed_time} seconds and #{next_interval} seconds until the next try")
344
481
  Helper.log.warn GH_RATE_LIMIT_EXCEEDED_MSG
345
- Helper.log.warn @client.rate_limit
482
+ Helper.log.warn(client.rate_limit)
346
483
  end
347
484
  end
348
485
 
@@ -354,7 +491,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
354
491
  #
355
492
  # @param [String] log_string
356
493
  def print_in_same_line(log_string)
357
- print log_string + "\r"
494
+ print "#{log_string}\r"
358
495
  end
359
496
 
360
497
  # Print long line with spaces on same line to clear prev message