github_changelog_generator 1.15.2 → 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 +4 -4
- data/README.md +3 -1
- data/Rakefile +1 -1
- 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 +2 -5
- data/lib/github_changelog_generator/generator/generator_processor.rb +21 -19
- 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 +169 -124
- data/lib/github_changelog_generator/options.rb +3 -0
- data/lib/github_changelog_generator/parser.rb +12 -2
- data/lib/github_changelog_generator/parser_file.rb +1 -1
- data/lib/github_changelog_generator/reader.rb +2 -2
- data/lib/github_changelog_generator/task.rb +3 -2
- data/lib/github_changelog_generator/version.rb +1 -1
- data/man/git-generate-changelog.1 +15 -3
- data/man/git-generate-changelog.1.html +9 -1
- data/man/git-generate-changelog.md +8 -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
- metadata +39 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb493bfacf7e5ec521d56e8ac70078b9f2297e88707aaa1adc267fa1f2caa829
|
4
|
+
data.tar.gz: b5368080a19d7b90f36087b1601cffed3034b4e0562e0c57b9bbe519a683cdee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5482719348a05eec6f1644a9ff8298720e1b33dc34bb7d8ee030e567878285e0b538d17039f8cc23aac5829a9eddeaf96f3d7e5d68b44d3e1ec848c2cbbcffc
|
7
|
+
data.tar.gz: e172b08ad77a2c4d6650441a36bd594dcbc186ab85afc47c382d1ea303927fbed3aae280987e25e264eb92419b75ae3262466a92950653a21b8e1093b120e3dd
|
data/README.md
CHANGED
@@ -73,7 +73,9 @@ or use `sudo gem install github_changelog_generator` (Linux).
|
|
73
73
|
|
74
74
|
### Running with CLI:
|
75
75
|
|
76
|
-
github_changelog_generator -u
|
76
|
+
github_changelog_generator -u github_project_namespace -p github_project
|
77
|
+
|
78
|
+
(where the project namespace is _likely_ your username if it's a project you own, but it could also be the namespace of the project)
|
77
79
|
|
78
80
|
|
79
81
|
### Running with Docker
|
data/Rakefile
CHANGED
@@ -13,7 +13,7 @@ RSpec::Core::RakeTask.new
|
|
13
13
|
|
14
14
|
desc "When releasing the gem, re-fetch latest cacert.pem from curl.haxx.se. Developer task."
|
15
15
|
task :update_ssl_ca_file do
|
16
|
-
`pushd lib/github_changelog_generator/ssl_certs && curl --remote-name --time-cond cacert.pem https://curl.
|
16
|
+
`pushd lib/github_changelog_generator/ssl_certs && curl --remote-name --time-cond cacert.pem https://curl.se/ca/cacert.pem && popd`
|
17
17
|
end
|
18
18
|
|
19
19
|
task default: %i[rubocop spec]
|
@@ -40,6 +40,15 @@ module GitHubChangelogGenerator
|
|
40
40
|
@content
|
41
41
|
end
|
42
42
|
|
43
|
+
def line_labels_for(issue)
|
44
|
+
labels = if @options[:issue_line_labels] == ["ALL"]
|
45
|
+
issue["labels"]
|
46
|
+
else
|
47
|
+
issue["labels"].select { |label| @options[:issue_line_labels].include?(label["name"]) }
|
48
|
+
end
|
49
|
+
labels.map { |label| " \[[#{label['name']}](#{label['url'].sub('api.github.com/repos', 'github.com')})\]" }.join("")
|
50
|
+
end
|
51
|
+
|
43
52
|
private
|
44
53
|
|
45
54
|
# Creates section objects for this entry.
|
@@ -72,7 +81,7 @@ module GitHubChangelogGenerator
|
|
72
81
|
end
|
73
82
|
|
74
83
|
sections_json.collect do |name, v|
|
75
|
-
Section.new(name: name.to_s, prefix: v["prefix"], labels: v["labels"], options: @options)
|
84
|
+
Section.new(name: name.to_s, prefix: v["prefix"], labels: v["labels"], body_only: v["body_only"], options: @options)
|
76
85
|
end
|
77
86
|
end
|
78
87
|
|
@@ -205,14 +214,5 @@ module GitHubChangelogGenerator
|
|
205
214
|
end
|
206
215
|
nil
|
207
216
|
end
|
208
|
-
|
209
|
-
def line_labels_for(issue)
|
210
|
-
labels = if @options[:issue_line_labels] == ["ALL"]
|
211
|
-
issue["labels"]
|
212
|
-
else
|
213
|
-
issue["labels"].select { |label| @options[:issue_line_labels].include?(label["name"]) }
|
214
|
-
end
|
215
|
-
labels.map { |label| " \[[#{label['name']}](#{label['url'].sub('api.github.com/repos', 'github.com')})\]" }.join("")
|
216
|
-
end
|
217
217
|
end
|
218
218
|
end
|
@@ -26,6 +26,12 @@ module GitHubChangelogGenerator
|
|
26
26
|
class Generator
|
27
27
|
attr_accessor :options, :filtered_tags, :tag_section_mapping, :sorted_tags
|
28
28
|
|
29
|
+
CREDIT_LINE = <<~CREDIT
|
30
|
+
\\* *This Changelog was automatically generated \
|
31
|
+
by [github_changelog_generator]\
|
32
|
+
(https://github.com/github-changelog-generator/github-changelog-generator)*
|
33
|
+
CREDIT
|
34
|
+
|
29
35
|
# A Generator responsible for all logic, related with changelog generation from ready-to-parse issues
|
30
36
|
#
|
31
37
|
# Example:
|
@@ -43,26 +49,21 @@ module GitHubChangelogGenerator
|
|
43
49
|
# @return [String] Generated changelog file
|
44
50
|
def compound_changelog
|
45
51
|
@options.load_custom_ruby_files
|
46
|
-
fetch_and_filter_tags
|
47
|
-
fetch_issues_and_pr
|
48
|
-
|
49
|
-
log = ""
|
50
|
-
log += @options[:frontmatter] if @options[:frontmatter]
|
51
|
-
log += "#{options[:header]}\n\n"
|
52
|
-
|
53
|
-
log += if @options[:unreleased_only]
|
54
|
-
generate_entry_between_tags(@filtered_tags[0], nil)
|
55
|
-
else
|
56
|
-
generate_entries_for_all_tags
|
57
|
-
end
|
58
|
-
|
59
|
-
log += File.read(@options[:base]) if File.file?(@options[:base])
|
60
|
-
|
61
|
-
credit_line = "\n\n\\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*"
|
62
|
-
log.gsub!(/#{credit_line}(\n)?/, "") # Remove old credit lines
|
63
|
-
log += "#{credit_line}\n"
|
64
52
|
|
65
|
-
|
53
|
+
Sync do
|
54
|
+
fetch_and_filter_tags
|
55
|
+
fetch_issues_and_pr
|
56
|
+
|
57
|
+
log = if @options[:unreleased_only]
|
58
|
+
generate_entry_between_tags(@filtered_tags[0], nil)
|
59
|
+
else
|
60
|
+
generate_entries_for_all_tags
|
61
|
+
end
|
62
|
+
log += File.read(@options[:base]) if File.file?(@options[:base])
|
63
|
+
log = remove_old_fixed_string(log)
|
64
|
+
log = insert_fixed_string(log)
|
65
|
+
@log = log
|
66
|
+
end
|
66
67
|
end
|
67
68
|
|
68
69
|
private
|
@@ -151,5 +152,26 @@ module GitHubChangelogGenerator
|
|
151
152
|
add_first_occurring_tag_to_prs(@sorted_tags, @pull_requests)
|
152
153
|
nil
|
153
154
|
end
|
155
|
+
|
156
|
+
# Remove the previously assigned fixed message.
|
157
|
+
# @param log [String] Old lines are fixed
|
158
|
+
def remove_old_fixed_string(log)
|
159
|
+
log.gsub!(/#{Regexp.escape(@options[:frontmatter])}/, "") if @options[:frontmatter]
|
160
|
+
log.gsub!(/#{Regexp.escape(@options[:header])}\n{,2}/, "")
|
161
|
+
log.gsub!(/\n{,2}#{Regexp.escape(CREDIT_LINE)}/, "") # Remove old credit lines
|
162
|
+
log
|
163
|
+
end
|
164
|
+
|
165
|
+
# Add template messages to given string. Previously added
|
166
|
+
# messages of the same wording are removed.
|
167
|
+
# @param log [String]
|
168
|
+
def insert_fixed_string(log)
|
169
|
+
ins = ""
|
170
|
+
ins += @options[:frontmatter] if @options[:frontmatter]
|
171
|
+
ins += "#{@options[:header]}\n\n"
|
172
|
+
log.insert(0, ins)
|
173
|
+
log += "\n\n#{CREDIT_LINE}"
|
174
|
+
log
|
175
|
+
end
|
154
176
|
end
|
155
177
|
end
|
@@ -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|
|
@@ -199,8 +197,7 @@ module GitHubChangelogGenerator
|
|
199
197
|
# @return [Boolean] True if SHA is in the branch git history.
|
200
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"] }
|
200
|
+
shas_in_branch = @fetcher.commits_in_branch(branch)
|
204
201
|
shas_in_branch.include?(sha)
|
205
202
|
end
|
206
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,19 +153,20 @@ 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
159
|
# @param [Array] issues 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
|
@@ -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,8 +15,8 @@ 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 " \
|
@@ -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 }
|
@@ -107,11 +124,11 @@ module GitHubChangelogGenerator
|
|
107
124
|
#
|
108
125
|
# @return [Array <Hash>] array of tags in repo
|
109
126
|
def github_fetch_tags
|
110
|
-
tags
|
111
|
-
page_i
|
112
|
-
count_pages = calculate_pages(
|
127
|
+
tags = []
|
128
|
+
page_i = 0
|
129
|
+
count_pages = calculate_pages(client, "tags", {})
|
113
130
|
|
114
|
-
iterate_pages(
|
131
|
+
iterate_pages(client, "tags") do |new_tags|
|
115
132
|
page_i += PER_PAGE_NUMBER
|
116
133
|
print_in_same_line("Fetching tags... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
|
117
134
|
tags.concat(new_tags)
|
@@ -142,9 +159,9 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
142
159
|
print "Fetching closed issues...\r" if @options[:verbose]
|
143
160
|
issues = []
|
144
161
|
page_i = 0
|
145
|
-
count_pages = calculate_pages(
|
162
|
+
count_pages = calculate_pages(client, "issues", closed_pr_options)
|
146
163
|
|
147
|
-
iterate_pages(
|
164
|
+
iterate_pages(client, "issues", closed_pr_options) do |new_issues|
|
148
165
|
page_i += PER_PAGE_NUMBER
|
149
166
|
print_in_same_line("Fetching issues... #{page_i}/#{count_pages * PER_PAGE_NUMBER}")
|
150
167
|
issues.concat(new_issues)
|
@@ -165,10 +182,10 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
165
182
|
pull_requests = []
|
166
183
|
options = { state: "closed" }
|
167
184
|
|
168
|
-
page_i
|
169
|
-
count_pages = calculate_pages(
|
185
|
+
page_i = 0
|
186
|
+
count_pages = calculate_pages(client, "pull_requests", options)
|
170
187
|
|
171
|
-
iterate_pages(
|
188
|
+
iterate_pages(client, "pull_requests", options) do |new_pr|
|
172
189
|
page_i += PER_PAGE_NUMBER
|
173
190
|
log_string = "Fetching merged dates... #{page_i}/#{count_pages * PER_PAGE_NUMBER}"
|
174
191
|
print_in_same_line(log_string)
|
@@ -185,16 +202,20 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
185
202
|
# @param [Array] issues
|
186
203
|
# @return [Void]
|
187
204
|
def fetch_events_async(issues)
|
188
|
-
i
|
189
|
-
threads = []
|
205
|
+
i = 0
|
190
206
|
# Add accept option explicitly for disabling the warning of preview API.
|
191
207
|
preview = { accept: Octokit::Preview::PREVIEW_TYPES[:project_card_events] }
|
192
208
|
|
193
|
-
|
194
|
-
|
195
|
-
|
209
|
+
barrier = Async::Barrier.new
|
210
|
+
semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
|
211
|
+
|
212
|
+
Sync do
|
213
|
+
client = self.client
|
214
|
+
|
215
|
+
issues.each do |issue|
|
216
|
+
semaphore.async do
|
196
217
|
issue["events"] = []
|
197
|
-
iterate_pages(
|
218
|
+
iterate_pages(client, "issue_events", issue["number"], preview) do |new_event|
|
198
219
|
issue["events"].concat(new_event)
|
199
220
|
end
|
200
221
|
issue["events"] = issue["events"].map { |event| stringify_keys_deep(event.to_hash) }
|
@@ -202,12 +223,12 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
202
223
|
i += 1
|
203
224
|
end
|
204
225
|
end
|
205
|
-
threads.each(&:join)
|
206
|
-
threads = []
|
207
|
-
end
|
208
226
|
|
209
|
-
|
210
|
-
|
227
|
+
barrier.wait
|
228
|
+
|
229
|
+
# to clear line from prev print
|
230
|
+
print_empty_line
|
231
|
+
end
|
211
232
|
|
212
233
|
Helper.log.info "Fetching events for issues and PR: #{i}"
|
213
234
|
end
|
@@ -217,21 +238,25 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
217
238
|
# @param [Array] prs The array of PRs.
|
218
239
|
# @return [Void] No return; PRs are updated in-place.
|
219
240
|
def fetch_comments_async(prs)
|
220
|
-
|
241
|
+
barrier = Async::Barrier.new
|
242
|
+
semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
|
221
243
|
|
222
|
-
|
223
|
-
|
224
|
-
|
244
|
+
Sync do
|
245
|
+
client = self.client
|
246
|
+
|
247
|
+
prs.each do |pr|
|
248
|
+
semaphore.async do
|
225
249
|
pr["comments"] = []
|
226
|
-
iterate_pages(
|
250
|
+
iterate_pages(client, "issue_comments", pr["number"]) do |new_comment|
|
227
251
|
pr["comments"].concat(new_comment)
|
228
252
|
end
|
229
253
|
pr["comments"] = pr["comments"].map { |comment| stringify_keys_deep(comment.to_hash) }
|
230
254
|
end
|
231
255
|
end
|
232
|
-
|
233
|
-
|
256
|
+
|
257
|
+
barrier.wait
|
234
258
|
end
|
259
|
+
|
235
260
|
nil
|
236
261
|
end
|
237
262
|
|
@@ -247,21 +272,6 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
247
272
|
commit_data["commit"]["committer"]["date"]
|
248
273
|
end
|
249
274
|
|
250
|
-
# Fetch and cache comparison between two github refs
|
251
|
-
#
|
252
|
-
# @param [String] older The older sha/tag/branch.
|
253
|
-
# @param [String] newer The newer sha/tag/branch.
|
254
|
-
# @return [Hash] Github api response for comparison.
|
255
|
-
def fetch_compare(older, newer)
|
256
|
-
unless @compares["#{older}...#{newer}"]
|
257
|
-
compare_data = check_github_response { @client.compare(user_project, older, newer || "HEAD") }
|
258
|
-
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"
|
259
|
-
|
260
|
-
@compares["#{older}...#{newer}"] = stringify_keys_deep(compare_data.to_hash)
|
261
|
-
end
|
262
|
-
@compares["#{older}...#{newer}"]
|
263
|
-
end
|
264
|
-
|
265
275
|
# Fetch commit for specified event
|
266
276
|
#
|
267
277
|
# @param [String] commit_id the SHA of a commit to fetch
|
@@ -273,9 +283,11 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
273
283
|
if found
|
274
284
|
stringify_keys_deep(found.to_hash)
|
275
285
|
else
|
286
|
+
client = self.client
|
287
|
+
|
276
288
|
# cache miss; don't add to @commits because unsure of order.
|
277
289
|
check_github_response do
|
278
|
-
commit =
|
290
|
+
commit = client.commit(user_project, commit_id)
|
279
291
|
commit = stringify_keys_deep(commit.to_hash)
|
280
292
|
commit
|
281
293
|
end
|
@@ -287,8 +299,25 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
287
299
|
# @return [Array] Commits in a repo.
|
288
300
|
def commits
|
289
301
|
if @commits.empty?
|
290
|
-
|
291
|
-
|
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
|
292
321
|
end
|
293
322
|
end
|
294
323
|
@commits
|
@@ -303,7 +332,15 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
303
332
|
|
304
333
|
# @return [String] Default branch of the repo
|
305
334
|
def default_branch
|
306
|
-
@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
|
307
344
|
end
|
308
345
|
|
309
346
|
# Fetch all SHAs occurring in or before a given tag and add them to
|
@@ -311,34 +348,40 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
311
348
|
#
|
312
349
|
# @param [Array] tags The array of tags.
|
313
350
|
# @return [Nil] No return; tags are updated in-place.
|
314
|
-
def
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
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"])
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
private
|
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])
|
327
377
|
end
|
328
378
|
end
|
329
|
-
threads.each(&:join)
|
330
|
-
threads = []
|
331
379
|
end
|
332
380
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
Helper.log.info "Fetching SHAs for tags: #{i}"
|
337
|
-
nil
|
381
|
+
@commits_in_tag_cache[sha] = shas
|
382
|
+
shas
|
338
383
|
end
|
339
384
|
|
340
|
-
private
|
341
|
-
|
342
385
|
def stringify_keys_deep(indata)
|
343
386
|
case indata
|
344
387
|
when Array
|
@@ -366,39 +409,41 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
366
409
|
# @yield [Sawyer::Resource] An OctoKit-provided response (which can be empty)
|
367
410
|
#
|
368
411
|
# @return [void]
|
369
|
-
def iterate_pages(client, method, *
|
370
|
-
|
412
|
+
def iterate_pages(client, method, *arguments, parent: nil, **options)
|
413
|
+
options = DEFAULT_REQUEST_OPTIONS.merge(options)
|
371
414
|
|
372
|
-
check_github_response { client.send(method, user_project, *
|
415
|
+
check_github_response { client.send(method, user_project, *arguments, **options) }
|
373
416
|
last_response = client.last_response.tap do |response|
|
374
417
|
raise(MovedPermanentlyError, response.data[:url]) if response.status == 301
|
375
418
|
end
|
376
419
|
|
377
420
|
yield(last_response.data)
|
378
421
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
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
|
392
439
|
end
|
393
440
|
end
|
394
441
|
|
395
442
|
# This is wrapper with rescue block
|
396
443
|
#
|
397
444
|
# @return [Object] returns exactly the same, what you put in the block, but wrap it with begin-rescue block
|
398
|
-
def check_github_response
|
399
|
-
Retriable.retriable(retry_options)
|
400
|
-
yield
|
401
|
-
end
|
445
|
+
def check_github_response(&block)
|
446
|
+
Retriable.retriable(retry_options, &block)
|
402
447
|
rescue MovedPermanentlyError => e
|
403
448
|
fail_with_message(e, "The repository has moved, update your configuration")
|
404
449
|
rescue Octokit::Forbidden => e
|
@@ -434,7 +479,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
434
479
|
Helper.log.warn("RETRY - #{exception.class}: '#{exception.message}'")
|
435
480
|
Helper.log.warn("#{try} tries in #{elapsed_time} seconds and #{next_interval} seconds until the next try")
|
436
481
|
Helper.log.warn GH_RATE_LIMIT_EXCEEDED_MSG
|
437
|
-
Helper.log.warn
|
482
|
+
Helper.log.warn(client.rate_limit)
|
438
483
|
end
|
439
484
|
end
|
440
485
|
|
@@ -446,7 +491,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'"
|
|
446
491
|
#
|
447
492
|
# @param [String] log_string
|
448
493
|
def print_in_same_line(log_string)
|
449
|
-
print log_string
|
494
|
+
print "#{log_string}\r"
|
450
495
|
end
|
451
496
|
|
452
497
|
# Print long line with spaces on same line to clear prev message
|