dependabot-dep 0.90.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/helpers/Makefile +9 -0
- data/helpers/build +26 -0
- data/helpers/go.mod +8 -0
- data/helpers/go.sum +2 -0
- data/helpers/importresolver/go.mod +1 -0
- data/helpers/importresolver/main.go +34 -0
- data/helpers/main.go +67 -0
- data/lib/dependabot/dep.rb +11 -0
- data/lib/dependabot/dep/file_fetcher.rb +70 -0
- data/lib/dependabot/dep/file_parser.rb +189 -0
- data/lib/dependabot/dep/file_updater.rb +78 -0
- data/lib/dependabot/dep/file_updater/lockfile_updater.rb +220 -0
- data/lib/dependabot/dep/file_updater/manifest_updater.rb +151 -0
- data/lib/dependabot/dep/metadata_finder.rb +57 -0
- data/lib/dependabot/dep/native_helpers.rb +20 -0
- data/lib/dependabot/dep/path_converter.rb +72 -0
- data/lib/dependabot/dep/requirement.rb +152 -0
- data/lib/dependabot/dep/update_checker.rb +312 -0
- data/lib/dependabot/dep/update_checker/file_preparer.rb +219 -0
- data/lib/dependabot/dep/update_checker/latest_version_finder.rb +167 -0
- data/lib/dependabot/dep/update_checker/requirements_updater.rb +221 -0
- data/lib/dependabot/dep/update_checker/version_resolver.rb +166 -0
- data/lib/dependabot/dep/version.rb +43 -0
- metadata +192 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "toml-rb"
|
4
|
+
require "open3"
|
5
|
+
require "dependabot/shared_helpers"
|
6
|
+
require "dependabot/dependency_file"
|
7
|
+
require "dependabot/dep/file_updater"
|
8
|
+
require "dependabot/dep/file_parser"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Dep
|
12
|
+
class FileUpdater
|
13
|
+
class LockfileUpdater
|
14
|
+
def initialize(dependencies:, dependency_files:, credentials:)
|
15
|
+
@dependencies = dependencies
|
16
|
+
@dependency_files = dependency_files
|
17
|
+
@credentials = credentials
|
18
|
+
end
|
19
|
+
|
20
|
+
def updated_lockfile_content
|
21
|
+
deps = dependencies.select { |d| appears_in_lockfile(d) }
|
22
|
+
return lockfile.content if deps.none?
|
23
|
+
|
24
|
+
base_directory = File.join("src", "project",
|
25
|
+
dependency_files.first.directory)
|
26
|
+
base_parts = base_directory.split("/").length
|
27
|
+
updated_content =
|
28
|
+
SharedHelpers.in_a_temporary_directory(base_directory) do |dir|
|
29
|
+
write_temporary_dependency_files
|
30
|
+
|
31
|
+
SharedHelpers.with_git_configured(credentials: credentials) do
|
32
|
+
# Shell out to dep, which handles everything for us.
|
33
|
+
# Note: We are currently doing a full install here (we're not
|
34
|
+
# passing no-vendor) because dep needs to generate the digests
|
35
|
+
# for each project.
|
36
|
+
command = "dep ensure -update #{deps.map(&:name).join(' ')}"
|
37
|
+
dir_parts = dir.realpath.to_s.split("/")
|
38
|
+
gopath = File.join(dir_parts[0..-(base_parts + 1)])
|
39
|
+
run_shell_command(command, "GOPATH" => gopath)
|
40
|
+
end
|
41
|
+
|
42
|
+
File.read("Gopkg.lock")
|
43
|
+
end
|
44
|
+
|
45
|
+
updated_content
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_reader :dependencies, :dependency_files, :credentials
|
51
|
+
|
52
|
+
def run_shell_command(command, env = {})
|
53
|
+
start = Time.now
|
54
|
+
stdout, process = Open3.capture2e(env, command)
|
55
|
+
time_taken = start - Time.now
|
56
|
+
|
57
|
+
# Raise an error with the output from the shell session if dep
|
58
|
+
# returns a non-zero status
|
59
|
+
return if process.success?
|
60
|
+
|
61
|
+
raise SharedHelpers::HelperSubprocessFailed.new(
|
62
|
+
message: stdout,
|
63
|
+
error_context: {
|
64
|
+
command: command,
|
65
|
+
time_taken: time_taken,
|
66
|
+
process_exit_value: process.to_s
|
67
|
+
}
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def write_temporary_dependency_files
|
72
|
+
File.write(lockfile.name, lockfile.content)
|
73
|
+
|
74
|
+
# Overwrite the manifest with our custom prepared one
|
75
|
+
File.write(prepared_manifest.name, prepared_manifest.content)
|
76
|
+
|
77
|
+
File.write("hello.go", dummy_app_content)
|
78
|
+
end
|
79
|
+
|
80
|
+
def prepared_manifest
|
81
|
+
DependencyFile.new(
|
82
|
+
name: manifest.name,
|
83
|
+
content: prepared_manifest_content
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
def prepared_manifest_content
|
88
|
+
parsed_manifest = TomlRB.parse(manifest.content)
|
89
|
+
|
90
|
+
parsed_manifest["override"] =
|
91
|
+
add_fsnotify_override(parsed_manifest["override"])
|
92
|
+
|
93
|
+
dependencies.each do |dep|
|
94
|
+
req = dep.requirements.find { |r| r[:file] == manifest.name }
|
95
|
+
next unless appears_in_lockfile(dep)
|
96
|
+
|
97
|
+
if req
|
98
|
+
update_constraint!(parsed_manifest, dep)
|
99
|
+
else
|
100
|
+
create_constraint!(parsed_manifest, dep)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
TomlRB.dump(parsed_manifest)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Used to lock the version when updating a top-level dependency
|
108
|
+
def update_constraint!(parsed_manifest, dep)
|
109
|
+
details =
|
110
|
+
parsed_manifest.
|
111
|
+
values_at(*Dep::FileParser::REQUIREMENT_TYPES).
|
112
|
+
flatten.compact.find { |d| d["name"] == dep.name }
|
113
|
+
|
114
|
+
req = dep.requirements.find { |r| r[:file] == manifest.name }
|
115
|
+
|
116
|
+
if req.fetch(:source).fetch(:type) == "git" && !details["branch"]
|
117
|
+
# Note: we don't try to update to a specific revision if the
|
118
|
+
# branch was previously specified because the change in
|
119
|
+
# specification type would be persisted in the lockfile
|
120
|
+
details["revision"] = dep.version if details["revision"]
|
121
|
+
details["version"] = dep.version if details["version"]
|
122
|
+
elsif req.fetch(:source).fetch(:type) == "default"
|
123
|
+
details.delete("branch")
|
124
|
+
details.delete("revision")
|
125
|
+
details["version"] = "=#{dep.version}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Used to lock the version when updating a subdependency
|
130
|
+
def create_constraint!(parsed_manifest, dep)
|
131
|
+
details = { "name" => dep.name }
|
132
|
+
|
133
|
+
# Fetch the details from the lockfile to check whether this
|
134
|
+
# sub-dependency needs a git revision or a version.
|
135
|
+
original_details =
|
136
|
+
parsed_file(lockfile).fetch("projects").
|
137
|
+
find { |p| p["name"] == dep.name }
|
138
|
+
|
139
|
+
if original_details["source"]
|
140
|
+
details["source"] = original_details["source"]
|
141
|
+
end
|
142
|
+
|
143
|
+
if original_details["version"]
|
144
|
+
details["version"] = dep.version
|
145
|
+
else
|
146
|
+
details["revision"] = dep.version
|
147
|
+
end
|
148
|
+
|
149
|
+
parsed_manifest["constraint"] ||= []
|
150
|
+
parsed_manifest["constraint"] << details
|
151
|
+
end
|
152
|
+
|
153
|
+
# Work around a dep bug that results in a panic
|
154
|
+
def add_fsnotify_override(overrides)
|
155
|
+
overrides ||= []
|
156
|
+
dep_name = "gopkg.in/fsnotify.v1"
|
157
|
+
|
158
|
+
override = overrides.find { |s| s["name"] == dep_name }
|
159
|
+
if override.nil?
|
160
|
+
override = { "name" => dep_name }
|
161
|
+
overrides << override
|
162
|
+
end
|
163
|
+
|
164
|
+
unless override["source"]
|
165
|
+
override["source"] = "gopkg.in/fsnotify/fsnotify.v1"
|
166
|
+
end
|
167
|
+
|
168
|
+
overrides
|
169
|
+
end
|
170
|
+
|
171
|
+
def dummy_app_content
|
172
|
+
base = "package main\n\n"\
|
173
|
+
"import \"fmt\"\n\n"
|
174
|
+
|
175
|
+
packages_to_import.each { |nm| base += "import \"#{nm}\"\n\n" }
|
176
|
+
|
177
|
+
base + "func main() {\n fmt.Printf(\"hello, world\\n\")\n}"
|
178
|
+
end
|
179
|
+
|
180
|
+
def packages_to_import
|
181
|
+
parsed_lockfile = TomlRB.parse(lockfile.content)
|
182
|
+
|
183
|
+
# If the lockfile was created using dep v0.5.0+ then it will tell us
|
184
|
+
# exactly which packages to import
|
185
|
+
if parsed_lockfile.dig("solve-meta", "input-imports")
|
186
|
+
return parsed_lockfile.dig("solve-meta", "input-imports")
|
187
|
+
end
|
188
|
+
|
189
|
+
# Otherwise we have no way of knowing, so import everything in the
|
190
|
+
# lockfile that isn't marked as internal
|
191
|
+
parsed_lockfile.fetch("projects").flat_map do |dep|
|
192
|
+
dep["packages"].map do |package|
|
193
|
+
next if package.start_with?("internal")
|
194
|
+
|
195
|
+
package == "." ? dep["name"] : File.join(dep["name"], package)
|
196
|
+
end.compact
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def appears_in_lockfile(dep)
|
201
|
+
!parsed_file(lockfile)["projects"]&.
|
202
|
+
find { |p| p["name"] == dep.name }.nil?
|
203
|
+
end
|
204
|
+
|
205
|
+
def parsed_file(file)
|
206
|
+
@parsed_file ||= {}
|
207
|
+
@parsed_file[file.name] ||= TomlRB.parse(file.content)
|
208
|
+
end
|
209
|
+
|
210
|
+
def manifest
|
211
|
+
@manifest ||= dependency_files.find { |f| f.name == "Gopkg.toml" }
|
212
|
+
end
|
213
|
+
|
214
|
+
def lockfile
|
215
|
+
@lockfile ||= dependency_files.find { |f| f.name == "Gopkg.lock" }
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/dep/file_updater"
|
4
|
+
|
5
|
+
module Dependabot
|
6
|
+
module Dep
|
7
|
+
class FileUpdater
|
8
|
+
class ManifestUpdater
|
9
|
+
def initialize(dependencies:, manifest:)
|
10
|
+
@dependencies = dependencies
|
11
|
+
@manifest = manifest
|
12
|
+
end
|
13
|
+
|
14
|
+
def updated_manifest_content
|
15
|
+
dependencies.
|
16
|
+
select { |dep| requirement_changed?(manifest, dep) }.
|
17
|
+
reduce(manifest.content.dup) do |content, dep|
|
18
|
+
updated_content = content
|
19
|
+
|
20
|
+
updated_content = update_requirements(
|
21
|
+
content: updated_content,
|
22
|
+
filename: manifest.name,
|
23
|
+
dependency: dep
|
24
|
+
)
|
25
|
+
updated_content = update_git_pin(
|
26
|
+
content: updated_content,
|
27
|
+
filename: manifest.name,
|
28
|
+
dependency: dep
|
29
|
+
)
|
30
|
+
|
31
|
+
raise "Expected content to change!" if content == updated_content
|
32
|
+
|
33
|
+
updated_content
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :dependencies, :manifest
|
40
|
+
|
41
|
+
def requirement_changed?(file, dependency)
|
42
|
+
changed_requirements =
|
43
|
+
dependency.requirements - dependency.previous_requirements
|
44
|
+
|
45
|
+
changed_requirements.any? { |f| f[:file] == file.name }
|
46
|
+
end
|
47
|
+
|
48
|
+
def update_requirements(content:, filename:, dependency:)
|
49
|
+
updated_content = content.dup
|
50
|
+
|
51
|
+
# The UpdateChecker ensures the order of requirements is preserved
|
52
|
+
# when updating, so we can zip them together in new/old pairs.
|
53
|
+
reqs = dependency.requirements.
|
54
|
+
zip(dependency.previous_requirements).
|
55
|
+
reject { |new_req, old_req| new_req == old_req }
|
56
|
+
|
57
|
+
# Loop through each changed requirement
|
58
|
+
reqs.each do |new_req, old_req|
|
59
|
+
raise "Bad req match" unless new_req[:file] == old_req[:file]
|
60
|
+
next if new_req[:requirement] == old_req[:requirement]
|
61
|
+
next unless new_req[:file] == filename
|
62
|
+
|
63
|
+
updated_content = update_manifest_req(
|
64
|
+
content: updated_content,
|
65
|
+
dep: dependency,
|
66
|
+
old_req: old_req.fetch(:requirement),
|
67
|
+
new_req: new_req.fetch(:requirement)
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
updated_content
|
72
|
+
end
|
73
|
+
|
74
|
+
def update_git_pin(content:, filename:, dependency:)
|
75
|
+
updated_pin =
|
76
|
+
dependency.requirements.
|
77
|
+
find { |r| r[:file] == filename }&.
|
78
|
+
dig(:source, :ref)
|
79
|
+
|
80
|
+
old_pin =
|
81
|
+
dependency.previous_requirements.
|
82
|
+
find { |r| r[:file] == filename }&.
|
83
|
+
dig(:source, :ref)
|
84
|
+
|
85
|
+
return content unless old_pin
|
86
|
+
|
87
|
+
update_manifest_pin(
|
88
|
+
content: content,
|
89
|
+
dep: dependency,
|
90
|
+
old_pin: old_pin,
|
91
|
+
new_pin: updated_pin
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
96
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
97
|
+
def update_manifest_req(content:, dep:, old_req:, new_req:)
|
98
|
+
declaration = content.scan(declaration_regex(dep)).
|
99
|
+
find { |m| old_req.nil? || m.include?(old_req) }
|
100
|
+
|
101
|
+
return content unless declaration
|
102
|
+
|
103
|
+
if old_req && new_req
|
104
|
+
content.gsub(declaration) do |line|
|
105
|
+
line.gsub(old_req, new_req)
|
106
|
+
end
|
107
|
+
elsif old_req && new_req.nil?
|
108
|
+
content.gsub(declaration) do |line|
|
109
|
+
line.gsub(/\R+.*version\s*=.*/, "")
|
110
|
+
end
|
111
|
+
elsif old_req.nil? && new_req
|
112
|
+
content.gsub(declaration) do |line|
|
113
|
+
indent = line.match(/(?<indent>\s*)name/).
|
114
|
+
named_captures.fetch("indent")
|
115
|
+
version_declaration = indent + "version = \"#{new_req}\""
|
116
|
+
line.gsub(/name\s*=.*/) { |nm_ln| nm_ln + version_declaration }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
121
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
122
|
+
|
123
|
+
def update_manifest_pin(content:, dep:, old_pin:, new_pin:)
|
124
|
+
declaration = content.scan(declaration_regex(dep)).
|
125
|
+
find { |m| m.include?(old_pin) }
|
126
|
+
|
127
|
+
return content unless declaration
|
128
|
+
|
129
|
+
if old_pin && new_pin
|
130
|
+
content.gsub(declaration) do |line|
|
131
|
+
line.gsub(old_pin, new_pin)
|
132
|
+
end
|
133
|
+
elsif old_pin && new_pin.nil?
|
134
|
+
content.gsub(declaration) do |line|
|
135
|
+
line.gsub(/\R+.*(revision|branch)\s*=.*/, "")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def declaration_regex(dep)
|
141
|
+
/
|
142
|
+
(?<=\]\])
|
143
|
+
(?:(?!^\[).)*
|
144
|
+
name\s*=\s*["']#{Regexp.escape(dep.name)}["']
|
145
|
+
(?:(?!^\[).)*
|
146
|
+
/mx
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/metadata_finders"
|
4
|
+
require "dependabot/metadata_finders/base"
|
5
|
+
require "dependabot/dep/path_converter"
|
6
|
+
|
7
|
+
module Dependabot
|
8
|
+
module Dep
|
9
|
+
class MetadataFinder < Dependabot::MetadataFinders::Base
|
10
|
+
private
|
11
|
+
|
12
|
+
def look_up_source
|
13
|
+
return look_up_git_dependency_source if git_dependency?
|
14
|
+
|
15
|
+
path_str = (specified_source_string || dependency.name)
|
16
|
+
url = Dependabot::Dep::PathConverter.
|
17
|
+
git_url_for_path_without_go_helper(path_str)
|
18
|
+
Source.from_url(url) if url
|
19
|
+
end
|
20
|
+
|
21
|
+
def git_dependency?
|
22
|
+
return false unless declared_source_details
|
23
|
+
|
24
|
+
dependency_type =
|
25
|
+
declared_source_details.fetch(:type, nil) ||
|
26
|
+
declared_source_details.fetch("type")
|
27
|
+
|
28
|
+
dependency_type == "git"
|
29
|
+
end
|
30
|
+
|
31
|
+
def look_up_git_dependency_source
|
32
|
+
specified_url =
|
33
|
+
declared_source_details.fetch(:url, nil) ||
|
34
|
+
declared_source_details.fetch("url")
|
35
|
+
|
36
|
+
Source.from_url(specified_url)
|
37
|
+
end
|
38
|
+
|
39
|
+
def specified_source_string
|
40
|
+
declared_source_details&.fetch(:source, nil) ||
|
41
|
+
declared_source_details&.fetch("source", nil)
|
42
|
+
end
|
43
|
+
|
44
|
+
def declared_source_details
|
45
|
+
sources = dependency.requirements.
|
46
|
+
map { |r| r.fetch(:source) }.
|
47
|
+
uniq.compact
|
48
|
+
|
49
|
+
raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
|
50
|
+
|
51
|
+
sources.first
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Dependabot::MetadataFinders.register("dep", Dependabot::Dep::MetadataFinder)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dependabot
|
4
|
+
module Dep
|
5
|
+
module NativeHelpers
|
6
|
+
def self.helper_path
|
7
|
+
clean_path(File.join(native_helpers_root, "dep/bin/helper"))
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.native_helpers_root
|
11
|
+
default_path = File.join(__dir__, "../../../helpers/install-dir")
|
12
|
+
ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.clean_path(path)
|
16
|
+
Pathname.new(path).cleanpath.to_path
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|