github_changelog_generator 1.15.0 → 1.16.3
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 +32 -54
- data/Rakefile +1 -1
- data/lib/github_changelog_generator/argv_parser.rb +225 -0
- data/lib/github_changelog_generator/generator/entry.rb +10 -10
- data/lib/github_changelog_generator/generator/generator.rb +41 -19
- data/lib/github_changelog_generator/generator/generator_fetcher.rb +5 -9
- data/lib/github_changelog_generator/generator/generator_processor.rb +23 -20
- data/lib/github_changelog_generator/generator/generator_tags.rb +15 -9
- data/lib/github_changelog_generator/generator/section.rb +27 -7
- data/lib/github_changelog_generator/helper.rb +1 -1
- data/lib/github_changelog_generator/octo_fetcher.rb +190 -126
- data/lib/github_changelog_generator/options.rb +4 -0
- data/lib/github_changelog_generator/parser.rb +70 -248
- data/lib/github_changelog_generator/parser_file.rb +29 -14
- data/lib/github_changelog_generator/reader.rb +2 -2
- data/lib/github_changelog_generator/ssl_certs/cacert.pem +851 -1680
- data/lib/github_changelog_generator/task.rb +3 -2
- data/lib/github_changelog_generator/version.rb +1 -1
- data/man/git-generate-changelog.1 +46 -34
- data/man/git-generate-changelog.1.html +39 -31
- data/man/git-generate-changelog.html +19 -19
- data/man/git-generate-changelog.md +39 -31
- data/spec/files/config_example +5 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/generator/entry_spec.rb +37 -31
- data/spec/unit/generator/generator_processor_spec.rb +99 -44
- data/spec/unit/generator/generator_spec.rb +47 -0
- data/spec/unit/generator/generator_tags_spec.rb +46 -3
- data/spec/unit/generator/section_spec.rb +34 -0
- data/spec/unit/octo_fetcher_spec.rb +45 -2
- data/spec/unit/parser_spec.rb +50 -0
- metadata +45 -9
@@ -2,8 +2,6 @@
|
|
2
2
|
|
3
3
|
module GitHubChangelogGenerator
|
4
4
|
class Generator
|
5
|
-
MAX_THREAD_NUMBER = 25
|
6
|
-
|
7
5
|
# Fetch event for issues and pull requests
|
8
6
|
# @return [Array] array of fetched issues
|
9
7
|
def fetch_events_for_issues_and_pr
|
@@ -64,7 +62,7 @@ module GitHubChangelogGenerator
|
|
64
62
|
# @param [Array] prs The PRs to associate.
|
65
63
|
# @return [Array] PRs without their merge_commit_sha in a tag.
|
66
64
|
def associate_tagged_prs(tags, prs, total)
|
67
|
-
@fetcher.
|
65
|
+
@fetcher.fetch_tag_shas(tags)
|
68
66
|
|
69
67
|
i = 0
|
70
68
|
prs.reject do |pr|
|
@@ -106,7 +104,7 @@ module GitHubChangelogGenerator
|
|
106
104
|
i = total - prs_left.count
|
107
105
|
prs_left.reject do |pr|
|
108
106
|
found = false
|
109
|
-
if pr["events"] && (event = pr["events"].find { |e| e["event"] == "merged" }) && sha_in_release_branch(event["commit_id"])
|
107
|
+
if pr["events"] && (event = pr["events"].find { |e| e["event"] == "merged" }) && sha_in_release_branch?(event["commit_id"])
|
110
108
|
found = true
|
111
109
|
i += 1
|
112
110
|
print("Associating PRs with tags: #{i}/#{total}\r") if @options[:verbose]
|
@@ -139,7 +137,7 @@ module GitHubChangelogGenerator
|
|
139
137
|
pr["first_occurring_tag"] = oldest_tag["name"]
|
140
138
|
found = true
|
141
139
|
i += 1
|
142
|
-
elsif sha_in_release_branch(rebased_sha)
|
140
|
+
elsif sha_in_release_branch?(rebased_sha)
|
143
141
|
found = true
|
144
142
|
i += 1
|
145
143
|
else
|
@@ -197,11 +195,9 @@ module GitHubChangelogGenerator
|
|
197
195
|
#
|
198
196
|
# @param [String] sha SHA to check.
|
199
197
|
# @return [Boolean] True if SHA is in the branch git history.
|
200
|
-
def sha_in_release_branch(sha)
|
198
|
+
def sha_in_release_branch?(sha)
|
201
199
|
branch = @options[:release_branch] || @fetcher.default_branch
|
202
|
-
|
203
|
-
shas_in_branch = commits_in_branch["commits"].collect { |commit| commit["sha"] }
|
204
|
-
shas_in_branch.include?(sha)
|
200
|
+
@fetcher.commits_in_branch(branch).include?(sha)
|
205
201
|
end
|
206
202
|
end
|
207
203
|
end
|
@@ -69,6 +69,9 @@ module GitHubChangelogGenerator
|
|
69
69
|
# leave issues without milestones
|
70
70
|
if issue["milestone"].nil?
|
71
71
|
true
|
72
|
+
# remove issues of open milestones if option is set
|
73
|
+
elsif issue["milestone"]["state"] == "open"
|
74
|
+
@options[:issues_of_open_milestones]
|
72
75
|
else
|
73
76
|
# check, that this milestone in tag list:
|
74
77
|
@filtered_tags.find { |tag| tag["name"] == issue["milestone"]["title"] }.nil?
|
@@ -130,21 +133,19 @@ module GitHubChangelogGenerator
|
|
130
133
|
end
|
131
134
|
|
132
135
|
def tag_older_new_tag?(newer_tag_time, time)
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
tag_in_range_new
|
136
|
+
if newer_tag_time.nil?
|
137
|
+
true
|
138
|
+
else
|
139
|
+
time <= newer_tag_time
|
140
|
+
end
|
139
141
|
end
|
140
142
|
|
141
143
|
def tag_newer_old_tag?(older_tag_time, time)
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
tag_in_range_old
|
144
|
+
if older_tag_time.nil?
|
145
|
+
true
|
146
|
+
else
|
147
|
+
time > older_tag_time
|
148
|
+
end
|
148
149
|
end
|
149
150
|
|
150
151
|
# Include issues with labels, specified in :include_labels
|
@@ -152,22 +153,24 @@ module GitHubChangelogGenerator
|
|
152
153
|
# @return [Array] filtered array of issues
|
153
154
|
def include_issues_by_labels(issues)
|
154
155
|
filtered_issues = filter_by_include_labels(issues)
|
155
|
-
|
156
|
-
filtered_issues
|
156
|
+
filter_wo_labels(filtered_issues)
|
157
157
|
end
|
158
158
|
|
159
|
-
# @param [Array]
|
159
|
+
# @param [Array] items Issues & PRs to filter when without labels
|
160
160
|
# @return [Array] Issues & PRs without labels or empty array if
|
161
161
|
# add_issues_wo_labels or add_pr_wo_labels are false
|
162
|
-
def filter_wo_labels(
|
163
|
-
if
|
164
|
-
|
165
|
-
|
166
|
-
|
162
|
+
def filter_wo_labels(items)
|
163
|
+
if items.any? && items.first.key?("pull_request")
|
164
|
+
return items if options[:add_pr_wo_labels]
|
165
|
+
elsif options[:add_issues_wo_labels]
|
166
|
+
return items
|
167
167
|
end
|
168
|
+
# The default is to filter items without labels
|
169
|
+
items.select { |item| item["labels"].map { |l| l["name"] }.any? }
|
168
170
|
end
|
169
171
|
|
170
172
|
# @todo Document this
|
173
|
+
# @param [Object] issues
|
171
174
|
def filter_by_include_labels(issues)
|
172
175
|
if options[:include_labels].nil?
|
173
176
|
issues
|
@@ -11,15 +11,10 @@ module GitHubChangelogGenerator
|
|
11
11
|
fetch_tags_dates(all_tags) # Creates a Hash @tag_times_hash
|
12
12
|
all_sorted_tags = sort_tags_by_date(all_tags)
|
13
13
|
|
14
|
-
@sorted_tags =
|
14
|
+
@sorted_tags = filter_included_tags(all_sorted_tags)
|
15
|
+
@sorted_tags = filter_excluded_tags(@sorted_tags)
|
15
16
|
@filtered_tags = get_filtered_tags(@sorted_tags)
|
16
|
-
|
17
|
-
# Because we need to properly create compare links, we need a sorted list
|
18
|
-
# of all filtered tags (including the excluded ones). We'll exclude those
|
19
|
-
# tags from section headers inside the mapping function.
|
20
|
-
section_tags = get_filtered_tags(all_sorted_tags)
|
21
|
-
|
22
|
-
@tag_section_mapping = build_tag_section_mapping(section_tags, @filtered_tags)
|
17
|
+
@tag_section_mapping = build_tag_section_mapping(@filtered_tags, @filtered_tags)
|
23
18
|
|
24
19
|
@filtered_tags
|
25
20
|
end
|
@@ -83,7 +78,7 @@ module GitHubChangelogGenerator
|
|
83
78
|
# @return [Array] link, name and time of the tag
|
84
79
|
def detect_link_tag_time(newer_tag)
|
85
80
|
# if tag is nil - set current time
|
86
|
-
newer_tag_time = newer_tag.nil? ? Time.new : get_time_of_tag(newer_tag)
|
81
|
+
newer_tag_time = newer_tag.nil? ? Time.new.getutc : get_time_of_tag(newer_tag)
|
87
82
|
|
88
83
|
# if it's future release tag - set this value
|
89
84
|
if newer_tag.nil? && options[:future_release]
|
@@ -161,6 +156,17 @@ module GitHubChangelogGenerator
|
|
161
156
|
filtered_tags
|
162
157
|
end
|
163
158
|
|
159
|
+
# @param [Array] all_tags all tags
|
160
|
+
# @return [Array] filtered tags according to :include_tags_regex option
|
161
|
+
def filter_included_tags(all_tags)
|
162
|
+
if options[:include_tags_regex]
|
163
|
+
regex = Regexp.new(options[:include_tags_regex])
|
164
|
+
all_tags.select { |tag| regex =~ tag["name"] }
|
165
|
+
else
|
166
|
+
all_tags
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
164
170
|
# @param [Array] all_tags all tags
|
165
171
|
# @return [Array] filtered tags according :exclude_tags or :exclude_tags_regex option
|
166
172
|
def filter_excluded_tags(all_tags)
|
@@ -7,7 +7,23 @@ module GitHubChangelogGenerator
|
|
7
7
|
#
|
8
8
|
# @see GitHubChangelogGenerator::Entry
|
9
9
|
class Section
|
10
|
-
|
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
|
11
27
|
|
12
28
|
def initialize(opts = {})
|
13
29
|
@name = opts[:name]
|
@@ -16,11 +32,12 @@ module GitHubChangelogGenerator
|
|
16
32
|
@issues = opts[:issues] || []
|
17
33
|
@options = opts[:options] || Options.new({})
|
18
34
|
@body_only = opts[:body_only] || false
|
35
|
+
@entry = Entry.new(options)
|
19
36
|
end
|
20
37
|
|
21
38
|
# Returns the content of a section.
|
22
39
|
#
|
23
|
-
# @return [String]
|
40
|
+
# @return [String] Generated section content
|
24
41
|
def generate_content
|
25
42
|
content = ""
|
26
43
|
|
@@ -49,7 +66,7 @@ module GitHubChangelogGenerator
|
|
49
66
|
encapsulated_title = encapsulate_string issue["title"]
|
50
67
|
|
51
68
|
title_with_number = "#{encapsulated_title} [\\##{issue['number']}](#{issue['html_url']})"
|
52
|
-
title_with_number = "#{title_with_number}#{line_labels_for(issue)}" if @options[:issue_line_labels].present?
|
69
|
+
title_with_number = "#{title_with_number}#{@entry.line_labels_for(issue)}" if @options[:issue_line_labels].present?
|
53
70
|
line = issue_line_with_user(title_with_number, issue)
|
54
71
|
issue_line_with_body(line, issue)
|
55
72
|
end
|
@@ -60,16 +77,16 @@ module GitHubChangelogGenerator
|
|
60
77
|
|
61
78
|
# get issue body till first line break
|
62
79
|
body_paragraph = body_till_first_break(issue["body"])
|
63
|
-
# remove spaces from
|
80
|
+
# remove spaces from beginning of the string
|
64
81
|
body_paragraph.rstrip!
|
65
82
|
# encapsulate to md
|
66
|
-
encapsulated_body = "\
|
83
|
+
encapsulated_body = " \n#{encapsulate_string(body_paragraph)}"
|
67
84
|
|
68
85
|
"**#{line}** #{encapsulated_body}"
|
69
86
|
end
|
70
87
|
|
71
88
|
def body_till_first_break(body)
|
72
|
-
body.split(/\n
|
89
|
+
body.split(/\n/, 2).first
|
73
90
|
end
|
74
91
|
|
75
92
|
def issue_line_with_user(line, issue)
|
@@ -95,7 +112,10 @@ module GitHubChangelogGenerator
|
|
95
112
|
string = string.gsub('\\', '\\\\')
|
96
113
|
|
97
114
|
ENCAPSULATED_CHARACTERS.each do |char|
|
98
|
-
|
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}")
|
99
119
|
end
|
100
120
|
|
101
121
|
string
|
@@ -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
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,47 +37,58 @@ module GitHubChangelogGenerator
|
|
31
37
|
@project = @options[:project]
|
32
38
|
@since = @options[:since]
|
33
39
|
@http_cache = @options[:http_cache]
|
34
|
-
@cache_file = nil
|
35
|
-
@cache_log = nil
|
36
40
|
@commits = []
|
37
|
-
@
|
38
|
-
|
39
|
-
|
40
|
-
@
|
41
|
+
@branches = nil
|
42
|
+
@graph = nil
|
43
|
+
@client = nil
|
44
|
+
@commits_in_tag_cache = {}
|
45
|
+
end
|
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
|
61
|
+
|
62
|
+
builder.use Octokit::Response::RaiseError
|
63
|
+
builder.adapter :async_http
|
64
|
+
end
|
41
65
|
end
|
42
66
|
|
43
|
-
def
|
44
|
-
|
67
|
+
def connection_options
|
68
|
+
ca_file = @options[:ssl_ca_file] || ENV["SSL_CA_FILE"] || File.expand_path("ssl_certs/cacert.pem", __dir__)
|
45
69
|
|
46
|
-
|
47
|
-
@cache_log = @options.fetch(:cache_log) { File.join(Dir.tmpdir, "github-changelog-logger.log") }
|
48
|
-
init_cache
|
70
|
+
Octokit.connection_options.merge({ ssl: { ca_file: ca_file } })
|
49
71
|
end
|
50
72
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
result[:api_endpoint] = endpoint if endpoint
|
57
|
-
result
|
58
|
-
end
|
73
|
+
def client_options
|
74
|
+
options = {
|
75
|
+
middleware: middleware,
|
76
|
+
connection_options: connection_options
|
77
|
+
}
|
59
78
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
end
|
79
|
+
if (github_token = fetch_github_token)
|
80
|
+
options[:access_token] = github_token
|
81
|
+
end
|
64
82
|
|
65
|
-
|
66
|
-
|
67
|
-
builder.use(Faraday::HttpCache, serializer: Marshal,
|
68
|
-
store: ActiveSupport::Cache::FileStore.new(@cache_file),
|
69
|
-
logger: Logger.new(@cache_log),
|
70
|
-
shared_cache: false)
|
71
|
-
builder.use Octokit::Response::RaiseError
|
72
|
-
builder.adapter Faraday.default_adapter
|
73
|
-
# builder.response :logger
|
83
|
+
if (endpoint = @options[:github_endpoint])
|
84
|
+
options[:api_endpoint] = endpoint
|
74
85
|
end
|
86
|
+
|
87
|
+
options
|
88
|
+
end
|
89
|
+
|
90
|
+
def client
|
91
|
+
@client ||= Octokit::Client.new(client_options)
|
75
92
|
end
|
76
93
|
|
77
94
|
DEFAULT_REQUEST_OPTIONS = { per_page: PER_PAGE_NUMBER }
|
@@ -88,6 +105,9 @@ module GitHubChangelogGenerator
|
|
88
105
|
# Returns the number of pages for a API call
|
89
106
|
#
|
90
107
|
# @return [Integer] number of pages for this API call in total
|
108
|
+
# @param [Object] request_options
|
109
|
+
# @param [Object] method
|
110
|
+
# @param [Object] client
|
91
111
|
def calculate_pages(client, method, request_options)
|
92
112
|
# Makes the first API call so that we can call last_response
|
93
113
|
check_github_response do
|
@@ -107,11 +127,11 @@ module GitHubChangelogGenerator
|
|
107
127
|
#
|
108
128
|
# @return [Array <Hash>] array of tags in repo
|
109
129
|
def github_fetch_tags
|
110
|
-
tags
|
111
|
-
page_i
|
112
|
-
count_pages = calculate_pages(
|
130
|
+
tags = []
|
131
|
+
page_i = 0
|
132
|
+
count_pages = calculate_pages(client, "tags", {})
|
113
133
|
|
114
|
-
iterate_pages(
|
134
|
+
iterate_pages(client, "tags") do |new_tags|
|
115
135
|
page_i += PER_PAGE_NUMBER
|
116
136
|
print_in_same_line("Fetching tags... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
|
117
137
|
tags.concat(new_tags)
|
@@ -142,9 +162,9 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
142
162
|
print "Fetching closed issues...\r" if @options[:verbose]
|
143
163
|
issues = []
|
144
164
|
page_i = 0
|
145
|
-
count_pages = calculate_pages(
|
165
|
+
count_pages = calculate_pages(client, "issues", closed_pr_options)
|
146
166
|
|
147
|
-
iterate_pages(
|
167
|
+
iterate_pages(client, "issues", **closed_pr_options) do |new_issues|
|
148
168
|
page_i += PER_PAGE_NUMBER
|
149
169
|
print_in_same_line("Fetching issues... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
|
150
170
|
issues.concat(new_issues)
|
@@ -165,10 +185,10 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
165
185
|
pull_requests = []
|
166
186
|
options = { state: "closed" }
|
167
187
|
|
168
|
-
page_i
|
169
|
-
count_pages = calculate_pages(
|
188
|
+
page_i = 0
|
189
|
+
count_pages = calculate_pages(client, "pull_requests", options)
|
170
190
|
|
171
|
-
iterate_pages(
|
191
|
+
iterate_pages(client, "pull_requests", **options) do |new_pr|
|
172
192
|
page_i += PER_PAGE_NUMBER
|
173
193
|
log_string = "Fetching merged dates... #{page_i}/#{count_pages * PER_PAGE_NUMBER}"
|
174
194
|
print_in_same_line(log_string)
|
@@ -185,14 +205,20 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
185
205
|
# @param [Array] issues
|
186
206
|
# @return [Void]
|
187
207
|
def fetch_events_async(issues)
|
188
|
-
i
|
189
|
-
|
208
|
+
i = 0
|
209
|
+
# Add accept option explicitly for disabling the warning of preview API.
|
210
|
+
preview = { accept: Octokit::Preview::PREVIEW_TYPES[:project_card_events] }
|
211
|
+
|
212
|
+
barrier = Async::Barrier.new
|
213
|
+
semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
|
214
|
+
|
215
|
+
Sync do
|
216
|
+
client = self.client
|
190
217
|
|
191
|
-
|
192
|
-
|
193
|
-
threads << Thread.new do
|
218
|
+
issues.each do |issue|
|
219
|
+
semaphore.async do
|
194
220
|
issue["events"] = []
|
195
|
-
iterate_pages(
|
221
|
+
iterate_pages(client, "issue_events", issue["number"], **preview) do |new_event|
|
196
222
|
issue["events"].concat(new_event)
|
197
223
|
end
|
198
224
|
issue["events"] = issue["events"].map { |event| stringify_keys_deep(event.to_hash) }
|
@@ -200,12 +226,12 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
200
226
|
i += 1
|
201
227
|
end
|
202
228
|
end
|
203
|
-
threads.each(&:join)
|
204
|
-
threads = []
|
205
|
-
end
|
206
229
|
|
207
|
-
|
208
|
-
|
230
|
+
barrier.wait
|
231
|
+
|
232
|
+
# to clear line from prev print
|
233
|
+
print_empty_line
|
234
|
+
end
|
209
235
|
|
210
236
|
Helper.log.info "Fetching events for issues and PR: #{i}"
|
211
237
|
end
|
@@ -215,21 +241,25 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
215
241
|
# @param [Array] prs The array of PRs.
|
216
242
|
# @return [Void] No return; PRs are updated in-place.
|
217
243
|
def fetch_comments_async(prs)
|
218
|
-
|
244
|
+
barrier = Async::Barrier.new
|
245
|
+
semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
|
219
246
|
|
220
|
-
|
221
|
-
|
222
|
-
|
247
|
+
Sync do
|
248
|
+
client = self.client
|
249
|
+
|
250
|
+
prs.each do |pr|
|
251
|
+
semaphore.async do
|
223
252
|
pr["comments"] = []
|
224
|
-
iterate_pages(
|
253
|
+
iterate_pages(client, "issue_comments", pr["number"]) do |new_comment|
|
225
254
|
pr["comments"].concat(new_comment)
|
226
255
|
end
|
227
256
|
pr["comments"] = pr["comments"].map { |comment| stringify_keys_deep(comment.to_hash) }
|
228
257
|
end
|
229
258
|
end
|
230
|
-
|
231
|
-
|
259
|
+
|
260
|
+
barrier.wait
|
232
261
|
end
|
262
|
+
|
233
263
|
nil
|
234
264
|
end
|
235
265
|
|
@@ -245,21 +275,6 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
245
275
|
commit_data["commit"]["committer"]["date"]
|
246
276
|
end
|
247
277
|
|
248
|
-
# Fetch and cache comparison between two github refs
|
249
|
-
#
|
250
|
-
# @param [String] older The older sha/tag/branch.
|
251
|
-
# @param [String] newer The newer sha/tag/branch.
|
252
|
-
# @return [Hash] Github api response for comparison.
|
253
|
-
def fetch_compare(older, newer)
|
254
|
-
unless @compares["#{older}...#{newer}"]
|
255
|
-
compare_data = check_github_response { @client.compare(user_project, older, newer || "HEAD") }
|
256
|
-
raise StandardError, "Sha #{older} and sha #{newer} are not related; please file a github-changelog-generator issues and describe how to replicate this issue." if compare_data["status"] == "diverged"
|
257
|
-
|
258
|
-
@compares["#{older}...#{newer}"] = stringify_keys_deep(compare_data.to_hash)
|
259
|
-
end
|
260
|
-
@compares["#{older}...#{newer}"]
|
261
|
-
end
|
262
|
-
|
263
278
|
# Fetch commit for specified event
|
264
279
|
#
|
265
280
|
# @param [String] commit_id the SHA of a commit to fetch
|
@@ -271,9 +286,11 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
271
286
|
if found
|
272
287
|
stringify_keys_deep(found.to_hash)
|
273
288
|
else
|
289
|
+
client = self.client
|
290
|
+
|
274
291
|
# cache miss; don't add to @commits because unsure of order.
|
275
292
|
check_github_response do
|
276
|
-
commit =
|
293
|
+
commit = client.commit(user_project, commit_id)
|
277
294
|
commit = stringify_keys_deep(commit.to_hash)
|
278
295
|
commit
|
279
296
|
end
|
@@ -285,8 +302,25 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
285
302
|
# @return [Array] Commits in a repo.
|
286
303
|
def commits
|
287
304
|
if @commits.empty?
|
288
|
-
|
289
|
-
|
305
|
+
Sync do
|
306
|
+
barrier = Async::Barrier.new
|
307
|
+
semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
|
308
|
+
|
309
|
+
if (since_commit = @options[:since_commit])
|
310
|
+
iterate_pages(client, "commits_since", since_commit, parent: semaphore) do |new_commits|
|
311
|
+
@commits.concat(new_commits)
|
312
|
+
end
|
313
|
+
else
|
314
|
+
iterate_pages(client, "commits", parent: semaphore) do |new_commits|
|
315
|
+
@commits.concat(new_commits)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
barrier.wait
|
320
|
+
|
321
|
+
@commits.sort! do |b, a|
|
322
|
+
a[:commit][:author][:date] <=> b[:commit][:author][:date]
|
323
|
+
end
|
290
324
|
end
|
291
325
|
end
|
292
326
|
@commits
|
@@ -301,42 +335,63 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
301
335
|
|
302
336
|
# @return [String] Default branch of the repo
|
303
337
|
def default_branch
|
304
|
-
@default_branch ||=
|
338
|
+
@default_branch ||= client.repository(user_project)[:default_branch]
|
339
|
+
end
|
340
|
+
|
341
|
+
# @param [String] name
|
342
|
+
# @return [Array<String>]
|
343
|
+
def commits_in_branch(name)
|
344
|
+
@branches ||= client.branches(user_project).map { |branch| [branch[:name], branch] }.to_h
|
345
|
+
|
346
|
+
if (branch = @branches[name])
|
347
|
+
commits_in_tag(branch[:commit][:sha])
|
348
|
+
else
|
349
|
+
[]
|
350
|
+
end
|
305
351
|
end
|
306
352
|
|
307
353
|
# Fetch all SHAs occurring in or before a given tag and add them to
|
308
354
|
# "shas_in_tag"
|
309
355
|
#
|
310
356
|
# @param [Array] tags The array of tags.
|
311
|
-
# @return
|
312
|
-
def
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
357
|
+
# @return void
|
358
|
+
def fetch_tag_shas(tags)
|
359
|
+
# Reverse the tags array to gain max benefit from the @commits_in_tag_cache
|
360
|
+
tags.reverse_each do |tag|
|
361
|
+
tag["shas_in_tag"] = commits_in_tag(tag["commit"]["sha"])
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
private
|
366
|
+
|
367
|
+
# @param [Set] shas
|
368
|
+
# @param [Object] sha
|
369
|
+
def commits_in_tag(sha, shas = Set.new)
|
370
|
+
# Reduce multiple runs for the same tag
|
371
|
+
return @commits_in_tag_cache[sha] if @commits_in_tag_cache.key?(sha)
|
372
|
+
|
373
|
+
@graph ||= commits.map { |commit| [commit[:sha], commit] }.to_h
|
374
|
+
return shas unless (current = @graph[sha])
|
375
|
+
|
376
|
+
queue = [current]
|
377
|
+
while queue.any?
|
378
|
+
commit = queue.shift
|
379
|
+
# If we've already processed this sha, just grab it's parents from the cache
|
380
|
+
if @commits_in_tag_cache.key?(commit[:sha])
|
381
|
+
shas.merge(@commits_in_tag_cache[commit[:sha]])
|
382
|
+
else
|
383
|
+
shas.add(commit[:sha])
|
384
|
+
commit[:parents].each do |p|
|
385
|
+
queue.push(@graph[p[:sha]]) unless shas.include?(p[:sha])
|
325
386
|
end
|
326
387
|
end
|
327
|
-
threads.each(&:join)
|
328
|
-
threads = []
|
329
388
|
end
|
330
389
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
Helper.log.info "Fetching SHAs for tags: #{i}"
|
335
|
-
nil
|
390
|
+
@commits_in_tag_cache[sha] = shas
|
391
|
+
shas
|
336
392
|
end
|
337
393
|
|
338
|
-
|
339
|
-
|
394
|
+
# @param [Object] indata
|
340
395
|
def stringify_keys_deep(indata)
|
341
396
|
case indata
|
342
397
|
when Array
|
@@ -360,43 +415,49 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
360
415
|
#
|
361
416
|
# @param [Octokit::Client] client
|
362
417
|
# @param [String] method (eg. 'tags')
|
418
|
+
# @param [Array] arguments
|
419
|
+
# @param [Async::Semaphore] parent
|
363
420
|
#
|
364
421
|
# @yield [Sawyer::Resource] An OctoKit-provided response (which can be empty)
|
365
422
|
#
|
366
423
|
# @return [void]
|
367
|
-
|
368
|
-
|
424
|
+
# @param [Hash] options
|
425
|
+
def iterate_pages(client, method, *arguments, parent: nil, **options)
|
426
|
+
options = DEFAULT_REQUEST_OPTIONS.merge(options)
|
369
427
|
|
370
|
-
check_github_response { client.send(method, user_project, *
|
428
|
+
check_github_response { client.send(method, user_project, *arguments, **options) }
|
371
429
|
last_response = client.last_response.tap do |response|
|
372
430
|
raise(MovedPermanentlyError, response.data[:url]) if response.status == 301
|
373
431
|
end
|
374
432
|
|
375
433
|
yield(last_response.data)
|
376
434
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
435
|
+
if parent.nil?
|
436
|
+
# The snail visits one leaf at a time:
|
437
|
+
until (next_one = last_response.rels[:next]).nil?
|
438
|
+
last_response = check_github_response { next_one.get }
|
439
|
+
yield(last_response.data)
|
440
|
+
end
|
441
|
+
elsif (last = last_response.rels[:last])
|
442
|
+
# OR we bring out the gatling gun:
|
443
|
+
parameters = querystring_as_hash(last.href)
|
444
|
+
last_page = Integer(parameters["page"])
|
445
|
+
|
446
|
+
(2..last_page).each do |page|
|
447
|
+
parent.async do
|
448
|
+
data = check_github_response { client.send(method, user_project, *arguments, page: page, **options) }
|
449
|
+
yield data
|
450
|
+
end
|
451
|
+
end
|
390
452
|
end
|
391
453
|
end
|
392
454
|
|
393
455
|
# This is wrapper with rescue block
|
394
456
|
#
|
395
457
|
# @return [Object] returns exactly the same, what you put in the block, but wrap it with begin-rescue block
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
end
|
458
|
+
# @param [Proc] block
|
459
|
+
def check_github_response(&block)
|
460
|
+
Retriable.retriable(retry_options, &block)
|
400
461
|
rescue MovedPermanentlyError => e
|
401
462
|
fail_with_message(e, "The repository has moved, update your configuration")
|
402
463
|
rescue Octokit::Forbidden => e
|
@@ -406,6 +467,8 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
406
467
|
end
|
407
468
|
|
408
469
|
# Presents the exception, and the aborts with the message.
|
470
|
+
# @param [Object] message
|
471
|
+
# @param [Object] error
|
409
472
|
def fail_with_message(error, message)
|
410
473
|
Helper.log.error("#{error.class}: #{error.message}")
|
411
474
|
sys_abort(message)
|
@@ -432,10 +495,11 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
432
495
|
Helper.log.warn("RETRY - #{exception.class}: '#{exception.message}'")
|
433
496
|
Helper.log.warn("#{try} tries in #{elapsed_time} seconds and #{next_interval} seconds until the next try")
|
434
497
|
Helper.log.warn GH_RATE_LIMIT_EXCEEDED_MSG
|
435
|
-
Helper.log.warn
|
498
|
+
Helper.log.warn(client.rate_limit)
|
436
499
|
end
|
437
500
|
end
|
438
501
|
|
502
|
+
# @param [Object] msg
|
439
503
|
def sys_abort(msg)
|
440
504
|
abort(msg)
|
441
505
|
end
|
@@ -444,7 +508,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
444
508
|
#
|
445
509
|
# @param [String] log_string
|
446
510
|
def print_in_same_line(log_string)
|
447
|
-
print log_string
|
511
|
+
print "#{log_string}\r"
|
448
512
|
end
|
449
513
|
|
450
514
|
# Print long line with spaces on same line to clear prev message
|