dependabot-dep 0.90.0

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