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.
- checksums.yaml +5 -5
- data/LICENSE +1 -1
- data/README.md +332 -285
- data/Rakefile +1 -1
- 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 +124 -125
- 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 +25 -21
- 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 +233 -96
- data/lib/github_changelog_generator/options.rb +74 -2
- data/lib/github_changelog_generator/parser.rb +118 -74
- data/lib/github_changelog_generator/parser_file.rb +7 -3
- data/lib/github_changelog_generator/reader.rb +2 -2
- data/lib/github_changelog_generator/task.rb +4 -3
- 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 +51 -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 +24 -0
- data/spec/unit/parse_file_spec.rb +2 -2
- 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 -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(
|
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,58 @@ module GitHubChangelogGenerator
|
|
31
37
|
@project = @options[:project]
|
32
38
|
@since = @options[:since]
|
33
39
|
@http_cache = @options[:http_cache]
|
34
|
-
@
|
35
|
-
@
|
36
|
-
|
37
|
-
|
38
|
-
@
|
40
|
+
@commits = []
|
41
|
+
@branches = nil
|
42
|
+
@graph = nil
|
43
|
+
@client = nil
|
44
|
+
@commits_in_tag_cache = {}
|
39
45
|
end
|
40
46
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
58
|
-
ca_file = @options[:ssl_ca_file] || ENV["SSL_CA_FILE"] || File.expand_path("
|
59
|
-
|
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
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
109
|
-
count_pages = calculate_pages(
|
127
|
+
tags = []
|
128
|
+
page_i = 0
|
129
|
+
count_pages = calculate_pages(client, "tags", {})
|
110
130
|
|
111
|
-
iterate_pages(
|
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
|
142
|
-
count_pages = calculate_pages(
|
161
|
+
page_i = 0
|
162
|
+
count_pages = calculate_pages(client, "issues", closed_pr_options)
|
143
163
|
|
144
|
-
iterate_pages(
|
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
|
-
|
166
|
-
|
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(
|
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
|
190
|
-
|
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
|
-
|
193
|
-
|
194
|
-
threads << Thread.new do
|
215
|
+
issues.each do |issue|
|
216
|
+
semaphore.async do
|
195
217
|
issue["events"] = []
|
196
|
-
iterate_pages(
|
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
|
-
|
209
|
-
|
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 =
|
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(
|
230
|
-
|
231
|
-
commit
|
232
|
-
|
233
|
-
|
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
|
297
|
+
# Fetch all commits
|
238
298
|
#
|
239
|
-
# @return [
|
240
|
-
def
|
241
|
-
commits
|
242
|
-
|
243
|
-
|
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, *
|
278
|
-
|
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, *
|
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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
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)
|
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(
|
320
|
-
Helper.log.error("#{
|
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
|
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
|
494
|
+
print "#{log_string}\r"
|
358
495
|
end
|
359
496
|
|
360
497
|
# Print long line with spaces on same line to clear prev message
|