github_changelog_generator 1.15.2 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96d97ec2074cb54d75216a93b42f41a6216a418c1b0e926ab55a0cb9cb32a09b
4
- data.tar.gz: 75faf09151502f8f2d3cd4a15da37b26648ed504d42e3829ad95d1d7c71a9268
3
+ metadata.gz: cb493bfacf7e5ec521d56e8ac70078b9f2297e88707aaa1adc267fa1f2caa829
4
+ data.tar.gz: b5368080a19d7b90f36087b1601cffed3034b4e0562e0c57b9bbe519a683cdee
5
5
  SHA512:
6
- metadata.gz: 9f6adfd28ffcb632837a9d037b0677f6527b03d6a8c787ab9f65b2981b740e1bd478d0f0b64b76f8fab93d00d101b9b5fb80cb86ba7d722c98b0728534485cfc
7
- data.tar.gz: 81ee9e88cd5efa0c8acc60ce9f2a1571ea72fb9d9b096af07236477ff560e7561bd3eef31a3c9b26a5bdd6edba2656fe18b32509a7fbe8e40325f4974a94651a
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 github_username -p github_project
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.haxx.se/ca/cacert.pem && popd`
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
- @log = log
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.fetch_tag_shas_async(tags)
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
- commits_in_branch = @fetcher.fetch_compare(@fetcher.oldest_commit["sha"], branch)
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
- tag_in_range_new = if newer_tag_time.nil?
134
- true
135
- else
136
- time <= newer_tag_time
137
- end
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
- tag_in_range_old = if older_tag_time.nil?
143
- true
144
- else
145
- time > older_tag_time
146
- end
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
- filtered_issues = filter_wo_labels(filtered_issues)
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(issues)
163
- if (!issues.empty? && issues.first.key?("pull_requests") && options[:add_pr_wo_labels]) || options[:add_issues_wo_labels]
164
- issues
165
- else
166
- issues.select { |issue| issue["labels"].map { |l| l["name"] }.any? }
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 = filter_excluded_tags(all_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
- attr_accessor :name, :prefix, :issues, :labels, :body_only
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] Generate section content
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 begining and end of the string
80
+ # remove spaces from beginning of the string
64
81
  body_paragraph.rstrip!
65
82
  # encapsulate to md
66
- encapsulated_body = "\s\s\n" + encapsulate_string(body_paragraph)
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/).first
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
- string = string.gsub(char, "\\#{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}")
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(STDOUT)
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 = 100
13
- MAX_THREAD_NUMBER = 10
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
- @compares = {}
38
- prepare_cache
39
- configure_octokit_ssl
40
- @client = Octokit::Client.new(github_options)
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 prepare_cache
44
- return unless @http_cache
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
- @cache_file = @options.fetch(:cache_file) { File.join(Dir.tmpdir, "github-changelog-http-cache") }
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 github_options
52
- result = {}
53
- github_token = fetch_github_token
54
- result[:access_token] = github_token if github_token
55
- endpoint = @options[:github_endpoint]
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
- def configure_octokit_ssl
61
- ca_file = @options[:ssl_ca_file] || ENV["SSL_CA_FILE"] || File.expand_path("ssl_certs/cacert.pem", __dir__)
62
- Octokit.connection_options = { ssl: { ca_file: ca_file } }
63
- end
79
+ if (github_token = fetch_github_token)
80
+ options[:access_token] = github_token
81
+ end
64
82
 
65
- def init_cache
66
- Octokit.middleware = Faraday::RackBuilder.new do |builder|
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 = 0
112
- count_pages = calculate_pages(@client, "tags", {})
127
+ tags = []
128
+ page_i = 0
129
+ count_pages = calculate_pages(client, "tags", {})
113
130
 
114
- iterate_pages(@client, "tags") do |new_tags|
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(@client, "issues", closed_pr_options)
162
+ count_pages = calculate_pages(client, "issues", closed_pr_options)
146
163
 
147
- iterate_pages(@client, "issues", closed_pr_options) do |new_issues|
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 = 0
169
- count_pages = calculate_pages(@client, "pull_requests", options)
185
+ page_i = 0
186
+ count_pages = calculate_pages(client, "pull_requests", options)
170
187
 
171
- iterate_pages(@client, "pull_requests", options) do |new_pr|
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 = 0
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
- issues.each_slice(MAX_THREAD_NUMBER) do |issues_slice|
194
- issues_slice.each do |issue|
195
- threads << Thread.new do
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(@client, "issue_events", issue["number"], preview) do |new_event|
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
- # to clear line from prev print
210
- print_empty_line
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
- threads = []
241
+ barrier = Async::Barrier.new
242
+ semaphore = Async::Semaphore.new(MAXIMUM_CONNECTIONS, parent: barrier)
221
243
 
222
- prs.each_slice(MAX_THREAD_NUMBER) do |prs_slice|
223
- prs_slice.each do |pr|
224
- threads << Thread.new do
244
+ Sync do
245
+ client = self.client
246
+
247
+ prs.each do |pr|
248
+ semaphore.async do
225
249
  pr["comments"] = []
226
- iterate_pages(@client, "issue_comments", pr["number"]) do |new_comment|
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
- threads.each(&:join)
233
- threads = []
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 = @client.commit(user_project, commit_id)
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
- iterate_pages(@client, "commits") do |new_commits|
291
- @commits.concat(new_commits)
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 ||= @client.repository(user_project)[: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 fetch_tag_shas_async(tags)
315
- i = 0
316
- threads = []
317
- print_in_same_line("Fetching SHAs for tags: #{i}/#{tags.count}\r") if @options[:verbose]
318
-
319
- tags.each_slice(MAX_THREAD_NUMBER) do |tags_slice|
320
- tags_slice.each do |tag|
321
- threads << Thread.new do
322
- # Use oldest commit because comparing two arbitrary tags may be diverged
323
- commits_in_tag = fetch_compare(oldest_commit["sha"], tag["name"])
324
- tag["shas_in_tag"] = commits_in_tag["commits"].collect { |commit| commit["sha"] }
325
- print_in_same_line("Fetching SHAs for tags: #{i + 1}/#{tags.count}") if @options[:verbose]
326
- i += 1
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
- # to clear line from prev print
334
- print_empty_line
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, *args)
370
- args << DEFAULT_REQUEST_OPTIONS.merge(extract_request_args(args))
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, *args) }
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
- until (next_one = last_response.rels[:next]).nil?
380
- last_response = check_github_response { next_one.get }
381
- yield(last_response.data)
382
- end
383
- end
384
-
385
- def extract_request_args(args)
386
- if args.size == 1 && args.first.is_a?(Hash)
387
- args.delete_at(0)
388
- elsif args.size > 1 && args.last.is_a?(Hash)
389
- args.delete_at(args.length - 1)
390
- else
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) do
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 @client.rate_limit
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 + "\r"
494
+ print "#{log_string}\r"
450
495
  end
451
496
 
452
497
  # Print long line with spaces on same line to clear prev message