-
Notifications
You must be signed in to change notification settings - Fork 915
/
git_commit_checker.rb
373 lines (294 loc) 路 10.7 KB
/
git_commit_checker.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# frozen_string_literal: true
require "excon"
require "gitlab"
require "dependabot/clients/github_with_retries"
require "dependabot/clients/gitlab_with_retries"
require "dependabot/clients/bitbucket_with_retries"
require "dependabot/metadata_finders"
require "dependabot/errors"
require "dependabot/utils"
require "dependabot/source"
require "dependabot/dependency"
require "dependabot/git_metadata_fetcher"
module Dependabot
class GitCommitChecker
VERSION_REGEX = /
(?<version>
(?<=^v)[0-9]+(?:\-[a-z0-9]+)?
|
[0-9]+\.[0-9]+(?:\.[a-z0-9\-]+)*
)$
/ix.freeze
def initialize(dependency:, credentials:,
ignored_versions: [], raise_on_ignored: false,
requirement_class: nil, version_class: nil)
@dependency = dependency
@credentials = credentials
@ignored_versions = ignored_versions
@raise_on_ignored = raise_on_ignored
@requirement_class = requirement_class
@version_class = version_class
end
def git_dependency?
return false if dependency_source_details.nil?
dependency_source_details.fetch(:type) == "git"
end
def pinned?
raise "Not a git dependency!" unless git_dependency?
ref = dependency_source_details.fetch(:ref)
branch = dependency_source_details.fetch(:branch)
return false if ref.nil?
return false if branch == ref
return true if branch
return true if dependency.version&.start_with?(ref)
# Check the specified `ref` isn't actually a branch
!local_upload_pack.match?("refs/heads/#{ref}")
end
def pinned_ref_looks_like_version?
return false unless pinned?
dependency_source_details.fetch(:ref).match?(VERSION_REGEX)
end
def pinned_ref_looks_like_commit_sha?
return false unless pinned?
ref = dependency_source_details.fetch(:ref)
return false unless ref.match?(/^[0-9a-f]{6,40}$/)
local_repo_git_metadata_fetcher.head_commit_for_ref(ref).nil?
end
def branch_or_ref_in_release?(version)
pinned_ref_in_release?(version) || branch_behind_release?(version)
end
def head_commit_for_current_branch
ref = ref_or_branch || "HEAD"
if pinned?
return dependency.version ||
local_repo_git_metadata_fetcher.head_commit_for_ref(ref)
end
sha = local_repo_git_metadata_fetcher.head_commit_for_ref(ref)
return sha if sha
raise Dependabot::GitDependencyReferenceNotFound, dependency.name
end
# rubocop:disable Metrics/PerceivedComplexity
def local_tag_for_latest_version
tags =
local_tags.
select { |t| version_tag?(t.name) && matches_existing_prefix?(t.name) }
filtered = tags.
reject { |t| tag_included_in_ignore_reqs?(t) }
if @raise_on_ignored && tags.any? && filtered.empty?
raise Dependabot::AllVersionsIgnored
end
tag = filtered.
reject { |t| tag_is_prerelease?(t) && !wants_prerelease? }.
max_by do |t|
version = t.name.match(VERSION_REGEX).named_captures.
fetch("version")
version_class.new(version)
end
return unless tag
version = tag.name.match(VERSION_REGEX).named_captures.fetch("version")
{
tag: tag.name,
version: version_class.new(version),
commit_sha: tag.commit_sha,
tag_sha: tag.tag_sha
}
end
# rubocop:enable Metrics/PerceivedComplexity
def git_repo_reachable?
local_upload_pack
true
rescue Dependabot::GitDependenciesNotReachable
false
end
private
attr_reader :dependency, :credentials, :ignored_versions
def pinned_ref_in_release?(version)
raise "Not a git dependency!" unless git_dependency?
return false unless pinned?
return false if listing_source_url.nil?
tag = listing_tag_for_version(version.to_s)
return false unless tag
commit_included_in_tag?(
commit: dependency_source_details.fetch(:ref),
tag: tag,
allow_identical: true
)
end
def branch_behind_release?(version)
raise "Not a git dependency!" unless git_dependency?
return false if ref_or_branch.nil?
return false if listing_source_url.nil?
tag = listing_tag_for_version(version.to_s)
return false unless tag
# Check if behind, excluding the case where it's identical, because
# we normally wouldn't switch you from tracking master to a release.
commit_included_in_tag?(
commit: ref_or_branch,
tag: tag,
allow_identical: false
)
end
def local_upload_pack
local_repo_git_metadata_fetcher.upload_pack
end
def local_tags
tags = local_repo_git_metadata_fetcher.tags
if dependency_source_details&.fetch(:ref, nil)&.start_with?("tags/")
tags = tags.map do |tag|
tag.dup.tap { |t| t.name = "tags/#{tag.name}" }
end
end
tags
end
def commit_included_in_tag?(tag:, commit:, allow_identical: false)
status =
case Source.from_url(listing_source_url)&.provider
when "github" then github_commit_comparison_status(tag, commit)
when "gitlab" then gitlab_commit_comparison_status(tag, commit)
when "bitbucket" then bitbucket_commit_comparison_status(tag, commit)
else raise "Unknown source"
end
return true if status == "behind"
allow_identical && status == "identical"
rescue Octokit::NotFound, Gitlab::Error::NotFound,
Clients::Bitbucket::NotFound,
Octokit::InternalServerError
false
end
def github_commit_comparison_status(ref1, ref2)
client = Clients::GithubWithRetries.
for_github_dot_com(credentials: credentials)
client.compare(listing_source_repo, ref1, ref2).status
end
def gitlab_commit_comparison_status(ref1, ref2)
client = Clients::GitlabWithRetries.
for_gitlab_dot_com(credentials: credentials)
comparison = client.compare(listing_source_repo, ref1, ref2)
if comparison.commits.none? then "behind"
elsif comparison.compare_same_ref then "identical"
else "ahead"
end
end
def bitbucket_commit_comparison_status(ref1, ref2)
url = "https://api.bitbucket.org/2.0/repositories/"\
"#{listing_source_repo}/commits/?"\
"include=#{ref2}&exclude=#{ref1}"
client = Clients::BitbucketWithRetries.
for_bitbucket_dot_org(credentials: credentials)
response = client.get(url)
# Conservatively assume that ref2 is ahead in the equality case, of
# if we get an unexpected format (e.g., due to a 404)
if JSON.parse(response.body).fetch("values", ["x"]).none? then "behind"
else "ahead"
end
end
def dependency_source_details
sources =
dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
return sources.first if sources.count <= 1
# If there are multiple source types, or multiple source URLs, then it's
# unclear how we should proceed
if sources.map { |s| [s.fetch(:type), s.fetch(:url, nil)] }.uniq.count > 1
raise "Multiple sources! #{sources.join(', ')}"
end
# Otherwise it's reasonable to take the first source and use that. This
# will happen if we have multiple git sources with difference references
# specified. In that case it's fine to update them all.
sources.first
end
def ref_or_branch
dependency_source_details.fetch(:ref) ||
dependency_source_details.fetch(:branch)
end
def version_tag?(tag)
tag.match?(VERSION_REGEX)
end
def matches_existing_prefix?(tag)
return true unless ref_or_branch&.match?(VERSION_REGEX)
ref_or_branch.gsub(VERSION_REGEX, "").gsub(/v$/i, "") ==
tag.gsub(VERSION_REGEX, "").gsub(/v$/i, "")
end
def listing_source_url
@listing_source_url ||=
begin
# Remove the git source, so the metadata finder looks on the
# registry
candidate_dep = Dependency.new(
name: dependency.name,
version: dependency.version,
requirements: [],
package_manager: dependency.package_manager
)
MetadataFinders.
for_package_manager(dependency.package_manager).
new(dependency: candidate_dep, credentials: credentials).
source_url
end
end
def listing_source_repo
return unless listing_source_url
Source.from_url(listing_source_url)&.repo
end
def listing_tag_for_version(version)
listing_tags.
find { |t| t.name =~ /(?:[^0-9\.]|\A)#{Regexp.escape(version)}\z/ }&.
name
end
def listing_tags
return [] unless listing_source_url
tags = listing_repo_git_metadata_fetcher.tags
if dependency_source_details&.fetch(:ref, nil)&.start_with?("tags/")
tags = tags.map do |tag|
tag.dup.tap { |t| t.name = "tags/#{tag.name}" }
end
end
tags
rescue GitDependenciesNotReachable
[]
end
def listing_upload_pack
return unless listing_source_url
listing_repo_git_metadata_fetcher.upload_pack
end
def ignore_reqs
ignored_versions.map { |req| requirement_class.new(req.split(",")) }
end
def wants_prerelease?
return false unless dependency_source_details&.fetch(:ref, nil)
return false unless pinned_ref_looks_like_version?
version = dependency_source_details.fetch(:ref).match(VERSION_REGEX).
named_captures.fetch("version")
version_class.new(version).prerelease?
end
def tag_included_in_ignore_reqs?(tag)
version = tag.name.match(VERSION_REGEX).named_captures.fetch("version")
ignore_reqs.any? { |r| r.satisfied_by?(version_class.new(version)) }
end
def tag_is_prerelease?(tag)
version = tag.name.match(VERSION_REGEX).named_captures.fetch("version")
version_class.new(version).prerelease?
end
def version_class
return @version_class if @version_class
Utils.version_class_for_package_manager(dependency.package_manager)
end
def requirement_class
return @requirement_class if @requirement_class
Utils.requirement_class_for_package_manager(dependency.package_manager)
end
def local_repo_git_metadata_fetcher
@local_repo_git_metadata_fetcher ||=
GitMetadataFetcher.new(
url: dependency_source_details.fetch(:url),
credentials: credentials
)
end
def listing_repo_git_metadata_fetcher
@listing_repo_git_metadata_fetcher ||=
GitMetadataFetcher.new(
url: listing_source_url,
credentials: credentials
)
end
end
end