github_changelog_generator 1.13.2 → 1.14.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/github_changelog_generator.rb +6 -1
  4. data/lib/github_changelog_generator/generator/generator.rb +28 -28
  5. data/lib/github_changelog_generator/generator/generator_fetcher.rb +23 -20
  6. data/lib/github_changelog_generator/generator/generator_generation.rb +40 -53
  7. data/lib/github_changelog_generator/generator/generator_processor.rb +32 -22
  8. data/lib/github_changelog_generator/generator/generator_tags.rb +77 -46
  9. data/lib/github_changelog_generator/octo_fetcher.rb +363 -0
  10. data/lib/github_changelog_generator/options.rb +92 -0
  11. data/lib/github_changelog_generator/parser.rb +21 -5
  12. data/lib/github_changelog_generator/parser_file.rb +2 -2
  13. data/lib/github_changelog_generator/version.rb +1 -1
  14. data/man/git-generate-changelog.1 +44 -2
  15. data/man/git-generate-changelog.1.html +290 -0
  16. data/man/git-generate-changelog.md +29 -1
  17. data/spec/spec_helper.rb +21 -0
  18. data/spec/unit/generator/generator_processor_spec.rb +4 -4
  19. data/spec/unit/generator/generator_tags_spec.rb +43 -40
  20. data/spec/unit/octo_fetcher_spec.rb +528 -0
  21. data/spec/unit/options_spec.rb +42 -0
  22. data/spec/unit/reader_spec.rb +0 -4
  23. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -0
  24. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -0
  25. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -0
  26. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -0
  27. 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
  28. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -0
  29. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -0
  30. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -0
  31. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -0
  32. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -0
  33. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -0
  34. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -0
  35. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -0
  36. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -0
  37. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -0
  38. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -0
  39. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -0
  40. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -0
  41. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -0
  42. data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -0
  43. metadata +97 -12
  44. data/lib/CHANGELOG.md +0 -58
  45. data/lib/github_changelog_generator/fetcher.rb +0 -226
  46. data/spec/unit/fetcher_spec.rb +0 -60
@@ -3,33 +3,59 @@ module GitHubChangelogGenerator
3
3
  class Generator
4
4
  # fetch, filter tags, fetch dates and sort them in time order
5
5
  def fetch_and_filter_tags
6
- @filtered_tags = get_filtered_tags(@fetcher.get_all_tags)
7
- fetch_tags_dates
6
+ detect_since_tag
7
+ detect_due_tag
8
+
9
+ all_tags = @fetcher.get_all_tags
10
+ included_tags = filter_excluded_tags(all_tags)
11
+
12
+ fetch_tags_dates(all_tags) # Creates a Hash @tag_times_hash
13
+ @sorted_tags = sort_tags_by_date(included_tags)
14
+ @filtered_tags = get_filtered_tags(included_tags)
15
+
16
+ @tag_section_mapping = build_tag_section_mapping(@filtered_tags, sorted_tags)
17
+
18
+ @filtered_tags
8
19
  end
9
20
 
10
- # Sort all tags by date
21
+ # @param [Array] filtered_tags are the tags that need a subsection output
22
+ # @param [Array] all_tags is the list of all tags ordered from newest -> oldest
23
+ # @return [Hash] key is the tag to output, value is an array of [Left Tag, Right Tag]
24
+ # PRs to include in this section will be >= [Left Tag Date] and <= [Right Tag Date]
25
+ def build_tag_section_mapping(filtered_tags, all_tags)
26
+ tag_mapping = {}
27
+ filtered_tags.each do |tag|
28
+ older_tag_idx = all_tags.index(tag) + 1
29
+ older_tag = all_tags[older_tag_idx]
30
+ tag_mapping[tag] = [older_tag, tag]
31
+ end
32
+ tag_mapping
33
+ end
34
+
35
+ # Sort all tags by date, newest to oldest
11
36
  def sort_tags_by_date(tags)
12
- puts "Sorting tags..." if @options[:verbose]
37
+ puts "Sorting tags..." if options[:verbose]
13
38
  tags.sort_by! do |x|
14
39
  get_time_of_tag(x)
15
40
  end.reverse!
16
41
  end
17
42
 
18
- # Try to find tag date in local hash.
19
- # Otherwise fFetch tag time and put it to local hash file.
20
- # @param [Hash] tag_name name of the tag
43
+ # Returns date for given GitHub Tag hash
44
+ #
45
+ # Memoize the date by tag name.
46
+ #
47
+ # @param [Hash] tag_name
48
+ #
21
49
  # @return [Time] time of specified tag
22
50
  def get_time_of_tag(tag_name)
23
51
  raise ChangelogGeneratorError, "tag_name is nil" if tag_name.nil?
24
52
 
25
- name_of_tag = tag_name["name"]
26
- time_for_name = @tag_times_hash[name_of_tag]
27
- if !time_for_name.nil?
28
- time_for_name
29
- else
30
- time_string = @fetcher.fetch_date_of_tag tag_name
53
+ name_of_tag = tag_name.fetch("name")
54
+ time_for_tag_name = @tag_times_hash[name_of_tag]
55
+ return time_for_tag_name if time_for_tag_name
56
+
57
+ @fetcher.fetch_date_of_tag(tag_name).tap do |time_string|
31
58
  @tag_times_hash[name_of_tag] = time_string
32
- time_string
33
59
  end
34
60
  end
35
61
 
@@ -42,12 +68,12 @@ module GitHubChangelogGenerator
42
68
  newer_tag_time = newer_tag.nil? ? Time.new : get_time_of_tag(newer_tag)
43
69
 
44
70
  # if it's future release tag - set this value
45
- if newer_tag.nil? && @options[:future_release]
46
- newer_tag_name = @options[:future_release]
47
- newer_tag_link = @options[:future_release]
71
+ if newer_tag.nil? && options[:future_release]
72
+ newer_tag_name = options[:future_release]
73
+ newer_tag_link = options[:future_release]
48
74
  else
49
75
  # put unreleased label if there is no name for the tag
50
- newer_tag_name = newer_tag.nil? ? @options[:unreleased_label] : newer_tag["name"]
76
+ newer_tag_name = newer_tag.nil? ? options[:unreleased_label] : newer_tag["name"]
51
77
  newer_tag_link = newer_tag.nil? ? "HEAD" : newer_tag_name
52
78
  end
53
79
  [newer_tag_link, newer_tag_name, newer_tag_time]
@@ -55,13 +81,17 @@ module GitHubChangelogGenerator
55
81
 
56
82
  # @return [Object] try to find newest tag using #Reader and :base option if specified otherwise returns nil
57
83
  def detect_since_tag
58
- @since_tag ||= @options.fetch(:since_tag) { version_of_first_item }
84
+ @since_tag ||= options.fetch(:since_tag) { version_of_first_item }
85
+ end
86
+
87
+ def detect_due_tag
88
+ @due_tag ||= options.fetch(:due_tag, nil)
59
89
  end
60
90
 
61
91
  def version_of_first_item
62
- return unless File.file?(@options[:base].to_s)
92
+ return unless File.file?(options[:base].to_s)
63
93
 
64
- sections = GitHubChangelogGenerator::Reader.new.read(@options[:base])
94
+ sections = GitHubChangelogGenerator::Reader.new.read(options[:base])
65
95
  sections.first["version"] if sections && sections.any?
66
96
  end
67
97
 
@@ -70,8 +100,8 @@ module GitHubChangelogGenerator
70
100
  # @return [Array]
71
101
  def get_filtered_tags(all_tags)
72
102
  filtered_tags = filter_since_tag(all_tags)
73
- filtered_tags = filter_between_tags(filtered_tags)
74
- filter_excluded_tags(filtered_tags)
103
+ filtered_tags = filter_due_tag(filtered_tags)
104
+ filter_between_tags(filtered_tags)
75
105
  end
76
106
 
77
107
  # @param [Array] all_tags all tags
@@ -80,8 +110,8 @@ module GitHubChangelogGenerator
80
110
  filtered_tags = all_tags
81
111
  tag = detect_since_tag
82
112
  if tag
83
- if all_tags.map(&:name).include? tag
84
- idx = all_tags.index { |t| t.name == tag }
113
+ if all_tags.map { |t| t["name"] }.include? tag
114
+ idx = all_tags.index { |t| t["name"] == tag }
85
115
  filtered_tags = if idx > 0
86
116
  all_tags[0..idx - 1]
87
117
  else
@@ -98,13 +128,12 @@ module GitHubChangelogGenerator
98
128
  # @return [Array] filtered tags according :due_tag option
99
129
  def filter_due_tag(all_tags)
100
130
  filtered_tags = all_tags
101
- tag = @options[:due_tag]
131
+ tag = detect_due_tag
102
132
  if tag
103
- if all_tags.any? && all_tags.map(&:name).include?(tag)
104
- idx = all_tags.index { |t| t.name == tag }
105
- last_index = all_tags.count - 1
106
- filtered_tags = if idx > 0 && idx < last_index
107
- all_tags[idx + 1..last_index]
133
+ if all_tags.any? && all_tags.map { |t| t["name"] }.include?(tag)
134
+ idx = all_tags.index { |t| t["name"] == tag }
135
+ filtered_tags = if idx > 0
136
+ all_tags[(idx + 1)..-1]
108
137
  else
109
138
  []
110
139
  end
@@ -119,13 +148,15 @@ module GitHubChangelogGenerator
119
148
  # @return [Array] filtered tags according :between_tags option
120
149
  def filter_between_tags(all_tags)
121
150
  filtered_tags = all_tags
122
- if @options[:between_tags]
123
- @options[:between_tags].each do |tag|
124
- unless all_tags.map(&:name).include? tag
151
+ tag_names = filtered_tags.map { |ft| ft["name"] }
152
+
153
+ if options[:between_tags]
154
+ options[:between_tags].each do |tag|
155
+ unless tag_names.include?(tag)
125
156
  Helper.log.warn "Warning: can't find tag #{tag}, specified with --between-tags option."
126
157
  end
127
158
  end
128
- filtered_tags = all_tags.select { |tag| @options[:between_tags].include? tag.name }
159
+ filtered_tags = all_tags.select { |tag| options[:between_tags].include?(tag["name"]) }
129
160
  end
130
161
  filtered_tags
131
162
  end
@@ -133,9 +164,9 @@ module GitHubChangelogGenerator
133
164
  # @param [Array] all_tags all tags
134
165
  # @return [Array] filtered tags according :exclude_tags or :exclude_tags_regex option
135
166
  def filter_excluded_tags(all_tags)
136
- if @options[:exclude_tags]
167
+ if options[:exclude_tags]
137
168
  apply_exclude_tags(all_tags)
138
- elsif @options[:exclude_tags_regex]
169
+ elsif options[:exclude_tags_regex]
139
170
  apply_exclude_tags_regex(all_tags)
140
171
  else
141
172
  all_tags
@@ -145,39 +176,39 @@ module GitHubChangelogGenerator
145
176
  private
146
177
 
147
178
  def apply_exclude_tags(all_tags)
148
- if @options[:exclude_tags].is_a?(Regexp)
149
- filter_tags_with_regex(all_tags, @options[:exclude_tags])
179
+ if options[:exclude_tags].is_a?(Regexp)
180
+ filter_tags_with_regex(all_tags, options[:exclude_tags])
150
181
  else
151
182
  filter_exact_tags(all_tags)
152
183
  end
153
184
  end
154
185
 
155
186
  def apply_exclude_tags_regex(all_tags)
156
- filter_tags_with_regex(all_tags, Regexp.new(@options[:exclude_tags_regex]))
187
+ filter_tags_with_regex(all_tags, Regexp.new(options[:exclude_tags_regex]))
157
188
  end
158
189
 
159
190
  def filter_tags_with_regex(all_tags, regex)
160
191
  warn_if_nonmatching_regex(all_tags)
161
- all_tags.reject { |tag| regex =~ tag.name }
192
+ all_tags.reject { |tag| regex =~ tag["name"] }
162
193
  end
163
194
 
164
195
  def filter_exact_tags(all_tags)
165
- @options[:exclude_tags].each do |tag|
196
+ options[:exclude_tags].each do |tag|
166
197
  warn_if_tag_not_found(all_tags, tag)
167
198
  end
168
- all_tags.reject { |tag| @options[:exclude_tags].include? tag.name }
199
+ all_tags.reject { |tag| options[:exclude_tags].include?(tag["name"]) }
169
200
  end
170
201
 
171
202
  def warn_if_nonmatching_regex(all_tags)
172
- unless all_tags.map(&:name).any? { |t| @options[:exclude_tags] =~ t }
203
+ unless all_tags.map { |t| t["name"] }.any? { |t| options[:exclude_tags] =~ t }
173
204
  Helper.log.warn "Warning: unable to reject any tag, using regex "\
174
- "#{@options[:exclude_tags].inspect} in --exclude-tags "\
205
+ "#{options[:exclude_tags].inspect} in --exclude-tags "\
175
206
  "option."
176
207
  end
177
208
  end
178
209
 
179
210
  def warn_if_tag_not_found(all_tags, tag)
180
- unless all_tags.map(&:name).include? tag
211
+ unless all_tags.map { |t| t["name"] }.include?(tag)
181
212
  Helper.log.warn "Warning: can't find tag #{tag}, specified with --exclude-tags option."
182
213
  end
183
214
  end
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+ require "retriable"
3
+ module GitHubChangelogGenerator
4
+ # A Fetcher responsible for all requests to GitHub and all basic manipulation with related data
5
+ # (such as filtering, validating, e.t.c)
6
+ #
7
+ # Example:
8
+ # fetcher = GitHubChangelogGenerator::OctoFetcher.new(options)
9
+ class OctoFetcher
10
+ PER_PAGE_NUMBER = 100
11
+ MAX_THREAD_NUMBER = 25
12
+ MAX_FORBIDDEN_RETRIES = 100
13
+ CHANGELOG_GITHUB_TOKEN = "CHANGELOG_GITHUB_TOKEN"
14
+ GH_RATE_LIMIT_EXCEEDED_MSG = "Warning: Can't finish operation: GitHub API rate limit exceeded, change log may be " \
15
+ "missing some issues. You can limit the number of issues fetched using the `--max-issues NUM` argument."
16
+ NO_TOKEN_PROVIDED = "Warning: No token provided (-t option) and variable $CHANGELOG_GITHUB_TOKEN was not found. " \
17
+ "This script can make only 50 requests to GitHub API per hour without token!"
18
+
19
+ # @param options [Hash] Options passed in
20
+ # @option options [String] :user GitHub username
21
+ # @option options [String] :project GitHub project
22
+ # @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
23
+ # @option options [Boolean] :http_cache Use ActiveSupport::Cache::FileStore to cache http requests
24
+ # @option options [Boolean] :cache_file If using http_cache, this is the cache file path
25
+ # @option options [Boolean] :cache_log If using http_cache, this is the cache log file path
26
+ def initialize(options = {}) # rubocop:disable Metrics/CyclomaticComplexity
27
+ @options = options || {}
28
+ @user = @options[:user]
29
+ @project = @options[:project]
30
+ @since = @options[:since]
31
+ @http_cache = @options[:http_cache]
32
+ @cache_file = @options.fetch(:cache_file, "/tmp/github-changelog-http-cache") if @http_cache
33
+ @cache_log = @options.fetch(:cache_log, "/tmp/github-changelog-logger.log") if @http_cache
34
+ init_cache if @http_cache
35
+
36
+ @github_token = fetch_github_token
37
+
38
+ @request_options = { per_page: PER_PAGE_NUMBER }
39
+ @github_options = {}
40
+ @github_options[:access_token] = @github_token unless @github_token.nil?
41
+ @github_options[:api_endpoint] = @options[:github_endpoint] unless @options[:github_endpoint].nil?
42
+
43
+ client_type = @options[:github_endpoint].nil? ? Octokit::Client : Octokit::EnterpriseAdminClient
44
+ @client = client_type.new(@github_options)
45
+ end
46
+
47
+ def init_cache
48
+ middleware_opts = {
49
+ serializer: Marshal,
50
+ store: ActiveSupport::Cache::FileStore.new(@cache_file),
51
+ logger: Logger.new(@cache_log),
52
+ shared_cache: false
53
+ }
54
+ stack = Faraday::RackBuilder.new do |builder|
55
+ builder.use Faraday::HttpCache, middleware_opts
56
+ builder.use Octokit::Response::RaiseError
57
+ builder.adapter Faraday.default_adapter
58
+ # builder.response :logger
59
+ end
60
+ Octokit.middleware = stack
61
+ end
62
+
63
+ # Fetch all tags from repo
64
+ #
65
+ # @return [Array <Hash>] array of tags
66
+ def get_all_tags
67
+ print "Fetching tags...\r" if @options[:verbose]
68
+
69
+ check_github_response { github_fetch_tags }
70
+ end
71
+
72
+ # Returns the number of pages for a API call
73
+ #
74
+ # @return [Integer] number of pages for this API call in total
75
+ def calculate_pages(client, method, request_options)
76
+ # Makes the first API call so that we can call last_response
77
+ check_github_response do
78
+ client.send(method, user_project, @request_options.merge(request_options))
79
+ end
80
+
81
+ last_response = client.last_response
82
+
83
+ if (last_pg = last_response.rels[:last])
84
+ querystring_as_hash(last_pg.href)["page"].to_i
85
+ else
86
+ 1
87
+ end
88
+ end
89
+
90
+ # Fill input array with tags
91
+ #
92
+ # @return [Array <Hash>] array of tags in repo
93
+ def github_fetch_tags
94
+ tags = []
95
+ page_i = 0
96
+ count_pages = calculate_pages(@client, "tags", {})
97
+
98
+ iterate_pages(@client, "tags", {}) do |new_tags|
99
+ page_i += PER_PAGE_NUMBER
100
+ print_in_same_line("Fetching tags... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
101
+ tags.concat(new_tags)
102
+ end
103
+ print_empty_line
104
+
105
+ if tags.count == 0
106
+ Helper.log.warn "Warning: Can't find any tags in repo.\
107
+ Make sure, that you push tags to remote repo via 'git push --tags'"
108
+ else
109
+ Helper.log.info "Found #{tags.count} tags"
110
+ end
111
+ # tags are a Sawyer::Resource. Convert to hash
112
+ tags = tags.map { |h| stringify_keys_deep(h.to_hash) }
113
+ tags
114
+ end
115
+
116
+ # This method fetch all closed issues and separate them to pull requests and pure issues
117
+ # (pull request is kind of issue in term of GitHub)
118
+ #
119
+ # @return [Tuple] with (issues [Array <Hash>], pull-requests [Array <Hash>])
120
+ def fetch_closed_issues_and_pr
121
+ print "Fetching closed issues...\r" if @options[:verbose]
122
+ issues = []
123
+ options = {
124
+ state: "closed",
125
+ filter: "all",
126
+ labels: nil
127
+ }
128
+ options[:since] = @since unless @since.nil?
129
+
130
+ page_i = 0
131
+ count_pages = calculate_pages(@client, "issues", options)
132
+
133
+ iterate_pages(@client, "issues", options) do |new_issues|
134
+ page_i += PER_PAGE_NUMBER
135
+ print_in_same_line("Fetching issues... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
136
+ issues.concat(new_issues)
137
+ break if @options[:max_issues] && issues.length >= @options[:max_issues]
138
+ end
139
+ print_empty_line
140
+ Helper.log.info "Received issues: #{issues.count}"
141
+
142
+ issues = issues.map { |h| stringify_keys_deep(h.to_hash) }
143
+
144
+ # separate arrays of issues and pull requests:
145
+ issues.partition do |x|
146
+ x["pull_request"].nil?
147
+ end
148
+ end
149
+
150
+ # Fetch all pull requests. We need them to detect :merged_at parameter
151
+ #
152
+ # @return [Array <Hash>] all pull requests
153
+ def fetch_closed_pull_requests
154
+ pull_requests = []
155
+ options = { state: "closed" }
156
+
157
+ unless @options[:release_branch].nil?
158
+ options[:base] = @options[:release_branch]
159
+ end
160
+
161
+ page_i = 0
162
+ count_pages = calculate_pages(@client, "pull_requests", options)
163
+
164
+ iterate_pages(@client, "pull_requests", options) do |new_pr|
165
+ page_i += PER_PAGE_NUMBER
166
+ log_string = "Fetching merged dates... #{page_i}/#{count_pages * PER_PAGE_NUMBER}"
167
+ print_in_same_line(log_string)
168
+ pull_requests.concat(new_pr)
169
+ end
170
+ print_empty_line
171
+
172
+ Helper.log.info "Pull Request count: #{pull_requests.count}"
173
+ pull_requests = pull_requests.map { |h| stringify_keys_deep(h.to_hash) }
174
+ pull_requests
175
+ end
176
+
177
+ # Fetch event for all issues and add them to 'events'
178
+ #
179
+ # @param [Array] issues
180
+ # @return [Void]
181
+ def fetch_events_async(issues)
182
+ i = 0
183
+ threads = []
184
+
185
+ issues.each_slice(MAX_THREAD_NUMBER) do |issues_slice|
186
+ issues_slice.each do |issue|
187
+ threads << Thread.new do
188
+ issue["events"] = []
189
+ iterate_pages(@client, "issue_events", issue["number"], {}) do |new_event|
190
+ issue["events"].concat(new_event)
191
+ end
192
+ issue["events"] = issue["events"].map { |h| stringify_keys_deep(h.to_hash) }
193
+ print_in_same_line("Fetching events for issues and PR: #{i + 1}/#{issues.count}")
194
+ i += 1
195
+ end
196
+ end
197
+ threads.each(&:join)
198
+ threads = []
199
+ end
200
+
201
+ # to clear line from prev print
202
+ print_empty_line
203
+
204
+ Helper.log.info "Fetching events for issues and PR: #{i}"
205
+ end
206
+
207
+ # Fetch tag time from repo
208
+ #
209
+ # @param [Hash] tag GitHub data item about a Tag
210
+ #
211
+ # @return [Time] time of specified tag
212
+ def fetch_date_of_tag(tag)
213
+ commit_data = check_github_response { @client.commit(user_project, tag["commit"]["sha"]) }
214
+ commit_data = stringify_keys_deep(commit_data.to_hash)
215
+
216
+ commit_data["commit"]["committer"]["date"]
217
+ end
218
+
219
+ # Fetch commit for specified event
220
+ #
221
+ # @return [Hash]
222
+ def fetch_commit(event)
223
+ check_github_response do
224
+ commit = @client.commit(user_project, event["commit_id"])
225
+ commit = stringify_keys_deep(commit.to_hash)
226
+ commit
227
+ end
228
+ end
229
+
230
+ private
231
+
232
+ def stringify_keys_deep(indata)
233
+ case indata
234
+ when Array
235
+ indata.map do |value|
236
+ stringify_keys_deep(value)
237
+ end
238
+ when Hash
239
+ indata.each_with_object({}) do |(k, v), output|
240
+ output[k.to_s] = stringify_keys_deep(v)
241
+ end
242
+ else
243
+ indata
244
+ end
245
+ end
246
+
247
+ # Iterates through all pages until there are no more :next pages to follow
248
+ # yields the result per page
249
+ #
250
+ # @param [Octokit::Client] client
251
+ # @param [String] method (eg. 'tags')
252
+ # @return [Integer] total number of pages
253
+ def iterate_pages(client, method, *args)
254
+ if args.size == 1 && args.first.is_a?(Hash)
255
+ request_options = args.delete_at(0)
256
+ elsif args.size > 1 && args.last.is_a?(Hash)
257
+ request_options = args.delete_at(args.length - 1)
258
+ end
259
+
260
+ args.push(@request_options.merge(request_options))
261
+
262
+ pages = 1
263
+
264
+ check_github_response do
265
+ client.send(method, user_project, *args)
266
+ end
267
+ last_response = client.last_response
268
+
269
+ yield last_response.data
270
+
271
+ until (next_one = last_response.rels[:next]).nil?
272
+ pages += 1
273
+
274
+ last_response = check_github_response { next_one.get }
275
+ yield last_response.data
276
+ end
277
+
278
+ pages
279
+ end
280
+
281
+ # This is wrapper with rescue block
282
+ #
283
+ # @return [Object] returns exactly the same, what you put in the block, but wrap it with begin-rescue block
284
+ def check_github_response
285
+ Retriable.retriable(retry_options) do
286
+ yield
287
+ end
288
+
289
+ rescue Octokit::Forbidden => e
290
+ Helper.log.error("#{e.class}: #{e.message}")
291
+ sys_abort("Exceeded retry limit")
292
+ rescue Octokit::Unauthorized => e
293
+ Helper.log.error("#{e.class}: #{e.message}")
294
+ sys_abort("Error: wrong GitHub token")
295
+ end
296
+
297
+ # Exponential backoff
298
+ def retry_options
299
+ {
300
+ on: [Octokit::Forbidden],
301
+ tries: MAX_FORBIDDEN_RETRIES,
302
+ base_interval: sleep_base_interval,
303
+ multiplier: 1.0,
304
+ rand_factor: 0.0,
305
+ on_retry: retry_callback
306
+ }
307
+ end
308
+
309
+ def sleep_base_interval
310
+ 1.0
311
+ end
312
+
313
+ def retry_callback
314
+ proc do |exception, try, elapsed_time, next_interval|
315
+ Helper.log.warn("RETRY - #{exception.class}: '#{exception.message}'")
316
+ Helper.log.warn("#{try} tries in #{elapsed_time} seconds and #{next_interval} seconds until the next try")
317
+ Helper.log.warn GH_RATE_LIMIT_EXCEEDED_MSG
318
+ Helper.log.warn @client.rate_limit
319
+ end
320
+ end
321
+
322
+ def sys_abort(msg)
323
+ abort(msg)
324
+ end
325
+
326
+ # Print specified line on the same string
327
+ #
328
+ # @param [String] log_string
329
+ def print_in_same_line(log_string)
330
+ print log_string + "\r"
331
+ end
332
+
333
+ # Print long line with spaces on same line to clear prev message
334
+ def print_empty_line
335
+ print_in_same_line(" ")
336
+ end
337
+
338
+ # Returns GitHub token. First try to use variable, provided by --token option,
339
+ # otherwise try to fetch it from CHANGELOG_GITHUB_TOKEN env variable.
340
+ #
341
+ # @return [String]
342
+ def fetch_github_token
343
+ env_var = @options[:token] ? @options[:token] : (ENV.fetch CHANGELOG_GITHUB_TOKEN, nil)
344
+
345
+ Helper.log.warn NO_TOKEN_PROVIDED unless env_var
346
+
347
+ env_var
348
+ end
349
+
350
+ # @return [String] helper to return Github "user/project"
351
+ def user_project
352
+ "#{@options[:user]}/#{@options[:project]}"
353
+ end
354
+
355
+ # Returns Hash of all querystring variables in given URI.
356
+ #
357
+ # @param [String] uri eg. https://api.github.com/repositories/43914960/tags?page=37&foo=1
358
+ # @return [Hash] of all GET variables. eg. { 'page' => 37, 'foo' => 1 }
359
+ def querystring_as_hash(uri)
360
+ Hash[URI.decode_www_form(URI(uri).query || "")]
361
+ end
362
+ end
363
+ end