dependabot-common 0.95.1 → 0.95.2

Sign up to get free protection for your applications and to get access to all the features.
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