dependabot-dep 0.90.0

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.
@@ -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