dependabot-common 0.95.1 → 0.95.2

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot.rb +4 -0
  3. data/lib/dependabot/clients/bitbucket.rb +105 -0
  4. data/lib/dependabot/clients/github_with_retries.rb +121 -0
  5. data/lib/dependabot/clients/gitlab.rb +72 -0
  6. data/lib/dependabot/dependency.rb +115 -0
  7. data/lib/dependabot/dependency_file.rb +60 -0
  8. data/lib/dependabot/errors.rb +179 -0
  9. data/lib/dependabot/file_fetchers.rb +18 -0
  10. data/lib/dependabot/file_fetchers/README.md +65 -0
  11. data/lib/dependabot/file_fetchers/base.rb +368 -0
  12. data/lib/dependabot/file_parsers.rb +18 -0
  13. data/lib/dependabot/file_parsers/README.md +45 -0
  14. data/lib/dependabot/file_parsers/base.rb +31 -0
  15. data/lib/dependabot/file_parsers/base/dependency_set.rb +77 -0
  16. data/lib/dependabot/file_updaters.rb +18 -0
  17. data/lib/dependabot/file_updaters/README.md +58 -0
  18. data/lib/dependabot/file_updaters/base.rb +52 -0
  19. data/lib/dependabot/git_commit_checker.rb +412 -0
  20. data/lib/dependabot/metadata_finders.rb +18 -0
  21. data/lib/dependabot/metadata_finders/README.md +53 -0
  22. data/lib/dependabot/metadata_finders/base.rb +117 -0
  23. data/lib/dependabot/metadata_finders/base/changelog_finder.rb +321 -0
  24. data/lib/dependabot/metadata_finders/base/changelog_pruner.rb +177 -0
  25. data/lib/dependabot/metadata_finders/base/commits_finder.rb +221 -0
  26. data/lib/dependabot/metadata_finders/base/release_finder.rb +255 -0
  27. data/lib/dependabot/pull_request_creator.rb +155 -0
  28. data/lib/dependabot/pull_request_creator/branch_namer.rb +170 -0
  29. data/lib/dependabot/pull_request_creator/commit_signer.rb +63 -0
  30. data/lib/dependabot/pull_request_creator/github.rb +277 -0
  31. data/lib/dependabot/pull_request_creator/gitlab.rb +162 -0
  32. data/lib/dependabot/pull_request_creator/labeler.rb +373 -0
  33. data/lib/dependabot/pull_request_creator/message_builder.rb +906 -0
  34. data/lib/dependabot/pull_request_updater.rb +43 -0
  35. data/lib/dependabot/pull_request_updater/github.rb +165 -0
  36. data/lib/dependabot/shared_helpers.rb +224 -0
  37. data/lib/dependabot/source.rb +120 -0
  38. data/lib/dependabot/update_checkers.rb +18 -0
  39. data/lib/dependabot/update_checkers/README.md +67 -0
  40. data/lib/dependabot/update_checkers/base.rb +220 -0
  41. data/lib/dependabot/utils.rb +33 -0
  42. data/lib/dependabot/version.rb +5 -0
  43. data/lib/rubygems_version_patch.rb +14 -0
  44. metadata +44 -2
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dependabot
4
+ module MetadataFinders
5
+ @metadata_finders = {}
6
+
7
+ def self.for_package_manager(package_manager)
8
+ metadata_finder = @metadata_finders[package_manager]
9
+ return metadata_finder if metadata_finder
10
+
11
+ raise "Unsupported package_manager #{package_manager}"
12
+ end
13
+
14
+ def self.register(package_manager, metadata_finder)
15
+ @metadata_finders[package_manager] = metadata_finder
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,53 @@
1
+ # Metadata finders
2
+
3
+ Metadata finders look up metadata about a dependency, such as its GitHub URL.
4
+
5
+ There is a `Dependabot::MetadataFinders` class for each language Dependabot
6
+ supports.
7
+
8
+ ## Public API
9
+
10
+ Each `Dependabot::MetadataFinders` class exposes the following methods:
11
+
12
+ | Method | Description |
13
+ |-----------------------|---------------------------------------------------------------------------------------------|
14
+ | `#source_url` | A link to the source data for the dependency. |
15
+ | `#homepage_url` | A link to the homepage for the dependency. |
16
+ | `#commits_url` | A link to a commit diff between the previous version of the dependency and the new version. |
17
+ | `#commits` | A list of commits between the previous version of the dependency and the new version. |
18
+ | `#changelog_url` | A link to the changelog for the dependency. |
19
+ | `#changelog_text` | The relevant text from the changelog. |
20
+ | `#release_url` | A link to the release notes for this version of the dependency. |
21
+ | `#release_text` | The relevant text from the release notes |
22
+ | `#upgrade_guide_url` | A link to the upgrade guide for this upgrade (if it exists). |
23
+ | `#upgrade_guide_text` | The text of the upgrade guide for this upgrade (if it exists). |
24
+
25
+ An integration might look as follows:
26
+
27
+ ```ruby
28
+ require 'dependabot/metadata_finders'
29
+
30
+ dependency = update_checker.updated_dependency
31
+
32
+ metadata_finder_class = Dependabot::MetadataFinders::Ruby::Bundler
33
+ metadata_finder = metadata_finder_class.new(
34
+ dependency: dependency,
35
+ credentials: credentials
36
+ )
37
+
38
+ puts "Changelog for #{dependency.name} is at #{metadata_finder.changelog_url}"
39
+ ```
40
+
41
+ ## Writing a metadata finder for a new language
42
+
43
+ All new metadata finders should inherit from `Dependabot::MetadataFinders::Base`
44
+ and implement the following methods:
45
+
46
+ | Method | Description |
47
+ |------------------------|-------------------------|
48
+ | `#look_up_source` | Private method that returns a `Dependabot::Source` object. Generally the source details are extracted from a source code URL provided by the language's dependency registry, but sometimes it's already know from parsing the dependency file. |
49
+
50
+ To ensure the above are implemented, you should include
51
+ `it_behaves_like "a dependency metadata finder"` in your specs for the new
52
+ metadata finder.
53
+
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/source"
4
+
5
+ module Dependabot
6
+ module MetadataFinders
7
+ class Base
8
+ require "dependabot/metadata_finders/base/changelog_finder"
9
+ require "dependabot/metadata_finders/base/release_finder"
10
+ require "dependabot/metadata_finders/base/commits_finder"
11
+
12
+ attr_reader :dependency, :credentials
13
+
14
+ def initialize(dependency:, credentials:)
15
+ @dependency = dependency
16
+ @credentials = credentials
17
+ end
18
+
19
+ def source_url
20
+ source&.url
21
+ end
22
+
23
+ def homepage_url
24
+ source_url
25
+ end
26
+
27
+ def changelog_url
28
+ @changelog_finder ||= ChangelogFinder.new(
29
+ dependency: dependency,
30
+ source: source,
31
+ credentials: credentials
32
+ )
33
+ @changelog_finder.changelog_url
34
+ end
35
+
36
+ def changelog_text
37
+ @changelog_finder ||= ChangelogFinder.new(
38
+ dependency: dependency,
39
+ source: source,
40
+ credentials: credentials
41
+ )
42
+ @changelog_finder.changelog_text
43
+ end
44
+
45
+ def upgrade_guide_url
46
+ @changelog_finder ||= ChangelogFinder.new(
47
+ dependency: dependency,
48
+ source: source,
49
+ credentials: credentials
50
+ )
51
+ @changelog_finder.upgrade_guide_url
52
+ end
53
+
54
+ def upgrade_guide_text
55
+ @changelog_finder ||= ChangelogFinder.new(
56
+ dependency: dependency,
57
+ source: source,
58
+ credentials: credentials
59
+ )
60
+ @changelog_finder.upgrade_guide_text
61
+ end
62
+
63
+ def releases_url
64
+ @release_finder ||= ReleaseFinder.new(
65
+ dependency: dependency,
66
+ source: source,
67
+ credentials: credentials
68
+ )
69
+ @release_finder.releases_url
70
+ end
71
+
72
+ def releases_text
73
+ @release_finder ||= ReleaseFinder.new(
74
+ dependency: dependency,
75
+ source: source,
76
+ credentials: credentials
77
+ )
78
+ @release_finder.releases_text
79
+ end
80
+
81
+ def commits_url
82
+ @commits_finder ||= CommitsFinder.new(
83
+ dependency: dependency,
84
+ source: source,
85
+ credentials: credentials
86
+ )
87
+ @commits_finder.commits_url
88
+ end
89
+
90
+ def commits
91
+ @commits_finder ||= CommitsFinder.new(
92
+ dependency: dependency,
93
+ source: source,
94
+ credentials: credentials
95
+ )
96
+ @commits_finder.commits
97
+ end
98
+
99
+ def maintainer_changes
100
+ nil
101
+ end
102
+
103
+ private
104
+
105
+ def source
106
+ return @source if @source_lookup_attempted
107
+
108
+ @source_lookup_attempted = true
109
+ @source = look_up_source
110
+ end
111
+
112
+ def look_up_source
113
+ raise NotImplementedError
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+
5
+ require "dependabot/clients/github_with_retries"
6
+ require "dependabot/clients/gitlab"
7
+ require "dependabot/clients/bitbucket"
8
+ require "dependabot/shared_helpers"
9
+ require "dependabot/metadata_finders/base"
10
+
11
+ module Dependabot
12
+ module MetadataFinders
13
+ class Base
14
+ class ChangelogFinder
15
+ require_relative "changelog_pruner"
16
+ require_relative "commits_finder"
17
+
18
+ # Earlier entries are preferred
19
+ CHANGELOG_NAMES = %w(changelog history news changes release).freeze
20
+
21
+ attr_reader :source, :dependency, :credentials
22
+
23
+ def initialize(source:, dependency:, credentials:)
24
+ @source = source
25
+ @dependency = dependency
26
+ @credentials = credentials
27
+ end
28
+
29
+ def changelog_url
30
+ changelog&.html_url
31
+ end
32
+
33
+ def changelog_text
34
+ return unless full_changelog_text
35
+
36
+ ChangelogPruner.new(
37
+ dependency: dependency,
38
+ changelog_text: full_changelog_text
39
+ ).pruned_text
40
+ end
41
+
42
+ def upgrade_guide_url
43
+ upgrade_guide&.html_url
44
+ end
45
+
46
+ def upgrade_guide_text
47
+ return unless upgrade_guide
48
+
49
+ @upgrade_guide_text ||= fetch_file_text(upgrade_guide)
50
+ end
51
+
52
+ private
53
+
54
+ # rubocop:disable Metrics/CyclomaticComplexity
55
+ # rubocop:disable Metrics/PerceivedComplexity
56
+ def changelog
57
+ return unless source
58
+
59
+ # Changelog won't be relevant for a git commit bump
60
+ return if git_source? && !ref_changed?
61
+
62
+ # If there is a changelog, and it includes the new version, return it
63
+ if new_version && default_branch_changelog &&
64
+ fetch_file_text(default_branch_changelog)&.include?(new_version)
65
+ return default_branch_changelog
66
+ end
67
+
68
+ # Otherwise, look for a changelog at the tag for this version
69
+ if new_version && relevant_tag_changelog &&
70
+ fetch_file_text(relevant_tag_changelog)&.include?(new_version)
71
+ return relevant_tag_changelog
72
+ end
73
+
74
+ # Fall back to the changelog (or nil) from the default branch
75
+ default_branch_changelog
76
+ end
77
+ # rubocop:enable Metrics/CyclomaticComplexity
78
+ # rubocop:enable Metrics/PerceivedComplexity
79
+
80
+ def default_branch_changelog
81
+ return unless source
82
+
83
+ @default_branch_changelog ||= changelog_from_ref(nil)
84
+ end
85
+
86
+ def relevant_tag_changelog
87
+ return unless source
88
+ return unless tag_for_new_version
89
+
90
+ @relevant_tag_changelog ||= changelog_from_ref(tag_for_new_version)
91
+ end
92
+
93
+ def changelog_from_ref(ref)
94
+ files =
95
+ dependency_file_list(ref).
96
+ select { |f| f.type == "file" }.
97
+ reject { |f| f.name.end_with?(".sh") }.
98
+ reject { |f| f.size > 1_000_000 }
99
+
100
+ CHANGELOG_NAMES.each do |name|
101
+ candidates = files.select { |f| f.name =~ /#{name}/i }
102
+ file = candidates.first if candidates.one?
103
+ file ||=
104
+ candidates.find do |f|
105
+ candidates -= [f] && next if fetch_file_text(f).nil?
106
+ ChangelogPruner.new(
107
+ dependency: dependency,
108
+ changelog_text: fetch_file_text(f)
109
+ ).includes_new_version?
110
+ end
111
+ file ||= candidates.max_by(&:size)
112
+ return file if file
113
+ end
114
+
115
+ nil
116
+ end
117
+
118
+ def tag_for_new_version
119
+ @tag_for_new_version ||=
120
+ CommitsFinder.new(
121
+ dependency: dependency,
122
+ source: source,
123
+ credentials: credentials
124
+ ).new_tag
125
+ end
126
+
127
+ def full_changelog_text
128
+ return unless changelog
129
+
130
+ fetch_file_text(changelog)
131
+ end
132
+
133
+ def fetch_file_text(file)
134
+ @file_text ||= {}
135
+
136
+ unless @file_text.key?(file.download_url)
137
+ @file_text[file.download_url] =
138
+ case source.provider
139
+ when "github" then fetch_github_file(file)
140
+ when "gitlab" then fetch_gitlab_file(file)
141
+ when "bitbucket" then fetch_bitbucket_file(file)
142
+ else raise "Unsupported provider '#{source.provider}"
143
+ end
144
+ end
145
+
146
+ return unless @file_text[file.download_url].valid_encoding?
147
+
148
+ @file_text[file.download_url].
149
+ force_encoding("UTF-8").
150
+ encode.sub(/\n*\z/, "")
151
+ end
152
+
153
+ def fetch_github_file(file)
154
+ # Hitting the download URL directly causes encoding problems
155
+ raw_content = github_client.get(file.url).content
156
+ Base64.decode64(raw_content).force_encoding("UTF-8").encode
157
+ end
158
+
159
+ def fetch_gitlab_file(file)
160
+ Excon.get(
161
+ file.download_url,
162
+ idempotent: true,
163
+ **SharedHelpers.excon_defaults
164
+ ).body
165
+ end
166
+
167
+ def fetch_bitbucket_file(file)
168
+ bitbucket_client.get(file.download_url).body
169
+ end
170
+
171
+ def upgrade_guide
172
+ return unless source
173
+
174
+ # Upgrade guide usually won't be relevant for bumping anything other
175
+ # than the major version
176
+ return unless major_version_upgrade?
177
+
178
+ dependency_file_list.
179
+ select { |f| f.type == "file" }.
180
+ select { |f| f.name.casecmp("upgrade.md").zero? }.
181
+ reject { |f| f.size > 1_000_000 }.
182
+ max_by(&:size)
183
+ end
184
+
185
+ def dependency_file_list(ref = nil)
186
+ @dependency_file_list ||= {}
187
+ @dependency_file_list[ref] ||= fetch_dependency_file_list(ref)
188
+ end
189
+
190
+ def fetch_dependency_file_list(ref)
191
+ case source.provider
192
+ when "github" then fetch_github_file_list(ref)
193
+ when "bitbucket" then fetch_bitbucket_file_list
194
+ when "gitlab" then fetch_gitlab_file_list
195
+ when "azure" then [] # TODO: Fetch files from Azure
196
+ else raise "Unexpected repo provider '#{source.provider}'"
197
+ end
198
+ end
199
+
200
+ def fetch_github_file_list(ref)
201
+ files = []
202
+
203
+ if source.directory
204
+ opts = { path: source.directory, ref: ref }.compact
205
+ tmp_files = github_client.contents(source.repo, opts)
206
+ files += tmp_files if tmp_files.is_a?(Array)
207
+ end
208
+
209
+ opts = { ref: ref }.compact
210
+ files += github_client.contents(source.repo, opts)
211
+
212
+ files.uniq.each do |f|
213
+ next unless %w(doc docs).include?(f.name) && f.type == "dir"
214
+
215
+ opts = { path: f.path, ref: ref }.compact
216
+ files += github_client.contents(source.repo, opts)
217
+ end
218
+
219
+ files
220
+ rescue Octokit::NotFound
221
+ []
222
+ end
223
+
224
+ def fetch_bitbucket_file_list
225
+ branch = default_bitbucket_branch
226
+ bitbucket_client.fetch_repo_contents(source.repo).map do |file|
227
+ OpenStruct.new(
228
+ name: file.fetch("path").split("/").last,
229
+ type: file.fetch("type") == "commit_file" ? "file" : file["type"],
230
+ size: file.fetch("size", 0),
231
+ html_url: "#{source.url}/src/#{branch}/#{file['path']}",
232
+ download_url: "#{source.url}/raw/#{branch}/#{file['path']}"
233
+ )
234
+ end
235
+ rescue Dependabot::Clients::Bitbucket::NotFound,
236
+ Dependabot::Clients::Bitbucket::Unauthorized,
237
+ Dependabot::Clients::Bitbucket::Forbidden
238
+ []
239
+ end
240
+
241
+ def fetch_gitlab_file_list
242
+ gitlab_client.repo_tree(source.repo).map do |file|
243
+ OpenStruct.new(
244
+ name: file.name,
245
+ type: file.type == "blob" ? "file" : file.type,
246
+ size: 0, # GitLab doesn't return file size
247
+ html_url: "#{source.url}/blob/master/#{file.path}",
248
+ download_url: "#{source.url}/raw/master/#{file.path}"
249
+ )
250
+ end
251
+ rescue Gitlab::Error::NotFound
252
+ []
253
+ end
254
+
255
+ def new_version
256
+ @new_version ||= git_source? ? new_ref : dependency.version
257
+ @new_version&.gsub(/^v/, "")
258
+ end
259
+
260
+ def previous_ref
261
+ dependency.previous_requirements.map do |r|
262
+ r.dig(:source, "ref") || r.dig(:source, :ref)
263
+ end.compact.first
264
+ end
265
+
266
+ def new_ref
267
+ dependency.requirements.map do |r|
268
+ r.dig(:source, "ref") || r.dig(:source, :ref)
269
+ end.compact.first
270
+ end
271
+
272
+ def ref_changed?
273
+ previous_ref && new_ref && previous_ref != new_ref
274
+ end
275
+
276
+ # TODO: Refactor me so that Composer doesn't need to be special cased
277
+ def git_source?
278
+ # Special case Composer, which uses git as a source but handles tags
279
+ # internally
280
+ return false if dependency.package_manager == "composer"
281
+
282
+ requirements = dependency.requirements
283
+ sources = requirements.map { |r| r.fetch(:source) }.uniq.compact
284
+ return false if sources.empty?
285
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
286
+
287
+ source_type = sources.first[:type] || sources.first.fetch("type")
288
+ source_type == "git"
289
+ end
290
+
291
+ def major_version_upgrade?
292
+ return false unless dependency.version&.match?(/^\d/)
293
+ return false unless dependency.previous_version&.match?(/^\d/)
294
+
295
+ dependency.version.split(".").first.to_i -
296
+ dependency.previous_version.split(".").first.to_i >= 1
297
+ end
298
+
299
+ def gitlab_client
300
+ @gitlab_client ||= Dependabot::Clients::Gitlab.
301
+ for_gitlab_dot_com(credentials: credentials)
302
+ end
303
+
304
+ def github_client
305
+ @github_client ||= Dependabot::Clients::GithubWithRetries.
306
+ for_github_dot_com(credentials: credentials)
307
+ end
308
+
309
+ def bitbucket_client
310
+ @bitbucket_client ||= Dependabot::Clients::Bitbucket.
311
+ for_bitbucket_dot_org(credentials: credentials)
312
+ end
313
+
314
+ def default_bitbucket_branch
315
+ @default_bitbucket_branch ||=
316
+ bitbucket_client.fetch_default_branch(source.repo)
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end