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 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