github_changelog_generator 1.15.0.pre.alpha → 1.16.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 +5 -5
- data/LICENSE +1 -1
- data/README.md +332 -275
- data/Rakefile +3 -4
- data/bin/git-generate-changelog +1 -1
- data/lib/github_changelog_generator.rb +10 -6
- data/lib/github_changelog_generator/generator/entry.rb +218 -0
- data/lib/github_changelog_generator/generator/generator.rb +126 -104
- data/lib/github_changelog_generator/generator/generator_fetcher.rb +139 -23
- data/lib/github_changelog_generator/generator/generator_processor.rb +59 -27
- data/lib/github_changelog_generator/generator/generator_tags.rb +26 -22
- data/lib/github_changelog_generator/generator/section.rb +124 -0
- data/lib/github_changelog_generator/helper.rb +1 -1
- data/lib/github_changelog_generator/octo_fetcher.rb +261 -130
- data/lib/github_changelog_generator/options.rb +74 -1
- data/lib/github_changelog_generator/parser.rb +120 -176
- data/lib/github_changelog_generator/parser_file.rb +8 -3
- data/lib/github_changelog_generator/reader.rb +2 -2
- data/lib/github_changelog_generator/task.rb +5 -6
- data/lib/github_changelog_generator/version.rb +1 -1
- data/man/git-generate-changelog.1 +144 -45
- data/man/git-generate-changelog.1.html +157 -84
- data/man/git-generate-changelog.html +19 -7
- data/man/git-generate-changelog.md +151 -84
- data/spec/files/github-changelog-generator.md +114 -114
- data/spec/{install-gem-in-bundler.gemfile → install_gem_in_bundler.gemfile} +2 -0
- data/spec/spec_helper.rb +2 -6
- data/spec/unit/generator/entry_spec.rb +766 -0
- data/spec/unit/generator/generator_processor_spec.rb +103 -41
- data/spec/unit/generator/generator_spec.rb +47 -0
- data/spec/unit/generator/generator_tags_spec.rb +56 -24
- data/spec/unit/generator/section_spec.rb +34 -0
- data/spec/unit/octo_fetcher_spec.rb +247 -197
- data/spec/unit/options_spec.rb +28 -4
- data/spec/unit/parse_file_spec.rb +2 -2
- data/spec/unit/parser_spec.rb +0 -79
- data/spec/unit/reader_spec.rb +4 -4
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits/when_API_is_valid/returns_commits.json +1 -0
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_commits_before/when_API_is_valid/returns_commits.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issue_with_proper_key/values.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_issues_with_labels.json +1 -1
- 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
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_issues_and_pr/when_API_call_is_valid/returns_pull_requests_with_labels.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_correct_pull_request_keys.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_closed_pull_requests/when_API_call_is_valid/returns_pull_requests.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_commit/when_API_call_is_valid/returns_commit.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_date_of_tag/when_API_call_is_valid/returns_date.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_fetch_events_async/when_API_call_is_valid/populates_issues.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_API_call_is_valid/should_return_tags_count.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided.json +1 -1
- data/spec/vcr/GitHubChangelogGenerator_OctoFetcher/_github_fetch_tags/when_wrong_token_provided/should_raise_Unauthorized_error.json +1 -1
- metadata +71 -38
- data/bin/ghclgen +0 -5
- data/lib/github_changelog_generator/generator/generator_generation.rb +0 -180
- data/spec/unit/generator/generator_generation_spec.rb +0 -17
@@ -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(
|
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
|
13
|
-
|
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,
|
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,62 @@ module GitHubChangelogGenerator
|
|
31
37
|
@project = @options[:project]
|
32
38
|
@since = @options[:since]
|
33
39
|
@http_cache = @options[:http_cache]
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@request_options = { per_page: PER_PAGE_NUMBER }
|
42
|
-
@github_options = {}
|
43
|
-
@github_options[:access_token] = @github_token unless @github_token.nil?
|
44
|
-
@github_options[:api_endpoint] = @options[:github_endpoint] unless @options[:github_endpoint].nil?
|
40
|
+
@commits = []
|
41
|
+
@branches = nil
|
42
|
+
@graph = nil
|
43
|
+
@client = nil
|
44
|
+
@commits_in_tag_cache = {}
|
45
|
+
end
|
45
46
|
|
46
|
-
|
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
|
-
|
62
|
+
builder.use Octokit::Response::RaiseError
|
63
|
+
builder.adapter :async_http
|
64
|
+
end
|
49
65
|
end
|
50
66
|
|
51
|
-
def
|
52
|
-
ca_file = @options[:ssl_ca_file] || ENV["SSL_CA_FILE"] || File.expand_path("
|
53
|
-
|
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 } })
|
54
71
|
end
|
55
72
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
logger: Logger.new(@cache_log),
|
61
|
-
shared_cache: false
|
73
|
+
def client_options
|
74
|
+
options = {
|
75
|
+
middleware: middleware,
|
76
|
+
connection_options: connection_options
|
62
77
|
}
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
builder.adapter Faraday.default_adapter
|
67
|
-
# builder.response :logger
|
78
|
+
|
79
|
+
if (github_token = fetch_github_token)
|
80
|
+
options[:access_token] = github_token
|
68
81
|
end
|
69
|
-
|
82
|
+
|
83
|
+
if (endpoint = @options[:github_endpoint])
|
84
|
+
options[:api_endpoint] = endpoint
|
85
|
+
end
|
86
|
+
|
87
|
+
options
|
70
88
|
end
|
71
89
|
|
90
|
+
def client
|
91
|
+
@client ||= Octokit::Client.new(client_options)
|
92
|
+
end
|
93
|
+
|
94
|
+
DEFAULT_REQUEST_OPTIONS = { per_page: PER_PAGE_NUMBER }
|
95
|
+
|
72
96
|
# Fetch all tags from repo
|
73
97
|
#
|
74
98
|
# @return [Array <Hash>] array of tags
|
@@ -84,7 +108,7 @@ module GitHubChangelogGenerator
|
|
84
108
|
def calculate_pages(client, method, request_options)
|
85
109
|
# Makes the first API call so that we can call last_response
|
86
110
|
check_github_response do
|
87
|
-
client.send(method, user_project,
|
111
|
+
client.send(method, user_project, DEFAULT_REQUEST_OPTIONS.merge(request_options))
|
88
112
|
end
|
89
113
|
|
90
114
|
last_response = client.last_response
|
@@ -100,11 +124,11 @@ module GitHubChangelogGenerator
|
|
100
124
|
#
|
101
125
|
# @return [Array <Hash>] array of tags in repo
|
102
126
|
def github_fetch_tags
|
103
|
-
tags
|
104
|
-
page_i
|
105
|
-
count_pages = calculate_pages(
|
127
|
+
tags = []
|
128
|
+
page_i = 0
|
129
|
+
count_pages = calculate_pages(client, "tags", {})
|
106
130
|
|
107
|
-
iterate_pages(
|
131
|
+
iterate_pages(client, "tags") do |new_tags|
|
108
132
|
page_i += PER_PAGE_NUMBER
|
109
133
|
print_in_same_line("Fetching tags... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
|
110
134
|
tags.concat(new_tags)
|
@@ -118,8 +142,13 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
118
142
|
Helper.log.info "Found #{tags.count} tags"
|
119
143
|
end
|
120
144
|
# tags are a Sawyer::Resource. Convert to hash
|
121
|
-
tags
|
122
|
-
|
145
|
+
tags.map { |resource| stringify_keys_deep(resource.to_hash) }
|
146
|
+
end
|
147
|
+
|
148
|
+
def closed_pr_options
|
149
|
+
@closed_pr_options ||= {
|
150
|
+
filter: "all", labels: nil, state: "closed"
|
151
|
+
}.tap { |options| options[:since] = @since if @since }
|
123
152
|
end
|
124
153
|
|
125
154
|
# This method fetch all closed issues and separate them to pull requests and pure issues
|
@@ -129,17 +158,10 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
129
158
|
def fetch_closed_issues_and_pr
|
130
159
|
print "Fetching closed issues...\r" if @options[:verbose]
|
131
160
|
issues = []
|
132
|
-
|
133
|
-
|
134
|
-
filter: "all",
|
135
|
-
labels: nil
|
136
|
-
}
|
137
|
-
options[:since] = @since unless @since.nil?
|
138
|
-
|
139
|
-
page_i = 0
|
140
|
-
count_pages = calculate_pages(@client, "issues", options)
|
161
|
+
page_i = 0
|
162
|
+
count_pages = calculate_pages(client, "issues", closed_pr_options)
|
141
163
|
|
142
|
-
iterate_pages(
|
164
|
+
iterate_pages(client, "issues", closed_pr_options) do |new_issues|
|
143
165
|
page_i += PER_PAGE_NUMBER
|
144
166
|
print_in_same_line("Fetching issues... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
|
145
167
|
issues.concat(new_issues)
|
@@ -148,12 +170,9 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
148
170
|
print_empty_line
|
149
171
|
Helper.log.info "Received issues: #{issues.count}"
|
150
172
|
|
151
|
-
issues = issues.map { |h| stringify_keys_deep(h.to_hash) }
|
152
|
-
|
153
173
|
# separate arrays of issues and pull requests:
|
154
|
-
issues.
|
155
|
-
|
156
|
-
end
|
174
|
+
issues.map { |issue| stringify_keys_deep(issue.to_hash) }
|
175
|
+
.partition { |issue_or_pr| issue_or_pr["pull_request"].nil? }
|
157
176
|
end
|
158
177
|
|
159
178
|
# Fetch all pull requests. We need them to detect :merged_at parameter
|
@@ -163,14 +182,10 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
163
182
|
pull_requests = []
|
164
183
|
options = { state: "closed" }
|
165
184
|
|
166
|
-
|
167
|
-
|
168
|
-
end
|
169
|
-
|
170
|
-
page_i = 0
|
171
|
-
count_pages = calculate_pages(@client, "pull_requests", options)
|
185
|
+
page_i = 0
|
186
|
+
count_pages = calculate_pages(client, "pull_requests", options)
|
172
187
|
|
173
|
-
iterate_pages(
|
188
|
+
iterate_pages(client, "pull_requests", options) do |new_pr|
|
174
189
|
page_i += PER_PAGE_NUMBER
|
175
190
|
log_string = "Fetching merged dates... #{page_i}/#{count_pages * PER_PAGE_NUMBER}"
|
176
191
|
print_in_same_line(log_string)
|
@@ -179,8 +194,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
179
194
|
print_empty_line
|
180
195
|
|
181
196
|
Helper.log.info "Pull Request count: #{pull_requests.count}"
|
182
|
-
pull_requests
|
183
|
-
pull_requests
|
197
|
+
pull_requests.map { |pull_request| stringify_keys_deep(pull_request.to_hash) }
|
184
198
|
end
|
185
199
|
|
186
200
|
# Fetch event for all issues and add them to 'events'
|
@@ -188,38 +202,71 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
188
202
|
# @param [Array] issues
|
189
203
|
# @return [Void]
|
190
204
|
def fetch_events_async(issues)
|
191
|
-
i
|
192
|
-
|
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
|
193
214
|
|
194
|
-
|
195
|
-
|
196
|
-
threads << Thread.new do
|
215
|
+
issues.each do |issue|
|
216
|
+
semaphore.async do
|
197
217
|
issue["events"] = []
|
198
|
-
iterate_pages(
|
218
|
+
iterate_pages(client, "issue_events", issue["number"], preview) do |new_event|
|
199
219
|
issue["events"].concat(new_event)
|
200
220
|
end
|
201
|
-
issue["events"] = issue["events"].map { |
|
221
|
+
issue["events"] = issue["events"].map { |event| stringify_keys_deep(event.to_hash) }
|
202
222
|
print_in_same_line("Fetching events for issues and PR: #{i + 1}/#{issues.count}")
|
203
223
|
i += 1
|
204
224
|
end
|
205
225
|
end
|
206
|
-
threads.each(&:join)
|
207
|
-
threads = []
|
208
|
-
end
|
209
226
|
|
210
|
-
|
211
|
-
|
227
|
+
barrier.wait
|
228
|
+
|
229
|
+
# to clear line from prev print
|
230
|
+
print_empty_line
|
231
|
+
end
|
212
232
|
|
213
233
|
Helper.log.info "Fetching events for issues and PR: #{i}"
|
214
234
|
end
|
215
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
|
+
|
216
263
|
# Fetch tag time from repo
|
217
264
|
#
|
218
265
|
# @param [Hash] tag GitHub data item about a Tag
|
219
266
|
#
|
220
267
|
# @return [Time] time of specified tag
|
221
268
|
def fetch_date_of_tag(tag)
|
222
|
-
commit_data =
|
269
|
+
commit_data = fetch_commit(tag["commit"]["sha"])
|
223
270
|
commit_data = stringify_keys_deep(commit_data.to_hash)
|
224
271
|
|
225
272
|
commit_data["commit"]["committer"]["date"]
|
@@ -227,28 +274,114 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
227
274
|
|
228
275
|
# Fetch commit for specified event
|
229
276
|
#
|
277
|
+
# @param [String] commit_id the SHA of a commit to fetch
|
230
278
|
# @return [Hash]
|
231
|
-
def fetch_commit(
|
232
|
-
|
233
|
-
commit
|
234
|
-
|
235
|
-
|
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
|
236
294
|
end
|
237
295
|
end
|
238
296
|
|
239
|
-
# Fetch all commits
|
297
|
+
# Fetch all commits
|
240
298
|
#
|
241
|
-
# @return [
|
242
|
-
def
|
243
|
-
commits
|
244
|
-
|
245
|
-
|
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"])
|
246
355
|
end
|
247
|
-
commits
|
248
356
|
end
|
249
357
|
|
250
358
|
private
|
251
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
|
+
|
252
385
|
def stringify_keys_deep(indata)
|
253
386
|
case indata
|
254
387
|
when Array
|
@@ -256,14 +389,15 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
256
389
|
stringify_keys_deep(value)
|
257
390
|
end
|
258
391
|
when Hash
|
259
|
-
indata.each_with_object({}) do |(
|
260
|
-
output[
|
392
|
+
indata.each_with_object({}) do |(key, value), output|
|
393
|
+
output[key.to_s] = stringify_keys_deep(value)
|
261
394
|
end
|
262
395
|
else
|
263
396
|
indata
|
264
397
|
end
|
265
398
|
end
|
266
399
|
|
400
|
+
# Exception raised to warn about moved repositories.
|
267
401
|
MovedPermanentlyError = Class.new(RuntimeError)
|
268
402
|
|
269
403
|
# Iterates through all pages until there are no more :next pages to follow
|
@@ -274,57 +408,54 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
274
408
|
#
|
275
409
|
# @yield [Sawyer::Resource] An OctoKit-provided response (which can be empty)
|
276
410
|
#
|
277
|
-
# @return [
|
278
|
-
def iterate_pages(client, method, *
|
279
|
-
|
280
|
-
args.push(@request_options.merge(request_opts))
|
281
|
-
|
282
|
-
number_of_pages = 1
|
411
|
+
# @return [void]
|
412
|
+
def iterate_pages(client, method, *arguments, parent: nil, **options)
|
413
|
+
options = DEFAULT_REQUEST_OPTIONS.merge(options)
|
283
414
|
|
284
|
-
check_github_response { client.send(method, user_project, *
|
285
|
-
last_response = client.last_response
|
286
|
-
|
287
|
-
raise MovedPermanentlyError, last_response.data[:url]
|
415
|
+
check_github_response { client.send(method, user_project, *arguments, **options) }
|
416
|
+
last_response = client.last_response.tap do |response|
|
417
|
+
raise(MovedPermanentlyError, response.data[:url]) if response.status == 301
|
288
418
|
end
|
289
419
|
|
290
420
|
yield(last_response.data)
|
291
421
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
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
|
309
439
|
end
|
310
440
|
end
|
311
441
|
|
312
442
|
# This is wrapper with rescue block
|
313
443
|
#
|
314
444
|
# @return [Object] returns exactly the same, what you put in the block, but wrap it with begin-rescue block
|
315
|
-
def check_github_response
|
316
|
-
Retriable.retriable(retry_options)
|
317
|
-
yield
|
318
|
-
end
|
445
|
+
def check_github_response(&block)
|
446
|
+
Retriable.retriable(retry_options, &block)
|
319
447
|
rescue MovedPermanentlyError => e
|
320
|
-
|
321
|
-
sys_abort("The repository has moved, please update your configuration")
|
448
|
+
fail_with_message(e, "The repository has moved, update your configuration")
|
322
449
|
rescue Octokit::Forbidden => e
|
323
|
-
|
324
|
-
sys_abort("Exceeded retry limit")
|
450
|
+
fail_with_message(e, "Exceeded retry limit")
|
325
451
|
rescue Octokit::Unauthorized => e
|
326
|
-
|
327
|
-
|
452
|
+
fail_with_message(e, "Error: wrong GitHub token")
|
453
|
+
end
|
454
|
+
|
455
|
+
# Presents the exception, and the aborts with the message.
|
456
|
+
def fail_with_message(error, message)
|
457
|
+
Helper.log.error("#{error.class}: #{error.message}")
|
458
|
+
sys_abort(message)
|
328
459
|
end
|
329
460
|
|
330
461
|
# Exponential backoff
|
@@ -348,7 +479,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
348
479
|
Helper.log.warn("RETRY - #{exception.class}: '#{exception.message}'")
|
349
480
|
Helper.log.warn("#{try} tries in #{elapsed_time} seconds and #{next_interval} seconds until the next try")
|
350
481
|
Helper.log.warn GH_RATE_LIMIT_EXCEEDED_MSG
|
351
|
-
Helper.log.warn
|
482
|
+
Helper.log.warn(client.rate_limit)
|
352
483
|
end
|
353
484
|
end
|
354
485
|
|
@@ -360,7 +491,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
360
491
|
#
|
361
492
|
# @param [String] log_string
|
362
493
|
def print_in_same_line(log_string)
|
363
|
-
print log_string
|
494
|
+
print "#{log_string}\r"
|
364
495
|
end
|
365
496
|
|
366
497
|
# Print long line with spaces on same line to clear prev message
|
@@ -373,7 +504,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
373
504
|
#
|
374
505
|
# @return [String]
|
375
506
|
def fetch_github_token
|
376
|
-
env_var = @options[:token]
|
507
|
+
env_var = @options[:token].presence || ENV["CHANGELOG_GITHUB_TOKEN"]
|
377
508
|
|
378
509
|
Helper.log.warn NO_TOKEN_PROVIDED unless env_var
|
379
510
|
|