dependabot-julia 0.342.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 +7 -0
- data/lib/dependabot/julia/dependency.rb +21 -0
- data/lib/dependabot/julia/file_fetcher.rb +63 -0
- data/lib/dependabot/julia/file_parser.rb +278 -0
- data/lib/dependabot/julia/file_updater.rb +295 -0
- data/lib/dependabot/julia/metadata_finder.rb +99 -0
- data/lib/dependabot/julia/package/package_details_fetcher.rb +129 -0
- data/lib/dependabot/julia/package_manager.rb +71 -0
- data/lib/dependabot/julia/registry_client.rb +340 -0
- data/lib/dependabot/julia/requirement.rb +94 -0
- data/lib/dependabot/julia/update_checker/latest_version_finder.rb +222 -0
- data/lib/dependabot/julia/update_checker/requirements_updater.rb +90 -0
- data/lib/dependabot/julia/update_checker.rb +146 -0
- data/lib/dependabot/julia/version.rb +44 -0
- data/lib/dependabot/julia.rb +65 -0
- metadata +281 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1b48cd9be4ed4e37e08b11eb39f35e71d449320f0efb7ee5d888e6f2be7d8ed3
|
|
4
|
+
data.tar.gz: 1f34fc7c632a67d01f7325c5d60b7a2d8e3e6c40178961713b42b817b7e3b5b4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b5c0b679487adf8dce2910599a8018f1858b68a7e987d64662ac92edeb32a4bf00925f4a43a35b507bb816c359d17614c320a9f0999ba82fff109d168712673e
|
|
7
|
+
data.tar.gz: 21ff26c3c0989184d86d36d8fa4104478b7d3e9797f694a74cb573b252562ebead236e09347fd9211217ab77cea903817183ee88ac0f1846f1bbcb1340fa2f4e
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "dependabot/dependency"
|
|
5
|
+
require "dependabot/julia/version"
|
|
6
|
+
require "sorbet-runtime"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module Julia
|
|
10
|
+
class Dependency < Dependabot::Dependency
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
# This class intentionally delegates most functionality to the base
|
|
14
|
+
# Dependabot::Dependency class.
|
|
15
|
+
#
|
|
16
|
+
# Note: Dependabot::Julia::Version is properly registered for the "julia" package manager
|
|
17
|
+
# and implements ::new(version_string) and ::correct?(version_string) methods that handle
|
|
18
|
+
# Julia version strings according to Julia's semantic versioning specification.
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "dependabot/file_fetchers"
|
|
5
|
+
require "dependabot/file_fetchers/base"
|
|
6
|
+
|
|
7
|
+
module Dependabot
|
|
8
|
+
module Julia
|
|
9
|
+
class FileFetcher < Dependabot::FileFetchers::Base
|
|
10
|
+
sig { override.params(filenames: T::Array[String]).returns(T::Boolean) }
|
|
11
|
+
def self.required_files_in?(filenames)
|
|
12
|
+
filenames.any? { |name| name.match?(/^(Julia)?Project\.toml$/i) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { override.returns(String) }
|
|
16
|
+
def self.required_files_message
|
|
17
|
+
"Repo must contain a Project.toml or JuliaProject.toml file."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { override.returns(T::Array[Dependabot::DependencyFile]) }
|
|
21
|
+
def fetch_files
|
|
22
|
+
# Julia is currently in beta - only fetch files if beta ecosystems are enabled
|
|
23
|
+
return [] unless allow_beta_ecosystems?
|
|
24
|
+
|
|
25
|
+
fetched_files = []
|
|
26
|
+
|
|
27
|
+
# Fetch the main project file (Project.toml or JuliaProject.toml)
|
|
28
|
+
fetched_files << project_file
|
|
29
|
+
|
|
30
|
+
# Fetch the Manifest file if it exists
|
|
31
|
+
fetched_files << manifest_file if manifest_file
|
|
32
|
+
|
|
33
|
+
fetched_files
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
sig { returns(Dependabot::DependencyFile) }
|
|
39
|
+
def project_file
|
|
40
|
+
@project_file ||= T.let(
|
|
41
|
+
fetch_file_if_present("Project.toml") ||
|
|
42
|
+
fetch_file_if_present("JuliaProject.toml") ||
|
|
43
|
+
raise(
|
|
44
|
+
Dependabot::DependencyFileNotFound,
|
|
45
|
+
"No Project.toml or JuliaProject.toml found."
|
|
46
|
+
),
|
|
47
|
+
T.nilable(Dependabot::DependencyFile)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
52
|
+
def manifest_file
|
|
53
|
+
@manifest_file ||= T.let(
|
|
54
|
+
fetch_file_if_present("Manifest.toml") ||
|
|
55
|
+
fetch_file_if_present("JuliaManifest.toml"),
|
|
56
|
+
T.nilable(Dependabot::DependencyFile)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Dependabot::FileFetchers.register("julia", Dependabot::Julia::FileFetcher)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "toml-rb"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "dependabot/dependency"
|
|
8
|
+
require "dependabot/file_parsers"
|
|
9
|
+
require "dependabot/file_parsers/base"
|
|
10
|
+
require "dependabot/julia/version"
|
|
11
|
+
require "dependabot/julia/requirement"
|
|
12
|
+
require "dependabot/julia/registry_client"
|
|
13
|
+
|
|
14
|
+
module Dependabot
|
|
15
|
+
module Julia
|
|
16
|
+
class FileParser < Dependabot::FileParsers::Base
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
sig do
|
|
20
|
+
params(
|
|
21
|
+
dependency_files: T::Array[Dependabot::DependencyFile],
|
|
22
|
+
source: Dependabot::Source,
|
|
23
|
+
repo_contents_path: T.nilable(String),
|
|
24
|
+
credentials: T::Array[Dependabot::Credential],
|
|
25
|
+
reject_external_code: T::Boolean,
|
|
26
|
+
options: T::Hash[Symbol, T.untyped]
|
|
27
|
+
).void
|
|
28
|
+
end
|
|
29
|
+
def initialize(
|
|
30
|
+
dependency_files:,
|
|
31
|
+
source:,
|
|
32
|
+
repo_contents_path: nil,
|
|
33
|
+
credentials: [],
|
|
34
|
+
reject_external_code: false,
|
|
35
|
+
options: {}
|
|
36
|
+
)
|
|
37
|
+
super
|
|
38
|
+
@parsed_project_file = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
|
39
|
+
@parsed_manifest_file = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
|
40
|
+
@registry_client = T.let(nil, T.nilable(Dependabot::Julia::RegistryClient))
|
|
41
|
+
@custom_registries = T.let(nil, T.nilable(T::Array[T::Hash[Symbol, T.untyped]]))
|
|
42
|
+
@temp_dir = T.let(nil, T.nilable(String))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { override.returns(T::Array[Dependabot::Dependency]) }
|
|
46
|
+
def parse
|
|
47
|
+
dependency_set = T.let([], T::Array[Dependabot::Dependency])
|
|
48
|
+
|
|
49
|
+
dependency_set += project_file_dependencies
|
|
50
|
+
dependency_set.uniq
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Helper methods for DependabotHelper.jl integration
|
|
56
|
+
|
|
57
|
+
sig { returns(Dependabot::Julia::RegistryClient) }
|
|
58
|
+
def registry_client
|
|
59
|
+
@registry_client ||= Dependabot::Julia::RegistryClient.new(
|
|
60
|
+
credentials: credentials,
|
|
61
|
+
custom_registries: custom_registries
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
66
|
+
def custom_registries
|
|
67
|
+
@custom_registries ||= begin
|
|
68
|
+
registries = options.dig(:registries, :julia) || []
|
|
69
|
+
# Convert string keys to symbols if needed
|
|
70
|
+
registries.map do |registry|
|
|
71
|
+
registry.is_a?(Hash) ? registry.transform_keys(&:to_sym) : registry
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { returns(String) }
|
|
77
|
+
def write_temp_project_file
|
|
78
|
+
@temp_dir ||= Dir.mktmpdir("julia_project")
|
|
79
|
+
project_path = File.join(@temp_dir, "Project.toml")
|
|
80
|
+
File.write(project_path, T.must(project_file).content)
|
|
81
|
+
project_path
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { returns(T.nilable(String)) }
|
|
85
|
+
def write_temp_manifest_file
|
|
86
|
+
return nil unless manifest_file
|
|
87
|
+
|
|
88
|
+
@temp_dir ||= Dir.mktmpdir("julia_project")
|
|
89
|
+
manifest_path = File.join(@temp_dir, "Manifest.toml")
|
|
90
|
+
File.write(manifest_path, T.must(manifest_file).content)
|
|
91
|
+
manifest_path
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig { returns(T::Array[Dependabot::Dependency]) }
|
|
95
|
+
def project_file_dependencies
|
|
96
|
+
dependencies = T.let([], T::Array[Dependabot::Dependency])
|
|
97
|
+
return dependencies unless project_file
|
|
98
|
+
|
|
99
|
+
# Use DependabotHelper.jl for project parsing
|
|
100
|
+
project_path = write_temp_project_file
|
|
101
|
+
manifest_path = write_temp_manifest_file if manifest_file
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
result = registry_client.parse_project(
|
|
105
|
+
project_path: project_path,
|
|
106
|
+
manifest_path: manifest_path
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if result["error"]
|
|
110
|
+
# Fallback to Ruby TOML parsing if Julia helper fails
|
|
111
|
+
Dependabot.logger.warn(
|
|
112
|
+
"DependabotHelper.jl parsing failed: #{result['error']}, " \
|
|
113
|
+
"falling back to Ruby parsing"
|
|
114
|
+
)
|
|
115
|
+
return fallback_project_file_dependencies
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Convert DependabotHelper.jl result to Dependabot::Dependency objects
|
|
119
|
+
dependencies = build_dependencies_from_julia_result(result)
|
|
120
|
+
ensure
|
|
121
|
+
# Cleanup temporary directory
|
|
122
|
+
FileUtils.rm_rf(@temp_dir) if @temp_dir && File.exist?(@temp_dir)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
dependencies
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { params(result: T::Hash[String, T.untyped]).returns(T::Array[Dependabot::Dependency]) }
|
|
129
|
+
def build_dependencies_from_julia_result(result)
|
|
130
|
+
dependencies = T.let([], T::Array[Dependabot::Dependency])
|
|
131
|
+
parsed_deps = T.cast(result["dependencies"] || [], T::Array[T.untyped])
|
|
132
|
+
|
|
133
|
+
parsed_deps.each do |dep_info|
|
|
134
|
+
dep_hash = T.cast(dep_info, T::Hash[String, T.untyped])
|
|
135
|
+
name = T.cast(dep_hash["name"], String)
|
|
136
|
+
uuid = T.cast(dep_hash["uuid"], T.nilable(String))
|
|
137
|
+
requirement_string = T.cast(dep_hash["requirement"] || "*", String)
|
|
138
|
+
resolved_version = T.cast(dep_hash["resolved_version"], T.nilable(String))
|
|
139
|
+
|
|
140
|
+
# Skip Julia version requirement
|
|
141
|
+
next if name == "julia"
|
|
142
|
+
|
|
143
|
+
dependencies << Dependabot::Dependency.new(
|
|
144
|
+
name: name,
|
|
145
|
+
version: resolved_version,
|
|
146
|
+
requirements: [{
|
|
147
|
+
requirement: requirement_string,
|
|
148
|
+
file: T.must(project_file).name,
|
|
149
|
+
groups: ["runtime"],
|
|
150
|
+
source: nil
|
|
151
|
+
}],
|
|
152
|
+
package_manager: "julia",
|
|
153
|
+
metadata: uuid ? { julia_uuid: uuid } : {}
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
dependencies
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Fallback method using Ruby TOML parsing
|
|
161
|
+
sig { returns(T::Array[Dependabot::Dependency]) }
|
|
162
|
+
def fallback_project_file_dependencies
|
|
163
|
+
dependencies = T.let([], T::Array[Dependabot::Dependency])
|
|
164
|
+
|
|
165
|
+
parsed_project = parsed_project_file
|
|
166
|
+
deps_section = T.cast(parsed_project["deps"] || {}, T::Hash[String, T.untyped])
|
|
167
|
+
compat_section = T.cast(parsed_project["compat"] || {}, T::Hash[String, T.untyped])
|
|
168
|
+
|
|
169
|
+
deps_section.each do |name, _uuid|
|
|
170
|
+
next if name == "julia" # Skip Julia version requirement
|
|
171
|
+
|
|
172
|
+
# Get the version requirement from compat section, default to "*" if not specified
|
|
173
|
+
requirement_string = T.cast(compat_section[name] || "*", String)
|
|
174
|
+
|
|
175
|
+
# Get the exact version from Manifest.toml if available
|
|
176
|
+
exact_version = version_from_manifest(name)
|
|
177
|
+
|
|
178
|
+
dependencies << Dependabot::Dependency.new(
|
|
179
|
+
name: name,
|
|
180
|
+
version: exact_version,
|
|
181
|
+
requirements: [{
|
|
182
|
+
requirement: requirement_string,
|
|
183
|
+
file: T.must(project_file).name,
|
|
184
|
+
groups: ["runtime"],
|
|
185
|
+
source: nil
|
|
186
|
+
}],
|
|
187
|
+
package_manager: "julia"
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
dependencies
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
sig { params(dependency_name: String).returns(T.nilable(String)) }
|
|
195
|
+
def version_from_manifest(dependency_name)
|
|
196
|
+
return nil unless manifest_file
|
|
197
|
+
|
|
198
|
+
# Try using DependabotHelper.jl first
|
|
199
|
+
temp_dir = Dir.mktmpdir("julia_manifest_only")
|
|
200
|
+
manifest_path = File.join(temp_dir, "Manifest.toml")
|
|
201
|
+
File.write(manifest_path, T.must(manifest_file).content)
|
|
202
|
+
|
|
203
|
+
begin
|
|
204
|
+
# We need the UUID for the DependabotHelper.jl call, so fallback to Ruby parsing
|
|
205
|
+
# Note: Future enhancement could add name-only lookup to DependabotHelper.jl
|
|
206
|
+
fallback_version_from_manifest(dependency_name)
|
|
207
|
+
ensure
|
|
208
|
+
FileUtils.rm_rf(temp_dir)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
sig { params(dependency_name: String).returns(T.nilable(String)) }
|
|
213
|
+
def fallback_version_from_manifest(dependency_name)
|
|
214
|
+
return nil unless manifest_file
|
|
215
|
+
|
|
216
|
+
parsed_manifest = parsed_manifest_file
|
|
217
|
+
|
|
218
|
+
# Look for the dependency in the manifest
|
|
219
|
+
deps_section = T.cast(parsed_manifest["deps"], T.nilable(T::Hash[String, T.untyped]))
|
|
220
|
+
if deps_section && deps_section[dependency_name]
|
|
221
|
+
# Manifest v2 format
|
|
222
|
+
dep_entries = deps_section[dependency_name]
|
|
223
|
+
if dep_entries.is_a?(Array) && dep_entries.first.is_a?(Hash)
|
|
224
|
+
return T.cast(dep_entries.first["version"], T.nilable(String))
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
232
|
+
def project_file
|
|
233
|
+
@project_file ||= T.let(
|
|
234
|
+
get_original_file("Project.toml") || get_original_file("JuliaProject.toml"),
|
|
235
|
+
T.nilable(Dependabot::DependencyFile)
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
240
|
+
def manifest_file
|
|
241
|
+
@manifest_file ||= T.let(
|
|
242
|
+
get_original_file("Manifest.toml") || get_original_file("JuliaManifest.toml"),
|
|
243
|
+
T.nilable(Dependabot::DependencyFile)
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
248
|
+
def parsed_project_file
|
|
249
|
+
return @parsed_project_file if @parsed_project_file
|
|
250
|
+
|
|
251
|
+
parsed_content = T.cast(TomlRB.parse(T.must(project_file).content), T::Hash[String, T.untyped])
|
|
252
|
+
@parsed_project_file = parsed_content
|
|
253
|
+
parsed_content
|
|
254
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError => e
|
|
255
|
+
raise Dependabot::DependencyFileNotParseable, "Error parsing #{T.must(project_file).name}: #{e.message}"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
259
|
+
def parsed_manifest_file
|
|
260
|
+
return {} unless manifest_file
|
|
261
|
+
return @parsed_manifest_file if @parsed_manifest_file
|
|
262
|
+
|
|
263
|
+
parsed_content = T.cast(TomlRB.parse(T.must(manifest_file).content), T::Hash[String, T.untyped])
|
|
264
|
+
@parsed_manifest_file = parsed_content
|
|
265
|
+
parsed_content
|
|
266
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError => e
|
|
267
|
+
raise Dependabot::DependencyFileNotParseable, "Error parsing #{T.must(manifest_file).name}: #{e.message}"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
sig { override.void }
|
|
271
|
+
def check_required_files
|
|
272
|
+
raise "No Project.toml or JuliaProject.toml!" unless project_file
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
Dependabot::FileParsers.register("julia", Dependabot::Julia::FileParser)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "toml-rb"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require "dependabot/file_updaters"
|
|
7
|
+
require "dependabot/file_updaters/base"
|
|
8
|
+
require "dependabot/julia/registry_client"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Julia
|
|
12
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
sig { returns(T::Array[Regexp]) }
|
|
16
|
+
def self.updated_files_regex
|
|
17
|
+
[/(?:Julia)?Project\.toml$/i, /(?:Julia)?Manifest(?:-v[\d.]+)?\.toml$/i]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { override.returns(T::Array[Dependabot::DependencyFile]) }
|
|
21
|
+
def updated_dependency_files
|
|
22
|
+
updated_files = []
|
|
23
|
+
|
|
24
|
+
# Use DependabotHelper.jl for manifest updating
|
|
25
|
+
if project_file
|
|
26
|
+
project_path = T.let(nil, T.nilable(String))
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
project_path = write_temp_project_file
|
|
30
|
+
|
|
31
|
+
result = registry_client.update_manifest(
|
|
32
|
+
project_path: project_path,
|
|
33
|
+
updates: build_updates_hash
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if result["error"]
|
|
37
|
+
# Fallback to Ruby TOML manipulation
|
|
38
|
+
Dependabot.logger.warn(
|
|
39
|
+
"DependabotHelper.jl update failed: #{result['error']}, " \
|
|
40
|
+
"falling back to Ruby updating"
|
|
41
|
+
)
|
|
42
|
+
return fallback_updated_dependency_files
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Create updated files from DependabotHelper.jl results
|
|
46
|
+
updated_files = build_updated_files_from_result(result)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
# Fallback to Ruby TOML manipulation if Julia helper fails
|
|
49
|
+
Dependabot.logger.warn(
|
|
50
|
+
"DependabotHelper.jl update failed with exception: #{e.message}, " \
|
|
51
|
+
"falling back to Ruby updating"
|
|
52
|
+
)
|
|
53
|
+
return fallback_updated_dependency_files
|
|
54
|
+
ensure
|
|
55
|
+
File.delete(project_path) if project_path && File.exist?(project_path)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
raise "No files changed!" if updated_files.empty?
|
|
60
|
+
|
|
61
|
+
updated_files
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Fallback method using Ruby TOML manipulation
|
|
65
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
66
|
+
def fallback_updated_dependency_files
|
|
67
|
+
updated_files = []
|
|
68
|
+
|
|
69
|
+
# Update Project.toml file
|
|
70
|
+
if project_file && file_changed?(T.must(project_file))
|
|
71
|
+
updated_files << updated_file(
|
|
72
|
+
file: T.must(project_file),
|
|
73
|
+
content: updated_project_content
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Update Manifest.toml file if it exists and dependencies have changed
|
|
78
|
+
if manifest_file
|
|
79
|
+
updated_manifest_content = build_updated_manifest_content
|
|
80
|
+
if updated_manifest_content != T.must(manifest_file).content
|
|
81
|
+
updated_files << updated_file(
|
|
82
|
+
file: T.must(manifest_file),
|
|
83
|
+
content: updated_manifest_content
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
raise "No files changed!" if updated_files.empty?
|
|
89
|
+
|
|
90
|
+
updated_files
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
sig { returns(T::Hash[String, String]) }
|
|
96
|
+
def build_updates_hash
|
|
97
|
+
updates = {}
|
|
98
|
+
dependencies.each do |dependency|
|
|
99
|
+
next unless dependency.version
|
|
100
|
+
|
|
101
|
+
updates[dependency.name] = dependency.version
|
|
102
|
+
end
|
|
103
|
+
updates
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { params(result: T::Hash[String, T.untyped]).returns(T::Array[Dependabot::DependencyFile]) }
|
|
107
|
+
def build_updated_files_from_result(result)
|
|
108
|
+
updated_files = T.let([], T::Array[Dependabot::DependencyFile])
|
|
109
|
+
|
|
110
|
+
if result["project_content"] && result["project_content"] != T.must(project_file).content
|
|
111
|
+
updated_files << updated_file(
|
|
112
|
+
file: T.must(project_file),
|
|
113
|
+
content: result["project_content"]
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if manifest_file && result["manifest_content"] &&
|
|
118
|
+
result["manifest_content"] != T.must(manifest_file).content
|
|
119
|
+
updated_files << updated_file(
|
|
120
|
+
file: T.must(manifest_file),
|
|
121
|
+
content: result["manifest_content"]
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
updated_files
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Helper methods for DependabotHelper.jl integration
|
|
129
|
+
|
|
130
|
+
sig { returns(Dependabot::Julia::RegistryClient) }
|
|
131
|
+
def registry_client
|
|
132
|
+
@registry_client ||= T.let(
|
|
133
|
+
Dependabot::Julia::RegistryClient.new(
|
|
134
|
+
credentials: credentials
|
|
135
|
+
),
|
|
136
|
+
T.nilable(Dependabot::Julia::RegistryClient)
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
sig { returns(String) }
|
|
141
|
+
def write_temp_project_file
|
|
142
|
+
temp_file = Tempfile.new(["Project", ".toml"])
|
|
143
|
+
temp_file.write(T.must(project_file).content)
|
|
144
|
+
temp_file.close
|
|
145
|
+
T.must(temp_file.path)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
sig { override.void }
|
|
149
|
+
def check_required_files
|
|
150
|
+
return if dependency_files.empty?
|
|
151
|
+
|
|
152
|
+
return if dependency_files.any? { |f| f.name.match?(/^(Julia)?Project\.toml$/i) }
|
|
153
|
+
|
|
154
|
+
raise Dependabot::DependencyFileNotFound, "No Project.toml or JuliaProject.toml found."
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
158
|
+
def project_file
|
|
159
|
+
@project_file ||= T.let(
|
|
160
|
+
dependency_files.find do |f|
|
|
161
|
+
f.name.match?(/^(Julia)?Project\.toml$/i)
|
|
162
|
+
end,
|
|
163
|
+
T.nilable(Dependabot::DependencyFile)
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
168
|
+
def manifest_file
|
|
169
|
+
@manifest_file ||= T.let(
|
|
170
|
+
dependency_files.find do |f|
|
|
171
|
+
f.name.match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
|
|
172
|
+
end,
|
|
173
|
+
T.nilable(Dependabot::DependencyFile)
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
sig { returns(String) }
|
|
178
|
+
def updated_project_content
|
|
179
|
+
return T.must(T.must(project_file).content) unless project_file
|
|
180
|
+
|
|
181
|
+
content = T.must(T.must(project_file).content)
|
|
182
|
+
|
|
183
|
+
dependencies.each do |dependency|
|
|
184
|
+
# Find the new requirement for this dependency
|
|
185
|
+
new_requirement = dependency.requirements
|
|
186
|
+
.find { |req| T.cast(req[:file], String) == T.must(project_file).name }
|
|
187
|
+
&.fetch(:requirement)
|
|
188
|
+
|
|
189
|
+
next unless new_requirement
|
|
190
|
+
|
|
191
|
+
content = update_dependency_requirement_in_content(content, dependency.name, new_requirement)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
content
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
sig { params(content: String, dependency_name: String, new_requirement: String).returns(String) }
|
|
198
|
+
def update_dependency_requirement_in_content(content, dependency_name, new_requirement)
|
|
199
|
+
# Pattern to match the dependency in [compat] section
|
|
200
|
+
# Handles various quote styles and spacing
|
|
201
|
+
pattern = /(^\s*#{Regexp.escape(dependency_name)}\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s#\n]+)(\s*(?:\#.*)?$)/mx
|
|
202
|
+
|
|
203
|
+
if content.match?(pattern)
|
|
204
|
+
# Replace existing entry
|
|
205
|
+
content.gsub(pattern, "\\1\"#{new_requirement}\"\\2")
|
|
206
|
+
else
|
|
207
|
+
# Add new entry to [compat] section
|
|
208
|
+
add_compat_entry_to_content(content, dependency_name, new_requirement)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
sig { params(content: String, dependency_name: String, requirement: String).returns(String) }
|
|
213
|
+
def add_compat_entry_to_content(content, dependency_name, requirement)
|
|
214
|
+
# Find [compat] section or create it
|
|
215
|
+
if content.match?(/^\s*\[compat\]\s*$/m)
|
|
216
|
+
# Add to existing [compat] section
|
|
217
|
+
content.gsub(/(\[compat\]\s*\n)/, "\\1#{dependency_name} = \"#{requirement}\"\n")
|
|
218
|
+
else
|
|
219
|
+
# Add new [compat] section at the end
|
|
220
|
+
content + "\n[compat]\n#{dependency_name} = \"#{requirement}\"\n"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
sig { returns(String) }
|
|
225
|
+
def build_updated_manifest_content
|
|
226
|
+
return T.must(T.must(manifest_file).content) unless manifest_file
|
|
227
|
+
|
|
228
|
+
content = T.must(T.must(manifest_file).content)
|
|
229
|
+
|
|
230
|
+
dependencies.each do |dependency|
|
|
231
|
+
next unless dependency.version
|
|
232
|
+
|
|
233
|
+
content = update_dependency_version_in_manifest(content, dependency.name, T.must(dependency.version))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
content
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
sig { params(content: String, dependency_name: String, new_version: String).returns(String) }
|
|
240
|
+
def update_dependency_version_in_manifest(content, dependency_name, new_version)
|
|
241
|
+
# Pattern to find the dependency entry and update its version
|
|
242
|
+
# Matches the [[deps.DependencyName]] section and updates the version line within it
|
|
243
|
+
dep_start = /^\[\[deps\.#{Regexp.escape(dependency_name)}\]\]\s*\n(?:.*\n)*?/
|
|
244
|
+
version_key = /^\s*version\s*=\s*/
|
|
245
|
+
old_version = /(?:"[^"]*"|'[^']*'|[^\s#\n]+)/
|
|
246
|
+
trailing = /\s*(?:\#.*)?$/
|
|
247
|
+
pattern = /(#{dep_start})(#{version_key})#{old_version}(#{trailing})/mx
|
|
248
|
+
|
|
249
|
+
if content.match?(pattern)
|
|
250
|
+
content.gsub(pattern, "\\1\\2\"#{new_version}\"\\3")
|
|
251
|
+
else
|
|
252
|
+
# If pattern doesn't match, fall back to original approach
|
|
253
|
+
Dependabot.logger.warn("Could not find manifest entry for #{dependency_name}, using fallback")
|
|
254
|
+
fallback_update_manifest_content(content, dependency_name, new_version)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
sig { params(content: String, dependency_name: String, new_version: String).returns(String) }
|
|
259
|
+
def fallback_update_manifest_content(content, dependency_name, new_version)
|
|
260
|
+
# Fallback to parse-and-dump for complex cases
|
|
261
|
+
parsed_manifest = T.cast(TomlRB.parse(content), T::Hash[String, T.untyped])
|
|
262
|
+
|
|
263
|
+
deps_section = T.cast(parsed_manifest["deps"] || {}, T::Hash[String, T.untyped])
|
|
264
|
+
if deps_section[dependency_name]
|
|
265
|
+
dep_entries = deps_section[dependency_name]
|
|
266
|
+
update_dependency_entries(dep_entries, new_version)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
T.cast(TomlRB.dump(parsed_manifest), String)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
sig { params(dependency: Dependabot::Dependency, manifest: T::Hash[String, T.untyped]).void }
|
|
273
|
+
def update_dependency_in_manifest(dependency, manifest)
|
|
274
|
+
deps_section = T.cast(manifest["deps"] || {}, T::Hash[String, T.untyped])
|
|
275
|
+
return unless deps_section[dependency.name]
|
|
276
|
+
|
|
277
|
+
dep_entries = deps_section[dependency.name]
|
|
278
|
+
update_dependency_entries(dep_entries, dependency.version)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
sig { params(dep_entries: T.untyped, version: T.nilable(String)).void }
|
|
282
|
+
def update_dependency_entries(dep_entries, version)
|
|
283
|
+
if dep_entries.is_a?(Array)
|
|
284
|
+
dep_entries.each do |dep_entry|
|
|
285
|
+
dep_entry["version"] = version if dep_entry.is_a?(Hash) && dep_entry["uuid"]
|
|
286
|
+
end
|
|
287
|
+
elsif dep_entries.is_a?(Hash) && dep_entries["uuid"]
|
|
288
|
+
dep_entries["version"] = version
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
Dependabot::FileUpdaters.register("julia", Dependabot::Julia::FileUpdater)
|