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.
- checksums.yaml +4 -4
- data/lib/dependabot.rb +4 -0
- data/lib/dependabot/clients/bitbucket.rb +105 -0
- data/lib/dependabot/clients/github_with_retries.rb +121 -0
- data/lib/dependabot/clients/gitlab.rb +72 -0
- data/lib/dependabot/dependency.rb +115 -0
- data/lib/dependabot/dependency_file.rb +60 -0
- data/lib/dependabot/errors.rb +179 -0
- data/lib/dependabot/file_fetchers.rb +18 -0
- data/lib/dependabot/file_fetchers/README.md +65 -0
- data/lib/dependabot/file_fetchers/base.rb +368 -0
- data/lib/dependabot/file_parsers.rb +18 -0
- data/lib/dependabot/file_parsers/README.md +45 -0
- data/lib/dependabot/file_parsers/base.rb +31 -0
- data/lib/dependabot/file_parsers/base/dependency_set.rb +77 -0
- data/lib/dependabot/file_updaters.rb +18 -0
- data/lib/dependabot/file_updaters/README.md +58 -0
- data/lib/dependabot/file_updaters/base.rb +52 -0
- data/lib/dependabot/git_commit_checker.rb +412 -0
- data/lib/dependabot/metadata_finders.rb +18 -0
- data/lib/dependabot/metadata_finders/README.md +53 -0
- data/lib/dependabot/metadata_finders/base.rb +117 -0
- data/lib/dependabot/metadata_finders/base/changelog_finder.rb +321 -0
- data/lib/dependabot/metadata_finders/base/changelog_pruner.rb +177 -0
- data/lib/dependabot/metadata_finders/base/commits_finder.rb +221 -0
- data/lib/dependabot/metadata_finders/base/release_finder.rb +255 -0
- data/lib/dependabot/pull_request_creator.rb +155 -0
- data/lib/dependabot/pull_request_creator/branch_namer.rb +170 -0
- data/lib/dependabot/pull_request_creator/commit_signer.rb +63 -0
- data/lib/dependabot/pull_request_creator/github.rb +277 -0
- data/lib/dependabot/pull_request_creator/gitlab.rb +162 -0
- data/lib/dependabot/pull_request_creator/labeler.rb +373 -0
- data/lib/dependabot/pull_request_creator/message_builder.rb +906 -0
- data/lib/dependabot/pull_request_updater.rb +43 -0
- data/lib/dependabot/pull_request_updater/github.rb +165 -0
- data/lib/dependabot/shared_helpers.rb +224 -0
- data/lib/dependabot/source.rb +120 -0
- data/lib/dependabot/update_checkers.rb +18 -0
- data/lib/dependabot/update_checkers/README.md +67 -0
- data/lib/dependabot/update_checkers/base.rb +220 -0
- data/lib/dependabot/utils.rb +33 -0
- data/lib/dependabot/version.rb +5 -0
- data/lib/rubygems_version_patch.rb +14 -0
- metadata +44 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dependabot
|
|
4
|
+
module FileParsers
|
|
5
|
+
@file_parsers = {}
|
|
6
|
+
|
|
7
|
+
def self.for_package_manager(package_manager)
|
|
8
|
+
file_parser = @file_parsers[package_manager]
|
|
9
|
+
return file_parser if file_parser
|
|
10
|
+
|
|
11
|
+
raise "Unsupported package_manager #{package_manager}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.register(package_manager, file_parser)
|
|
15
|
+
@file_parsers[package_manager] = file_parser
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# File parsers
|
|
2
|
+
|
|
3
|
+
File parsers take a set of dependency files and extract a list of dependencies
|
|
4
|
+
for the project.
|
|
5
|
+
|
|
6
|
+
There is a `Dependabot::FileParsers` class for each language Dependabot
|
|
7
|
+
supports.
|
|
8
|
+
|
|
9
|
+
## Public API
|
|
10
|
+
|
|
11
|
+
Each `Dependabot::FileParsers` class implements the following methods:
|
|
12
|
+
|
|
13
|
+
| Method | Description |
|
|
14
|
+
|---------------------|-----------------------------------------------------------------------------------------------|
|
|
15
|
+
| `#parse` | Returns an array of `Dependabot::Dependency` instances, representing the dependencies for the project. Each `Dependabot::Dependency` has a `name`, `version` and a `requirements` array |
|
|
16
|
+
|
|
17
|
+
An integration might look as follows:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require 'dependabot/file_parsers'
|
|
21
|
+
|
|
22
|
+
files = fetcher.files
|
|
23
|
+
|
|
24
|
+
parser_class = Dependabot::FileParsers::Ruby::Bundler
|
|
25
|
+
source = Dependabot::Source.new(provider: 'github', repo: "gocardless/business")
|
|
26
|
+
parser = parser_class.new(dependency_files: files, source: source)
|
|
27
|
+
|
|
28
|
+
dependencies = parser.parse
|
|
29
|
+
|
|
30
|
+
puts "Found the following dependencies: #{dependencies.map(&:name)}"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Writing a file parser for a new language
|
|
34
|
+
|
|
35
|
+
All new file parsers should inherit from `Dependabot::FileParsers::Base` and
|
|
36
|
+
implement the following methods:
|
|
37
|
+
|
|
38
|
+
| Method | Description |
|
|
39
|
+
|-------------------------|-----------------------------------------------------------------------------------------------|
|
|
40
|
+
| `#parse` | See Public API section. |
|
|
41
|
+
| `#check_required_files` | Raise a runtime error unless an appropriate set of files is provided. Private. |
|
|
42
|
+
|
|
43
|
+
To ensure the above are implemented, you should include
|
|
44
|
+
`it_behaves_like "a dependency file parser"` in your specs for the new file
|
|
45
|
+
parser.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dependabot
|
|
4
|
+
module FileParsers
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :dependency_files, :credentials, :source
|
|
7
|
+
|
|
8
|
+
def initialize(dependency_files:, source:, credentials: [])
|
|
9
|
+
@dependency_files = dependency_files
|
|
10
|
+
@credentials = credentials
|
|
11
|
+
@source = source
|
|
12
|
+
|
|
13
|
+
check_required_files
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def check_required_files
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get_original_file(filename)
|
|
27
|
+
dependency_files.find { |f| f.name == filename }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dependabot/dependency"
|
|
4
|
+
require "dependabot/file_parsers/base"
|
|
5
|
+
require "dependabot/utils"
|
|
6
|
+
|
|
7
|
+
module Dependabot
|
|
8
|
+
module FileParsers
|
|
9
|
+
class Base
|
|
10
|
+
class DependencySet
|
|
11
|
+
def initialize(dependencies = [])
|
|
12
|
+
unless dependencies.is_a?(Array) &&
|
|
13
|
+
dependencies.all? { |dep| dep.is_a?(Dependency) }
|
|
14
|
+
raise ArgumentError, "must be an array of Dependency objects"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@dependencies = dependencies
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :dependencies
|
|
21
|
+
|
|
22
|
+
def <<(dep)
|
|
23
|
+
unless dep.is_a?(Dependency)
|
|
24
|
+
raise ArgumentError, "must be a Dependency object"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
existing_dependency = dependencies.find { |d| d.name == dep.name }
|
|
28
|
+
|
|
29
|
+
return self if existing_dependency&.to_h == dep.to_h
|
|
30
|
+
|
|
31
|
+
if existing_dependency
|
|
32
|
+
dependencies[dependencies.index(existing_dependency)] =
|
|
33
|
+
combined_dependency(existing_dependency, dep)
|
|
34
|
+
else
|
|
35
|
+
dependencies << dep
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def +(other)
|
|
42
|
+
unless other.is_a?(DependencySet)
|
|
43
|
+
raise ArgumentError, "must be a DependencySet"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
other.dependencies.each { |dep| self << dep }
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def combined_dependency(old_dep, new_dep)
|
|
53
|
+
package_manager = old_dep.package_manager
|
|
54
|
+
v_cls = Utils.version_class_for_package_manager(package_manager)
|
|
55
|
+
|
|
56
|
+
# If we already have a requirement use the existing version
|
|
57
|
+
# (if present). Otherwise, use whatever the lowest version is
|
|
58
|
+
new_version =
|
|
59
|
+
if old_dep.requirements.any? then old_dep.version || new_dep.version
|
|
60
|
+
elsif !v_cls.correct?(new_dep.version) then old_dep.version
|
|
61
|
+
elsif !v_cls.correct?(old_dep.version) then new_dep.version
|
|
62
|
+
elsif v_cls.new(new_dep.version) > v_cls.new(old_dep.version)
|
|
63
|
+
old_dep.version
|
|
64
|
+
else new_dep.version
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Dependency.new(
|
|
68
|
+
name: old_dep.name,
|
|
69
|
+
version: new_version,
|
|
70
|
+
requirements: (old_dep.requirements + new_dep.requirements).uniq,
|
|
71
|
+
package_manager: package_manager
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dependabot
|
|
4
|
+
module FileUpdaters
|
|
5
|
+
@file_updaters = {}
|
|
6
|
+
|
|
7
|
+
def self.for_package_manager(package_manager)
|
|
8
|
+
file_updater = @file_updaters[package_manager]
|
|
9
|
+
return file_updater if file_updater
|
|
10
|
+
|
|
11
|
+
raise "Unsupported package_manager #{package_manager}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.register(package_manager, file_updater)
|
|
15
|
+
@file_updaters[package_manager] = file_updater
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# File updaters
|
|
2
|
+
|
|
3
|
+
File updaters update a dependency file to use the latest version of a given
|
|
4
|
+
dependency. They rely on information provided to them by update checkers.
|
|
5
|
+
|
|
6
|
+
There is a `Dependabot::FileUpdaters` class for each language Dependabot
|
|
7
|
+
supports.
|
|
8
|
+
|
|
9
|
+
## Public API
|
|
10
|
+
|
|
11
|
+
Each `Dependabot::FileUpdaters` class implements the following methods:
|
|
12
|
+
|
|
13
|
+
| Method | Description |
|
|
14
|
+
|------------------------------|-----------------------------------------------------------------------------------------------|
|
|
15
|
+
| `.updated_files_regex` | An array of regular expressions matching the names of the files this class updates. Intended to be used by integrators when checking whether a commit may cause merge-conflicts with a dependency update pull request. |
|
|
16
|
+
| `#updated_dependency_files` | Returns an array of updated `Dependabot::DependencyFile` instances, with their content updated to include the updated dependency. |
|
|
17
|
+
|
|
18
|
+
An integration might look as follows:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
require 'dependabot/file_updaters'
|
|
22
|
+
|
|
23
|
+
unless update_checker.can_update?(requirements_to_update: :own)
|
|
24
|
+
raise "Dependency doesn't need update!"
|
|
25
|
+
end
|
|
26
|
+
dependencies = update_checker.updated_dependencies(requirements_to_update: :own)
|
|
27
|
+
|
|
28
|
+
file_updater_class = Dependabot::FileUpdaters::Ruby::Bundler
|
|
29
|
+
file_updater = file_updater_class.new(
|
|
30
|
+
dependencies: dependencies,
|
|
31
|
+
dependency_files: files,
|
|
32
|
+
credentials: [{
|
|
33
|
+
"type" => "git_source",
|
|
34
|
+
"host" => "github.com",
|
|
35
|
+
"username" => "x-access-token",
|
|
36
|
+
"password" => "token"
|
|
37
|
+
}]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
file_updater.updated_dependency_files.each do |file|
|
|
41
|
+
puts "Updated #{file.name} with new content:\n\n#{file.content}"
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Writing a file updater for a new language
|
|
46
|
+
|
|
47
|
+
All new file updaters should inherit from `Dependabot::FileUpdaters::Base` and
|
|
48
|
+
implement the following methods:
|
|
49
|
+
|
|
50
|
+
| Method | Description |
|
|
51
|
+
|-----------------------------|-------------------------|
|
|
52
|
+
| `.updated_files_regex` | See Public API section. |
|
|
53
|
+
| `#updated_dependency_files` | See Public API section. |
|
|
54
|
+
|
|
55
|
+
To ensure the above are implemented, you should include
|
|
56
|
+
`it_behaves_like "a dependency file updater"` in your specs for the new file
|
|
57
|
+
updater.
|
|
58
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dependabot
|
|
4
|
+
module FileUpdaters
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :dependencies, :dependency_files, :credentials
|
|
7
|
+
|
|
8
|
+
def self.updated_files_regex
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(dependencies:, dependency_files:, credentials:)
|
|
13
|
+
@dependencies = dependencies
|
|
14
|
+
@dependency_files = dependency_files
|
|
15
|
+
@credentials = credentials
|
|
16
|
+
|
|
17
|
+
check_required_files
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def updated_dependency_files
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def check_required_files
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_original_file(filename)
|
|
31
|
+
dependency_files.find { |f| f.name == filename }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def file_changed?(file)
|
|
35
|
+
dependencies.any? { |dep| requirement_changed?(file, dep) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def requirement_changed?(file, dependency)
|
|
39
|
+
changed_requirements =
|
|
40
|
+
dependency.requirements - dependency.previous_requirements
|
|
41
|
+
|
|
42
|
+
changed_requirements.any? { |f| f[:file] == file.name }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def updated_file(file:, content:)
|
|
46
|
+
updated_file = file.dup
|
|
47
|
+
updated_file.content = content
|
|
48
|
+
updated_file
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "excon"
|
|
4
|
+
require "gitlab"
|
|
5
|
+
require "dependabot/clients/github_with_retries"
|
|
6
|
+
require "dependabot/metadata_finders"
|
|
7
|
+
require "dependabot/errors"
|
|
8
|
+
require "dependabot/utils"
|
|
9
|
+
require "dependabot/source"
|
|
10
|
+
|
|
11
|
+
# rubocop:disable Metrics/ClassLength
|
|
12
|
+
module Dependabot
|
|
13
|
+
class GitCommitChecker
|
|
14
|
+
VERSION_REGEX = /(?<version>[0-9]+\.[0-9]+(?:\.[a-zA-Z0-9\-]+)*)$/.freeze
|
|
15
|
+
KNOWN_HOSTS = /github\.com|bitbucket\.org|gitlab.com/.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(dependency:, credentials:, ignored_versions: [],
|
|
18
|
+
requirement_class: nil, version_class: nil)
|
|
19
|
+
@dependency = dependency
|
|
20
|
+
@credentials = credentials
|
|
21
|
+
@ignored_versions = ignored_versions
|
|
22
|
+
@requirement_class = requirement_class
|
|
23
|
+
@version_class = version_class
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def git_dependency?
|
|
27
|
+
return false if dependency_source_details.nil?
|
|
28
|
+
|
|
29
|
+
dependency_source_details.fetch(:type) == "git"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pinned?
|
|
33
|
+
raise "Not a git dependency!" unless git_dependency?
|
|
34
|
+
|
|
35
|
+
ref = dependency_source_details.fetch(:ref)
|
|
36
|
+
branch = dependency_source_details.fetch(:branch)
|
|
37
|
+
|
|
38
|
+
return false if ref.nil?
|
|
39
|
+
return false if branch == ref
|
|
40
|
+
return true if branch
|
|
41
|
+
return true if dependency.version&.start_with?(ref)
|
|
42
|
+
|
|
43
|
+
# Check the specified `ref` isn't actually a branch
|
|
44
|
+
!local_upload_pack.match?("refs/heads/#{ref}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def pinned_ref_looks_like_version?
|
|
48
|
+
return false unless pinned?
|
|
49
|
+
|
|
50
|
+
dependency_source_details.fetch(:ref).match?(VERSION_REGEX)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def branch_or_ref_in_release?(version)
|
|
54
|
+
pinned_ref_in_release?(version) || branch_behind_release?(version)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def head_commit_for_current_branch
|
|
58
|
+
return dependency.version if pinned?
|
|
59
|
+
|
|
60
|
+
branch_ref = ref_or_branch ? "refs/heads/#{ref_or_branch}" : "HEAD"
|
|
61
|
+
|
|
62
|
+
# Remove the opening clause of the upload pack as this isn't always
|
|
63
|
+
# followed by a line break. When it isn't (e.g., with Bitbucket) it causes
|
|
64
|
+
# problems for our `sha_for_update_pack_line` logic
|
|
65
|
+
line = local_upload_pack.
|
|
66
|
+
gsub(/.*git-upload-pack/, "").
|
|
67
|
+
lines.find { |l| l.include?(" #{branch_ref}") }
|
|
68
|
+
|
|
69
|
+
return sha_for_update_pack_line(line) if line
|
|
70
|
+
|
|
71
|
+
raise Dependabot::GitDependencyReferenceNotFound, dependency.name
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def local_tag_for_latest_version
|
|
75
|
+
tag =
|
|
76
|
+
local_tags.
|
|
77
|
+
select { |t| t.name.match?(VERSION_REGEX) }.
|
|
78
|
+
reject { |t| tag_included_in_ignore_reqs?(t) }.
|
|
79
|
+
reject { |t| tag_is_prerelease?(t) && !wants_prerelease? }.
|
|
80
|
+
max_by do |t|
|
|
81
|
+
version = t.name.match(VERSION_REGEX).named_captures.fetch("version")
|
|
82
|
+
version_class.new(version)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return unless tag
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
tag: tag.name,
|
|
89
|
+
commit_sha: tag.commit_sha,
|
|
90
|
+
tag_sha: tag.tag_sha
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
attr_reader :dependency, :credentials, :ignored_versions
|
|
97
|
+
|
|
98
|
+
def pinned_ref_in_release?(version)
|
|
99
|
+
raise "Not a git dependency!" unless git_dependency?
|
|
100
|
+
|
|
101
|
+
return false unless pinned?
|
|
102
|
+
return false if listing_source_url.nil?
|
|
103
|
+
|
|
104
|
+
tag = listing_tag_for_version(version.to_s)
|
|
105
|
+
return false unless tag
|
|
106
|
+
|
|
107
|
+
commit_included_in_tag?(
|
|
108
|
+
commit: dependency_source_details.fetch(:ref),
|
|
109
|
+
tag: tag,
|
|
110
|
+
allow_identical: true
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def branch_behind_release?(version)
|
|
115
|
+
raise "Not a git dependency!" unless git_dependency?
|
|
116
|
+
|
|
117
|
+
return false if ref_or_branch.nil?
|
|
118
|
+
return false if listing_source_url.nil?
|
|
119
|
+
|
|
120
|
+
tag = listing_tag_for_version(version.to_s)
|
|
121
|
+
return false unless tag
|
|
122
|
+
|
|
123
|
+
# Check if behind, excluding the case where it's identical, because
|
|
124
|
+
# we normally wouldn't switch you from tracking master to a release.
|
|
125
|
+
commit_included_in_tag?(
|
|
126
|
+
commit: ref_or_branch,
|
|
127
|
+
tag: tag,
|
|
128
|
+
allow_identical: false
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def local_upload_pack
|
|
133
|
+
@local_upload_pack ||=
|
|
134
|
+
fetch_upload_pack_for(dependency_source_details.fetch(:url))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def local_tags
|
|
138
|
+
return [] unless local_upload_pack
|
|
139
|
+
|
|
140
|
+
tags_for_upload_pack(local_upload_pack)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def tags_for_upload_pack(upload_pack)
|
|
144
|
+
peeled_lines = []
|
|
145
|
+
unpeeled_lines = []
|
|
146
|
+
|
|
147
|
+
upload_pack.lines.each do |line|
|
|
148
|
+
next unless line.split(" ").last.start_with?("refs/tags")
|
|
149
|
+
|
|
150
|
+
if line.strip.end_with?("^{}") then peeled_lines << line
|
|
151
|
+
else unpeeled_lines << line
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
unpeeled_lines.map do |line|
|
|
156
|
+
tag_name = line.split(" refs/tags/").last.strip
|
|
157
|
+
tag_sha = sha_for_update_pack_line(line)
|
|
158
|
+
peeled_line = peeled_lines.find do |pl|
|
|
159
|
+
pl.split(" refs/tags/").last.strip == "#{tag_name}^{}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
commit_sha =
|
|
163
|
+
peeled_line ? sha_for_update_pack_line(peeled_line) : tag_sha
|
|
164
|
+
|
|
165
|
+
if dependency_source_details&.fetch(:ref, nil)&.start_with?("tags/")
|
|
166
|
+
tag_name = "tags/#{tag_name}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
OpenStruct.new(name: tag_name, tag_sha: tag_sha, commit_sha: commit_sha)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
174
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
175
|
+
def fetch_upload_pack_for(uri)
|
|
176
|
+
response = Excon.get(
|
|
177
|
+
service_pack_uri(uri),
|
|
178
|
+
idempotent: true,
|
|
179
|
+
**SharedHelpers.excon_defaults
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return response.body if response.status == 200
|
|
183
|
+
if response.status >= 500 && uri.match?(KNOWN_HOSTS)
|
|
184
|
+
raise "Server error at #{uri}: #{response.body}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
raise Dependabot::GitDependenciesNotReachable, [uri]
|
|
188
|
+
rescue Excon::Error::Socket, Excon::Error::Timeout
|
|
189
|
+
retry_count ||= 0
|
|
190
|
+
retry_count += 1
|
|
191
|
+
|
|
192
|
+
sleep(rand(0.9)) && retry if retry_count < 2 && uri.match?(KNOWN_HOSTS)
|
|
193
|
+
raise if uri.match?(KNOWN_HOSTS)
|
|
194
|
+
|
|
195
|
+
raise Dependabot::GitDependenciesNotReachable, [uri]
|
|
196
|
+
end
|
|
197
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
198
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
199
|
+
|
|
200
|
+
def service_pack_uri(uri)
|
|
201
|
+
service_pack_uri = uri_with_auth(uri)
|
|
202
|
+
service_pack_uri = service_pack_uri.gsub(%r{/$}, "")
|
|
203
|
+
service_pack_uri += ".git" unless service_pack_uri.end_with?(".git")
|
|
204
|
+
service_pack_uri + "/info/refs?service=git-upload-pack"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def uri_with_auth(uri)
|
|
208
|
+
bare_uri =
|
|
209
|
+
if uri.include?("git@") then uri.split("git@").last.sub(":", "/")
|
|
210
|
+
else uri.sub(%r{.*?://}, "")
|
|
211
|
+
end
|
|
212
|
+
cred = credentials.select { |c| c["type"] == "git_source" }.
|
|
213
|
+
find { |c| bare_uri.start_with?(c["host"]) }
|
|
214
|
+
|
|
215
|
+
if bare_uri.match?(%r{[^/]+:[^/]+@})
|
|
216
|
+
# URI already has authentication details
|
|
217
|
+
"https://#{bare_uri}"
|
|
218
|
+
elsif cred
|
|
219
|
+
# URI doesn't have authentication details, but we have credentials
|
|
220
|
+
auth_string = "#{cred.fetch('username')}:#{cred.fetch('password')}"
|
|
221
|
+
"https://#{auth_string}@#{bare_uri}"
|
|
222
|
+
else
|
|
223
|
+
# No credentials, so just return the https URI
|
|
224
|
+
"https://#{bare_uri}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def commit_included_in_tag?(tag:, commit:, allow_identical: false)
|
|
229
|
+
status =
|
|
230
|
+
case Source.from_url(listing_source_url)&.provider
|
|
231
|
+
when "github" then github_commit_comparison_status(tag, commit)
|
|
232
|
+
when "gitlab" then gitlab_commit_comparison_status(tag, commit)
|
|
233
|
+
when "bitbucket" then bitbucket_commit_comparison_status(tag, commit)
|
|
234
|
+
else raise "Unknown source"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
return true if status == "behind"
|
|
238
|
+
|
|
239
|
+
allow_identical && status == "identical"
|
|
240
|
+
rescue Octokit::NotFound, Gitlab::Error::NotFound,
|
|
241
|
+
Octokit::InternalServerError
|
|
242
|
+
false
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def github_commit_comparison_status(ref1, ref2)
|
|
246
|
+
client = Clients::GithubWithRetries.
|
|
247
|
+
for_github_dot_com(credentials: credentials)
|
|
248
|
+
|
|
249
|
+
client.compare(listing_source_repo, ref1, ref2).status
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def gitlab_commit_comparison_status(ref1, ref2)
|
|
253
|
+
access_token = credentials.
|
|
254
|
+
select { |cred| cred["type"] == "git_source" }.
|
|
255
|
+
find { |cred| cred["host"] == "gitlab.com" }&.
|
|
256
|
+
fetch("token")
|
|
257
|
+
|
|
258
|
+
client = Gitlab.client(endpoint: "https://gitlab.com/api/v4",
|
|
259
|
+
private_token: access_token.to_s)
|
|
260
|
+
|
|
261
|
+
comparison = client.compare(listing_source_repo, ref1, ref2)
|
|
262
|
+
|
|
263
|
+
if comparison.commits.none? then "behind"
|
|
264
|
+
elsif comparison.compare_same_ref then "identical"
|
|
265
|
+
else "ahead"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def bitbucket_commit_comparison_status(ref1, ref2)
|
|
270
|
+
url = "https://api.bitbucket.org/2.0/repositories/"\
|
|
271
|
+
"#{listing_source_repo}/commits/?"\
|
|
272
|
+
"include=#{ref2}&exclude=#{ref1}"
|
|
273
|
+
|
|
274
|
+
response = Excon.get(url,
|
|
275
|
+
headers: bitbucket_auth_header,
|
|
276
|
+
idempotent: true,
|
|
277
|
+
**SharedHelpers.excon_defaults)
|
|
278
|
+
|
|
279
|
+
# Conservatively assume that ref2 is ahead in the equality case, of
|
|
280
|
+
# if we get an unexpected format (e.g., due to a 404)
|
|
281
|
+
if JSON.parse(response.body).fetch("values", ["x"]).none? then "behind"
|
|
282
|
+
else "ahead"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def bitbucket_auth_header
|
|
287
|
+
token = credentials.
|
|
288
|
+
select { |cred| cred["type"] == "git_source" }.
|
|
289
|
+
find { |cred| cred["host"] == "bitbucket.org" }&.
|
|
290
|
+
fetch("token")
|
|
291
|
+
|
|
292
|
+
if token.nil? then {}
|
|
293
|
+
elsif token.include?(":")
|
|
294
|
+
encoded_token = Base64.encode64(token).delete("\n")
|
|
295
|
+
{ "Authorization" => "Basic #{encoded_token}" }
|
|
296
|
+
elsif Base64.decode64(token).ascii_only? &&
|
|
297
|
+
Base64.decode64(token).include?(":")
|
|
298
|
+
{ "Authorization" => "Basic #{token.delete("\n")}" }
|
|
299
|
+
else
|
|
300
|
+
{ "Authorization" => "Bearer #{token}" }
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def dependency_source_details
|
|
305
|
+
sources =
|
|
306
|
+
dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
|
|
307
|
+
|
|
308
|
+
return sources.first if sources.count <= 1
|
|
309
|
+
|
|
310
|
+
# If there are multiple source types, or multiple source URLs, then it's
|
|
311
|
+
# unclear how we should proceed
|
|
312
|
+
if sources.map { |s| [s.fetch(:type), s.fetch(:url, nil)] }.uniq.count > 1
|
|
313
|
+
raise "Multiple sources! #{sources.join(', ')}"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Otherwise it's reasonable to take the first source and use that. This
|
|
317
|
+
# will happen if we have multiple git sources with difference references
|
|
318
|
+
# specified. In that case it's fine to update them all.
|
|
319
|
+
sources.first
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def ref_or_branch
|
|
323
|
+
dependency_source_details.fetch(:ref) ||
|
|
324
|
+
dependency_source_details.fetch(:branch)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def listing_source_url
|
|
328
|
+
@listing_source_url ||=
|
|
329
|
+
begin
|
|
330
|
+
# Remove the git source, so the metadata finder looks on the
|
|
331
|
+
# registry
|
|
332
|
+
candidate_dep = Dependency.new(
|
|
333
|
+
name: dependency.name,
|
|
334
|
+
version: dependency.version,
|
|
335
|
+
requirements: [],
|
|
336
|
+
package_manager: dependency.package_manager
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
MetadataFinders.
|
|
340
|
+
for_package_manager(dependency.package_manager).
|
|
341
|
+
new(dependency: candidate_dep, credentials: credentials).
|
|
342
|
+
source_url
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def listing_source_repo
|
|
347
|
+
return unless listing_source_url
|
|
348
|
+
|
|
349
|
+
Source.from_url(listing_source_url)&.repo
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def listing_tag_for_version(version)
|
|
353
|
+
listing_tags.
|
|
354
|
+
find { |t| t.name =~ /(?:[^0-9\.]|\A)#{Regexp.escape(version)}\z/ }&.
|
|
355
|
+
name
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def listing_tags
|
|
359
|
+
return [] unless listing_upload_pack
|
|
360
|
+
|
|
361
|
+
tags_for_upload_pack(listing_upload_pack)
|
|
362
|
+
rescue GitDependenciesNotReachable
|
|
363
|
+
[]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def listing_upload_pack
|
|
367
|
+
return unless listing_source_url
|
|
368
|
+
|
|
369
|
+
@listing_upload_pack ||= fetch_upload_pack_for(listing_source_url)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def ignore_reqs
|
|
373
|
+
ignored_versions.map { |req| requirement_class.new(req.split(",")) }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def wants_prerelease?
|
|
377
|
+
return false unless dependency_source_details&.fetch(:ref, nil)
|
|
378
|
+
return false unless pinned_ref_looks_like_version?
|
|
379
|
+
|
|
380
|
+
version = dependency_source_details.fetch(:ref).match(VERSION_REGEX).
|
|
381
|
+
named_captures.fetch("version")
|
|
382
|
+
version_class.new(version).prerelease?
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def tag_included_in_ignore_reqs?(tag)
|
|
386
|
+
version = tag.name.match(VERSION_REGEX).named_captures.fetch("version")
|
|
387
|
+
ignore_reqs.any? { |r| r.satisfied_by?(version_class.new(version)) }
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def tag_is_prerelease?(tag)
|
|
391
|
+
version = tag.name.match(VERSION_REGEX).named_captures.fetch("version")
|
|
392
|
+
version_class.new(version).prerelease?
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def version_class
|
|
396
|
+
return @version_class if @version_class
|
|
397
|
+
|
|
398
|
+
Utils.version_class_for_package_manager(dependency.package_manager)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def requirement_class
|
|
402
|
+
return @requirement_class if @requirement_class
|
|
403
|
+
|
|
404
|
+
Utils.requirement_class_for_package_manager(dependency.package_manager)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def sha_for_update_pack_line(line)
|
|
408
|
+
line.split(" ").first.chars.last(40).join
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
# rubocop:enable Metrics/ClassLength
|