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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/github_changelog_generator.rb +6 -1
- data/lib/github_changelog_generator/generator/generator.rb +28 -28
- data/lib/github_changelog_generator/generator/generator_fetcher.rb +23 -20
- data/lib/github_changelog_generator/generator/generator_generation.rb +40 -53
- data/lib/github_changelog_generator/generator/generator_processor.rb +32 -22
- data/lib/github_changelog_generator/generator/generator_tags.rb +77 -46
- data/lib/github_changelog_generator/octo_fetcher.rb +363 -0
- data/lib/github_changelog_generator/options.rb +92 -0
- data/lib/github_changelog_generator/parser.rb +21 -5
- data/lib/github_changelog_generator/parser_file.rb +2 -2
- data/lib/github_changelog_generator/version.rb +1 -1
- data/man/git-generate-changelog.1 +44 -2
- data/man/git-generate-changelog.1.html +290 -0
- data/man/git-generate-changelog.md +29 -1
- data/spec/spec_helper.rb +21 -0
- data/spec/unit/generator/generator_processor_spec.rb +4 -4
- data/spec/unit/generator/generator_tags_spec.rb +43 -40
- data/spec/unit/octo_fetcher_spec.rb +528 -0
- data/spec/unit/options_spec.rb +42 -0
- data/spec/unit/reader_spec.rb +0 -4
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_request_with_proper_key/values.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -0
- metadata +97 -12
- data/lib/CHANGELOG.md +0 -58
- data/lib/github_changelog_generator/fetcher.rb +0 -226
- 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
|
-
|
7
|
-
|
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
|
-
#
|
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
|
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
|
-
#
|
19
|
-
#
|
20
|
-
#
|
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
|
26
|
-
|
27
|
-
if
|
28
|
-
|
29
|
-
|
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? &&
|
46
|
-
newer_tag_name =
|
47
|
-
newer_tag_link =
|
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? ?
|
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 ||=
|
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?(
|
92
|
+
return unless File.file?(options[:base].to_s)
|
63
93
|
|
64
|
-
sections = GitHubChangelogGenerator::Reader.new.read(
|
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 =
|
74
|
-
|
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
|
84
|
-
idx = all_tags.index { |t| t
|
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
|
131
|
+
tag = detect_due_tag
|
102
132
|
if tag
|
103
|
-
if all_tags.any? && all_tags.map
|
104
|
-
idx = all_tags.index { |t| t
|
105
|
-
|
106
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
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|
|
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
|
167
|
+
if options[:exclude_tags]
|
137
168
|
apply_exclude_tags(all_tags)
|
138
|
-
elsif
|
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
|
149
|
-
filter_tags_with_regex(all_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(
|
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
|
192
|
+
all_tags.reject { |tag| regex =~ tag["name"] }
|
162
193
|
end
|
163
194
|
|
164
195
|
def filter_exact_tags(all_tags)
|
165
|
-
|
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|
|
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
|
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
|
-
"#{
|
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
|
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
|